Merge branch 'master' into themes

# Conflicts:
#	rtv/__main__.py
#	rtv/page.py
#	rtv/submission_page.py
#	rtv/subreddit_page.py
#	rtv/subscription_page.py
#	rtv/terminal.py
#	rtv/theme.py
This commit is contained in:
Michael Lazar
2017-09-10 22:26:06 -04:00
13 changed files with 176 additions and 120 deletions

View File

@@ -193,23 +193,23 @@ class OAuthHelper(object):
# If an exception is raised it will be seen by the thread # If an exception is raised it will be seen by the thread
# so we don't need to explicitly shutdown() the server # so we don't need to explicitly shutdown() the server
_logger.exception(e) _logger.exception(e)
self.term.show_notification('Browser Error') self.term.show_notification('Browser Error', style='error')
else: else:
self.server.shutdown() self.server.shutdown()
finally: finally:
thread.join() thread.join()
if self.params['error'] == 'access_denied': if self.params['error'] == 'access_denied':
self.term.show_notification('Denied access') self.term.show_notification('Denied access', style='error')
return return
elif self.params['error']: elif self.params['error']:
self.term.show_notification('Authentication error') self.term.show_notification('Authentication error', style='error')
return return
elif self.params['state'] is None: elif self.params['state'] is None:
# Something went wrong but it's not clear what happened # Something went wrong but it's not clear what happened
return return
elif self.params['state'] != state: elif self.params['state'] != state:
self.term.show_notification('UUID mismatch') self.term.show_notification('UUID mismatch', style='error')
return return
with self.term.loader('Logging in'): with self.term.loader('Logging in'):

View File

@@ -4,7 +4,6 @@ from __future__ import unicode_literals
import os import os
import sys import sys
import time import time
import curses
import logging import logging
from functools import wraps from functools import wraps
@@ -60,7 +59,7 @@ class Page(object):
def refresh_content(self, order=None, name=None): def refresh_content(self, order=None, name=None):
raise NotImplementedError raise NotImplementedError
def _draw_item(self, window, data, inverted, highlight): def _draw_item(self, window, data, inverted):
raise NotImplementedError raise NotImplementedError
def get_selected_item(self): def get_selected_item(self):
@@ -467,7 +466,8 @@ class Page(object):
if self.content.order is not None: if self.content.order is not None:
order = self.content.order.split('-')[0] order = self.content.order.split('-')[0]
col = text.find(order) - 3 col = text.find(order) - 3
window.chgat(0, col, 3, self.term.attr('order_bar', True)) attr = self.term.theme.get('order_bar', modifier='selected')
window.chgat(0, col, 3, attr)
self._row += 1 self._row += 1
@@ -536,12 +536,15 @@ class Page(object):
# Now that the windows are setup, we can take a second pass through # Now that the windows are setup, we can take a second pass through
# to draw the content # to draw the content
for index, (win, data, inverted) in enumerate(self._subwindows): for index, (win, data, inverted) in enumerate(self._subwindows):
highlight = (index == self.nav.cursor_index) if index == self.nav.cursor_index:
if highlight: # This lets the theme know to invert the cursor
win.bkgd(str(' '), self.term.attr('@highlight')) modifier = 'selected'
else: else:
win.bkgd(str(' '), self.term.attr('@normal')) modifier = None
self._draw_item(win, data, inverted, highlight)
with self.term.theme.set_modifier(modifier):
win.bkgd(str(' '), self.term.attr('normal'))
self._draw_item(win, data, inverted)
self._row += win_n_rows self._row += win_n_rows

View File

