pynex/nex.py

201 lines
6.0 KiB
Python

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)