#!/usr/bin/python3 import enum, socket, traceback, subprocess, curses from curses.textpad import Textbox, rectangle class MenuType(str, enum.Enum): DOC = '0' MENU = '1' QUERY = '7' BIN = '9' HTML = 'h' INFO = 'i' SOUND = 's' IMAGE = 'I' GIF = 'g' ESC_WHITE = '\033[1;37m' ESC_CYAN = '\033[1;36m' ESC_RESET = '\033[0m' ESC_RED = '\033[1;31m' ESC_GREEN = '\033[1;32m' ESC_YELLO = '\033[1;33m' ESC_BLUE = '\033[1;34m' class Fetcher(object): def __init__(self): self.mode = MenuType.MENU self.host = 'republic.circumlunar.space' self.port = 70 self.path = '/~ake' self.history = [] def go(self, mode=None, host=None, port=None, path=None, query=None): self.history.append(( self.mode, self.host, self.port, self.path )) if mode is not None: self.mode = mode if host is not None: self.host = host if port is not None: self.port = int(port) if path is not None: self.path = path if mode == MenuType.QUERY: self.path += '\t{}'.format(query) def back(self): try: mode, host, port, path = self.history.pop() self.mode = mode self.host = host self.port = port self.path = path except: print('No entries in history') def fetch(self): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((self.host, self.port)) s.sendall('{}\r\n'.format(self.path).encode('utf-8')) resp = b'' while True: chunk = s.recv(1024) if not chunk: break resp += chunk return resp except: return None class Navigator(Fetcher): def __init__(self): super().__init__() self.links = [] self.buffer = [] def goto(self, num): if num <= 0 or num > len(self.links): print('\tWrong link number') else: self.go(*self.links[num - 1]) def download(self, filename): data = self.fetch() with open(filename, 'wb') as f: f.write(data) def render(self): self.buffer = [] if self.mode == MenuType.HTML: if self.path.lower().startswith('url:'): subprocess.call(['/usr/bin/xdg-open', self.path[4:]]) else: subprocess.call(['/usr/bin/lynx', 'gopher://{}:{}/0{}'.format(self.host, self.port, self.path)]) self.back() return data = self.fetch() if data is None: self.buffer = 'Failed to display gopher://{}:{}/{}{}\n{}'.format( self.host, self.port, self.mode, self.path, traceback.format_exc() ).split('\n') self.mode = MenuType.DOC return if self.mode == MenuType.DOC: self.buffer = data.decode('utf-8').split('\n') elif self.mode == MenuType.MENU or self.mode == MenuType.QUERY: self.links = [] items = [item.split('\t') for item in data.decode('utf-8').split('\n')] for item in items: if len(item) < 4: continue itype = item[0][0] itext = item[0][1:] if itype == MenuType.INFO: self.buffer.append((None, itext)) elif itype == MenuType.DOC: self.links.append((itype, item[2], item[3], item[1])) self.buffer.append(( len(self.links), itext, itype )) elif itype == MenuType.MENU: self.links.append((itype, item[2], item[3], item[1])) self.buffer.append(( len(self.links), itext, itype )) elif itype == MenuType.BIN or itype == MenuType.SOUND or itype == MenuType.IMAGE or itype == MenuType.GIF: self.links.append((itype, item[2], item[3], item[1])) self.buffer.append(( len(self.links), itext, itype )) elif itype == MenuType.QUERY: self.links.append((itype, item[2], item[3], item[1])) self.buffer.append(( len(self.links), itext, itype )) elif itype == MenuType.HTML: self.links.append((itype, item[2], item[3], item[1])) self.buffer.append(( len(self.links), itext, itype )) else: self.buffer.append((None, 'Unknown type: {}'.format(itype))) else: self.buffer.append('\tNot yet implemented') def main(stdscr): navi.render() start = 0 link = 0 key = None history = [] pagesize = curses.LINES - 2 curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) curses.init_pair(2, curses.COLOR_MAGENTA, curses.COLOR_BLACK) curses.init_pair(3, curses.COLOR_BLUE, curses.COLOR_BLACK) curses.init_pair(4, curses.COLOR_CYAN, curses.COLOR_BLACK) while True: stdscr.clear() stdscr.addstr(0, 0, '[ gopher://{}:{}/{}{}'.format(navi.host, navi.port, navi.mode, navi.path), curses.A_BOLD) stdscr.addstr(0, curses.COLS - 1, ']') stdscr.refresh() first = None last = None for i in range(min(pagesize, len(navi.buffer) - start)): color = curses.A_NORMAL if navi.mode == MenuType.MENU or navi.mode == MenuType.QUERY: text = navi.buffer[i + start][1] color = curses.A_BOLD if navi.buffer[i + start][0] is not None: if first is None: first = navi.buffer[i + start][0] - 1 last = navi.buffer[i + start][0] - 1 linktype = navi.buffer[i + start][2] if linktype == MenuType.DOC: color |= curses.color_pair(1) elif linktype == MenuType.BIN: color |= curses.color_pair(2) elif linktype == MenuType.MENU: color |= curses.color_pair(3) elif linktype == MenuType.QUERY: color |= curses.color_pair(4) if navi.buffer[i + start][0] == link + 1: color |= curses.A_REVERSE text = navi.buffer[i + start][1] # stdscr.addstr(i + 1, 0, '> ') else: text = navi.buffer[i + start] try: stdscr.addstr(i + 1, 2, text, color) except: pass stdscr.move(curses.LINES - 1, 0) stdscr.refresh() key = stdscr.getkey() if key == 'KEY_PPAGE': start = max(0, start - pagesize) elif key == 'KEY_NPAGE': start = start + pagesize elif key == 'KEY_RIGHT' or key == '\n': if navi.links[link][0] == MenuType.QUERY: curses.echo() stdscr.addstr(curses.LINES - 1, 0, 'Query: ') s = stdscr.getstr(curses.LINES - 1, 7).decode('utf-8') curses.noecho() navi.go(*navi.links[link], query=s) elif navi.links[link][0] == MenuType.BIN: curses.echo() stdscr.addstr(curses.LINES - 1, 0, 'Filename: ') filename = stdscr.getstr(curses.LINES - 1, 10).decode('utf-8') curses.noecho() if filename != '': navi.go(*navi.links[link]) navi.download(filename) navi.back() else: navi.goto(link + 1) history.append((start, link)) link = 0 start = 0 navi.render() elif key == 'KEY_LEFT': navi.back() if len(history) > 0: start, link = history.pop() navi.render() elif key == 'KEY_UP': if navi.mode == MenuType.DOC or last is None or link == 0: start = max(0, start - pagesize) elif link > 0: link -= 1 if link < first: start = max(0, start - pagesize) elif key == 'KEY_DOWN': if navi.mode == MenuType.DOC or last is None: start += pagesize elif link < len(navi.links): link += 1 if link > last: start += pagesize elif key == 'g': curses.echo() stdscr.addstr(curses.LINES - 1, 0, 'Host: ') s = stdscr.getstr(curses.LINES - 1, 6).decode('utf-8') curses.noecho() parts = s.split(':') navi.go(host=parts[0], port=70 if len(parts) < 2 else int(parts[1]), path='', mode=MenuType.MENU) history.append((start, link)) link = 0 start = 0 navi.render() elif key == 'q': break # elif str(key).isdigit(): # stdscr.addstr(curses.LINES - 1, 0, key) # curses.echo() # s = str(key) + stdscr.getstr(curses.LINES - 1, 1) # navi.goto() # curses.noecho() if __name__ == '__main__': navi = Navigator() curses.wrapper(main)