@@ -3,7 +3,6 @@ from __future__ import unicode_literals
import re import re
import time import time
import curses
from . import docs from . import docs
from .content import SubmissionContent, SubredditContent from .content import SubmissionContent, SubredditContent
@@ -264,16 +263,18 @@ class SubmissionPage(Page):
self.clear_input_queue() self.clear_input_queue()
def _draw_item(self, win, data, inverted, highlight): def _draw_item(self, win, data, inverted):
if data['type'] in ('MoreComments', 'HiddenComment'): if data['type'] == 'MoreComments':
self._draw_more_comments(win, data, highlight) return self._draw_more_comments(win, data)
elif data['type'] == 'HiddenComment':
return self._draw_more_comments(win, data)
elif data['type'] == 'Comment': elif data['type'] == 'Comment':
self._draw_comment(win, data, inverted, highlight) return self._draw_comment(win, data, inverted)
else: else:
self._draw_submission(win, data, highlight) return self._draw_submission(win, data)
def _draw_comment(self, win, data, inverted, highlight): def _draw_comment(self, win, data, inverted):
n_rows, n_cols = win.getmaxyx() n_rows, n_cols = win.getmaxyx()
n_cols -= 1 n_cols -= 1
@@ -287,7 +288,7 @@ class SubmissionPage(Page):
split_body = data['split_body'] split_body = data['split_body']
if data['n_rows'] > n_rows: if data['n_rows'] > n_rows:
# Only when there is a single comment on the page and not inverted # Only when there is a single comment on the page and not inverted
if not inverted and len(self._subwindows) == 0: if not inverted and len(self._subwindows) == 1:
cutoff = data['n_rows'] - n_rows + 1 cutoff = data['n_rows'] - n_rows + 1
split_body = split_body[:-cutoff] split_body = split_body[:-cutoff]
split_body.append('(Not enough space to display)') split_body.append('(Not enough space to display)')
@@ -295,104 +296,104 @@ class SubmissionPage(Page):
row = offset row = offset
if row in valid_rows: if row in valid_rows:
if data['is_author']: if data['is_author']:
attr = self.term.attr('comment_author_self', highlight) attr = self.term.attr('comment_author_self')
text = '{author} [S]'.format(**data) text = '{author} [S]'.format(**data)
else: else:
attr = self.term.attr('comment_author', highlight) attr = self.term.attr('comment_author')
text = '{author}'.format(**data) text = '{author}'.format(**data)
self.term.add_line(win, text, row, 1, attr) self.term.add_line(win, text, row, 1, attr)
if data['flair']: if data['flair']:
attr = self.term.attr('user_flair', highlight) attr = self.term.attr('user_flair')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, '{flair}'.format(**data), attr=attr) self.term.add_line(win, '{flair}'.format(**data), attr=attr)
arrow, attr = self.term.get_arrow(data['likes'], highlight) arrow, attr = self.term.get_arrow(data['likes'])
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, arrow, attr=attr) self.term.add_line(win, arrow, attr=attr)
attr = self.term.attr('score', highlight) attr = self.term.attr('score')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, '{score}'.format(**data), attr=attr) self.term.add_line(win, '{score}'.format(**data), attr=attr)
attr = self.term.attr('created', highlight) attr = self.term.attr('created')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, '{created}'.format(**data), attr=attr) self.term.add_line(win, '{created}'.format(**data), attr=attr)
if data['gold']: if data['gold']:
attr = self.term.attr('gold', highlight) attr = self.term.attr('gold')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, self.term.guilded, attr=attr) self.term.add_line(win, self.term.guilded, attr=attr)
if data['stickied']: if data['stickied']:
attr = self.term.attr('stickied', highlight) attr = self.term.attr('stickied')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, '[stickied]', attr=attr) self.term.add_line(win, '[stickied]', attr=attr)
if data['saved']: if data['saved']:
attr = self.term.attr('saved', highlight) attr = self.term.attr('saved')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, '[saved]', attr=attr) self.term.add_line(win, '[saved]', attr=attr)
for row, text in enumerate(split_body, start=offset+1): for row, text in enumerate(split_body, start=offset+1):
attr = self.term.attr('comment_text', highlight) attr = self.term.attr('comment_text')
if row in valid_rows: if row in valid_rows:
self.term.add_line(win, text, row, 1, attr=attr) self.term.add_line(win, text, row, 1, attr=attr)
# 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.
index = data['level'] % len(self.term.theme.BAR_LEVELS) index = data['level'] % len(self.term.theme.BAR_LEVELS)
attr = self.term.attr(self.term.theme.BAR_LEVELS[index], highlight) attr = self.term.attr(self.term.theme.BAR_LEVELS[index])
for y in range(n_rows): for y in range(n_rows):
self.term.addch(win, y, 0, self.term.vline, attr) self.term.addch(win, y, 0, self.term.vline, attr)
def _draw_more_comments(self, win, data, highlight): def _draw_more_comments(self, win, data):
n_rows, n_cols = win.getmaxyx() n_rows, n_cols = win.getmaxyx()
n_cols -= 1 n_cols -= 1
attr = self.term.attr('hidden_comment_text', highlight) attr = self.term.attr('hidden_comment_text')
self.term.add_line(win, '{body}'.format(**data), 0, 1, attr=attr) self.term.add_line(win, '{body}'.format(**data), 0, 1, attr=attr)
attr = self.term.attr('hidden_comment_expand', highlight) attr = self.term.attr('hidden_comment_expand')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, '[{count}]'.format(**data), attr=attr) self.term.add_line(win, '[{count}]'.format(**data), attr=attr)
index = data['level'] % len(self.term.theme.BAR_LEVELS) index = data['level'] % len(self.term.theme.BAR_LEVELS)
attr = self.term.attr(self.term.theme.BAR_LEVELS[index], highlight) attr = self.term.attr(self.term.theme.BAR_LEVELS[index])
self.term.addch(win, 0, 0, self.term.vline, attr) self.term.addch(win, 0, 0, self.term.vline, attr)
def _draw_submission(self, win, data, highlight): def _draw_submission(self, win, data):
n_rows, n_cols = win.getmaxyx() n_rows, n_cols = win.getmaxyx()
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
attr = self.term.attr('submission_title', highlight) attr = self.term.attr('submission_title')
for row, text in enumerate(data['split_title'], start=1): for row, text in enumerate(data['split_title'], start=1):
self.term.add_line(win, text, row, 1, attr) self.term.add_line(win, text, row, 1, attr)
row = len(data['split_title']) + 1 row = len(data['split_title']) + 1
attr = self.term.attr('submission_author', highlight) attr = self.term.attr('submission_author')
self.term.add_line(win, '{author}'.format(**data), row, 1, attr) self.term.add_line(win, '{author}'.format(**data), row, 1, attr)
if data['flair']: if data['flair']:
attr = self.term.attr('submission_flair', highlight) attr = self.term.attr('submission_flair')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, '{flair}'.format(**data), attr=attr) self.term.add_line(win, '{flair}'.format(**data), attr=attr)
attr = self.term.attr('created', highlight) attr = self.term.attr('created')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, '{created}'.format(**data), attr=attr) self.term.add_line(win, '{created}'.format(**data), attr=attr)
attr = self.term.attr('submission_subreddit', highlight) attr = self.term.attr('submission_subreddit')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, '/r/{subreddit}'.format(**data), attr=attr) self.term.add_line(win, '/r/{subreddit}'.format(**data), attr=attr)
row = len(data['split_title']) + 2 row = len(data['split_title']) + 2
if data['url_full'] in self.config.history: if data['url_full'] in self.config.history:
attr = self.term.attr('url_seen', highlight) attr = self.term.attr('url_seen')
else: else:
attr = self.term.attr('url', highlight) attr = self.term.attr('url')
self.term.add_line(win, '{url}'.format(**data), row, 1, attr) self.term.add_line(win, '{url}'.format(**data), row, 1, attr)
offset = len(data['split_title']) + 3 offset = len(data['split_title']) + 3
@@ -404,34 +405,34 @@ class SubmissionPage(Page):
split_text = split_text[:-cutoff] split_text = split_text[:-cutoff]
split_text.append('(Not enough space to display)') split_text.append('(Not enough space to display)')
attr = self.term.attr('submission_text', highlight) attr = self.term.attr('submission_text')
for row, text in enumerate(split_text, start=offset): for row, text in enumerate(split_text, start=offset):
self.term.add_line(win, text, row, 1, attr=attr) self.term.add_line(win, text, row, 1, attr=attr)
row = len(data['split_title']) + len(split_text) + 3 row = len(data['split_title']) + len(split_text) + 3
attr = self.term.attr('score', highlight) attr = self.term.attr('score')
self.term.add_line(win, '{score}'.format(**data), row, 1, attr=attr) self.term.add_line(win, '{score}'.format(**data), row, 1, attr=attr)
arrow, attr = self.term.get_arrow(data['likes'], highlight) arrow, attr = self.term.get_arrow(data['likes'])
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, arrow, attr=attr) self.term.add_line(win, arrow, attr=attr)
attr = self.term.attr('comment_count', highlight) attr = self.term.attr('comment_count')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, '{comments}'.format(**data), attr=attr) self.term.add_line(win, '{comments}'.format(**data), attr=attr)
if data['gold']: if data['gold']:
attr = self.term.attr('gold', highlight) attr = self.term.attr('gold')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, self.term.guilded, attr=attr) self.term.add_line(win, self.term.guilded, attr=attr)
if data['nsfw']: if data['nsfw']:
attr = self.term.attr('nsfw', highlight) attr = self.term.attr('nsfw')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, 'NSFW', attr=attr) self.term.add_line(win, 'NSFW', attr=attr)
if data['saved']: if data['saved']:
attr = self.term.attr('saved', highlight) attr = self.term.attr('saved')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, '[saved]', attr=attr) self.term.add_line(win, '[saved]', attr=attr)

