diff --git a/README.md b/README.md index 689a769..4f003c1 100644 --- a/README.md +++ b/README.md @@ -303,6 +303,26 @@ Supported chat commands: - `/edcode`: extract code blocks from the most recent message and open `app.ed.edbuf()` on the contents - `/exit` or `/quit`: exit the chat applet +### `app.bro` + +An implementation of DeckBro, a simple text-only HTTP browser for [DeckText](decktext-spec.md) and simple HTML documents. + +Usage: `from app.bro import bro`, then `bro(starting_url)`. + +Available commands: + +r)ef b)ack f)wd n)ext p)rev l)ink or pg#: q + +- `g [url]`: go to a new URL +- `r`: refresh the current document +- `b`: go back in history +- `f`: go forward in history +- `n`: go to the next DeckText page of the document (this is the default action if you just press Enter) +- `p`: go to the previous DeckText page of the document +- `[num]`: jump to the page `[num]` if it exists +- `l [num]`: visit the link number `[num]` on the current page (if it exists) +- `q`: quit the browser + ### `app.blog` A simple interface to any POST-based blog API that supports HTTP Basic Auth. diff --git a/app/bro.py b/app/bro.py new file mode 100644 index 0000000..474e87b --- /dev/null +++ b/app/bro.py @@ -0,0 +1,146 @@ +# DeckBro: a simple DeckText/HTML browser for T-DeckARD environment +# Usage: from app.bro import DeckBro +# Created by Luxferre in 2025-2026, released into public domain + +from deck.pager import print_paged +from deck.input import input, input_multi +from deck.menu import menu +from deck.http import http_fetch, http_set_ua +from deck.text import DeckText + +class DeckBro: + def __init__(self, start_url=None): + self.history = [] + self.rendered_pages = [] + self.history_pos = 0 # always refers to the current slot + self.url = start_url + self.doc = DeckText() + if self.url: + self.go(self.url) + + def render(self): + pagenum = 1 + pagetotal = len(self.doc.pages) + self.rendered_pages = [] + for page in self.doc.pages: + outtext = f'{self.doc.title} ({pagenum}/{pagetotal})\n---\n' + outtext += page['content'] + if len(page['links']) > 0: + linknum = 1 + outtext += '\n--- Links ---\n' + for l in page['links']: + outtext += f'[{linknum}] {l[1]}: {l[0]}\n' + linknum += 1 + self.rendered_pages.append(outtext) + pagenum += 1 + return len(self.rendered_pages) + + def display(self): + curpage = 1 + totalpages = len(self.rendered_pages) + menustring = 'g)o r)ef b)ack f)wd n)ext p)rev l)ink or pg#: ' + do_new_display = True + while True: + if do_new_display and curpage > 0 and curpage <= totalpages: + print_paged(self.rendered_pages[curpage - 1]) + print(f'[{self.url}]') + user_choice = input(menustring).strip() + pgnum = None + try: # page number input detection + pgnum = int(user_choice) + except: pass + if pgnum is not None: + if pgnum > 0 and pgnum <= totalpages: + curpage = pgnum + do_new_display = True + else: + print('Invalid page number!') + do_new_display = False + continue + user_opt = '' + try: + user_opt = ' '.join(user_choice.split(' ')[1:]) + except: pass + if user_choice == '': + user_choice = 'n' # go to the next page by default + ch_letter = user_choice[0].lower() + if ch_letter == 'q': break + elif ch_letter == 'n': + do_new_display = True + curpage += 1 + if curpage > totalpages: # wrap around + curpage = 1 + elif ch_letter == 'p': + do_new_display = True + curpage -= 1 + if curpage < 1: # wrap around + curpage = totalpages + elif ch_letter == 'g': + self.go(user_opt) + break + elif ch_letter == 'l': + addr = None + try: + lnum = int(user_opt) + addr = self.doc.pages[curpage - 1]['links'][lnum - 1][0] + except: pass + if addr is None: + print('Invalid link number entered!') + do_new_display = False + else: + self.go(addr) + break + elif ch_letter == 'r': + self.refresh() + break + elif ch_letter == 'b': + if self.back() is not None: + break + else: + do_new_display = False + elif ch_letter == 'f': + if self.forward() is not None: + break + else: + do_new_display = False + + def go(self, url=None, update_history=True): + if url: + self.url = url.strip() + status, pagetext = http_fetch(self.url) + if status != 200: + if pagetext == '': + pagetext = f'Error {status}' + else: + pagetext = f'Error {status}: \n{pagetext}' + self.doc.load(pagetext) + if len(self.doc.pages) > 0 and update_history: + self.history = self.history[0:self.history_pos+1] + self.history.append(self.url) + self.history_pos = len(self.history) - 1 + self.render() + self.display() + + def refresh(self): + self.go(self.url, update_history=False) + + def back(self): + if self.history_pos > 0: + self.history_pos -= 1 + new_url = self.history[self.history_pos] + self.go(new_url, update_history=False) + return new_url + return None + + def forward(self): + if self.history_pos < len(self.history) - 1: + self.history_pos += 1 + new_url = self.history[self.history_pos] + self.go(new_url, update_history=False) + return new_url + return None + +# create and start a default DeckBro instance +def bro(url=None): + inst = DeckBro() + inst.go(url) diff --git a/tdeckboot.py b/tdeckboot.py index 723c12f..6bd5df3 100644 --- a/tdeckboot.py +++ b/tdeckboot.py @@ -15,6 +15,7 @@ from deck.http import auth_header, url_escape, form_encode from deck.otp import totp, hotp, get_epoch from deck import xlat from app.ed import edit, view, edbuf, viewbuf, edurl, viewurl +from app.bro import bro from app.blog import blogpost, blogpost_str, bloged from app.llmchat import llmchat try: