about summary refs log blame commit diff stats
path: root/zet.tlv
blob: 0a638c47ec666e8c3c21468059750ff4731994dc (plain) (tree)
























                                                                                
                            







                                                
                            


                                
                            







































                                                                              














                                                                

                                        
                              
                          

                           
                                                                 







                                                                






























                                                     






















































                                                 









                                
          






                                                                              

                              


                              
                         
















                                                                       



                                                                        


                      
                              
















                                                                
                                    
                           














                                                                                  





                                      
                                                                







                                    
                           





                                          
                      





                                           


                               





                              
                            








                                      





                              


                           
                                                          







                                                     
                                
                                                                              
































                                                                            































































                                                                                           






                              


                           
                                                                  


                                    
                                                                      






                                        
                                       

                    

                       


                              




                             
                               
          
                    
        






                                                                       

            






                                 
                                                 




                                                    
            
                                          
                


            



















































                                                                             
                            
                                 








































                                                                                                                                 

                                      
                      

          

                              













































                                                                                                 
                       
        
                              











                                                                             

                              
                           















                                                                                                                      

                                                                          
                                                                         
                               



                                    
                                                                
                                
                         



















                                                                                                  

                                                      

                    



                                                      




                                    

                                                        



                                      
                               









                                                 
                                 




























                                                                                                   
                          





















































































                                                                                                       

                                      

                                          
                          







































































































                                                                                                         


                             












































                                                                                                 
                       
        


                             





                                       

                       

          


                             


                                                                            
        
                           











                                                                                                                      


                             

                                                                          
                                      







                                                                         
                         


                            
























                                                        
                                              
        


                             
                           
                            




                                                                                             
                                                             






                                                                                                                      
                      
        


                             


                                     

                                                 
                                 




















                                                                                                   
                                                         



                                  







                                                                        
                                







                                                    
                                 























































                                                                                                                                 
                                
                     
                             








                             
                                                     



                     
                                                               


                    
                                               



                      
                                                                  




















                                                    
                                 























































































                                                                                                                                 



                             
                                 
















































































































                                                                                                                                 
                                
                     
                             
                             
                             
      







                                                                        
                                     
                     

                                               






                                            
                                 














































































































                                                                                                                                 



































                                                             

                       


























                                                               

                       




                                                               



                                     

          














































                                                                                                                                                                                                                                   





                                                                        
                           
                     

                             










                                
                                 



















































































































































































                                                                                                                                 
                       

































































































                                                                                                   
                       












































































































                                                                                                                   
                       



































                                                                               
                           
                     

                             


















                                                                         

                       



































































                                                                                                                   
                       






                                                
                                 








































































































































                                                                                                                                 
                                 



































































































































                                                                                                                                 















                                                                                                
                         
















































                                                                         

                       












                                                                                                 





                                               
                                 





































































































































                                                                                                                                 
                     


                                                                 
                                 







































                                                                                                   
                           









                                 
                                                              



                                                                                             
                                                             









                                                                                                                                


                                                              

                                                                 
                                 
























                                                                                                   
                                






                                                         
                                                                 

                                       












                                                         


                                    

                                                                 
                                 



















                                                                                                   
                            




                                                    
                          
                                



                                                         
                        

                                                             

                            
                                                             
                            


                                      

                                     
            
                                                          
                            
            
                                                          
                            
            

                                       
                                                                 
                                       
            





















                                                                                                
            





                                                                
                         


                            
            

















                                                        
            



















































                                                                                                                   
                
              










                                                                
            







                                      
            


                                                             
          
                       

                     


                                                 

                            
                                 


                                                                                                        

                                      











                                                              

                                          





                                                              









                                                              



















































































































                                                                                                                                 




                                                              

                            
                                 


                                                                                                        

























































































































































                                                                                                                                 




                                                                        
                           
                     
                           
                     
                             




                                


                             
                                 




























































































































































































                                                                                                                                 












                                                                                                                                      





































                                                                                                 




































                                                                              






































                                                                                           
                                                      
                                           







                                                     
                                                                                






                                                   




                                                                                           





                                                                              
                                                          




                                        




                                                                      





                                                                              

































                                                                                     
        
# .tlv file generated by https://github.com/akkartik/teliva
# You may edit it if you are careful; however, you may see cryptic errors if you
# violate Teliva's assumptions.
#
# .tlv files are representations of Teliva programs. Teliva programs consist of
# sequences of definitions. Each definition is a table of key/value pairs. Keys
# and values are both strings.
#
# Lines in .tlv files always follow exactly one of the following forms:
# - comment lines at the top of the file starting with '#' at column 0
# - beginnings of definitions starting with '- ' at column 0, followed by a
#   key/value pair
# - key/value pairs consisting of '  ' at column 0, containing either a
#   spaceless value on the same line, or a multi-line value
# - multiline values indented by more than 2 spaces, starting with a '>'
#
# If these constraints are violated, Teliva may unceremoniously crash. Please
# report bugs at http://akkartik.name/contact
- __teliva_timestamp: original
  str_helpers:
    >-- some string helpers from http://lua-users.org/wiki/StringIndexing
    >
    >-- index characters using []
    >getmetatable('').__index = function(str,i)
    >  if type(i) == 'number' then
    >    return str:sub(i,i)
    >  else
    >    return string[i]
    >  end
    >end
    >
    >-- ranges using (), selected bytes using {}
    >getmetatable('').__call = function(str,i,j)
    >  if type(i)~='table' then
    >    return str:sub(i,j)
    >  else
    >    local t={}
    >    for k,v in ipairs(i) do
    >      t[k]=str:sub(v,v)
    >    end
    >    return table.concat(t)
    >  end
    >end
    >
    >-- iterate over an ordered sequence
    >function q(x)
    >  if type(x) == 'string' then
    >    return x:gmatch('.')
    >  else
    >    return ipairs(x)
    >  end
    >end
    >
    >-- insert within string
    >function string.insert(str1, str2, pos)
    >  return str1:sub(1,pos)..str2..str1:sub(pos+1)
    >end
    >
    >function string.remove(s, pos)
    >  return s:sub(1,pos-1)..s:sub(pos+1)
    >end
    >
    >-- TODO: backport utf-8 support from Lua 5.3
- __teliva_timestamp: original
  debugy:
    >debugy = 5
- __teliva_timestamp: original
  dbg:
    >-- helper for debug by print; overlay debug information towards the right
    >-- reset debugy every time you refresh screen
    >function dbg(window, s)
    >  local oldy = 0
    >  local oldx = 0
    >  oldy, oldx = window:getyx()
    >  window:mvaddstr(debugy, 60, s)
    >  debugy = debugy+1
    >  window:mvaddstr(oldy, oldx, '')
    >end
- __teliva_timestamp: original
  check:
    >function check(x, msg)
    >  if x then
    >    Window:addch('.')
    >  else
    >    print('F - '..msg)
    >    print('  '..str(x)..' is false/nil')
    >    teliva_num_test_failures = teliva_num_test_failures + 1
    >    -- overlay first test failure on editors
    >    if teliva_first_failure == nil then
    >      teliva_first_failure = msg
    >    end
    >  end
    >end
- __teliva_timestamp: original
  check_eq:
    >function check_eq(x, expected, msg)
    >  if eq(x, expected) then
    >    Window:addch('.')
    >  else
    >    print('F - '..msg)
    >    print('  expected '..str(expected)..' but got '..str(x))
    >    teliva_num_test_failures = teliva_num_test_failures + 1
    >    -- overlay first test failure on editors
    >    if teliva_first_failure == nil then
    >      teliva_first_failure = msg
    >    end
    >  end
    >end
- __teliva_timestamp: original
  eq:
    >function eq(a, b)
    >  if type(a) ~= type(b) then return false end
    >  if type(a) == 'table' then
    >    if #a ~= #b then return false end
    >    for k, v in pairs(a) do
    >      if b[k] ~= v then
    >        return false
    >      end
    >      return true
    >    end
    >  end
    >  return a == b
    >end
- __teliva_timestamp: original
  str:
    >-- smarter tostring
    >-- slow; used only for debugging
    >function str(x)
    >  if type(x) == 'table' then
    >    local result = ''
    >    result = result..#x..'{'
    >    for k, v in pairs(x) do
    >      result = result..str(k)..'='..str(v)..', '
    >    end
    >    result = result..'}'
    >    return result
    >  end
    >  return tostring(x)
    >end
- __teliva_timestamp: original
  map:
    >-- only for arrays
    >function map(l, f)
    >  result = {}
    >  for _, x in ipairs(l) do
    >    table.insert(result, f(x))
    >  end
    >  return result
    >end
- __teliva_timestamp: original
  reduce:
    >-- only for arrays
    >function reduce(l, f, init)
    >  result = init
    >  for _, x in ipairs(l) do
    >    result = f(result, x)
    >  end
    >  return result
    >end