View File

@@ -253,7 +253,7 @@ class SubredditPage(Page):
self.content = page.selected_subreddit self.content = page.selected_subreddit
self.nav = Navigator(self.content.get) self.nav = Navigator(self.content.get)
def _draw_item(self, win, data, inverted, highlight): def _draw_item(self, win, data, inverted):
n_rows, n_cols = win.getmaxyx() n_rows, n_cols = win.getmaxyx()
n_cols -= 1 # Leave space for the cursor in the first column n_cols -= 1 # Leave space for the cursor in the first column
@@ -264,75 +264,75 @@ class SubredditPage(Page):
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):
attr = self.term.attr('submission_title', highlight) attr = self.term.attr('submission_title')
if row in valid_rows: if row in valid_rows:
self.term.add_line(win, text, row, 1, attr) self.term.add_line(win, text, row, 1, attr)
row = n_title + offset row = n_title + offset
if row in valid_rows: if row in valid_rows:
if data['url_full'] in self.config.history: if data['url_full'] in self.config.history:
attr = self.term.attr('url_seen', highlight) attr = self.term.attr('url_seen')
else: else:
attr = self.term.attr('url', highlight) attr = self.term.attr('url')
self.term.add_line(win, '{url}'.format(**data), row, 1, attr) self.term.add_line(win, '{url}'.format(**data), row, 1, attr)
row = n_title + offset + 1 row = n_title + offset + 1
if row in valid_rows: if row in valid_rows:
attr = self.term.attr('score', highlight) attr = self.term.attr('score')
self.term.add_line(win, '{score}'.format(**data), row, 1, attr) self.term.add_line(win, '{score}'.format(**data), row, 1, attr)
self.term.add_space(win) self.term.add_space(win)
arrow, attr = self.term.get_arrow(data['likes'], highlight) arrow, attr = self.term.get_arrow(data['likes'])
self.term.add_line(win, arrow, attr=attr) self.term.add_line(win, arrow, attr=attr)
self.term.add_space(win) self.term.add_space(win)
attr = self.term.attr('created', highlight) attr = self.term.attr('created')
self.term.add_line(win, '{created}'.format(**data), attr=attr) self.term.add_line(win, '{created}'.format(**data), attr=attr)
if data['comments'] is not None: if data['comments'] is not None:
attr = self.term.attr('separator', highlight) attr = self.term.attr('separator')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, '-', attr=attr) self.term.add_line(win, '-', attr=attr)
attr = self.term.attr('comment_count', highlight) attr = self.term.attr('comment_count')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, '{comments}'.format(**data), attr=attr) self.term.add_line(win, '{comments}'.format(**data), attr=attr)
if data['saved']: if data['saved']:
attr = self.term.attr('saved', highlight) attr = self.term.attr('saved')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, '[saved]', attr=attr) self.term.add_line(win, '[saved]', attr=attr)
if data['stickied']: if data['stickied']:
attr = self.term.attr('stickied', highlight) attr = self.term.attr('stickied')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, '[stickied]', attr=attr) self.term.add_line(win, '[stickied]', attr=attr)
if data['gold']: if data['gold']:
attr = self.term.attr('gold', highlight) attr = self.term.attr('gold')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, self.term.guilded, attr=attr) self.term.add_line(win, self.term.guilded, attr=attr)
if data['nsfw']: if data['nsfw']:
attr = self.term.attr('nsfw', highlight) attr = self.term.attr('nsfw')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, 'NSFW', attr=attr) self.term.add_line(win, 'NSFW', attr=attr)
row = n_title + offset + 2 row = n_title + offset + 2
if row in valid_rows: if row in valid_rows:
attr = self.term.attr('submission_author', highlight) attr = self.term.attr('submission_author')
self.term.add_line(win, '{author}'.format(**data), row, 1, attr) self.term.add_line(win, '{author}'.format(**data), row, 1, attr)
self.term.add_space(win) self.term.add_space(win)
attr = self.term.attr('submission_subreddit', highlight) attr = self.term.attr('submission_subreddit')
self.term.add_line(win, '/r/{subreddit}'.format(**data), attr=attr) self.term.add_line(win, '/r/{subreddit}'.format(**data), attr=attr)
if data['flair']: if data['flair']:
attr = self.term.attr('submission_flair', highlight) attr = self.term.attr('submission_flair')
self.term.add_space(win) self.term.add_space(win)
self.term.add_line(win, '{flair}'.format(**data), attr=attr) self.term.add_line(win, '{flair}'.format(**data), attr=attr)
attr = self.term.attr('cursor', highlight) attr = self.term.attr('cursor')
for y in range(n_rows): for y in range(n_rows):
self.term.addch(win, y, 0, str(' '), attr) self.term.addch(win, y, 0, str(' '), attr)

