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
# so we don't need to explicitly shutdown() the server
_logger.exception(e)
self.term.show_notification('Browser Error')
self.term.show_notification('Browser Error', style='error')
else:
self.server.shutdown()
finally:
thread.join()
if self.params['error'] == 'access_denied':
self.term.show_notification('Denied access')
self.term.show_notification('Denied access', style='error')
return
elif self.params['error']:
self.term.show_notification('Authentication error')
self.term.show_notification('Authentication error', style='error')
return
elif self.params['state'] is None:
# Something went wrong but it's not clear what happened
return
elif self.params['state'] != state:
self.term.show_notification('UUID mismatch')
self.term.show_notification('UUID mismatch', style='error')
return
with self.term.loader('Logging in'):

View File

@@ -4,7 +4,6 @@ from __future__ import unicode_literals
import os
import sys
import time
import curses
import logging
from functools import wraps
@@ -60,7 +59,7 @@ class Page(object):
def refresh_content(self, order=None, name=None):
raise NotImplementedError
def _draw_item(self, window, data, inverted, highlight):
def _draw_item(self, window, data, inverted):
raise NotImplementedError
def get_selected_item(self):
@@ -467,7 +466,8 @@ class Page(object):
if self.content.order is not None:
order = self.content.order.split('-')[0]
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
@@ -536,12 +536,15 @@ class Page(object):
# 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):
highlight = (index == self.nav.cursor_index)
if highlight:
win.bkgd(str(' '), self.term.attr('@highlight'))
if index == self.nav.cursor_index:
# This lets the theme know to invert the cursor
modifier = 'selected'
else:
win.bkgd(str(' '), self.term.attr('@normal'))
self._draw_item(win, data, inverted, highlight)
modifier = None
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

View File

@@ -3,7 +3,6 @@ from __future__ import unicode_literals
import re
import time
import curses
from . import docs
from .content import SubmissionContent, SubredditContent
@@ -264,16 +263,18 @@ class SubmissionPage(Page):
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'):
self._draw_more_comments(win, data, highlight)
if data['type'] == 'MoreComments':
return self._draw_more_comments(win, data)
elif data['type'] == 'HiddenComment':
return self._draw_more_comments(win, data)
elif data['type'] == 'Comment':
self._draw_comment(win, data, inverted, highlight)
return self._draw_comment(win, data, inverted)
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_cols -= 1
@@ -287,7 +288,7 @@ class SubmissionPage(Page):
split_body = data['split_body']
if data['n_rows'] > n_rows:
# 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
split_body = split_body[:-cutoff]
split_body.append('(Not enough space to display)')
@@ -295,104 +296,104 @@ class SubmissionPage(Page):
row = offset
if row in valid_rows:
if data['is_author']:
attr = self.term.attr('comment_author_self', highlight)
attr = self.term.attr('comment_author_self')
text = '{author} [S]'.format(**data)
else:
attr = self.term.attr('comment_author', highlight)
attr = self.term.attr('comment_author')
text = '{author}'.format(**data)
self.term.add_line(win, text, row, 1, attr)
if data['flair']:
attr = self.term.attr('user_flair', highlight)
attr = self.term.attr('user_flair')
self.term.add_space(win)
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_line(win, arrow, attr=attr)
attr = self.term.attr('score', highlight)
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', highlight)
attr = self.term.attr('created')
self.term.add_space(win)
self.term.add_line(win, '{created}'.format(**data), attr=attr)
if data['gold']:
attr = self.term.attr('gold', highlight)
attr = self.term.attr('gold')
self.term.add_space(win)
self.term.add_line(win, self.term.guilded, attr=attr)
if data['stickied']:
attr = self.term.attr('stickied', highlight)
attr = self.term.attr('stickied')
self.term.add_space(win)
self.term.add_line(win, '[stickied]', attr=attr)
if data['saved']:
attr = self.term.attr('saved', highlight)
attr = self.term.attr('saved')
self.term.add_space(win)
self.term.add_line(win, '[saved]', attr=attr)
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:
self.term.add_line(win, text, row, 1, attr=attr)
# Unfortunately vline() doesn't support custom color so we have to
# build it one segment at a time.
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):
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_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)
attr = self.term.attr('hidden_comment_expand', highlight)
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], highlight)
attr = self.term.attr(self.term.theme.BAR_LEVELS[index])
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_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):
self.term.add_line(win, text, row, 1, attr)
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)
if data['flair']:
attr = self.term.attr('submission_flair', highlight)
attr = self.term.attr('submission_flair')
self.term.add_space(win)
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_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_line(win, '/r/{subreddit}'.format(**data), attr=attr)
row = len(data['split_title']) + 2
if data['url_full'] in self.config.history:
attr = self.term.attr('url_seen', highlight)
attr = self.term.attr('url_seen')
else:
attr = self.term.attr('url', highlight)
attr = self.term.attr('url')
self.term.add_line(win, '{url}'.format(**data), row, 1, attr)
offset = len(data['split_title']) + 3
@@ -404,34 +405,34 @@ class SubmissionPage(Page):
split_text = split_text[:-cutoff]
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):
self.term.add_line(win, text, row, 1, attr=attr)
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)
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_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_line(win, '{comments}'.format(**data), attr=attr)
if data['gold']:
attr = self.term.attr('gold', highlight)
attr = self.term.attr('gold')
self.term.add_space(win)
self.term.add_line(win, self.term.guilded, attr=attr)
if data['nsfw']:
attr = self.term.attr('nsfw', highlight)
attr = self.term.attr('nsfw')
self.term.add_space(win)
self.term.add_line(win, 'NSFW', attr=attr)
if data['saved']:
attr = self.term.attr('saved', highlight)
attr = self.term.attr('saved')
self.term.add_space(win)
self.term.add_line(win, '[saved]', attr=attr)