- __teliva_timestamp: original
  filter:
    >-- only for arrays
    >function filter(l, f)
    >  result = {}
    >  for _, x in ipairs(l) do
    >    if f(x) then
    >      table.insert(result, x)
    >    end
    >  end
    >  return result
    >end
- __teliva_timestamp: original
  find_index:
    >function find_index(arr, x)
    >  for n, y in ipairs(arr) do
    >    if x == y then
    >      return n
    >    end
    >  end
    >end
- __teliva_timestamp: original
  trim:
    >function trim(s)
    >  return s:gsub('^%s*', ''):gsub('%s*$', '')
    >end
- __teliva_timestamp: original
  split:
    >function split(s, d)
    >  result = {}
    >  for match in (s..d):gmatch("(.-)"..d) do
    >    table.insert(result, match);
    >  end
    >  return result
    >end
- __teliva_timestamp: original
  sort_letters:
    >function sort_letters(s)
    >  tmp = {}
    >  for i=1,#s do
    >    table.insert(tmp, s[i])
    >  end
    >  table.sort(tmp)
    >  local result = ''
    >  for _, c in pairs(tmp) do
    >    result = result..c
    >  end
    >  return result
    >end
    >
    >function test_sort_letters(s)
    >  check_eq(sort_letters(''), '', 'test_sort_letters: empty')
    >  check_eq(sort_letters('ba'), 'ab', 'test_sort_letters: non-empty')
    >  check_eq(sort_letters('abba'), 'aabb', 'test_sort_letters: duplicates')
    >end
- __teliva_timestamp: original
  count_letters:
    >function count_letters(s)
    >  local result = {}
    >  for i=1,s:len() do
    >    local c = s[i]
    >    if result[c] == nil then
    >      result[c] = 1
    >    else
    >      result[c] = result[c] + 1
    >    end
    >  end
    >  return result
    >end
- __teliva_timestamp: original
  append:
    >-- concatenate list 'elems' into 'l', modifying 'l' in the process
    >function append(l, elems)
    >  for i=1,#elems do
    >    l[#l+1] = elems[i]
    >  end
    >end
- __teliva_timestamp: original
  menu:
    >-- To show app-specific hotkeys in the menu bar, add hotkey/command
    >-- arrays of strings to the menu array.
    >menu = {
    >  {'^e', 'edit'},
    >}
- __teliva_timestamp: original
  Window:
    >Window = curses.stdscr()
- __teliva_timestamp: original
  window:
    >-- constructor for fake screen and window
    >-- call it like this:
    >--   local w = window{
    >--     kbd=kbd('abc'),
    >--     scr=scr{h=5, w=4},
    >--   }
    >-- eventually it'll do everything a real ncurses window can
    >function window(h)
    >  h.__index = h
    >  setmetatable(h, h)
    >  h.__index = function(table, key)
    >    return rawget(h, key)
    >  end
    >  h.attrset = function(self, x)
    >    self.scr.attrs = x
    >  end
    >  h.attron = function(self, x)
    >    -- currently same as attrset since Lua 5.1 doesn't have bitwise operators
    >    -- doesn't support multiple attrs at once
    >--    local old = self.scr.attrs
    >--    self.scr.attrs = old|x
    >    self.scr.attrs = x
    >  end
    >  h.attroff = function(self, x)
    >    -- currently borked since Lua 5.1 doesn't have bitwise operators
    >    -- doesn't support multiple attrs at once
    >--    local old = self.scr.attrs
    >--    self.scr.attrs = old & (~x)
    >    self.scr.attrs = curses.A_NORMAL
    >  end
    >  h.getch = function(self)
    >    return table.remove(h.kbd, 1)
    >  end
    >  h.addch = function(self, c)
    >    local scr = self.scr
    >    if scr.cursy <= scr.h then
    >      scr[scr.cursy][scr.cursx] = {data=c, attrs=scr.attrs}
    >      scr.cursx = scr.cursx+1
    >      if scr.cursx > scr.w then
    >        scr.cursy = scr.cursy+1
    >        scr.cursx = 1
    >      end
    >    end
    >  end
    >  h.addstr = function(self, s)
    >    for i=1,s:len() do
    >      self:addch(s[i])
    >    end
    >  end
    >  h.mvaddch = function(self, y, x, c)
    >    self.scr.cursy = y
    >    self.scr.cursx = x
    >    self:addch(c)
    >  end
    >  h.mvaddstr = function(self, y, x, s)
    >    self.scr.cursy = y
    >    self.scr.cursx = x
    >    self:addstr(s)
    >  end
    >  h.clear = function(self)
    >    clear_scr(self.scr)
    >  end
    >  return h
    >end
- __teliva_timestamp: original
  kbd:
    >function kbd(keys)
    >  local result = {}
    >  for i=1,keys:len() do
    >    table.insert(result, keys[i])
    >  end
    >  return result
    >end
- __teliva_timestamp: original
  scr:
    >function scr(props)
    >  props.cursx = 1
    >  props.cursy = 1
    >  clear_scr(props)
    >  return props
    >end
- __teliva_timestamp: original
  clear_scr:
    >function clear_scr(props)
    >  for y=1,props.h do
    >    props[y] = {}
    >    for x=1,props.w do
    >      props[y][x] = {data=' ', attrs=curses.A_NORMAL}
    >    end
    >  end
    >  return props
    >end
- __teliva_timestamp: original
  check_screen:
    >function check_screen(window, contents, message)
    >  local x, y = 1, 1
    >  for i=1,contents:len() do
    >    check_eq(window.scr[y][x].data, contents[i], message..'/'..y..','..x)
    >    x = x+1
    >    if x > window.scr.w then
    >      y = y+1
    >      x = 1
    >    end
    >  end
    >end
    >
    >-- putting it all together, an example test of both keyboard and screen
    >function test_check_screen()
    >  local lines = {
    >    c='123',
    >    d='234',
    >    a='345',
    >    b='456',
    >  }
    >  local w = window{
    >    kbd=kbd('abc'),
    >    scr=scr{h=3, w=5},
    >  }
    >  local y = 1
    >  while true do
    >    local c = w:getch()
    >    if c == nil then break end
    >    w:mvaddstr(y, 1, lines[c])
    >    y = y+1
    >  end
    >  check_screen(w, '345  '..
    >                  '456  '..
    >                  '123  ',
    >              'test_check_screen')
    >end
- __teliva_timestamp: original
  check_reverse:
    >function check_reverse(window, contents, message)
    >  local x, y = 1, 1
    >  for i=1,contents:len() do
    >    if contents[i] ~= ' ' then
    >      -- hacky version while we're without bitwise operators on Lua 5.1
    >--      check(window.scr[y][x].attrs & curses.A_REVERSE, message..'/'..y..','..x)
    >      check_eq(window.scr[y][x].attrs, curses.A_REVERSE, message..'/'..y..','..x)
    >    else
    >      -- hacky version while we're without bitwise operators on Lua 5.1
    >--      check(window.scr[y][x].attrs & (~curses.A_REVERSE), message..'/'..y..','..x)
    >      check(window.scr[y][x].attrs ~= curses.A_REVERSE, message..'/'..y..','..x)
    >    end
    >    x = x+1
    >    if x > window.scr.w then
    >      y = y+1
    >      x = 1
    >    end
    >  end
    >end
- __teliva_timestamp: original
  check_bold:
    >function check_bold(window, contents, message)
    >  local x, y = 1, 1
    >  for i=1,contents:len() do
    >    if contents[i] ~= ' ' then
    >      -- hacky version while we're without bitwise operators on Lua 5.1
    >--      check(window.scr[y][x].attrs & curses.A_BOLD, message..'/'..y..','..x)
    >      check_eq(window.scr[y][x].attrs, curses.A_BOLD, message..'/'..y..','..x)
    >    else
    >      -- hacky version while we're without bitwise operators on Lua 5.1
    >--      check(window.scr[y][x].attrs & (~curses.A_BOLD), message..'/'..y..','..x)
    >      check(window.scr[y][x].attrs ~= curses.A_BOLD, message..'/'..y..','..x)
    >    end
    >    x = x+1
    >    if x > window.scr.w then
    >      y = y+1
    >      x = 1
    >    end
    >  end
    >end
- __teliva_timestamp: original
  check_color:
    >-- check which parts of a screen have the given color_pair
    >function check_color(window, cp, contents, message)
    >  local x, y = 1, 1
    >  for i=1,contents:len() do
    >    if contents[i] ~= ' ' then
    >      -- hacky version while we're without bitwise operators on Lua 5.1
    >--      check(window.scr[y][x].attrs & curses.color_pair(cp), message..'/'..y..','..x)
    >      check_eq(window.scr[y][x].attrs, curses.color_pair(cp), message..'/'..y..','..x)
    >    else
    >      -- hacky version while we're without bitwise operators on Lua 5.1
    >--      check(window.scr[y][x].attrs & (~curses.A_BOLD), message..'/'..y..','..x)
    >      check(window.scr[y][x].attrs ~= curses.color_pair(cp), message..'/'..y..','..x)
    >    end
    >    x = x+1
    >    if x > window.scr.w then
    >      y = y+1
    >      x = 1
    >    end
    >  end
    >end
- __teliva_timestamp: original
  spaces:
    >function spaces(n)
    >  for i=1,n do
    >    Window:addch(' ')
    >  end
    >end
- __teliva_timestamp: original
  init_colors:
    >function init_colors()
    >  -- light background
    >  curses.init_pair(view_settings.current_zettel_bg, 236, 230)
    >  curses.init_pair(1, 236, 250)
    >  curses.init_pair(2, 236, 252)
    >  -- dark background
    >--?   curses.init_pair(view_settings.current_zettel_bg, 252, 130)
    >--?   curses.init_pair(1, 252, 240)
    >--?   curses.init_pair(2, 252, 242)
    >end
- __teliva_timestamp: original
  main:
    >function main()
    >  init_colors()
    >  current_zettel_id = zettels.root
    >
    >  while true do
    >    render(Window)
    >    update(Window)
    >  end
    >end
- __teliva_timestamp: original
  depth:
    >function depth(zettel)
    >  local result = 0
    >  while zettel.parent do
    >    result = result+1
    >    zettel = zettel.parent
    >  end
    >  return result
    >end
- __teliva_timestamp: original
  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
    >  for i=1,#zettel.data do
    >    local c = zettel.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: original
  current_zettel_id:
    >current_zettel_id = ''
- __teliva_timestamp: original
  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
    >  current_zettel_bg=3,  -- color pair index initialized in init_colors
    >}
