diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87522e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.venv +*.log diff --git a/nex.py b/nex.py index fcf5391..95d50b9 100644 --- a/nex.py +++ b/nex.py @@ -1,45 +1,76 @@ 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.current_page = 0 + self.curr_page_idx = 0 def back(self): - if self.current_page > 0: - self.current_page -= 1 + if self.curr_page_idx > 0: + self.curr_page_idx -= 1 self.render() def forward(self): - if self.current_page < len(self.history) - 1: - self.current_page += 1 + if self.curr_page_idx < len(self.history) - 1: + self.curr_page_idx += 1 self.render() def load(self, url): - self.current_page += 1 - if self.current_page >= len(self.history): + self.curr_page_idx += 1 + if self.curr_page_idx >= len(self.history): self.history.append(Page(url)) else: - self.history[self.current_page] = Page(url) - self.history[self.current_page].request() + self.history[self.curr_page_idx] = Page(url) + self.history[self.curr_page_idx].request() def render(self): - print(self.history[self.current_page].content) + 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.content = '' + 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: - sock.connect((self.host, PORT)) + 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() @@ -48,10 +79,33 @@ class Page: if len(data) == 0: break buf.extend(data) - self.content = buf.decode('ascii') + 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) -> (str, str): +def parse_url(url: str) -> Tuple[str, str]: clean_url = url.replace('nex://', '') split_url = clean_url.split('/', 1) if len(split_url) == 1: @@ -59,14 +113,88 @@ def parse_url(url: str) -> (str, str): 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() - while True: - url = input('Enter a URL') + 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) - session.render() - command = input('Go [b]ack or [f]orward') - if command == 'b': - session.back() - elif command == 'f': - session.forward() + 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)