From 4deccee7543a337e48328002dcc82e626c38e6f7 Mon Sep 17 00:00:00 2001 From: Ivan Habunek Date: Wed, 28 Aug 2019 17:00:46 +0200 Subject: [PATCH] Decouple Timeline and TUI, use signals instead --- toot/tui/NOTES.md | 5 +-- toot/tui/app.py | 86 ++++++++++++++++++++++---------------------- toot/tui/timeline.py | 29 ++++++++------- 3 files changed, 63 insertions(+), 57 deletions(-) diff --git a/toot/tui/NOTES.md b/toot/tui/NOTES.md index b70c657..2868538 100644 --- a/toot/tui/NOTES.md +++ b/toot/tui/NOTES.md @@ -18,10 +18,11 @@ TODO/Ideas: * Think about how to show media * download media and use local image viewer? * convert to ascii art? -* use signals to avoid tightly coupling components * interaction with clipboard - how to copy a status to clipbard? * Show **notifications** -* Show **threads** +* Status source + * shortcut to copy source + * syntax highlighting? Questions: * is it possible to make a span a urwid.Text selectable? e.g. for urls and hashtags diff --git a/toot/tui/app.py b/toot/tui/app.py index f3ad7da..044a08c 100644 --- a/toot/tui/app.py +++ b/toot/tui/app.py @@ -160,6 +160,19 @@ class TUI(urwid.Frame): future = self.executor.submit(fn, *args, **kwargs) future.add_done_callback(_done) + def connect_default_timeline_signals(self, timeline): + def _compose(*args): + self.show_compose() + + def _source(timeline, status): + self.show_status_source(status) + + urwid.connect_signal(timeline, "focus", self.refresh_footer) + urwid.connect_signal(timeline, "reblog", self.async_toggle_reblog) + urwid.connect_signal(timeline, "favourite", self.async_toggle_favourite) + urwid.connect_signal(timeline, "source", _source) + urwid.connect_signal(timeline, "compose", _compose) + def build_timeline(self, statuses): def _close(*args): raise urwid.ExitMainLoop() @@ -167,38 +180,36 @@ class TUI(urwid.Frame): 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) + timeline = Timeline("home", statuses) + + self.connect_default_timeline_signals(timeline) 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): + """When thread is closed, go back to the main timeline.""" 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"]] + statuses = ancestors + [status] + descendants focus = len(ancestors) - statuses = ancestors + [status] + descendants - timeline = Timeline("thread", self, statuses, focus, is_thread=True) - urwid.connect_signal(timeline, "focus", _focus) + timeline = Timeline("thread", statuses, focus, is_thread=True) + + self.connect_default_timeline_signals(timeline) urwid.connect_signal(timeline, "close", _close) self.body = timeline @@ -245,42 +256,25 @@ class TUI(urwid.Frame): self.open_overlay( widget=StatusSource(status), title="Status source", - options={ - "align": 'center', - "width": ('relative', 80), - "valign": 'middle', - "height": ('relative', 80), - }, ) def show_exception(self, exception): self.open_overlay( widget=ExceptionStackTrace(exception), title="Unhandled Exception", - options={ - "align": 'center', - "width": ('relative', 80), - "valign": 'middle', - "height": ('relative', 80), - }, ) def show_compose(self): + def _close(*args): + self.close_overlay() + + def _post(timeline, content, warning, visibility): + self.post_status(content, warning, visibility) + composer = StatusComposer() - urwid.connect_signal(composer, "close", - lambda *args: self.close_overlay()) - urwid.connect_signal(composer, "post", - lambda _, content, warning, visibility: self.post_status(content, warning, visibility)) - self.open_overlay( - widget=composer, - title="Compose status", - options={ - "align": 'center', - "width": ('relative', 80), - "valign": 'middle', - "height": ('relative', 80), - }, - ) + urwid.connect_signal(composer, "close", _close) + urwid.connect_signal(composer, "post", _post) + self.open_overlay(composer, title="Compose status") def post_status(self, content, warning, visibility): data = api.post_status(self.app, self.user, content, @@ -290,7 +284,7 @@ class TUI(urwid.Frame): self.footer.set_message("Status posted {} \\o/".format(status.id)) self.close_overlay() - def async_toggle_favourite(self, status): + def async_toggle_favourite(self, timeline, status): def _favourite(): logger.info("Favouriting {}".format(status)) api.favourite(self.app, self.user, status.id) @@ -303,14 +297,14 @@ class TUI(urwid.Frame): # Create a new Status with flipped favourited flag new_data = status.data new_data["favourited"] = not status.favourited - self.timeline.update_status(Status(new_data, status.instance)) + timeline.update_status(Status(new_data, status.instance)) self.run_in_thread( _unfavourite if status.favourited else _favourite, done_callback=_done ) - def async_toggle_reblog(self, status): + def async_toggle_reblog(self, timeline, status): def _reblog(): logger.info("Reblogging {}".format(status)) api.reblog(self.app, self.user, status.id) @@ -323,7 +317,7 @@ class TUI(urwid.Frame): # Create a new Status with flipped reblogged flag new_data = status.data new_data["reblogged"] = not status.reblogged - self.timeline.update_status(Status(new_data, status.instance)) + timeline.update_status(Status(new_data, status.instance)) self.run_in_thread( _unreblog if status.reblogged else _reblog, @@ -332,14 +326,22 @@ class TUI(urwid.Frame): # --- Overlay handling ----------------------------------------------------- + default_overlay_options = dict( + align="center", width=("relative", 80), + valign="middle", height=("relative", 80), + ) + def open_overlay(self, widget, options={}, title=""): top_widget = urwid.LineBox(widget, title=title) bottom_widget = self.body + _options = self.default_overlay_options + _options.update(options) + self.overlay = urwid.Overlay( top_widget, bottom_widget, - **options + **_options ) self.body = self.overlay diff --git a/toot/tui/timeline.py b/toot/tui/timeline.py index ab51575..6bbdd33 100644 --- a/toot/tui/timeline.py +++ b/toot/tui/timeline.py @@ -15,15 +15,18 @@ class Timeline(urwid.Columns): Displays a list of statuses to the left, and status details on the right. """ signals = [ - "focus", - "next", - "close", - "thread", + "close", # Close thread + "compose", # Compose a new toot + "favourite", # Favourite status + "focus", # Focus changed + "next", # Fetch more statuses + "reblog", # Reblog status + "source", # Show status source + "thread", # Show thread for status ] - def __init__(self, name, tui, statuses, focus=0, is_thread=False): + def __init__(self, name, 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, focus=focus) @@ -95,23 +98,23 @@ class Timeline(urwid.Columns): if key in ("b", "B"): status = self.get_focused_status() - self.tui.async_toggle_reblog(status) + self._emit("reblog", status) return - if key in ('c', 'C'): - self.tui.show_compose() + if key in ("c", "C"): + self._emit("compose") return if key in ("f", "F"): status = self.get_focused_status() - self.tui.async_toggle_favourite(status) + self._emit("favourite", status) return - if key in ('q', 'Q'): + if key in ("q", "Q"): self._emit("close") return - if key in ('t', 'T'): + if key in ("t", "T"): status = self.get_focused_status() self._emit("thread", status) return @@ -123,7 +126,7 @@ class Timeline(urwid.Columns): if key in ("u", "U"): status = self.get_focused_status() - self.tui.show_status_source(status) + self._emit("source", status) return return super().keypress(size, key)