View File

@@ -253,7 +253,7 @@ class SubredditPage(Page):
self.content = page.selected_subreddit
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_cols -= 1 # Leave space for the cursor in the first column
@@ -264,75 +264,75 @@ class SubredditPage(Page):
n_title = len(data['split_title'])
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:
self.term.add_line(win, text, row, 1, attr)
row = n_title + offset
if row in valid_rows:
if data['url_full'] in self.config.history:
attr = self.term.attr('url_seen', highlight)
attr = self.term.attr('url_seen')
else:
attr = self.term.attr('url', highlight)
attr = self.term.attr('url')
self.term.add_line(win, '{url}'.format(**data), row, 1, attr)
row = n_title + offset + 1
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_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_space(win)
attr = self.term.attr('created', highlight)
attr = self.term.attr('created')
self.term.add_line(win, '{created}'.format(**data), attr=attr)
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_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_line(win, '{comments}'.format(**data), attr=attr)
if data['saved']:
attr = self.term.attr('saved', highlight)
attr = self.term.attr('saved')
self.term.add_space(win)
self.term.add_line(win, '[saved]', attr=attr)
if data['stickied']:
attr = self.term.attr('stickied', highlight)
attr = self.term.attr('stickied')
self.term.add_space(win)
self.term.add_line(win, '[stickied]', attr=attr)
if data['gold']:
attr = self.term.attr('gold', highlight)
attr = self.term.attr('gold')
self.term.add_space(win)
self.term.add_line(win, self.term.guilded, attr=attr)
if data['nsfw']:
attr = self.term.attr('nsfw', highlight)
attr = self.term.attr('nsfw')
self.term.add_space(win)
self.term.add_line(win, 'NSFW', attr=attr)
row = n_title + offset + 2
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_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)
if data['flair']:
attr = self.term.attr('submission_flair', highlight)
attr = self.term.attr('submission_flair')
self.term.add_space(win)
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):
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
pass
def _draw_item(self, win, data, inverted, highlight):
def _draw_item(self, win, data, inverted):
n_rows, n_cols = win.getmaxyx()
n_cols -= 1 # Leave space for the cursor in the first column
@@ -94,20 +94,20 @@ class SubscriptionPage(Page):
row = offset
if row in valid_rows:
if data['type'] == 'Multireddit':
attr = self.term.attr('multireddit_name', highlight)
attr = self.term.attr('multireddit_name')
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)
row = offset + 1
for row, text in enumerate(data['split_title'], start=row):
if row in valid_rows:
if data['type'] == 'Multireddit':
attr = self.term.attr('multireddit_text', highlight)
attr = self.term.attr('multireddit_text')
else:
attr = self.term.attr('subscription_text', highlight)
attr = self.term.attr('subscription_text')
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):
self.term.addch(win, y, 0, str(' '), attr)

