Add an action to display status links and open them
We add a new [L]inks action that opens an overlay window with links found in the content of selected status. Links are selectable and upon click/enter we open the web browser at link's URL.
This commit is contained in:
parent
5fc46d0cfc
commit
28e1281187
|
@ -8,10 +8,10 @@ from toot import api, __version__
|
||||||
from .compose import StatusComposer
|
from .compose import StatusComposer
|
||||||
from .constants import PALETTE
|
from .constants import PALETTE
|
||||||
from .entities import Status
|
from .entities import Status
|
||||||
from .overlays import ExceptionStackTrace, GotoMenu, Help, StatusSource
|
from .overlays import ExceptionStackTrace, GotoMenu, Help, StatusSource, StatusLinks
|
||||||
from .overlays import StatusDeleteConfirmation
|
from .overlays import StatusDeleteConfirmation
|
||||||
from .timeline import Timeline
|
from .timeline import Timeline
|
||||||
from .utils import show_media
|
from .utils import parse_content_links, show_media
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -182,6 +182,9 @@ class TUI(urwid.Frame):
|
||||||
def _source(timeline, status):
|
def _source(timeline, status):
|
||||||
self.show_status_source(status)
|
self.show_status_source(status)
|
||||||
|
|
||||||
|
def _links(timeline, status):
|
||||||
|
self.show_links(status)
|
||||||
|
|
||||||
def _media(timeline, status):
|
def _media(timeline, status):
|
||||||
self.show_media(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, "reblog", self.async_toggle_reblog)
|
||||||
urwid.connect_signal(timeline, "reply", _reply)
|
urwid.connect_signal(timeline, "reply", _reply)
|
||||||
urwid.connect_signal(timeline, "source", _source)
|
urwid.connect_signal(timeline, "source", _source)
|
||||||
|
urwid.connect_signal(timeline, "links", _links)
|
||||||
|
|
||||||
def build_timeline(self, name, statuses):
|
def build_timeline(self, name, statuses):
|
||||||
def _close(*args):
|
def _close(*args):
|
||||||
|
@ -302,6 +306,14 @@ class TUI(urwid.Frame):
|
||||||
title="Status source",
|
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):
|
def show_exception(self, exception):
|
||||||
self.open_overlay(
|
self.open_overlay(
|
||||||
widget=ExceptionStackTrace(exception),
|
widget=ExceptionStackTrace(exception),
|
||||||
|
|
|
@ -20,6 +20,20 @@ class StatusSource(urwid.ListBox):
|
||||||
super().__init__(walker)
|
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):
|
class ExceptionStackTrace(urwid.ListBox):
|
||||||
"""Shows an exception stack trace."""
|
"""Shows an exception stack trace."""
|
||||||
def __init__(self, ex):
|
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(" [R] - Reply to current status"))
|
||||||
yield urwid.Text(h(" [S] - Show text marked as sensitive"))
|
yield urwid.Text(h(" [S] - Show text marked as sensitive"))
|
||||||
yield urwid.Text(h(" [T] - Show status thread (replies)"))
|
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(" [U] - Show the status data in JSON as received from the server"))
|
||||||
yield urwid.Text(h(" [V] - Open status in default browser"))
|
yield urwid.Text(h(" [V] - Open status in default browser"))
|
||||||
yield urwid.Divider()
|
yield urwid.Divider()
|
||||||
|
|
|
@ -26,6 +26,7 @@ class Timeline(urwid.Columns):
|
||||||
"reblog", # Reblog status
|
"reblog", # Reblog status
|
||||||
"reply", # Compose a reply to a status
|
"reply", # Compose a reply to a status
|
||||||
"source", # Show status source
|
"source", # Show status source
|
||||||
|
"links", # Show status links
|
||||||
"thread", # Show thread for status
|
"thread", # Show thread for status
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -140,6 +141,10 @@ class Timeline(urwid.Columns):
|
||||||
self.refresh_status_details()
|
self.refresh_status_details()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if key in ("l", "L"):
|
||||||
|
self._emit("links", status)
|
||||||
|
return
|
||||||
|
|
||||||
if key in ("t", "T"):
|
if key in ("t", "T"):
|
||||||
self._emit("thread", status)
|
self._emit("thread", status)
|
||||||
return
|
return
|
||||||
|
@ -281,6 +286,7 @@ class StatusDetails(urwid.Pile):
|
||||||
"[F]avourite",
|
"[F]avourite",
|
||||||
"[V]iew",
|
"[V]iew",
|
||||||
"[T]hread" if not self.in_thread else "",
|
"[T]hread" if not self.in_thread else "",
|
||||||
|
"[L]inks",
|
||||||
"[R]eply",
|
"[R]eply",
|
||||||
"So[u]rce",
|
"So[u]rce",
|
||||||
"[H]elp",
|
"[H]elp",
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from html.parser import HTMLParser
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
@ -74,3 +75,30 @@ def show_media(paths):
|
||||||
raise Exception("Cannot find an image viewer")
|
raise Exception("Cannot find an image viewer")
|
||||||
|
|
||||||
subprocess.run([viewer] + paths)
|
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 <a> 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[:]
|
||||||
|
|
Loading…
Reference in New Issue