2023-06-19 08:00:03 +00:00
|
|
|
import sys
|
|
|
|
import socket
|
2023-07-02 22:19:00 +00:00
|
|
|
import re
|
|
|
|
import logging
|
2023-07-05 00:23:03 +00:00
|
|
|
import pytermgui as ptg
|
|
|
|
from typing import Tuple
|
2023-06-19 08:00:03 +00:00
|
|
|
|
|
|
|
PORT = 1900
|
|
|
|
|
2023-07-02 22:19:00 +00:00
|
|
|
logger = logging.getLogger('nex')
|
|
|
|
logger.setLevel(logging.INFO)
|
|
|
|
logger.addHandler(logging.FileHandler('nex.log'))
|
|
|
|
|
2023-06-19 08:00:03 +00:00
|
|
|
|
2023-07-03 08:52:14 +00:00
|
|
|
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]
|
2023-06-19 08:00:03 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Page:
|
|
|
|
def __init__(self, url):
|
|
|
|
self.url = url
|
|
|
|
self.host, self.selector = parse_url(url)
|
2023-07-02 22:19:00 +00:00
|
|
|
self.raw_content = ''
|
|
|
|
self.content = self.raw_content
|
|
|
|
self.links = ['' for i in range(10)]
|
2023-06-19 08:00:03 +00:00
|
|
|
|
|
|
|
def request(self):
|
|
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
2023-07-02 22:19:00 +00:00
|
|
|
try:
|
|
|
|
sock.connect((self.host, PORT))
|
|
|
|
except ConnectionError as e:
|
|
|
|
logger.error(e)
|
2023-07-03 08:52:14 +00:00
|
|
|
self.content = f'[error]Could not connect to {self.host}'
|
2023-07-02 22:19:00 +00:00
|
|
|
return
|
2023-06-19 08:00:03 +00:00
|
|
|
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)
|
2023-07-02 22:19:00 +00:00
|
|
|
try:
|
|
|
|
self.raw_content = buf.decode('ascii')
|
|
|
|
except UnicodeDecodeError as e:
|
|
|
|
logger.error(e)
|
2023-07-03 08:52:14 +00:00
|
|
|
self.content = '[warning]Content could not be decoded'
|
2023-07-02 22:19:00 +00:00
|
|
|
return
|
|
|
|
self.content = self.raw_content
|
|
|
|
self._parse_links()
|
|
|
|
for i, l in enumerate(self.links):
|
2023-07-03 08:52:14 +00:00
|
|
|
self.content = re.sub(l, f'[bold primary]\[{i}][/] {l}',
|
2023-07-02 22:19:00 +00:00
|
|
|
self.content, 1)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def title(self):
|
2023-07-05 00:23:03 +00:00
|
|
|
prefix = 'nex://'
|
|
|
|
if self.url.startswith(prefix):
|
|
|
|
return self.url[len(prefix):]
|
|
|
|
return self.url
|
2023-06-19 08:00:03 +00:00
|
|
|
|
2023-07-02 22:19:00 +00:00
|
|
|
def _parse_links(self):
|
|
|
|
pattern = re.compile(r'=>\s*([\w/:\.~-]*)\s*')
|
|
|
|
self.links = pattern.findall(self.raw_content)
|
2023-06-19 08:00:03 +00:00
|
|
|
|
2023-07-02 22:19:00 +00:00
|
|
|
|
2023-07-05 00:23:03 +00:00
|
|
|
def create_landing_page() -> Page:
|
|
|
|
landing_page = Page('about:home')
|
|
|
|
landing_page.raw_content = 'Welcome to PyNex'
|
|
|
|
landing_page.content = landing_page.raw_content
|
|
|
|
return landing_page
|
2023-06-19 08:00:03 +00:00
|
|
|
|
|
|
|
|
2023-07-03 08:52:14 +00:00
|
|
|
class Session:
|
|
|
|
def __init__(self):
|
2023-07-05 00:23:03 +00:00
|
|
|
home_page = create_landing_page()
|
|
|
|
self.pages = {home_page.url: home_page}
|
|
|
|
self.history = [home_page.url]
|
2023-07-03 08:52:14 +00:00
|
|
|
self.curr_index = 0
|
|
|
|
|
|
|
|
@property
|
|
|
|
def current_page(self):
|
|
|
|
return self.pages[self.history[self.curr_index]]
|
|
|
|
|
|
|
|
def prev(self):
|
|
|
|
if self.curr_index > 0:
|
|
|
|
self.curr_index -= 1
|
|
|
|
return self.current_page
|
|
|
|
|
|
|
|
def next(self):
|
|
|
|
if self.curr_index < len(self.history) - 1:
|
|
|
|
self.curr_index += 1
|
|
|
|
return self.current_page
|
|
|
|
|
|
|
|
def new_page(self, url):
|
|
|
|
self.curr_index += 1
|
|
|
|
if self.curr_index >= len(self.history):
|
|
|
|
self.history.append(url)
|
|
|
|
else:
|
|
|
|
self.history[self.curr_index] = url
|
|
|
|
page = Page(url)
|
|
|
|
page.request()
|
|
|
|
self.pages[url] = page
|
|
|
|
return self.current_page
|
2023-07-02 22:19:00 +00:00
|
|
|
|
|
|
|
|
2023-07-03 08:52:14 +00:00
|
|
|
class Browser(ptg.WindowManager):
|
|
|
|
def __init__(self, session=Session(), *args):
|
2023-07-02 22:19:00 +00:00
|
|
|
super().__init__()
|
2023-07-03 08:52:14 +00:00
|
|
|
self.layout = ptg.Layout()
|
2023-07-02 22:19:00 +00:00
|
|
|
self.session = session
|
2023-07-05 00:23:03 +00:00
|
|
|
|
2023-07-03 08:52:14 +00:00
|
|
|
self.page_container = ptg.Container(self.session.current_page.content)
|
|
|
|
self.body = ptg.Window(self.page_container,
|
|
|
|
overflow=ptg.Overflow.SCROLL,
|
|
|
|
vertical_align=ptg.VerticalAlignment.TOP)
|
|
|
|
self.footer = ptg.Window(
|
|
|
|
ptg.Splitter(
|
2023-07-05 00:39:04 +00:00
|
|
|
["(B)ack", self.back],
|
|
|
|
["(F)orward", self.forward],
|
|
|
|
["(R)eload", self.reload],
|
|
|
|
["(Q)uit", lambda *_: self.stop()]
|
2023-07-03 08:52:14 +00:00
|
|
|
),
|
|
|
|
box="EMPTY",
|
|
|
|
is_persistant=True
|
|
|
|
)
|
|
|
|
|
|
|
|
self._create_layout()
|
|
|
|
self._create_key_bindings()
|
|
|
|
self.update()
|
2023-07-02 22:19:00 +00:00
|
|
|
|
2023-07-03 08:52:14 +00:00
|
|
|
def _create_key_bindings(self):
|
|
|
|
self.bind(ptg.keys.DOWN, lambda *_: self.body.scroll(5))
|
|
|
|
self.bind(ptg.keys.UP, lambda *_: self.body.scroll(-5))
|
2023-07-05 00:39:04 +00:00
|
|
|
self.bind('j', lambda *_: self.body.scroll(5))
|
|
|
|
self.bind('k', lambda *_: self.body.scroll(-5))
|
|
|
|
self.bind('b', self.back)
|
|
|
|
self.bind('f', self.forward)
|
|
|
|
self.bind('r', self.reload)
|
2023-07-03 08:52:14 +00:00
|
|
|
self.bind('q', lambda *_: self.stop())
|
2023-07-02 22:19:00 +00:00
|
|
|
|
2023-07-03 08:52:14 +00:00
|
|
|
for i in range(10):
|
|
|
|
self.bind(f'{i}', lambda widget, key: self.follow_link(key))
|
2023-07-02 22:19:00 +00:00
|
|
|
|
2023-07-03 08:52:14 +00:00
|
|
|
def _create_layout(self):
|
|
|
|
self.layout.add_slot("Body")
|
|
|
|
self.layout.add_break()
|
|
|
|
self.layout.add_slot("Footer", height=1)
|
|
|
|
|
|
|
|
self.add(self.body)
|
|
|
|
self.add(self.footer)
|
2023-07-02 22:19:00 +00:00
|
|
|
|
2023-07-03 08:52:14 +00:00
|
|
|
def update(self):
|
2023-07-05 00:23:03 +00:00
|
|
|
page = self.session.current_page
|
|
|
|
self.page_container.set_widgets([page.content])
|
|
|
|
self.body.set_title(f'[bold tertiary]{page.title}', 1)
|
2023-07-03 08:52:14 +00:00
|
|
|
|
|
|
|
def back(self, *args):
|
|
|
|
self.session.prev()
|
2023-07-02 22:19:00 +00:00
|
|
|
self.update()
|
|
|
|
|
2023-07-03 08:52:14 +00:00
|
|
|
def forward(self, *args):
|
|
|
|
self.session.next()
|
2023-07-02 22:19:00 +00:00
|
|
|
self.update()
|
|
|
|
|
2023-07-03 08:52:14 +00:00
|
|
|
def reload(self, *args):
|
|
|
|
self.session.current_page.request()
|
|
|
|
self.update()
|
2023-07-02 22:19:00 +00:00
|
|
|
|
2023-07-03 08:52:14 +00:00
|
|
|
def go(self, url):
|
|
|
|
self.session.new_page(url)
|
|
|
|
self.update()
|
2023-07-02 22:19:00 +00:00
|
|
|
|
2023-07-03 08:52:14 +00:00
|
|
|
def follow_link(self, key):
|
|
|
|
index = int(key)
|
|
|
|
url = self.session.current_page.links[index]
|
|
|
|
if url.startswith('nex://'):
|
|
|
|
self.session.new_page(url)
|
|
|
|
else:
|
|
|
|
url = '/'.join([self.session.current_page.url.rstrip('/'), url])
|
|
|
|
self.session.new_page(url)
|
|
|
|
self.update()
|
2023-07-02 22:19:00 +00:00
|
|
|
|
|
|
|
|
2023-07-03 08:52:14 +00:00
|
|
|
def main(url=None):
|
|
|
|
with Browser() as browser:
|
|
|
|
if url is not None:
|
|
|
|
browser.go(url)
|
2023-07-02 22:19:00 +00:00
|
|
|
|
|
|
|
|
2023-06-19 08:00:03 +00:00
|
|
|
if __name__ == '__main__':
|
2023-07-02 22:19:00 +00:00
|
|
|
if len(sys.argv) > 1:
|
|
|
|
url = sys.argv[1]
|
2023-07-03 08:52:14 +00:00
|
|
|
main(url)
|