#!/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 2>&1") local key = io.read(1) os.execute("stty -cbreak /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: (B)ack (F)orward (G)o to url - (B)ookmark - (L)ink - (T)ab - (U)rl (R)eload current address (A)dd - (B)ookmark - (T)ab (D)elete - (B)ookmark - (T)ab (L)ist - (B)ookmarks - (T)abs (U)pdate bookmark title (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()