View File

@@ -83,7 +83,7 @@ class SubscriptionPage(Page):
# Subscriptions can't be sorted, so disable showing the order menu # Subscriptions can't be sorted, so disable showing the order menu
pass pass
def _draw_item(self, win, data, inverted, highlight): def _draw_item(self, win, data, inverted):
n_rows, n_cols = win.getmaxyx() n_rows, n_cols = win.getmaxyx()
n_cols -= 1 # Leave space for the cursor in the first column n_cols -= 1 # Leave space for the cursor in the first column
@@ -94,20 +94,20 @@ class SubscriptionPage(Page):
row = offset row = offset
if row in valid_rows: if row in valid_rows:
if data['type'] == 'Multireddit': if data['type'] == 'Multireddit':
attr = self.term.attr('multireddit_name', highlight) attr = self.term.attr('multireddit_name')
else: else:
attr = self.term.attr('subscription_name', highlight) attr = self.term.attr('subscription_name')
self.term.add_line(win, '{name}'.format(**data), row, 1, attr) self.term.add_line(win, '{name}'.format(**data), row, 1, attr)
row = offset + 1 row = offset + 1
for row, text in enumerate(data['split_title'], start=row): for row, text in enumerate(data['split_title'], start=row):
if row in valid_rows: if row in valid_rows:
if data['type'] == 'Multireddit': if data['type'] == 'Multireddit':
attr = self.term.attr('multireddit_text', highlight) attr = self.term.attr('multireddit_text')
else: else:
attr = self.term.attr('subscription_text', highlight) attr = self.term.attr('subscription_text')
self.term.add_line(win, text, row, 1, attr) self.term.add_line(win, text, row, 1, attr)
attr = self.term.attr('cursor', highlight) attr = self.term.attr('cursor')
for y in range(n_rows): for y in range(n_rows):
self.term.addch(win, y, 0, str(' '), attr) self.term.addch(win, y, 0, str(' '), attr)

