local serial = require "serialization" local event = require "event" local rpc = require "rpc" local _, unicode = pcall(require, "unicode") local cfg = {} local status = "" local bboard local ssub = (unicode or {}).sub or string.sub local function loadConfig() local f = io.open("/etc/bbs.cfg","rb") if not f then return false end cfg = serial.unserialize(f:read("*a")) f:close() return true end local function saveConfig() local f = io.open("/etc/bbs.cfg","wb") if not f then return false end f:write(serial.serialize(cfg)) f:close() return true end -- figure out terminal geometry local function getCursor() io.write("\27[6n") local b = "" repeat b = b .. io.read(1) until b:match("\27%[%d+;%d+R") local y,x = b:match("\27%[(%d+);(%d+)R") return tonumber(x), tonumber(y) end io.write("\27[999;999H") local mx, my = getCursor() print(mx, my) local function setStatus(str) local ox, oy = getCursor() status = str io.write(string.format("\27[%d;1H\27[0m\27[7m\27[2K%s\27[0m\27[%i;%iH", my, status, oy, ox)) end local function menu(items, state) local state = state or {1,1} local i, si = state[1], state[2] while true do if si > i - 3 then si = math.max(1, i-5) elseif i > (si+my) - 6 then si = math.min(i - (my-3) + 5, #items - (my - 3)) end i = math.max(1,math.min(#items, i)) print(string.format("\27[H\27[0m\27[7m\27[2K%s\27[0m", items.header or "")) for j = si, (si+my)-3 do print((i==j and "\27[7m" or "\27[0m") .. "\27[2K" .. (items[j] or ""):sub(1,mx)) end setStatus(status) local _,_,ch,co = event.pull(60,"key_down") ch=string.char(ch or 0) if co == 208 then -- down i = i + 1 elseif co == 200 then -- up i = i - 1 elseif co == 201 then -- pgdn i = i - (my - 2) elseif co == 209 then -- pgdn i = i + my - 2 else return i, ch, co, {i, si} end end end local function popup(str,title) local len = (unicode or {}).len or string.len if title then title=string.format("[%s]",title or "") else title = "" end local width, height, content = 0, 0, {} for line in str:gmatch("[^\n]*") do height = height + 1 width = math.max(width,len(line)) content[#content+1] = line end if width < 1 or height < 1 then return false end local startx,starty = ((mx//2)-(width//2))-2, ((my//2)-(height//2))-2 io.write(string.format("\27[%d;%dH╒═%s%s╕",starty,startx,title,("═"):rep((width+1)-len(title)))) for k,v in pairs(content) do io.write(string.format("\27[%d;%dH│ %s%s │",starty+k,startx,v,(" "):rep(width-len(v)))) end io.write(string.format("\27[%d;%dH┕%s┙",starty+1+#content,startx,("━"):rep(width+2))) end local function query(str, default) local rv io.write(string.format(str, default or "no default")) repeat rv = io.read() rv = #rv > 0 and rv or default or "" until #rv > 0 return rv end local function help() popup([[ [↑][↓] Move 1 entry [PgUp][PgDn] Move 1 page [Enter] Select item [c] Compose new post [r] Reply to post [t] Thread view [q] Exit Press any key to continue...]],"Usage") event.pull("key_down") end local function compose(board, post) local post = post or {} io.write("\27[2J\27[H") post.author = cfg.handle post.subject = query("Subject [%s]? ", post.subject) fn = os.tmpname() os.execute(string.format("edit '%s'",fn)) f = io.open(fn, "rb") if f then post.content = f:read("*a") f:close() os.execute(string.format("rm '%s'", fn)) print("Posting...") bboard.post(board,post) else print("Post empty, cancelled.") end end local function viewPost(board,id) io.write("\27[2J\27[H") post = bboard.getPost(board, id) fn = os.tmpname() f = io.open(fn, "wb") f:write(string.format("Author: %s\nSubject: %s\n", post.author, post.subject)) f:write(post.content) f:close() os.execute(string.format("less '%s'", fn)) os.execute(string.format("rm '%s'", fn)) end local function threadView(board,id) local run = true local i, ch, co, st local list = bboard.getThread(board, id) while run do local vl = {header=string.format("%4s %15s %s", "ID", "Author", "Subject")} status = string.format("[%s] Thread: %s - %i items", cfg.handle, list[1].subject, #list) for k,v in pairs(list) do vl[k] = string.format("%4d %15s %s", v.id, v.author:sub(1,15), v.subject):sub(1,mx) end i, ch, co, st = menu(vl, st) if ch == "q" then -- quit run = false elseif co == 28 then -- enter, view viewPost(board, list[i].id) elseif ch == "r" then -- reply local prev = list[i] compose(board, {replyTo=prev.id, subject="Re:"..prev.subject}) list = bboard.list(board) elseif ch == "h" or ch == "?" then help() end end end local function boardView(name) local run = true local i, ch, co, st, fn, f, post local list = bboard.list(name) while run do status = string.format("[%s] %s/%s - %i items", cfg.handle, cfg.server, name, #list) local vl = {header=string.format("%4s %15s %s", "ID", "Author", "Subject")} for k,v in pairs(list) do vl[k] = string.format("%4d %15s %s", v.id, v.author:sub(1,15), v.subject):sub(1,mx) end i, ch, co, st = menu(vl, st) if ch == "q" then -- quit run = false elseif co == 28 then -- enter, view viewPost(name, list[i].id) elseif ch == "c" then -- compose compose(name) list = bboard.list(name) elseif ch == "r" then -- reply local prev = list[i] compose(name, {replyTo=prev.id, subject="Re:"..prev.subject}) list = bboard.list(name) elseif ch == "t" then -- thread view threadView(name,list[i].id) elseif ch == "h" or ch == "?" then help() end end end io.write("\27[2J\27[H") setStatus("Loading config") loadConfig() setStatus("Basic setup...") cfg.server = query("Server [%s]?", cfg.server) cfg.handle = query("Username? [%s]?", cfg.handle) setStatus("Saving config") saveConfig() setStatus("Connecting...") bboard = rpc.proxy(cfg.server, "bbs_") local run = true local i, ch, co, st while run do local list = bboard.boards() setStatus(string.format("Connected to %s - %i boards",cfg.server,#list)) list.header = "Choose a board..." i, ch, co, st = menu(list, st) if ch == "q" then run = false elseif co == 28 then boardView(list[i]) elseif ch == "h" or ch == "?" then help() end end