From 1a8c515922cf36e4fe6e926028eb21e19a940673 Mon Sep 17 00:00:00 2001 From: Ivan Habunek Date: Wed, 28 Aug 2019 15:32:57 +0200 Subject: [PATCH] Implement thread (status context) view --- toot/tui/app.py | 57 +++++++++++++++++++++++++++++++------ toot/tui/timeline.py | 67 +++++++++++++++++++++++++++++--------------- toot/tui/utils.py | 38 ++++++++++++++++++++----- 3 files changed, 123 insertions(+), 39 deletions(-) diff --git a/toot/tui/app.py b/toot/tui/app.py index bbd4505..f3ad7da 100644 --- a/toot/tui/app.py +++ b/toot/tui/app.py @@ -161,13 +161,49 @@ class TUI(urwid.Frame): future.add_done_callback(_done) def build_timeline(self, statuses): - timeline = Timeline(self, statuses) - urwid.connect_signal(timeline, "status_focused", - lambda _, args: self.status_focused(*args)) - urwid.connect_signal(timeline, "next", - lambda *args: self.async_load_statuses(is_initial=False)) + def _close(*args): + raise urwid.ExitMainLoop() + + def _next(*args): + self.async_load_statuses(is_initial=False) + + def _focus(timeline): + self.refresh_footer(timeline) + + def _thread(timeline, status): + self.show_thread(status) + + timeline = Timeline("home", self, statuses) + urwid.connect_signal(timeline, "focus", _focus) + urwid.connect_signal(timeline, "next", _next) + urwid.connect_signal(timeline, "close", _close) + urwid.connect_signal(timeline, "thread", _thread) return timeline + def show_thread(self, status): + def _close(*args): + self.body = self.timeline + self.body.refresh_status_details() + self.refresh_footer(self.timeline) + + def _focus(timeline): + self.refresh_footer(timeline) + + # This is pretty fast, so it's probably ok to block while context is + # loaded, can be made async later if needed + context = api.context(self.app, self.user, status.id) + ancestors = [Status(s, self.app.instance) for s in context["ancestors"]] + descendants = [Status(s, self.app.instance) for s in context["descendants"]] + focus = len(ancestors) + + statuses = ancestors + [status] + descendants + timeline = Timeline("thread", self, statuses, focus, is_thread=True) + urwid.connect_signal(timeline, "focus", _focus) + urwid.connect_signal(timeline, "close", _close) + + self.body = timeline + self.refresh_footer(timeline) + def async_load_statuses(self, is_initial): """Asynchronously load a list of statuses.""" @@ -185,7 +221,8 @@ class TUI(urwid.Frame): def _done_initial(statuses): """Process initial batch of statuses, construct a Timeline.""" self.timeline = self.build_timeline(statuses) - self.timeline.status_focused() # Draw first status + self.timeline.refresh_status_details() # Draw first status + self.refresh_footer(self.timeline) self.body = self.timeline def _done_next(statuses): @@ -196,10 +233,12 @@ class TUI(urwid.Frame): self.run_in_thread(_load_statuses, done_callback=_done_initial if is_initial else _done_next) - def status_focused(self, status, index, count): + def refresh_footer(self, timeline): + """Show status details in footer.""" + status, index, count = timeline.get_focused_status_with_counts() self.footer.set_status([ - ("footer_status_bold", "[home] "), status.id, - " - status ", str(index + 1), " of ", str(count), + ("footer_status_bold", "[{}] ".format(timeline.name)), + status.id, " - status ", str(index + 1), " of ", str(count), ]) def show_status_source(self, status): diff --git a/toot/tui/timeline.py b/toot/tui/timeline.py index c01d2b9..ab51575 100644 --- a/toot/tui/timeline.py +++ b/toot/tui/timeline.py @@ -4,7 +4,7 @@ import webbrowser from toot.utils import format_content -from .utils import highlight_hashtags, parse_datetime +from .utils import highlight_hashtags, parse_datetime, highlight_keys from .widgets import SelectableText, SelectableColumns logger = logging.getLogger("toot") @@ -15,17 +15,19 @@ class Timeline(urwid.Columns): Displays a list of statuses to the left, and status details on the right. """ signals = [ - "status_focused", - "status_activated", + "focus", "next", + "close", + "thread", ] - def __init__(self, tui, statuses): + def __init__(self, name, tui, statuses, focus=0, is_thread=False): + self.name = name self.tui = tui + self.is_thread = is_thread self.statuses = statuses - - self.status_list = self.build_status_list(statuses) - self.status_details = StatusDetails(statuses[0]) + self.status_list = self.build_status_list(statuses, focus=focus) + self.status_details = StatusDetails(statuses[focus], is_thread) super().__init__([ ("weight", 40, self.status_list), @@ -33,10 +35,11 @@ class Timeline(urwid.Columns): ("weight", 60, self.status_details), ], dividechars=1) - def build_status_list(self, statuses): + def build_status_list(self, statuses, focus): items = [self.build_list_item(status) for status in statuses] walker = urwid.SimpleFocusListWalker(items) - urwid.connect_signal(walker, "modified", self.status_focused) + walker.set_focus(focus) + urwid.connect_signal(walker, "modified", self.modified) return urwid.ListBox(walker) def build_list_item(self, status): @@ -57,17 +60,27 @@ class Timeline(urwid.Columns): status = self.get_focused_status() self._emit("status_activated", [status]) - def status_focused(self): + def get_focused_status_with_counts(self): + """Returns status, status index in list and number of statuses""" + return ( + self.get_focused_status(), + self.status_list.body.focus, + len(self.statuses), + ) + + def modified(self): """Called when the list focus switches to a new status""" + status, index, count = self.get_focused_status_with_counts() + self.draw_status_details(status) + self._emit("focus") + + def refresh_status_details(self): + """Redraws the details of the focused status.""" status = self.get_focused_status() self.draw_status_details(status) - index = self.status_list.body.focus - count = len(self.statuses) - self._emit("status_focused", [status, index, count]) - def draw_status_details(self, status): - self.status_details = StatusDetails(status) + self.status_details = StatusDetails(status, self.is_thread) self.contents[2] = self.status_details, ("weight", 50, False) def keypress(self, size, key): @@ -94,6 +107,15 @@ class Timeline(urwid.Columns): self.tui.async_toggle_favourite(status) return + if key in ('q', 'Q'): + self._emit("close") + return + + if key in ('t', 'T'): + status = self.get_focused_status() + self._emit("thread", status) + return + if key in ("v", "V"): status = self.get_focused_status() if status.data["url"]: @@ -142,7 +164,8 @@ class Timeline(urwid.Columns): class StatusDetails(urwid.Pile): - def __init__(self, status): + def __init__(self, status, in_thread): + self.in_thread = in_thread widget_list = list(self.content_generator(status)) return super().__init__(widget_list) @@ -180,13 +203,11 @@ class StatusDetails(urwid.Pile): # Push things to bottom yield ("weight", 1, urwid.SolidFill(" ")) - yield ("pack", urwid.Text([ - ("cyan_bold", "B"), ("cyan", "oost"), " | ", - ("cyan_bold", "F"), ("cyan", "avourite"), " | ", - ("cyan_bold", "V"), ("cyan", "iew"), " | ", - ("cyan", "So"), ("cyan_bold", "u"), ("cyan", "rce"), " | ", - ("cyan_bold", "H"), ("cyan", "elp"), " ", - ])) + + options = "[B]oost [F]avourite [V]iew {}So[u]rce [H]elp".format( + "[T]hread " if not self.in_thread else "") + options = highlight_keys(options, "cyan_bold", "cyan") + yield ("pack", urwid.Text(options)) def build_linebox(self, contents): contents = urwid.Pile(list(contents)) diff --git a/toot/tui/utils.py b/toot/tui/utils.py index 604230a..ef0d497 100644 --- a/toot/tui/utils.py +++ b/toot/tui/utils.py @@ -5,13 +5,37 @@ from datetime import datetime HASHTAG_PATTERN = re.compile(r'(?>> highlight_keys("[P]rint [V]iew", "blue") + >>> [('blue', 'P'), 'rint ', ('blue', 'V'), 'iew'] + """ + def _gen(): + highlighted = False + for part in re.split("\\[|\\]", text): + if part: + if highlighted: + yield (high_attr, part) if high_attr else part + else: + yield (low_attr, part) if low_attr else part + highlighted = not highlighted + return list(_gen()) + + +def highlight_hashtags(line, attr="hashtag"): + return [ + (attr, p) if p.startswith("#") else p + for p in re.split(HASHTAG_PATTERN, line) + ]