View File

@@ -183,7 +183,7 @@ class Terminal(object):
finally: finally:
self.stdscr.nodelay(0) self.stdscr.nodelay(0)
def get_arrow(self, likes, highlight=False): def get_arrow(self, likes):
""" """
Curses does define constants for symbols (e.g. curses.ACS_BULLET). Curses does define constants for symbols (e.g. curses.ACS_BULLET).
However, they rely on using the curses.addch() function, which has been However, they rely on using the curses.addch() function, which has been
@@ -193,11 +193,11 @@ class Terminal(object):
""" """
if likes is None: if likes is None:
return self.neutral_arrow, self.attr('neutral_vote', highlight) return self.neutral_arrow, self.attr('neutral_vote')
elif likes: elif likes:
return self.up_arrow, self.attr('upvote', highlight) return self.up_arrow, self.attr('upvote')
else: else:
return self.down_arrow, self.attr('downvote', highlight) return self.down_arrow, self.attr('downvote')
def clean(self, string, n_cols=None): def clean(self, string, n_cols=None):
""" """
@@ -282,7 +282,8 @@ class Terminal(object):
row, col = window.getyx() row, col = window.getyx()
_, max_cols = window.getmaxyx() _, max_cols = window.getmaxyx()
if max_cols - col - 1 <= 0: n_cols = max_cols - col - 1
if n_cols <= 0:
# Trying to draw outside of the screen bounds # Trying to draw outside of the screen bounds
return return
@@ -300,6 +301,8 @@ class Terminal(object):
notification window notification window
""" """
assert style in ('info', 'warning', 'error', 'success')
if isinstance(message, six.string_types): if isinstance(message, six.string_types):
message = message.splitlines() message = message.splitlines()
@@ -396,7 +399,7 @@ class Terminal(object):
_logger.warning(stderr) _logger.warning(stderr)
self.show_notification( self.show_notification(
'Program exited with status={0}\n{1}'.format( 'Program exited with status={0}\n{1}'.format(
code, stderr.strip())) code, stderr.strip()), style='error')
else: else:
# Non-blocking, open a background process # Non-blocking, open a background process
@@ -821,17 +824,18 @@ class Terminal(object):
else: else:
self.stdscr.clearok(True) self.stdscr.clearok(True)
def attr(self, element, highlight=False): def attr(self, element):
""" """
Shortcut for fetching the color + attribute code for an element. Shortcut for fetching the color + attribute code for an element.
""" """
return self.theme.get(element, highlight=highlight)
return self.theme.get(element)
def set_theme(self, theme=None): def set_theme(self, theme=None):
""" """
Check that the terminal supports the provided theme, and applies Check that the terminal supports the provided theme, and applies
the theme to the terminal if possible. the theme to the terminal if possible.
If the terminal doesn't support the theme, this falls back to the If the terminal doesn't support the theme, this falls back to the
default theme. The default theme only requires 8 colors so it default theme. The default theme only requires 8 colors so it
should be compatible with any terminal that supports basic colors. should be compatible with any terminal that supports basic colors.
@@ -862,4 +866,4 @@ class Terminal(object):
# Apply the default color to the whole screen # Apply the default color to the whole screen
self.stdscr.bkgd(str(' '), theme.get('@normal')) self.stdscr.bkgd(str(' '), theme.get('@normal'))
self.theme = theme self.theme = theme

