stubb/stubb

791 lines
21 KiB
Lua
Executable File

#!/usr/bin/env lua
local version = 'v1.0.0'
local socket = require("socket")
local urlparser = require("socket.url")
local separator = '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n'
local valid_types = {'0', '1', 'i', '7'}
local download_types = {I = 'IMG', h = 'HTM', ['9'] = 'BIN', g = 'GIF', s = 'SND'}
session = { tabs = { }, favorites = {} }
local save_file_location = string.format('%s/.stubbfaves', assert(os.getenv('HOME'), 'Unable to get home directory'))
local search_provider = 'gopher://gopher.floodgap.com:70/7/v2/vs'
--||__||--||__||--||__||--||__||--||__||--||__||--||__||--||
------------------------------------------------------------
-- Helper methods ------------------------------------------
------------------------------------------------------------
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 pairs(t) do
print(i, string.format('%q', v))
end
end
-- Checks to see if an autoindexed table has a value
table.has = function(t, item)
for _, v in ipairs(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
function invalid_key(key)
if key == 'enter' then io.write('\27[2A\27[200D\27[K\27[1A\27[200D\27[K')
elseif key == 'other' then io.write('\27[1A\27[200D\27[K')
else io.write('\27[1A\27[200D\27[K\27[1A\27[200D\27[K') end
end
--||__||--||__||--||__||--||__||--||__||--||__||--||__||--||
------------------------------------------------------------
-- TCP Requests and processing -----------------------------
------------------------------------------------------------
function request_gopher(host, port, query, gtype)
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
if gtype ~= 'h' and download_types[gtype] then
local s = tcp:receive('*a')
response = s
else
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
end
elseif connection then
response[1] = 'iConnection error. Bad request.'
tcp:close()
else
response[1] = 'iConnection error. Bad host.'
end
tcp:close()
return response
end
function parse_url(u) -- takes URL string
if not string.find(u, '://', 1, true) then
u = 'gopher://'..u
end
local default = {port=70, scheme='gopher', path=''}
local parsed = urlparser.parse(u, default)
if string.find(parsed.path, '^/[%d%a]') then
parsed.gophertype = string.sub(parsed.path, 2, 2)
parsed.path = string.sub(parsed.path, 3)
elseif string.sub(parsed.path, -1) == '/' or parsed.path == '' then
parsed.gophertype = '1'
else
parsed.gophertype = '0'
end
parsed.full = parsed.scheme .. '://' .. parsed.host .. ':' .. parsed.port .. '/' .. parsed.gophertype .. parsed.path
return parsed
end
function go_to_url(u, add2history, noprint)
local url = parse_url(u)
if url.scheme == 'http' then
print('Attempting to open http link in "lynx"...')
os.execute(string.format('lynx %s', url.scheme .. '://' .. url.authority .. url.path))
print('Operation complete')
return true
end
local t = session.tabs[session.current_tab]
if not url.host or url.scheme ~= 'gopher' then
print(separator)
print('Invalid url')
elseif table.has(valid_types, url.gophertype) or url.gophertype == 'h' then
if url.query ~= '' and url.query then url.path = url.path .. '?' .. url.query end
t.filedata = request_gopher(url.host, url.port, url.path, url.gophertype)
print(separator)
local res = handle_response(t.filedata, url.gophertype)
if noprint then
return res
else
print(res)
end
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
elseif download_types[url.gophertype] then
download_file(url)
end
end
function download_file(u)
print(separator)
local save_path = assert(os.getenv('HOME'))
local fn = nil
io.write('Save file as: ')
fn = io.read()
if not fn or fn == '' then return print('Cancelled') end
io.write(string.format('Saving as %q, is this correct? ', fn))
local r = string.lower(io.read())
if r ~= 'y' and r ~= 'yes' then return false end
print(string.format('Downloading %q...', fn))
local data = request_gopher(u.host, u.port, u.path, u.gophertype)
if not data then return false end
local f = io.open(save_path .. '/' .. fn, 'wb')
f:write(data)
f:close()
print(string.format('%q saved to %s/%s', fn, save_path, fn))
end
function handle_response(res_table, gtype)
local out = ''
if gtype == '0' or gtype == 0 or gtype == 'h' then
out = table.concat(res_table, '\n')
else
session.tabs[session.current_tab].current_links = {}
for i, v in ipairs(res_table) do
out = out .. (display_gophermap_row(v) or '').. '\n'
end
end
return out
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
return spacer .. (text or '')
elseif table.has(valid_types, gophertype) and gophertpe ~= '7' 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)
return string.format(' %s %5s %s', leader, linknum, text) or ''
else
local leader = download_types[gophertype]
if not leader then return '' end
if gophertype == 'h' and string.sub(val[2],1,3) == 'URL' then
table.insert(t.current_links, string.sub(val[2], 5))
else
local url = string.format('%s:%s/%s%s', val[3] or '', val[4] or '', gophertype, val[2] or '/')
table.insert(t.current_links, url)
end
local linknum = string.format('(%d)', #t.current_links)
return string.format(' %s %5s %s', leader, linknum, text) or ''
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)
if string.byte(key) == 27 then io.read(2) end
os.execute("stty -cbreak </dev/tty >/dev/tty 2>&1");
print('\27[1D ')
key = string.byte(key) == string.byte('\n') and 'enter' or key
return string.lower(key);
end
--||__||--||__||--||__||--||__||--||__||--||__||--||__||--||
------------------------------------------------------------
-- Initialize and run application --------------------------
------------------------------------------------------------
function init()
if arg[1] == '-h' or arg[1] == '--help' then
print_help()
os.exit(0)
end
new_tab()
if file_exists(save_file_location) then
dofile(save_file_location)
end
mainloop()
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)
local key = getch()
io.write(string.char(27) .. "[100D")
io.write(string.char(27) .. "[1A")
io.write(string.char(27) .. "[K")
if key == 'q' then save_favorites(); os.exit(0)
elseif key == 'h' then print_help()
elseif key == 's' then search()
elseif key == 'b' then back()
elseif key == 'f' then forward()
elseif key == 'u' then update_favorite()
elseif key == 'r' then refresh()
elseif key == 'p' then pipe()
elseif key == 'a' then add()
elseif key == 'l' then list()
elseif key == 'd' then delete()
elseif key == 'g' then go_to()
elseif key == 'c' then clean_screen()
elseif key == 'enter' then io.write('\27[2A\27[200D\27[K')
else print('\27[2A\27[200D\27[K') end
end
end
--||__||--||__||--||__||--||__||--||__||--||__||--||__||--||
------------------------------------------------------------
-- Top Level Menu Options ----------------------------------
------------------------------------------------------------
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 print_help()
print(separator)
print('\nStubb ' .. version)
local helpstring = [[
Stubb is a text based gopher client gophertypes 0 (text) and 1 (gopher map) are
rendered to the screen, all other types are downloaded to a user's home folder.
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).
Command map:
<Navigation>
(B)ack
(F)orward
(G)o to url
- (B)ookmark
- (L)ink
- (T)ab
- (U)rl
(R)eload current address
(P)ipe to
- (R)eader
- (C)urrent
- (L)ink
- (T)ab
- (C)urrent
- (L)ink
- (D)isk
- (C)urrent
- (L)ink
(S)earch
<Bookmarks/Tabs>
(A)dd
- (B)ookmark
- (T)ab
(D)elete
- (B)ookmark
- (T)ab
(L)ist
- (B)ookmarks
- (T)abs
(U)pdate bookmark title
<System>
(C)lean the screen
(H)elp
(Q)uit Stubb
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 search()
io.write('Enter your search terms: ')
local st = io.read()
if not st or st == '' then return print('Invalid search entry') end
if url ~= '!' and url ~= '' then
go_to_url(search_provider .. '\t' .. st, true)
else
print('Cancelled')
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])
else invalid_key('other') 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])
else
invalid_key('other')
end
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 refresh()
local th = session.tabs[session.current_tab].history
if th.loc > 0 then
go_to_url(th.items[th.loc])
end
end
function pipe()
io.write('Pipe to: (D)isk, (R)eader, New (T)ab')
local subkey = getch()
if subkey == 'd' then save_file()
elseif subkey == 't' then open_in_tab()
elseif subkey == 'r' then open_in_less()
else invalid_key(subkey) end
end
function add()
io.write('Add: (B)ookmark, (T)ab')
local subkey = getch()
if subkey == 'b' then add_favorite()
elseif subkey == 't' then new_tab()
else invalid_key(subkey) end
end
function list()
io.write('List: (B)ookmarks, (T)abs')
local subkey = getch()
if subkey == 'b' then show_favorites()
elseif subkey == 't' then show_tabs()
else invalid_key(subkey) end
end
function delete()
io.write('Delete: (B)ookmark, (T)ab')
local subkey = getch()
if subkey == 'b' then remove_favorite()
elseif subkey == 't' then remove_tab()
else invalid_key(subkey) end
end
function go_to()
io.write('Go to: (B)ookmark, (L)ink, (T)ab, (U)rl')
local subkey = getch()
if subkey == 'b' then
go_to_link(true)
elseif subkey == 'l' then
go_to_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('Cancelled')
end
else invalid_key(subkey) end
end
function clean_screen()
os.execute('clear')
end
--||__||--||__||--||__||--||__||--||__||--||__||--||__||--||
------------------------------------------------------------
-- Secondary actions ---------------------------------------
------------------------------------------------------------
function go_to_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 open_in_tab()
local t = session.tabs[session.current_tab]
local sub_menu = 'Pipe what: (C)urrent, (L)ink'
local favorite
io.write('Pipe what: (C)urrent, (L)ink')
local subkey = getch()
if subkey == 'c' then
local target_url = t.history.items[t.history.loc]
if not target_url then return print('No current page. Cancelled.') end
new_tab()
go_to_url(target_url, true)
elseif subkey == 'l' then
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))
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 = t.current_links[linkid]
end
if linkurl then
new_tab()
go_to_url(linkurl, true)
else
local out = string.format('Link id %d does not exist...', linkid)
print_gopher_error(out)
end
end
end
function open_in_less()
local th = session.tabs[session.current_tab].history
local str
if th.loc > 0 then
str = go_to_url(th.items[th.loc], false, true)
end
if str then
local less = io.popen(os.getenv('PAGER') or'less', 'w')
less:write(str)
less:close()
else
print('No current URL to open in less')
end
end
function add_favorite()
local th = session.tabs[session.current_tab].history
if #th.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 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)
if #session.tabs < session.current_tab then
session.current_tab = #session.tabs
refresh()
end
print('Tab has been removed')
else
print('Invalid tab id')
end
else
print('Invalid tab id')
end
end
function print_gopher_error(text)
print(separator)
print(text)
end
function save_file()
io.write('Save to disk: (C)urrent, (L)ink')
local subkey = getch()
local out
local t = session.tabs[session.current_tab]
if subkey == 'c' then
local t = session.tabs[session.current_tab]
out = t.filedata
if not out then return print('No file available. Save cancelled.') end
elseif subkey == 'l' then
io.write('Enter link id (! to cancel): ')
local linkid = tonumber(io.read())
if linkid == '!' then return print('Cancelled') end
local location = t.current_links[linkid]
if location then
local parsed = parse_url(location)
out = request_gopher(parsed.host, parsed.port, parsed.path)
if not out then return print('No file data available. Save cancelled.') end
else
return print('Invalid link id. Save cancelled.')
end
else
return print('Invalid entry')
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
print('Saving...')
local filepath = homedir .. '/' .. filename
local f = io.open(filepath, 'w+')
f:write(table.concat(out,'\n'))
f:close()
print(string.format('File saved as: %s', filepath))
else
print('Invalid filename')
end
end
-- function handle_response(res_table, gtype)
-- local out = ''
-- if gtype == '0' or gtype == 0 then
-- out = table.concat(res_table, '\n')
-- else
-- session.tabs[session.current_tab].current_links = {}
-- for i, v in ipairs(res_table) do
-- out = out .. (display_gophermap_row(v) or '').. '\n'
-- end
-- end
-- return out
-- 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 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={}
}
table.insert(session.tabs, t)
session.current_tab = #session.tabs
end
-- run the program
init()