469 lines
16 KiB
Lua
469 lines
16 KiB
Lua
# .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 string.sub(str,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 string.sub(str,i,j)
|
|
> else
|
|
> local t={}
|
|
> for k,v in ipairs(i) do
|
|
> t[k]=string.sub(str,v,v)
|
|
> end
|
|
> return table.concat(t)
|
|
> end
|
|
>end
|
|
>
|
|
>-- iterate over an ordered sequence
|
|
>function q(x)
|
|
> if type(x) == 'string' then
|
|
> return x:gmatch('.')
|
|
> else
|
|
> return ipairs(x)
|
|
> end
|
|
>end
|
|
>
|
|
>-- insert within string
|
|
>function string.insert(str1, str2, pos)
|
|
> return str1:sub(1,pos)..str2..str1:sub(pos+1)
|
|
>end
|
|
>
|
|
>function string.remove(s, pos)
|
|
> return s:sub(1,pos-1)..s:sub(pos+1)
|
|
>end
|
|
>
|
|
>-- TODO: backport utf-8 support from Lua 5.3
|
|
- __teliva_timestamp: original
|
|
debugy:
|
|
>debugy = 5
|
|
- __teliva_timestamp: original
|
|
dbg:
|
|
>-- helper for debug by print; overlay debug information towards the right
|
|
>-- reset debugy every time you refresh screen
|
|
>function dbg(window, s)
|
|
> local oldy = 0
|
|
> local oldx = 0
|
|
> oldy, oldx = window:getyx()
|
|
> window:mvaddstr(debugy, 60, s)
|
|
> debugy = debugy+1
|
|
> window:mvaddstr(oldy, oldx, '')
|
|
>end
|
|
- __teliva_timestamp: original
|
|
check_eq:
|
|
>function check_eq(x, expected, msg)
|
|
> if x == expected then
|
|
> curses.addch('.')
|
|
> else
|
|
> print('F - '..msg)
|
|
> print(' expected '..tostring(expected)..' but got '..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
|
|
map:
|
|
>-- only for arrays
|
|
>function map(l, f)
|
|
> result = {}
|
|
> for _, x in ipairs(l) do
|
|
> table.insert(result, f(x))
|
|
> end
|
|
> return result
|
|
>end
|
|
- __teliva_timestamp: original
|
|
reduce:
|
|
>-- only for arrays
|
|
>function reduce(l, f, init)
|
|
> result = init
|
|
> for _, x in ipairs(l) do
|
|
> result = f(result, x)
|
|
> end
|
|
> return result
|
|
>end
|
|
- __teliva_timestamp: original
|
|
filter:
|
|
>-- only for arrays
|
|
>function filter(l, f)
|
|
> result = {}
|
|
> for _, x in ipairs(l) do
|
|
> if f(x) then
|
|
> table.insert(result, x)
|
|
> end
|
|
> end
|
|
> return result
|
|
>end
|
|
- __teliva_timestamp: original
|
|
find_index:
|
|
>function find_index(arr, x)
|
|
> for n, y in ipairs(arr) do
|
|
> if x == y then
|
|
> return n
|
|
> end
|
|
> end
|
|
>end
|
|
- __teliva_timestamp: original
|
|
trim:
|
|
>function trim(s)
|
|
> return s:gsub('^%s*', ''):gsub('%s*$', '')
|
|
>end
|
|
- __teliva_timestamp: original
|
|
split:
|
|
>function split(s, d)
|
|
> result = {}
|
|
> for match in (s..d):gmatch("(.-)"..d) do
|
|
> table.insert(result, match);
|
|
> end
|
|
> return result
|
|
>end
|
|
- __teliva_timestamp: original
|
|
window:
|
|
>window = curses.stdscr()
|
|
>curses.curs_set(0) -- we'll simulate our own cursor
|
|
- __teliva_timestamp: original
|
|
render:
|
|
>function render(window)
|
|
> window:clear()
|
|
> -- draw stuff to screen here
|
|
> window:attron(curses.A_BOLD)
|
|
> window:mvaddstr(1, 5, "example app")
|
|
> window:attrset(curses.A_NORMAL)
|
|
> for i=0,15 do
|
|
> window:attrset(curses.color_pair(i))
|
|
> window:mvaddstr(3+i, 5, "========================")
|
|
> end
|
|
>end
|
|
- __teliva_timestamp: original
|
|
menu:
|
|
>menu = {
|
|
> {'^u', 'clear'},
|
|
> {'^w', 'write prose to file "toot" (edit hotkey does NOT save)'},
|
|
>}
|
|
- __teliva_timestamp: original
|
|
update:
|
|
>function update(window)
|
|
> local key = curses.getch()
|
|
> 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)
|
|
> elseif key == curses.KEY_UP then
|
|
> cursor = cursor_up(prose, cursor)
|
|
> elseif key == curses.KEY_BACKSPACE then
|
|
> if cursor > 1 then
|
|
> cursor = cursor-1
|
|
> prose = prose:remove(cursor)
|
|
> end
|
|
> elseif key == 21 then -- ctrl-u
|
|
> prose = ''
|
|
> cursor = 1
|
|
> elseif key == 23 then -- ctrl-w
|
|
> local out = io.open('toot', 'w')
|
|
> out:write(prose, '\n')
|
|
> out:close()
|
|
> elseif key == 10 or (key >= 32 and key < 127) then
|
|
> prose = prose:insert(string.char(key), cursor-1)
|
|
> cursor = cursor+1
|
|
> end
|
|
>end
|
|
- __teliva_timestamp: original
|
|
init_colors:
|
|
>function init_colors()
|
|
> for i=0,7 do
|
|
> curses.init_pair(i, i, -1)
|
|
> end
|
|
> curses.init_pair(8, 7, 0)
|
|
> curses.init_pair(9, 7, 1)
|
|
> curses.init_pair(10, 7, 2)
|
|
> curses.init_pair(11, 7, 3)
|
|
> curses.init_pair(12, 7, 4)
|
|
> curses.init_pair(13, 7, 5)
|
|
> curses.init_pair(14, 7, 6)
|
|
> curses.init_pair(15, -1, 15)
|
|
>end
|
|
- __teliva_timestamp: original
|
|
main:
|
|
>function main()
|
|
> init_colors()
|
|
>
|
|
> while true do
|
|
> render(window)
|
|
> update(window)
|
|
> end
|
|
>end
|
|
- __teliva_timestamp: original
|
|
prose:
|
|
>prose = ''
|
|
- __teliva_timestamp: original
|
|
cursor:
|
|
>cursor = 1
|
|
- __teliva_timestamp: original
|
|
render_delimiter:
|
|
>function render_delimiter(window, s, pos, cursor)
|
|
> local newpos = pos
|
|
> for i=1,string.len(s) do
|
|
> if newpos == cursor and i ~= 1 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)
|
|
> window:addch(s[i])
|
|
> else
|
|
> -- most characters at cursor = render in reverse video
|
|
> window:attron(curses.A_REVERSE)
|
|
> window:addch(s[i])
|
|
> window:attroff(curses.A_REVERSE)
|
|
> end
|
|
> else
|
|
> window:addch(s[i])
|
|
> end
|
|
> newpos = newpos+1
|
|
> end
|
|
> return newpos
|
|
>end
|
|
- __teliva_timestamp: original
|
|
cursor_down:
|
|
>function cursor_down(s, idx)
|
|
> local colidx = col_within_line(s, idx)
|
|
> local newidx = skip_past_newline(s, idx)
|
|
> while true do
|
|
> if s[newidx] == '\n' then break end
|
|
> local newcolidx = col_within_line(s, newidx)
|
|
> if newcolidx == colidx then break end
|
|
> newidx = newidx+1
|
|
> end
|
|
> return newidx
|
|
>end
|
|
>
|
|
>function test_cursor_down()
|
|
> check_eq(cursor_down('abc\ndef', 1), 5, 'cursor_down: non-bottom line first char')
|
|
> check_eq(cursor_down('abc\ndef', 2), 6, 'cursor_down: non-bottom line mid char')
|
|
> check_eq(cursor_down('abc\ndef', 3), 7, 'cursor_down: non-bottom line final char')
|
|
> check_eq(cursor_down('abc\ndef', 4), 8, 'cursor_down: non-bottom line end')
|
|
> check_eq(cursor_down('abc\ndef', 5), 5, 'cursor_down: bottom line first char')
|
|
> check_eq(cursor_down('abc\ndef', 6), 6, 'cursor_down: bottom line mid char')
|
|
> check_eq(cursor_down('abc\ndef', 7), 7, 'cursor_down: bottom line final char')
|
|
>end
|
|
- __teliva_timestamp: original
|
|
skip_past_newline:
|
|
>function skip_past_newline(s, idx)
|
|
> local result = idx
|
|
> while true do
|
|
> if result >= string.len(s) then
|
|
> return idx
|
|
> end
|
|
> if s[result] == '\n' then
|
|
> return result+1
|
|
> end
|
|
> result = result+1
|
|
> end
|
|
>end
|
|
- __teliva_timestamp: original
|
|
col_within_line:
|
|
>function col_within_line(s, idx)
|
|
> if idx <= 1 then
|
|
> return idx
|
|
> end
|
|
> idx = idx-1
|
|
> local result = 1
|
|
> while idx >= 1 do
|
|
> if s[idx] == '\n' then break end
|
|
> idx = idx-1
|
|
> result=result+1
|
|
> end
|
|
> return result
|
|
>end
|
|
>
|
|
>function test_col_within_line()
|
|
> check_eq(col_within_line('', 4), 4, 'col_within_line("")')
|
|
> check_eq(col_within_line('abc\ndef', 1), 1, 'col_within_line(..., 1)')
|
|
> check_eq(col_within_line('abc\ndef', 3), 3, 'col_within_line(..., -1)')
|
|
> check_eq(col_within_line('abc\ndef', 4), 4, 'col_within_line(..., newline)')
|
|
> check_eq(col_within_line('abc\ndef', 5), 1, 'col_within_line(..., after newline)')
|
|
>end
|
|
- __teliva_timestamp: original
|
|
skip_to_start_of_previous_line:
|
|
>function skip_to_start_of_previous_line(s, idx)
|
|
> local result = idx
|
|
> -- skip to newline
|
|
> if idx == 1 then return 1 end
|
|
> result = result-1 -- just in case we start out on a newline
|
|
> while true do
|
|
> if result <= 1 then
|
|
> return idx
|
|
> end
|
|
> if s[result] == '\n' then
|
|
> result = result-1
|
|
> break
|
|
> end
|
|
> result = result-1
|
|
> end
|
|
> -- dbg(window, 'skip: '..tostring(result))
|
|
> while true do
|
|
> if result <= 1 then
|
|
> return result
|
|
> end
|
|
> if s[result] == '\n' then
|
|
> return result+1
|
|
> end
|
|
> result = result-1
|
|
> end
|
|
>end
|
|
>
|
|
>function test_skip_to_start_of_previous_line()
|
|
> check_eq(skip_to_start_of_previous_line('abc\ndef\nghi', 1), 1, 'start of previous line: first line, first char')
|
|
> check_eq(skip_to_start_of_previous_line('abc\ndef\nghi', 2), 2, 'start of previous line: first line, mid char')
|
|
> check_eq(skip_to_start_of_previous_line('abc\ndef\nghi', 3), 3, 'start of previous line: first line, final char')
|
|
> check_eq(skip_to_start_of_previous_line('abc\ndef\nghi', 4), 4, 'start of previous line: first line, newline')
|
|
> check_eq(skip_to_start_of_previous_line('abc\ndef\nghi', 5), 1, 'start of previous line: second line, first char')
|
|
> check_eq(skip_to_start_of_previous_line('abc\ndef\nghi', 6), 1, 'start of previous line: second line, mid char')
|
|
> check_eq(skip_to_start_of_previous_line('abc\ndef\nghi', 7), 1, 'start of previous line: second line, final char')
|
|
> check_eq(skip_to_start_of_previous_line('abc\ndef\nghi', 8), 1, 'start of previous line: second line, newline')
|
|
> check_eq(skip_to_start_of_previous_line('abc\ndef\nghi', 9), 5, 'start of previous line: final line, first char')
|
|
> check_eq(skip_to_start_of_previous_line('abc\ndef\nghi', 10), 5, 'start of previous line: final line, mid char')
|
|
> check_eq(skip_to_start_of_previous_line('abc\ndef\nghi', 11), 5, 'start of previous line: final line, final char')
|
|
> check_eq(skip_to_start_of_previous_line('abc\ndef\nghi', 12), 5, 'start of previous line: end of file')
|
|
>
|
|
> check_eq(skip_to_start_of_previous_line('abc\n\nghi', 7), 5, 'start of previous line: to empty line')
|
|
> check_eq(skip_to_start_of_previous_line('abc\nd\nghi', 8), 5, 'start of previous line: to shorter line')
|
|
>end
|
|
- __teliva_timestamp: original
|
|
cursor_up:
|
|
>function cursor_up(s, idx)
|
|
> if idx <= 1 then return idx end
|
|
> -- check column within current line, then go to start of previous line, then count off columns there
|
|
> local colidx = col_within_line(s, idx)
|
|
> local newidx = skip_to_start_of_previous_line(s, idx)
|
|
> if newidx == idx then return idx end
|
|
> if s[newidx] == '\n' then return newidx end
|
|
> for i=2,colidx do -- we're already starting at col 1
|
|
> if newidx >= string.len(s) then break end
|
|
> if s[newidx] == '\n' then break end
|
|
> newidx = newidx+1
|
|
> end
|
|
> return newidx
|
|
>end
|
|
>
|
|
>function test_cursor_up()
|
|
> check_eq(cursor_up('abc\ndef', 1), 1, 'cursor_up: top line first char')
|
|
> check_eq(cursor_up('abc\ndef', 2), 2, 'cursor_up: top line mid char')
|
|
> check_eq(cursor_up('abc\ndef', 3), 3, 'cursor_up: top line final char')
|
|
> check_eq(cursor_up('abc\ndef', 4), 4, 'cursor_up: top line end')
|
|
> check_eq(cursor_up('abc\ndef', 5), 1, 'cursor_up: non-top line first char')
|
|
> check_eq(cursor_up('abc\ndef', 6), 2, 'cursor_up: non-top line mid char')
|
|
> check_eq(cursor_up('abc\ndef', 7), 3, 'cursor_up: non-top line final char')
|
|
> check_eq(cursor_up('abc\ndef\n', 8), 4, 'cursor_up: non-top line end')
|
|
> check_eq(cursor_up('ab\ndef\n', 7), 3, 'cursor_up: to shorter line')
|
|
> -- idx that's too high for s not working; let's see if that matters
|
|
>end
|
|
- __teliva_timestamp: original
|
|
render:
|
|
>function render(window)
|
|
> window:clear()
|
|
> debugy = 5
|
|
> local toots = split(prose, '\n\n===\n\n')
|
|
> pos = 1
|
|
> debugy = 5
|
|
> for i, toot in ipairs(toots) do
|
|
> if i > 1 then
|
|
> pos = render_delimiter(window, '\n\n===\n\n', pos, cursor)
|
|
> end
|
|
> pos = render_text(window, toot, pos, cursor)
|
|
> print('')
|
|
> window:attron(curses.A_BOLD)
|
|
> window:addstr(string.len(toot))
|
|
> window:attroff(curses.A_BOLD)
|
|
> end
|
|
> curses.refresh()
|
|
>end
|
|
- __teliva_timestamp: original
|
|
render_text:
|
|
>-- https://gankra.github.io/blah/text-hates-you
|
|
>-- https://lord.io/text-editing-hates-you-too
|
|
>
|
|
>-- manual tests:
|
|
>-- cursor on some character
|
|
>-- cursor on (within) '\n\n===\n\n' delimiter (delimiter is hardcoded; things may break if you change it)
|
|
>-- cursor at end of each line
|
|
>-- render digits
|
|
>
|
|
>-- positions serve two purposes:
|
|
>-- character to index into prose
|
|
>-- cursor-printing
|
|
>
|
|
>-- sequence of stories
|
|
>-- focus on rendering a single piece of text, first get that rock-solid
|
|
>-- split prose into toots, manage transitions between toots in response to cursor movements
|
|
>-- cursor movement: left/right vs up/down
|
|
>
|
|
>-- what is the ideal representation?
|
|
>-- prose + cursor has issues in multi-toot context. when to display cursor?
|
|
>function render_text(window, s, pos, cursor)
|
|
> local newpos = pos
|
|
>--? dbg(window, '--')
|
|
> for i=1,string.len(s) do
|
|
>--? dbg(window, tostring(newpos)..' '..tostring(string.byte(s[i])))
|
|
> if newpos == cursor then
|
|
>--? dbg(window, 'cursor: '..tostring(cursor))
|
|
> 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)
|
|
> window:addstr(s[i])
|
|
> 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
|
|
> window:addstr(s[i])
|
|
> end
|
|
> newpos = newpos+1
|
|
> end
|
|
> if newpos == cursor then
|
|
> window:attron(curses.A_REVERSE)
|
|
> window:addch(' ')
|
|
> window:attroff(curses.A_REVERSE)
|
|
> end
|
|
> return newpos
|
|
>end
|