239 lines
6.1 KiB
Lua
239 lines
6.1 KiB
Lua
|
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
|