Cherry picking backwards-compatible changes from the themes branch

This commit is contained in:
Michael Lazar
2017-09-08 01:10:32 -04:00
parent 0268408f71
commit 982861560a
16 changed files with 414 additions and 248 deletions

View File

@@ -35,9 +35,10 @@ from . import docs
from . import packages from . import packages
from .packages import praw from .packages import praw
from .config import Config, copy_default_config, copy_default_mailcap from .config import Config, copy_default_config, copy_default_mailcap
from .theme import Theme
from .oauth import OAuthHelper from .oauth import OAuthHelper
from .terminal import Terminal from .terminal import Terminal
from .objects import curses_session, Color, patch_webbrowser from .objects import curses_session, patch_webbrowser
from .subreddit_page import SubredditPage from .subreddit_page import SubredditPage
from .exceptions import ConfigError from .exceptions import ConfigError
from .__version__ import __version__ from .__version__ import __version__
@@ -169,11 +170,9 @@ def main():
try: try:
with curses_session() as stdscr: with curses_session() as stdscr:
# Initialize global color-pairs with curses theme = Theme(config['monochrome'])
if not config['monochrome']: term = Terminal(stdscr, config, theme)
Color.init()
term = Terminal(stdscr, config)
with term.loader('Initializing', catch_exception=False): with term.loader('Initializing', catch_exception=False):
reddit = praw.Reddit(user_agent=user_agent, reddit = praw.Reddit(user_agent=user_agent,
decode_html_entities=False, decode_html_entities=False,

View File

@@ -135,6 +135,9 @@ class OrderedSet(object):
class Config(object): class Config(object):
"""
This class manages the loading and saving of configs and other files.
"""
def __init__(self, history_file=HISTORY, token_file=TOKEN, **kwargs): def __init__(self, history_file=HISTORY, token_file=TOKEN, **kwargs):

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

@@ -230,7 +230,8 @@ class LoadScreen(object):
for e_type, message in self.EXCEPTION_MESSAGES: for e_type, message in self.EXCEPTION_MESSAGES:
# Some exceptions we want to swallow and display a notification # Some exceptions we want to swallow and display a notification
if isinstance(e, e_type): if isinstance(e, e_type):
self._terminal.show_notification(message.format(e)) msg = message.format(e)
self._terminal.show_notification(msg, style='error')
return True return True
def animate(self, delay, interval, message, trail): def animate(self, delay, interval, message, trail):
@@ -250,12 +251,16 @@ class LoadScreen(object):
return return
time.sleep(0.01) time.sleep(0.01)
# Build the notification window # Build the notification window. Note that we need to use
# curses.newwin() instead of stdscr.derwin() so the text below the
# notification window does not got erased when we cover it up.
message_len = len(message) + len(trail) message_len = len(message) + len(trail)
n_rows, n_cols = self._terminal.stdscr.getmaxyx() n_rows, n_cols = self._terminal.stdscr.getmaxyx()
s_row = (n_rows - 3) // 2 v_offset, h_offset = self._terminal.stdscr.getbegyx()
s_col = (n_cols - message_len - 1) // 2 s_row = (n_rows - 3) // 2 + v_offset
s_col = (n_cols - message_len - 1) // 2 + h_offset
window = curses.newwin(3, message_len + 2, s_row, s_col) window = curses.newwin(3, message_len + 2, s_row, s_col)
window.bkgd(str(' '), self._terminal.attr('notice_loading'))
# Animate the loading prompt until the stopping condition is triggered # Animate the loading prompt until the stopping condition is triggered
# when the context manager exits. # when the context manager exits.
@@ -285,49 +290,6 @@ class LoadScreen(object):
time.sleep(0.01) time.sleep(0.01)
class Color(object):
"""
Color attributes for curses.
"""
RED = curses.A_NORMAL
GREEN = curses.A_NORMAL
YELLOW = curses.A_NORMAL
BLUE = curses.A_NORMAL
MAGENTA = curses.A_NORMAL
CYAN = curses.A_NORMAL
WHITE = curses.A_NORMAL
_colors = {
'RED': (curses.COLOR_RED, -1),
'GREEN': (curses.COLOR_GREEN, -1),
'YELLOW': (curses.COLOR_YELLOW, -1),
'BLUE': (curses.COLOR_BLUE, -1),
'MAGENTA': (curses.COLOR_MAGENTA, -1),
'CYAN': (curses.COLOR_CYAN, -1),
'WHITE': (curses.COLOR_WHITE, -1),
}
@classmethod
def init(cls):
"""
Initialize color pairs inside of curses using the default background.
This should be called once during the curses initial setup. Afterwards,
curses color pairs can be accessed directly through class attributes.
"""
for index, (attr, code) in enumerate(cls._colors.items(), start=1):
curses.init_pair(index, code[0], code[1])
setattr(cls, attr, curses.color_pair(index))
@classmethod
def get_level(cls, level):
levels = [cls.MAGENTA, cls.CYAN, cls.GREEN, cls.YELLOW]
return levels[level % len(levels)]
class Navigator(object): class Navigator(object):
""" """
Handles the math behind cursor movement and screen paging. Handles the math behind cursor movement and screen paging.

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
@@ -12,7 +11,7 @@ import six
from kitchen.text.display import textual_width from kitchen.text.display import textual_width
from . import docs from . import docs
from .objects import Controller, Color, Command from .objects import Controller, Command
from .clipboard import copy from .clipboard import copy
from .exceptions import TemporaryFileError, ProgramError from .exceptions import TemporaryFileError, ProgramError
from .__version__ import __version__ from .__version__ import __version__
@@ -158,19 +157,15 @@ class Page(object):
@PageController.register(Command('PAGE_TOP')) @PageController.register(Command('PAGE_TOP'))
def move_page_top(self): def move_page_top(self):
self._remove_cursor()
self.nav.page_index = self.content.range[0] self.nav.page_index = self.content.range[0]
self.nav.cursor_index = 0 self.nav.cursor_index = 0
self.nav.inverted = False self.nav.inverted = False
self._add_cursor()
@PageController.register(Command('PAGE_BOTTOM')) @PageController.register(Command('PAGE_BOTTOM'))
def move_page_bottom(self): def move_page_bottom(self):
self._remove_cursor()
self.nav.page_index = self.content.range[1] self.nav.page_index = self.content.range[1]
self.nav.cursor_index = 0 self.nav.cursor_index = 0
self.nav.inverted = True self.nav.inverted = True
self._add_cursor()
@PageController.register(Command('UPVOTE')) @PageController.register(Command('UPVOTE'))
@logged_in @logged_in
@@ -376,7 +371,6 @@ class Page(object):
self._draw_banner() self._draw_banner()
self._draw_content() self._draw_content()
self._draw_footer() self._draw_footer()
self._add_cursor()
self.term.clear_screen() self.term.clear_screen()
self.term.stdscr.refresh() self.term.stdscr.refresh()
@@ -388,8 +382,7 @@ class Page(object):
window = self.term.stdscr.derwin(1, n_cols, self._row, 0) window = self.term.stdscr.derwin(1, n_cols, self._row, 0)
window.erase() window.erase()
# curses.bkgd expects bytes in py2 and unicode in py3 # curses.bkgd expects bytes in py2 and unicode in py3
ch, attr = str(' '), curses.A_REVERSE | curses.A_BOLD | Color.CYAN window.bkgd(str(' '), self.term.attr('title_bar'))
window.bkgd(ch, attr)
sub_name = self.content.name sub_name = self.content.name
sub_name = sub_name.replace('/r/front', 'Front Page') sub_name = sub_name.replace('/r/front', 'Front Page')
@@ -421,7 +414,7 @@ class Page(object):
sys.stdout.write(title) sys.stdout.write(title)
sys.stdout.flush() sys.stdout.flush()
if self.reddit.user is not None: if self.reddit and self.reddit.user is not None:
# The starting position of the name depends on if we're converting # The starting position of the name depends on if we're converting
# to ascii or not # to ascii or not
width = len if self.config['ascii'] else textual_width width = len if self.config['ascii'] else textual_width
@@ -442,8 +435,7 @@ class Page(object):
n_rows, n_cols = self.term.stdscr.getmaxyx() n_rows, n_cols = self.term.stdscr.getmaxyx()
window = self.term.stdscr.derwin(1, n_cols, self._row, 0) window = self.term.stdscr.derwin(1, n_cols, self._row, 0)
window.erase() window.erase()
ch, attr = str(' '), curses.A_BOLD | Color.YELLOW window.bkgd(str(' '), self.term.attr('order_bar'))
window.bkgd(ch, attr)
banner = docs.BANNER_SEARCH if self.content.query else docs.BANNER banner = docs.BANNER_SEARCH if self.content.query else docs.BANNER
items = banner.strip().split(' ') items = banner.strip().split(' ')
@@ -455,7 +447,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, attr | curses.A_REVERSE) attr = self.term.theme.get('order_bar', modifier='selected')
window.chgat(0, col, 3, attr)
self._row += 1 self._row += 1
@@ -465,8 +458,7 @@ class Page(object):
""" """
n_rows, n_cols = self.term.stdscr.getmaxyx() n_rows, n_cols = self.term.stdscr.getmaxyx()
window = self.term.stdscr.derwin( window = self.term.stdscr.derwin(n_rows - self._row - 1, n_cols, self._row, 0)
n_rows - self._row - 1, n_cols, self._row, 0)
window.erase() window.erase()
win_n_rows, win_n_cols = window.getmaxyx() win_n_rows, win_n_cols = window.getmaxyx()
@@ -493,10 +485,8 @@ class Page(object):
top_item_height = None top_item_height = None
subwin_n_cols = win_n_cols - data['h_offset'] subwin_n_cols = win_n_cols - data['h_offset']
start = current_row - subwin_n_rows + 1 if inverted else current_row start = current_row - subwin_n_rows + 1 if inverted else current_row
subwindow = window.derwin( subwindow = window.derwin(subwin_n_rows, subwin_n_cols, start, data['h_offset'])
subwin_n_rows, subwin_n_cols, start, data['h_offset']) self._subwindows.append((subwindow, data, subwin_inverted))
attr = self._draw_item(subwindow, data, subwin_inverted)
self._subwindows.append((subwindow, attr))
available_rows -= (subwin_n_rows + 1) # Add one for the blank line available_rows -= (subwin_n_rows + 1) # Add one for the blank line
current_row += step * (subwin_n_rows + 1) current_row += step * (subwin_n_rows + 1)
if available_rows <= 0: if available_rows <= 0:
@@ -518,6 +508,25 @@ class Page(object):
self.nav.flip((len(self._subwindows) - 1)) self.nav.flip((len(self._subwindows) - 1))
return self._draw_content() return self._draw_content()
if self.nav.cursor_index >= len(self._subwindows):
# Don't allow the cursor to go over the number of subwindows
# This could happen if the window is resized and the cursor index is
# pushed out of bounds
self.nav.cursor_index = len(self._subwindows) - 1
# Now that the windows are setup, we can take a second pass through
# to draw the content
for index, (win, data, inverted) in enumerate(self._subwindows):
if index == self.nav.cursor_index:
# This lets the theme know to invert the cursor
modifier = 'selected'
else:
modifier = None
win.bkgd(str(' '), self.term.attr('normal'))
with self.term.theme.set_modifier(modifier):
self._draw_item(win, data, inverted)
self._row += win_n_rows self._row += win_n_rows
def _draw_footer(self): def _draw_footer(self):
@@ -525,54 +534,23 @@ class Page(object):
n_rows, n_cols = self.term.stdscr.getmaxyx() n_rows, n_cols = self.term.stdscr.getmaxyx()
window = self.term.stdscr.derwin(1, n_cols, self._row, 0) window = self.term.stdscr.derwin(1, n_cols, self._row, 0)
window.erase() window.erase()
ch, attr = str(' '), curses.A_REVERSE | curses.A_BOLD | Color.CYAN window.bkgd(str(' '), self.term.attr('help_bar'))
window.bkgd(ch, attr)
text = self.FOOTER.strip() text = self.FOOTER.strip()
self.term.add_line(window, text, 0, 0) self.term.add_line(window, text, 0, 0)
self._row += 1 self._row += 1
def _add_cursor(self):
self._edit_cursor(curses.A_REVERSE)
def _remove_cursor(self):
self._edit_cursor(curses.A_NORMAL)
def _move_cursor(self, direction): def _move_cursor(self, direction):
self._remove_cursor()
# Note: ACS_VLINE doesn't like changing the attribute, so disregard the # Note: ACS_VLINE doesn't like changing the attribute, so disregard the
# redraw flag and opt to always redraw # redraw flag and opt to always redraw
valid, redraw = self.nav.move(direction, len(self._subwindows)) valid, redraw = self.nav.move(direction, len(self._subwindows))
if not valid: if not valid:
self.term.flash() self.term.flash()
self._add_cursor()
def _move_page(self, direction): def _move_page(self, direction):
self._remove_cursor()
valid, redraw = self.nav.move_page(direction, len(self._subwindows)-1) valid, redraw = self.nav.move_page(direction, len(self._subwindows)-1)
if not valid: if not valid:
self.term.flash() self.term.flash()
self._add_cursor()
def _edit_cursor(self, attribute):
# Don't allow the cursor to go below page index 0
if self.nav.absolute_index < 0:
return
# Don't allow the cursor to go over the number of subwindows
# This could happen if the window is resized and the cursor index is
# pushed out of bounds
if self.nav.cursor_index >= len(self._subwindows):
self.nav.cursor_index = len(self._subwindows) - 1
window, attr = self._subwindows[self.nav.cursor_index]
if attr is not None:
attribute |= attr
n_rows, _ = window.getmaxyx()
for row in range(n_rows):
window.chgat(row, 0, 1, attribute)
def _prompt_period(self, order): def _prompt_period(self, order):

View File

@@ -3,12 +3,11 @@ 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
from .page import Page, PageController, logged_in from .page import Page, PageController, logged_in
from .objects import Navigator, Color, Command from .objects import Navigator, Command
from .exceptions import TemporaryFileError from .exceptions import TemporaryFileError
@@ -119,7 +118,7 @@ class SubmissionPage(Page):
@SubmissionController.register(Command('SUBMISSION_OPEN_IN_BROWSER')) @SubmissionController.register(Command('SUBMISSION_OPEN_IN_BROWSER'))
def open_link(self): def open_link(self):
""" """
Open the selected item with the webbrowser Open the selected item with the web browser
""" """
data = self.get_selected_item() data = self.get_selected_item()
@@ -207,6 +206,10 @@ class SubmissionPage(Page):
@SubmissionController.register(Command('SUBMISSION_OPEN_IN_URLVIEWER')) @SubmissionController.register(Command('SUBMISSION_OPEN_IN_URLVIEWER'))
def comment_urlview(self): def comment_urlview(self):
"""
Open the selected comment with the URL viewer
"""
data = self.get_selected_item() data = self.get_selected_item()
comment = data.get('body') or data.get('text') or data.get('url_full') comment = data.get('body') or data.get('text') or data.get('url_full')
if comment: if comment:
@@ -285,89 +288,114 @@ 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)')
row = offset row = offset
if row in valid_rows: if row in valid_rows:
attr = curses.A_BOLD
attr |= (Color.BLUE if not data['is_author'] else Color.GREEN)
text = '{author} '.format(**data)
if data['is_author']: if data['is_author']:
text += '[S] ' attr = self.term.attr('comment_author_self')
text = '{author} [S]'.format(**data)
else:
attr = self.term.attr('comment_author')
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 = curses.A_BOLD | Color.YELLOW attr = self.term.attr('user_flair')
self.term.add_line(win, '{flair} '.format(**data), attr=attr) self.term.add_space(win)
self.term.add_line(win, '{flair}'.format(**data), attr=attr)
text, attr = self.term.get_arrow(data['likes']) arrow, attr = self.term.get_arrow(data['likes'])
self.term.add_line(win, text, attr=attr) self.term.add_space(win)
self.term.add_line(win, ' {score} {created} '.format(**data)) self.term.add_line(win, arrow, attr=attr)
attr = self.term.attr('score')
self.term.add_space(win)
self.term.add_line(win, '{score}'.format(**data), attr=attr)
attr = self.term.attr('created')
self.term.add_space(win)
self.term.add_line(win, '{created}'.format(**data), attr=attr)
if data['gold']: if data['gold']:
text, attr = self.term.guilded attr = self.term.attr('gold')
self.term.add_line(win, text, attr=attr) self.term.add_space(win)
self.term.add_line(win, self.term.guilded, attr=attr)
if data['stickied']: if data['stickied']:
text, attr = '[stickied]', Color.GREEN attr = self.term.attr('stickied')
self.term.add_line(win, text, attr=attr) self.term.add_space(win)
self.term.add_line(win, '[stickied]', attr=attr)
if data['saved']: if data['saved']:
text, attr = '[saved]', Color.GREEN attr = self.term.attr('saved')
self.term.add_line(win, text, attr=attr) self.term.add_space(win)
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')
if row in valid_rows: if row in valid_rows:
self.term.add_line(win, text, row, 1) 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.
attr = Color.get_level(data['level']) index = data['level'] % len(self.term.theme.BAR_LEVELS)
x = 0 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, x, self.term.vline, attr) self.term.addch(win, y, 0, self.term.vline, attr)
return attr | self.term.vline
def _draw_more_comments(self, win, data): 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
self.term.add_line(win, '{body}'.format(**data), 0, 1) attr = self.term.attr('hidden_comment_text')
self.term.add_line( self.term.add_line(win, '{body}'.format(**data), 0, 1, attr=attr)
win, ' [{count}]'.format(**data), attr=curses.A_BOLD)
attr = Color.get_level(data['level']) attr = self.term.attr('hidden_comment_expand')
self.term.add_space(win)
self.term.add_line(win, '[{count}]'.format(**data), attr=attr)
index = data['level'] % len(self.term.theme.BAR_LEVELS)
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)
return attr | self.term.vline
def _draw_submission(self, win, data): 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')
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, curses.A_BOLD) self.term.add_line(win, text, row, 1, attr)
row = len(data['split_title']) + 1 row = len(data['split_title']) + 1
attr = curses.A_BOLD | Color.GREEN 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)
attr = curses.A_BOLD | Color.YELLOW
if data['flair']: if data['flair']:
self.term.add_line(win, ' {flair}'.format(**data), attr=attr) attr = self.term.attr('submission_flair')
self.term.add_line(win, ' {created} {subreddit}'.format(**data)) self.term.add_space(win)
self.term.add_line(win, '{flair}'.format(**data), attr=attr)
attr = self.term.attr('created')
self.term.add_space(win)
self.term.add_line(win, '{created}'.format(**data), attr=attr)
attr = self.term.attr('submission_subreddit')
self.term.add_space(win)
self.term.add_line(win, '/r/{subreddit}'.format(**data), attr=attr)
row = len(data['split_title']) + 2 row = len(data['split_title']) + 2
seen = (data['url_full'] in self.config.history) if data['url_full'] in self.config.history:
link_color = Color.MAGENTA if seen else Color.BLUE attr = self.term.attr('url_seen')
attr = curses.A_UNDERLINE | link_color else:
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
# 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
@@ -377,25 +405,35 @@ 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')
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) 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
self.term.add_line(win, '{score} '.format(**data), row, 1) attr = self.term.attr('score')
text, attr = self.term.get_arrow(data['likes']) self.term.add_line(win, '{score}'.format(**data), row, 1, attr=attr)
self.term.add_line(win, text, attr=attr)
self.term.add_line(win, ' {comments} '.format(**data)) arrow, attr = self.term.get_arrow(data['likes'])
self.term.add_space(win)
self.term.add_line(win, arrow, attr=attr)
attr = self.term.attr('comment_count')
self.term.add_space(win)
self.term.add_line(win, '{comments}'.format(**data), attr=attr)
if data['gold']: if data['gold']:
text, attr = self.term.guilded attr = self.term.attr('gold')
self.term.add_line(win, text, attr=attr) self.term.add_space(win)
self.term.add_line(win, self.term.guilded, attr=attr)
if data['nsfw']: if data['nsfw']:
text, attr = 'NSFW', (curses.A_BOLD | Color.RED) attr = self.term.attr('nsfw')
self.term.add_line(win, text, attr=attr) self.term.add_space(win)
self.term.add_line(win, 'NSFW', attr=attr)
if data['saved']: if data['saved']:
text, attr = '[saved]', Color.GREEN attr = self.term.attr('saved')
self.term.add_line(win, text, attr=attr) self.term.add_space(win)
self.term.add_line(win, '[saved]', attr=attr)
win.border() win.border()

