about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--zet.tlv820
1 files changed, 820 insertions, 0 deletions
diff --git a/zet.tlv b/zet.tlv
index 5d21b41..18582f5 100644
--- a/zet.tlv
+++ b/zet.tlv
@@ -1467,3 +1467,823 @@
     >    prev='id2',
     >  },
     >}
+- __teliva_timestamp:
+    >Thu Feb 10 20:24:13 2022
+  menu:
+    >-- To show app-specific hotkeys in the menu bar, add hotkey/command
+    >-- arrays of strings to the menu array.
+    >menu = {
+    >  {'a/b/c', 'insert'},
+    >  {'e', 'edit'},
+    >  {'j/k/l/h', 'move'},
+    >  {'x/X/y/Y', 'resize'},
+    >  {'s', 'stash'},
+    >  {'t', 'link with stash'},
+    >}
+- __teliva_timestamp:
+    >Thu Feb 10 20:25:14 2022
+  stash:
+    >stash = nil
+- __teliva_timestamp:
+    >Thu Feb 10 20:32:38 2022
+  update:
+    >function update(window)
+    >  local key = curses.getch()
+    >  local h, w = window:getmaxyx()
+    >  local curr = zettels[current_zettel_id]
+    >  assert(curr, string.format('cursor fell off the edge of the world: %s', type(current_zettel_id)))
+    >  -- move along the graph
+    >  if key == string.byte('j') then
+    >    if curr.child then
+    >      current_zettel_id = curr.child
+    >    elseif curr.next then
+    >      current_zettel_id = curr.next
+    >    elseif curr.parent and zettels[curr.parent].next then
+    >      current_zettel_id = zettels[curr.parent].next
+    >    end
+    >  elseif key == string.byte('k') then
+    >    if curr.parent then current_zettel_id = curr.parent end
+    >  elseif key == string.byte('h') then
+    >    if curr.prev then
+    >      current_zettel_id = curr.prev
+    >    elseif curr.parent then
+    >      current_zettel_id = curr.parent
+    >    end
+    >  elseif key == string.byte('l') then
+    >    if curr.next then
+    >      current_zettel_id = curr.next
+    >    elseif curr.parent and zettels[curr.parent].next then
+    >      current_zettel_id = zettels[curr.parent].next
+    >    end
+    >  -- move along the screen
+    >  elseif key == curses.KEY_UP then
+    >    if render_state.curr_h > 1 then
+    >      current_zettel_id = render_state.wh2id[render_state.curr_w][render_state.curr_h - 1]
+    >    end
+    >  elseif key == curses.KEY_DOWN then
+    >    if render_state.wh2id[render_state.curr_w][render_state.curr_h + 1] then
+    >      current_zettel_id = render_state.wh2id[render_state.curr_w][render_state.curr_h + 1]
+    >    end
+    >  elseif key == curses.KEY_LEFT then
+    >    if render_state.curr_w > 1 then
+    >      current_zettel_id = render_state.wh2id[render_state.curr_w - 1][render_state.curr_h]
+    >    end
+    >  elseif key == curses.KEY_RIGHT then
+    >    if render_state.wh2id[render_state.curr_w + 1] and render_state.wh2id[render_state.curr_w + 1][render_state.curr_h] then
+    >      current_zettel_id = render_state.wh2id[render_state.curr_w + 1][render_state.curr_h]
+    >    end
+    >  -- mutations
+    >  elseif key == string.byte('e') then
+    >    editz(window)
+    >  elseif key == string.byte('a') then
+    >    -- insert sibling after
+    >    local old = curr.next
+    >    curr.next = new_id()
+    >    local new = zettels[curr.next]
+    >    new.data = ''
+    >    new.next = old
+    >    new.prev = current_zettel_id
+    >    if old then
+    >      zettels[old].prev = curr.next
+    >      assert(curr.parent == zettels[old].parent, 'siblings should have same parent')
+    >    end
+    >    new.parent = curr.parent
+    >    current_zettel_id = curr.next
+    >    render(window) -- recompute render_state
+    >    editz(window)
+    >  elseif key == string.byte('b') then
+    >    -- insert sibling before
+    >    local old = curr.prev
+    >    curr.prev = new_id()
+    >    local new = zettels[curr.prev]
+    >    new.data = ''
+    >    new.prev = old
+    >    new.next = current_zettel_id
+    >    if old then
+    >      zettels[old].next = curr.prev
+    >      assert(curr.parent == zettels[old].parent, 'siblings should have same parent')
+    >    end
+    >    new.parent = curr.parent
+    >    current_zettel_id = curr.prev
+    >    render(window) -- recompute render_state
+    >    editz(window)
+    >  elseif key == string.byte('c') then
+    >    -- insert child
+    >    local old = curr.child
+    >    curr.child = new_id()
+    >    local new = zettels[curr.child]
+    >    new.data = ''
+    >    new.next = old
+    >    if old then
+    >      assert(zettels[old].prev == nil, "first child shouldn't have a previous sibling")
+    >      zettels[old].prev = curr.child
+    >    end
+    >    new.parent = curr
+    >    current_zettel_id = curr.child
+    >    render(window) -- recompute render_state
+    >    editz(window)
+    >  -- cross-links
+    >  elseif key == string.byte('s') then
+    >    -- save zettel to a stash
+    >    stash = current_zettel_id
+    >  elseif key == string.byte('t') then
+    >    -- cross-link a zettel bidirectionally with what's on the stash
+    >    if curr.crosslinks then
+    >      curr.crosslinks.a = stash
+    >    else
+    >      curr.crosslinks = {a=stash}
+    >    end
+    >  -- view settings
+    >  elseif key == string.byte('x') then
+    >    if view_settings.width > 5 then
+    >      view_settings.width = view_settings.width - 5
+    >    end
+    >  elseif key == string.byte('X') then
+    >    if view_settings.width < w-5 then
+    >      view_settings.width = view_settings.width + 5
+    >    end
+    >  elseif key == string.byte('y') then
+    >    if view_settings.height > 0 then
+    >      view_settings.height = view_settings.height - 1
+    >    end
+    >  elseif key == string.byte('Y') then
+    >    if view_settings.height < h-2 then
+    >      view_settings.height = view_settings.height + 1
+    >    end
+    >  end
+    >end
+- __teliva_timestamp:
+    >Thu Feb 10 20:39:15 2022
+  render:
+    >function render(window)
+    >  window:clear()
+    >  local lines, cols = window:getmaxyx()
+    >  local bg=1
+    >  local y, x = 0, 0 -- units of characters (0-based)
+    >  local w, h = 1, 1 -- units of zettels (1-based)
+    >  -- render zettels depth-first, while tracking relative positions
+    >  local done = {}
+    >  local inprogress = {zettels.root}
+    >  render_state.wh2id = {{}}
+    >  while #inprogress > 0 do
+    >    local currid = table.remove(inprogress)
+    >    if not done[currid] then
+    >      done[currid] = true
+    >      table.insert(render_state.wh2id[w], currid)
+    >      local zettel = zettels[currid]
+    >      if currid == current_zettel_id then
+    >        render_state.curr_w = w
+    >        render_state.curr_h = h
+    >      end
+    >      local currbg = (currid == current_zettel_id) and view_settings.current_zettel_bg or bg
+    >      render_zettel(window, currbg, depth(zettel) * view_settings.indent, y, x, zettel)
+    >      if zettel.next then table.insert(inprogress, zettel.next) end
+    >      if zettel.child then table.insert(inprogress, zettel.child) end
+    >      if zettel.crosslinks then
+    >        for relation, target in pairs(zettel.crosslinks) do
+    >          table.insert(inprogress, target)
+    >        end
+    >      end
+    >      bg = 3 - bg  -- toggle between color pairs 1 and 2
+    >      y = y + view_settings.height + view_settings.vmargin
+    >      h = h + 1
+    >      if y + view_settings.height > lines then
+    >        y = 0
+    >        h = 1
+    >        x = x + view_settings.width + view_settings.hmargin
+    >        w = w + 1
+    >        if x + view_settings.width > cols then break end
+    >        table.insert(render_state.wh2id, {})
+    >      end
+    >    end
+    >  end
+    >  window:mvaddstr(lines-1, 0, '')
+    >  bg = 1
+    >  x = 0
+    >  for i=1,3 do
+    >    local zettel = nil
+    >    if i == 1 and stash then
+    >      zettel = zettels[stash]
+    >    end
+    >    render_zettel(window, bg, 0, lines-1, x, zettel)
+    >    bg = 3 - bg
+    >    x = x + view_settings.width + view_settings.hmargin
+    >  end
+    >  curses.refresh()
+    >end
+- __teliva_timestamp:
+    >Thu Feb 10 20:40:08 2022
+  __teliva_note:
+    >initial support for cross-links
+    >
+    >Kinda confusing because zettels still show indent based on their
+    >hierarchical location rather than the path they're rendered in.
+  render_zettel:
+    >function render_zettel(window, bg, indent, starty, startx, zettel)
+    >  window:attrset(curses.color_pair(bg))
+    >  for y=0,view_settings.height-1 do
+    >    for x=0,view_settings.width-1 do
+    >      window:mvaddch(y+starty, x+startx, ' ')
+    >    end
+    >  end
+    >  local y, x = 0, indent+1
+    >  local data = ''
+    >  if zettel then
+    >    data = zettel.data
+    >  end
+    >  for i=1,#data do
+    >    local c = data[i]
+    >    if c == '\n' then
+    >      y = y+1
+    >      x = indent+1
+    >    else
+    >      window:mvaddstr(y+starty, x+startx, c)
+    >      x = x+1
+    >      if x >= startx + view_settings.width then
+    >        y = y+1
+    >        x = indent+1
+    >      end
+    >    end
+    >    if y >= view_settings.height then
+    >      break
+    >    end
+    >  end
+    >end
+- __teliva_timestamp:
+    >Thu Feb 10 20:44:29 2022
+  __teliva_note:
+    >looks better after dynamically recomputing depth while rendering
+  render:
+    >function render(window)
+    >  window:clear()
+    >  local lines, cols = window:getmaxyx()
+    >  local bg=1
+    >  local y, x = 0, 0 -- units of characters (0-based)
+    >  local w, h = 1, 1 -- units of zettels (1-based)
+    >  -- render zettels depth-first, while tracking relative positions
+    >  local done = {}
+    >  local inprogress = {{id=zettels.root,depth=0}}
+    >  render_state.wh2id = {{}}
+    >  while #inprogress > 0 do
+    >    local curr = table.remove(inprogress)
+    >    if not done[curr.id] then
+    >      done[curr.id] = true
+    >      table.insert(render_state.wh2id[w], curr.id)
+    >      local zettel = zettels[curr.id]
+    >      if curr.id == current_zettel_id then
+    >        render_state.curr_w = w
+    >        render_state.curr_h = h
+    >      end
+    >      local currbg = (curr.id == current_zettel_id) and view_settings.current_zettel_bg or bg
+    >      render_zettel(window, currbg, curr.depth * view_settings.indent, y, x, zettel)
+    >      if zettel.next then table.insert(inprogress, {id=zettel.next, depth=curr.depth}) end
+    >      if zettel.child then table.insert(inprogress, {id=zettel.child, depth=curr.depth+1}) end
+    >      if zettel.crosslinks then
+    >        for relation, target in pairs(zettel.crosslinks) do
+    >          table.insert(inprogress, {id=target, depth=curr.depth+1})
+    >        end
+    >      end
+    >      bg = 3 - bg  -- toggle between color pairs 1 and 2
+    >      y = y + view_settings.height + view_settings.vmargin
+    >      h = h + 1
+    >      if y + view_settings.height > lines then
+    >        y = 0
+    >        h = 1
+    >        x = x + view_settings.width + view_settings.hmargin
+    >        w = w + 1
+    >        if x + view_settings.width > cols then break end
+    >        table.insert(render_state.wh2id, {})
+    >      end
+    >    end
+    >  end
+    >  window:mvaddstr(lines-1, 0, '')
+    >  bg = 1
+    >  x = 0
+    >  for i=1,3 do
+    >    local zettel = nil
+    >    if i == 1 and stash then
+    >      zettel = zettels[stash]
+    >    end
+    >    render_zettel(window, bg, 0, lines-1, x, zettel)
+    >    bg = 3 - bg
+    >    x = x + view_settings.width + view_settings.hmargin
+    >  end
+    >  curses.refresh()
+    >end
+- __teliva_timestamp:
+    >Thu Feb 10 20:55:19 2022
+  render_zettel:
+    >function render_zettel(window, bg, indent, edge_label, starty, startx, zettel)
+    >  window:attrset(curses.color_pair(bg))
+    >  for y=0,view_settings.height-1 do
+    >    for x=0,view_settings.width-1 do
+    >      window:mvaddch(y+starty, x+startx, ' ')
+    >    end
+    >  end
+    >  if indent > 1 then
+    >    window:attrset(curses.color_pair(bg+1))  -- go from zettel color to its edge color
+    >    window:mvaddstr(starty, startx+indent-1, edge_label)
+    >    window:attrset(curses.color_pair(bg))
+    >  end
+    >  local y, x = 0, indent+1
+    >  local data = ''
+    >  if zettel then
+    >    data = zettel.data
+    >  end
+    >  for i=1,#data do
+    >    local c = data[i]
+    >    if c == '\n' then
+    >      y = y+1
+    >      x = indent+1
+    >    else
+    >      window:mvaddstr(y+starty, x+startx, c)
+    >      x = x+1
+    >      if x >= startx + view_settings.width then
+    >        y = y+1
+    >        x = indent+1
+    >      end
+    >    end
+    >    if y >= view_settings.height then
+    >      break
+    >    end
+    >  end
+    >end
+- __teliva_timestamp:
+    >Thu Feb 10 20:58:49 2022
+  view_settings:
+    >view_settings = {
+    >  -- dimensions for rendering a single zettel; extra text gets truncated
+    >  width=50,
+    >  height=3,
+    >  -- spacing between zettels
+    >  hmargin=1,
+    >  vmargin=1,
+    >  --
+    >  indent=2,  -- how children of a zettel are indicated
+    >}
+- __teliva_timestamp:
+    >Thu Feb 10 20:59:18 2022
+  render:
+    >function render(window)
+    >  window:clear()
+    >  local lines, cols = window:getmaxyx()
+    >  local bg=3
+    >  local y, x = 0, 0 -- units of characters (0-based)
+    >  local w, h = 1, 1 -- units of zettels (1-based)
+    >  -- render zettels depth-first, while tracking relative positions
+    >  local done = {}
+    >  local inprogress = {{id=zettels.root,depth=0,edge=''}}
+    >  render_state.wh2id = {{}}
+    >  while #inprogress > 0 do
+    >    local curr = table.remove(inprogress)
+    >    if not done[curr.id] then
+    >      done[curr.id] = true
+    >      table.insert(render_state.wh2id[w], curr.id)
+    >      local zettel = zettels[curr.id]
+    >      if curr.id == current_zettel_id then
+    >        render_state.curr_w = w
+    >        render_state.curr_h = h
+    >      end
+    >      local currbg = (curr.id == current_zettel_id) and 1 or bg  -- 1 is the color pair for the current zettel
+    >      render_zettel(window, currbg, curr.depth * view_settings.indent, curr.edge, y, x, zettel)
+    >      if zettel.next then table.insert(inprogress, {id=zettel.next, depth=curr.depth, edge='|'}) end
+    >      if zettel.child then table.insert(inprogress, {id=zettel.child, depth=curr.depth+1, edge='\\'}) end
+    >      if zettel.crosslinks then
+    >        for relation, target in pairs(zettel.crosslinks) do
+    >          table.insert(inprogress, {id=target, depth=curr.depth+1, edge=relation})
+    >        end
+    >      end
+    >      bg = 8 - bg  -- toggle between color pairs 3 and 5
+    >      y = y + view_settings.height + view_settings.vmargin
+    >      h = h + 1
+    >      if y + view_settings.height > lines then
+    >        y = 0
+    >        h = 1
+    >        x = x + view_settings.width + view_settings.hmargin
+    >        w = w + 1
+    >        if x + view_settings.width > cols then break end
+    >        table.insert(render_state.wh2id, {})
+    >      end
+    >    end
+    >  end
+    >  window:mvaddstr(lines-1, 0, '')
+    >  bg = 3
+    >  x = 0
+    >  for i=1,3 do
+    >    local zettel = nil
+    >    if i == 1 and stash then
+    >      zettel = zettels[stash]
+    >    end
+    >    render_zettel(window, bg, 0, '', lines-1, x, zettel)
+    >    bg = 8 - bg  -- toggle between color pairs 3 and 5
+    >    x = x + view_settings.width + view_settings.hmargin
+    >  end
+    >  curses.refresh()
+    >end
+- __teliva_timestamp:
+    >Thu Feb 10 21:02:41 2022
+  __teliva_note:
+    >label the incoming edge for each zettel
+    >
+    >Is it a child, sibling or other cross-link?
+  init_colors:
+    >function init_colors()
+    >  -- light background
+    >    -- current zettel
+    >  curses.init_pair(1, 236, 230)
+    >  curses.init_pair(2, 1,   230)  -- edge label for current zettel
+    >    -- non-current zettel #1
+    >  curses.init_pair(3, 236, 250)
+    >  curses.init_pair(4, 1,   250)  -- edge label for pair 3
+    >    -- non-current zettel #2
+    >  curses.init_pair(5, 236, 252)
+    >  curses.init_pair(6, 1,   252)  -- edge label for pair 5
+    >  -- dark background
+    >--?     -- current zettel
+    >--?   curses.init_pair(7, 252, 130)
+    >--?     -- other zettels
+    >--?   curses.init_pair(1, 252, 240)
+    >--?   curses.init_pair(2, 252, 242)
+    >--?     -- edge labels
+    >--?   curses.init_pair(3, 1, 240)  -- same bg as pair 1
+    >--?   curses.init_pair(4, 1, 242)  -- same bg as pair 2
+    >--?   curses.init_pair(9, 1, 130)  -- same bg as pair 7 for current zettel
+    >end
+- __teliva_timestamp:
+    >Thu Feb 10 21:11:35 2022
+  menu:
+    >-- To show app-specific hotkeys in the menu bar, add hotkey/command
+    >-- arrays of strings to the menu array.
+    >menu = {
+    >  {'a/b/c', 'insert'},
+    >  {'e', 'edit'},
+    >  {'j/k/l/h', 'move'},
+    >  {'x/X/y/Y', 'resize'},
+    >  {'s', 'stash'},
+    >  {'t', 'link with stash'},
+    >  {'z', 'scroll'},
+    >}
+- __teliva_timestamp:
+    >Thu Feb 10 21:13:19 2022
+  main:
+    >function main()
+    >  init_colors()
+    >  curses.curs_set(0)  -- hide cursor except when editing
+    >
+    >  local infile = io.open('zet', 'r')
+    >  if infile then
+    >    read_zettels(infile)
+    >  end
+    >  current_zettel_id = zettels.root  -- cursor
+    >  view_settings.first_zettel = zettels.root  -- start rendering here
+    >
+    >  while true do
+    >    render(window)
+    >    update(window)
+    >
+    >    -- save zettels, but hold on to previous state on disk
+    >    -- until last possible second
+    >    local filename = os.tmpname()
+    >    local outfile = io.open(filename, 'w')
+    >    if outfile then
+    >      write_zettels(outfile)
+    >      os.rename(filename, 'zet')
+    >    end
+    >  end
+    >end
+- __teliva_timestamp:
+    >Thu Feb 10 21:13:36 2022
+  render:
+    >function render(window)
+    >  window:clear()
+    >  local lines, cols = window:getmaxyx()
+    >  local bg=3
+    >  local y, x = 0, 0 -- units of characters (0-based)
+    >  local w, h = 1, 1 -- units of zettels (1-based)
+    >  -- render zettels depth-first, while tracking relative positions
+    >  local done = {}
+    >  local inprogress = {{id=view_settings.first_zettel,depth=0,edge=''}}
+    >  render_state.wh2id = {{}}
+    >  while #inprogress > 0 do
+    >    local curr = table.remove(inprogress)
+    >    if not done[curr.id] then
+    >      done[curr.id] = true
+    >      table.insert(render_state.wh2id[w], curr.id)
+    >      local zettel = zettels[curr.id]
+    >      if curr.id == current_zettel_id then
+    >        render_state.curr_w = w
+    >        render_state.curr_h = h
+    >      end
+    >      local currbg = (curr.id == current_zettel_id) and 1 or bg  -- 1 is the color pair for the current zettel
+    >      render_zettel(window, currbg, curr.depth * view_settings.indent, curr.edge, y, x, zettel)
+    >      if zettel.next then table.insert(inprogress, {id=zettel.next, depth=curr.depth, edge='|'}) end
+    >      if zettel.child then table.insert(inprogress, {id=zettel.child, depth=curr.depth+1, edge='\\'}) end
+    >      if zettel.crosslinks then
+    >        for relation, target in pairs(zettel.crosslinks) do
+    >          table.insert(inprogress, {id=target, depth=curr.depth+1, edge=relation})
+    >        end
+    >      end
+    >      bg = 8 - bg  -- toggle between color pairs 3 and 5
+    >      y = y + view_settings.height + view_settings.vmargin
+    >      h = h + 1
+    >      if y + view_settings.height > lines then
+    >        y = 0
+    >        h = 1
+    >        x = x + view_settings.width + view_settings.hmargin
+    >        w = w + 1
+    >        if x + view_settings.width > cols then break end
+    >        table.insert(render_state.wh2id, {})
+    >      end
+    >    end
+    >  end
+    >  window:mvaddstr(lines-1, 0, '')
+    >  bg = 3
+    >  x = 0
+    >  for i=1,3 do
+    >    local zettel = nil
+    >    if i == 1 and stash then
+    >      zettel = zettels[stash]
+    >    end
+    >    render_zettel(window, bg, 0, '', lines-1, x, zettel)
+    >    bg = 8 - bg  -- toggle between color pairs 3 and 5
+    >    x = x + view_settings.width + view_settings.hmargin
+    >  end
+    >  curses.refresh()
+    >end
+- __teliva_timestamp:
+    >Thu Feb 10 21:19:26 2022
+  __teliva_note:
+    >bugfix: cross-links should be bidirectional
+  update:
+    >function update(window)
+    >  local key = curses.getch()
+    >  local h, w = window:getmaxyx()
+    >  local curr = zettels[current_zettel_id]
+    >  assert(curr, string.format('cursor fell off the edge of the world: %s', type(current_zettel_id)))
+    >  -- move along the graph
+    >  if key == string.byte('j') then
+    >    if curr.child then
+    >      current_zettel_id = curr.child
+    >    elseif curr.next then
+    >      current_zettel_id = curr.next
+    >    elseif curr.parent and zettels[curr.parent].next then
+    >      current_zettel_id = zettels[curr.parent].next
+    >    end
+    >  elseif key == string.byte('k') then
+    >    if curr.parent then current_zettel_id = curr.parent end
+    >  elseif key == string.byte('h') then
+    >    if curr.prev then
+    >      current_zettel_id = curr.prev
+    >    elseif curr.parent then
+    >      current_zettel_id = curr.parent
+    >    end
+    >  elseif key == string.byte('l') then
+    >    if curr.next then
+    >      current_zettel_id = curr.next
+    >    elseif curr.parent and zettels[curr.parent].next then
+    >      current_zettel_id = zettels[curr.parent].next
+    >    end
+    >  -- move along the screen
+    >  elseif key == curses.KEY_UP then
+    >    if render_state.curr_h > 1 then
+    >      current_zettel_id = render_state.wh2id[render_state.curr_w][render_state.curr_h - 1]
+    >    end
+    >  elseif key == curses.KEY_DOWN then
+    >    if render_state.wh2id[render_state.curr_w][render_state.curr_h + 1] then
+    >      current_zettel_id = render_state.wh2id[render_state.curr_w][render_state.curr_h + 1]
+    >    end
+    >  elseif key == curses.KEY_LEFT then
+    >    if render_state.curr_w > 1 then
+    >      current_zettel_id = render_state.wh2id[render_state.curr_w - 1][render_state.curr_h]
+    >    end
+    >  elseif key == curses.KEY_RIGHT then
+    >    if render_state.wh2id[render_state.curr_w + 1] and render_state.wh2id[render_state.curr_w + 1][render_state.curr_h] then
+    >      current_zettel_id = render_state.wh2id[render_state.curr_w + 1][render_state.curr_h]
+    >    end
+    >  -- mutations
+    >  elseif key == string.byte('e') then
+    >    editz(window)
+    >  elseif key == string.byte('a') then
+    >    -- insert sibling after
+    >    local old = curr.next
+    >    curr.next = new_id()
+    >    local new = zettels[curr.next]
+    >    new.data = ''
+    >    new.next = old
+    >    new.prev = current_zettel_id
+    >    if old then
+    >      zettels[old].prev = curr.next
+    >      assert(curr.parent == zettels[old].parent, 'siblings should have same parent')
+    >    end
+    >    new.parent = curr.parent
+    >    current_zettel_id = curr.next
+    >    render(window) -- recompute render_state
+    >    editz(window)
+    >  elseif key == string.byte('b') then
+    >    -- insert sibling before
+    >    local old = curr.prev
+    >    curr.prev = new_id()
+    >    local new = zettels[curr.prev]
+    >    new.data = ''
+    >    new.prev = old
+    >    new.next = current_zettel_id
+    >    if old then
+    >      zettels[old].next = curr.prev
+    >      assert(curr.parent == zettels[old].parent, 'siblings should have same parent')
+    >    end
+    >    new.parent = curr.parent
+    >    current_zettel_id = curr.prev
+    >    render(window) -- recompute render_state
+    >    editz(window)
+    >  elseif key == string.byte('c') then
+    >    -- insert child
+    >    local old = curr.child
+    >    curr.child = new_id()
+    >    local new = zettels[curr.child]
+    >    new.data = ''
+    >    new.next = old
+    >    if old then
+    >      assert(zettels[old].prev == nil, "first child shouldn't have a previous sibling")
+    >      zettels[old].prev = curr.child
+    >    end
+    >    new.parent = curr
+    >    current_zettel_id = curr.child
+    >    render(window) -- recompute render_state
+    >    editz(window)
+    >  -- cross-links
+    >  elseif key == string.byte('s') then
+    >    -- save zettel to a stash
+    >    stash = current_zettel_id
+    >  elseif key == string.byte('t') then
+    >    -- cross-link a zettel bidirectionally with what's on the stash
+    >    local insert_crosslink =
+    >      function(a, rel, b_id)
+    >        if a.crosslinks == nil then
+    >          a.crosslinks = {}
+    >        end
+    >        a.crosslinks[rel] = b_id
+    >      end
+    >    insert_crosslink(curr, 'a', stash)
+    >    insert_crosslink(zettels[stash], 'a', current_zettel_id)
+    >  -- view settings
+    >  elseif key == string.byte('x') then
+    >    if view_settings.width > 5 then
+    >      view_settings.width = view_settings.width - 5
+    >    end
+    >  elseif key == string.byte('X') then
+    >    if view_settings.width < w-5 then
+    >      view_settings.width = view_settings.width + 5
+    >    end
+    >  elseif key == string.byte('y') then
+    >    if view_settings.height > 0 then
+    >      view_settings.height = view_settings.height - 1
+    >    end
+    >  elseif key == string.byte('Y') then
+    >    if view_settings.height < h-2 then
+    >      view_settings.height = view_settings.height + 1
+    >    end
+    >  elseif key == string.byte('z') then
+    >    -- scroll to show the current zettel at top of screen
+    >    -- often has the effect of zooming in on its hierarchy
+    >    view_settings.first_zettel = current_zettel_id
+    >  end
+    >end
+- __teliva_timestamp:
+    >Thu Feb 10 21:20:45 2022
+  __teliva_note:
+    >clear stash after linking
+  update:
+    >function update(window)
+    >  local key = curses.getch()
+    >  local h, w = window:getmaxyx()
+    >  local curr = zettels[current_zettel_id]
+    >  assert(curr, string.format('cursor fell off the edge of the world: %s', type(current_zettel_id)))
+    >  -- move along the graph
+    >  if key == string.byte('j') then
+    >    if curr.child then
+    >      current_zettel_id = curr.child
+    >    elseif curr.next then
+    >      current_zettel_id = curr.next
+    >    elseif curr.parent and zettels[curr.parent].next then
+    >      current_zettel_id = zettels[curr.parent].next
+    >    end
+    >  elseif key == string.byte('k') then
+    >    if curr.parent then current_zettel_id = curr.parent end
+    >  elseif key == string.byte('h') then
+    >    if curr.prev then
+    >      current_zettel_id = curr.prev
+    >    elseif curr.parent then
+    >      current_zettel_id = curr.parent
+    >    end
+    >  elseif key == string.byte('l') then
+    >    if curr.next then
+    >      current_zettel_id = curr.next
+    >    elseif curr.parent and zettels[curr.parent].next then
+    >      current_zettel_id = zettels[curr.parent].next
+    >    end
+    >  -- move along the screen
+    >  elseif key == curses.KEY_UP then
+    >    if render_state.curr_h > 1 then
+    >      current_zettel_id = render_state.wh2id[render_state.curr_w][render_state.curr_h - 1]
+    >    end
+    >  elseif key == curses.KEY_DOWN then
+    >    if render_state.wh2id[render_state.curr_w][render_state.curr_h + 1] then
+    >      current_zettel_id = render_state.wh2id[render_state.curr_w][render_state.curr_h + 1]
+    >    end
+    >  elseif key == curses.KEY_LEFT then
+    >    if render_state.curr_w > 1 then
+    >      current_zettel_id = render_state.wh2id[render_state.curr_w - 1][render_state.curr_h]
+    >    end
+    >  elseif key == curses.KEY_RIGHT then
+    >    if render_state.wh2id[render_state.curr_w + 1] and render_state.wh2id[render_state.curr_w + 1][render_state.curr_h] then
+    >      current_zettel_id = render_state.wh2id[render_state.curr_w + 1][render_state.curr_h]
+    >    end
+    >  -- mutations
+    >  elseif key == string.byte('e') then
+    >    editz(window)
+    >  elseif key == string.byte('a') then
+    >    -- insert sibling after
+    >    local old = curr.next
+    >    curr.next = new_id()
+    >    local new = zettels[curr.next]
+    >    new.data = ''
+    >    new.next = old
+    >    new.prev = current_zettel_id
+    >    if old then
+    >      zettels[old].prev = curr.next
+    >      assert(curr.parent == zettels[old].parent, 'siblings should have same parent')
+    >    end
+    >    new.parent = curr.parent
+    >    current_zettel_id = curr.next
+    >    render(window) -- recompute render_state
+    >    editz(window)
+    >  elseif key == string.byte('b') then
+    >    -- insert sibling before
+    >    local old = curr.prev
+    >    curr.prev = new_id()
+    >    local new = zettels[curr.prev]
+    >    new.data = ''
+    >    new.prev = old
+    >    new.next = current_zettel_id
+    >    if old then
+    >      zettels[old].next = curr.prev
+    >      assert(curr.parent == zettels[old].parent, 'siblings should have same parent')
+    >    end
+    >    new.parent = curr.parent
+    >    current_zettel_id = curr.prev
+    >    render(window) -- recompute render_state
+    >    editz(window)
+    >  elseif key == string.byte('c') then
+    >    -- insert child
+    >    local old = curr.child
+    >    curr.child = new_id()
+    >    local new = zettels[curr.child]
+    >    new.data = ''
+    >    new.next = old
+    >    if old then
+    >      assert(zettels[old].prev == nil, "first child shouldn't have a previous sibling")
+    >      zettels[old].prev = curr.child
+    >    end
+    >    new.parent = curr
+    >    current_zettel_id = curr.child
+    >    render(window) -- recompute render_state
+    >    editz(window)
+    >  -- cross-links
+    >  elseif key == string.byte('s') then
+    >    -- save zettel to a stash
+    >    stash = current_zettel_id
+    >  elseif key == string.byte('t') then
+    >    -- cross-link a zettel bidirectionally with what's on the stash
+    >    local insert_crosslink =
+    >      function(a, rel, b_id)
+    >        if a.crosslinks == nil then
+    >          a.crosslinks = {}
+    >        end
+    >        a.crosslinks[rel] = b_id
+    >      end
+    >    insert_crosslink(curr, 'a', stash)
+    >    insert_crosslink(zettels[stash], 'a', current_zettel_id)
+    >    stash = nil
+    >  -- view settings
+    >  elseif key == string.byte('x') then
+    >    if view_settings.width > 5 then
+    >      view_settings.width = view_settings.width - 5
+    >    end
+    >  elseif key == string.byte('X') then
+    >    if view_settings.width < w-5 then
+    >      view_settings.width = view_settings.width + 5
+    >    end
+    >  elseif key == string.byte('y') then
+    >    if view_settings.height > 0 then
+    >      view_settings.height = view_settings.height - 1
+    >    end
+    >  elseif key == string.byte('Y') then
+    >    if view_settings.height < h-2 then
+    >      view_settings.height = view_settings.height + 1
+    >    end
+    >  elseif key == string.byte('z') then
+    >    -- scroll to show the current zettel at top of screen
+    >    -- often has the effect of zooming in on its hierarchy
+    >    view_settings.first_zettel = current_zettel_id
+    >  end
+    >end