543 lines
13 KiB
Lua
Executable File
543 lines
13 KiB
Lua
Executable File
#!/usr/bin/env lua
|
|
|
|
local version = 'v0.8.5'
|
|
local socket = require("socket")
|
|
local urlparser = require("socket.url")
|
|
local separator = '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n'
|
|
local valid_types = {'0', '1', 'i'}
|
|
session = { tabs = { } }
|
|
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 t = session.tabs[session.current_tab]
|
|
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(t.current_links, url)
|
|
local linknum = string.format('(%d)', #t.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()
|
|
local th = session.tabs[session.current_tab].history
|
|
print('\n')
|
|
while true do
|
|
local bar = string.format('\27[7m o--[ S t u b b ]--> (H)elp, (Q)uit TAB:%d/%d HST: %d/%d \27[0m', session.current_tab, #session.tabs,th.loc, #th.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 == 's' then
|
|
save_file()
|
|
elseif key == 'b' then
|
|
back()
|
|
elseif key == 'f' then
|
|
forward()
|
|
elseif key == 'u' then
|
|
update_favorite()
|
|
elseif key == 'r' then
|
|
refresh()
|
|
elseif key == 'a' then
|
|
io.write('Add: (B)ookmark, (T)ab')
|
|
local subkey = string.lower(getch())
|
|
if subkey == 'b' then
|
|
add_favorite()
|
|
elseif subkey == 't' then
|
|
new_tab()
|
|
end
|
|
elseif key == 'l' then
|
|
io.write('List: (B)ookmarks, (T)abs')
|
|
local subkey = string.lower(getch())
|
|
if subkey == 'b' then
|
|
show_favorites()
|
|
elseif subkey == 't' then
|
|
show_tabs()
|
|
end
|
|
elseif key == 'd' then
|
|
io.write('Delete: (B)ookmark, (T)ab')
|
|
local subkey = string.lower(getch())
|
|
if subkey == 'b' then
|
|
remove_favorite()
|
|
elseif subkey == 't' then
|
|
remove_tab()
|
|
end
|
|
elseif key == 'g' then
|
|
io.write('Go to: (B)ookmark, (L)ink, (T)ab, (U)rl')
|
|
local subkey = string.lower(getch())
|
|
if subkey == 'b' then
|
|
visit_link(true)
|
|
elseif subkey == 'l' then
|
|
visit_link()
|
|
elseif subkey == 't' then
|
|
io.write('Enter tab id (! to cancel): ')
|
|
local t = io.read()
|
|
local tabid = tonumber(t)
|
|
if t ~= '!' and tabid and session.tabs[tabid] then
|
|
session.current_tab = tabid
|
|
refresh()
|
|
else
|
|
local x = tabid and print('Invalid tab id') or print('Cancelled')
|
|
end
|
|
elseif subkey == 'u' then
|
|
io.write('Enter gopher url (! to cancel): ')
|
|
local url = io.read()
|
|
if url ~= '!' and url ~= '' then
|
|
go_to_url(url, true)
|
|
else
|
|
print('\n')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function refresh()
|
|
local th = session.tabs[session.current_tab].history
|
|
if th.loc > 0 then
|
|
go_to_url(th.items[th.loc])
|
|
end
|
|
end
|
|
|
|
|
|
function add_favorite()
|
|
local th = session.tabs[session.current_tab].history
|
|
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 = th.items[th.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 remove (! 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 remove_tab()
|
|
io.write('Enter the tab id to remove (! to cancel): ')
|
|
local tabid = io.read()
|
|
if tabid == '!' then return print('Cancelled') end
|
|
local id = tonumber(tabid)
|
|
if id then
|
|
local item = session.tabs[id]
|
|
if item then
|
|
table.remove(session.tabs, id)
|
|
print('Tab has been removed')
|
|
else
|
|
print('Invalid tab id')
|
|
end
|
|
else
|
|
print('Invalid tab id')
|
|
end
|
|
end
|
|
|
|
|
|
function visit_link(favorite)
|
|
local t = session.tabs[session.current_tab]
|
|
local linkid
|
|
repeat
|
|
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))
|
|
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 = t.current_links[linkid]
|
|
end
|
|
if linkurl then
|
|
table.truncate(t.history.items, t.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()
|
|
local th = session.tabs[session.current_tab].history
|
|
if th.loc > 1 and #th.items > 1 then
|
|
th.loc = th.loc - 1
|
|
go_to_url(th.items[th.loc])
|
|
end
|
|
end
|
|
|
|
|
|
function forward()
|
|
local th = session.tabs[session.current_tab].history
|
|
if th.loc < #th.items then
|
|
th.loc = th.loc + 1
|
|
go_to_url(th.items[th.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.
|
|
|
|
The command bar lists the current tab number, out of how many total tabs. It also lists the current place in history (for the tab you are currently on).
|
|
|
|
Commands:
|
|
|
|
<Navigation>
|
|
(B)ack
|
|
(F)orward
|
|
(G)o to url
|
|
- (B)ookmark
|
|
- (L)ink
|
|
- (T)ab
|
|
- (U)rl
|
|
(R)eload current address
|
|
|
|
<Bookmarks>
|
|
(A)dd
|
|
- (B)ookmark
|
|
- (T)ab
|
|
(D)elete
|
|
- (B)ookmark
|
|
- (T)ab
|
|
(L)ist
|
|
- (B)ookmarks
|
|
- (T)abs
|
|
(U)pdate bookmark title
|
|
|
|
<System>
|
|
(H)elp
|
|
(Q)uit Stubb
|
|
(S)ave file to disk
|
|
|
|
For more information, source code, or to get a copy:
|
|
|
|
http://tildegit.org/sloum/stubb
|
|
|
|
Stubb is free, as in freedom, software. Modify it as you see fit, give it to whoever you want. It would be great if you referenced the original or cross linked, but it is not required.
|
|
|
|
]]
|
|
print(helpstring)
|
|
end
|
|
|
|
|
|
function print_gopher_error(text)
|
|
print(separator)
|
|
print(text)
|
|
end
|
|
|
|
|
|
function save_file()
|
|
local t = session.tabs[session.current_tab]
|
|
if not t.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(t.filedata,'\n'))
|
|
f:close()
|
|
print(string.format('File saved as: %s', filepath))
|
|
else
|
|
print('Invalid filename')
|
|
end
|
|
end
|
|
|
|
|
|
function go_to_url(u, add2history)
|
|
local url = parse_url(u)
|
|
local t = session.tabs[session.current_tab]
|
|
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
|
|
t.filedata = request_gopher(url.host, url.port, url.path)
|
|
print(separator)
|
|
handle_response(t.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(t.history.items, t.history.loc + 1)
|
|
table.insert(t.history.items, o)
|
|
t.history.loc = t.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.tabs[session.current_tab].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 show_tabs()
|
|
print(separator)
|
|
print('\n [ T A B S ]\n\n')
|
|
for i, v in ipairs(session.tabs) do
|
|
local u = v.history.items[v.history.loc] or 'Empty tab'
|
|
local t = session.current_tab == i and ' --> ' or ' '
|
|
print(string.format('%s(%d) %s', t, i, u))
|
|
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
|
|
|
|
|
|
function new_tab()
|
|
local t = {
|
|
history = {
|
|
loc=0,
|
|
items={}
|
|
},
|
|
current_links = {},
|
|
favorites={}
|
|
}
|
|
|
|
if #session.tabs < 4 then
|
|
table.insert(session.tabs, t)
|
|
session.current_tab = #session.tabs
|
|
end
|
|
end
|
|
|
|
|
|
function init()
|
|
new_tab()
|
|
if file_exists(save_file_location) then
|
|
dofile(save_file_location)
|
|
end
|
|
mainloop()
|
|
end
|
|
|
|
-- run the program
|
|
init()
|