View File

@@ -3,12 +3,11 @@ from __future__ import unicode_literals
import re import re
import time import time
import curses
from . import docs from . import docs
from .content import SubredditContent from .content import SubredditContent
from .page import Page, PageController, logged_in from .page import Page, PageController, logged_in
from .objects import Navigator, Color, Command from .objects import Navigator, Command
from .submission_page import SubmissionPage from .submission_page import SubmissionPage
from .subscription_page import SubscriptionPage from .subscription_page import SubscriptionPage
from .exceptions import TemporaryFileError from .exceptions import TemporaryFileError
@@ -265,50 +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')
if row in valid_rows: if row in valid_rows:
self.term.add_line(win, text, row, 1, curses.A_BOLD) 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:
seen = (data['url_full'] in self.config.history) if data['url_full'] in self.config.history:
link_color = Color.MAGENTA if seen else Color.BLUE attr = self.term.attr('url_seen')
attr = curses.A_UNDERLINE | link_color else:
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:
self.term.add_line(win, '{score} '.format(**data), row, 1)
text, attr = self.term.get_arrow(data['likes']) attr = self.term.attr('score')
self.term.add_line(win, text, attr=attr) self.term.add_line(win, '{score}'.format(**data), row, 1, attr)
self.term.add_line(win, ' {created} '.format(**data)) self.term.add_space(win)
arrow, attr = self.term.get_arrow(data['likes'])
self.term.add_line(win, arrow, attr=attr)
self.term.add_space(win)
attr = self.term.attr('created')
self.term.add_line(win, '{created}'.format(**data), attr=attr)
if data['comments'] is not None: if data['comments'] is not None:
text, attr = '-', curses.A_BOLD attr = self.term.attr('separator')
self.term.add_line(win, text, attr=attr) self.term.add_space(win)
self.term.add_line(win, ' {comments} '.format(**data)) self.term.add_line(win, '-', attr=attr)
attr = self.term.attr('comment_count')
self.term.add_space(win)
self.term.add_line(win, '{comments}'.format(**data), attr=attr)
if data['saved']: if data['saved']:
text, attr = '[saved]', Color.GREEN attr = self.term.attr('saved')
self.term.add_line(win, text, attr=attr) self.term.add_space(win)
self.term.add_line(win, '[saved]', attr=attr)
if data['stickied']: if data['stickied']:
text, attr = '[stickied]', Color.GREEN attr = self.term.attr('stickied')
self.term.add_line(win, text, attr=attr) self.term.add_space(win)
self.term.add_line(win, '[stickied]', attr=attr)
if data['gold']: if data['gold']:
text, attr = self.term.guilded attr = self.term.attr('gold')
self.term.add_line(win, text, attr=attr) self.term.add_space(win)
self.term.add_line(win, self.term.guilded, attr=attr)
if data['nsfw']: if data['nsfw']:
text, attr = 'NSFW', (curses.A_BOLD | Color.RED) attr = self.term.attr('nsfw')
self.term.add_line(win, text, attr=attr) self.term.add_space(win)
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:
text = '{author}'.format(**data) attr = self.term.attr('submission_author')
self.term.add_line(win, text, row, 1, Color.GREEN) self.term.add_line(win, '{author}'.format(**data), row, 1, attr)
text = ' /r/{subreddit}'.format(**data) self.term.add_space(win)
self.term.add_line(win, text, attr=Color.YELLOW)
attr = self.term.attr('submission_subreddit')
self.term.add_line(win, '/r/{subreddit}'.format(**data), attr=attr)
if data['flair']: if data['flair']:
text = ' {flair}'.format(**data) attr = self.term.attr('submission_flair')
self.term.add_line(win, text, attr=Color.RED) self.term.add_space(win)
self.term.add_line(win, '{flair}'.format(**data), attr=attr)
attr = self.term.attr('cursor')
for y in range(n_rows):
self.term.addch(win, y, 0, str(' '), attr)