View File

@@ -183,7 +183,7 @@ class Terminal(object):
finally:
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).
However, they rely on using the curses.addch() function, which has been
@@ -193,11 +193,11 @@ class Terminal(object):
"""
if likes is None:
return self.neutral_arrow, self.attr('neutral_vote', highlight)
return self.neutral_arrow, self.attr('neutral_vote')
elif likes:
return self.up_arrow, self.attr('upvote', highlight)
return self.up_arrow, self.attr('upvote')
else:
return self.down_arrow, self.attr('downvote', highlight)
return self.down_arrow, self.attr('downvote')
def clean(self, string, n_cols=None):
"""
@@ -282,7 +282,8 @@ class Terminal(object):
row, col = window.getyx()
_, 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
return
@@ -300,6 +301,8 @@ class Terminal(object):
notification window
"""
assert style in ('info', 'warning', 'error', 'success')
if isinstance(message, six.string_types):
message = message.splitlines()
@@ -396,7 +399,7 @@ class Terminal(object):
_logger.warning(stderr)
self.show_notification(
'Program exited with status={0}\n{1}'.format(
code, stderr.strip()))
code, stderr.strip()), style='error')
else:
# Non-blocking, open a background process
@@ -821,17 +824,18 @@ class Terminal(object):
else:
self.stdscr.clearok(True)
def attr(self, element, highlight=False):
def attr(self, 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):
"""
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.
@@ -862,4 +866,4 @@ class Terminal(object):
# Apply the default color to the whole screen
self.stdscr.bkgd(str(' '), theme.get('@normal'))
self.theme = theme
self.theme = theme

View File

@@ -1,8 +1,9 @@
import os
import codecs
import configparser
import curses
import logging
import os
import configparser
from contextlib import contextmanager
from .config import THEMES, DEFAULT_THEMES
from .exceptions import ConfigError
@@ -116,6 +117,7 @@ class Theme(object):
self.monochrome = monochrome
self._color_pair_map = None
self._attribute_map = None
self._modifier = None
self.required_color_pairs = 0
self.required_colors = 0
@@ -206,7 +208,7 @@ class Theme(object):
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.
"""
@@ -214,10 +216,25 @@ class Theme(object):
raise RuntimeError('Attempted to access theme attribute before '
'calling initialize_curses_theme()')
if highlight:
val = val + '.highlight'
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._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
def list_themes(cls, path=THEMES):

View File

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

View File

@@ -12,7 +12,7 @@ import requests
from six.moves import reload_module
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
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)
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):
# Normal setup and cleanup

View File

@@ -79,15 +79,16 @@ def test_submission_page_construct(reddit, terminal, config, oauth):
# Comment
comment_data = page.content.get(0)
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
comment_data = page.content.get(1)
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
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
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')
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):

View File

@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import curses
import six
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)
# 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
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)
# Cursor - 2 lines
window.subwin.chgat.assert_any_call(0, 0, 1, 262144)
window.subwin.chgat.assert_any_call(1, 0, 1, 262144)
window.subwin.addch.assert_any_call(0, 0, ' ', 262144)
window.subwin.addch.assert_any_call(1, 0, ' ', 262144)
# Reload with a smaller terminal window
terminal.stdscr.ncols = 20

View File

@@ -9,6 +9,7 @@ import codecs
import six
import pytest
from rtv.theme import Theme
from rtv.docs import HELP, COMMENT_EDIT_FILE
from rtv.exceptions import TemporaryFileError, BrowserError
@@ -20,14 +21,10 @@ except ImportError:
def test_terminal_properties(terminal, config):
assert len(terminal.up_arrow) == 2
assert isinstance(terminal.up_arrow[0], six.text_type)
assert len(terminal.down_arrow) == 2
assert isinstance(terminal.down_arrow[0], 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)
assert isinstance(terminal.up_arrow, six.text_type)
assert isinstance(terminal.down_arrow, six.text_type)
assert isinstance(terminal.neutral_arrow, six.text_type)
assert isinstance(terminal.guilded, six.text_type)
terminal._display = None
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_WIDTH is not None
assert terminal.theme is not None
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'
assert terminal.strip_textpad(text) == (
'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)