View File

@@ -1,8 +1,9 @@
import os
import codecs import codecs
import configparser
import curses import curses
import logging import logging
import os import configparser
from contextlib import contextmanager
from .config import THEMES, DEFAULT_THEMES from .config import THEMES, DEFAULT_THEMES
from .exceptions import ConfigError from .exceptions import ConfigError
@@ -116,6 +117,7 @@ class Theme(object):
self.monochrome = monochrome self.monochrome = monochrome
self._color_pair_map = None self._color_pair_map = None
self._attribute_map = None self._attribute_map = None
self._modifier = None
self.required_color_pairs = 0 self.required_color_pairs = 0
self.required_colors = 0 self.required_colors = 0
@@ -206,7 +208,7 @@ class Theme(object):
self._attribute_map[element] = attrs self._attribute_map[element] = attrs
def get(self, val, highlight=False): def get(self, element, modifier=None):
""" """
Returns the curses attribute code for the given element. Returns the curses attribute code for the given element.
""" """
@@ -214,10 +216,25 @@ class Theme(object):
raise RuntimeError('Attempted to access theme attribute before ' raise RuntimeError('Attempted to access theme attribute before '
'calling initialize_curses_theme()') 'calling initialize_curses_theme()')
if highlight: modifier = modifier or self._modifier
val = val + '.highlight' if modifier:
modified_element = '{0}.{1}'.format(element, modifier)
if modified_element in self._elements:
return self._elements[modified_element]
return self._attribute_map[val] return self._attribute_map[element]
@contextmanager
def set_modifier(self, modifier=None):
# This case is undefined if the context manager is nested
assert self._modifier is None
self._modifier = modifier
try:
yield
finally:
self._modifier = None
@classmethod @classmethod
def list_themes(cls, path=THEMES): def list_themes(cls, path=THEMES):

View File

