Semi-major refactor, added upvotes/downvotes.

This commit is contained in:
Michael Lazar
2015-03-14 00:19:16 -07:00
parent 9ba161cdba
commit eba9238adc
6 changed files with 194 additions and 125 deletions

View File

@@ -10,13 +10,10 @@ from .errors import SubmissionURLError, SubredditNameError
def split_text(big_text, width): def split_text(big_text, width):
return [ return [
text text for line in big_text.splitlines()
for line in big_text.splitlines() # wrap returns an empty list when "line" is a newline. In order to
# wrap returns an empty list when "line" is a newline # consider newlines we need a list containing an empty string.
# in order to consider newlines we need a list containing an for text in (textwrap.wrap(line, width=width) or [''])]
# empty string
for text in (textwrap.wrap(line, width=width) or [''])
]
def strip_subreddit_url(permalink): def strip_subreddit_url(permalink):
""" """
@@ -64,12 +61,10 @@ class BaseContent(object):
def iterate(self, index, step, n_cols): def iterate(self, index, step, n_cols):
while True: while True:
# Hack to prevent displaying negative indicies if iterating in the
# negative direction.
if step < 0 and index < 0: if step < 0 and index < 0:
# Hack to prevent displaying negative indicies if iterating in
# the negative direction.
break break
try: try:
yield self.get(index, n_cols=n_cols) yield self.get(index, n_cols=n_cols)
except IndexError: except IndexError:
@@ -109,6 +104,11 @@ class BaseContent(object):
data['object'] = comment data['object'] = comment
data['level'] = comment.nested_level data['level'] = comment.nested_level
if getattr(comment.submission, 'author'):
sub_author = comment.submission.author.name
else:
sub_author = '[deleted]'
if isinstance(comment, praw.objects.MoreComments): if isinstance(comment, praw.objects.MoreComments):
data['type'] = 'MoreComments' data['type'] = 'MoreComments'
data['count'] = comment.count data['count'] = comment.count
@@ -119,9 +119,9 @@ class BaseContent(object):
data['created'] = humanize_timestamp(comment.created_utc) data['created'] = humanize_timestamp(comment.created_utc)
data['score'] = '{} pts'.format(comment.score) data['score'] = '{} pts'.format(comment.score)
data['author'] = (comment.author.name if getattr(comment, 'author') else '[deleted]') data['author'] = (comment.author.name if getattr(comment, 'author') else '[deleted]')
sub_author = (comment.submission.author.name if getattr(comment.submission, 'author') else '[deleted]')
data['is_author'] = (data['author'] == sub_author) data['is_author'] = (data['author'] == sub_author)
data['flair'] = (comment.author_flair_text if comment.author_flair_text else '') data['flair'] = (comment.author_flair_text if comment.author_flair_text else '')
data['likes'] = comment.likes
return data return data
@@ -148,6 +148,7 @@ class BaseContent(object):
data['flair'] = (sub.link_flair_text if sub.link_flair_text else '') data['flair'] = (sub.link_flair_text if sub.link_flair_text else '')
data['url_full'] = sub.url data['url_full'] = sub.url
data['url'] = ('selfpost' if is_selfpost(sub.url) else sub.url) data['url'] = ('selfpost' if is_selfpost(sub.url) else sub.url)
data['likes'] = sub.likes
return data return data

View File

@@ -5,9 +5,8 @@ import praw
from requests.exceptions import ConnectionError, HTTPError from requests.exceptions import ConnectionError, HTTPError
from praw.errors import InvalidUserPass from praw.errors import InvalidUserPass
from . import utils
from .errors import SubmissionURLError, SubredditNameError from .errors import SubmissionURLError, SubredditNameError
from .utils import curses_session, load_config, HELP from .utils import Symbol, curses_session, load_config, HELP
from .subreddit import SubredditPage from .subreddit import SubredditPage
from .submission import SubmissionPage from .submission import SubmissionPage
@@ -28,7 +27,7 @@ terminal window.
EPILOG = """ EPILOG = """
Controls Controls
----- --------
RTV currently supports browsing both subreddits and individual submissions. RTV currently supports browsing both subreddits and individual submissions.
In each mode the controls are slightly different. In subreddit mode you can In each mode the controls are slightly different. In subreddit mode you can
browse through the top submissions on either the front page or a specific browse through the top submissions on either the front page or a specific
@@ -45,7 +44,7 @@ def main():
formatter_class=argparse.RawDescriptionHelpFormatter) formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('-s', dest='subreddit', help='subreddit name') parser.add_argument('-s', dest='subreddit', help='subreddit name')
parser.add_argument('-l', dest='link', help='full link to a submission') parser.add_argument('-l', dest='link', help='full link to a submission')
parser.add_argument('--unicode', help='enable unicode (beta)', parser.add_argument('--unicode', help='enable unicode (experimental)',
action='store_true') action='store_true')
group = parser.add_argument_group( group = parser.add_argument_group(
@@ -64,7 +63,7 @@ def main():
if getattr(args, key) is None: if getattr(args, key) is None:
setattr(args, key, val) setattr(args, key, val)
utils.UNICODE = args.unicode Symbol.UNICODE = args.unicode
if args.subreddit is None: if args.subreddit is None:
args.subreddit = 'front' args.subreddit = 'front'
@@ -75,6 +74,7 @@ def main():
reddit.config.decode_html_entities = True reddit.config.decode_html_entities = True
if args.username: if args.username:
# PRAW will prompt for password if it is None
reddit.login(args.username, args.password) reddit.login(args.username, args.password)
with curses_session() as stdscr: with curses_session() as stdscr:

View File

@@ -1,6 +1,6 @@
import curses import curses
from .utils import Color, clean from .utils import Color, Symbol
class Navigator(object): class Navigator(object):
""" """
@@ -129,14 +129,41 @@ class BasePage(object):
continue continue
self.stdscr.nodelay(0) self.stdscr.nodelay(0)
def upvote(self):
data = self.content.get(self.nav.absolute_index)
if 'likes' not in data:
curses.flash()
elif data['likes']:
data['object'].clear_vote()
data['likes'] = None
else:
data['object'].upvote()
data['likes'] = True
def downvote(self):
data = self.content.get(self.nav.absolute_index)
if 'likes' not in data:
curses.flash()
if data['likes'] is False:
data['object'].clear_vote()
data['likes'] = None
else:
data['object'].downvote()
data['likes'] = False
def draw(self): def draw(self):
n_rows, n_cols = self.stdscr.getmaxyx() n_rows, n_cols = self.stdscr.getmaxyx()
if n_rows < self.MIN_HEIGHT or n_cols < self.MIN_WIDTH: if n_rows < self.MIN_HEIGHT or n_cols < self.MIN_WIDTH:
return return
# Note: 2 argument form of derwin breaks PDcurses on Windows 7!
self._header_window = self.stdscr.derwin(1, n_cols, 0, 0) self._header_window = self.stdscr.derwin(1, n_cols, 0, 0)
self._content_window = self.stdscr.derwin(1, 0) self._content_window = self.stdscr.derwin(n_rows-1, n_cols, 1, 0)
self.stdscr.erase() self.stdscr.erase()
self._draw_header() self._draw_header()
@@ -157,7 +184,7 @@ 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 ')
self._header_window.addnstr(0, 0, clean(sub_name), n_cols-1) self._header_window.addnstr(0, 0, Symbol.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
@@ -165,7 +192,7 @@ class BasePage(object):
# Only print the username if it fits in the empty space on the right # Only print the username if it fits in the empty space on the 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) self._header_window.addnstr(0, s_col, Symbol.clean(username), n)
self._header_window.refresh() self._header_window.refresh()
@@ -215,14 +242,11 @@ class BasePage(object):
self.remove_cursor() self.remove_cursor()
valid, redraw = self.nav.move(direction, len(self._subwindows)) valid, redraw = self.nav.move(direction, len(self._subwindows))
if not valid: if not valid: curses.flash()
curses.flash()
# TODO: If we don't redraw, ACS_VLINE gets screwed up when changing the
# attr back to normal. There may be a way around this.
if True: #if redraw
self._draw_content()
# Note: ACS_VLINE doesn't like changing the attribute, so always redraw.
# if redraw: self._draw_content()
self._draw_content()
self.add_cursor() self.add_cursor()
def _edit_cursor(self, attribute=None): def _edit_cursor(self, attribute=None):
@@ -231,7 +255,6 @@ class BasePage(object):
if self.nav.absolute_index < 0: if self.nav.absolute_index < 0:
return return
# TODO: attach attr to data[attr] or something
window, attr = self._subwindows[self.nav.cursor_index] window, attr = self._subwindows[self.nav.cursor_index]
if attr is not None: if attr is not None:
attribute |= attr attribute |= attr

View File

@@ -1,12 +1,9 @@
import curses import curses
import sys import sys
import webbrowser
import six
from .content import SubmissionContent from .content import SubmissionContent
from .page import BasePage from .page import BasePage
from .utils import LoadScreen, Color, ESCAPE, display_help, open_new_tab, clean from .utils import LoadScreen, Color, Symbol, display_help, open_browser
class SubmissionPage(BasePage): class SubmissionPage(BasePage):
@@ -42,6 +39,9 @@ class SubmissionPage(BasePage):
self.toggle_comment() self.toggle_comment()
self.draw() self.draw()
elif cmd in (curses.KEY_LEFT, ord('h')):
break
elif cmd == ord('o'): elif cmd == ord('o'):
self.open_link() self.open_link()
self.draw() self.draw()
@@ -54,17 +54,19 @@ class SubmissionPage(BasePage):
display_help(self.stdscr) display_help(self.stdscr)
self.draw() self.draw()
elif cmd == ord('a'):
self.upvote()
self.draw()
elif cmd == ord('z'):
self.downvote()
self.draw()
elif cmd == ord('q'): elif cmd == ord('q'):
sys.exit() sys.exit()
elif cmd == curses.KEY_RESIZE: elif cmd == curses.KEY_RESIZE:
self.draw() self.draw()
elif cmd in (ESCAPE, curses.KEY_LEFT, ord('h')):
break
else:
curses.beep()
def toggle_comment(self): def toggle_comment(self):
@@ -81,19 +83,16 @@ class SubmissionPage(BasePage):
# Always open the page for the submission # Always open the page for the submission
# May want to expand at some point to open comment permalinks # May want to expand at some point to open comment permalinks
url = self.content.get(-1)['permalink'] url = self.content.get(-1)['permalink']
open_new_tab(url) open_browser(url)
def draw_item(self, win, data, inverted=False): def draw_item(self, win, data, inverted=False):
if data['type'] == 'MoreComments': if data['type'] == 'MoreComments':
return self.draw_more_comments(win, data) return self.draw_more_comments(win, data)
elif data['type'] == 'HiddenComment': elif data['type'] == 'HiddenComment':
return self.draw_more_comments(win, data) return self.draw_more_comments(win, data)
elif data['type'] == 'Comment': elif data['type'] == 'Comment':
return self.draw_comment(win, data, inverted=inverted) return self.draw_comment(win, data, inverted=inverted)
else: else:
return self.draw_submission(win, data) return self.draw_submission(win, data)
@@ -109,53 +108,66 @@ class SubmissionPage(BasePage):
row = offset row = offset
if row in valid_rows: if row in valid_rows:
text = '{author}'.format(**data)
text = Symbol.clean('{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, clean(text), n_cols-1, attr) win.addnstr(row, 1, text, n_cols-1, attr)
text = ' {flair}'.format(**data)
win.addnstr(clean(text), n_cols-win.getyx()[1], curses.A_BOLD | Color.YELLOW) if data['flair']:
text = ' {score} {created}'.format(**data) text = Symbol.clean('{flair} '.format(**data))
win.addnstr(clean(text), n_cols - win.getyx()[1]) attr = curses.A_BOLD | Color.YELLOW
win.addnstr(text, n_cols-win.getyx()[1], attr)
if data['likes'] is None:
text, attr = Symbol.BULLET, curses.A_BOLD
elif data['likes']:
text, attr = Symbol.UARROW, (curses.A_BOLD | Color.GREEN)
else:
text, attr = Symbol.DARROW, (curses.A_BOLD | Color.RED)
win.addnstr(text, n_cols-win.getyx()[1], attr)
text = Symbol.clean(' {score} {created}'.format(**data))
win.addnstr(text, n_cols-win.getyx()[1])
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:
win.addnstr(row, 1, clean(text), n_cols-1) text = Symbol.clean(text)
win.addnstr(row, 1, text, n_cols-1)
# Vertical line, unfortunately vline() doesn't support custom color so # Unfortunately vline() doesn't support custom color so we have to
# we have to build it one chr at a time. # build it one segment at a time.
attr = Color.get_level(data['level']) attr = Color.get_level(data['level'])
for y in range(n_rows): for y in range(n_rows):
x = 0 x = 0
# Nobody pays attention to curses ;(
# http://bugs.python.org/issue21088 # http://bugs.python.org/issue21088
if (sys.version_info.major, if (sys.version_info.major,
sys.version_info.minor, sys.version_info.minor,
sys.version_info.micro) == (3, 4, 0): sys.version_info.micro) == (3, 4, 0):
x, y = y, x x, y = y, x
win.addch(y, x, curses.ACS_VLINE, attr) win.addch(y, x, curses.ACS_VLINE, attr)
return attr | curses.ACS_VLINE return (attr | curses.ACS_VLINE)
@staticmethod @staticmethod
def draw_more_comments(win, data): def draw_more_comments(win, data):
n_rows, n_cols = win.getmaxyx() n_rows, n_cols = win.getmaxyx()
n_cols -= 1 n_cols -= 1
text = '{body}'.format(**data)
win.addnstr(0, 1, clean(text), n_cols-1)
text = ' [{count}]'.format(**data)
win.addnstr(clean(text), n_cols - win.getyx()[1], curses.A_BOLD)
text = Symbol.clean('{body}'.format(**data))
win.addnstr(0, 1, text, n_cols-1)
text = Symbol.clean(' [{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'])
for y in range(n_rows): win.addch(0, 0, curses.ACS_VLINE, attr)
win.addch(y, 0, curses.ACS_VLINE, attr)
return attr | curses.ACS_VLINE return (attr | curses.ACS_VLINE)
@staticmethod @staticmethod
def draw_submission(win, data): def draw_submission(win, data):
@@ -169,28 +181,31 @@ class SubmissionPage(BasePage):
return return
for row, text in enumerate(data['split_title'], start=1): for row, text in enumerate(data['split_title'], start=1):
win.addnstr(row, 1, clean(text), n_cols, curses.A_BOLD) text = Symbol.clean(text)
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 = '{author}'.format(**data) text = Symbol.clean('{author}'.format(**data))
win.addnstr(row, 1, clean(text), n_cols, attr) win.addnstr(row, 1, text, n_cols, attr)
text = ' {flair}'.format(**data) attr = curses.A_BOLD | Color.YELLOW
win.addnstr(clean(text), n_cols-win.getyx()[1], curses.A_BOLD | Color.YELLOW) text = Symbol.clean(' {flair}'.format(**data))
text = ' {created} {subreddit}'.format(**data) win.addnstr(text, n_cols-win.getyx()[1], attr)
win.addnstr(clean(text), n_cols - win.getyx()[1]) text = Symbol.clean(' {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 = '{url}'.format(**data) text = Symbol.clean('{url}'.format(**data))
win.addnstr(row, 1, clean(text), n_cols, attr) win.addnstr(row, 1, text, n_cols, attr)
offset = len(data['split_title']) + 3 offset = len(data['split_title']) + 3
for row, text in enumerate(data['split_text'], start=offset): for row, text in enumerate(data['split_text'], start=offset):
win.addnstr(row, 1, clean(text), n_cols) text = Symbol.clean(text)
win.addnstr(row, 1, text, n_cols)
row = len(data['split_title']) + len(data['split_text']) + 3 row = len(data['split_title']) + len(data['split_text']) + 3
text = '{score} {comments}'.format(**data) text = Symbol.clean('{score} {comments}'.format(**data))
win.addnstr(row, 1, clean(text), n_cols, curses.A_BOLD) win.addnstr(row, 1, text, n_cols, curses.A_BOLD)
win.border() win.border()

View File

@@ -7,9 +7,8 @@ from .errors import SubredditNameError
from .page import BasePage from .page import BasePage
from .submission import SubmissionPage from .submission import SubmissionPage
from .content import SubredditContent from .content import SubredditContent
from .utils import (LoadScreen, Color, text_input, display_message, from .utils import (LoadScreen, Symbol, Color, text_input, display_message,
display_help, open_new_tab, clean) display_help, open_browser)
# Used to keep track of browsing history across the current session # Used to keep track of browsing history across the current session
_opened_links = set() _opened_links = set()
@@ -54,6 +53,14 @@ class SubredditPage(BasePage):
display_help(self.stdscr) display_help(self.stdscr)
self.draw() self.draw()
elif cmd == ord('a'):
self.upvote()
self.draw()
elif cmd == ord('z'):
self.downvote()
self.draw()
elif cmd == ord('q'): elif cmd == ord('q'):
sys.exit() sys.exit()
@@ -64,9 +71,6 @@ class SubredditPage(BasePage):
self.prompt_subreddit() self.prompt_subreddit()
self.draw() self.draw()
else:
curses.beep()
def refresh_content(self, name=None): def refresh_content(self, name=None):
name = name or self.content.name name = name or self.content.name
@@ -75,19 +79,23 @@ class SubredditPage(BasePage):
self.content = SubredditContent.from_name( self.content = SubredditContent.from_name(
self.reddit, name, self.loader) self.reddit, name, self.loader)
except (SubredditNameError, HTTPError): except SubredditNameError:
display_message(self.stdscr, ['Invalid Subreddit']) display_message(self.stdscr, ['Invalid subreddit'])
except HTTPError:
display_message(self.stdscr, ['Could not reach subreddit'])
else: else:
self.nav.page_index, self.nav.cursor_index = 0, 0 self.nav.page_index, self.nav.cursor_index = 0, 0
self.nav.inverted = False self.nav.inverted = False
def prompt_subreddit(self): def prompt_subreddit(self):
"Open a prompt to type in a new subreddit"
attr = curses.A_BOLD | Color.CYAN attr = curses.A_BOLD | Color.CYAN
prompt = 'Enter Subreddit: /r/' prompt = 'Enter Subreddit: /r/'
n_rows, n_cols = self.stdscr.getmaxyx() n_rows, n_cols = self.stdscr.getmaxyx()
self.stdscr.addstr(n_rows-1, 0, clean(prompt), attr) self.stdscr.addstr(n_rows-1, 0, prompt, attr)
self.stdscr.refresh() self.stdscr.refresh()
window = self.stdscr.derwin(1, n_cols-len(prompt),n_rows-1, len(prompt)) window = self.stdscr.derwin(1, n_cols-len(prompt),n_rows-1, len(prompt))
window.attrset(attr) window.attrset(attr)
@@ -108,9 +116,10 @@ class SubredditPage(BasePage):
_opened_links.add(data['url_full']) _opened_links.add(data['url_full'])
def open_link(self): def open_link(self):
"Open a link with the webbrowser"
url = self.content.get(self.nav.absolute_index)['url_full'] url = self.content.get(self.nav.absolute_index)['url_full']
open_new_tab(url) open_browser(url)
global _opened_links global _opened_links
_opened_links.add(url) _opened_links.add(url)
@@ -128,27 +137,38 @@ 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:
attr = curses.A_BOLD text = Symbol.clean(text)
win.addstr(row, 1, clean(text), attr) 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 = '{url}'.format(**data) text = Symbol.clean('{url}'.format(**data))
win.addnstr(row, 1, clean(text), n_cols-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 = '{created} {comments} {score}'.format(**data) text = Symbol.clean('{score} '.format(**data))
win.addnstr(row, 1, clean(text), n_cols-1) win.addnstr(row, 1, text, n_cols-1)
if data['likes'] is None:
text, attr = Symbol.BULLET, curses.A_BOLD
elif data['likes']:
text, attr = Symbol.UARROW, curses.A_BOLD | Color.GREEN
else:
text, attr = Symbol.DARROW, curses.A_BOLD | Color.RED
win.addnstr(text, n_cols-win.getyx()[1], attr)
text = Symbol.clean(' {created} {comments}'.format(**data))
win.addnstr(text, n_cols-win.getyx()[1])
row = n_title + offset + 2 row = n_title + offset + 2
if row in valid_rows: if row in valid_rows:
text = '{author}'.format(**data) text = Symbol.clean('{author}'.format(**data))
win.addnstr(row, 1, clean(text), n_cols-1, curses.A_BOLD) win.addnstr(row, 1, text, n_cols-1, curses.A_BOLD)
text = ' {subreddit}'.format(**data) text = Symbol.clean(' {subreddit}'.format(**data))
win.addnstr(clean(text), n_cols - win.getyx()[1], Color.YELLOW) win.addnstr(text, n_cols-win.getyx()[1], Color.YELLOW)
text = ' {flair}'.format(**data) text = Symbol.clean(' {flair}'.format(**data))
win.addnstr(clean(text), n_cols - win.getyx()[1], Color.RED) win.addnstr(text, n_cols-win.getyx()[1], Color.RED)

View File

@@ -12,9 +12,6 @@ from six.moves import configparser
from .errors import EscapePressed from .errors import EscapePressed
FORCE_ASCII = True
ESCAPE = 27
HELP = """ HELP = """
Global Commands Global Commands
`UP/DOWN` or `j/k` : Scroll to the prev/next item `UP/DOWN` or `j/k` : Scroll to the prev/next item
@@ -32,30 +29,43 @@ Submission Mode
`RIGHT` or `l` : Fold the selected comment, or load additional comments `RIGHT` or `l` : Fold the selected comment, or load additional comments
""" """
def clean(string): class Symbol(object):
"""
Required reading!
http://nedbatchelder.com/text/unipain.html
Python 2 input string will be a unicode type (unicode code points). Curses UNICODE = False
will accept that if all of the points are in the ascii range. However, if
any of the code points are not valid ascii curses will throw a
UnicodeEncodeError: 'ascii' codec can't encode character, ordinal not in
range(128). However, if we encode the unicode to a utf-8 byte string and
pass that to curses, curses will render correctly.
Python 3 input string will be a string type (unicode code points). Curses ESCAPE = 27
will accept that in all cases. However, the n character count in addnstr
will get screwed up.
""" # Curses does define constants for these (e.g. curses.ACS_BULLET)
if six.PY2: # However, they rely on using the curses.addch() function, which has been
string = string.encode('utf-8', 'replace') # found to be buggy and a PITA to work with. By defining them as unicode
else: # points they can be added via the more reliable curses.addstr().
string = string.encode('utf-8', 'replace') # http://bugs.python.org/issue21088
pass UARROW = u'\u25b2'.encode('utf-8')
DARROW = u'\u25bc'.encode('utf-8')
BULLET = u'\u2022'.encode('utf-8')
return string @classmethod
def clean(cls, string):
"""
Required reading!
http://nedbatchelder.com/text/unipain.html
Python 2 input string will be a unicode type (unicode code points). Curses
will accept that if all of the points are in the ascii range. However, if
any of the code points are not valid ascii curses will throw a
UnicodeEncodeError: 'ascii' codec can't encode character, ordinal not in
range(128). However, if we encode the unicode to a utf-8 byte string and
pass that to curses, curses will render correctly.
Python 3 input string will be a string type (unicode code points). Curses
will accept that in all cases. However, the n character count in addnstr
will get screwed up.
"""
encoding = 'utf-8' if cls.UNICODE else 'ascii'
string = string.encode(encoding, 'replace')
return string
class Color(object): class Color(object):
@@ -123,7 +133,7 @@ def text_input(window):
def validate(ch): def validate(ch):
"Filters characters for special key sequences" "Filters characters for special key sequences"
if ch == ESCAPE: if ch == Symbol.ESCAPE:
raise EscapePressed raise EscapePressed
# Fix backspace for iterm # Fix backspace for iterm
@@ -245,7 +255,7 @@ class LoadScreen(object):
window.refresh() window.refresh()
time.sleep(interval) time.sleep(interval)
def open_new_tab(url): def open_browser(url):
""" """
Call webbrowser.open_new_tab(url) and redirect stdout/stderr to devnull. Call webbrowser.open_new_tab(url) and redirect stdout/stderr to devnull.