# .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 > >function string.pos(s, sub) > return string.find(s, sub, 1, true) -- plain=true to disable regular expressions >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 > end > for k, v in pairs(b) do > if a[k] ~= v then > return false > end > end > return true > 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 > elseif type(x) == 'string' then > return '"'..x..'"' > end > return tostring(x) >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 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: >function filter(h, f) > result = {} > for k, v in pairs(h) do > if f(k, v) then > result[k] = v > end > end > return result >end - __teliva_timestamp: original ifilter: >-- only for arrays >function ifilter(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 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: >-- TODO: handle unicode >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 count: >-- turn an array of elements into a map from elements to their frequency >-- analogous to count_letters for non-strings >function count(a) > local result = {} > for i, v in ipairs(a) do > if result[v] == nil then > result[v] = 1 > else > result[v] = result[v] + 1 > end > end > return result >end - __teliva_timestamp: original union: >function union(a, b) > for k, v in pairs(b) do > a[k] = v > end > return a >end - __teliva_timestamp: original subtract: >-- set subtraction >function subtract(a, b) > for k, v in pairs(b) do > a[k] = nil > end > return a >end - __teliva_timestamp: original all: >-- universal quantifier on sets >function all(s, f) > for k, v in pairs(s) do > if not f(k, v) then > return false > end > end > return true >end - __teliva_timestamp: original to_array: >-- turn a set into an array >-- drops values >function to_array(h) > local result = {} > for k, _ in pairs(h) do > table.insert(result, k) > 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 > table.insert(l, elems[i]) > end >end - __teliva_timestamp: original prepend: >-- concatenate list 'elems' into the start of 'l', modifying 'l' in the process >function prepend(l, elems) > for i=1,#elems do > table.insert(l, i, elems[i]) > end >end - __teliva_timestamp: original all_but: >function all_but(x, idx) > if type(x) == 'table' then > local result = {} > for i, elem in ipairs(x) do > if i ~= idx then > table.insert(result,elem) > end > end > return result > elseif type(x) == 'string' then > if idx < 1 then return x:sub(1) end > return x:sub(1, idx-1) .. x:sub(idx+1) > else > error('all_but: unsupported type '..type(x)) > end >end > >function test_all_but() > check_eq(all_but('', 0), '', 'all_but: empty') > check_eq(all_but('abc', 0), 'abc', 'all_but: invalid low index') > check_eq(all_but('abc', 4), 'abc', 'all_but: invalid high index') > check_eq(all_but('abc', 1), 'bc', 'all_but: first index') > check_eq(all_but('abc', 3), 'ab', 'all_but: final index') > check_eq(all_but('abc', 2), 'ac', 'all_but: middle index') >end - __teliva_timestamp: original set: >function set(l) > local result = {} > for i, elem in ipairs(l) do > result[elem] = true > end > return result >end - __teliva_timestamp: original set_eq: >function set_eq(l1, l2) > return eq(set(l1), set(l2)) >end > >function test_set_eq() > check(set_eq({1}, {1}), 'set_eq: identical') > check(not set_eq({1, 2}, {1, 3}), 'set_eq: different') > check(set_eq({1, 2}, {2, 1}), 'set_eq: order') > check(set_eq({1, 2, 2}, {2, 1}), 'set_eq: duplicates') >end - __teliva_timestamp: original clear: >function clear(lines) > while #lines > 0 do > table.remove(lines) > end >end - __teliva_timestamp: original zap: >function zap(target, src) > clear(target) > append(target, src) >end - __teliva_timestamp: original mfactorial: >-- memoized version of factorial >-- doesn't memoize recursive calls, but may be good enough >mfactorial = memo1(factorial) - __teliva_timestamp: original factorial: >function factorial(n) > local result = 1 > for i=1,n do > result = result*i > end > return result >end - __teliva_timestamp: original memo1: >-- a higher-order function that takes a function of a single arg >-- (that never returns nil) >-- and returns a memoized version of it >function memo1(f) > local memo = {} > return function(x) > if memo[x] == nil then > memo[x] = f(x) > end > return memo[x] > end >end > >-- mfactorial doesn't seem noticeably faster >function test_memo1() > for i=0,30 do > check_eq(mfactorial(i), factorial(i), 'memo1 over factorial: '..str(i)) > end >end - __teliva_timestamp: original num_permutations: >-- number of permutations of n distinct objects, taken r at a time >function num_permutations(n, r) > return factorial(n)/factorial(n-r) >end > >-- mfactorial doesn't seem noticeably faster >function test_memo1() > for i=0,30 do > for j=0,i do > check_eq(num_permutations(i, j), mfactorial(i)/mfactorial(i-j), 'num_permutations memoizes: '..str(i)..'P'..str(j)) > end > 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) > local c = table.remove(h.kbd, 1) > if c == nil then return c end > return string.byte(c) -- for verisimilitude with ncurses > end > h.addch = function(self, c) > local scr = self.scr > if c == '\n' then > scr.cursy = scr.cursy+1 > scr.cursx = 0 > return > end > 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 > h.refresh = function(self) > -- nothing > 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) > props.cursy = 1 > props.cursx = 1 > 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 b = w:getch() > if b == nil then break end > w:mvaddstr(y, 1, lines[string.char(b)]) > 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', '< {'^b', ' {'^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.write(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