- __teliva_timestamp: original
  zettels:
    >zettels = {
    >  root="a",
    >  a={
    >    data="abc\ndef",
    >    child="c",
    >    next="b",
    >  },
    >  b={
    >    data="ghi\njklm",
    >    prev="a",
    >  },
    >  c={
    >    data="c",
    >    parent="a",
    >    next="d",
    >  },
    >  d={
    >    data="d",
    >    parent="a",
    >    prev="c",
    >  }
    >}
- __teliva_timestamp: original
  render_state:
    >-- some information about what's been drawn on screen
    >render_state = {
    >  -- where the current zettel is, in units of zettels
    >  curr_h = 1,
    >  curr_w = 1,
    >  -- what zettel is at each position on screen, in units of zettels
    >  hw2id = {},
    >}
- __teliva_timestamp: original
  update:
    >function update(window)
    >  local key = window:getch()
    >  local curr = zettels[current_zettel_id]
    >  -- graph-based navigation
    >  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
    >  -- screen-based navigation
    >  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
    >  --
    >  elseif key == 5 then  -- ctrl-e
    >    editz(window)
    >  end
    >end
- __teliva_timestamp: original
  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
    >      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-2, 0, '')
    >  for i=1,3 do
    >    window:attrset(curses.color_pair(i%2+1))
    >    window:addstr('')
    >    spaces(view_settings.width-string.len(''))
    >    window:attrset(curses.color_pair(0))
    >    window:addstr(' ')  -- margin
    >  end
    >  window:mvaddstr(lines-1, 0, '? ')
    >  window:refresh()
    >end
- __teliva_timestamp: original
  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
    >  current_zettel_bg=3,  -- color pair index initialized in init_colors
    >}
- __teliva_timestamp: original
  editz:
    >function editz(window)
    >  menu = { {'^e', 'back to browsing'},}
    >  local top = (render_state.curr_h - 1) * (view_settings.height + view_settings.vmargin)
    >  local bottom = top + view_settings.height
    >  local left = (render_state.curr_w - 1) * (view_settings.width + view_settings.hmargin)
    >  local right = left + view_settings.width
    >  local cursor = 1
    >  curses.curs_set(0)
    >  local quit = false
    >  while not quit do
    >    editz_render(window, zettels[current_zettel_id].data, cursor, top, bottom, left, right)
    >    quit, zettels[current_zettel_id].data, cursor = editz_update(window, zettels[current_zettel_id].data, cursor)
    >  end
    >  curses.curs_set(1)
    >end
- __teliva_timestamp: original
  editz_render:
    >function editz_render(window, s, cursor, top, minbottom, left, right)
    >  local h, w = window:getmaxyx()
    >  window:attrset(curses.color_pair(view_settings.current_zettel_bg))
    >  for y=top,minbottom-1 do
    >    for x=left,right-1 do
    >      window:mvaddch(y, x, ' ')
    >    end
    >  end
    >  local y, x = top, left + 1  -- left padding; TODO: indent
    >  window:mvaddstr(y, x, '')
    >  for i=1,s:len() do
    >    -- render character
    >    if i == cursor then
    >      if s[i] == '\n' then
    >        -- newline at cursor = render extra space in reverse video before jumping to new line
    >        window:attron(curses.A_REVERSE)
    >        window:addch(' ')
    >        window:attroff(curses.A_REVERSE)
    >      else
    >        -- most characters at cursor = render in reverse video
    >        window:attron(curses.A_REVERSE)
    >        window:addstr(s[i])
    >        window:attroff(curses.A_REVERSE)
    >      end
    >    else
    >      if s[i] ~= '\n' then
    >        window:addstr(s[i])
    >      end
    >    end
    >    -- update cursor position
    >    if s[i] == '\n' then
    >      if i == cursor then x = x + 1; end
    >      for col=x,right-1 do window:addch(' '); end
    >      x = left
    >      y = y + 1
    >      if y >= h-2 then return end
    >      window:mvaddstr(y, x, '')
    >      for col=x,right-1 do window:addch(' '); end
    >      x = left + 1  -- left padding; TODO: indent
    >      window:mvaddstr(y, x, '')
    >    else
    >      x = x + 1
    >      if x >= right then
    >        y = y + 1
    >        if y >= h-2 then return end
    >        x = left + 1  -- left padding; TODO: indent
    >        window:mvaddstr(y, x, '')
    >      end
    >    end
    >  end
    >  if cursor > s:len() then
    >    window:attron(curses.A_REVERSE)
    >    window:addch(' ')
    >    window:attroff(curses.A_REVERSE)
    >  else
    >    window:addch(' ')
    >  end
    >end
- __teliva_timestamp: original
  editz_update:
    >function editz_update(window, prose, cursor)
    >  local key = window:getch()
    >  local h, w = window:getmaxyx()
    >  if key == curses.KEY_LEFT then
    >    if cursor > 1 then
    >      cursor = cursor-1
    >    end
    >  elseif key == curses.KEY_RIGHT then
    >    if cursor <= #prose then
    >      cursor = cursor+1
    >    end
    >  elseif key == curses.KEY_DOWN then
    >    cursor = cursor_down(prose, cursor, w)
    >  elseif key == curses.KEY_UP then
    >    cursor = cursor_up(prose, cursor, w)
    >  elseif key == curses.KEY_BACKSPACE or key == 8 or key == 127 then  -- ctrl-h, ctrl-?, delete
    >    if cursor > 1 then
    >      cursor = cursor-1
    >      prose = prose:remove(cursor)
    >    end
    >  elseif key == 5 then  -- ctrl-e
    >    return true, prose, cursor
    >  elseif key == 10 or (key >= 32 and key < 127) then
    >    prose = prose:insert(string.char(key), cursor-1)
    >    cursor = cursor+1
    >  end
    >  return false, prose, cursor
    >end
