433 lines
11 KiB
Lua
Executable File
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
|