stubb/stubb-lite

433 lines
11 KiB
Lua
Executable File

#!/usr/bin/env lua
local version = 'v0.6.0'
local socket = require("socket")
local urlparser = require("socket.url")
local separator = '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n'
local valid_types = {'0', '1', 'i'}
session = { history={ loc=0, items={} }, current_links = {}, favorites={} }
local save_file_location = string.format('%s/.stubbfaves', assert(os.getenv('HOME'), 'Unable to get home directory'))
function string.split(str, sep)
assert(str and sep, 'sting.split requires both an input string and a field separator string')
local start = 1
local t = {}
while true do
local out
local point = string.find(str, sep, start)
if point == start then
out = ''
elseif not point then
out = string.sub(str, start)
else
out = string.sub(str, start, point - 1)
end
table.insert(t, out)
start = point and point + 1 or nil
if not start then
break
end
end
return t
end
function table.out(t)
for i, v in ipairs(t) do
print(string.format('%q', v))
end
end
table.has = function(t, item)
for i, v in pairs(t) do
if v == item then
return true
end
end
return false
end
table.truncate = function(t, i)
while t[i] do
table.remove(t, i)
end
end
--based on:
--https://stackoverflow.com/questions/9013290/lua-socket-client/9014261#9014261
function request_gopher(host, port, query)
local tcp = assert(socket.tcp())
local response = {}
tcp:settimeout(4, 't')
local connection = tcp:connect(host, port);
local request
if connection then
request = tcp:send(query.."\n");
end
if request then
while true do
local s, status, partial = tcp:receive()
if status == "closed" then break end
if s ~= '.' then
table.insert(response, (s or partial))
end
end
tcp:close()
elseif connection then
response[1] = 'iConnection error. Bad request.'
tcp:close()
else
response[1] = 'iConnection error. Bad host.'
end
return response
end
function display_gophermap_row(row)
local val = string.split(row, '\t')
local gophertype = string.sub(val[1], 1, 1)
local text = string.sub(val[1], 2)
local spacer = ' '
if gophertype == 'i' then
print(spacer..text)
elseif table.has(valid_types, gophertype) then
local leader = gophertype == '0' and 'TXT' or 'MAP'
local url = string.format('%s:%s/%s%s', val[3] or '', val[4] or '', gophertype, val[2] or '/')
table.insert(session.current_links, url)
local linknum = string.format('(%d)', #session.current_links)
print(string.format(' %s %5s %s', leader, linknum, text))
end
end
function getch()
-- taken from: http://lua.2524044.n2.nabble.com/How-to-get-one-keystroke-without-hitting-Enter-td5858614.html
os.execute("stty cbreak </dev/tty >/dev/tty 2>&1")
local key = io.read(1)
os.execute("stty -cbreak </dev/tty >/dev/tty 2>&1");
print('\27[1D ')
return(key);
end
function mainloop()
print('\n')
while true do
local bar = string.format('\27[7m o--[ S t u b b ]--> (H)elp, (Q)uit %d/%d \27[0m', session.history.loc, #session.history.items)
print(bar)
-- print('\27[7m (G)o to url, (B)ack, (V)isit link, (Q)uit \27[0m')
local key = string.lower(getch())
if key == 'q' then
save_favorites()
os.exit(0)
elseif key == 'h' then
print_help()
elseif key == 'b' then
back()
elseif key == 'f' then
forward()
elseif key == 'a' then
add_favorite()
elseif key == 'l' then
show_favorites()
elseif key == 'u' then
update_favorite()
elseif key == 'd' then
remove_favorite()
elseif key == 's' then
save_file()
elseif key == 'r' then
refresh()
elseif key == 'g' then
io.write('Enter gopher url > ')
local url = io.read()
if url ~= '0' and url ~= '' then
go_to_url(url, true)
else
print('\n')
end
elseif key == 'v' then
visit_link()
end
end
end
function refresh()
if session.history.loc > 0 then
go_to_url(session.history.items[session.history.loc])
end
end
function add_favorite()
if #session.history.items == 0 then return false end
local title
repeat
io.write('Enter title for bookmark (! to cancel): ')
title = io.read()
until title
if title == '!' then return print('Cancelled') end
table.insert(session.favorites,{display = title, link = session.history.items[session.history.loc]})
print('Bookmark added')
save_favorites()
end
function update_favorite()
local title
io.write('Enter the bookmark id (! to cancel) > ')
local favid = io.read()
if favid == '!' then return print('Cancelled') end
local id = tonumber(favid) or tonumber(string.sub(favid,2))
if id then
local item = session.favorites[id]
if item then
print(string.format('Old title: %s', item.display))
while not title or title == '' do
io.write('Enter new title (! to cancel) > ')
title = io.read()
if title == '!' then return print('Cancelled') end
end
item.display = title
print('Bookmark has been updated')
save_favorites()
else
print('Invalid bookmark id')
end
else
print('Invalid bookmark id')
end
end
function remove_favorite()
io.write('Enter the bookmark id (! to cancel) > ')
local favid = io.read()
if favid == '!' then return print('Cancelled') end
local id = tonumber(favid) or tonumber(string.sub(favid,2))
if id then
local item = session.favorites[id]
if item then
table.remove(session.favorites, id)
print('Bookmark has been removed')
save_favorites()
else
print('Invalid bookmark id')
end
else
print('Invalid bookmark id')
end
end
function visit_link()
local linkid
local favorite
repeat
favorite = false
io.write('Enter link id (! to cancel) > ')
local inp = io.read()
if inp == '!' then return print('Cancelled') end
linkid = tonumber(inp)
if not linkid and string.sub(inp, 1, 1) == 'f' then
linkid = tonumber(string.sub(inp, 2))
favorite = true
end
until linkid
local linkurl
if favorite then
selection = session.favorites[linkid]
if selection then
linkurl = session.favorites[linkid].link
else
return print('Invalid bookmark id')
end
else
linkurl = session.current_links[linkid]
end
if linkurl then
table.truncate(session.history.items, session.history.loc + 1)
go_to_url(linkurl, true)
else
local out = string.format('Link id %d does not exist...', linkid)
print_gopher_error(out)
end
end
function back()
if session.history.loc > 1 and #session.history.items > 1 then
session.history.loc = session.history.loc - 1
go_to_url(session.history.items[session.history.loc])
end
end
function forward()
if session.history.loc < #session.history.items then
session.history.loc = session.history.loc + 1
go_to_url(session.history.items[session.history.loc])
end
end
function print_help()
print(separator)
print('\nStubb ' .. version)
local helpstring = [[
Stubb is a text based gopher client that only displays and supports gophertypes:
0 (text) and 1 (gopher map). The application is named for the character Stubb
from Moby Dick known for his imaginative patter and good humor.
Commands:
<Navigation>
(B)ack
(F)orward
(G)o to url
(R)eload current address
(V)iew link/bookmark
<Bookmarks>
(A)dd bookmark
(D)elete bookmark
(L)ist bookmarks
(U)pdate bookmark title
<System>
(H)elp
(Q)uit Stubb
(S)ave file to disk
]]
print(helpstring)
end
function print_gopher_error(text)
print(separator)
print(text)
end
function save_file()
if not session.filedata then return print('No file available. Save canceled.') end
local homedir = assert(os.getenv('HOME'), 'Unable to find home directory')
io.write('Enter the filename to save as (! to cancel): ' .. homedir .. '/')
local filename = io.read()
if filename == '!' or filename == '' then
return print('Cancelled')
elseif string.match(filename, '^[%.%w_-]+$') then
local filepath = homedir .. '/' .. filename
local f = io.open(filepath, 'w+')
f:write(table.concat(session.filedata,'\n'))
f:close()
print(string.format('File saved as: %s', filepath))
else
print('Invalid filename')
end
end
function go_to_url(u, add2history)
url = parse_url(u)
if not url.host or url.scheme ~= 'gopher' then
print(separator)
print(' I n v a l i d u r l')
elseif table.has(valid_types, url.gophertype) then
session.filedata = request_gopher(url.host, url.port, url.path)
print(separator)
handle_response(session.filedata, url.gophertype)
local o = url.scheme .. '://' .. url.host .. ':' .. url.port .. '/' .. url.gophertype .. url.path
print('\n\n')
print(o)
if add2history then
table.truncate(session.history.items, session.history.loc + 1)
table.insert(session.history.items, o)
session.history.loc = session.history.loc + 1
end
end
end
function handle_response(res_table, gtype)
if gtype == '0' or gtype == 0 then
local o = table.concat(res_table, '\n')
print(o)
else
session.current_links = {}
for i, v in ipairs(res_table) do
display_gophermap_row(v)
end
end
end
-- Modified version of socket.url.parse
-- Sets up defaults and adds parsing of gophertype
function parse_url(u)
if not string.find(u, '://', 1, true) then
u = 'gopher://'..u
end
local default = {port=70, scheme='gopher'}
local parsed = urlparser.parse(u, default)
if not parsed.path then
parsed.path = '/'
end
if string.find(parsed.path, '^/[%d%a]') then
parsed.gophertype = string.sub(parsed.path, 2, 2)
parsed.path = string.sub(parsed.path, 3) or '/'
elseif string.sub(parsed.path, -1) == '/' then
parsed.gophertype = '1'
else
parsed.gophertype = '0'
end
return parsed
end
function show_favorites()
print(separator)
print('\n >---B O O K M A R K S--->\n\n')
for i, v in ipairs(session.favorites) do
print(string.format(' (f%d) %s', i, v.display))
end
print('\n' .. separator)
end
function save_favorites()
local file = io.open(save_file_location, 'w+')
file:write("session.favorites = {\n")
for _,v in ipairs(session.favorites) do
file:write(string.format(" {[%q] = %q, [%q] = %q},\n", 'display', v.display, 'link', v.link))
end
file:write("}\n")
file:close()
end
function file_exists(name)
local f=io.open(name,"r")
if f~=nil then io.close(f) return true else return false end
end
-- run the program
if file_exists(save_file_location) then
dofile(save_file_location)
end
mainloop()
-- end of program