@@ -52,6 +52,9 @@ class MockStdscr(mock.MagicMock):
def getyx(self): def getyx(self):
return self.y, self.x return self.y, self.x
def getbegyx(self):
return 0, 0
def getmaxyx(self): def getmaxyx(self):
return self.nlines, self.ncols return self.nlines, self.ncols
@@ -154,12 +157,14 @@ def stdscr():
patch('curses.curs_set'), \ patch('curses.curs_set'), \
patch('curses.init_pair'), \ patch('curses.init_pair'), \
patch('curses.color_pair'), \ patch('curses.color_pair'), \
patch('curses.has_colors'), \
patch('curses.start_color'), \ patch('curses.start_color'), \
patch('curses.use_default_colors'): patch('curses.use_default_colors'):
out = MockStdscr(nlines=40, ncols=80, x=0, y=0) out = MockStdscr(nlines=40, ncols=80, x=0, y=0)
curses.initscr.return_value = out curses.initscr.return_value = out
curses.newwin.side_effect = lambda *args: out.derwin(*args) curses.newwin.side_effect = lambda *args: out.derwin(*args)
curses.color_pair.return_value = 23 curses.color_pair.return_value = 23
curses.has_colors.return_value = True
curses.ACS_VLINE = 0 curses.ACS_VLINE = 0
yield out yield out

View File

@@ -12,7 +12,7 @@ import requests
from six.moves import reload_module from six.moves import reload_module
from rtv import exceptions from rtv import exceptions
from rtv.objects import Color, Controller, Navigator, Command, KeyMap, \ from rtv.objects import Controller, Navigator, Command, KeyMap, \
curses_session, patch_webbrowser curses_session, patch_webbrowser
try: try:
@@ -189,21 +189,6 @@ def test_objects_load_screen_nested_complex(terminal, stdscr, use_ascii):
stdscr.subwin.addstr.assert_called_once_with(1, 1, error_message) stdscr.subwin.addstr.assert_called_once_with(1, 1, error_message)
def test_objects_color(stdscr):
colors = ['RED', 'GREEN', 'YELLOW', 'BLUE', 'MAGENTA', 'CYAN', 'WHITE']
# Check that all colors start with the default value
for color in colors:
assert getattr(Color, color) == curses.A_NORMAL
Color.init()
# Check that all colors are populated
for color in colors:
assert getattr(Color, color) == 23
def test_objects_curses_session(stdscr): def test_objects_curses_session(stdscr):
# Normal setup and cleanup # Normal setup and cleanup

View File

@@ -79,15 +79,16 @@ def test_submission_page_construct(reddit, terminal, config, oauth):
# Comment # Comment
comment_data = page.content.get(0) comment_data = page.content.get(0)
text = comment_data['split_body'][0].encode('utf-8') text = comment_data['split_body'][0].encode('utf-8')
window.subwin.addstr.assert_any_call(1, 1, text) window.subwin.addstr.assert_any_call(1, 1, text, curses.A_NORMAL)
# More Comments # More Comments
comment_data = page.content.get(1) comment_data = page.content.get(1)
text = comment_data['body'].encode('utf-8') text = comment_data['body'].encode('utf-8')
window.subwin.addstr.assert_any_call(0, 1, text) window.subwin.addstr.assert_any_call(0, 1, text, curses.A_NORMAL)
# Cursor should not be drawn when the page is first opened # Cursor should not be drawn when the page is first opened
assert not window.subwin.chgat.called assert not any(args[0][3] == curses.A_REVERSE
for args in window.subwin.addch.call_args_list)
# Reload with a smaller terminal window # Reload with a smaller terminal window
terminal.stdscr.ncols = 20 terminal.stdscr.ncols = 20
@@ -264,7 +265,7 @@ def test_submission_comment_not_enough_space(submission_page, terminal):
text = '(Not enough space to display)'.encode('ascii') text = '(Not enough space to display)'.encode('ascii')
window = terminal.stdscr.subwin window = terminal.stdscr.subwin
window.subwin.addstr.assert_any_call(6, 1, text) window.subwin.addstr.assert_any_call(6, 1, text, curses.A_NORMAL)
def test_submission_vote(submission_page, refresh_token): def test_submission_vote(submission_page, refresh_token):

View File

@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
import curses
import six import six
from rtv import __version__ from rtv import __version__
@@ -38,7 +40,7 @@ def test_subreddit_page_construct(reddit, terminal, config, oauth):
window.subwin.addstr.assert_any_call(0, 1, text, 2097152) window.subwin.addstr.assert_any_call(0, 1, text, 2097152)
# Cursor should have been drawn # Cursor should have been drawn
assert window.subwin.chgat.called window.subwin.addch.assert_any_call(0, 0, ' ', curses.A_REVERSE)
# Reload with a smaller terminal window # Reload with a smaller terminal window
terminal.stdscr.ncols = 20 terminal.stdscr.ncols = 20

