import sys import socket import pytermgui as ptg from typing import Tuple import re import logging PORT = 1900 logger = logging.getLogger('nex') logger.setLevel(logging.INFO) logger.addHandler(logging.FileHandler('nex.log')) class Session: def __init__(self): self.history = [Page('home')] self.curr_page_idx = 0 def back(self): if self.curr_page_idx > 0: self.curr_page_idx -= 1 self.render() def forward(self): if self.curr_page_idx < len(self.history) - 1: self.curr_page_idx += 1 self.render() def load(self, url): self.curr_page_idx += 1 if self.curr_page_idx >= len(self.history): self.history.append(Page(url)) else: self.history[self.curr_page_idx] = Page(url) self.history[self.curr_page_idx].request() def render(self): return self.history[self.curr_page_idx].content def reload(self): self.history[self.curr_page_idx].request() @property def current_page(self): return self.history[self.curr_page_idx] def follow_link(self, link_index: int): logger.info(link_index) url = self.current_page.links[link_index] if url.startswith('nex://'): self.load(url) else: url = '/'.join([self.current_page.url.rstrip('/'), url]) self.load(url) class Page: def __init__(self, url): self.url = url self.host, self.selector = parse_url(url) self.raw_content = '' self.content = self.raw_content self.links = ['' for i in range(10)] def request(self): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: try: sock.connect((self.host, PORT)) except ConnectionError as e: logger.error(e) self.content = f'Error connecting to {self.host}' return selector = self.selector + '\r\n' sock.sendall(selector.encode('ascii')) buf = bytearray() while True: data = sock.recv(4096) if len(data) == 0: break buf.extend(data) try: self.raw_content = buf.decode('ascii') except UnicodeDecodeError as e: logger.error(e) self.content = 'Content could not be decoded' return self.content = self.raw_content self._parse_links() for i, l in enumerate(self.links): self.content = re.sub(l, f'[bold cyan]\[{i}][/] {l}', self.content, 1) @property def title(self): return f'nex://{self.url}' def _parse_links(self): pattern = re.compile(r'=>\s*([\w/:\.~-]*)\s*') self.links = pattern.findall(self.raw_content) # Find links # Determine if absolute or relative # Parse out relative directory navigation # Store parsed URLs in array # Add numbers to page content corresponding to array index def parse_url(url: str) -> Tuple[str, str]: clean_url = url.replace('nex://', '') split_url = clean_url.split('/', 1) if len(split_url) == 1: return clean_url, '' return split_url[0], split_url[1] class BrowserWindow(ptg.Window): overflow = ptg.Overflow.SCROLL vertical_align = ptg.VerticalAlignment.TOP def __init__(self, session: Session): super().__init__() self.session = session self.set_widgets([session.render()]) def update(self) -> None: self.set_widgets([session.render()]) def window_back(self, *args): self.session.back() self.update() def window_forward(self, *args): self.session.forward() self.update() def window_reload(self, *args): self.session.reload() self.update() def follow_link(self, key: str): index = int(key) self.session.follow_link(index) self.update() def _define_layout() -> ptg.Layout: layout = ptg.Layout() layout.add_slot("Header", height=1) layout.add_break() layout.add_slot("Body") layout.add_break() layout.add_slot("Footer", height=1) return layout def _create_footer(browser: BrowserWindow, manager: ptg.WindowManager): return ptg.Window( ptg.Splitter( ptg.KeyboardButton("Back", browser.window_back, bound='b'), ptg.KeyboardButton("Forward", browser.window_forward, bound='f'), ptg.KeyboardButton("Reload", browser.window_reload, bound='r'), ptg.KeyboardButton("Quit", lambda *_: manager.stop(), bound='q') ), box="EMPTY", is_persistant=True ) if __name__ == '__main__': session = Session() if len(sys.argv) > 1: url = sys.argv[1] if len(sys.argv) > 2 and sys.argv[2] == '--notui': url = sys.argv[1] session.load(url) print(session.render()) session.follow_link(0) print(session.render()) sys.exit(0) session.load(url) browser = BrowserWindow(session) with ptg.WindowManager() as manager: manager.layout = _define_layout() header = ptg.Window(f"[bold accent]{session.current_page.title}", box="EMPTY", is_persistant=True) manager.add(header) manager.add(browser, assign='body') manager.add(_create_footer(browser, manager), assign='footer') manager.bind('j', lambda *_: browser.scroll(5)) manager.bind(ptg.keys.DOWN, lambda *_: browser.scroll(5)) manager.bind('k', lambda *_: browser.scroll(-5)) manager.bind(ptg.keys.UP, lambda *_: browser.scroll(-5)) manager.bind('q', lambda *_: manager.stop()) for i in range(10): manager.bind(f'{i}', lambda widget, key: browser.follow_link(key)) logger.info(manager.bindings)