- __teliva_timestamp: original
  cursor_down:
    >function cursor_down(s, old_idx, width)
    >  local max = s:len()
    >  local i = 1
    >  -- compute oldcol, the screen column of old_idx
    >  local oldcol = 0
    >  local col = 0
    >  while true do
    >    if i > max then
    >      -- abnormal old_idx
    >      return old_idx
    >    end
    >    if i == old_idx then
    >      oldcol = col
    >      break
    >    end
    >    if s[i] == '\n' then
    >      col = 0
    >    else
    >      col = col+1
    >    end
    >    i = i+1
    >  end
    >  -- skip rest of line
    >  while true do
    >    if i > max then
    >      -- current line is at bottom
    >      if col >= width then
    >        return i
    >      end
    >      return old_idx
    >    end
    >    if s[i] == '\n' then
    >      break
    >    end
    >    if i - old_idx >= width then
    >      return i
    >    end
    >    col = col+1
    >    i = i+1
    >  end
    >  -- compute index at same column on next line
    >  -- i is at a newline
    >  i = i+1
    >  col = 0
    >  while true do
    >    if i > max then
    >      -- next line is at bottom and is too short; position at end of it
    >      return i
    >    end
    >    if s[i] == '\n' then
    >      -- next line is too short; position at end of it
    >      return i
    >    end
    >    if col == oldcol then
    >      return i
    >    end
    >    col = col+1
    >    i = i+1
    >  end
    >end
    >
    >function test_cursor_down()
    >  -- lines that don't wrap
    >  check_eq(cursor_down('abc\ndef', 1, 5), 5, 'cursor_down: non-bottom line first char')
    >  check_eq(cursor_down('abc\ndef', 2, 5), 6, 'cursor_down: non-bottom line mid char')
    >  check_eq(cursor_down('abc\ndef', 3, 5), 7, 'cursor_down: non-bottom line final char')
    >  check_eq(cursor_down('abc\ndef', 4, 5), 8, 'cursor_down: non-bottom line end')
    >  check_eq(cursor_down('abc\ndef', 5, 5), 5, 'cursor_down: bottom line first char')
    >  check_eq(cursor_down('abc\ndef', 6, 5), 6, 'cursor_down: bottom line mid char')
    >  check_eq(cursor_down('abc\ndef', 7, 5), 7, 'cursor_down: bottom line final char')
    >  check_eq(cursor_down('abc\n\ndef', 2, 5), 5, 'cursor_down: to shorter line')
    >
    >  -- within a single wrapping line
    >  --   |abcde|  <-- wrap, no newline
    >  --   |fgh  |
    >  check_eq(cursor_down('abcdefgh', 1, 5), 6, 'cursor_down from wrapping line: first char')
    >  check_eq(cursor_down('abcdefgh', 2, 5), 7, 'cursor_down from wrapping line: mid char')
    >  check_eq(cursor_down('abcdefgh', 5, 5), 9, 'cursor_down from wrapping line: to shorter line')
    >
    >  -- within a single very long wrapping line
    >  --   |abcde|  <-- wrap, no newline
    >  --   |fghij|  <-- wrap, no newline
    >  --   |klm  |
    >  check_eq(cursor_down('abcdefghijklm', 1, 5), 6, 'cursor_down within wrapping line: first char')
    >  check_eq(cursor_down('abcdefghijklm', 2, 5), 7, 'cursor_down within wrapping line: mid char')
    >  check_eq(cursor_down('abcdefghijklm', 5, 5), 10, 'cursor_down within wrapping line: final char')
    >end
- __teliva_timestamp: original
  __teliva_note:
    >initial commit: show/edit zettels
  cursor_up:
    >function cursor_up(s, old_idx, width)
    >  local max = s:len()
    >  local i = 1
    >  -- compute oldcol, the screen column of old_idx
    >  local oldcol = 0
    >  local col = 0
    >  local newline_before_current_line = 0
    >  while true do
    >    if i > max or i == old_idx then
    >      oldcol = col
    >      break
    >    end
    >    if s[i] == '\n' then
    >      col = 0
    >      newline_before_current_line = i
    >    else
    >      col = col+1
    >      if col == width then
    >        col = 0
    >      end
    >    end
    >    i = i+1
    >  end
    >  -- find previous newline
    >  i = i-col-1
    >  if old_idx - newline_before_current_line > width then
    >    -- we're in a wrapped line
    >    return old_idx - width
    >  end
    >  -- scan back to start of previous line
    >  if s[i] == '\n' then
    >    i = i-1
    >  end
    >  while true do
    >    if i < 1 then
    >      -- current line is at top
    >      break
    >    end
    >    if s[i] == '\n' then
    >      break
    >    end
    >    i = i-1
    >  end
    >  -- i is at a newline
    >  i = i+1
    >  -- skip whole screen lines within previous line
    >  while newline_before_current_line - i > width do
    >    i = i + width
    >  end
    >  -- compute index at same column on previous screen line
    >  col = 0
    >  while true do
    >    if i > max then
    >      -- next line is at bottom and is too short; position at end of it
    >      return i
    >    end
    >    if s[i] == '\n' then
    >      -- next line is too short; position at end of it
    >      return i
    >    end
    >    if col == oldcol then
    >      return i
    >    end
    >    col = col+1
    >    i = i+1
    >  end
    >end
    >
    >function test_cursor_up()
    >  -- lines that don't wrap
    >  check_eq(cursor_up('abc\ndef', 1, 5), 1, 'cursor_up: top line first char')
    >  check_eq(cursor_up('abc\ndef', 2, 5), 2, 'cursor_up: top line mid char')
    >  check_eq(cursor_up('abc\ndef', 3, 5), 3, 'cursor_up: top line final char')
    >  check_eq(cursor_up('abc\ndef', 4, 5), 4, 'cursor_up: top line end')
    >  check_eq(cursor_up('abc\ndef', 5, 5), 1, 'cursor_up: non-top line first char')
    >  check_eq(cursor_up('abc\ndef', 6, 5), 2, 'cursor_up: non-top line mid char')
    >  check_eq(cursor_up('abc\ndef', 7, 5), 3, 'cursor_up: non-top line final char')
    >  check_eq(cursor_up('abc\ndef\n', 8, 5), 4, 'cursor_up: non-top line end')
    >  check_eq(cursor_up('ab\ndef\n', 7, 5), 3, 'cursor_up: to shorter line')
    >
    >  -- within a single wrapping line
    >  --   |abcde|  <-- wrap, no newline
    >  --   |fgh  |
    >  check_eq(cursor_up('abcdefgh', 6, 5), 1, 'cursor_up from wrapping line: first char')
    >  check_eq(cursor_up('abcdefgh', 7, 5), 2, 'cursor_up from wrapping line: mid char')
    >  check_eq(cursor_up('abcdefgh', 8, 5), 3, 'cursor_up from wrapping line: final char')
    >  check_eq(cursor_up('abcdefgh', 9, 5), 4, 'cursor_up from wrapping line: wrapped line end')
    >
    >  -- within a single very long wrapping line
    >  --   |abcde|  <-- wrap, no newline
    >  --   |fghij|  <-- wrap, no newline
    >  --   |klm  |
    >  check_eq(cursor_up('abcdefghijklm', 11, 5), 6, 'cursor_up within wrapping line: first char')
    >  check_eq(cursor_up('abcdefghijklm', 12, 5), 7, 'cursor_up within wrapping line: mid char')
    >  check_eq(cursor_up('abcdefghijklm', 13, 5), 8, 'cursor_up within wrapping line: final char')
    >  check_eq(cursor_up('abcdefghijklm', 14, 5), 9, 'cursor_up within wrapping line: wrapped line end')
    >
    >  -- from below to (the bottom of) a wrapping line
    >  --   |abcde|  <-- wrap, no newline
    >  --   |fg   |
    >  --   |hij  |
    >  check_eq(cursor_up('abcdefg\nhij', 9, 5), 6, 'cursor_up to wrapping line: first char')
    >  check_eq(cursor_up('abcdefg\nhij', 10, 5), 7, 'cursor_up to wrapping line: mid char')
    >  check_eq(cursor_up('abcdefg\nhij', 11, 5), 8, 'cursor_up to wrapping line: final char')
    >  check_eq(cursor_up('abcdefg\nhij', 12, 5), 8, 'cursor_up to wrapping line: to shorter line')
    >end
- __teliva_timestamp:
    >Wed Feb  9 08:15:25 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
    >      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, '')
    >  for i=1,3 do
    >    window:attrset(curses.color_pair(i%2+1))
    >    window:addstr('')
    >    spaces(view_settings.width-string.len(''))
    >    window:attrset(curses.color_pair(0))
    >    window:addstr(' ')  -- margin
    >  end
    >  window:refresh()
    >end
- __teliva_timestamp:
    >Wed Feb  9 08:15:35 2022
  main:
    >function main()
    >  init_colors()
    >  current_zettel_id = zettels.root
    >
    >  curses.curs_set(0)
    >  while true do
    >    render(Window)
    >    update(Window)
    >  end
    >end
- __teliva_timestamp:
    >Wed Feb  9 08:16:24 2022
  __teliva_note:
    >get rid of commandline
    >
    >There's a reason vim hides it. Confusing to have two cursors on screen.
  editz:
    >function editz(window)
    >  menu = { {'^e', 'back to browsing'},}
    >  local top = (render_state.curr_h - 1) * (view_settings.height + view_settings.vmargin)
    >  local bottom = top + view_settings.height
    >  local left = (render_state.curr_w - 1) * (view_settings.width + view_settings.hmargin)
    >  local right = left + view_settings.width
    >  local cursor = 1
    >  local quit = false
    >  while not quit do
    >    editz_render(window, zettels[current_zettel_id].data, cursor, top, bottom, left, right)
    >    quit, zettels[current_zettel_id].data, cursor = editz_update(window, zettels[current_zettel_id].data, cursor)
    >  end
    >end