View File

@@ -1,12 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
import curses
from . import docs from . import docs
from .page import Page, PageController from .page import Page, PageController
from .content import SubscriptionContent, SubredditContent from .content import SubscriptionContent, SubredditContent
from .objects import Color, Navigator, Command from .objects import Navigator, Command
class SubscriptionController(PageController): class SubscriptionController(PageController):
@@ -95,10 +93,21 @@ class SubscriptionPage(Page):
row = offset row = offset
if row in valid_rows: if row in valid_rows:
attr = curses.A_BOLD | Color.YELLOW if data['type'] == 'Multireddit':
attr = self.term.attr('multireddit_name')
else:
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:
self.term.add_line(win, text, row, 1) if data['type'] == 'Multireddit':
attr = self.term.attr('multireddit_text')
else:
attr = self.term.attr('subscription_text')
self.term.add_line(win, text, row, 1, attr)
attr = self.term.attr('cursor')
for y in range(n_rows):
self.term.addch(win, y, 0, str(' '), attr)

View File

@@ -20,9 +20,9 @@ from tempfile import NamedTemporaryFile
import six import six
from kitchen.text.display import textual_width_chop from kitchen.text.display import textual_width_chop
from . import exceptions from . import exceptions, mime_parsers
from . import mime_parsers from .theme import Theme
from .objects import LoadScreen, Color from .objects import LoadScreen
try: try:
# Fix only needed for versions prior to python 3.6 # Fix only needed for versions prior to python 3.6
@@ -46,16 +46,20 @@ class Terminal(object):
MIN_HEIGHT = 10 MIN_HEIGHT = 10
MIN_WIDTH = 20 MIN_WIDTH = 20
# ASCII code # ASCII codes
ESCAPE = 27 ESCAPE = 27
RETURN = 10 RETURN = 10
SPACE = 32 SPACE = 32
def __init__(self, stdscr, config): def __init__(self, stdscr, config, theme=None):
self.stdscr = stdscr self.stdscr = stdscr
self.config = config self.config = config
self.loader = LoadScreen(self) self.loader = LoadScreen(self)
self.theme = None
self.set_theme(theme)
self._display = None self._display = None
self._mailcap_dict = mailcap.getcaps() self._mailcap_dict = mailcap.getcaps()
self._term = os.environ.get('TERM') self._term = os.environ.get('TERM')
@@ -66,27 +70,19 @@ class Terminal(object):
@property @property
def up_arrow(self): def up_arrow(self):
symbol = '^' if self.config['ascii'] else '' return '^' if self.config['ascii'] else ''
attr = curses.A_BOLD | Color.GREEN
return symbol, attr
@property @property
def down_arrow(self): def down_arrow(self):
symbol = 'v' if self.config['ascii'] else '' return 'v' if self.config['ascii'] else ''
attr = curses.A_BOLD | Color.RED
return symbol, attr
@property @property
def neutral_arrow(self): def neutral_arrow(self):
symbol = 'o' if self.config['ascii'] else '' return 'o' if self.config['ascii'] else ''
attr = curses.A_BOLD
return symbol, attr
@property @property
def guilded(self): def guilded(self):
symbol = '*' if self.config['ascii'] else '' return '*' if self.config['ascii'] else ''
attr = curses.A_BOLD | Color.YELLOW
return symbol, attr
@property @property
def vline(self): def vline(self):
@@ -197,11 +193,11 @@ class Terminal(object):
""" """
if likes is None: if likes is None:
return self.neutral_arrow return self.neutral_arrow, self.attr('neutral_vote')
elif likes: elif likes:
return self.up_arrow return self.up_arrow, self.attr('upvote')
else: else:
return self.down_arrow return self.down_arrow, self.attr('downvote')
def clean(self, string, n_cols=None): def clean(self, string, n_cols=None):
""" """
@@ -278,7 +274,21 @@ class Terminal(object):
params = [] if attr is None else [attr] params = [] if attr is None else [attr]
window.addstr(row, col, text, *params) window.addstr(row, col, text, *params)
def show_notification(self, message, timeout=None): @staticmethod
def add_space(window):
"""
Shortcut for adding a single space to a window at the current position
"""
row, col = window.getyx()
_, max_cols = window.getmaxyx()
if max_cols - col - 1 <= 0:
# Trying to draw outside of the screen bounds
return
window.addstr(row, col, ' ')
def show_notification(self, message, timeout=None, style='info'):
""" """
Overlay a message box on the center of the screen and wait for input. Overlay a message box on the center of the screen and wait for input.
@@ -286,12 +296,17 @@ class Terminal(object):
message (list or string): List of strings, one per line. message (list or string): List of strings, one per line.
timeout (float): Optional, maximum length of time that the message timeout (float): Optional, maximum length of time that the message
will be shown before disappearing. will be shown before disappearing.
style (str): The theme element that will be applied to the
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()
n_rows, n_cols = self.stdscr.getmaxyx() n_rows, n_cols = self.stdscr.getmaxyx()
v_offset, h_offset = self.stdscr.getbegyx()
box_width = max(len(m) for m in message) + 2 box_width = max(len(m) for m in message) + 2
box_height = len(message) + 2 box_height = len(message) + 2
@@ -301,10 +316,11 @@ class Terminal(object):
box_height = min(box_height, n_rows) box_height = min(box_height, n_rows)
message = message[:box_height-2] message = message[:box_height-2]
s_row = (n_rows - box_height) // 2 s_row = (n_rows - box_height) // 2 + v_offset
s_col = (n_cols - box_width) // 2 s_col = (n_cols - box_width) // 2 + h_offset
window = curses.newwin(box_height, box_width, s_row, s_col) window = curses.newwin(box_height, box_width, s_row, s_col)
window.bkgd(str(' '), self.attr('notice_{0}'.format(style)))
window.erase() window.erase()
window.border() window.border()
@@ -382,7 +398,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
@@ -692,18 +708,22 @@ class Terminal(object):
""" """
n_rows, n_cols = self.stdscr.getmaxyx() n_rows, n_cols = self.stdscr.getmaxyx()
ch, attr = str(' '), curses.A_BOLD | curses.A_REVERSE | Color.CYAN v_offset, h_offset = self.stdscr.getbegyx()
ch, attr = str(' '), self.attr('prompt')
prompt = self.clean(prompt, n_cols-1) prompt = self.clean(prompt, n_cols-1)
# Create a new window to draw the text at the bottom of the screen, # Create a new window to draw the text at the bottom of the screen,
# so we can erase it when we're done. # so we can erase it when we're done.
prompt_win = curses.newwin(1, len(prompt)+1, n_rows-1, 0) s_row = v_offset + n_rows - 1
s_col = h_offset
prompt_win = curses.newwin(1, len(prompt) + 1, s_row, s_col)
prompt_win.bkgd(ch, attr) prompt_win.bkgd(ch, attr)
self.add_line(prompt_win, prompt) self.add_line(prompt_win, prompt)
prompt_win.refresh() prompt_win.refresh()
# Create a separate window for text input # Create a separate window for text input
input_win = curses.newwin(1, n_cols-len(prompt), n_rows-1, len(prompt)) s_col = h_offset + len(prompt)
input_win = curses.newwin(1, n_cols - len(prompt), s_row, s_col)
input_win.bkgd(ch, attr) input_win.bkgd(ch, attr)
input_win.refresh() input_win.refresh()
@@ -802,3 +822,34 @@ class Terminal(object):
self.stdscr.touchwin() self.stdscr.touchwin()
else: else:
self.stdscr.clearok(True) self.stdscr.clearok(True)
def attr(self, element):
"""
Shortcut for fetching the color + attribute code for an element.
"""
return self.theme.get(element)
def set_theme(self, theme=None):
"""
Set the terminal theme. This is a stub for what will eventually
support managing custom themes.
Check that the terminal supports the provided theme, and applies
the theme to the terminal if possible.
If the terminal doesn't support the theme, this falls back to the
default theme. The default theme only requires 8 colors so it
should be compatible with any terminal that supports basic colors.
"""
monochrome = (not curses.has_colors())
if theme is None:
theme = Theme(monochrome=monochrome)
theme.bind_curses()
# Apply the default color to the whole screen
self.stdscr.bkgd(str(' '), theme.get('normal'))
self.theme = theme

