From 366e9382d3d5d1e1196661b713f9e6c765b4ad71 Mon Sep 17 00:00:00 2001 From: Ivan Habunek Date: Tue, 27 Aug 2019 10:02:13 +0200 Subject: [PATCH] Implement posting statuses --- toot/tui/NOTES.md | 28 +++++++++----- toot/tui/app.py | 32 +++++++++++++++- toot/tui/compose.py | 87 +++++++++++++++++++++++++++++++++++++++++++ toot/tui/constants.py | 6 ++- toot/tui/timeline.py | 33 +++++++++++----- 5 files changed, 165 insertions(+), 21 deletions(-) create mode 100644 toot/tui/compose.py diff --git a/toot/tui/NOTES.md b/toot/tui/NOTES.md index 98b654c..7759df5 100644 --- a/toot/tui/NOTES.md +++ b/toot/tui/NOTES.md @@ -1,16 +1,24 @@ -maybe ??? -https://github.com/CanonicalLtd/subiquity/blob/master/subiquitycore/core.py#L280 - -educational: -https://github.com/TomasTomecek/sen/blob/master/sen/tui/ui.py - -check out: -https://github.com/rndusr/stig/tree/master/stig/tui +Interesting urwid implementations: +* https://github.com/CanonicalLtd/subiquity/blob/master/subiquitycore/core.py#L280 +* https://github.com/TomasTomecek/sen/blob/master/sen/tui/ui.py +* https://github.com/rndusr/stig/tree/master/stig/tui TODO/Ideas: * pack left column in timeline view -* when an error happens, show it in the status bar and have "press E to view exception" to show it in an overlay. - * maybe even have error reporting? e.g. button to open an issue on github? +* allow scrolling of toot contents if they don't fit the screen, perhaps using + pageup/pagedown +* consider adding semi-automated error reporting when viewing an exception, + something along the lines of "press T to submit a ticket", which would link + to a pre-filled issue submit page. +* show new toots, some ideas: + * R to reload/refresh timeline + * streaming new toots? not sold on the idea + * go up on first toot to fetch any newer ones, and prepend them? +* Switch timeline to top/bottom layout for narrow views. +* Think about how to show media + * download media and use local image viewer? + * convert to ascii art? +* use signals to avoid tightly coupling components 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 3e8fb70..0397151 100644 --- a/toot/tui/app.py +++ b/toot/tui/app.py @@ -7,6 +7,7 @@ from concurrent.futures import ThreadPoolExecutor from toot import api, __version__ +from .compose import StatusComposer from .constants import PALETTE from .entities import Status from .timeline import Timeline @@ -225,6 +226,30 @@ class TUI(urwid.Frame): }, ) + def show_compose(self): + composer = StatusComposer() + urwid.connect_signal(composer, "close", + lambda *args: self.close_overlay()) + urwid.connect_signal(composer, "post", + lambda _, content, warning: self.post_status(content, warning)) + self.open_overlay( + widget=composer, + title="Compose status", + options={ + "align": 'center', + "width": ('relative', 80), + "valign": 'middle', + "height": ('relative', 80), + }, + ) + + def post_status(self, content, warning): + data = api.post_status(self.app, self.user, content, spoiler_text=warning) + status = Status(data, self.app.instance) + self.timeline.prepend_status(status) + self.footer.set_message("Status posted {} \\o/".format(status.id)) + self.close_overlay() + def async_toggle_favourite(self, status): def _favourite(): logger.info("Favouriting {}".format(status)) @@ -285,11 +310,16 @@ class TUI(urwid.Frame): # --- Keys ----------------------------------------------------------------- def unhandled_input(self, key): + # TODO: this should not be in unhandled input if key in ('e', 'E'): if self.exception: self.show_exception(self.exception) - if key in ('q', 'Q'): + elif key == 'esc': + if self.overlay: + self.close_overlay() + + elif key in ('q', 'Q'): if self.overlay: self.close_overlay() else: diff --git a/toot/tui/compose.py b/toot/tui/compose.py new file mode 100644 index 0000000..cffb52d --- /dev/null +++ b/toot/tui/compose.py @@ -0,0 +1,87 @@ +import urwid +import logging + +logger = logging.getLogger(__name__) + + +class EditBox(urwid.AttrWrap): + def __init__(self): + edit = urwid.Edit(multiline=True, allow_tab=True) + return super().__init__(edit, "editbox", "editbox_focused") + + +class Button(urwid.AttrWrap): + def __init__(self, *args, **kwargs): + button = urwid.Button(*args, **kwargs) + padding = urwid.Padding(button, width=len(args[0]) + 4) + return super().__init__(padding, "button", "button_focused") + + def set_label(self, *args, **kwargs): + self.original_widget.original_widget.set_label(*args, **kwargs) + self.original_widget.width = len(args[0]) + 4 + + +class StatusComposer(urwid.Frame): + signals = ["close", "post"] + + def __init__(self): + # This can be added by button press + self.content = EditBox() + self.content_warning = None + self.cw_button = Button("Add content warning", on_press=self.toggle_cw) + + contents = [ + urwid.Text("Status message"), + self.content, + urwid.Divider(), + self.cw_button, + Button("Post", on_press=self.post), + Button("Cancel", on_press=self.close), + ] + + self.walker = urwid.SimpleListWalker(contents) + self.listbox = urwid.ListBox(self.walker) + return super().__init__(self.listbox) + + def toggle_cw(self, button): + if self.content_warning: + self.cw_button.set_label("Add content warning") + self.walker.pop(2) + self.walker.pop(2) + self.walker.pop(2) + self.walker.set_focus(3) + self.content_warning = None + else: + self.cw_button.set_label("Remove content warning") + self.content_warning = EditBox() + self.walker.insert(2, self.content_warning) + self.walker.insert(2, urwid.Text("Content warning")) + self.walker.insert(2, urwid.Divider()) + self.walker.set_focus(4) + + def clear_error_message(self): + self.footer = None + + def set_error_message(self, msg): + self.footer = urwid.Text(("footer_message_error", msg)) + + def post(self, button): + self.clear_error_message() + + # Don't lstrip content to avoid removing intentional leading whitespace + # However, do strip both sides to check if there is any content there + content = self.content.edit_text.rstrip() + content = None if not content.strip() else content + + warning = (self.content_warning.edit_text.rstrip() + if self.content_warning else "") + warning = None if not warning.strip() else warning + + if not content: + self.set_error_message("Cannot post an empty message") + return + + self._emit("post", content, warning) + + def close(self, button): + self._emit("close") diff --git a/toot/tui/constants.py b/toot/tui/constants.py index a078cd9..89e8bcf 100644 --- a/toot/tui/constants.py +++ b/toot/tui/constants.py @@ -1,8 +1,12 @@ # name, fg, bg, mono, fg_h, bg_h PALETTE = [ # Components + ('button', 'white', 'black'), + ('button_focused', 'light gray', 'dark magenta'), + ('editbox', 'white', 'black'), + ('editbox_focused', '', 'dark magenta'), ('footer_message', 'dark green', ''), - ('footer_message_error', 'white', 'dark red'), + ('footer_message_error', 'light red', ''), ('footer_status', 'white', 'dark blue'), ('footer_status_bold', 'white, bold', 'dark blue'), ('header', 'white', 'dark blue'), diff --git a/toot/tui/timeline.py b/toot/tui/timeline.py index 2fdc920..cd7a948 100644 --- a/toot/tui/timeline.py +++ b/toot/tui/timeline.py @@ -2,7 +2,6 @@ import logging import urwid import webbrowser -from toot import api from toot.utils import format_content from .utils import highlight_hashtags @@ -14,9 +13,6 @@ logger = logging.getLogger("toot") class Timeline(urwid.Columns): """ Displays a list of statuses to the left, and status details on the right. - - TODO: Switch to top/bottom for narrow views. - TODO: Cache rendered statuses? """ signals = [ "status_focused", @@ -68,13 +64,16 @@ class Timeline(urwid.Columns): def status_focused(self): """Called when the list focus switches to a new status""" status = self.get_focused_status() - self.status_details = StatusDetails(status) - self.contents[1] = self.status_details, ("weight", 50, False) + 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.contents[1] = self.status_details, ("weight", 50, False) + def keypress(self, size, key): # If down is pressed on last status in list emit a signal to load more. # TODO: Consider pre-loading statuses earlier @@ -90,6 +89,10 @@ class Timeline(urwid.Columns): self.tui.async_toggle_reblog(status) return + if key in ('c', 'C'): + self.tui.show_compose() + return + if key in ("f", "F"): status = self.get_focused_status() self.tui.async_toggle_favourite(status) @@ -112,6 +115,17 @@ class Timeline(urwid.Columns): self.status_index_map[status.id] = len(self.statuses) - 1 self.status_list.body.append(self.build_list_item(status)) + def prepend_status(self, status): + self.statuses.insert(0, status) + # Need to rebuild the map, there has to be a better way + self.status_index_map = { + status.id: n for n, status in enumerate(self.statuses) + } + self.status_list.body.insert(0, self.build_list_item(status)) + + if self.status_list.body.focus == 0: + self.draw_status_details(status) + def add_statuses(self, statuses): for status in statuses: self.add_status(status) @@ -129,8 +143,8 @@ class Timeline(urwid.Columns): # Redraw status details if status is focused if index == self.status_list.body.focus: - self.status_details = StatusDetails(status) - self.contents[1] = self.status_details, ("weight", 50, False) + self.draw_status_details(status) + class StatusDetails(urwid.Pile): def __init__(self, status): @@ -161,7 +175,8 @@ class StatusDetails(urwid.Pile): yield ("pack", urwid.Text([ ("cyan_bold", "B"), ("cyan", "oost"), " | ", ("cyan_bold", "F"), ("cyan", "avourite"), " | ", - ("cyan_bold", "V"), ("cyan", "iew in browser"), " | ", + ("cyan_bold", "V"), ("cyan", "iew"), " | ", + ("cyan", "So"), ("cyan_bold", "u"), ("cyan", "rce"), " | ", ("cyan_bold", "H"), ("cyan", "elp"), " ", ]))