diff --git a/rtv/curses_helpers.py b/rtv/curses_helpers.py index 1b14b62..a92c590 100644 --- a/rtv/curses_helpers.py +++ b/rtv/curses_helpers.py @@ -6,12 +6,12 @@ from curses import textpad, ascii from contextlib import contextmanager from .docs import HELP -from .helpers import strip_textpad +from .helpers import strip_textpad, clean from .exceptions import EscapeInterrupt __all__ = ['ESCAPE', 'UARROW', 'DARROW', 'BULLET', 'show_notification', 'show_help', 'LoadScreen', 'Color', 'text_input', 'curses_session', - 'prompt_input'] + 'prompt_input', 'add_line'] ESCAPE = 27 @@ -25,6 +25,49 @@ DARROW = u'\u25bc'.encode('utf-8') BULLET = u'\u2022'.encode('utf-8') GOLD = u'\u272A'.encode('utf-8') +def add_line(window, text, row=None, col=None, attr=None): + """ + Unicode aware version of curses's built-in addnstr method. + + Safely draws a line of text on the window starting at position (row, col). + Checks the boundaries of the window and cuts off the text if it exceeds + the length of the window. + """ + + # The following arg combinations must be supported to conform with addnstr + # (window, text) + # (window, text, attr) + # (window, text, row, col) + # (window, text, row, col, attr) + + # Text must be unicode or ascii. Can't be UTF-8! + text = clean(text) + + cursor_row, cursor_col = window.getyx() + row = row if row is not None else cursor_row + col = col if col is not None else cursor_col + + max_rows, max_cols = window.getmaxyx() + n_cols = max_cols - col - 1 + if n_cols <= 0: + # Trying to draw outside of the screen bounds + return + + # We have n_cols available to draw the text. Add characters to a text buffer + # until we reach the end of the screen + buffer, space_left = [], n_cols + for char in text: + space_left -= unicode_width(char) + if space_left < 0: + break + buffer.append(char) + + trimmed_text = ''.join(buffer) + if attr is None: + window.addnstr(row, col, trimmed_text, n_cols) + else: + window.addnstr(row, col, trimmed_text, n_cols, attr) + def show_notification(stdscr, message): """ @@ -52,7 +95,7 @@ def show_notification(stdscr, message): window.border() for index, line in enumerate(message, start=1): - window.addnstr(index, 1, line, box_width - 2) + add_line(window, line, index, 1) window.refresh() ch = stdscr.getch() diff --git a/rtv/page.py b/rtv/page.py index c091137..1262264 100644 --- a/rtv/page.py +++ b/rtv/page.py @@ -8,9 +8,9 @@ from contextlib import contextmanager import praw.errors import requests -from .helpers import clean, open_editor +from .helpers import open_editor from .curses_helpers import (Color, show_notification, show_help, text_input, - prompt_input) + prompt_input, add_line) from .docs import COMMENT_EDIT_FILE, SUBMISSION_FILE __all__ = ['Navigator', 'BaseController', 'BasePage'] @@ -470,17 +470,16 @@ class BasePage(object): self._header_window.bkgd(' ', attr) sub_name = self.content.name.replace('/r/front', 'Front Page ') - sub_name = 'blank' - self._header_window.addnstr(0, 0, clean(sub_name), n_cols - 1) + add_line(self._header_window, sub_name, 0, 0) if self.reddit.user is not None: username = self.reddit.user.name + # TODO: use unicode width here instead of length s_col = (n_cols - len(username) - 1) - # Only print the username if it fits in the empty space on the - # right + # Only print username if it fits in the empty space on the right if (s_col - 1) >= len(sub_name): n = (n_cols - s_col - 1) - self._header_window.addnstr(0, s_col, clean(username), n) + add_line(self._header_window, username, 0, s_col) self._header_window.refresh() diff --git a/rtv/submission.py b/rtv/submission.py index 19116f2..c717d17 100644 --- a/rtv/submission.py +++ b/rtv/submission.py @@ -7,9 +7,9 @@ import praw.errors from .content import SubmissionContent from .page import BasePage, Navigator, BaseController -from .helpers import clean, open_browser, open_editor +from .helpers import open_browser, open_editor from .curses_helpers import (BULLET, UARROW, DARROW, GOLD, Color, LoadScreen, - show_notification, text_input) + show_notification, text_input, add_line) from .docs import COMMENT_FILE __all__ = ['SubmissionController', 'SubmissionPage'] @@ -158,15 +158,13 @@ class SubmissionPage(BasePage): row = offset if row in valid_rows: - text = clean(u'{author} '.format(**data)) attr = curses.A_BOLD attr |= (Color.BLUE if not data['is_author'] else Color.GREEN) - win.addnstr(row, 1, text, n_cols - 1, attr) + add_line(win, u'{author} '.format(**data), row, 1, attr) if data['flair']: - text = clean(u'{flair} '.format(**data)) attr = curses.A_BOLD | Color.YELLOW - win.addnstr(text, n_cols - win.getyx()[1], attr) + add_line(win, u'{flair} '.format(**data), attr=attr) if data['likes'] is None: text, attr = BULLET, curses.A_BOLD @@ -174,20 +172,18 @@ class SubmissionPage(BasePage): text, attr = UARROW, (curses.A_BOLD | Color.GREEN) else: text, attr = DARROW, (curses.A_BOLD | Color.RED) - win.addnstr(text, n_cols - win.getyx()[1], attr) + add_line(win, text, attr=attr) - text = clean(u' {score} {created} '.format(**data)) - win.addnstr(text, n_cols - win.getyx()[1]) + add_line(win, u' {score} {created} '.format(**data)) if data['gold']: text, attr = GOLD, (curses.A_BOLD | Color.YELLOW) - win.addnstr(text, n_cols - win.getyx()[1], attr) + add_line(win, text, attr=attr) n_body = len(data['split_body']) for row, text in enumerate(data['split_body'], start=offset + 1): if row in valid_rows: - text = clean(text) - win.addnstr(row, 1, text, n_cols - 1) + add_line(win, text, row, 1) # Unfortunately vline() doesn't support custom color so we have to # build it one segment at a time. @@ -210,13 +206,9 @@ class SubmissionPage(BasePage): n_rows, n_cols = win.getmaxyx() n_cols -= 1 - text = clean(u'{body}'.format(**data)) - win.addnstr(0, 1, text, n_cols - 1) - text = clean(u' [{count}]'.format(**data)) - win.addnstr(text, n_cols - win.getyx()[1], curses.A_BOLD) + add_line(win, u'{body}'.format(**data), 0, 1) + add_line(win, u' [{count}]'.format(**data), attr=curses.A_BOLD) - # Unfortunately vline() doesn't support custom color so we have to - # build it one segment at a time. attr = Color.get_level(data['level']) win.addch(0, 0, curses.ACS_VLINE, attr) @@ -229,23 +221,18 @@ class SubmissionPage(BasePage): n_cols -= 3 # one for each side of the border + one for offset for row, text in enumerate(data['split_title'], start=1): - text = clean(text) - win.addnstr(row, 1, text, n_cols, curses.A_BOLD) + add_line(win, text, row, 1, curses.A_BOLD) row = len(data['split_title']) + 1 attr = curses.A_BOLD | Color.GREEN - text = clean(u'{author}'.format(**data)) - win.addnstr(row, 1, text, n_cols, attr) + add_line(win, u'{author}'.format(**data), row, 1, attr) attr = curses.A_BOLD | Color.YELLOW - text = clean(u' {flair}'.format(**data)) - win.addnstr(text, n_cols - win.getyx()[1], attr) - text = clean(u' {created} {subreddit}'.format(**data)) - win.addnstr(text, n_cols - win.getyx()[1]) + add_line(win, u' {flair}'.format(**data), attr=attr) + add_line(win, u' {created} {subreddit}'.format(**data)) row = len(data['split_title']) + 2 attr = curses.A_UNDERLINE | Color.BLUE - text = clean(u'{url}'.format(**data)) - win.addnstr(row, 1, text, n_cols, attr) + add_line(win, u'{url}'.format(**data), row, 1, attr) offset = len(data['split_title']) + 3 # Cut off text if there is not enough room to display the whole post @@ -256,12 +243,10 @@ class SubmissionPage(BasePage): split_text.append('(Not enough space to display)') for row, text in enumerate(split_text, start=offset): - text = clean(text) - win.addnstr(row, 1, text, n_cols) + add_line(win, text, row, 1) row = len(data['split_title']) + len(split_text) + 3 - text = clean(u'{score} '.format(**data)) - win.addnstr(row, 1, text, n_cols - 1) + add_line(win, u'{score} '.format(**data), row, 1) if data['likes'] is None: text, attr = BULLET, curses.A_BOLD @@ -269,17 +254,15 @@ class SubmissionPage(BasePage): text, attr = UARROW, curses.A_BOLD | Color.GREEN else: text, attr = DARROW, curses.A_BOLD | Color.RED - win.addnstr(text, n_cols - win.getyx()[1], attr) - - text = clean(u' {comments} '.format(**data)) - win.addnstr(text, n_cols - win.getyx()[1]) + add_line(win, text, attr=attr) + add_line(win, u' {comments} '.format(**data)) if data['gold']: text, attr = GOLD, (curses.A_BOLD | Color.YELLOW) - win.addnstr(text, n_cols - win.getyx()[1], attr) + add_line(win, text, attr=attr) if data['nsfw']: text, attr = 'NSFW', (curses.A_BOLD | Color.RED) - win.addnstr(text, n_cols - win.getyx()[1], attr) + add_line(win, text, attr=attr) win.border() \ No newline at end of file diff --git a/rtv/subreddit.py b/rtv/subreddit.py index d1f7ff3..4bac5b7 100644 --- a/rtv/subreddit.py +++ b/rtv/subreddit.py @@ -9,9 +9,9 @@ from .exceptions import SubredditError, AccountError from .page import BasePage, Navigator, BaseController from .submission import SubmissionPage from .content import SubredditContent -from .helpers import clean, open_browser, open_editor +from .helpers import open_browser, open_editor from .docs import SUBMISSION_FILE -from .curses_helpers import (BULLET, UARROW, DARROW, GOLD, Color, +from .curses_helpers import (BULLET, UARROW, DARROW, GOLD, Color, add_line, LoadScreen, show_notification, prompt_input) __all__ = ['opened_links', 'SubredditController', 'SubredditPage'] @@ -163,21 +163,18 @@ class SubredditPage(BasePage): n_title = len(data['split_title']) for row, text in enumerate(data['split_title'], start=offset): if row in valid_rows: - text = clean(text) - win.addnstr(row, 1, text, n_cols - 1, curses.A_BOLD) + add_line(win, text, row, 1, curses.A_BOLD) row = n_title + offset if row in valid_rows: seen = (data['url_full'] in opened_links) link_color = Color.MAGENTA if seen else Color.BLUE attr = curses.A_UNDERLINE | link_color - text = clean(u'{url}'.format(**data)) - win.addnstr(row, 1, text, n_cols - 1, attr) + add_line(win, u'{url}'.format(**data), row, 1, attr) row = n_title + offset + 1 if row in valid_rows: - text = clean(u'{score} '.format(**data)) - win.addnstr(row, 1, text, n_cols - 1) + add_line(win, u'{score} '.format(**data), row, 1) if data['likes'] is None: text, attr = BULLET, curses.A_BOLD @@ -185,24 +182,19 @@ class SubredditPage(BasePage): text, attr = UARROW, curses.A_BOLD | Color.GREEN else: text, attr = DARROW, curses.A_BOLD | Color.RED - win.addnstr(text, n_cols - win.getyx()[1], attr) - - text = clean(u' {created} {comments} '.format(**data)) - win.addnstr(text, n_cols - win.getyx()[1]) + add_line(win, text, attr=attr) + add_line(win, u' {created} {comments} '.format(**data)) if data['gold']: text, attr = GOLD, (curses.A_BOLD | Color.YELLOW) - win.addnstr(text, n_cols - win.getyx()[1], attr) + add_line(win, text, attr=attr) if data['nsfw']: text, attr = 'NSFW', (curses.A_BOLD | Color.RED) - win.addnstr(text, n_cols - win.getyx()[1], attr) + add_line(win, text, attr=attr) row = n_title + offset + 2 if row in valid_rows: - text = clean(u'{author}'.format(**data)) - win.addnstr(row, 1, text, n_cols - 1, curses.A_BOLD) - text = clean(u' {subreddit}'.format(**data)) - win.addnstr(text, n_cols - win.getyx()[1], Color.YELLOW) - text = clean(u' {flair}'.format(**data)) - win.addnstr(text, n_cols - win.getyx()[1], Color.RED) \ No newline at end of file + add_line(win, u'{author}'.format(**data), row, 1, curses.A_BOLD) + add_line(win, u' {subreddit}'.format(**data), attr=Color.YELLOW) + add_line(Win, u' {flair}'.format(**data), attr=Color.RED) \ No newline at end of file