about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorKartik K. Agaram <vc@akkartik.com>2022-04-23 00:31:47 -0700
committerKartik K. Agaram <vc@akkartik.com>2022-04-23 00:31:47 -0700
commitf7636967c54153d10e6e5524a29cf4405fd2e6e7 (patch)
treed9cc8ef08def9a97680838550c7e6c2f26ce4e00
parent54e727641c067201bbd0e7f6be7459fce4239a3f (diff)
downloadteliva-f7636967c54153d10e6e5524a29cf4405fd2e6e7.tar.gz
smol.tlv: start of a browser for the small web
-rw-r--r--smol.tlv1581
1 files changed, 1581 insertions, 0 deletions
diff --git a/smol.tlv b/smol.tlv
new file mode 100644
index 0000000..ae0107b
--- /dev/null
+++ b/smol.tlv
@@ -0,0 +1,1581 @@
+# .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 = {}
+- __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
+  sep:
+    >-- horizontal separator
+    >function sep(window)
+    >  local y, _ = window:getyx()
+    >  window:mvaddstr(y+1, 0, '')
+    >  local _, cols = window:getmaxyx()
+    >  for col=1,cols do
+    >    window:addstr('_')
+    >  end
+    >end
+- __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
+    >  window:refresh()
+    >end
+- __teliva_timestamp: original
+  update:
+    >function update(window)
+    >  local key = window:getch()
+    >  -- process key here
+    >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
+  doc:blurb:
+    >To show a brief description of the app on the 'big picture' screen, put the text in a special buffer called 'doc:blurb'.
+    >
+    >You can also override the default big picture screen entirely by creating a buffer called 'doc:main'.
+- __teliva_timestamp:
+    >Mon Apr 11 21:47:50 2022
+  main:
+    >function main()
+    >  init_colors()
+    >  read_feeds()
+    >
+    >  while true do
+    >    render(Window)
+    >    update(Window)
+    >  end
+    >end
+- __teliva_timestamp:
+    >Mon Apr 11 21:49:18 2022
+  read_feeds:
+    >function read_feeds()
+    >  local f = start_reading('feeds')
+    >  while true
+    >    local line = f.read()
+    >    if line == nil then break end
+    >  end
+    >end
+- __teliva_timestamp:
+    >Mon Apr 11 21:52:26 2022
+  read_feeds:
+    >function read_feeds()
+    >  local f = start_reading('feeds')
+    >  Feeds = {}
+    >  while true
+    >    local feed = f.read()
+    >    if feed == nil then break end
+    >    table.insert(Feeds, feed)
+    >  end
+    >end
+- __teliva_timestamp:
+    >Mon Apr 11 21:52:33 2022
+  Feeds:
+    >Feeds = {}
+- __teliva_timestamp:
+    >Mon Apr 11 21:52:59 2022
+  render:
+    >function render(window)
+    >  window:clear()
+    >  -- draw stuff to screen here
+    >  for i, feed in ipairs(Feeds) do
+    >    print(feed)
+    >  end
+    >  window:refresh()
+    >end
+- __teliva_timestamp:
+    >Mon Apr 11 21:53:11 2022
+  read_feeds:
+    >function read_feeds()
+    >  local f = start_reading('feeds')
+    >  Feeds = {}
+    >  while true do
+    >    local feed = f.read()
+    >    if feed == nil then break end
+    >    table.insert(Feeds, feed)
+    >  end
+    >end
+- __teliva_timestamp:
+    >Mon Apr 11 21:53:22 2022
+  read_feeds:
+    >function read_feeds()
+    >  local f = start_reading(nil, 'feeds')
+    >  Feeds = {}
+    >  while true do
+    >    local feed = f.read()
+    >    if feed == nil then break end
+    >    table.insert(Feeds, feed)
+    >  end
+    >end
+- __teliva_timestamp:
+    >Mon Apr 11 21:53:39 2022
+  read_feeds:
+    >function read_feeds()
+    >  local f = start_reading(nil, 'feeds')
+    >  if f == nil then return end
+    >  Feeds = {}
+    >  while true do
+    >    local feed = f.read()
+    >    if feed == nil then break end
+    >    table.insert(Feeds, feed)
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 22:39:15 2022
+  menu:
+    >-- To show app-specific hotkeys in the menu bar, add hotkey/command
+    >-- arrays of strings to the menu array.
+    >menu = {
+    >  {'^r', 'refresh all feeds'},
+    >}
+- __teliva_timestamp:
+    >Fri Apr 22 22:39:45 2022
+  update:
+    >function update(window)
+    >  local key = window:getch()
+    >  if key == 18 then  -- ctrl-r
+    >    print('aaa')
+    >    window:getch()
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 22:40:02 2022
+  update:
+    >function update(window)
+    >  local key = window:getch()
+    >  if key == 18 then  -- ctrl-r
+    >    download_feeds()
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 22:40:27 2022
+  update:
+    >function update(window)
+    >  local key = window:getch()
+    >  if key == 18 then  -- ctrl-r
+    >    reload_feeds()
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 22:40:37 2022
+  menu:
+    >-- To show app-specific hotkeys in the menu bar, add hotkey/command
+    >-- arrays of strings to the menu array.
+    >menu = {
+    >  {'^r', 'reload feeds'},
+    >}
+- __teliva_timestamp:
+    >Fri Apr 22 22:41:00 2022
+  reload_feeds:
+    >function reload_feeds()
+    >  print('aaa')
+    >  Window:getch()
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 22:41:37 2022
+  reload_feeds:
+    >function reload_feeds()
+    >  for _, feed in ipairs(Feeds) do
+    >    print(feed)
+    >  end
+    >  Window:getch()
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 22:43:36 2022
+  reload_feeds:
+    >function reload_feeds()
+    >  for _, feed in ipairs(Feeds) do
+    >    local response, status, headers = http.request(feed)
+    >    Window:clear()
+    >    print(response)
+    >    Window:getch()
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 22:55:25 2022
+  Items:
+    >Items = {}
+- __teliva_timestamp:
+    >Fri Apr 22 22:57:54 2022
+  reload_feeds:
+    >function reload_feeds()
+    >  for _, feed in ipairs(Feeds) do
+    >    local response, status, headers = http.request(feed)
+    >    print(type(status))
+    >--?     parse_items(response)
+    >    Window:clear()
+    >    print(response)
+    >--?     print(feed, #Items)
+    >    Window:getch()
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 22:58:15 2022
+  reload_feeds:
+    >function reload_feeds()
+    >  for _, feed in ipairs(Feeds) do
+    >    local response, status, headers = http.request(feed)
+    >    Window:clear()
+    >    print(type(status))
+    >--?     parse_items(response)
+    >    print(response)
+    >--?     print(feed, #Items)
+    >    Window:getch()
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 22:59:10 2022
+  reload_feeds:
+    >function reload_feeds()
+    >  for _, feed in ipairs(Feeds) do
+    >    local response, status, headers = http.request(feed)
+    >    if response == 200 then
+    >      parse_items(response)
+    >      Window:clear()
+    >      print(response)
+    >--?       print(feed, #Items)
+    >      Window:getch()
+    >    end
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 22:59:38 2022
+  reload_feeds:
+    >function reload_feeds()
+    >  for _, feed in ipairs(Feeds) do
+    >    local response, status, headers = http.request(feed)
+    >    print(status)
+    >    if status == 200 then
+    >      parse_items(response)
+    >      Window:clear()
+    >      print(response)
+    >--?       print(feed, #Items)
+    >      Window:getch()
+    >    end
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:01:46 2022
+  parse_items:
+    >function parse_items(lines)
+    >  for line in lines:gmatch('[^\n]*') do
+    >    print('^', line, '$')
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:02:26 2022
+  reload_feeds:
+    >function reload_feeds()
+    >  for _, feed in ipairs(Feeds) do
+    >    local response, status, headers = http.request(feed)
+    >    print(status)
+    >    if status == 200 then
+    >      Window:clear()
+    >      parse_items(response)
+    >      print(response)
+    >--?       print(feed, #Items)
+    >      Window:getch()
+    >    end
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:02:53 2022
+  parse_items:
+    >function parse_items(lines)
+    >  for line in lines:gmatch('[^\n]+') do
+    >    print('^', line, '$')
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:06:14 2022
+  parse_items:
+    >function parse_items(lines)
+    >  for line in lines:gmatch('[^\n]+') do
+    >    local t, text = line:gmatch('([^%s]+)\t(.*)')
+    >    print('^', t, '--', text, '$')
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:06:29 2022
+  parse_items:
+    >function parse_items(lines)
+    >  for line in lines:gmatch('[^\n]+') do
+    >    local t, text = line:match('([^%s]+)\t(.*)')
+    >    print('^', t, '--', text, '$')
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:07:06 2022
+  reload_feeds:
+    >function reload_feeds()
+    >  for _, feed in ipairs(Feeds) do
+    >    local response, status, headers = http.request(feed)
+    >    print(status)
+    >    if status == 200 then
+    >      Window:clear()
+    >      parse_items(feed, response)
+    >      print(response)
+    >--?       print(feed, #Items)
+    >      Window:getch()
+    >    end
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:10:44 2022
+  parse_items:
+    >function parse_items(feed, lines)
+    >  if Items[feed] == nil then Items[feed] = {} end
+    >  for line in lines:gmatch('[^\n]+') do
+    >    local t, text = line:match('([^%s]+)\t(.*)')
+    >    table.insert(Items[feed], {time=parse_time(t), text=text})
+    >  end
+    >  table.sort(Items[feed], function(a, b) return a.time < b.time end)
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:11:24 2022
+  reload_feeds:
+    >function reload_feeds()
+    >  for _, feed in ipairs(Feeds) do
+    >    local response, status, headers = http.request(feed)
+    >    print(status)
+    >    if status == 200 then
+    >      Window:clear()
+    >      parse_items(feed, response)
+    >      for _, x in ipairs(Items) do
+    >        print(x.time, x.text)
+    >--?       print(feed, #Items)
+    >      Window:getch()
+    >    end
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:11:36 2022
+  reload_feeds:
+    >function reload_feeds()
+    >  for _, feed in ipairs(Feeds) do
+    >    local response, status, headers = http.request(feed)
+    >    print(status)
+    >    if status == 200 then
+    >      Window:clear()
+    >      parse_items(feed, response)
+    >      for _, x in ipairs(Items) do
+    >        print(x.time, x.text)
+    >      end
+    >--?       print(feed, #Items)
+    >      Window:getch()
+    >    end
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:12:05 2022
+  parse_time:
+    >function parse_time(t)
+    >  return t
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:13:02 2022
+  reload_feeds:
+    >function reload_feeds()
+    >  for _, feed in ipairs(Feeds) do
+    >    local response, status, headers = http.request(feed)
+    >    print(status)
+    >    if status == 200 then
+    >      Window:clear()
+    >      parse_items(feed, response)
+    >      for _, x in ipairs(Items[feed]) do
+    >        print(x.time, x.text)
+    >      end
+    >--?       print(feed, #Items)
+    >      Window:getch()
+    >    end
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:13:50 2022
+  parse_items:
+    >function parse_items(feed, lines)
+    >  if Items[feed] == nil then Items[feed] = {} end
+    >  for line in lines:gmatch('[^\n]+') do
+    >    local t, text = line:match('([^%s]+)\t(.*)')
+    >    print(t, text)
+    >    table.insert(Items[feed], {time=parse_time(t), text=text})
+    >  end
+    >  table.sort(Items[feed], function(a, b) return a.time < b.time end)
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:14:36 2022
+  parse_items:
+    >function parse_items(feed, lines)
+    >  if Items[feed] == nil then Items[feed] = {} end
+    >  for line in lines:gmatch('[^\n]+') do
+    >    print(line)
+    >    local t, text = line:match('([^%s]+)\t(.*)')
+    >    print(t, text)
+    >    table.insert(Items[feed], {time=parse_time(t), text=text})
+    >  end
+    >  table.sort(Items[feed], function(a, b) return a.time < b.time end)
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:19:20 2022
+  parse_items:
+    >function parse_items(feed, lines)
+    >  if Items[feed] == nil then Items[feed] = {} end
+    >  for line in lines:gmatch('[^\n]+') do
+    >    print(line)
+    >    local t, status = line:match('([^%s]+)\t(.*)')
+    >    if t and status then
+    >      print(t, text)
+    >      table.insert(Items[feed], {time=parse_time(t), text=text})
+    >    end
+    >  end
+    >  table.sort(Items[feed], function(a, b) return a.time < b.time end)
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:19:44 2022
+  parse_items:
+    >function parse_items(feed, lines)
+    >  if Items[feed] == nil then Items[feed] = {} end
+    >  for line in lines:gmatch('[^\n]+') do
+    >    print(line)
+    >    local t, status = line:match('([^%s]+)\t(.*)')
+    >    if t and status then
+    >      print(t, status)
+    >      table.insert(Items[feed], {time=parse_time(t), status=status})
+    >    end
+    >  end
+    >  table.sort(Items[feed], function(a, b) return a.time < b.time end)
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:19:56 2022
+  reload_feeds:
+    >function reload_feeds()
+    >  for _, feed in ipairs(Feeds) do
+    >    local response, status, headers = http.request(feed)
+    >    print(status)
+    >    if status == 200 then
+    >      Window:clear()
+    >      parse_items(feed, response)
+    >      for _, x in ipairs(Items[feed]) do
+    >        print(x.time, x.status)
+    >      end
+    >--?       print(feed, #Items)
+    >      Window:getch()
+    >    end
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:21:40 2022
+  parse_items:
+    >function parse_items(feed, lines)
+    >  if Items[feed] == nil then Items[feed] = {} end
+    >  for line in lines:gmatch('[^\n]+') do
+    >    local t, status = line:match('([^%s]+)\t(.*)')
+    >    if t and status then
+    >      table.insert(Items[feed], {time=parse_time(t), status=status})
+    >    end
+    >  end
+    >  table.sort(Items[feed], function(a, b) return a.time < b.time end)
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:23:29 2022
+  reload_feeds:
+    >function reload_feeds()
+    >  for _, feed in ipairs(Feeds) do
+    >    local response, status, headers = http.request(feed)
+    >    print(status)
+    >    if status == 200 then
+    >      Window:clear()
+    >      parse_items(feed, response)
+    >      local all_items = {}
+    >      for feed, items in pairs(Items) do
+    >        for _, item in ipairs(items) do
+    >          table.insert(all_items, item)
+    >        end
+    >      end
+    >      table.sort(all_items, function(a, b) return a.time < b.time end)
+    >      for _, item in ipairs(all_items) do
+    >        print(x.time, x.status)
+    >      end
+    >--?       print(feed, #Items)
+    >      Window:getch()
+    >    end
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:23:48 2022
+  reload_feeds:
+    >function reload_feeds()
+    >  for _, feed in ipairs(Feeds) do
+    >    local response, status, headers = http.request(feed)
+    >    print(status)
+    >    if status == 200 then
+    >      Window:clear()
+    >      parse_items(feed, response)
+    >      local all_items = {}
+    >      for feed, items in pairs(Items) do
+    >        for _, item in ipairs(items) do
+    >          table.insert(all_items, item)
+    >        end
+    >      end
+    >      table.sort(all_items, function(a, b) return a.time < b.time end)
+    >      for _, x in ipairs(all_items) do
+    >        print(x.time, x.status)
+    >      end
+    >--?       print(feed, #Items)
+    >      Window:getch()
+    >    end
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:24:28 2022
+  parse_items:
+    >function parse_items(feed, lines)
+    >  if Items[feed] == nil then Items[feed] = {} end
+    >  for line in lines:gmatch('[^\n]+') do
+    >    local t, status = line:match('([^%s]+)\t(.*)')
+    >    if t and status then
+    >      table.insert(Items[feed], {feed=feed, time=parse_time(t), status=status})
+    >    end
+    >  end
+    >  table.sort(Items[feed], function(a, b) return a.time < b.time end)
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:25:34 2022
+  reload_feeds:
+    >function reload_feeds()
+    >  for _, feed in ipairs(Feeds) do
+    >    local response, status, headers = http.request(feed)
+    >    print(status)
+    >    if status == 200 then
+    >      Window:clear()
+    >      parse_items(feed, response)
+    >      local all_items = {}
+    >      for feed, items in pairs(Items) do
+    >        for _, item in ipairs(items) do
+    >          table.insert(all_items, item)
+    >        end
+    >      end
+    >      table.sort(all_items, function(a, b) return a.time < b.time end)
+    >      for _, x in ipairs(all_items) do
+    >        print(x.feed, x.time, x.status)
+    >      end
+    >--?       print(feed, #Items)
+    >      Window:getch()
+    >    end
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:27:08 2022
+  reload_feeds:
+    >function reload_feeds()
+    >  for _, feed in ipairs(Feeds) do
+    >    local response, status, headers = http.request(feed)
+    >    print(status)
+    >    if status == 200 then
+    >      Window:clear()
+    >      parse_items(feed, response)
+    >      local all_items = {}
+    >      for feed, items in pairs(Items) do
+    >        for _, item in ipairs(items) do
+    >          table.insert(all_items, item)
+    >        end
+    >      end
+    >      table.sort(all_items, function(a, b) return a.time < b.time end)
+    >      for _, x in ipairs(all_items) do
+    >        print(x.feed, x.time, x.status)
+    >      end
+    >      Window:getch()
+    >    end
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:29:49 2022
+  parse_items:
+    >function parse_items(feed, lines)
+    >  if Items[feed] == nil then Items[feed] = {} end
+    >  for line in lines:gmatch('[^\n]+') do
+    >    local t, status = line:match('([^%s]+)\t(.*)')
+    >    if t and status then
+    >      table.insert(Items[feed], {feed=feed, time=t, parsed_time=parse_time(t), status=status})
+    >    end
+    >  end
+    >  table.sort(Items[feed], compare_parsed_time)
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:31:16 2022
+  compare_parsed_time:
+    >function compare_parsed_time(a, b)
+    >  local a, b = a.parsed_time, b.parsed_time
+    >  if a.
+- __teliva_timestamp:
+    >Fri Apr 22 23:34:46 2022
+  parse_time:
+    >function parse_time(t)
+    >  local year, month, day, hour, min, sec, patt_end = str:match("^(%d%d%d%d)%-(%d%d)%-(%d%d)[Tt](%d%d%.?%d*):(%d%d):(%d%d)()")
+    >  return { year=year, month=month, day=day, hour=hour, min=min, sec=sec }
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:36:13 2022
+  compare_parsed_time:
+    >function compare_parsed_time(a, b)
+    >  local a, b = a.parsed_time, b.parsed_time
+    >  if a.year ~= b.year then return a.year < b.year end
+    >  if a.month ~= b.month then return a.month < b.month end
+    >  if a.day ~= b.day then return a.day < b.day end
+    >  if a.hour ~= b.hour then return a.hour < b.hour end
+    >  if a.min ~= b.min then return a.min < b.min end
+    >  return a.sec < b.sec
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:36:35 2022
+  parse_time:
+    >function parse_time(t)
+    >  local year, month, day, hour, min, sec, patt_end = t:match("^(%d%d%d%d)%-(%d%d)%-(%d%d)[Tt](%d%d%.?%d*):(%d%d):(%d%d)()")
+    >  return { year=year, month=month, day=day, hour=hour, min=min, sec=sec }
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:39:20 2022
+  parse_time:
+    >function parse_time(t)
+    >  local year, month, day, hour, min, sec, tz_hour, tz_min = t:match("^(%d%d%d%d)%-(%d%d)%-(%d%d)[Tt](%d%d%.?%d*):(%d%d):(%d%d)[+-](%d%d):(%d%d)")
+    >  return { year=year, month=month, day=day, hour=hour, min=min, sec=sec, tz_hour=tz_hour, tz_min=tz_min }
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:47:55 2022
+  parse_time:
+    >function parse_time(t)
+    >  local year, month, day, hour, min, sec, tz_op, tz_hour, tz_min = t:match("^(%d%d%d%d)%-(%d%d)%-(%d%d)[Tt](%d%d%.?%d*):(%d%d):(%d%d)([+-])(%d%d):(%d%d)")
+    >  if tz_op == '+' then
+    >    hour = int(hour) + int(tz_hour)
+    >    min = int(min) + int(tz_min)
+    >  else
+    >    hour = int(hour) - int(tz_hour)
+    >    min = int(min) - int(tz_min)
+    >  end
+    >  return os.time{ year=int(year), month=int(month), day=int(day), hour=int(hour), min=int(min), sec=int(sec) }
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:48:46 2022
+  compare_parsed_time:
+    >function compare_parsed_time(a, b)
+    >  return a.parsed_time < b.parsed_time
+    >end
+    >
+    >-- unused
+    >function compare_parsed_time2(a, b)
+    >  local a, b = a.parsed_time, b.parsed_time
+    >  if a.year ~= b.year then return a.year < b.year end
+    >  if a.month ~= b.month then return a.month < b.month end
+    >  if a.day ~= b.day then return a.day < b.day end
+    >  if a.hour ~= b.hour then return a.hour < b.hour end
+    >  if a.min ~= b.min then return a.min < b.min end
+    >  return a.sec < b.sec
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:49:13 2022
+  parse_time:
+    >function parse_time(t)
+    >  local year, month, day, hour, min, sec, tz_op, tz_hour, tz_min = t:match("^(%d%d%d%d)%-(%d%d)%-(%d%d)[Tt](%d%d%.?%d*):(%d%d):(%d%d)([+-])(%d%d):(%d%d)")
+    >  local int = tonumber
+    >  if tz_op == '+' then
+    >    hour = int(hour) + int(tz_hour)
+    >    min = int(min) + int(tz_min)
+    >  else
+    >    hour = int(hour) - int(tz_hour)
+    >    min = int(min) - int(tz_min)
+    >  end
+    >  return os.time{ year=int(year), month=int(month), day=int(day), hour=int(hour), min=int(min), sec=int(sec) }
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:51:08 2022
+  parse_time:
+    >function parse_time(t)
+    >  local year, month, day, hour, min, sec, tz_op, tz_hour, tz_min = t:match("^(%d%d%d%d)%-(%d%d)%-(%d%d)[Tt](%d%d%.?%d*):(%d%d):(%d%d)([+-])(%d%d):(%d%d)")
+    >  local int = tonumber
+    >  -- convert to UTC
+    >  if tz_op == '+' then
+    >    hour = int(hour) - int(tz_hour)
+    >    min = int(min) - int(tz_min)
+    >  else
+    >    hour = int(hour) + int(tz_hour)
+    >    min = int(min) + int(tz_min)
+    >  end
+    >  return os.time{ year=int(year), month=int(month), day=int(day), hour=hour, min=min, sec=int(sec) }
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:52:27 2022
+  __teliva_note:
+    >parsing twtxt feeds and sorting items after accounting for timezones
+  reload_feeds:
+    >function reload_feeds()
+    >  for _, feed in ipairs(Feeds) do
+    >    local response, status, headers = http.request(feed)
+    >    print(status)
+    >    if status == 200 then
+    >      Window:clear()
+    >      parse_items(feed, response)
+    >      local all_items = {}
+    >      for feed, items in pairs(Items) do
+    >        for _, item in ipairs(items) do
+    >          table.insert(all_items, item)
+    >        end
+    >      end
+    >      table.sort(all_items, function(a, b) return a.parsed_time < b.parsed_time end)
+    >      for _, x in ipairs(all_items) do
+    >        print(x.feed, x.time, x.status)
+    >      end
+    >      Window:getch()
+    >    end
+    >  end
+    >end
+- __teliva_timestamp:
+    >Fri Apr 22 23:57:10 2022
+  update:
+    >function update(window)
+    >  local key = window:getch()
+    >  if key == 18 then  -- ctrl-r
+    >    reload_feeds(window)
+    >  end
+    >end
+- __teliva_timestamp:
+    >Sat Apr 23 00:13:26 2022
+  render_feed_status:
+    >function render_feed_status(window, fs)
+    >  window:clear()
+    >  for _, fs in ipairs(feed_status) do
+    >    if fs.status then
+    >      if fs.status/100 == 2 then
+    >        window:attrset(curses.color_pair(10))
+    >        window:addstr(fs.feed..'\n')
+    >        window:attrset(curses.A_NORMAL)
+    >      else
+    >        window:attrset(curses.color_pair(9))
+    >        window:addstr(fs.feed..'\n')
+    >        window:attrset(curses.A_NORMAL)
+    >      end
+    >    else
+    >      window:addstr(fs.feed..'\n')
+    >    end
+    >  end
+    >  window:refresh()
+    >end
+- __teliva_timestamp:
+    >Sat Apr 23 00:18:59 2022
+  render:
+    >function render(window)
+    >  window:clear()
+    >  -- draw feeds
+    >  for i, feed in ipairs(Feeds) do
+    >    print(feed)
+    >  end
+    >  -- draw items
+    >  local all_items = {}
+    >  local n = 0
+    >  for feed, items in pairs(Items) do
+    >    n = n+1
+    >    for _, item in ipairs(items) do
+    >      table.insert(all_items, item)
+    >    end
+    >  end
+    >  table.sort(all_items, function(a, b) return a.parsed_time < b.parsed_time end)
+    >  for _, x in ipairs(all_items) do
+    >    print(x.feed, x.time, x.status)
+    >  end
+    >  window:refresh()
+    >end
+- __teliva_timestamp:
+    >Sat Apr 23 00:20:31 2022
+  __teliva_note:
+    >show progress when downloading feeds
+  reload_feeds:
+    >function reload_feeds(window)
+    >  window:nodelay(true)
+    >  feed_status = {}
+    >  for _, feed in ipairs(Feeds) do
+    >    table.insert(feed_status, {feed=feed})
+    >  end
+    >  for _, fs in ipairs(feed_status) do
+    >    local response, status, headers = http.request(fs.feed)
+    >    fs.status = status
+    >    parse_items(fs.feed, response)
+    >    render_feed_status(window, fs)
+    >    if window:getch() then break end
+    >  end
+    >  window:nodelay(false)
+    >end
+- __teliva_timestamp:
+    >Sat Apr 23 00:28:09 2022
+  render:
+    >function render(window)
+    >  window:clear()
+    >  -- draw feeds
+    >  print(str(#Feeds)..' feeds')
+    >  -- draw items
+    >  local all_items = {}
+    >  local n = 0
+    >  for feed, items in pairs(Items) do
+    >    n = n+1
+    >    for _, item in ipairs(items) do
+    >      table.insert(all_items, item)
+    >    end
+    >  end
+    >  table.sort(all_items, function(a, b) return a.parsed_time < b.parsed_time end)
+    >  for _, x in ipairs(all_items) do
+    >    print(x.feed, x.time, x.status)
+    >  end
+    >  window:refresh()
+    >end
+- __teliva_timestamp:
+    >Sat Apr 23 00:29:10 2022
+  reload_feeds:
+    >function reload_feeds(window)
+    >  window:nodelay(true)
+    >  feed_status = {}
+    >  for _, feed in ipairs(Feeds) do
+    >    table.insert(feed_status, {feed=feed})
+    >  end
+    >  render_feed_status(
+    >  for _, fs in ipairs(feed_status) do
+    >    local response, status, headers = http.request(fs.feed)
+    >    fs.status = status
+    >    parse_items(fs.feed, response)
+    >    render_feed_status(window, fs)
+    >    if window:getch() then break end
+    >  end
+    >  window:nodelay(false)
+    >end
+- __teliva_timestamp:
+    >Sat Apr 23 00:29:23 2022
+  render_feed_status:
+    >function render_feed_status(window, feed_status)
+    >  window:clear()
+    >  for _, fs in ipairs(feed_status) do
+    >    if fs.status then
+    >      if fs.status/100 == 2 then
+    >        window:attrset(curses.color_pair(10))
+    >        window:addstr(fs.feed..'\n')
+    >        window:attrset(curses.A_NORMAL)
+    >      else
+    >        window:attrset(curses.color_pair(9))
+    >        window:addstr(fs.feed..'\n')
+    >        window:attrset(curses.A_NORMAL)
+    >      end
+    >    else
+    >      window:addstr(fs.feed..'\n')
+    >    end
+    >  end
+    >  window:refresh()
+    >end
+- __teliva_timestamp:
+    >Sat Apr 23 00:29:48 2022
+  reload_feeds:
+    >function reload_feeds(window)
+    >  window:nodelay(true)
+    >  local feed_status = {}
+    >  for _, feed in ipairs(Feeds) do
+    >    table.insert(feed_status, {feed=feed})
+    >  end
+    >  render_feed_status(feed_status)
+    >  for _, fs in ipairs(feed_status) do
+    >    local response, status, headers = http.request(fs.feed)
+    >    fs.status = status
+    >    parse_items(fs.feed, response)
+    >    render_feed_status(window, feed_status)
+    >    if window:getch() then break end
+    >  end
+    >  window:nodelay(false)
+    >end
+- __teliva_timestamp:
+    >Sat Apr 23 00:30:18 2022
+  __teliva_note:
+    >fix an accidental global variable
+  reload_feeds:
+    >function reload_feeds(window)
+    >  window:nodelay(true)
+    >  local feed_status = {}
+    >  for _, feed in ipairs(Feeds) do
+    >    table.insert(feed_status, {feed=feed})
+    >  end
+    >  render_feed_status(window, feed_status)
+    >  for _, fs in ipairs(feed_status) do
+    >    local response, status, headers = http.request(fs.feed)
+    >    fs.status = status
+    >    parse_items(fs.feed, response)
+    >    render_feed_status(window, feed_status)
+    >    if window:getch() then break end
+    >  end
+    >  window:nodelay(false)
+    >end