From d8213f227175f0dba3221f1915e7e5c99d1ef387 Mon Sep 17 00:00:00 2001 From: Michael Lazar Date: Tue, 19 Sep 2017 02:10:37 -0400 Subject: [PATCH] Switching to more vim-inspired theme attributes --- rtv/__main__.py | 2 + rtv/oauth.py | 8 +- rtv/objects.py | 4 +- rtv/page.py | 18 +-- rtv/submission_page.py | 60 ++++---- rtv/subreddit_page.py | 30 ++-- rtv/subscription_page.py | 10 +- rtv/terminal.py | 19 ++- rtv/theme.py | 270 ++++++++++++++++++++------------- rtv/themes/default.cfg | 52 ------- rtv/themes/default.cfg.example | 53 +++++++ rtv/themes/solarized-dark.cfg | 96 ++++++------ rtv/themes/solarized-light.cfg | 96 ++++++------ 13 files changed, 385 insertions(+), 333 deletions(-) delete mode 100644 rtv/themes/default.cfg create mode 100644 rtv/themes/default.cfg.example diff --git a/rtv/__main__.py b/rtv/__main__.py index 5d45c2d..bab62c8 100644 --- a/rtv/__main__.py +++ b/rtv/__main__.py @@ -178,8 +178,10 @@ def main(): term = Terminal(stdscr, config) if config['monochrome']: + _logger.info('Using monochrome theme') theme = Theme(use_color=False) elif config['theme']: + _logger.info('Loading theme: %s', config['theme']) theme = Theme.from_name(config['theme']) else: # Set to None to let the terminal figure out which default diff --git a/rtv/oauth.py b/rtv/oauth.py index 13c5945..ec53811 100644 --- a/rtv/oauth.py +++ b/rtv/oauth.py @@ -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', style='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', style='error') + self.term.show_notification('Denied access', style='Error') return elif self.params['error']: - self.term.show_notification('Authentication error', style='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', style='error') + self.term.show_notification('UUID mismatch', style='Error') return with self.term.loader('Logging in'): diff --git a/rtv/objects.py b/rtv/objects.py index 40274ac..262ccd1 100644 --- a/rtv/objects.py +++ b/rtv/objects.py @@ -231,7 +231,7 @@ class LoadScreen(object): # Some exceptions we want to swallow and display a notification if isinstance(e, e_type): msg = message.format(e) - self._terminal.show_notification(msg, style='error') + self._terminal.show_notification(msg, style='Error') return True def animate(self, delay, interval, message, trail): @@ -260,7 +260,7 @@ class LoadScreen(object): 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.bkgd(str(' '), self._terminal.attr('notice_loading')) + window.bkgd(str(' '), self._terminal.attr('NoticeLoading')) # Animate the loading prompt until the stopping condition is triggered # when the context manager exits. diff --git a/rtv/page.py b/rtv/page.py index 24cee56..ae3553e 100644 --- a/rtv/page.py +++ b/rtv/page.py @@ -367,7 +367,7 @@ class Page(object): window = self.term.stdscr.derwin(1, n_cols, self._row, 0) window.erase() # curses.bkgd expects bytes in py2 and unicode in py3 - window.bkgd(str(' '), self.term.attr('title_bar')) + window.bkgd(str(' '), self.term.attr('PageTitle')) sub_name = self.content.name sub_name = sub_name.replace('/r/front', 'Front Page') @@ -420,7 +420,7 @@ class Page(object): n_rows, n_cols = self.term.stdscr.getmaxyx() window = self.term.stdscr.derwin(1, n_cols, self._row, 0) window.erase() - window.bkgd(str(' '), self.term.attr('order_bar')) + window.bkgd(str(' '), self.term.attr('PageOrder')) banner = docs.BANNER_SEARCH if self.content.query else docs.BANNER items = banner.strip().split(' ') @@ -432,7 +432,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='highlight') + attr = self.term.attr('PageOrderHighlight') window.chgat(0, col, 3, attr) self._row += 1 @@ -499,17 +499,17 @@ class Page(object): # pushed out of bounds self.nav.cursor_index = len(self._subwindows) - 1 + # TODO: Don't highlight the submission box + # 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: - 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'): + win.bkgd(str(' '), self.term.attr('Selected')) + with self.term.theme.turn_on_selected(): self._draw_item(win, data, inverted) else: - 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 @@ -519,7 +519,7 @@ class Page(object): n_rows, n_cols = self.term.stdscr.getmaxyx() window = self.term.stdscr.derwin(1, n_cols, self._row, 0) window.erase() - window.bkgd(str(' '), self.term.attr('help_bar')) + window.bkgd(str(' '), self.term.attr('Help')) text = self.FOOTER.strip() self.term.add_line(window, text, 0, 0) diff --git a/rtv/submission_page.py b/rtv/submission_page.py index ff587d3..fee0dd3 100644 --- a/rtv/submission_page.py +++ b/rtv/submission_page.py @@ -315,15 +315,15 @@ class SubmissionPage(Page): row = offset if row in valid_rows: if data['is_author']: - attr = self.term.attr('comment_author_self') + attr = self.term.attr('CommentAuthorSelf') text = '{author} [S]'.format(**data) else: - attr = self.term.attr('comment_author') + attr = self.term.attr('CommentAuthor') text = '{author}'.format(**data) self.term.add_line(win, text, row, 1, attr) if data['flair']: - attr = self.term.attr('user_flair') + attr = self.term.attr('UserFlair') self.term.add_space(win) self.term.add_line(win, '{flair}'.format(**data), attr=attr) @@ -331,38 +331,38 @@ class SubmissionPage(Page): self.term.add_space(win) self.term.add_line(win, arrow, attr=attr) - attr = self.term.attr('score') + 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') + 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') + 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') + 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') + 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') + attr = self.term.attr('CommentText') 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]) + # curses.vline() doesn't support custom colors so need to build the + # cursor bar on the left of the comment one character at a time + index = data['level'] % len(self.term.theme.CURSOR_BARS) + attr = self.term.attr(self.term.theme.CURSOR_BARS[index]) for y in range(n_rows): self.term.addch(win, y, 0, self.term.vline, attr) @@ -371,15 +371,15 @@ class SubmissionPage(Page): n_rows, n_cols = win.getmaxyx() n_cols -= 1 - attr = self.term.attr('hidden_comment_text') + attr = self.term.attr('HiddenCommentText') self.term.add_line(win, '{body}'.format(**data), 0, 1, attr=attr) - attr = self.term.attr('hidden_comment_expand') + attr = self.term.attr('HiddenCommentExpand') 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]) + index = data['level'] % len(self.term.theme.CURSOR_BARS) + attr = self.term.attr(self.term.theme.CURSOR_BARS[index]) self.term.addch(win, 0, 0, self.term.vline, attr) def _draw_submission(self, win, data): @@ -387,32 +387,32 @@ class SubmissionPage(Page): 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') + attr = self.term.attr('SubmissionTitle') 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') + attr = self.term.attr('SubmissionAuthor') self.term.add_line(win, '{author}'.format(**data), row, 1, attr) if data['flair']: - attr = self.term.attr('submission_flair') + attr = self.term.attr('SubmissionFlair') self.term.add_space(win) self.term.add_line(win, '{flair}'.format(**data), attr=attr) - attr = self.term.attr('created') + 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') + attr = self.term.attr('SubmissionSubreddit') 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') + attr = self.term.attr('LinkSeen') else: - attr = self.term.attr('url') + attr = self.term.attr('Link') self.term.add_line(win, '{url}'.format(**data), row, 1, attr) offset = len(data['split_title']) + 3 @@ -424,34 +424,34 @@ class SubmissionPage(Page): split_text = split_text[:-cutoff] split_text.append('(Not enough space to display)') - attr = self.term.attr('submission_text') + attr = self.term.attr('SubmissionText') 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') + 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']) self.term.add_space(win) self.term.add_line(win, arrow, attr=attr) - attr = self.term.attr('comment_count') + attr = self.term.attr('CommentCount') self.term.add_space(win) self.term.add_line(win, '{comments}'.format(**data), attr=attr) if data['gold']: - attr = self.term.attr('gold') + 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') + 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') + attr = self.term.attr('Saved') self.term.add_space(win) self.term.add_line(win, '[saved]', attr=attr) diff --git a/rtv/subreddit_page.py b/rtv/subreddit_page.py index 7d1463e..1d060d2 100644 --- a/rtv/subreddit_page.py +++ b/rtv/subreddit_page.py @@ -304,22 +304,22 @@ 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') + attr = self.term.attr('SubmissionTitle') 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') + attr = self.term.attr('LinkSeen') else: - attr = self.term.attr('url') + attr = self.term.attr('Link') 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') + attr = self.term.attr('Score') self.term.add_line(win, '{score}'.format(**data), row, 1, attr) self.term.add_space(win) @@ -327,52 +327,52 @@ class SubredditPage(Page): self.term.add_line(win, arrow, attr=attr) self.term.add_space(win) - attr = self.term.attr('created') + 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') + attr = self.term.attr('Separator') self.term.add_space(win) self.term.add_line(win, '-', attr=attr) - attr = self.term.attr('comment_count') + attr = self.term.attr('CommentCount') self.term.add_space(win) self.term.add_line(win, '{comments}'.format(**data), attr=attr) if data['saved']: - attr = self.term.attr('saved') + 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') + 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') + 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') + 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') + attr = self.term.attr('SubmissionAuthor') self.term.add_line(win, '{author}'.format(**data), row, 1, attr) self.term.add_space(win) - attr = self.term.attr('submission_subreddit') + attr = self.term.attr('SubmissionSubreddit') self.term.add_line(win, '/r/{subreddit}'.format(**data), attr=attr) if data['flair']: - attr = self.term.attr('submission_flair') + attr = self.term.attr('SubmissionFlair') self.term.add_space(win) self.term.add_line(win, '{flair}'.format(**data), attr=attr) - attr = self.term.attr('cursor') + attr = self.term.attr('CursorBlock') for y in range(n_rows): self.term.addch(win, y, 0, str(' '), attr) diff --git a/rtv/subscription_page.py b/rtv/subscription_page.py index f196446..5616728 100644 --- a/rtv/subscription_page.py +++ b/rtv/subscription_page.py @@ -93,20 +93,20 @@ class SubscriptionPage(Page): row = offset if row in valid_rows: if data['type'] == 'Multireddit': - attr = self.term.attr('multireddit_name') + attr = self.term.attr('MultiredditName') else: - attr = self.term.attr('subscription_name') + attr = self.term.attr('SubscriptionName') 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') + attr = self.term.attr('MultiredditText') else: - attr = self.term.attr('subscription_text') + attr = self.term.attr('SubscriptionText') self.term.add_line(win, text, row, 1, attr) - attr = self.term.attr('cursor') + attr = self.term.attr('CursorBlock') for y in range(n_rows): self.term.addch(win, y, 0, str(' '), attr) diff --git a/rtv/terminal.py b/rtv/terminal.py index 4dabc94..be672c3 100644 --- a/rtv/terminal.py +++ b/rtv/terminal.py @@ -191,11 +191,11 @@ class Terminal(object): """ if likes is None: - return self.neutral_arrow, self.attr('neutral_vote') + return self.neutral_arrow, self.attr('NeutralVote') elif likes: - return self.up_arrow, self.attr('upvote') + return self.up_arrow, self.attr('Upvote') else: - return self.down_arrow, self.attr('downvote') + return self.down_arrow, self.attr('Downvote') def clean(self, string, n_cols=None): """ @@ -287,7 +287,7 @@ class Terminal(object): window.addstr(row, col, ' ') - def show_notification(self, message, timeout=None, style='info'): + def show_notification(self, message, timeout=None, style='Info'): """ Overlay a message box on the center of the screen and wait for input. @@ -299,7 +299,7 @@ class Terminal(object): notification window """ - assert style in ('info', 'warning', 'error', 'success') + assert style in ('Info', 'Warning', 'Error', 'Success') if isinstance(message, six.string_types): message = message.splitlines() @@ -319,7 +319,7 @@ class Terminal(object): s_col = (n_cols - box_width) // 2 + h_offset window = curses.newwin(box_height, box_width, s_row, s_col) - window.bkgd(str(' '), self.attr('notice_{0}'.format(style))) + window.bkgd(str(' '), self.attr('Notice{0}'.format(style))) window.erase() window.border() @@ -708,7 +708,7 @@ class Terminal(object): n_rows, n_cols = self.stdscr.getmaxyx() v_offset, h_offset = self.stdscr.getbegyx() - ch, attr = str(' '), self.attr('prompt') + ch, attr = str(' '), self.attr('Prompt') prompt = self.clean(prompt, n_cols-1) # Create a new window to draw the text at the bottom of the screen, @@ -864,8 +864,7 @@ class Terminal(object): theme = Theme() theme.bind_curses() + self.theme = theme # Apply the default color to the whole screen - self.stdscr.bkgd(str(' '), theme.get('@normal')) - - self.theme = theme + self.stdscr.bkgd(str(' '), self.attr('Normal')) diff --git a/rtv/theme.py b/rtv/theme.py index 214163a..2049407 100644 --- a/rtv/theme.py +++ b/rtv/theme.py @@ -47,66 +47,120 @@ class Theme(object): 'white': 15, } - # Add keywords for the 256 ansi color codes for i in range(256): COLOR_CODES['ansi_{0}'.format(i)] = i + # TODO: Do another pass through these names + # For compatibility with as many terminals as possible, the default theme # 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), + 'Normal': (None, None, None), + 'Selected': (None, None, None), + 'SelectedCursor': (None, None, curses.A_REVERSE), - '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), - 'bar_level_2.highlight': (curses.COLOR_CYAN, None, curses.A_REVERSE), - 'bar_level_3': (curses.COLOR_GREEN, None, curses.A_NORMAL), - 'bar_level_3.highlight': (curses.COLOR_GREEN, None, curses.A_REVERSE), - 'bar_level_4': (curses.COLOR_YELLOW, None, curses.A_NORMAL), - 'bar_level_4.highlight': (curses.COLOR_YELLOW, None, curses.A_REVERSE), - 'comment_author': (curses.COLOR_BLUE, None, curses.A_BOLD), - 'comment_author_self': (curses.COLOR_GREEN, None, curses.A_BOLD), - 'comment_count': (None, None, curses.A_NORMAL), - 'comment_text': (None, None, curses.A_NORMAL), - 'created': (None, None, curses.A_NORMAL), - 'cursor': (None, None, curses.A_NORMAL), - 'cursor.highlight': (None, None, curses.A_REVERSE), - 'downvote': (curses.COLOR_RED, None, curses.A_BOLD), - 'gold': (curses.COLOR_YELLOW, None, curses.A_BOLD), - 'help_bar': (curses.COLOR_CYAN, None, curses.A_BOLD | curses.A_REVERSE), - 'hidden_comment_expand': (None, None, curses.A_BOLD), - 'hidden_comment_text': (None, None, curses.A_NORMAL), - 'multireddit_name': (curses.COLOR_YELLOW, None, curses.A_BOLD), - 'multireddit_text': (None, None, curses.A_NORMAL), - 'neutral_vote': (None, None, curses.A_BOLD), - 'notice_info': (None, None, curses.A_NORMAL), - 'notice_loading': (None, None, curses.A_NORMAL), - 'notice_error': (curses.COLOR_RED, None, curses.A_NORMAL), - 'notice_success': (curses.COLOR_GREEN, None, curses.A_NORMAL), - 'nsfw': (curses.COLOR_RED, None, curses.A_BOLD), - 'order_bar': (curses.COLOR_YELLOW, None, curses.A_BOLD), - 'order_bar.highlight': (curses.COLOR_YELLOW, None, curses.A_BOLD | curses.A_REVERSE), - 'prompt': (curses.COLOR_CYAN, None, curses.A_BOLD | curses.A_REVERSE), - 'saved': (curses.COLOR_GREEN, None, curses.A_NORMAL), - 'score': (None, None, curses.A_NORMAL), - 'separator': (None, None, curses.A_BOLD), - 'stickied': (curses.COLOR_GREEN, None, curses.A_NORMAL), - 'subscription_name': (curses.COLOR_YELLOW, None, curses.A_BOLD), - 'subscription_text': (None, None, curses.A_NORMAL), - 'submission_author': (curses.COLOR_GREEN, None, curses.A_NORMAL), - 'submission_flair': (curses.COLOR_RED, None, curses.A_NORMAL), - 'submission_subreddit': (curses.COLOR_YELLOW, None, curses.A_NORMAL), - 'submission_text': (None, None, curses.A_NORMAL), - 'submission_title': (None, None, curses.A_BOLD), - 'title_bar': (curses.COLOR_CYAN, None, curses.A_BOLD | curses.A_REVERSE), - 'upvote': (curses.COLOR_GREEN, None, curses.A_BOLD), - 'url': (curses.COLOR_BLUE, None, curses.A_UNDERLINE), - 'url_seen': (curses.COLOR_MAGENTA, None, curses.A_UNDERLINE), - 'user_flair': (curses.COLOR_YELLOW, None, curses.A_BOLD) + 'PageTitle': (curses.COLOR_CYAN, None, curses.A_BOLD | curses.A_REVERSE), + 'PageOrder': (curses.COLOR_YELLOW, None, curses.A_BOLD), + 'PageOrderHighlight': (curses.COLOR_YELLOW, None, curses.A_BOLD | curses.A_REVERSE), + 'Help': (curses.COLOR_CYAN, None, curses.A_BOLD | curses.A_REVERSE), + 'Prompt': (curses.COLOR_CYAN, None, curses.A_BOLD | curses.A_REVERSE), + 'NoticeInfo': (None, None, curses.A_BOLD), + 'NoticeLoading': (None, None, curses.A_BOLD), + 'NoticeError': (None, None, curses.A_BOLD), + 'NoticeSuccess': (None, None, curses.A_BOLD), + + 'CursorBlock': (None, None, None), + 'CursorBar1': (curses.COLOR_MAGENTA, None, None), + 'CursorBar2': (curses.COLOR_CYAN, None, None), + 'CursorBar3': (curses.COLOR_GREEN, None, None), + 'CursorBar4': (curses.COLOR_YELLOW, None, None), + + 'CommentAuthor': (curses.COLOR_BLUE, None, curses.A_BOLD), + 'CommentAuthorSelf': (curses.COLOR_GREEN, None, curses.A_BOLD), + 'CommentCount': (None, None, None), + 'CommentText': (None, None, None), + 'Created': (None, None, None), + 'Downvote': (curses.COLOR_RED, None, curses.A_BOLD), + 'Gold': (curses.COLOR_YELLOW, None, curses.A_BOLD), + 'HiddenCommentExpand': (None, None, curses.A_BOLD), + 'HiddenCommentText': (None, None, None), + 'MultiredditName': (curses.COLOR_YELLOW, None, curses.A_BOLD), + 'MultiredditText': (None, None, None), + 'NeutralVote': (None, None, curses.A_BOLD), + 'NSFW': (curses.COLOR_RED, None, curses.A_BOLD), + 'Saved': (curses.COLOR_GREEN, None, None), + 'Score': (None, None, None), + 'Separator': (None, None, curses.A_BOLD), + 'Stickied': (curses.COLOR_GREEN, None, None), + 'SubscriptionName': (curses.COLOR_YELLOW, None, curses.A_BOLD), + 'SubscriptionText': (None, None, None), + 'SubmissionAuthor': (curses.COLOR_GREEN, None, None), + 'SubmissionFlair': (curses.COLOR_RED, None, None), + 'SubmissionSubreddit': (curses.COLOR_YELLOW, None, None), + 'SubmissionText': (None, None, None), + 'SubmissionTitle': (None, None, curses.A_BOLD), + 'Upvote': (curses.COLOR_GREEN, None, curses.A_BOLD), + 'Link': (curses.COLOR_BLUE, None, curses.A_UNDERLINE), + 'LinkSeen': (curses.COLOR_MAGENTA, None, curses.A_UNDERLINE), + 'UserFlair': (curses.COLOR_YELLOW, None, curses.A_BOLD) } - BAR_LEVELS = ['bar_level_1', 'bar_level_2', 'bar_level_3', 'bar_level_4'] + # List of elements that might be highlighted by the "Selected" row + SELECTED_ELEMENTS = [ + 'CommentAuthor', + 'CommentAuthorSelf', + 'CommentCount', + 'CommentText', + 'Created', + 'Downvote', + 'Gold', + 'HiddenCommentExpand', + 'HiddenCommentText', + 'MultiredditName', + 'MultiredditText', + 'NeutralVote', + 'NSFW', + 'Saved', + 'Score', + 'Separator', + 'Stickied', + 'SubscriptionName', + 'SubscriptionText', + 'SubmissionAuthor', + 'SubmissionFlair', + 'SubmissionSubreddit', + 'SubmissionText', + 'SubmissionTitle', + 'Upvote', + 'Link', + 'LinkSeen', + 'UserFlair' + ] + + # List of elements that might be highlighted by the "SelectedCursor" row + SELECTED_CURSOR_ELEMENTS = [ + 'CursorBlock', + 'CursorBar1', + 'CursorBar2', + 'CursorBar3', + 'CursorBar4' + ] + + # List of page elements that cannot be selected + PAGE_ELEMENTS = [ + 'PageOrder', + 'PageOrderHighlight', + 'PageTitle', + 'Help', + 'Prompt', + 'NoticeInfo', + 'NoticeLoading', + 'NoticeError', + 'NoticeSuccess', + ] + + # The SubmissionPage uses this to determine which color bar to use + CURSOR_BARS = ['CursorBar1', 'CursorBar2', 'CursorBar3', 'CursorBar4'] def __init__(self, name=None, source=None, elements=None, use_color=True): """ @@ -131,9 +185,10 @@ class Theme(object): self.name = name self.source = source self.use_color = use_color + self._color_pair_map = None self._attribute_map = None - self._modifier = None + self._selected = None self.required_color_pairs = 0 self.required_colors = 0 @@ -141,45 +196,32 @@ class Theme(object): if elements is None: elements = self.DEFAULT_THEME.copy() - # Fill in any keywords that are defined in the default theme but were - # not passed into the elements dictionary. + # Set any elements that weren't defined by the config to fallback to + # the default color and attributes for key in self.DEFAULT_THEME.keys(): - - # 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 fallback to the default color if key not in elements: elements[key] = (None, None, None) - # Set undefined highlight elements to match their base element - modifier_key = key + '.highlight' - if modifier_key not in elements: - elements[modifier_key] = elements[key] + self._set_fallback(elements, 'Normal', (-1, -1, curses.A_NORMAL)) + self._set_fallback(elements, 'Selected', 'Normal') + self._set_fallback(elements, 'SelectedCursor', 'Normal') - # 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'] - else: - default = elements['@normal'] + # Most elements have two possible attribute states: + # 1. The default state - inherits from "Normal" + # 2. The selected state - inherits from "Selected" and is + # prefixed by the "@" sign. + for name in self.SELECTED_ELEMENTS: + dest = '@{0}'.format(name) + self._set_fallback(elements, name, 'Selected', dest) + self._set_fallback(elements, name, 'Normal') - elements[key] = ( - default[0] if val[0] is None else val[0], - default[1] if val[1] is None else val[1], - default[2] if val[2] is None else val[2]) + for name in self.SELECTED_CURSOR_ELEMENTS: + dest = '@{0}'.format(name) + self._set_fallback(elements, name, 'SelectedCursor', dest) + self._set_fallback(elements, name, 'Normal') + + for name in self.PAGE_ELEMENTS: + self._set_fallback(elements, name, 'Normal') self.elements = elements @@ -238,47 +280,43 @@ class Theme(object): self._attribute_map[element] = attrs - def get(self, element, modifier=None): + def get(self, element, selected=False): """ - Returns the curses attribute code for the given element. - - If element is None, return the background code (e.g. @normal). + Returns the curses attribute code for the given element. """ 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 and not element.startswith('@'): - element = element + '.' + modifier + if selected or self._selected: + element = '@{0}'.format(element) return self._attribute_map[element] @contextmanager - def set_modifier(self, modifier=None): + def turn_on_selected(self): """ - Sets the active modifier inside of context block. + Sets the selected modifier inside of context block. For example: - >>> with theme.set_modifier('highlight'): - >>> attr = theme.get('cursor') + >>> with theme.turn_on_selected(): + >>> attr = theme.get('CursorBlock') Is the same as: - >>> attr = theme.get('cursor', modifier='highlight') + >>> attr = theme.get('CursorBlock', selected=True) Is also the same as: - >>> attr = theme.get('cursor.highlight') + >>> attr = theme.get('@CursorBlock') """ - # This case is undefined if the context manager is nested - assert self._modifier is None + # This context manager should never be nested + assert self._selected is None - self._modifier = modifier + self._selected = True try: yield finally: - self._modifier = None + self._selected = None @classmethod def list_themes(cls, path=THEMES): @@ -378,9 +416,11 @@ class Theme(object): filename: The name of the filename to load. source: A description of where the theme was loaded from. """ + _logger.info('Loading theme %s', filename) try: config = configparser.ConfigParser() + config.optionxform = six.text_type # Preserve case with codecs.open(filename, encoding='utf-8') as fp: config.readfp(fp) except configparser.ParsingError as e: @@ -399,6 +439,7 @@ class Theme(object): if element not in cls.DEFAULT_THEME: # Could happen if using a new config with an older version # of the software + _logger.info('Skipping element %s', element) continue elements[element] = cls._parse_line(element, line, filename) @@ -453,13 +494,26 @@ class Theme(object): else: attrs_code |= attr_code - if element.startswith('@') and None in (fg_code, bg_code, attrs_code): - raise ConfigError( - 'Error loading {0}, {1} cannot have unspecified attributes:\n' - ' {1} = {2}'.format(filename, element, line)) - return fg_code, bg_code, attrs_code + @staticmethod + def _set_fallback(elements, src_field, fallback, dest_field=None): + """ + Helper function used to set the fallback attributes of an element when + they are defined by the configuration as "None" or "-". + """ + + if dest_field is None: + dest_field = src_field + if isinstance(fallback, six.string_types): + fallback = elements[fallback] + + attrs = elements[src_field] + elements[dest_field] = ( + attrs[0] if attrs[0] is not None else fallback[0], + attrs[1] if attrs[1] is not None else fallback[1], + attrs[2] if attrs[2] is not None else fallback[2]) + @staticmethod def rgb_to_ansi(color): """ diff --git a/rtv/themes/default.cfg b/rtv/themes/default.cfg deleted file mode 100644 index 4fd47a6..0000000 --- a/rtv/themes/default.cfg +++ /dev/null @@ -1,52 +0,0 @@ -[theme] -; = -@normal = default default normal -@highlight = default default normal - -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 = - - bold -notice_loading = - - bold -notice_error = red - bold -notice_success = green - bold -nsfw = red - bold+reverse -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 - bold -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/default.cfg.example b/rtv/themes/default.cfg.example new file mode 100644 index 0000000..d9b2a5a --- /dev/null +++ b/rtv/themes/default.cfg.example @@ -0,0 +1,53 @@ +# http://www.calmar.ws/vim/256-xterm-24bit-rgb-color-chart.html +# https://upload.wikimedia.org/wikipedia/commons/1/15/Xterm_256color_chart.svg + +[theme] +; = +Normal = default default - +Selected = - - - +SelectedCursor = - - reverse + +PageTitle = cyan - bold+reverse +PageOrder = yellow - bold +PageOrderHighlight = yellow - bold+reverse +Help = cyan - bold+reverse +Prompt = cyan - bold+reverse +NoticeInfo = - - bold +NoticeLoading = - - bold +NoticeError = - - bold +NoticeSuccess = - - bold + +CursorBlock = - - - +CursorBar1 = magenta - - +CursorBar2 = cyan - - +CursorBar3 = green - - +CursorBar4 = yellow - - + +CommentAuthor = blue - bold +CommentAuthorSelf = green - bold +CommentCount = - - - +CommentText = - - - +Created = - - - +Downvote = red - bold +Gold = yellow - bold +HiddenCommentExpand = - - bold +HiddenCommentText = - - - +MultiredditName = yellow - bold +MultiredditText = - - - +NeutralVote = - - bold +NSFW = red - bold+reverse +Saved = green - - +Score = - - - +Separator = - - bold +Stickied = green - - +SubscriptionName = yellow - bold +SubscriptionText = - - - +SubmissionAuthor = green - bold +SubmissionFlair = red - - +SubmissionSubreddit = yellow - - +SubmissionText = - - - +SubmissionTitle = - - bold +Upvote = green - bold +Link = blue - underline +LinkSeen = magenta - underline +UserFlair = yellow - bold \ No newline at end of file diff --git a/rtv/themes/solarized-dark.cfg b/rtv/themes/solarized-dark.cfg index 14fb5b0..cc486fa 100644 --- a/rtv/themes/solarized-dark.cfg +++ b/rtv/themes/solarized-dark.cfg @@ -19,53 +19,51 @@ [theme] ; = -@normal = ansi_244 ansi_234 normal -@highlight = ansi_244 ansi_235 normal +Normal = ansi_244 ansi_234 - +Selected = ansi_244 ansi_235 - +SelectedCursor = ansi_244 ansi_235 bold+reverse -bar_level_1 = ansi_125 - -bar_level_1.highlight = ansi_125 - reverse -bar_level_2 = ansi_160 - -bar_level_2.highlight = ansi_125 - reverse -bar_level_3 = ansi_61 - -bar_level_3.highlight = ansi_125 - reverse -bar_level_4 = ansi_37 - -bar_level_4.highlight = ansi_125 - reverse -comment_author = ansi_33 - bold -comment_author_self = ansi_64 - bold -comment_count = - - -comment_text = - - -created = - - -cursor = - - -cursor.highlight = ansi_240 - reverse -downvote = ansi_160 - bold -gold = ansi_136 - bold -help_bar = ansi_37 - bold+reverse -hidden_comment_expand = ansi_245 - bold -hidden_comment_text = ansi_245 - -multireddit_name = ansi_240 - bold -multireddit_text = ansi_245 - -neutral_vote = - - bold -notice_info = - - bold -notice_loading = - - bold -notice_error = ansi_160 - bold -notice_success = ansi_64 - bold -nsfw = ansi_125 - bold+reverse -order_bar = ansi_245 - bold -order_bar.highlight = ansi_245 - bold+reverse -prompt = ansi_33 - bold+reverse -saved = ansi_125 - -score = - - -separator = - - bold -stickied = ansi_136 - -subscription_name = ansi_240 - bold -subscription_text = ansi_245 - -submission_author = ansi_64 - bold -submission_flair = ansi_160 - -submission_subreddit = ansi_166 - -submission_text = - - -submission_title = ansi_244 - bold -title_bar = ansi_37 - bold+reverse -upvote = ansi_64 - bold -url = ansi_33 - underline -url_seen = ansi_61 - underline -user_flair = ansi_136 - bold \ No newline at end of file +PageTitle = ansi_37 - bold+reverse +PageOrder = ansi_240 - bold +PageOrderHighlight = ansi_240 - bold+reverse +Help = ansi_37 - bold+reverse +Prompt = ansi_33 - bold+reverse +NoticeInfo = - - bold +NoticeLoading = - - bold +NoticeError = ansi_160 - bold +NoticeSuccess = ansi_64 - bold + +CursorBlock = ansi_240 - - +CursorBar1 = ansi_125 - bold +CursorBar2 = ansi_160 - bold +CursorBar3 = ansi_61 - bold +CursorBar4 = ansi_37 - bold + +CommentAuthor = ansi_33 - bold +CommentAuthorSelf = ansi_64 - bold +CommentCount = - - - +CommentText = - - - +Created = - - - +Downvote = ansi_160 - bold +Gold = ansi_136 - bold +HiddenCommentExpand = ansi_245 - bold +HiddenCommentText = ansi_245 - - +MultiredditName = ansi_240 - bold +MultiredditText = ansi_245 - - +NeutralVote = - - bold +NSFW = ansi_125 - bold+reverse +Saved = ansi_125 - - +Score = - - - +Separator = - - bold +Stickied = ansi_136 - - +SubscriptionName = ansi_240 - bold +SubscriptionText = ansi_245 - - +SubmissionAuthor = ansi_64 - bold +SubmissionFlair = ansi_160 - - +SubmissionSubreddit = ansi_166 - - +SubmissionText = - - - +SubmissionTitle = ansi_245 - bold +Upvote = ansi_64 - bold +Link = ansi_33 - underline +LinkSeen = ansi_61 - underline +UserFlair = ansi_136 - bold \ No newline at end of file diff --git a/rtv/themes/solarized-light.cfg b/rtv/themes/solarized-light.cfg index 2d5833c..ecac0f3 100644 --- a/rtv/themes/solarized-light.cfg +++ b/rtv/themes/solarized-light.cfg @@ -19,53 +19,51 @@ [theme] ; = -@normal = ansi_241 ansi_230 normal -@highlight = ansi_241 ansi_254 normal +Normal = ansi_241 ansi_230 - +Selected = ansi_241 ansi_254 - +SelectedCursor = ansi_241 ansi_254 reverse -bar_level_1 = ansi_125 - -bar_level_1.highlight = ansi_125 - reverse -bar_level_2 = ansi_160 - -bar_level_2.highlight = ansi_125 - reverse -bar_level_3 = ansi_61 - -bar_level_3.highlight = ansi_125 - reverse -bar_level_4 = ansi_37 - -bar_level_4.highlight = ansi_125 - reverse -comment_author = ansi_33 - bold -comment_author_self = ansi_64 - bold -comment_count = - - -comment_text = - - -created = - - -cursor = - - -cursor.highlight = ansi_245 - reverse -downvote = ansi_160 - bold -gold = ansi_136 - bold -help_bar = ansi_37 - bold+reverse -hidden_comment_expand = ansi_245 - bold -hidden_comment_text = ansi_245 - -multireddit_name = ansi_240 - bold -multireddit_text = ansi_245 - -neutral_vote = - - bold -notice_info = - - bold -notice_loading = - - bold -notice_error = ansi_160 - bold -notice_success = ansi_64 - bold -nsfw = ansi_125 - bold+reverse -order_bar = ansi_245 - bold -order_bar.highlight = ansi_245 - bold+reverse -prompt = ansi_33 - bold+reverse -saved = ansi_125 - -score = - - -separator = - - bold -stickied = ansi_136 - -subscription_name = ansi_240 - bold -subscription_text = ansi_245 - -submission_author = ansi_64 - bold -submission_flair = ansi_160 - -submission_subreddit = ansi_166 - -submission_text = - - -submission_title = ansi_240 - bold -title_bar = ansi_37 - bold+reverse -upvote = ansi_64 - bold -url = ansi_33 - underline -url_seen = ansi_61 - underline -user_flair = ansi_136 - bold +PageTitle = ansi_37 - bold+reverse +PageOrder = ansi_245 - bold +PageOrderHighlight = ansi_245 - bold+reverse +Help = ansi_37 - bold+reverse +Prompt = ansi_33 - bold+reverse +NoticeInfo = - - bold +NoticeLoading = - - bold +NoticeError = ansi_160 - bold +NoticeSuccess = ansi_64 - bold + +CursorBlock = ansi_245 - - +CursorBar1 = ansi_125 - - +CursorBar2 = ansi_160 - - +CursorBar3 = ansi_61 - - +CursorBar4 = ansi_37 - - + +CommentAuthor = ansi_33 - bold +CommentAuthorSelf = ansi_64 - bold +CommentCount = - - - +CommentText = - - - +Created = - - - +Downvote = ansi_160 - bold +Gold = ansi_136 - bold +HiddenCommentExpand = ansi_245 - bold +HiddenCommentText = ansi_245 - - +MultiredditName = ansi_240 - bold +MultiredditText = ansi_245 - - +NeutralVote = - - bold +NSFW = ansi_125 - bold+reverse +Saved = ansi_125 - - +Score = - - - +Separator = - - bold +Stickied = ansi_136 - - +SubscriptionName = ansi_240 - bold +SubscriptionText = ansi_245 - - +SubmissionAuthor = ansi_64 - bold +SubmissionFlair = ansi_160 - - +SubmissionSubreddit = ansi_166 - - +SubmissionText = - - - +SubmissionTitle = ansi_240 - bold +Upvote = ansi_64 - bold +Link = ansi_33 - underline +LinkSeen = ansi_61 - underline +UserFlair = ansi_136 - bold \ No newline at end of file