From 9af4b8c7099afd9f3f872521a5c8794890f6fc3c Mon Sep 17 00:00:00 2001 From: Michael Lazar Date: Mon, 11 Sep 2017 00:30:18 -0400 Subject: [PATCH] Refactoring the monochrome stuff --- CONTROLS.rst | 2 + rtv/__main__.py | 13 ++++-- rtv/docs.py | 3 +- rtv/page.py | 40 ++++++++-------- rtv/templates/rtv.cfg | 6 +++ rtv/terminal.py | 36 ++++++++++----- rtv/theme.py | 83 +++++++++++++++++++--------------- rtv/themes/default.cfg | 54 ---------------------- rtv/themes/monochrome.cfg | 50 -------------------- rtv/themes/solarized-dark.cfg | 4 +- rtv/themes/solarized-light.cfg | 4 +- 11 files changed, 110 insertions(+), 185 deletions(-) delete mode 100644 rtv/themes/default.cfg delete mode 100644 rtv/themes/monochrome.cfg diff --git a/CONTROLS.rst b/CONTROLS.rst index 17c8d89..cbc35dd 100644 --- a/CONTROLS.rst +++ b/CONTROLS.rst @@ -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 diff --git a/rtv/__main__.py b/rtv/__main__.py index 47d87e5..9a9a575 100644 --- a/rtv/__main__.py +++ b/rtv/__main__.py @@ -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, diff --git a/rtv/docs.py b/rtv/docs.py index 9c5ea10..8ca3e23 100644 --- a/rtv/docs.py +++ b/rtv/docs.py @@ -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 """ diff --git a/rtv/page.py b/rtv/page.py index 29ec624..03df37e 100644 --- a/rtv/page.py +++ b/rtv/page.py @@ -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 diff --git a/rtv/templates/rtv.cfg b/rtv/templates/rtv.cfg index f85b75c..d52909e 100644 --- a/rtv/templates/rtv.cfg +++ b/rtv/templates/rtv.cfg @@ -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, MOVE_DOWN = j, +PREVIOUS_THEME = NEXT_THEME = PAGE_UP = m, , PAGE_DOWN = n, , diff --git a/rtv/terminal.py b/rtv/terminal.py index b07b646..dd8641f 100644 --- a/rtv/terminal.py +++ b/rtv/terminal.py @@ -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 \ No newline at end of file + self.theme = theme diff --git a/rtv/theme.py b/rtv/theme.py index 0f74703..f59c6fd 100644 --- a/rtv/theme.py +++ b/rtv/theme.py @@ -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): diff --git a/rtv/themes/default.cfg b/rtv/themes/default.cfg deleted file mode 100644 index c8f5dfa..0000000 --- a/rtv/themes/default.cfg +++ /dev/null @@ -1,54 +0,0 @@ -# RTV theme - -[theme] -; = -@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 diff --git a/rtv/themes/monochrome.cfg b/rtv/themes/monochrome.cfg deleted file mode 100644 index 4b8be66..0000000 --- a/rtv/themes/monochrome.cfg +++ /dev/null @@ -1,50 +0,0 @@ -# RTV theme - -[theme] -; = -@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 diff --git a/rtv/themes/solarized-dark.cfg b/rtv/themes/solarized-dark.cfg index 1922f99..14fb5b0 100644 --- a/rtv/themes/solarized-dark.cfg +++ b/rtv/themes/solarized-dark.cfg @@ -19,8 +19,8 @@ [theme] ; = -@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 diff --git a/rtv/themes/solarized-light.cfg b/rtv/themes/solarized-light.cfg index 25cecf7..2d5833c 100644 --- a/rtv/themes/solarized-light.cfg +++ b/rtv/themes/solarized-light.cfg @@ -19,8 +19,8 @@ [theme] ; = -@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