Implemented add_line wrapper to fix unicode bugs with curses.addnstr.

This commit is contained in:
Michael Lazar
2015-05-02 16:23:02 -07:00
parent 49a9147ca6
commit 02e9e5e399
4 changed files with 85 additions and 68 deletions

View File

@@ -6,12 +6,12 @@ from curses import textpad, ascii
from contextlib import contextmanager from contextlib import contextmanager
from .docs import HELP from .docs import HELP
from .helpers import strip_textpad from .helpers import strip_textpad, clean
from .exceptions import EscapeInterrupt from .exceptions import EscapeInterrupt
__all__ = ['ESCAPE', 'UARROW', 'DARROW', 'BULLET', 'show_notification', __all__ = ['ESCAPE', 'UARROW', 'DARROW', 'BULLET', 'show_notification',
'show_help', 'LoadScreen', 'Color', 'text_input', 'curses_session', 'show_help', 'LoadScreen', 'Color', 'text_input', 'curses_session',
'prompt_input'] 'prompt_input', 'add_line']
ESCAPE = 27 ESCAPE = 27
@@ -25,6 +25,49 @@ DARROW = u'\u25bc'.encode('utf-8')
BULLET = u'\u2022'.encode('utf-8') BULLET = u'\u2022'.encode('utf-8')
GOLD = u'\u272A'.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): def show_notification(stdscr, message):
""" """
@@ -52,7 +95,7 @@ def show_notification(stdscr, message):
window.border() window.border()
for index, line in enumerate(message, start=1): for index, line in enumerate(message, start=1):
window.addnstr(index, 1, line, box_width - 2) add_line(window, line, index, 1)
window.refresh() window.refresh()
ch = stdscr.getch() ch = stdscr.getch()

View File

