#!/usr/bin/env python3 from textual.app import App, ComposeResult from textual.widgets import Input, Button, Static, Footer from textual.containers import Container from textual.binding import Binding from textual.events import Resize from mime.gemtext import Gemtext from mime.plaintext import Plaintext from mime.highlightedcode import HighlightedCode,mimetolexer from protocol.gemini import GeminiProtocol from protocol.data import DataProtocol from protocol.http import HttpProtocol from protocol.about import AboutProtocol from subprocess import Popen, DEVNULL from gemurllib.parse import urljoin protocols = { "gemini": GeminiProtocol, "data": DataProtocol, "http": HttpProtocol, "https": HttpProtocol, "about": AboutProtocol, } class Browset(App): url = "about:blank" CSS_PATH = "browset.css" BINDINGS = [ Binding("ctrl+q,ctrl+c", "app.quit", "Quit", show=True), Binding("ctrl+left", "back()", "Back", show=False), Binding("ctrl+right", "soon()", "Soon", show=False), Binding("ctrl+up", "top()", "Top", show=False), Binding("ctrl+o", "external_open()", "Open Externally"), ] history = [] fistory = [] # forward history content = ["#h1", "## hey [b]Is this unformatted?[/b]", "```startpre","in preformatted text this hsould have a scrollbar if it is too wide oh yeah tonight is gonna be great.", "``` (ended pre)", "afterward"] def compose(self) -> ComposeResult: yield Footer() yield Container( Button("🔙", variant='primary', name='back', classes='mobile'), # ⏪ Button("🔝", variant='primary', name='top'), # ⏫ Button("🔜", variant='primary', name='soon'), # ⏩ Button("🔄", variant='primary', name='refresh'), # 🔁 Input(placeholder="Enter URI", id="url"), id="toolbar" ) yield Gemtext(fp=self.content, id="content") async def on_input_submitted(self, message: Input.Submitted) -> None: self._do_url(message.value, setbar=False) async def on_button_pressed(self, event: Button.Pressed) -> None: if event.button.variant == 'primary': if event.button.name == 'refresh': self._do_url(self.url) elif event.button.name == 'back': self.action_back() elif event.button.name == 'soon': self.action_soon() elif event.button.name == 'top': self.action_top() else: url = event.button.name self._do_url(url) async def on_resize(self, event: Resize) -> None: toolbar = self.query_one("#toolbar") if event.size.width < 60: toolbar.add_class('mobile') else: toolbar.remove_class('mobile') def action_external_open(self): # Run this command in the background Popen(["xdg-open", self.url], stdout=DEVNULL, stderr=DEVNULL) def action_back(self): if len(self.history): self.fistory.append(self.url) url = self.history.pop() self._do_url(url, histore=False, clearF=False) def action_soon(self): if len(self.fistory): url = self.fistory.pop() self._do_url(url, clearF=False) def action_top(self): if self.url[-1] == '/': self._do_url('../') elif self.url: self._do_url('./') def _do_url(self, url, histore=True, setbar=True, clearF=True): if not ":" in url: url = urljoin(self.url,url) if clearF: self.fistory = [] if setbar: input = self.query_one("#url") input.value = url input.action_end() if histore: self.history.append(self.url) self.url = url protocol = url.split(":")[0] if protocol in protocols: (mime, fp) = protocols[protocol].get(url) else: (mime, fp) = ("text/error", ["Unsupported protocol: " + protocol]) self.query_one("#content").remove() if "text/gemini" in mime: content = Gemtext(fp=fp, id="content") elif HighlightedCode.can_handle_mime(mime): content = HighlightedCode(fp=fp, id="content", mime=mime) elif "text/" in mime: content = Plaintext(fp=fp, id="content") else: content = Plaintext(fp=["Unhandled mimetype: " + mime], id="content") self.mount(content) if __name__ == "__main__": app = Browset() app.run()