stubb/stubb

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()