1 # .tlv file generated by https://github.com/akkartik/teliva
2 # You may edit it if you are careful; however, you may see cryptic errors if you
3 # violate Teliva's assumptions.
5 # .tlv files are representations of Teliva programs. Teliva programs consist of
6 # sequences of definitions. Each definition is a table of key/value pairs. Keys
7 # and values are both strings.
9 # Lines in .tlv files always follow exactly one of the following forms:
10 # - comment lines at the top of the file starting with '#' at column 0
11 # - beginnings of definitions starting with '- ' at column 0, followed by a
13 # - key/value pairs consisting of ' ' at column 0, containing either a
14 # spaceless value on the same line, or a multi-line value
15 # - multiline values indented by more than 2 spaces, starting with a '>'
17 # If these constraints are violated, Teliva may unceremoniously crash. Please
18 # report bugs at http://akkartik.name/contact
19 - __teliva_timestamp: original
21 >-- some string helpers from http://lua-users.org/wiki/StringIndexing
23 >-- index characters using []
24 >getmetatable('').__index = function(str,i)
25 > if type(i) == 'number' then
32 >-- ranges using (), selected bytes using {}
33 >getmetatable('').__call = function(str,i,j)
34 > if type(i)~='table' then
38 > for k,v in ipairs(i) do
41 > return table.concat(t)
45 >-- iterate over an ordered sequence
47 > if type(x) == 'string' then
48 > return x:gmatch('.')
54 >-- insert within string
55 >function string.insert(str1, str2, pos)
56 > return str1:sub(1,pos)..str2..str1:sub(pos+1)
59 >function string.remove(s, pos)
60 > return s:sub(1,pos-1)..s:sub(pos+1)
63 >function string.pos(s, sub)
64 > return string.find(s, sub, 1, true) -- plain=true to disable regular expressions
67 >-- TODO: backport utf-8 support from Lua 5.3
68 - __teliva_timestamp: original
71 - __teliva_timestamp: original
73 >-- helper for debug by print; overlay debug information towards the right
74 >-- reset debugy every time you refresh screen
75 >function dbg(window, s)
78 > oldy, oldx = window:getyx()
79 > window:mvaddstr(debugy, 60, s)
81 > window:mvaddstr(oldy, oldx, '')
83 - __teliva_timestamp: original
85 >function check(x, msg)
90 > print(' '..str(x)..' is false/nil')
91 > teliva_num_test_failures = teliva_num_test_failures + 1
92 > -- overlay first test failure on editors
93 > if teliva_first_failure == nil then
94 > teliva_first_failure = msg
98 - __teliva_timestamp: original
100 >function check_eq(x, expected, msg)
101 > if eq(x, expected) then
105 > print(' expected '..str(expected)..' but got '..str(x))
106 > teliva_num_test_failures = teliva_num_test_failures + 1
107 > -- overlay first test failure on editors
108 > if teliva_first_failure == nil then
109 > teliva_first_failure = msg
113 - __teliva_timestamp: original
116 > if type(a) ~= type(b) then return false end
117 > if type(a) == 'table' then
118 > if #a ~= #b then return false end
119 > for k, v in pairs(a) do
124 > for k, v in pairs(b) do
133 - __teliva_timestamp: original
136 >-- slow; used only for debugging
138 > if type(x) == 'table' then
140 > result = result..#x..'{'
141 > for k, v in pairs(x) do
142 > result = result..str(k)..'='..str(v)..', '
144 > result = result..'}'
146 > elseif type(x) == 'string' then
151 - __teliva_timestamp: original
153 >function find_index(arr, x)
154 > for n, y in ipairs(arr) do
160 - __teliva_timestamp: original
163 > return s:gsub('^%s*', ''):gsub('%s*$', '')
165 - __teliva_timestamp: original
167 >function split(s, d)
169 > for match in (s..d):gmatch("(.-)"..d) do
170 > table.insert(result, match);
174 - __teliva_timestamp: original
176 >function clear(lines)
177 > while #lines > 0 do
178 > table.remove(lines)
181 - __teliva_timestamp: original
183 >Window = curses.stdscr()
184 - __teliva_timestamp: original
186 >function render_line(window, y, line)
187 > window:mvaddstr(y, 0, '')
188 > for i=1,line:len() do
189 > window:addstr(line[i])
192 - __teliva_timestamp: original
194 >function render_link(window, y, line)
195 > local rendered_line = line:gsub('=>%s*%S*%s*', '')
196 > if trim(rendered_line) == '' then
197 > rendered_line = line
199 > render_line(window, y, rendered_line)
201 - __teliva_timestamp: original
207 > source=false, -- show source (link urls, etc.)
209 - __teliva_timestamp: original
211 >function render_page(window)
213 > window:attron(curses.color_pair(6))
215 > window:attroff(curses.color_pair(6))
217 >--? dbg(window, state.highlight_index)
218 > for i, line in pairs(state.lines) do
219 > if not state.source and line:find('=> ') == 1 then
220 > if state.highlight_index == 0 or i == state.highlight_index then
221 > -- highlighted link
222 > state.highlight_index = i -- TODO: ugly state update while rendering, just for first render after gemini_get
223 > window:attron(curses.A_REVERSE)
224 > render_link(window, y, line)
225 > window:attroff(curses.A_REVERSE)
228 > window:attron(curses.A_BOLD)
229 > render_link(window, y, line)
230 > window:attroff(curses.A_BOLD)
234 > render_line(window, y, line)
239 - __teliva_timestamp: original
241 >function render(window, lines)
243 > render_page(window, lines)
247 - __teliva_timestamp: original
250 > {'Enter', 'go to highlight'},
252 > {'^g', 'enter url'},
253 > {'^u', 'view source'},
255 - __teliva_timestamp: original
257 >function edit_line(window)
260 > local screen_rows, screen_cols = window:getmaxyx()
262 > {'enter', 'submit'},
267 > window:mvaddstr(screen_rows-1, 9, '')
269 > window:mvaddstr(screen_rows-1, 9, result)
270 > window:attron(curses.A_REVERSE)
271 > -- window:refresh()
272 > local key = window:getch()
273 > window:attrset(curses.A_NORMAL)
274 > if key >= 32 and key < 127 then
275 > local screen_rows, screen_cols = window:getmaxyx()
276 > if #result < screen_cols then
277 > result = result:insert(string.char(key), cursor-1)
280 > elseif key == curses.KEY_LEFT then
284 > elseif key == curses.KEY_RIGHT then
285 > if cursor <= #result then
288 > elseif key == curses.KEY_BACKSPACE then
291 > result = result:remove(cursor)
293 > elseif key == 21 then -- ctrl-u
296 > elseif key == 10 then -- enter
298 > elseif key == 7 then -- ctrl-g
303 - __teliva_timestamp: original
305 >function is_link(line)
306 > return line:find('=>%s*%S*%s*') == 1
308 - __teliva_timestamp: original
310 >function next_link()
311 > local new_index = state.highlight_index
313 > new_index = new_index+1
314 > if new_index > #state.lines then return end
315 > if is_link(state.lines[new_index]) then break end
317 > state.highlight_index = new_index
319 - __teliva_timestamp: original
321 >function previous_link()
322 > local new_index = state.highlight_index
324 > new_index = new_index - 1
325 > if new_index < 1 then return end
326 > if is_link(state.lines[new_index]) then break end
328 > state.highlight_index = new_index
330 - __teliva_timestamp: original
332 >function update(window)
333 > local key = window:getch()
334 > local screen_rows, screen_cols = window:getmaxyx()
335 > if key == curses.KEY_DOWN then
337 > elseif key == curses.KEY_UP then
339 > elseif key == curses.KEY_LEFT then
340 > if #state.history > 1 then
341 > table.remove(state.history)
342 > gemini_get(table.remove(state.history))
344 > elseif key == 21 then -- ctrl-u
345 > state.source = not state.source
346 > elseif key == 10 then -- enter
347 > local s, e, new_url = string.find(state.lines[state.highlight_index], '=>%s*(%S*)')
348 > gemini_get(url.absolute(state.url, new_url))
349 > elseif key == 7 then -- ctrl-g
350 > window:mvaddstr(screen_rows-2, 0, '')
352 > window:mvaddstr(screen_rows-1, 0, '')
354 > window:mvaddstr(screen_rows-1, 5, 'go: ')
356 > local old_menu = menu
357 > local new_url = edit_line(window)
360 > state.url = new_url
361 > gemini_get(new_url)
366 - __teliva_timestamp: original
368 >function init_colors()
370 > curses.init_pair(i, i, -1)
372 > curses.init_pair(8, 7, 0)
373 > curses.init_pair(9, 7, 1)
374 > curses.init_pair(10, 7, 2)
375 > curses.init_pair(11, 7, 3)
376 > curses.init_pair(12, 7, 4)
377 > curses.init_pair(13, 7, 5)
378 > curses.init_pair(14, 7, 6)
379 > curses.init_pair(15, -1, 15)
381 - __teliva_timestamp: original
391 > lines = gemini_get(state.url)
394 > render(Window, lines)
395 > update(Window, lines)
398 - __teliva_timestamp: original
400 >function http_get(url)
401 > -- https://stackoverflow.com/questions/42445423/luasocket-serveraccept-timeout-tcp
402 > local parsed_url = socket.url.parse(url)
403 > local tcp = socket.tcp()
404 > tcp:connect(parsed_url.host, 80)
405 > tcp:send('GET / HTTP/1.1\r\n')
406 > -- http requires the Host header
407 > tcp:send(string.format('Host: %s\r\n', parsed_url.host))
409 > -- tcp:receive('*a') doesn't seem to detect when a request is done
410 > -- so we have to manage the size of the expected response
413 > local s, status = tcp:receive()
414 > if s == nil then break end
415 > if s == '' then break end
416 > local header, value = s:match('(.-): (.*)')
417 > if header == nil then
420 > headers[header:lower()] = value
421 > print(header, value)
424 > local bytes_remaining = tonumber(headers['content-length'])
427 > local s, status = tcp:receive(bytes_remaining)
428 > if s == nil then break end
430 > bytes_remaining = bytes_remaining - s:len()
431 > if bytes_remaining <= 0 then break end
435 - __teliva_timestamp: original
437 >-- http://notebook.kulchenko.com/programming/https-ssl-calls-with-lua-and-luasec
438 >function https_get(url)
439 > local parsed_url = socket.url.parse(url)
443 > verify = 'none', -- I don't know what I'm doing
446 > local conn = socket.tcp()
447 > conn:connect(parsed_url.host, parsed_url.port or 443)
448 > conn, err = ssl.wrap(conn, params)
449 > if conn == nil then
455 > conn:send(url .. "\r\n")
456 > local line, err = conn:receive()
459 - __teliva_timestamp: original
461 >function parse_gemini_body(conn, type)
462 > if type == 'text/gemini' then
464 > local line, err = conn:receive()
465 > if line == nil then break end
466 > table.insert(state.lines, line)
468 > elseif type:sub(1, 5) == 'text/' then
470 > local line, err = conn:receive()
471 > if line == nil then break end
472 > table.insert(state.lines, line)
476 - __teliva_timestamp: original
478 >-- http://notebook.kulchenko.com/programming/https-ssl-calls-with-lua-and-luasec
479 >-- https://tildegit.org/solderpunk/gemini-demo-2
480 >-- returns an array of lines, containing either the body or just an error
481 >function gemini_get(url)
482 > if url:find("://") == nil then
483 > url = "gemini://" .. url
485 > local parsed_url = socket.url.parse(url)
489 > verify = 'none', -- I don't know what I'm doing
492 > local conn = socket.tcp()
493 > local conn2, err = conn:connect(parsed_url.host, parsed_url.port or 1965)
495 > state.highlight_index = 0 -- highlighted link not computed yet
496 > if conn2 == nil then
497 > table.insert(state.lines, err)
500 > conn, err = ssl.wrap(conn, params)
501 > if conn == nil then
502 > table.insert(state.lines, err)
506 > conn:send(url .. "\r\n")
507 > local line, err = conn:receive()
508 > if line == nil then
509 > table.insert(state.lines, err)
512 > local status, meta = line:match("(%S+) (%S+)")
513 > if status[1] == '2' then
514 > parse_gemini_body(conn, meta)
516 > table.insert(state.history, url)
517 > elseif status[1] == '3' then
518 > gemini_get(socket.url.absolute(url, meta))
519 > elseif status[1] == '4' or line[1] == '5' then
520 > table.insert(state.lines, 'Error: '..meta)
522 > table.insert(state.lines, 'invalid response from server: '..line)
525 - __teliva_timestamp:
526 >Thu Feb 17 20:04:42 2022
528 >A bare-bones browser for the Gemini protocol
530 >https://gemini.circumlunar.space
532 >A couple of good pages to try it out with:
533 > $ src/teliva gemini.tlv gemini.circumlunar.space
534 > $ src/teliva gemini.tlv gemini.conman.org
535 > $ src/teliva gemini.tlv gemini.susa.net/cgi-bin/links.lua # shows 20 random links from Gemini space