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