View File

@@ -45,8 +45,8 @@ def test_subscription_page_construct(reddit, terminal, config, oauth,
window.addstr.assert_any_call(0, 0, menu) window.addstr.assert_any_call(0, 0, menu)
# Cursor - 2 lines # Cursor - 2 lines
window.subwin.chgat.assert_any_call(0, 0, 1, 262144) window.subwin.addch.assert_any_call(0, 0, ' ', 262144)
window.subwin.chgat.assert_any_call(1, 0, 1, 262144) window.subwin.addch.assert_any_call(1, 0, ' ', 262144)
# Reload with a smaller terminal window # Reload with a smaller terminal window
terminal.stdscr.ncols = 20 terminal.stdscr.ncols = 20

View File

@@ -9,6 +9,7 @@ import codecs
import six import six
import pytest import pytest
from rtv.theme import Theme
from rtv.docs import HELP, COMMENT_EDIT_FILE from rtv.docs import HELP, COMMENT_EDIT_FILE
from rtv.exceptions import TemporaryFileError, BrowserError from rtv.exceptions import TemporaryFileError, BrowserError
@@ -20,14 +21,10 @@ except ImportError:
def test_terminal_properties(terminal, config): def test_terminal_properties(terminal, config):
assert len(terminal.up_arrow) == 2 assert isinstance(terminal.up_arrow, six.text_type)
assert isinstance(terminal.up_arrow[0], six.text_type) assert isinstance(terminal.down_arrow, six.text_type)
assert len(terminal.down_arrow) == 2 assert isinstance(terminal.neutral_arrow, six.text_type)
assert isinstance(terminal.down_arrow[0], six.text_type) assert isinstance(terminal.guilded, six.text_type)
assert len(terminal.neutral_arrow) == 2
assert isinstance(terminal.neutral_arrow[0], six.text_type)
assert len(terminal.guilded) == 2
assert isinstance(terminal.guilded[0], six.text_type)
terminal._display = None terminal._display = None
with mock.patch('rtv.terminal.sys') as sys, \ with mock.patch('rtv.terminal.sys') as sys, \
@@ -60,6 +57,8 @@ def test_terminal_properties(terminal, config):
assert terminal.MIN_HEIGHT is not None assert terminal.MIN_HEIGHT is not None
assert terminal.MIN_WIDTH is not None assert terminal.MIN_WIDTH is not None
assert terminal.theme is not None
def test_terminal_functions(terminal): def test_terminal_functions(terminal):
@@ -558,3 +557,42 @@ def test_strip_textpad(terminal):
text = 'alpha bravo\ncharlie \ndelta \n echo \n\nfoxtrot\n\n\n' text = 'alpha bravo\ncharlie \ndelta \n echo \n\nfoxtrot\n\n\n'
assert terminal.strip_textpad(text) == ( assert terminal.strip_textpad(text) == (
'alpha bravocharlie delta\n echo\n\nfoxtrot') 'alpha bravocharlie delta\n echo\n\nfoxtrot')
def test_add_space(terminal, stdscr):
stdscr.x, stdscr.y = 10, 20
terminal.add_space(stdscr)
stdscr.addstr.assert_called_with(20, 10, ' ')
# Not enough room to add a space
stdscr.reset_mock()
stdscr.x = 10
stdscr.ncols = 11
terminal.add_space(stdscr)
assert not stdscr.addstr.called
def test_attr(terminal):
assert terminal.attr('cursor') == 0
assert terminal.attr('cursor.selected') == curses.A_REVERSE
assert terminal.attr('neutral_vote') == curses.A_BOLD
with terminal.theme.set_modifier('selected'):
assert terminal.attr('cursor') == curses.A_REVERSE
assert terminal.attr('neutral_vote') == curses.A_BOLD
def test_set_theme(terminal, stdscr):
stdscr.reset_mock()
terminal.set_theme()
assert not terminal.theme.monochrome
stdscr.bkgd.assert_called_once_with(' ', 0)
stdscr.reset_mock()
theme = Theme(monochrome=True)
terminal.set_theme(theme=theme)
assert terminal.theme.monochrome
stdscr.bkgd.assert_called_once_with(' ', 0)