@@ -8,9 +8,9 @@ from contextlib import contextmanager
import praw.errors import praw.errors
import requests import requests
from .helpers import clean, open_editor from .helpers import open_editor
from .curses_helpers import (Color, show_notification, show_help, text_input, 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 from .docs import COMMENT_EDIT_FILE, SUBMISSION_FILE
__all__ = ['Navigator', 'BaseController', 'BasePage'] __all__ = ['Navigator', 'BaseController', 'BasePage']
@@ -470,17 +470,16 @@ class BasePage(object):
self._header_window.bkgd(' ', attr) self._header_window.bkgd(' ', attr)
sub_name = self.content.name.replace('/r/front', 'Front Page ') sub_name = self.content.name.replace('/r/front', 'Front Page ')
sub_name = 'blank' add_line(self._header_window, sub_name, 0, 0)
self._header_window.addnstr(0, 0, clean(sub_name), n_cols - 1)
if self.reddit.user is not None: if self.reddit.user is not None:
username = self.reddit.user.name username = self.reddit.user.name
# TODO: use unicode width here instead of length
s_col = (n_cols - len(username) - 1) s_col = (n_cols - len(username) - 1)
# Only print the username if it fits in the empty space on the # Only print username if it fits in the empty space on the right
# right
if (s_col - 1) >= len(sub_name): if (s_col - 1) >= len(sub_name):
n = (n_cols - s_col - 1) 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() self._header_window.refresh()

View File

@@ -7,9 +7,9 @@ import praw.errors
from .content import SubmissionContent from .content import SubmissionContent
from .page import BasePage, Navigator, BaseController 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, 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 from .docs import COMMENT_FILE
__all__ = ['SubmissionController', 'SubmissionPage'] __all__ = ['SubmissionController', 'SubmissionPage']
@@ -158,15 +158,13 @@ class SubmissionPage(BasePage):
row = offset row = offset
if row in valid_rows: if row in valid_rows:
text = clean(u'{author} '.format(**data))
attr = curses.A_BOLD attr = curses.A_BOLD
attr |= (Color.BLUE if not data['is_author'] else Color.GREEN) 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']: if data['flair']:
text = clean(u'{flair} '.format(**data))
attr = curses.A_BOLD | Color.YELLOW 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: if data['likes'] is None:
text, attr = BULLET, curses.A_BOLD text, attr = BULLET, curses.A_BOLD
@@ -174,20 +172,18 @@ class SubmissionPage(BasePage):
text, attr = UARROW, (curses.A_BOLD | Color.GREEN) text, attr = UARROW, (curses.A_BOLD | Color.GREEN)
else: else:
text, attr = DARROW, (curses.A_BOLD | Color.RED) 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)) add_line(win, u' {score} {created} '.format(**data))
win.addnstr(text, n_cols - win.getyx()[1])
if data['gold']: if data['gold']:
text, attr = GOLD, (curses.A_BOLD | Color.YELLOW) 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']) n_body = len(data['split_body'])
for row, text in enumerate(data['split_body'], start=offset + 1): for row, text in enumerate(data['split_body'], start=offset + 1):
if row in valid_rows: if row in valid_rows:
text = clean(text) add_line(win, text, row, 1)
win.addnstr(row, 1, text, n_cols - 1)
# Unfortunately vline() doesn't support custom color so we have to # Unfortunately vline() doesn't support custom color so we have to
# build it one segment at a time. # build it one segment at a time.
@@ -210,13 +206,9 @@ class SubmissionPage(BasePage):
n_rows, n_cols = win.getmaxyx() n_rows, n_cols = win.getmaxyx()
n_cols -= 1 n_cols -= 1
text = clean(u'{body}'.format(**data)) add_line(win, u'{body}'.format(**data), 0, 1)
win.addnstr(0, 1, text, n_cols - 1) add_line(win, u' [{count}]'.format(**data), attr=curses.A_BOLD)
text = clean(u' [{count}]'.format(**data))
win.addnstr(text, n_cols - win.getyx()[1], 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']) attr = Color.get_level(data['level'])
win.addch(0, 0, curses.ACS_VLINE, attr) 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 n_cols -= 3 # one for each side of the border + one for offset
for row, text in enumerate(data['split_title'], start=1): for row, text in enumerate(data['split_title'], start=1):
text = clean(text) add_line(win, text, row, 1, curses.A_BOLD)
win.addnstr(row, 1, text, n_cols, curses.A_BOLD)
row = len(data['split_title']) + 1 row = len(data['split_title']) + 1
attr = curses.A_BOLD | Color.GREEN attr = curses.A_BOLD | Color.GREEN
text = clean(u'{author}'.format(**data)) add_line(win, u'{author}'.format(**data), row, 1, attr)
win.addnstr(row, 1, text, n_cols, attr)
attr = curses.A_BOLD | Color.YELLOW attr = curses.A_BOLD | Color.YELLOW
text = clean(u' {flair}'.format(**data)) add_line(win, u' {flair}'.format(**data), attr=attr)
win.addnstr(text, n_cols - win.getyx()[1], attr) add_line(win, u' {created} {subreddit}'.format(**data))
text = clean(u' {created} {subreddit}'.format(**data))
win.addnstr(text, n_cols - win.getyx()[1])
row = len(data['split_title']) + 2 row = len(data['split_title']) + 2
attr = curses.A_UNDERLINE | Color.BLUE attr = curses.A_UNDERLINE | Color.BLUE
text = clean(u'{url}'.format(**data)) add_line(win, u'{url}'.format(**data), row, 1, attr)
win.addnstr(row, 1, text, n_cols, attr)
offset = len(data['split_title']) + 3 offset = len(data['split_title']) + 3
# Cut off text if there is not enough room to display the whole post # 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)') split_text.append('(Not enough space to display)')
for row, text in enumerate(split_text, start=offset): for row, text in enumerate(split_text, start=offset):
text = clean(text) add_line(win, text, row, 1)
win.addnstr(row, 1, text, n_cols)
row = len(data['split_title']) + len(split_text) + 3 row = len(data['split_title']) + len(split_text) + 3
text = clean(u'{score} '.format(**data)) add_line(win, u'{score} '.format(**data), row, 1)
win.addnstr(row, 1, text, n_cols - 1)
if data['likes'] is None: if data['likes'] is None:
text, attr = BULLET, curses.A_BOLD text, attr = BULLET, curses.A_BOLD
@@ -269,17 +254,15 @@ class SubmissionPage(BasePage):
text, attr = UARROW, curses.A_BOLD | Color.GREEN text, attr = UARROW, curses.A_BOLD | Color.GREEN
else: else:
text, attr = DARROW, curses.A_BOLD | Color.RED text, attr = DARROW, curses.A_BOLD | Color.RED
win.addnstr(text, n_cols - win.getyx()[1], attr) add_line(win, text, attr=attr)
add_line(win, u' {comments} '.format(**data))
text = clean(u' {comments} '.format(**data))
win.addnstr(text, n_cols - win.getyx()[1])
if data['gold']: if data['gold']:
text, attr = GOLD, (curses.A_BOLD | Color.YELLOW) 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']: if data['nsfw']:
text, attr = 'NSFW', (curses.A_BOLD | Color.RED) 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() win.border()

View File

@@ -9,9 +9,9 @@ from .exceptions import SubredditError, AccountError
from .page import BasePage, Navigator, BaseController from .page import BasePage, Navigator, BaseController
from .submission import SubmissionPage from .submission import SubmissionPage
from .content import SubredditContent 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 .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) LoadScreen, show_notification, prompt_input)
__all__ = ['opened_links', 'SubredditController', 'SubredditPage'] __all__ = ['opened_links', 'SubredditController', 'SubredditPage']
@@ -163,21 +163,18 @@ class SubredditPage(BasePage):
n_title = len(data['split_title']) n_title = len(data['split_title'])
for row, text in enumerate(data['split_title'], start=offset): for row, text in enumerate(data['split_title'], start=offset):
if row in valid_rows: if row in valid_rows:
text = clean(text) add_line(win, text, row, 1, curses.A_BOLD)
win.addnstr(row, 1, text, n_cols - 1, curses.A_BOLD)
row = n_title + offset row = n_title + offset
if row in valid_rows: if row in valid_rows:
seen = (data['url_full'] in opened_links) seen = (data['url_full'] in opened_links)
link_color = Color.MAGENTA if seen else Color.BLUE link_color = Color.MAGENTA if seen else Color.BLUE
attr = curses.A_UNDERLINE | link_color attr = curses.A_UNDERLINE | link_color
text = clean(u'{url}'.format(**data)) add_line(win, u'{url}'.format(**data), row, 1, attr)
win.addnstr(row, 1, text, n_cols - 1, attr)
row = n_title + offset + 1 row = n_title + offset + 1
if row in valid_rows: if row in valid_rows:
text = clean(u'{score} '.format(**data)) add_line(win, u'{score} '.format(**data), row, 1)
win.addnstr(row, 1, text, n_cols - 1)
if data['likes'] is None: if data['likes'] is None:
text, attr = BULLET, curses.A_BOLD text, attr = BULLET, curses.A_BOLD
@@ -185,24 +182,19 @@ class SubredditPage(BasePage):
text, attr = UARROW, curses.A_BOLD | Color.GREEN text, attr = UARROW, curses.A_BOLD | Color.GREEN
else: else:
text, attr = DARROW, curses.A_BOLD | Color.RED text, attr = DARROW, curses.A_BOLD | Color.RED
win.addnstr(text, n_cols - win.getyx()[1], attr) add_line(win, text, attr=attr)
add_line(win, u' {created} {comments} '.format(**data))
text = clean(u' {created} {comments} '.format(**data))
win.addnstr(text, n_cols - win.getyx()[1])
if data['gold']: if data['gold']:
text, attr = GOLD, (curses.A_BOLD | Color.YELLOW) 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']: if data['nsfw']:
text, attr = 'NSFW', (curses.A_BOLD | Color.RED) 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 row = n_title + offset + 2
if row in valid_rows: if row in valid_rows:
text = clean(u'{author}'.format(**data)) add_line(win, u'{author}'.format(**data), row, 1, curses.A_BOLD)
win.addnstr(row, 1, text, n_cols - 1, curses.A_BOLD) add_line(win, u' {subreddit}'.format(**data), attr=Color.YELLOW)
text = clean(u' {subreddit}'.format(**data)) add_line(Win, u' {flair}'.format(**data), attr=Color.RED)
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)