diff --git a/toot/tui/app.py b/toot/tui/app.py index 8daa03c..1490e6b 100644 --- a/toot/tui/app.py +++ b/toot/tui/app.py @@ -8,10 +8,10 @@ from toot import api, __version__ from .compose import StatusComposer from .constants import PALETTE from .entities import Status -from .overlays import ExceptionStackTrace, GotoMenu, Help, StatusSource +from .overlays import ExceptionStackTrace, GotoMenu, Help, StatusSource, StatusLinks from .overlays import StatusDeleteConfirmation from .timeline import Timeline -from .utils import show_media +from .utils import parse_content_links, show_media logger = logging.getLogger(__name__) @@ -182,6 +182,9 @@ class TUI(urwid.Frame): def _source(timeline, status): self.show_status_source(status) + def _links(timeline, status): + self.show_links(status) + def _media(timeline, status): self.show_media(status) @@ -197,6 +200,7 @@ class TUI(urwid.Frame): urwid.connect_signal(timeline, "reblog", self.async_toggle_reblog) urwid.connect_signal(timeline, "reply", _reply) urwid.connect_signal(timeline, "source", _source) + urwid.connect_signal(timeline, "links", _links) def build_timeline(self, name, statuses): def _close(*args): @@ -302,6 +306,14 @@ class TUI(urwid.Frame): title="Status source", ) + def show_links(self, status): + links = parse_content_links(status.data["content"]) + self.open_overlay( + widget=StatusLinks(links), + title="Status links", + options={"height": len(links) + 2}, + ) + def show_exception(self, exception): self.open_overlay( widget=ExceptionStackTrace(exception), diff --git a/toot/tui/overlays.py b/toot/tui/overlays.py index 64f7872..734b0dc 100644 --- a/toot/tui/overlays.py +++ b/toot/tui/overlays.py @@ -20,6 +20,20 @@ class StatusSource(urwid.ListBox): super().__init__(walker) +class StatusLinks(urwid.ListBox): + """Shows status links.""" + + def __init__(self, links): + + def widget(url, title): + return Button(title or url, on_press=lambda btn: webbrowser.open(url)) + + walker = urwid.SimpleFocusListWalker( + [widget(url, title) for url, title in links] + ) + super().__init__(walker) + + class ExceptionStackTrace(urwid.ListBox): """Shows an exception stack trace.""" def __init__(self, ex): @@ -132,6 +146,7 @@ class Help(urwid.Padding): yield urwid.Text(h(" [R] - Reply to current status")) yield urwid.Text(h(" [S] - Show text marked as sensitive")) yield urwid.Text(h(" [T] - Show status thread (replies)")) + yield urwid.Text(h(" [L] - Show the status links")) yield urwid.Text(h(" [U] - Show the status data in JSON as received from the server")) yield urwid.Text(h(" [V] - Open status in default browser")) yield urwid.Divider() diff --git a/toot/tui/timeline.py b/toot/tui/timeline.py index a414c1d..15fda64 100644 --- a/toot/tui/timeline.py +++ b/toot/tui/timeline.py @@ -26,6 +26,7 @@ class Timeline(urwid.Columns): "reblog", # Reblog status "reply", # Compose a reply to a status "source", # Show status source + "links", # Show status links "thread", # Show thread for status ] @@ -140,6 +141,10 @@ class Timeline(urwid.Columns): self.refresh_status_details() return + if key in ("l", "L"): + self._emit("links", status) + return + if key in ("t", "T"): self._emit("thread", status) return @@ -281,6 +286,7 @@ class StatusDetails(urwid.Pile): "[F]avourite", "[V]iew", "[T]hread" if not self.in_thread else "", + "[L]inks", "[R]eply", "So[u]rce", "[H]elp", diff --git a/toot/tui/utils.py b/toot/tui/utils.py index 90f85ad..6d0cac4 100644 --- a/toot/tui/utils.py +++ b/toot/tui/utils.py @@ -1,3 +1,4 @@ +from html.parser import HTMLParser import re import shutil import subprocess @@ -74,3 +75,30 @@ def show_media(paths): raise Exception("Cannot find an image viewer") subprocess.run([viewer] + paths) + + +class LinkParser(HTMLParser): + + def reset(self): + super().reset() + self.links = [] + + def handle_starttag(self, tag, attrs): + if tag == "a": + href, title = None, None + for name, value in attrs: + if name == "href": + href = value + if name == "title": + title = value + if href: + self.links.append((href, title)) + + +def parse_content_links(content): + """Parse tags from status's `content` and return them as a list of + (href, title), where `title` may be None. + """ + parser = LinkParser() + parser.feed(content) + return parser.links[:]