- __teliva_timestamp:
    >Wed Feb  9 08:22:20 2022
  editz_render:
    >function editz_render(window, s, cursor, top, minbottom, left, right)
    >  local h, w = window:getmaxyx()
    >  local cursor_y, cursor_x = 0, 0
    >  window:attrset(curses.color_pair(view_settings.current_zettel_bg))
    >  for y=top,minbottom-1 do
    >    for x=left,right-1 do
    >      window:mvaddch(y, x, ' ')
    >    end
    >  end
    >  local y, x = top, left + 1  -- left padding; TODO: indent
    >  window:mvaddstr(y, x, '')
    >  for i=1,s:len() do
    >    if i == cursor then
    >      cursor_y = y
    >      cursor_x = x
    >    end
    >    if s[i] ~= '\n' then
    >      window:addstr(s[i])
    >      x = x + 1
    >      if x >= right then
    >        y = y + 1
    >        if y >= h-2 then return end
    >        x = left + 1  -- left padding; TODO: indent
    >        window:mvaddstr(y, x, '')
    >      end
    >    else
    >      for col=x+1,right-1 do window:addch(' '); end
    >      x = left
    >      y = y + 1
    >      if y >= h-2 then return end
    >      window:mvaddstr(y, x, '')
    >      for col=x,right-1 do window:addch(' '); end
    >      x = left + 1  -- left padding; TODO: indent
    >      window:mvaddstr(y, x, '')
    >    end
    >  end
    >  if cursor_y == 0 and cursor_x == 0 then
    >    cursor_y = y
    >    cursor_x = x
    >  end
    >  window:mvaddstr(cursor_y, cursor_x, '')
    >end
- __teliva_timestamp:
    >Wed Feb  9 08:25:05 2022
  editz:
    >function editz(window)
    >  local old_menu = menu
    >  menu = { {'^e', 'back to browsing'},}
    >  local top = (render_state.curr_h - 1) * (view_settings.height + view_settings.vmargin)
    >  local bottom = top + view_settings.height
    >  local left = (render_state.curr_w - 1) * (view_settings.width + view_settings.hmargin)
    >  local right = left + view_settings.width
    >  local cursor = zettels[current_zettel_id].data:len()+1
    >  local quit = false
    >  curses.curs_set(1)
    >  while not quit do
    >    editz_render(window, zettels[current_zettel_id].data, cursor, top, bottom, left, right)
    >    quit, zettels[current_zettel_id].data, cursor = editz_update(window, zettels[current_zettel_id].data, cursor)
    >  end
    >  curses.curs_set(0)
    >  menu = old_menu
    >end
- __teliva_timestamp:
    >Wed Feb  9 08:28:13 2022
  __teliva_note:
    >stop simulating the cursor
    >
    >editz_render is now much simpler
  editz_update:
    >function editz_update(window, prose, cursor)
    >  local key = window:getch()
    >  local h, w = window:getmaxyx()
    >  if key == curses.KEY_LEFT then
    >    if cursor > 1 then
    >      cursor = cursor-1
    >    end
    >  elseif key == curses.KEY_RIGHT then
    >    if cursor <= #prose then
    >      cursor = cursor+1
    >    end
    >  elseif key == curses.KEY_DOWN then
    >    cursor = cursor_down(prose, cursor, w)
    >  elseif key == curses.KEY_UP then
    >    cursor = cursor_up(prose, cursor, w)
    >  elseif key == curses.KEY_BACKSPACE or key == 8 or key == 127 then  -- ctrl-h, ctrl-?, delete
    >    if cursor > 1 then
    >      cursor = cursor-1
    >      prose = prose:remove(cursor)
    >    end
    >  elseif key == 5 then  -- ctrl-e
    >    return true, prose, cursor
    >  elseif key == 10 or (key >= 32 and key < 127) then
    >    prose = prose:insert(string.char(key), cursor-1)
    >    cursor = cursor+1
    >  end
    >  return false, prose, cursor
    >end
- __teliva_timestamp:
    >Wed Feb  9 17:55:52 2022
  menu:
    >-- To show app-specific hotkeys in the menu bar, add hotkey/command
    >-- arrays of strings to the menu array.
    >menu = {
    >  {'j', 'child'},
    >  {'k', 'parent'},
    >  {'l,h', 'next/prev sib'},
    >  {'e', 'edit'},
    >}
- __teliva_timestamp:
    >Wed Feb  9 17:56:18 2022
  __teliva_note:
    >no need for chords once we drop the commandline
  update:
    >function update(window)
    >  local key = window:getch()
    >  local curr = zettels[current_zettel_id]
    >  -- graph-based navigation
    >  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
    >  -- screen-based navigation
    >  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
    >  --
    >  elseif key == string.byte('e') then
    >    local old_menu = menu
    >    editz(window)
    >    menu = old_menu
    >  end
    >end
- __teliva_timestamp:
    >Wed Feb  9 18:00:42 2022
  menu:
    >-- To show app-specific hotkeys in the menu bar, add hotkey/command
    >-- arrays of strings to the menu array.
    >menu = {
    >  {'j', 'child'},
    >  {'k', 'parent'},
    >  {'l,h', 'next/prev sib'},
    >  {'e', 'edit'},
    >  {'a,b', 'insert sib'},
    >  {'c', 'insert child'},
    >}
- __teliva_timestamp:
    >Wed Feb  9 18:16:23 2022
  zettels:
    >zettels = {
    >  root="id1",
    >  final=4,
    >  id1={
    >    data="this is zettel A\n\nit has some text",
    >    child="id3",
    >    next="id2",
    >  },
    >  id2={
    >    data="this is a sibling of zettel A at the top level",
    >    prev="id1",
    >  },
    >  id3={
    >    data="this is zettel B, a child of A",
    >    parent="id1",
    >    next="id4",
    >  },
    >  id4={
    >    data="this is another child of zettel A, a sibling of B",
    >    parent="id1",
    >    prev="id3",
    >  }
    >}
- __teliva_timestamp:
    >Wed Feb  9 23:04:49 2022
  new_id:
    >function new_id()
    >  zettels.final = zettels.final+1
    >  local result = 'id'..tostring(zettels.final)
    >  zettels[result] = {}
    >  return result
    >end
- __teliva_timestamp:
    >Wed Feb  9 23:10:57 2022
  __teliva_note:
    >creating new zettels
    >
    >feels natural to immediately start editing them
  update:
    >function update(window)
    >  local key = window:getch()
    >  local curr = zettels[current_zettel_id]
    >  assert(curr, string.format('cursor fell off the edge of the world: %s', type(current_zettel_id)))
    >  -- graph-based navigation
    >  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
    >  -- screen-based navigation
    >  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
    >  --
    >  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
    >    zettels[old].prev = curr.next
    >    new.prev = current_zettel_id
    >    assert(curr.parent == zettels[old].parent, 'siblings should have same parent')
    >    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
    >    zettels[old].next = curr.prev
    >    new.next = current_zettel_id
    >    assert(curr.parent == zettels[old].parent, 'siblings should have same parent')
    >    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
    >    assert(zettels[old].prev == nil, "first child shouldn't have a previous sibling")
    >    zettels[old].prev = curr.child
    >    new.parent = curr
    >    current_zettel_id = curr.child
    >    render(window) -- recompute render_state
    >    editz(window)
    >  end
    >end
- __teliva_timestamp:
    >Thu Feb 10 00:01:58 2022
  update:
    >function update(window)
    >  local key = window: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)))
    >  -- graph-based navigation
    >  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
    >  -- screen-based navigation
    >  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
    >  --
    >  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
    >    zettels[old].prev = curr.next
    >    new.prev = current_zettel_id
    >    assert(curr.parent == zettels[old].parent, 'siblings should have same parent')
    >    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
    >    zettels[old].next = curr.prev
    >    new.next = current_zettel_id
    >    assert(curr.parent == zettels[old].parent, 'siblings should have same parent')
    >    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
    >    assert(zettels[old].prev == nil, "first child shouldn't have a previous sibling")
    >    zettels[old].prev = curr.child
    >    new.parent = curr
    >    current_zettel_id = curr.child
    >    render(window) -- recompute render_state
    >    editz(window)
    >  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 00:02:35 2022
  menu:
    >-- To show app-specific hotkeys in the menu bar, add hotkey/command
    >-- arrays of strings to the menu array.
    >menu = {
    >  {'j', 'child'},
    >  {'k', 'parent'},
    >  {'l,h', 'next/prev sib'},
    >  {'e', 'edit'},
    >  {'a,b', 'insert sib'},
    >  {'c', 'insert child'},
    >  {'x,X,y,Y', 'resize'},
    >}
- __teliva_timestamp:
    >Thu Feb 10 06:57:51 2022
  __teliva_note:
    >squeeze menu to make way for next feature
  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 sib/child'},
    >  {'e', 'edit'},
    >  {'j,k,l,h', 'move to child/parent/sib'},
    >  {'x,X,y,Y', 'resize'},
    >}
- __teliva_timestamp:
    >Thu Feb 10 07:00:46 2022
  __teliva_note:
    >bugfix: handle missing parent/child/sib
  update:
    >function update(window)
    >  local key = window: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)))
    >  -- graph-based navigation
    >  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
    >  -- screen-based navigation
    >  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
    >  --
    >  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)
    >  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 07:27:43 2022
  write_zettels:
    >function write_zettels(outfile)
    >  outfile:write(json.encode(zettels))
    >  outfile:close()
    >end
- __teliva_timestamp:
    >Thu Feb 10 07:28:30 2022
  read_zettels:
    >function read_zettels(infile)
    >  zettels = json.decode(infile:read('*a'))
    >  infile:close()
    >end
- __teliva_timestamp:
    >Thu Feb 10 07:30:25 2022
  __teliva_note:
    >saving/loading zettels to/from disk
  main:
    >function main()
    >  init_colors()
    >  curses.curs_set(0)  -- hide cursor except when editing
    >
    >  -- read zettels from disk if possible
    >  local infile = io.open('zet', 'r')
    >  if infile then
    >    read_zettels(infile)
    >  else
    >    local outfile = io.open('zet', 'w')
    >    if outfile then
    >      write_zettels(outfile)
    >    end
    >  end
    >  current_zettel_id = zettels.root
    >
    >  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')
    >    write_zettels(outfile)
    >    os.rename(filename, 'zet')
    >  end
    >end
- __teliva_timestamp:
    >Thu Feb 10 07:32:46 2022
  __teliva_note:
    >stop writing sample zettels to disk
    >
    >That was just for ease of testing write_zettels()
  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
    >
    >  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 07:43:39 2022
  zettels:
    >-- initial state of the zettels
    >-- if you came here to clear the zettels,
    >-- delete everything (ctrl-k and ctrl-u will delete a whole line at a time)
    >-- until it looks like this:
    >--
    >--   zettels = {
    >--     root='id1',
    >--     final=1,
    >--     id1={
    >--       data='',
    >--     },
    >--   }
    >--
    >-- I don't yet trust any deletion feature I create to not mess up your data.
    >-- Besides, this is a good excuse to start making this app your own.
    >
    >zettels = {
    >  root='id1',
    >  final=5,
    >  id1={
    >    data='this is zettel A\n\nit has some text',
    >    child='id3',
    >    next='id2',
    >  },
    >  id2={
    >    data='this is a sibling of zettel A at the top level',
    >    prev='id1',
    >    next='id5',
    >  },
    >  id3={
    >    data='this is zettel B, a child of A',
    >    parent='id1',
    >    next='id4',
    >  },
    >  id4={
    >    data='this is another child of zettel A, a sibling of B',
    >    parent='id1',
    >    prev='id3',
    >  },
    >  id5={
    >    data="(To clean up these sample zettels, hit ctrl-u and edit 'zettels')\n\nI don't yet trust any deletion feature I create to not mess up your data.\nBesides, this is a good excuse to start making this app your own.)",
    >    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 = window: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
    >  window: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
    >  window: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
    >  window: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
    >  window: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 = window: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 = window: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
- __teliva_timestamp:
    >Thu Feb 10 21:51:09 2022
  __teliva_note:
    >fix regression in editor
  editz_render:
    >function editz_render(window, s, cursor, top, minbottom, left, right)
    >  local h, w = window:getmaxyx()
    >  local cursor_y, cursor_x = 0, 0
    >  window:attrset(curses.color_pair(1)) -- 1 is the color combination for the current zettel
    >  for y=top,minbottom-1 do
    >    for x=left,right-1 do
    >      window:mvaddch(y, x, ' ')
    >    end
    >  end
    >  local y, x = top, left + 1  -- left padding; TODO: indent
    >  window:mvaddstr(y, x, '')
    >  for i=1,s:len() do
    >    if i == cursor then
    >      cursor_y = y
    >      cursor_x = x
    >    end
    >    if s[i] ~= '\n' then
    >      window:addstr(s[i])
    >      x = x + 1
    >      if x >= right then
    >        y = y + 1
    >        if y >= h-2 then return end
    >        x = left + 1  -- left padding; TODO: indent
    >        window:mvaddstr(y, x, '')
    >      end
    >    else
    >      for col=x+1,right-1 do window:addch(' '); end
    >      x = left
    >      y = y + 1
    >      if y >= h-2 then return end
    >      window:mvaddstr(y, x, '')
    >      for col=x,right-1 do window:addch(' '); end
    >      x = left + 1  -- left padding; TODO: indent
    >      window:mvaddstr(y, x, '')
    >    end
    >  end
    >  if cursor_y == 0 and cursor_x == 0 then
    >    cursor_y = y
    >    cursor_x = x
    >  end
    >  window:mvaddstr(cursor_y, cursor_x, '')
    >end
- __teliva_timestamp:
    >Fri Feb 11 01:33:31 2022
  __teliva_note:
    >support /tmp being on a separate volume
    >
    >also better error-checking
  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 outfile = io.open('teliva_tmp', 'w')
    >    if outfile then
    >      write_zettels(outfile)
    >      local status, message = os.rename('teliva_tmp', 'zet')
    >      assert(status, message)  -- unceremoniously abort, but we hopefully only lost a little
    >    end
    >    -- TODO: what if io.open failed for a non-sandboxing related reason?!
    >    -- We could silently fail to save.
    >  end
    >end
- __teliva_timestamp:
    >Fri Feb 11 07:51:42 2022
  __teliva_note:
    >bugfix in parent link when inserting child
  update:
    >function update(window)
    >  local key = window: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 = current_zettel_id
    >    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
- __teliva_timestamp:
    >Sat Feb 12 15:11:15 2022
  editz_update:
    >function editz_update(window, prose, cursor, original_prose)
    >  local key = window:getch()
    >  local h, w = window:getmaxyx()
    >  -- cursor movement
    >  if key == curses.KEY_LEFT then
    >    if cursor > 1 then
    >      cursor = cursor-1
    >    end
    >  elseif key == curses.KEY_RIGHT then
    >    if cursor <= #prose then
    >      cursor = cursor+1
    >    end
    >  elseif key == curses.KEY_DOWN then
    >    cursor = cursor_down(prose, cursor, w)
    >  elseif key == curses.KEY_UP then
    >    cursor = cursor_up(prose, cursor, w)
    >  elseif key == curses.KEY_BACKSPACE or key == 8 or key == 127 then  -- ctrl-h, ctrl-?, delete
    >    if cursor > 1 then
    >      cursor = cursor-1
    >      prose = prose:remove(cursor)
    >    end
    >  elseif key == 1 then  -- ctrl-a
    >  elseif key == 12 then  -- ctrl-l
    >  elseif key == 6 then  -- ctrl-f
    >  elseif key == 2 then  -- ctrl-b
    >  -- delete
    >  elseif key == 11 then  -- ctrl-k
    >  -- exit
    >  elseif key == 5 then  -- ctrl-e
    >    return true, prose, cursor
    >  elseif key == 7 then  -- ctrl-g
    >    return true, original_prose, cursor
    >  -- insert
    >  elseif key == 10 or (key >= 32 and key < 127) then
    >    prose = prose:insert(string.char(key), cursor-1)
    >    cursor = cursor+1
    >  end
    >  return false, prose, cursor
    >end
- __teliva_timestamp:
    >Sat Feb 12 15:11:33 2022
  editz:
    >function editz(window)
    >  local old_menu = menu
    >  menu = {
    >    {'^e', 'finish edit'},
    >    {'^g', 'cancel edit'},
    >    {'^a', '<<line'},
    >    {'^b', '<word'},
    >    {'^f', 'word>'},
    >    {'^l', 'line>>'},
    >    {'^k', 'del to line>>'},
    >  }
    >  local old_data = zettels[current_zettel_id].data:sub(1)
    >  local top = (render_state.curr_h - 1) * (view_settings.height + view_settings.vmargin)
    >  local bottom = top + view_settings.height
    >  local left = (render_state.curr_w - 1) * (view_settings.width + view_settings.hmargin)
    >  local right = left + view_settings.width
    >  local cursor = zettels[current_zettel_id].data:len()+1
    >  local quit = false
    >  curses.curs_set(1)
    >  while not quit do
    >    editz_render(window, zettels[current_zettel_id].data, cursor, top, bottom, left, right)
    >    quit, zettels[current_zettel_id].data, cursor = editz_update(window, zettels[current_zettel_id].data, cursor, old_data)
    >  end
    >  curses.curs_set(0)
    >  menu = old_menu
    >end
- __teliva_timestamp:
    >Sat Feb 12 15:55:10 2022
  __teliva_note:
    >editor: move to start of line, move/delete to end of line
  editz_update:
    >function editz_update(window, prose, cursor, original_prose)
    >  local key = window:getch()
    >  local h, w = window:getmaxyx()
    >  -- cursor movement
    >  if key == curses.KEY_LEFT then
    >    if cursor > 1 then
    >      cursor = cursor-1
    >    end
    >  elseif key == curses.KEY_RIGHT then
    >    if cursor <= #prose then
    >      cursor = cursor+1
    >    end
    >  elseif key == curses.KEY_DOWN then
    >    cursor = cursor_down(prose, cursor, w)
    >  elseif key == curses.KEY_UP then
    >    cursor = cursor_up(prose, cursor, w)
    >  elseif key == curses.KEY_BACKSPACE or key == 8 or key == 127 then  -- ctrl-h, ctrl-?, delete
    >    if cursor > 1 then
    >      cursor = cursor-1
    >      prose = prose:remove(cursor)
    >    end
    >  elseif key == 1 then  -- ctrl-a
    >    while cursor > 1 do
    >      if prose[cursor-1] == '\n' then break end
    >      cursor = cursor-1
    >    end
    >  elseif key == 12 then  -- ctrl-l
    >    local max = prose:len()
    >    while cursor <= max and prose[cursor] ~= '\n' do
    >      cursor = cursor+1
    >    end
    >  elseif key == 6 then  -- ctrl-f
    >  elseif key == 2 then  -- ctrl-b
    >  -- delete
    >  elseif key == 11 then  -- ctrl-k
    >    while cursor <= prose:len() and prose[cursor] ~= '\n' do
    >      prose = prose:remove(cursor)
    >    end
    >  -- exit
    >  elseif key == 5 then  -- ctrl-e
    >    return true, prose, cursor
    >  elseif key == 7 then  -- ctrl-g
    >    return true, original_prose, cursor
    >  -- insert
    >  elseif key == 10 or (key >= 32 and key < 127) then
    >    prose = prose:insert(string.char(key), cursor-1)
    >    cursor = cursor+1
    >  end
    >  return false, prose, cursor
    >end
- __teliva_timestamp:
    >Sat Feb 12 17:01:45 2022
  __teliva_note:
    >editor: word-movement shortcuts
  editz_update:
    >function editz_update(window, prose, cursor, original_prose)
    >  local key = window:getch()
    >  local h, w = window:getmaxyx()
    >  -- cursor movement
    >  if key == curses.KEY_LEFT then
    >    if cursor > 1 then
    >      cursor = cursor-1
    >    end
    >  elseif key == curses.KEY_RIGHT then
    >    if cursor <= #prose then
    >      cursor = cursor+1
    >    end
    >  elseif key == curses.KEY_DOWN then
    >    cursor = cursor_down(prose, cursor, w)
    >  elseif key == curses.KEY_UP then
    >    cursor = cursor_up(prose, cursor, w)
    >  elseif key == curses.KEY_BACKSPACE or key == 8 or key == 127 then  -- ctrl-h, ctrl-?, delete
    >    if cursor > 1 then
    >      cursor = cursor-1
    >      prose = prose:remove(cursor)
    >    end
    >  elseif key == 1 then  -- ctrl-a
    >    -- to start of line
    >    while cursor > 1 do
    >      if prose[cursor-1] == '\n' then break end
    >      cursor = cursor-1
    >    end
    >  elseif key == 12 then  -- ctrl-l
    >    -- to end of line
    >    local max = prose:len()
    >    while cursor <= max and prose[cursor] ~= '\n' do
    >      cursor = cursor+1
    >    end
    >  elseif key == 6 then  -- ctrl-f
    >    -- to next word
    >    local max = prose:len()
    >    while cursor <= max and prose[cursor]:match('%w') do
    >      cursor = cursor+1
    >    end
    >    while cursor <= max and prose[cursor]:match('%W') do
    >      cursor = cursor+1
    >    end
    >  elseif key == 2 then  -- ctrl-b
    >    -- to previous word
    >    if cursor > prose:len() then
    >      cursor = prose:len()
    >    end
    >    while cursor > 1 and prose[cursor]:match('%W') do
    >      cursor = cursor-1
    >    end
    >    while cursor > 1 and prose[cursor]:match('%w') do
    >      cursor = cursor-1
    >    end
    >  -- delete
    >  elseif key == 11 then  -- ctrl-k
    >    while cursor <= prose:len() and prose[cursor] ~= '\n' do
    >      prose = prose:remove(cursor)
    >    end
    >  -- exit
    >  elseif key == 5 then  -- ctrl-e
    >    return true, prose, cursor
    >  elseif key == 7 then  -- ctrl-g
    >    return true, original_prose, cursor
    >  -- insert
    >  elseif key == 10 or (key >= 32 and key < 127) then
    >    prose = prose:insert(string.char(key), cursor-1)
    >    cursor = cursor+1
    >  end
    >  return false, prose, cursor
    >end
- __teliva_timestamp:
    >Sat Feb 12 17:12:27 2022
  editz_render:
    >function editz_render(window, s, cursor, top, minbottom, left, right)
    >  local h, w = window:getmaxyx()
    >  local cursor_y, cursor_x = 0, 0
    >  window:attrset(curses.color_pair(1)) -- 1 is the color combination for the current zettel
    >  for y=top,minbottom-1 do
    >    for x=left,right-1 do
    >      window:mvaddch(y, x, ' ')
    >    end
    >  end
    >  for x=left,right-1 do
    >    window:mvaddch(minbottom, x, ' ')
    >  end
    >  local y, x = top, left + 1  -- left padding; TODO: indent
    >  window:mvaddstr(y, x, '')
    >  for i=1,s:len() do
    >    if i == cursor then
    >      cursor_y = y
    >      cursor_x = x
    >    end
    >    if s[i] ~= '\n' then
    >      window:addstr(s[i])
    >      x = x + 1
    >      if x >= right then
    >        y = y + 1
    >        if y >= h-2 then return end
    >        x = left + 1  -- left padding; TODO: indent
    >        window:mvaddstr(y, x, '')
    >      end
    >    else
    >      for col=x+1,right-1 do window:addch(' '); end
    >      x = left
    >      y = y + 1
    >      if y >= h-2 then return end
    >      window:mvaddstr(y, x, '')
    >      for col=x,right-1 do window:addch(' '); end
    >      x = left + 1  -- left padding; TODO: indent
    >      window:mvaddstr(y, x, '')
    >    end
    >  end
    >  if cursor_y == 0 and cursor_x == 0 then
    >    cursor_y = y
    >    cursor_x = x
    >  end
    >  window:mvaddstr(cursor_y, cursor_x, '')
    >end
- __teliva_timestamp:
    >Sat Feb 12 17:15:15 2022
  render_state:
    >-- some information about what's been drawn on screen
    >render_state = {
    >  -- where the current zettel is, in units of zettels
    >  curr_h = 1,
    >  curr_w = 1,
    >  -- what zettel is at each position on screen, in units of zettels
    >  hw2id = {},
    >  -- list of zettels currently displayed
    >  displayed = {},
    >}
- __teliva_timestamp:
    >Sat Feb 12 17:16:20 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 = {{}}
    >  render_state.displayed = {}
    >  while #inprogress > 0 do
    >    local curr = table.remove(inprogress)
    >    if not done[curr.id] then
    >      done[curr.id] = true
    >      render_state.displayed[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
    >  window:refresh()
    >end
- __teliva_timestamp:
    >Sat Feb 12 17:18:34 2022
  __teliva_note:
    >scroll as needed when moving along the graph
  update:
    >function update(window)
    >  local key = window: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
    >    -- scroll if necessary
    >    if not render_state.displayed[current_zettel_id] then
    >      view_settings.first_zettel = current_zettel_id
    >    end
    >  elseif key == string.byte('k') then
    >    if curr.parent then
    >      current_zettel_id = curr.parent
    >    end
    >    -- scroll if necessary
    >    if not render_state.displayed[current_zettel_id] then
    >      view_settings.first_zettel = current_zettel_id
    >    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
    >    -- scroll if necessary
    >    if not render_state.displayed[current_zettel_id] then
    >      view_settings.first_zettel = current_zettel_id
    >    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
    >    -- scroll if necessary
    >    if not render_state.displayed[current_zettel_id] then
    >      view_settings.first_zettel = current_zettel_id
    >    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 = current_zettel_id
    >    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
- __teliva_timestamp:
    >Sat Feb 12 17:23:33 2022
  __teliva_note:
    >editor 'k' shortcut: fall back to next sibling if needed
    >
    >Now we should be able to navigate either with j/k or h/l.
  update:
    >function update(window)
    >  local key = window: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
    >    -- child or next sibling
    >    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
    >    -- scroll if necessary
    >    if not render_state.displayed[current_zettel_id] then
    >      view_settings.first_zettel = current_zettel_id
    >    end
    >  elseif key == string.byte('k') then
    >    -- parent or previous sibling
    >    if curr.parent then
    >      current_zettel_id = curr.parent
    >    elseif curr.prev then
    >      current_zettel_id = curr.prev
    >    end
    >    -- scroll if necessary
    >    if not render_state.displayed[current_zettel_id] then
    >      view_settings.first_zettel = current_zettel_id
    >    end
    >  elseif key == string.byte('h') then
    >    -- previous sibling or parent
    >    if curr.prev then
    >      current_zettel_id = curr.prev
    >    elseif curr.parent then
    >      current_zettel_id = curr.parent
    >    end
    >    -- scroll if necessary
    >    if not render_state.displayed[current_zettel_id] then
    >      view_settings.first_zettel = current_zettel_id
    >    end
    >  elseif key == string.byte('l') then
    >    -- next sibling or next sibling of parent
    >    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
    >    -- scroll if necessary
    >    if not render_state.displayed[current_zettel_id] then
    >      view_settings.first_zettel = current_zettel_id
    >    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 = current_zettel_id
    >    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
- __teliva_timestamp:
    >Sat Feb 12 17:27:18 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'},
    >  {'<', 'back'},
    >  {'x,X,y,Y', 'resize'},
    >  {'s', 'stash'},
    >  {'t', 'link with stash'},
    >  {'z', 'scroll'},
    >}
- __teliva_timestamp:
    >Sat Feb 12 17:57:15 2022
  update:
    >function update(window)
    >  local key = window: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)))
    >  -- read from or write to render_state.history
    >  if key == string.byte('<') then
    >    -- previous zettel moved to
    >    -- does NOT undo mutations
    >    if #render_state.history > 0 then
    >      local previous_state = render_state.history[#render_state.history]
    >      view_settings.first_zettel = previous_state.first_zettel
    >      current_zettel_id = previous_state.cursor
    >      table.remove(render_state.history)
    >    end
    >    return
    >  end
    >  if key ~= string.byte('e') then
    >    table.insert(render_state.history, {first_zettel=view_settings.first_zettel, cursor=current_zettel_id})
    >  end
    >  -- move along the graph
    >  if key == string.byte('j') then
    >    -- child or next sibling
    >    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
    >    -- scroll if necessary
    >    if not render_state.displayed[current_zettel_id] then
    >      view_settings.first_zettel = current_zettel_id
    >    end
    >  elseif key == string.byte('k') then
    >    -- parent or previous sibling
    >    if curr.parent then
    >      current_zettel_id = curr.parent
    >    elseif curr.prev then
    >      current_zettel_id = curr.prev
    >    end
    >    -- scroll if necessary
    >    if not render_state.displayed[current_zettel_id] then
    >      view_settings.first_zettel = current_zettel_id
    >    end
    >  elseif key == string.byte('h') then
    >    -- previous sibling or parent
    >    if curr.prev then
    >      current_zettel_id = curr.prev
    >    elseif curr.parent then
    >      current_zettel_id = curr.parent
    >    end
    >    -- scroll if necessary
    >    if not render_state.displayed[current_zettel_id] then
    >      view_settings.first_zettel = current_zettel_id
    >    end
    >  elseif key == string.byte('l') then
    >    -- next sibling or next sibling of parent
    >    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
    >    -- scroll if necessary
    >    if not render_state.displayed[current_zettel_id] then
    >      view_settings.first_zettel = current_zettel_id
    >    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 = current_zettel_id
    >    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
- __teliva_timestamp:
    >Sat Feb 12 17:58:01 2022
  __teliva_note:
    >make cursor movements less risky using a back button '<'
  render_state:
    >-- some information about what's been drawn on screen
    >-- not saved between app restarts
    >render_state = {
    >  -- where the current zettel is, in units of zettels
    >  curr_h = 1,
    >  curr_w = 1,
    >  -- what zettel is at each position on screen, in units of zettels
    >  hw2id = {},
    >  -- list of zettels currently displayed
    >  displayed = {},
    >  -- history of screen render state
    >  history = {},  -- elems {first_zettel=view_settings.first_zettel, cursor=current_zettel_id}
    >}
- __teliva_timestamp:
    >Thu Feb 17 20:15:14 2022
  doc:blurb:
    >A rudimentary Zettelkasten app trying to hew very close to the original analog setup, as described by abramdemski:
    >
    >https://www.lesswrong.com/posts/NfdHG6oHBJ8Qxc26s/the-zettelkasten-method-1
    >
    >The key attributes of Zettelkasten seem to be:
    >- notes organized in small fragments called 'cards' that can't hold much text
    >- a tree-based organization using sibling and child cards, with the ability to insert children and siblings to any card, any time
    >- ability to cross-link any card to any other, turning the tree into a graph (but still with a strong sense of hierarchy)
    >
    >zet.tlv satisfies these properties, but isn't very intuitive or usable yet. Contributions appreciated.
- __teliva_timestamp:
    >Mon Mar  7 07:50:32 2022
  main:
    >function main()
    >  init_colors()
    >  curses.curs_set(0)  -- hide cursor except when editing
    >
    >  local infile = start_reading(nil, 'zet')
    >  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 outfile = io.open('teliva_tmp', 'w')
    >    if outfile then
    >      write_zettels(outfile)
    >      local status, message = os.rename('teliva_tmp', 'zet')
    >      assert(status, message)  -- unceremoniously abort, but we hopefully only lost a little
    >    end
    >    -- TODO: what if io.open failed for a non-sandboxing related reason?!
    >    -- We could silently fail to save.
    >  end
    >end
- __teliva_timestamp:
    >Mon Mar  7 07:51:06 2022
  __teliva_note:
    >switch to new file API for reading
  read_zettels:
    >function read_zettels(infile)
    >  zettels = jsonf.decode(infile)
    >end
- __teliva_timestamp:
    >Mon Mar  7 10:31:27 2022
  main:
    >function main()
    >  init_colors()
    >  curses.curs_set(0)  -- hide cursor except when editing
    >
    >  -- load any saved zettels
    >  local infile = start_reading(nil, 'zet')
    >  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
    >    local outfile = start_writing(nil, 'zet')
    >    if outfile then
    >      write_zettels(outfile)
    >    end
    >    -- TODO: what if io.open failed for a non-sandboxing related reason?!
    >    -- We could silently fail to save.
    >  end
    >end
- __teliva_timestamp:
    >Mon Mar  7 10:32:08 2022
  __teliva_note:
    >switch to new file API for writing
  write_zettels:
    >function write_zettels(outfile)
    >  outfile:send(json.encode(zettels))
    >  outfile:close()
    >end
- __teliva_timestamp:
    >Thu Mar 10 04:21:28 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 >= 2 then  -- need at least 2 spaces to be able to print edge_label
    >    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
    >
    >function test_render_zettel_single_line_topleft()
    >  local w = window{scr=scr{h=5, w=10}}
    >  render_zettel(w, 34, 1,  -- color 34, indent 1
    >                   '*', 1, 1, -- startx, starty
    >                   {data='abc'})
    >  check_screen(w,    '  abc     '..
    >                     '          '..
    >                     '          '..
    >                     '          '..
    >                     '          ',
    >               'render_zettel: single line zettel from top-left of screen')
    >  -- entire width is used by the single zettel
    >  -- column 1 = margin, column 2 = indent
    >  check_color(w, 34, '##########'..
    >                     '##########'..
    >                     '##########'..
    >                     '          '..
    >                     '          ',
    >              'render_zettel: single line zettel from top-left of screen, background')
    >end
    >
    >function test_render_zettel_from_middle_of_screen()
    >  local w = window{scr=scr{h=5, w=10}}
    >  render_zettel(w, 34, 1, '*', 3, 4, {data='abc'})  -- startx=3, starty=4
    >  check_screen(w,    '          '..
    >                     '          '..
    >                     '     abc  '..
    >                     '          '..
    >                     '          ',
    >               'render_zettel from middle of screen')
    >  check_color(w, 34, '          '..
    >                     '          '..
    >                     '   #######'..
    >                     '   #######'..
    >                     '   #######',
    >               'render_zettel from middle of screen, background')
    >end
    >
    >function test_render_zettel_indented_prints_edge_label()
    >  local w = window{scr=scr{h=5, w=10}}
    >  render_zettel(w, 34, 2, '*', 3, 4, {data='abc'})  -- startx=3, starty=4
    >  check_screen(w,    '          '..
    >                     '          '..
    >                     '    * abc '..
    >                     '          '..
    >                     '          ',
    >               'render_zettel: indent >= 2 prints edge label')
    >end
    >
    >function test_render_zettel_crops_long_lines()
    >  local w = window{scr=scr{h=5, w=10}}
    >  render_zettel(w, 34, 2, '*', 3, 4, {data='abc def'})  -- startx=3, starty=4
    >  check_screen(w,    '          '..
    >                     '          '..
    >                     '    * abc '..
    >                     '          '..
    >                     '          ',
    >               'render_zettel: crops long lines')
    >end
    >
    >function test_render_zettel_multiple_lines()
    >  local w = window{scr=scr{h=5, w=10}}
    >  render_zettel(w, 34, 2, '*', 3, 4, {data='abc\ndef'})  -- startx=3, starty=4
    >  check_screen(w,    '          '..
    >                     '          '..
    >                     '    * abc '..
    >                     '      def '..
    >                     '          ',
    >               'render_zettel: multiple lines')
    >end
    >
    >function test_render_zettel_truncates_extra_lines()
    >  local w = window{scr=scr{h=5, w=10}}
    >  render_zettel(w, 34, 2, '*', 3, 4, {data='a\nb\nc\nd'})  -- startx=3, starty=4
    >  check_screen(w,    '          '..
    >                     '          '..
    >                     '    * a   '..
    >                     '      b   '..
    >                     '      c   ',
    >               'render_zettel: truncates extra lines')
    >end