From 62d7855fc0e1559e239e285d354ffcd3ce132b7d Mon Sep 17 00:00:00 2001 From: Ivan Habunek Date: Mon, 15 Jun 2020 21:27:41 +0200 Subject: [PATCH] Remove curses app, replaced by tui --- Makefile | 2 +- README.rst | 2 +- setup.py | 2 +- toot/commands.py | 6 - toot/console.py | 14 - toot/ui/README.md | 4 - toot/ui/__init__.py | 0 toot/ui/app.py | 789 -------------------------------------------- toot/ui/parsers.py | 38 --- toot/ui/utils.py | 75 ----- 10 files changed, 3 insertions(+), 929 deletions(-) delete mode 100644 toot/ui/README.md delete mode 100644 toot/ui/__init__.py delete mode 100644 toot/ui/app.py delete mode 100644 toot/ui/parsers.py delete mode 100644 toot/ui/utils.py diff --git a/Makefile b/Makefile index c03d43e..0297eeb 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: clean publish test +.PHONY: clean publish test docs dist : python setup.py sdist --formats=gztar,zip diff --git a/README.rst b/README.rst index c8ca742..f597649 100644 --- a/README.rst +++ b/README.rst @@ -36,7 +36,7 @@ Features Terminal User Interface ----------------------- -toot includes a curses-based terminal user interface (TUI). Run it with ``toot tui``. +toot includes a terminal user interface (TUI). Run it with ``toot tui``. .. image :: https://raw.githubusercontent.com/ihabunek/toot/master/docs/_static/tui_list.png diff --git a/setup.py b/setup.py index 9143d57..4176ff2 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ setup( 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', ], - packages=['toot', 'toot.ui', 'toot.tui'], + packages=['toot', 'toot.tui'], python_requires=">=3.4", install_requires=[ "requests>=2.13,<3.0", diff --git a/toot/commands.py b/toot/commands.py index 7575791..1daee12 100644 --- a/toot/commands.py +++ b/toot/commands.py @@ -75,12 +75,6 @@ def thread(app, user, args): print_timeline(thread) -def curses(app, user, args): - generator = get_timeline_generator(app, user, args) - from toot.ui.app import TimelineApp - TimelineApp(app, user, generator).run() - - def post(app, user, args): # TODO: this might be achievable, explore options if args.editor and not sys.stdin.isatty(): diff --git a/toot/console.py b/toot/console.py index 4ab1965..e62b9c0 100644 --- a/toot/console.py +++ b/toot/console.py @@ -155,14 +155,6 @@ timeline_args = common_timeline_args + [ }), ] -curses_args = common_timeline_args + [ - (["-c", "--count"], { - "type": timeline_count, - "help": "number of toots to show per page (1-20, default 20).", - "default": 20, - }), -] - AUTH_COMMANDS = [ Command( name="login", @@ -203,12 +195,6 @@ TUI_COMMANDS = [ arguments=[], require_auth=True, ), - Command( - name="curses", - description="An experimental timeline app (DEPRECATED, use 'toot tui' instead)", - arguments=curses_args, - require_auth=False, - ), ] diff --git a/toot/ui/README.md b/toot/ui/README.md deleted file mode 100644 index 6f39c85..0000000 --- a/toot/ui/README.md +++ /dev/null @@ -1,4 +0,0 @@ -This curses TUI has been deprecated in favour of the new one in `toot.tui` which -uses the Urwid framework to avoid having to do all the drawing manually. - -This app will no longer be maintained and will be removed in a future release. diff --git a/toot/ui/__init__.py b/toot/ui/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/toot/ui/app.py b/toot/ui/app.py deleted file mode 100644 index 748099a..0000000 --- a/toot/ui/app.py +++ /dev/null @@ -1,789 +0,0 @@ -# -*- coding: utf-8 -*- -import os -import webbrowser - -from toot import __version__, api - -from toot.exceptions import ConsoleError -from toot.ui.parsers import parse_status -from toot.ui.utils import draw_horizontal_divider, draw_lines, size_as_drawn -from toot.wcstring import fit_text - -# Attempt to load curses, which is not available on windows -try: - import curses - import curses.panel - import curses.textpad -except ImportError: - raise ConsoleError("Curses is not available on this platform") - - -class Color: - @classmethod - def setup_palette(class_): - curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK) - curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_BLACK) - curses.init_pair(3, curses.COLOR_GREEN, curses.COLOR_BLACK) - curses.init_pair(4, curses.COLOR_YELLOW, curses.COLOR_BLACK) - curses.init_pair(5, curses.COLOR_RED, curses.COLOR_BLACK) - curses.init_pair(6, curses.COLOR_CYAN, curses.COLOR_BLACK) - curses.init_pair(7, curses.COLOR_MAGENTA, curses.COLOR_BLACK) - curses.init_pair(8, curses.COLOR_WHITE, curses.COLOR_BLUE) - curses.init_pair(9, curses.COLOR_WHITE, curses.COLOR_RED) - - class_.WHITE = curses.color_pair(1) - class_.BLUE = curses.color_pair(2) - class_.GREEN = curses.color_pair(3) - class_.YELLOW = curses.color_pair(4) - class_.RED = curses.color_pair(5) - class_.CYAN = curses.color_pair(6) - class_.MAGENTA = curses.color_pair(7) - class_.WHITE_ON_BLUE = curses.color_pair(8) - class_.WHITE_ON_RED = curses.color_pair(9) - - class_.HASHTAG = class_.BLUE | curses.A_BOLD - - -class HeaderWindow: - def __init__(self, stdscr, height, width, y, x): - self.window = stdscr.subwin(height, width, y, x) - self.window.bkgdset(' ', Color.WHITE_ON_BLUE) - self.height = height - self.width = width - - def draw(self, user): - username = "{}@{}".format(user.username, user.instance) - - self.window.erase() - self.window.addstr(" toot", curses.A_BOLD) - self.window.addstr(" | ") - self.window.addstr(username) - self.window.addstr(" | ") - self.window.refresh() - - -class FooterWindow: - def __init__(self, stdscr, height, width, y, x): - self.window = stdscr.subwin(height, width, y, x) - self.height = height - self.width = width - - def draw_status(self, selected, count): - text = "Showing toot {} of {}".format(selected + 1, count) - text = fit_text(text, self.width) - self.window.addstr(0, 0, text, Color.WHITE_ON_BLUE | curses.A_BOLD) - self.window.refresh() - - def draw_message(self, text, color): - text = fit_text(text, self.width - 1) - self.window.addstr(1, 0, text, color) - self.window.refresh() - - def clear_message(self): - self.window.addstr(1, 0, "".ljust(self.width - 1)) - self.window.refresh() - - -class StatusListWindow: - """Window which shows the scrollable list of statuses (left side).""" - def __init__(self, stdscr, height, width, top, left): - # Dimensions and position of region in stdscr which will contain the pad - self.region_height = height - self.region_width = width - self.region_top = top - self.region_left = left - - # How many statuses fit on one page (excluding border, at 3 lines per status) - self.page_size = (height - 2) // 3 - - # Initially, size the pad to the dimensions of the region, will be - # increased later to accomodate statuses - self.pad = curses.newpad(10, width) - self.pad.box() - - # Make curses interpret escape sequences for getch (why is this off by default?) - self.pad.keypad(True) - - self.scroll_pos = 0 - - def draw_statuses(self, statuses, selected, starting=0): - # Resize window to accomodate statuses if required - height, width = self.pad.getmaxyx() - - new_height = len(statuses) * 3 + 1 - if new_height > height: - self.pad.resize(new_height, width) - self.pad.box() - - last_idx = len(statuses) - 1 - - for index, status in enumerate(statuses): - if index >= starting: - highlight = selected == index - draw_divider = index < last_idx - self.draw_status_row(status, index, highlight, draw_divider) - - def draw_status_row(self, status, index, highlight=False, draw_divider=True): - offset = 3 * index - - height, width = self.pad.getmaxyx() - color = Color.GREEN if highlight else Color.WHITE - - trunc_width = width - 15 - acct = fit_text("@" + status['account']['acct'], trunc_width) - display_name = fit_text(status['account']['display_name'], trunc_width) - - if status['account']['display_name']: - self.pad.addstr(offset + 1, 14, display_name, color) - self.pad.addstr(offset + 2, 14, acct, color) - else: - self.pad.addstr(offset + 1, 14, acct, color) - if status['in_reply_to_id'] is not None: - self.pad.addstr(offset + 1, width - 3, '⤶', Color.CYAN) - - date, time = status['created_at'] - self.pad.addstr(offset + 1, 1, " " + date.ljust(12), color) - self.pad.addstr(offset + 2, 1, " " + time.ljust(12), color) - - if status['favourited']: - self.pad.addstr(offset + 2, width - 3, '⭐', Color.YELLOW) - - if draw_divider: - draw_horizontal_divider(self.pad, offset + 3) - - self.refresh() - - def refresh(self): - self.pad.refresh( - self.scroll_pos * 3, # top - 0, # left - self.region_top, - self.region_left, - self.region_height + 1, # +1 required to refresh full height, not sure why - self.region_width, - ) - - def scroll_to(self, index): - self.scroll_pos = index - self.refresh() - - def scroll_up(self): - if self.scroll_pos > 0: - self.scroll_to(self.scroll_pos - 1) - - def scroll_down(self): - self.scroll_to(self.scroll_pos + 1) - - def scroll_if_required(self, new_index): - if new_index < self.scroll_pos: - self.scroll_up() - elif new_index >= self.scroll_pos + self.page_size: - self.scroll_down() - else: - self.refresh() - - -class StatusDetailWindow: - """Window which shows details of a status (right side)""" - def __init__(self, stdscr, height, width, y, x): - self.window = stdscr.subwin(height, width, y, x) - self.height = height - self.width = width - - def content_lines(self, status): - acct = status['account']['acct'] - name = status['account']['display_name'] - - if name: - yield name, Color.YELLOW - yield "@" + acct, Color.GREEN - yield - - text_width = self.width - 4 - - if status['sensitive']: - for line in status['spoiler_text']: - yield line - yield - - if status['sensitive'] and not status['show_sensitive']: - yield "Marked as sensitive, press s to view".ljust(text_width), Color.WHITE_ON_RED - return - - for line in status['content']: - yield line - - if status['media_attachments']: - yield - yield "Media:" - for attachment in status['media_attachments']: - yield attachment['text_url'] or attachment['url'] - - def footer_lines(self, status): - if status['url'] is not None: - yield status['url'] - - if status['boosted_by']: - acct = status['boosted_by']['acct'] - yield "Boosted by @{}".format(acct), Color.GREEN - - if status['reblogged']: - yield "↷ Boosted", Color.CYAN - - yield ( - "{replies_count} replies, " - "{reblogs_count} reblogs, " - "{favourites_count} favourites" - ).format(**status), Color.CYAN - - def draw(self, status): - self.window.erase() - self.window.box() - - if not status: - return - - content = self.content_lines(status) - footer = self.footer_lines(status) - - y = draw_lines(self.window, content, 1, 2, Color.WHITE) - draw_horizontal_divider(self.window, y) - draw_lines(self.window, footer, y + 1, 2, Color.WHITE) - - self.window.refresh() - - -class Modal: - def __init__(self, stdscr, resize_callback=None): - self.stdscr = stdscr - self.resize_callback = resize_callback - - self.setup_windows() - self.full_redraw() - self.panel = curses.panel.new_panel(self.window) - self.hide() - - def get_content(self): - raise NotImplementedError() - - def get_size_pos(self, stdscr): - screen_height, screen_width = stdscr.getmaxyx() - - content = self.get_content() - height = len(content) + 2 - width = max(len(l) for l in content) + 4 - - y = (screen_height - height) // 2 - x = (screen_width - width) // 2 - - return height, width, y, x - - def setup_windows(self): - height, width, y, x = self.get_size_pos(self.stdscr) - self.window = curses.newwin(height, width, y, x) - - def full_redraw(self): - self.setup_windows() - self.window.box() - draw_lines(self.window, self.get_content(), 1, 2, Color.WHITE) - - def show(self): - self.panel.top() - self.panel.show() - self.window.refresh() - curses.panel.update_panels() - - def hide(self): - self.panel.hide() - curses.panel.update_panels() - - def loop(self): - self.show() - - while True: - ch = self.window.getch() - key = chr(ch).lower() if curses.ascii.isprint(ch) else None - - if key == 'q': - break - elif ch == curses.KEY_RESIZE: - if self.resize_callback: - self.resize_callback() - self.full_redraw() - - self.hide() - - -class HelpModal(Modal): - def get_content(self): - return [ - ("toot v{}".format(__version__), Color.GREEN | curses.A_BOLD), - "", - "Key bindings:", - "", - " h - show help", - " j or ↓ - move down", - " k or ↑ - move up", - " v - view current toot in browser", - " b - toggle boost status", - " f - toggle favourite status", - " c - post a new status", - " r - reply to status", - " q - quit application", - " s - show sensitive content" - "", - "Press q to exit help.", - "", - ("https://github.com/ihabunek/toot", Color.YELLOW), - ] - - -class DeprecationNoticeModal(Modal): - def get_content(self): - return [ - ("DEPRECATION NOTICE", Color.RED | curses.A_BOLD), - "", - "This experimental terminal UI has been deprecated and will be ", - "removed in the near future.", - "", - "The new TUI can be lauched by running `toot tui`. This new UI ", - "contains all the functionality of this one and much more. ", - "It will be supported for the forseeable future.", - "", - "For details see:", - ("https://github.com/ihabunek/toot/pull/108", Color.CYAN), - "", - ("Press q to close this notice.", Color.YELLOW), - ] - - -class EntryModal(Modal): - def __init__(self, stdscr, title, footer=None, size=(None, None), default=None, resize_callback=None): - self.stdscr = stdscr - self.resize_callback = resize_callback - self.content = [] if default is None else default.split() - self.cursor_pos = 0 - self.pad_y, self.pad_x = 2, 2 - - self.title = title - self.footer = footer - self.size = size - if self.footer: - self.pad_y += 1 - - self.setup_windows() - self.full_redraw() - self.panel = curses.panel.new_panel(self.window) - self.hide() - - def get_size_pos(self, stdscr): - screen_height, screen_width = stdscr.getmaxyx() - if self.size[0]: - height = self.size[0] + (self.pad_y * 2) + 1 - else: - height = int(screen_height / 1.33) - if self.size[1]: - width = self.size[1] + (self.pad_x * 2) + 1 - else: - width = int(screen_width / 1.25) - - y = (screen_height - height) // 2 - x = (screen_width - width) // 2 - - return height, width, y, x - - def setup_windows(self): - height, width, y, x = self.get_size_pos(self.stdscr) - - self.window = curses.newwin(height, width, y, x) - self.text_window = self.window.derwin(height - (self.pad_y * 2), width - (self.pad_x * 2), self.pad_y, self.pad_x) - self.text_window.keypad(True) - - def full_redraw(self): - self.window.erase() - self.window.box() - - draw_lines(self.window, ["{} (^D to confirm):".format(self.title)], 1, 2, Color.WHITE) - if self.footer: - window_height, window_width = self.window.getmaxyx() - draw_lines(self.window, [self.footer], window_height - self.pad_y + 1, 2, Color.WHITE) - - self.window.refresh() - self.refresh_text() - - def refresh_text(self): - text = self.get_content() - lines = text.split('\n') - draw_lines(self.text_window, lines, 0, 0, Color.WHITE) - - text_window_height, text_window_width = self.text_window.getmaxyx() - text_on_screen = (''.join(self.content)[:self.cursor_pos] + '_').split('\n') - y, x = size_as_drawn(text_on_screen, text_window_width) - self.text_window.move(y, x) - - def show(self): - super().show() - self.refresh_text() - - def clear(self): - self.content = [] - self.cursor_pos = 0 - - def on_resize(self): - if self.resize_callback: - self.resize_callback() - self.setup_windows() - self.full_redraw() - - def do_command(self, ch): - if curses.ascii.isprint(ch) or ch == curses.ascii.LF: - text_window_height, text_window_width = self.text_window.getmaxyx() - y, x = size_as_drawn((self.get_content() + chr(ch)).split('\n'), text_window_width) - if y < text_window_height - 1 and x < text_window_width: - self.content.insert(self.cursor_pos, chr(ch)) - self.cursor_pos += 1 - else: - curses.beep() - - elif ch == curses.KEY_BACKSPACE: - if self.cursor_pos > 0: - del self.content[self.cursor_pos - 1] - self.cursor_pos -= 1 - else: - curses.beep() - - elif ch == curses.KEY_DC: - if self.cursor_pos >= 0 and self.cursor_pos < len(self.content): - del self.content[self.cursor_pos] - else: - curses.beep() - - elif ch == curses.KEY_LEFT: - if self.cursor_pos > 0: - self.cursor_pos -= 1 - else: - curses.beep() - - elif ch == curses.KEY_RIGHT: - if self.cursor_pos + 1 <= len(self.content): - self.cursor_pos += 1 - else: - curses.beep() - - elif ch in (curses.ascii.EOT, curses.ascii.RS): # ^D or (for some terminals) Ctrl+Enter - return False, False - - elif ch == curses.ascii.ESC: - self.clear() - return False, True - - elif ch == curses.KEY_RESIZE: - self.on_resize() - return True, False - - self.refresh_text() - return True, False - - def get_content(self): - return ''.join(self.content) - - def loop(self): - self.show() - while True: - ch = self.text_window.getch() - if not ch: - continue - should_continue, abort_flag = self.do_command(ch) - if not should_continue: - break - self.hide() - if abort_flag: - return None - else: - return self.get_content() - - -class ComposeModal(EntryModal): - def __init__(self, stdscr, default_cw=None, **kwargs): - super().__init__(stdscr, title="Compose a toot", footer="^D to submit, ESC to quit, ^W to mark sensitive (cw)", **kwargs) - self.cw = default_cw - self.cwmodal = EntryModal(stdscr, title="Content warning", size=(1, 60), default=self.cw, resize_callback=self.on_resize) - - def do_command(self, ch): - if ch == curses.ascii.ctrl(ord('w')): - self.cwmodal.on_resize() - self.cw = self.cwmodal.loop() or None - self.full_redraw() - return True, False - else: - return super().do_command(ch) - - def loop(self): - content = super().loop() - return content, self.cw - - -class TimelineApp: - def __init__(self, app, user, status_generator): - self.app = app - self.user = user - self.status_generator = status_generator - self.statuses = [] - self.stdscr = None - - def run(self): - os.environ.setdefault('ESCDELAY', '25') - curses.wrapper(self._wrapped_run) - - def _wrapped_run(self, stdscr): - self.stdscr = stdscr - - Color.setup_palette() - self.setup_windows() - - # Load some data and redraw - self.fetch_next() - self.selected = 0 - self.full_redraw() - - self.deprecation_modal.loop() - self.full_redraw() - - self.loop() - - def setup_windows(self): - screen_height, screen_width = self.stdscr.getmaxyx() - - if screen_width < 60: - raise ConsoleError("Terminal screen is too narrow, toot curses requires at least 60 columns to display properly.") - - header_height = 1 - footer_height = 2 - footer_top = screen_height - footer_height - - left_width = max(min(screen_width // 3, 60), 30) - main_height = screen_height - header_height - footer_height - main_width = screen_width - left_width - - self.header = HeaderWindow(self.stdscr, header_height, screen_width, 0, 0) - self.footer = FooterWindow(self.stdscr, footer_height, screen_width, footer_top, 0) - self.left = StatusListWindow(self.stdscr, main_height, left_width, header_height, 0) - self.right = StatusDetailWindow(self.stdscr, main_height, main_width, header_height, left_width) - - self.help_modal = HelpModal(self.stdscr, resize_callback=self.on_resize) - self.deprecation_modal = DeprecationNoticeModal(self.stdscr, resize_callback=self.on_resize) - - def loop(self): - while True: - ch = self.left.pad.getch() - key = chr(ch).lower() if curses.ascii.isprint(ch) else None - - if key == 'q': - return - - elif key == 'h': - self.help_modal.loop() - self.full_redraw() - - elif key == 'v': - status = self.get_selected_status() - if status: - webbrowser.open(status['url']) - - elif key == 'j' or ch == curses.KEY_DOWN: - self.select_next() - - elif key == 'k' or ch == curses.KEY_UP: - self.select_previous() - - elif key == 's': - self.show_sensitive() - - elif key == 'b': - self.toggle_reblog() - - elif key == 'f': - self.toggle_favourite() - - elif key == 'c': - self.compose() - - elif key == 'r': - self.reply() - - elif ch == curses.KEY_RESIZE: - self.on_resize() - - def show_sensitive(self): - status = self.get_selected_status() - if status['sensitive'] and not status['show_sensitive']: - status['show_sensitive'] = True - self.right.draw(status) - - def compose(self): - """Compose and submit a new status""" - app, user = self.app, self.user - if not app or not user: - self.footer.draw_message("You must be logged in to post", Color.RED) - return - - compose_modal = ComposeModal(self.stdscr, resize_callback=self.on_resize) - content, cw = compose_modal.loop() - self.full_redraw() - if content is None: - return - elif len(content) == 0: - self.footer.draw_message("Status must contain content", Color.RED) - return - - self.footer.draw_message("Submitting status...", Color.YELLOW) - response = api.post_status(app, user, content, spoiler_text=cw, sensitive=cw is not None) - status = parse_status(response) - self.statuses.insert(0, status) - self.selected += 1 - self.left.draw_statuses(self.statuses, self.selected) - self.footer.draw_message("✓ Status posted", Color.GREEN) - - def reply(self): - """Reply to the selected status""" - status = self.get_selected_status() - app, user = self.app, self.user - if not app or not user: - self.footer.draw_message("You must be logged in to reply", Color.RED) - return - - compose_modal = ComposeModal(self.stdscr, default_cw='\n'.join(status['spoiler_text']) or None, resize_callback=self.on_resize) - content, cw = compose_modal.loop() - self.full_redraw() - if content is None: - return - elif len(content) == 0: - self.footer.draw_message("Status must contain content", Color.RED) - return - - self.footer.draw_message("Submitting reply...", Color.YELLOW) - response = api.post_status(app, user, content, spoiler_text=cw, sensitive=cw is not None, in_reply_to_id=status['id']) - status = parse_status(response) - self.statuses.insert(0, status) - self.selected += 1 - self.left.draw_statuses(self.statuses, self.selected) - self.footer.draw_message("✓ Reply posted", Color.GREEN) - - def toggle_reblog(self): - """Reblog or unreblog selected status.""" - status = self.get_selected_status() - assert status - app, user = self.app, self.user - if not app or not user: - self.footer.draw_message("You must be logged in to reblog", Color.RED) - return - status_id = status['id'] - if status['reblogged']: - status['reblogged'] = False - self.footer.draw_message("Unboosting status...", Color.YELLOW) - api.unreblog(app, user, status_id) - self.footer.draw_message("✓ Status unboosted", Color.GREEN) - else: - status['reblogged'] = True - self.footer.draw_message("Boosting status...", Color.YELLOW) - api.reblog(app, user, status_id) - self.footer.draw_message("✓ Status boosted", Color.GREEN) - - self.right.draw(status) - - def toggle_favourite(self): - """Favourite or unfavourite selected status.""" - status = self.get_selected_status() - assert status - app, user = self.app, self.user - if not app or not user: - self.footer.draw_message("You must be logged in to favourite", Color.RED) - return - status_id = status['id'] - if status['favourited']: - self.footer.draw_message("Undoing favourite status...", Color.YELLOW) - api.unfavourite(app, user, status_id) - self.footer.draw_message("✓ Status unfavourited", Color.GREEN) - else: - self.footer.draw_message("Favourite status...", Color.YELLOW) - api.favourite(app, user, status_id) - self.footer.draw_message("✓ Status favourited", Color.GREEN) - status['favourited'] = not status['favourited'] - - self.right.draw(status) - - def select_previous(self): - """Move to the previous status in the timeline.""" - self.footer.clear_message() - - if self.selected == 0: - self.footer.draw_message("Cannot move beyond first toot.", Color.GREEN) - return - - old_index = self.selected - new_index = self.selected - 1 - - self.selected = new_index - self.redraw_after_selection_change(old_index, new_index) - - def select_next(self): - """Move to the next status in the timeline.""" - self.footer.clear_message() - - old_index = self.selected - new_index = self.selected + 1 - - # Load more statuses if no more are available - if self.selected + 1 >= len(self.statuses): - self.fetch_next() - self.left.draw_statuses(self.statuses, self.selected, new_index - 1) - self.draw_footer_status() - - self.selected = new_index - self.redraw_after_selection_change(old_index, new_index) - - def fetch_next(self): - try: - self.footer.draw_message("Loading toots...", Color.BLUE) - statuses = next(self.status_generator) - except StopIteration: - return None - - for status in statuses: - self.statuses.append(parse_status(status)) - - self.footer.draw_message("Loaded {} toots".format(len(statuses)), Color.GREEN) - - return len(statuses) - - def on_resize(self): - self.setup_windows() - self.full_redraw() - - def full_redraw(self): - """Perform a full redraw of the UI.""" - self.left.draw_statuses(self.statuses, self.selected) - self.right.draw(self.get_selected_status()) - - self.header.draw(self.user) - self.draw_footer_status() - - - def redraw_after_selection_change(self, old_index, new_index): - old_status = self.statuses[old_index] - new_status = self.statuses[new_index] - - # Perform a partial redraw - self.left.draw_status_row(old_status, old_index, highlight=False, draw_divider=False) - self.left.draw_status_row(new_status, new_index, highlight=True, draw_divider=False) - self.left.scroll_if_required(new_index) - - self.right.draw(new_status) - self.draw_footer_status() - - def get_selected_status(self): - if len(self.statuses) > self.selected: - return self.statuses[self.selected] - - def draw_footer_status(self): - self.footer.draw_status(self.selected, len(self.statuses)) diff --git a/toot/ui/parsers.py b/toot/ui/parsers.py deleted file mode 100644 index ba2f75b..0000000 --- a/toot/ui/parsers.py +++ /dev/null @@ -1,38 +0,0 @@ -from toot.utils import format_content - - -def parse_status(status): - _status = status.get('reblog') or status - account = parse_account(_status['account']) - content = list(format_content(_status['content'])) - spoiler_text = list(format_content(_status['spoiler_text'])) if _status['spoiler_text'] else [] - - created_at = status['created_at'][:19].split('T') - boosted_by = parse_account(status['account']) if status['reblog'] else None - - return { - 'account': account, - 'boosted_by': boosted_by, - 'created_at': created_at, - 'content': content, - 'favourited': status.get('favourited'), - 'favourites_count': _status['favourites_count'], - 'id': status['id'], - 'in_reply_to_id': _status.get('in_reply_to_id'), - 'media_attachments': _status['media_attachments'], - 'url': _status['url'], - 'reblogged': status.get('reblogged'), - 'reblogs_count': _status['reblogs_count'], - 'replies_count': _status.get('replies_count', 0), - 'spoiler_text': spoiler_text, - 'sensitive': _status['sensitive'], - 'show_sensitive': False, - } - - -def parse_account(account): - return { - 'id': account['id'], - 'acct': account['acct'], - 'display_name': account['display_name'], - } diff --git a/toot/ui/utils.py b/toot/ui/utils.py deleted file mode 100644 index 1365198..0000000 --- a/toot/ui/utils.py +++ /dev/null @@ -1,75 +0,0 @@ -import re - -from toot.wcstring import fit_text, wc_wrap - - -def draw_horizontal_divider(window, y): - height, width = window.getmaxyx() - - # Don't draw out of bounds - if y < height - 1: - line = '├' + '─' * (width - 2) + '┤' - window.addstr(y, 0, line) - - -def enumerate_lines(lines, text_width, default_color): - def parse_line(line): - if isinstance(line, tuple) and len(line) == 2: - return line[0], line[1] - elif isinstance(line, str): - return line, default_color - elif line is None: - return "", default_color - - raise ValueError("Wrong yield in generator") - - def wrap_lines(lines): - for line in lines: - line, color = parse_line(line) - if line: - for wrapped in wc_wrap(line, text_width): - yield wrapped, color - else: - yield "", color - - return enumerate(wrap_lines(lines)) - - -HASHTAG_PATTERN = re.compile(r'(? 0: - for wrapped_line in wrapped: - x = len(wrapped_line) - y += 1 - else: - x = 0 - y += 1 - return y - 1, x - 1 if x != 0 else 0 - - -def draw_lines(window, lines, start_y, padding, default_color): - height, width = window.getmaxyx() - text_width = width - 2 * padding - - for dy, (line, color) in enumerate_lines(lines, text_width, default_color): - y = start_y + dy - if y < height - 1: - window.addstr(y, padding, fit_text(line, text_width), color) - highlight_hashtags(window, y, padding, line) - - return y + 1