Refactoring the monochrome stuff

This commit is contained in:
Michael Lazar
2017-09-11 00:30:18 -04:00
parent a208a6764a
commit 9af4b8c709
11 changed files with 110 additions and 185 deletions

View File

@@ -17,6 +17,8 @@ Basic Commands
:``q``/``Q``: Quit/Force quit
:``y``: Copy submission permalink to clipboard
:``Y``: Copy submission link to clipboard
:``F2``: Cycle to the previous color theme
:``F3``: Cycle to the next color theme
----------------------
Authenticated Commands

View File

@@ -174,12 +174,15 @@ def main():
try:
with curses_session() as stdscr:
if config['theme']:
theme = Theme.from_name(config['theme'], config['monochrome'])
else:
theme = Theme(monochrome=config['monochrome'])
term = Terminal(stdscr, config)
if config['theme']:
theme = Theme.from_name(config['theme'])
else:
theme = None
term.set_theme(theme, monochrome=config['monochrome'])
term = Terminal(stdscr, config, theme)
with term.loader('Initializing', catch_exception=False):
reddit = praw.Reddit(user_agent=user_agent,

View File

@@ -60,6 +60,8 @@ https://github.com/michael-lazar/rtv
b : Display urls with urlview
y : Copy submission permalink to clipboard
Y : Copy submission link to clipboard
F2 : Cycle to previous theme
F3 : Cycle to next theme
[Prompt]
The `/` prompt accepts subreddits in the following formats
@@ -87,7 +89,6 @@ BANNER_SEARCH = """
[1]relevance [2]top [3]comments [4]new
"""
FOOTER_SUBREDDIT = """
[?]Help [q]Quit [l]Comments [/]Prompt [u]Login [o]Open [c]Post [a/z]Vote
"""

View File

@@ -11,6 +11,7 @@ import six
from kitchen.text.display import textual_width
from . import docs
from .theme import Theme
from .objects import Controller, Command
from .clipboard import copy
from .exceptions import TemporaryFileError, ProgramError
@@ -54,7 +55,6 @@ class Page(object):
self.active = True
self._row = 0
self._subwindows = None
self._theme_list = None
def refresh_content(self, order=None, name=None):
raise NotImplementedError
@@ -91,23 +91,19 @@ class Page(object):
def force_exit(self):
sys.exit()
@PageController.register(Command('PREVIOUS_THEME'))
def previous_theme(self):
theme = Theme()
self.term.set_theme(theme)
self.draw()
self.term.show_notification(theme.name, timeout=1)
@PageController.register(Command('NEXT_THEME'))
def next_theme(self):
if self._theme_list is None:
self._theme_list = self.term.theme.list_themes()['default']
names = sorted(self._theme_list.keys())
if self.term.theme.name in self._theme_list:
index = names.index(self.term.theme.name) + 1
if index >= len(names):
index = 0
else:
index = 0
new_theme = self._theme_list[names[index]]
self.term.set_theme(new_theme)
theme = Theme()
self.term.set_theme(theme)
self.draw()
self.term.show_notification(new_theme.name, timeout=1)
self.term.show_notification(theme.name, timeout=1)
@PageController.register(Command('HELP'))
def show_help(self):
@@ -466,7 +462,7 @@ class Page(object):
if self.content.order is not None:
order = self.content.order.split('-')[0]
col = text.find(order) - 3
attr = self.term.theme.get('order_bar', modifier='selected')
attr = self.term.theme.get('order_bar', modifier='highlight')
window.chgat(0, col, 3, attr)
self._row += 1
@@ -537,13 +533,13 @@ class Page(object):
# 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'
win.bkgd(str(' '), self.term.attr('@highlight'))
# This lets the theme know to invert the cursor color and
# apply any other special highlighting effects to the window
with self.term.theme.set_modifier('highlight'):
self._draw_item(win, data, inverted)
else:
modifier = None
with self.term.theme.set_modifier(modifier):
win.bkgd(str(' '), self.term.attr('normal'))
win.bkgd(str(' '), self.term.attr('@normal'))
self._draw_item(win, data, inverted)
self._row += win_n_rows

View File

@@ -43,6 +43,11 @@ max_comment_cols = 120
; Hide username if logged in, display "Logged in" instead
hide_username = False
; Color theme, use "rtv --list-themes" to view a list of valid options.
; This can be an absolute filepath, or the name of a theme file that has
; been installed into either the custom of default theme paths.
;theme = monokai
################
# OAuth Settings
################
@@ -110,6 +115,7 @@ SORT_NEW = 4
SORT_CONTROVERSIAL = 5
MOVE_UP = k, <KEY_UP>
MOVE_DOWN = j, <KEY_DOWN>
PREVIOUS_THEME = <KEY_F2>
NEXT_THEME = <KEY_F3>
PAGE_UP = m, <KEY_PPAGE>, <NAK>
PAGE_DOWN = n, <KEY_NPAGE>, <EOT>

View File

@@ -51,14 +51,12 @@ class Terminal(object):
RETURN = 10
SPACE = 32
def __init__(self, stdscr, config, theme=None):
def __init__(self, stdscr, config):
self.stdscr = stdscr
self.config = config
self.loader = LoadScreen(self)
self.theme = None
self.set_theme(theme)
self.theme = None # Initialized by term.set_theme()
self._display = None
self._mailcap_dict = mailcap.getcaps()
@@ -831,7 +829,7 @@ class Terminal(object):
return self.theme.get(element)
def set_theme(self, theme=None):
def set_theme(self, theme=None, monochrome=False):
"""
Check that the terminal supports the provided theme, and applies
the theme to the terminal if possible.
@@ -839,11 +837,25 @@ class Terminal(object):
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.
Using ``monochrome=True`` will force loading the current theme
without any color support. The intention is that this be used as
a fallback for the default theme to support the old --monochrome
command line flag.
"""
monochrome = (not curses.has_colors())
if not monochrome and curses.has_colors():
terminal_colors = curses.COLORS
else:
terminal_colors = 0
if theme is None:
theme = Theme(monochrome=monochrome)
theme = Theme()
elif monochrome:
# No need to display a warning message if the user has
# explicitly turned off support for colors
pass
elif theme.required_color_pairs > curses.COLOR_PAIRS:
_logger.warning(
@@ -851,19 +863,19 @@ class Terminal(object):
'supports %s color pairs, switching to default theme',
theme.name, theme.required_color_pairs, self._term,
curses.COLOR_PAIRS)
theme = Theme(monochrome=monochrome)
theme = Theme()
elif theme.required_colors > curses.COLORS:
elif theme.required_colors > terminal_colors:
_logger.warning(
'Theme %s requires %s colors, but TERM %s only '
'supports %s colors, switching to default theme',
theme.name, theme.required_colors, self._term,
curses.COLORS)
theme = Theme(monochrome=monochrome)
theme = Theme()
theme.bind_curses()
theme.bind_curses(use_color=bool(terminal_colors))
# Apply the default color to the whole screen
self.stdscr.bkgd(str(' '), theme.get('@normal'))
self.theme = theme
self.theme = theme

View File

@@ -15,7 +15,8 @@ class Theme(object):
ATTRIBUTE_CODES = {
'-': None,
'': curses.A_NORMAL,
'': None,
'normal': curses.A_NORMAL,
'bold': curses.A_BOLD,
'reverse': curses.A_REVERSE,
'underline': curses.A_UNDERLINE,
@@ -48,10 +49,11 @@ class Theme(object):
COLOR_CODES['ansi_{0}'.format(i)] = i
# For compatibility with as many terminals as possible, the default theme
# can only use the 8 basic colors with the default background.
# can only use the 8 basic colors with the default color as the background
DEFAULT_THEME = {
'@normal': (-1, -1, curses.A_NORMAL),
'@highlight': (-1, -1, curses.A_NORMAL),
'bar_level_1': (curses.COLOR_MAGENTA, None, curses.A_NORMAL),
'bar_level_1.highlight': (curses.COLOR_MAGENTA, None, curses.A_REVERSE),
'bar_level_2': (curses.COLOR_CYAN, None, curses.A_NORMAL),
@@ -103,18 +105,15 @@ class Theme(object):
BAR_LEVELS = ['bar_level_1', 'bar_level_2', 'bar_level_3', 'bar_level_4']
def __init__(self, name='default', elements=None, monochrome=False):
def __init__(self, name='default', elements=None):
"""
Params:
name (str): A unique string that describes the theme
elements (dict): The theme's element map, should be in the same
format as Theme.DEFAULT_THEME.
monochrome (bool): If true, force all color pairs to use the
terminal's default foreground/background color.
"""
self.name = name
self.monochrome = monochrome
self._color_pair_map = None
self._attribute_map = None
self._modifier = None
@@ -125,19 +124,24 @@ class Theme(object):
if elements is None:
elements = self.DEFAULT_THEME.copy()
# Fill in missing elements
# Fill in any keywords that are defined in the default theme but were
# not passed into the elements dictionary.
for key in self.DEFAULT_THEME.keys():
# Set undefined modifiers to the system default
# The "@normal"/"@highlight" are special elements that act as
# fallbacks for all of the other elements. They must always be
# defined and can't have the colors/attribute empty by setting
# them to "-" or None.
if key.startswith('@'):
if key not in elements:
elements[key] = self.DEFAULT_THEME[key]
continue
# Modifiers are handled below
if key.endswith('.highlight'):
continue
# Set undefined elements to bubble up to the modifier
# Set undefined elements to fallback to the default color
if key not in elements:
elements[key] = (None, None, None)
@@ -146,7 +150,9 @@ class Theme(object):
if modifier_key not in elements:
elements[modifier_key] = elements[key]
# Replace ``None`` attributes with their default modifiers
# At this point all of the possible keys should exist in the element map.
# Now we can "bubble up" the undefined attributes to copy the default
# of the @normal and @highlight modifiers.
for key, val in elements.items():
if key.endswith('.highlight'):
default = elements['@highlight']
@@ -160,33 +166,35 @@ class Theme(object):
self.elements = elements
if not self.monochrome:
colors, color_pairs = set(), set()
for fg, bg, _ in self.elements.values():
colors.add(fg)
colors.add(bg)
color_pairs.add((fg, bg))
# Pre-calculate how many colors / color pairs the theme will need
colors, color_pairs = set(), set()
for fg, bg, _ in self.elements.values():
colors.add(fg)
colors.add(bg)
color_pairs.add((fg, bg))
# Don't count the default fg/bg as a color pair
color_pairs.discard((-1, -1))
self.required_color_pairs = len(color_pairs)
# Don't count the default (-1, -1) as a color pair because it doesn't
# need to be initialized by curses.init_pair().
color_pairs.discard((-1, -1))
self.required_color_pairs = len(color_pairs)
# Determine which color set the terminal needs to
# support in order to be able to use the theme
self.required_colors = None
for marker in [0, 8, 16, 256]:
if max(colors) < marker:
self.required_colors = marker
break
# Determine how many colors the terminal needs to support in order to
# be able to use the theme. This uses the common breakpoints that 99%
# of terminals follow and doesn't take into account 88 color themes.
self.required_colors = None
for marker in [0, 8, 16, 256]:
if max(colors) < marker:
self.required_colors = marker
break
def bind_curses(self):
def bind_curses(self, use_color=True):
"""
Bind the theme's colors to curses's internal color pair map.
This method must be called once (after curses has been initialized)
before any element attributes can be accessed. Color codes and other
special attributes will be mixed bitwise into a single value that
can be understood by curses.
can be passed into curses draw functions.
"""
self._color_pair_map = {}
self._attribute_map = {}
@@ -195,7 +203,7 @@ class Theme(object):
fg, bg, attrs = item
color_pair = (fg, bg)
if not self.monochrome and color_pair != (-1, -1):
if use_color and color_pair != (-1, -1):
# Curses limits the number of available color pairs, so we
# need to reuse them if there are multiple elements with the
# same foreground and background.
@@ -211,16 +219,17 @@ class Theme(object):
def get(self, element, modifier=None):
"""
Returns the curses attribute code for the given element.
If element is None, return the background code (e.g. @normal).
"""
if self._attribute_map is None:
raise RuntimeError('Attempted to access theme attribute before '
'calling initialize_curses_theme()')
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]
if modifier and not element.startswith('@'):
element = element + '.' + modifier
return self._attribute_map[element]
@@ -298,7 +307,7 @@ class Theme(object):
print('')
@classmethod
def from_name(cls, name, monochrome=False, path=THEMES):
def from_name(cls, name, path=THEMES):
"""
Search for the given theme on the filesystem and attempt to load it.
@@ -313,12 +322,12 @@ class Theme(object):
for filename in filenames:
if os.path.isfile(filename):
return cls.from_file(filename, monochrome)
return cls.from_file(filename)
raise ConfigError('Could not find theme named "{0}"'.format(name))
@classmethod
def from_file(cls, filename, monochrome=False):
def from_file(cls, filename):
"""
Load a theme from the specified configuration file.
"""
@@ -347,7 +356,7 @@ class Theme(object):
continue
elements[element] = cls._parse_line(element, line, filename)
return cls(theme_name, elements, monochrome)
return cls(theme_name, elements)
@classmethod
def _parse_line(cls, element, line, filename=None):

View File

@@ -1,54 +0,0 @@
# RTV theme
[theme]
;<element> = <foreground> <background> <attributes>
@normal = default default
@highlight = default default
bar_level_1 = magenta -
bar_level_1.highlight = magenta - reverse
bar_level_2 = cyan -
bar_level_2.highlight = cyan - reverse
bar_level_3 = green -
bar_level_3.highlight = green - reverse
bar_level_4 = yellow -
bar_level_4.highlight = yellow - reverse
comment_author = blue - bold
comment_author_self = green - bold
comment_count = - -
comment_text = - -
created = - -
cursor = - -
cursor.highlight = - - reverse
downvote = red - bold
gold = yellow - bold
help_bar = cyan - bold+reverse
hidden_comment_expand = - - bold
hidden_comment_text = - -
multireddit_name = yellow - bold
multireddit_text = - -
neutral_vote = - - bold
notice_info = - -
notice_loading = - -
notice_error = red -
notice_success = green -
nsfw = red - bold
order_bar = yellow - bold
order_bar.highlight = yellow - bold+reverse
prompt = cyan - bold+reverse
saved = green -
score = - -
separator = - - bold
stickied = green -
subscription_name = yellow - bold
subscription_text = - -
submission_author = green -
submission_flair = red -
submission_subreddit = yellow -
submission_text = - -
submission_title = - - bold
title_bar = cyan - bold+reverse
upvote = green - bold
url = blue - underline
url_seen = magenta - underline
user_flair = yellow - bold

View File

@@ -1,50 +0,0 @@
# RTV theme
[theme]
;<element> = <foreground> <background> <attributes>
@normal = default default
@highlight = default default reverse
bar_level_1 = - -
bar_level_2 = - -
bar_level_3 = - -
bar_level_4 = - -
comment_author = - - bold
comment_author_self = - - bold
comment_count = - -
comment_text = - -
created = - -
cursor = - -
cursor.highlight = - - reverse
downvote = - - bold
gold = - - bold
help_bar = - - bold+reverse
hidden_comment_expand = - - bold
hidden_comment_text = - -
multireddit_name = - - bold
multireddit_text = - -
neutral_vote = - - bold
notice_info = - -
notice_loading = - -
notice_error = - -
notice_success = - -
nsfw = - - bold
order_bar = - - bold
order_bar.highlight = - - bold+reverse
prompt = - - bold+reverse
saved = - -
score = - -
separator = - - bold
stickied = - -
subscription_name = - - bold
subscription_text = - -
submission_author = - -
submission_flair = - - bold
submission_subreddit = - -
submission_text = - -
submission_title = - - bold
title_bar = - - bold+reverse
upvote = - - bold
url = - - underline
url_seen = - - underline
user_flair = - - bold

View File

@@ -19,8 +19,8 @@
[theme]
;<element> = <foreground> <background> <attributes>
@normal = ansi_244 ansi_234
@highlight = ansi_244 ansi_235
@normal = ansi_244 ansi_234 normal
@highlight = ansi_244 ansi_235 normal
bar_level_1 = ansi_125 -
bar_level_1.highlight = ansi_125 - reverse

View File

@@ -19,8 +19,8 @@
[theme]
;<element> = <foreground> <background> <attributes>
@normal = ansi_241 ansi_230
@highlight = ansi_241 ansi_254
@normal = ansi_241 ansi_230 normal
@highlight = ansi_241 ansi_254 normal
bar_level_1 = ansi_125 -
bar_level_1.highlight = ansi_125 - reverse