113
rtv/theme.py Normal file
View File

@@ -0,0 +1,113 @@
"""
This file is a stub that contains the default RTV theme.
This will eventually be expanded to support loading/managing custom themes.
"""
import curses
from contextlib import contextmanager
DEFAULT_THEME = {
'normal': (-1, -1, curses.A_NORMAL),
'bar_level_1': (curses.COLOR_MAGENTA, -1, curses.A_NORMAL),
'bar_level_1.selected': (curses.COLOR_MAGENTA, -1, curses.A_REVERSE),
'bar_level_2': (curses.COLOR_CYAN, -1, curses.A_NORMAL),
'bar_level_2.selected': (curses.COLOR_CYAN, -1, curses.A_REVERSE),
'bar_level_3': (curses.COLOR_GREEN, -1, curses.A_NORMAL),
'bar_level_3.selected': (curses.COLOR_GREEN, -1, curses.A_REVERSE),
'bar_level_4': (curses.COLOR_YELLOW, -1, curses.A_NORMAL),
'bar_level_4.selected': (curses.COLOR_YELLOW, -1, curses.A_REVERSE),
'comment_author': (curses.COLOR_BLUE, -1, curses.A_BOLD),
'comment_author_self': (curses.COLOR_GREEN, -1, curses.A_BOLD),
'comment_count': (-1, -1, curses.A_NORMAL),
'comment_text': (-1, -1, curses.A_NORMAL),
'created': (-1, -1, curses.A_NORMAL),
'cursor': (-1, -1, curses.A_NORMAL),
'cursor.selected': (-1, -1, curses.A_REVERSE),
'downvote': (curses.COLOR_RED, -1, curses.A_BOLD),
'gold': (curses.COLOR_YELLOW, -1, curses.A_BOLD),
'help_bar': (curses.COLOR_CYAN, -1, curses.A_BOLD | curses.A_REVERSE),
'hidden_comment_expand': (-1, -1, curses.A_BOLD),
'hidden_comment_text': (-1, -1, curses.A_NORMAL),
'multireddit_name': (curses.COLOR_YELLOW, -1, curses.A_BOLD),
'multireddit_text': (-1, -1, curses.A_NORMAL),
'neutral_vote': (-1, -1, curses.A_BOLD),
'notice_info': (-1, -1, curses.A_NORMAL),
'notice_loading': (-1, -1, curses.A_NORMAL),
'notice_error': (-1, -1, curses.A_NORMAL),
'notice_success': (-1, -1, curses.A_NORMAL),
'nsfw': (curses.COLOR_RED, -1, curses.A_BOLD | curses.A_REVERSE),
'order_bar': (curses.COLOR_YELLOW, -1, curses.A_BOLD),
'order_bar.selected': (curses.COLOR_YELLOW, -1, curses.A_BOLD | curses.A_REVERSE),
'prompt': (curses.COLOR_CYAN, -1, curses.A_BOLD | curses.A_REVERSE),
'saved': (curses.COLOR_GREEN, -1, curses.A_NORMAL),
'score': (-1, -1, curses.A_NORMAL),
'separator': (-1, -1, curses.A_BOLD),
'stickied': (curses.COLOR_GREEN, -1, curses.A_NORMAL),
'subscription_name': (curses.COLOR_YELLOW, -1, curses.A_BOLD),
'subscription_text': (-1, -1, curses.A_NORMAL),
'submission_author': (curses.COLOR_GREEN, -1, curses.A_NORMAL),
'submission_flair': (curses.COLOR_RED, -1, curses.A_NORMAL),
'submission_subreddit': (curses.COLOR_YELLOW, -1, curses.A_NORMAL),
'submission_text': (-1, -1, curses.A_NORMAL),
'submission_title': (-1, -1, curses.A_BOLD),
'title_bar': (curses.COLOR_CYAN, -1, curses.A_BOLD | curses.A_REVERSE),
'upvote': (curses.COLOR_GREEN, -1, curses.A_BOLD),
'url': (curses.COLOR_BLUE, -1, curses.A_UNDERLINE),
'url_seen': (curses.COLOR_MAGENTA, -1, curses.A_UNDERLINE),
'user_flair': (curses.COLOR_YELLOW, -1, curses.A_BOLD)
}
class Theme(object):
BAR_LEVELS = ['bar_level_1', 'bar_level_2', 'bar_level_3', 'bar_level_4']
def __init__(self, monochrome=True):
self.monochrome = monochrome
self._modifier = None
self._elements = {}
self._color_pairs = {}
def bind_curses(self):
if self.monochrome:
# Skip initializing the colors and just use the attributes
self._elements = {key: val[2] for key, val in DEFAULT_THEME.items()}
return
# Shortcut for the default fg/bg
self._color_pairs[(-1, -1)] = curses.A_NORMAL
for key, (fg, bg, attr) in DEFAULT_THEME.items():
# Register the color pair for the element
if (fg, bg) not in self._color_pairs:
index = len(self._color_pairs) + 1
curses.init_pair(index, fg, bg)
self._color_pairs[(fg, bg)] = curses.color_pair(index)
self._elements[key] = self._color_pairs[(fg, bg)] | attr
def get(self, element, modifier=None):
modifier = modifier or self._modifier
if modifier:
modified_element = '{0}.{1}'.format(element, modifier)
if modified_element in self._elements:
return self._elements[modified_element]
return self._elements[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

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 # TODO: Add a new test for this
# assert not window.subwin.chgat.called
# 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

@@ -20,14 +20,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, \