Cherry picking backwards-compatible changes from the themes branch
This commit is contained in:
@@ -35,9 +35,10 @@ from . import docs
|
||||
from . import packages
|
||||
from .packages import praw
|
||||
from .config import Config, copy_default_config, copy_default_mailcap
|
||||
from .theme import Theme
|
||||
from .oauth import OAuthHelper
|
||||
from .terminal import Terminal
|
||||
from .objects import curses_session, Color, patch_webbrowser
|
||||
from .objects import curses_session, patch_webbrowser
|
||||
from .subreddit_page import SubredditPage
|
||||
from .exceptions import ConfigError
|
||||
from .__version__ import __version__
|
||||
@@ -169,11 +170,9 @@ def main():
|
||||
try:
|
||||
with curses_session() as stdscr:
|
||||
|
||||
# Initialize global color-pairs with curses
|
||||
if not config['monochrome']:
|
||||
Color.init()
|
||||
theme = Theme(config['monochrome'])
|
||||
term = Terminal(stdscr, config, theme)
|
||||
|
||||
term = Terminal(stdscr, config)
|
||||
with term.loader('Initializing', catch_exception=False):
|
||||
reddit = praw.Reddit(user_agent=user_agent,
|
||||
decode_html_entities=False,
|
||||
|
||||
@@ -135,6 +135,9 @@ class OrderedSet(object):
|
||||
|
||||
|
||||
class Config(object):
|
||||
"""
|
||||
This class manages the loading and saving of configs and other files.
|
||||
"""
|
||||
|
||||
def __init__(self, history_file=HISTORY, token_file=TOKEN, **kwargs):
|
||||
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -230,7 +230,8 @@ class LoadScreen(object):
|
||||
for e_type, message in self.EXCEPTION_MESSAGES:
|
||||
# Some exceptions we want to swallow and display a notification
|
||||
if isinstance(e, e_type):
|
||||
self._terminal.show_notification(message.format(e))
|
||||
msg = message.format(e)
|
||||
self._terminal.show_notification(msg, style='error')
|
||||
return True
|
||||
|
||||
def animate(self, delay, interval, message, trail):
|
||||
@@ -250,12 +251,16 @@ class LoadScreen(object):
|
||||
return
|
||||
time.sleep(0.01)
|
||||
|
||||
# Build the notification window
|
||||
# Build the notification window. Note that we need to use
|
||||
# curses.newwin() instead of stdscr.derwin() so the text below the
|
||||
# notification window does not got erased when we cover it up.
|
||||
message_len = len(message) + len(trail)
|
||||
n_rows, n_cols = self._terminal.stdscr.getmaxyx()
|
||||
s_row = (n_rows - 3) // 2
|
||||
s_col = (n_cols - message_len - 1) // 2
|
||||
v_offset, h_offset = self._terminal.stdscr.getbegyx()
|
||||
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'))
|
||||
|
||||
# Animate the loading prompt until the stopping condition is triggered
|
||||
# when the context manager exits.
|
||||
@@ -285,49 +290,6 @@ class LoadScreen(object):
|
||||
time.sleep(0.01)
|
||||
|
||||
|
||||
class Color(object):
|
||||
"""
|
||||
Color attributes for curses.
|
||||
"""
|
||||
|
||||
RED = curses.A_NORMAL
|
||||
GREEN = curses.A_NORMAL
|
||||
YELLOW = curses.A_NORMAL
|
||||
BLUE = curses.A_NORMAL
|
||||
MAGENTA = curses.A_NORMAL
|
||||
CYAN = curses.A_NORMAL
|
||||
WHITE = curses.A_NORMAL
|
||||
|
||||
_colors = {
|
||||
'RED': (curses.COLOR_RED, -1),
|
||||
'GREEN': (curses.COLOR_GREEN, -1),
|
||||
'YELLOW': (curses.COLOR_YELLOW, -1),
|
||||
'BLUE': (curses.COLOR_BLUE, -1),
|
||||
'MAGENTA': (curses.COLOR_MAGENTA, -1),
|
||||
'CYAN': (curses.COLOR_CYAN, -1),
|
||||
'WHITE': (curses.COLOR_WHITE, -1),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def init(cls):
|
||||
"""
|
||||
Initialize color pairs inside of curses using the default background.
|
||||
|
||||
This should be called once during the curses initial setup. Afterwards,
|
||||
curses color pairs can be accessed directly through class attributes.
|
||||
"""
|
||||
|
||||
for index, (attr, code) in enumerate(cls._colors.items(), start=1):
|
||||
curses.init_pair(index, code[0], code[1])
|
||||
setattr(cls, attr, curses.color_pair(index))
|
||||
|
||||
@classmethod
|
||||
def get_level(cls, level):
|
||||
|
||||
levels = [cls.MAGENTA, cls.CYAN, cls.GREEN, cls.YELLOW]
|
||||
return levels[level % len(levels)]
|
||||
|
||||
|
||||
class Navigator(object):
|
||||
"""
|
||||
Handles the math behind cursor movement and screen paging.
|
||||
|
||||
80
rtv/page.py
80
rtv/page.py
@@ -4,7 +4,6 @@ from __future__ import unicode_literals
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import curses
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
||||
@@ -12,7 +11,7 @@ import six
|
||||
from kitchen.text.display import textual_width
|
||||
|
||||
from . import docs
|
||||
from .objects import Controller, Color, Command
|
||||
from .objects import Controller, Command
|
||||
from .clipboard import copy
|
||||
from .exceptions import TemporaryFileError, ProgramError
|
||||
from .__version__ import __version__
|
||||
@@ -158,19 +157,15 @@ class Page(object):
|
||||
|
||||
@PageController.register(Command('PAGE_TOP'))
|
||||
def move_page_top(self):
|
||||
self._remove_cursor()
|
||||
self.nav.page_index = self.content.range[0]
|
||||
self.nav.cursor_index = 0
|
||||
self.nav.inverted = False
|
||||
self._add_cursor()
|
||||
|
||||
@PageController.register(Command('PAGE_BOTTOM'))
|
||||
def move_page_bottom(self):
|
||||
self._remove_cursor()
|
||||
self.nav.page_index = self.content.range[1]
|
||||
self.nav.cursor_index = 0
|
||||
self.nav.inverted = True
|
||||
self._add_cursor()
|
||||
|
||||
@PageController.register(Command('UPVOTE'))
|
||||
@logged_in
|
||||
@@ -376,7 +371,6 @@ class Page(object):
|
||||
self._draw_banner()
|
||||
self._draw_content()
|
||||
self._draw_footer()
|
||||
self._add_cursor()
|
||||
self.term.clear_screen()
|
||||
self.term.stdscr.refresh()
|
||||
|
||||
@@ -388,8 +382,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
|
||||
ch, attr = str(' '), curses.A_REVERSE | curses.A_BOLD | Color.CYAN
|
||||
window.bkgd(ch, attr)
|
||||
window.bkgd(str(' '), self.term.attr('title_bar'))
|
||||
|
||||
sub_name = self.content.name
|
||||
sub_name = sub_name.replace('/r/front', 'Front Page')
|
||||
@@ -421,7 +414,7 @@ class Page(object):
|
||||
sys.stdout.write(title)
|
||||
sys.stdout.flush()
|
||||
|
||||
if self.reddit.user is not None:
|
||||
if self.reddit and self.reddit.user is not None:
|
||||
# The starting position of the name depends on if we're converting
|
||||
# to ascii or not
|
||||
width = len if self.config['ascii'] else textual_width
|
||||
@@ -442,8 +435,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()
|
||||
ch, attr = str(' '), curses.A_BOLD | Color.YELLOW
|
||||
window.bkgd(ch, attr)
|
||||
window.bkgd(str(' '), self.term.attr('order_bar'))
|
||||
|
||||
banner = docs.BANNER_SEARCH if self.content.query else docs.BANNER
|
||||
items = banner.strip().split(' ')
|
||||
@@ -455,7 +447,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, attr | curses.A_REVERSE)
|
||||
attr = self.term.theme.get('order_bar', modifier='selected')
|
||||
window.chgat(0, col, 3, attr)
|
||||
|
||||
self._row += 1
|
||||
|
||||
@@ -465,8 +458,7 @@ class Page(object):
|
||||
"""
|
||||
|
||||
n_rows, n_cols = self.term.stdscr.getmaxyx()
|
||||
window = self.term.stdscr.derwin(
|
||||
n_rows - self._row - 1, n_cols, self._row, 0)
|
||||
window = self.term.stdscr.derwin(n_rows - self._row - 1, n_cols, self._row, 0)
|
||||
window.erase()
|
||||
win_n_rows, win_n_cols = window.getmaxyx()
|
||||
|
||||
@@ -493,10 +485,8 @@ class Page(object):
|
||||
top_item_height = None
|
||||
subwin_n_cols = win_n_cols - data['h_offset']
|
||||
start = current_row - subwin_n_rows + 1 if inverted else current_row
|
||||
subwindow = window.derwin(
|
||||
subwin_n_rows, subwin_n_cols, start, data['h_offset'])
|
||||
attr = self._draw_item(subwindow, data, subwin_inverted)
|
||||
self._subwindows.append((subwindow, attr))
|
||||
subwindow = window.derwin(subwin_n_rows, subwin_n_cols, start, data['h_offset'])
|
||||
self._subwindows.append((subwindow, data, subwin_inverted))
|
||||
available_rows -= (subwin_n_rows + 1) # Add one for the blank line
|
||||
current_row += step * (subwin_n_rows + 1)
|
||||
if available_rows <= 0:
|
||||
@@ -518,6 +508,25 @@ class Page(object):
|
||||
self.nav.flip((len(self._subwindows) - 1))
|
||||
return self._draw_content()
|
||||
|
||||
if self.nav.cursor_index >= len(self._subwindows):
|
||||
# Don't allow the cursor to go over the number of subwindows
|
||||
# This could happen if the window is resized and the cursor index is
|
||||
# pushed out of bounds
|
||||
self.nav.cursor_index = len(self._subwindows) - 1
|
||||
|
||||
# Now that the windows are setup, we can take a second pass through
|
||||
# to draw the content
|
||||
for index, (win, data, inverted) in enumerate(self._subwindows):
|
||||
if index == self.nav.cursor_index:
|
||||
# This lets the theme know to invert the cursor
|
||||
modifier = 'selected'
|
||||
else:
|
||||
modifier = None
|
||||
|
||||
win.bkgd(str(' '), self.term.attr('normal'))
|
||||
with self.term.theme.set_modifier(modifier):
|
||||
self._draw_item(win, data, inverted)
|
||||
|
||||
self._row += win_n_rows
|
||||
|
||||
def _draw_footer(self):
|
||||
@@ -525,54 +534,23 @@ class Page(object):
|
||||
n_rows, n_cols = self.term.stdscr.getmaxyx()
|
||||
window = self.term.stdscr.derwin(1, n_cols, self._row, 0)
|
||||
window.erase()
|
||||
ch, attr = str(' '), curses.A_REVERSE | curses.A_BOLD | Color.CYAN
|
||||
window.bkgd(ch, attr)
|
||||
window.bkgd(str(' '), self.term.attr('help_bar'))
|
||||
|
||||
text = self.FOOTER.strip()
|
||||
self.term.add_line(window, text, 0, 0)
|
||||
self._row += 1
|
||||
|
||||
def _add_cursor(self):
|
||||
self._edit_cursor(curses.A_REVERSE)
|
||||
|
||||
def _remove_cursor(self):
|
||||
self._edit_cursor(curses.A_NORMAL)
|
||||
|
||||
def _move_cursor(self, direction):
|
||||
self._remove_cursor()
|
||||
# Note: ACS_VLINE doesn't like changing the attribute, so disregard the
|
||||
# redraw flag and opt to always redraw
|
||||
valid, redraw = self.nav.move(direction, len(self._subwindows))
|
||||
if not valid:
|
||||
self.term.flash()
|
||||
self._add_cursor()
|
||||
|
||||
def _move_page(self, direction):
|
||||
self._remove_cursor()
|
||||
valid, redraw = self.nav.move_page(direction, len(self._subwindows)-1)
|
||||
if not valid:
|
||||
self.term.flash()
|
||||
self._add_cursor()
|
||||
|
||||
def _edit_cursor(self, attribute):
|
||||
|
||||
# Don't allow the cursor to go below page index 0
|
||||
if self.nav.absolute_index < 0:
|
||||
return
|
||||
|
||||
# Don't allow the cursor to go over the number of subwindows
|
||||
# This could happen if the window is resized and the cursor index is
|
||||
# pushed out of bounds
|
||||
if self.nav.cursor_index >= len(self._subwindows):
|
||||
self.nav.cursor_index = len(self._subwindows) - 1
|
||||
|
||||
window, attr = self._subwindows[self.nav.cursor_index]
|
||||
if attr is not None:
|
||||
attribute |= attr
|
||||
|
||||
n_rows, _ = window.getmaxyx()
|
||||
for row in range(n_rows):
|
||||
window.chgat(row, 0, 1, attribute)
|
||||
|
||||
def _prompt_period(self, order):
|
||||
|
||||
|
||||
@@ -3,12 +3,11 @@ from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import time
|
||||
import curses
|
||||
|
||||
from . import docs
|
||||
from .content import SubmissionContent, SubredditContent
|
||||
from .page import Page, PageController, logged_in
|
||||
from .objects import Navigator, Color, Command
|
||||
from .objects import Navigator, Command
|
||||
from .exceptions import TemporaryFileError
|
||||
|
||||
|
||||
@@ -119,7 +118,7 @@ class SubmissionPage(Page):
|
||||
@SubmissionController.register(Command('SUBMISSION_OPEN_IN_BROWSER'))
|
||||
def open_link(self):
|
||||
"""
|
||||
Open the selected item with the webbrowser
|
||||
Open the selected item with the web browser
|
||||
"""
|
||||
|
||||
data = self.get_selected_item()
|
||||
@@ -207,6 +206,10 @@ class SubmissionPage(Page):
|
||||
|
||||
@SubmissionController.register(Command('SUBMISSION_OPEN_IN_URLVIEWER'))
|
||||
def comment_urlview(self):
|
||||
"""
|
||||
Open the selected comment with the URL viewer
|
||||
"""
|
||||
|
||||
data = self.get_selected_item()
|
||||
comment = data.get('body') or data.get('text') or data.get('url_full')
|
||||
if comment:
|
||||
@@ -285,89 +288,114 @@ 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)')
|
||||
|
||||
row = offset
|
||||
if row in valid_rows:
|
||||
|
||||
attr = curses.A_BOLD
|
||||
attr |= (Color.BLUE if not data['is_author'] else Color.GREEN)
|
||||
text = '{author} '.format(**data)
|
||||
if data['is_author']:
|
||||
text += '[S] '
|
||||
attr = self.term.attr('comment_author_self')
|
||||
text = '{author} [S]'.format(**data)
|
||||
else:
|
||||
attr = self.term.attr('comment_author')
|
||||
text = '{author}'.format(**data)
|
||||
self.term.add_line(win, text, row, 1, attr)
|
||||
|
||||
if data['flair']:
|
||||
attr = curses.A_BOLD | Color.YELLOW
|
||||
self.term.add_line(win, '{flair} '.format(**data), attr=attr)
|
||||
attr = self.term.attr('user_flair')
|
||||
self.term.add_space(win)
|
||||
self.term.add_line(win, '{flair}'.format(**data), attr=attr)
|
||||
|
||||
text, attr = self.term.get_arrow(data['likes'])
|
||||
self.term.add_line(win, text, attr=attr)
|
||||
self.term.add_line(win, ' {score} {created} '.format(**data))
|
||||
arrow, attr = self.term.get_arrow(data['likes'])
|
||||
self.term.add_space(win)
|
||||
self.term.add_line(win, arrow, attr=attr)
|
||||
|
||||
attr = self.term.attr('score')
|
||||
self.term.add_space(win)
|
||||
self.term.add_line(win, '{score}'.format(**data), attr=attr)
|
||||
|
||||
attr = self.term.attr('created')
|
||||
self.term.add_space(win)
|
||||
self.term.add_line(win, '{created}'.format(**data), attr=attr)
|
||||
|
||||
if data['gold']:
|
||||
text, attr = self.term.guilded
|
||||
self.term.add_line(win, text, attr=attr)
|
||||
attr = self.term.attr('gold')
|
||||
self.term.add_space(win)
|
||||
self.term.add_line(win, self.term.guilded, attr=attr)
|
||||
|
||||
if data['stickied']:
|
||||
text, attr = '[stickied]', Color.GREEN
|
||||
self.term.add_line(win, text, attr=attr)
|
||||
attr = self.term.attr('stickied')
|
||||
self.term.add_space(win)
|
||||
self.term.add_line(win, '[stickied]', attr=attr)
|
||||
|
||||
if data['saved']:
|
||||
text, attr = '[saved]', Color.GREEN
|
||||
self.term.add_line(win, text, attr=attr)
|
||||
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')
|
||||
if row in valid_rows:
|
||||
self.term.add_line(win, text, row, 1)
|
||||
self.term.add_line(win, text, row, 1, attr=attr)
|
||||
|
||||
# Unfortunately vline() doesn't support custom color so we have to
|
||||
# build it one segment at a time.
|
||||
attr = Color.get_level(data['level'])
|
||||
x = 0
|
||||
index = data['level'] % len(self.term.theme.BAR_LEVELS)
|
||||
attr = self.term.attr(self.term.theme.BAR_LEVELS[index])
|
||||
for y in range(n_rows):
|
||||
self.term.addch(win, y, x, self.term.vline, attr)
|
||||
|
||||
return attr | self.term.vline
|
||||
self.term.addch(win, y, 0, self.term.vline, attr)
|
||||
|
||||
def _draw_more_comments(self, win, data):
|
||||
|
||||
n_rows, n_cols = win.getmaxyx()
|
||||
n_cols -= 1
|
||||
|
||||
self.term.add_line(win, '{body}'.format(**data), 0, 1)
|
||||
self.term.add_line(
|
||||
win, ' [{count}]'.format(**data), attr=curses.A_BOLD)
|
||||
attr = self.term.attr('hidden_comment_text')
|
||||
self.term.add_line(win, '{body}'.format(**data), 0, 1, attr=attr)
|
||||
|
||||
attr = Color.get_level(data['level'])
|
||||
attr = self.term.attr('hidden_comment_expand')
|
||||
self.term.add_space(win)
|
||||
self.term.add_line(win, '[{count}]'.format(**data), attr=attr)
|
||||
|
||||
index = data['level'] % len(self.term.theme.BAR_LEVELS)
|
||||
attr = self.term.attr(self.term.theme.BAR_LEVELS[index])
|
||||
self.term.addch(win, 0, 0, self.term.vline, attr)
|
||||
|
||||
return attr | self.term.vline
|
||||
|
||||
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')
|
||||
for row, text in enumerate(data['split_title'], start=1):
|
||||
self.term.add_line(win, text, row, 1, curses.A_BOLD)
|
||||
self.term.add_line(win, text, row, 1, attr)
|
||||
|
||||
row = len(data['split_title']) + 1
|
||||
attr = curses.A_BOLD | Color.GREEN
|
||||
attr = self.term.attr('submission_author')
|
||||
self.term.add_line(win, '{author}'.format(**data), row, 1, attr)
|
||||
attr = curses.A_BOLD | Color.YELLOW
|
||||
|
||||
if data['flair']:
|
||||
self.term.add_line(win, ' {flair}'.format(**data), attr=attr)
|
||||
self.term.add_line(win, ' {created} {subreddit}'.format(**data))
|
||||
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')
|
||||
self.term.add_space(win)
|
||||
self.term.add_line(win, '{created}'.format(**data), attr=attr)
|
||||
|
||||
attr = self.term.attr('submission_subreddit')
|
||||
self.term.add_space(win)
|
||||
self.term.add_line(win, '/r/{subreddit}'.format(**data), attr=attr)
|
||||
|
||||
row = len(data['split_title']) + 2
|
||||
seen = (data['url_full'] in self.config.history)
|
||||
link_color = Color.MAGENTA if seen else Color.BLUE
|
||||
attr = curses.A_UNDERLINE | link_color
|
||||
if data['url_full'] in self.config.history:
|
||||
attr = self.term.attr('url_seen')
|
||||
else:
|
||||
attr = self.term.attr('url')
|
||||
self.term.add_line(win, '{url}'.format(**data), row, 1, attr)
|
||||
|
||||
offset = len(data['split_title']) + 3
|
||||
|
||||
# Cut off text if there is not enough room to display the whole post
|
||||
@@ -377,25 +405,35 @@ class SubmissionPage(Page):
|
||||
split_text = split_text[:-cutoff]
|
||||
split_text.append('(Not enough space to display)')
|
||||
|
||||
attr = self.term.attr('submission_text')
|
||||
for row, text in enumerate(split_text, start=offset):
|
||||
self.term.add_line(win, text, row, 1)
|
||||
self.term.add_line(win, text, row, 1, attr=attr)
|
||||
|
||||
row = len(data['split_title']) + len(split_text) + 3
|
||||
self.term.add_line(win, '{score} '.format(**data), row, 1)
|
||||
text, attr = self.term.get_arrow(data['likes'])
|
||||
self.term.add_line(win, text, attr=attr)
|
||||
self.term.add_line(win, ' {comments} '.format(**data))
|
||||
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')
|
||||
self.term.add_space(win)
|
||||
self.term.add_line(win, '{comments}'.format(**data), attr=attr)
|
||||
|
||||
if data['gold']:
|
||||
text, attr = self.term.guilded
|
||||
self.term.add_line(win, text, attr=attr)
|
||||
attr = self.term.attr('gold')
|
||||
self.term.add_space(win)
|
||||
self.term.add_line(win, self.term.guilded, attr=attr)
|
||||
|
||||
if data['nsfw']:
|
||||
text, attr = 'NSFW', (curses.A_BOLD | Color.RED)
|
||||
self.term.add_line(win, text, attr=attr)
|
||||
attr = self.term.attr('nsfw')
|
||||
self.term.add_space(win)
|
||||
self.term.add_line(win, 'NSFW', attr=attr)
|
||||
|
||||
if data['saved']:
|
||||
text, attr = '[saved]', Color.GREEN
|
||||
self.term.add_line(win, text, attr=attr)
|
||||
attr = self.term.attr('saved')
|
||||
self.term.add_space(win)
|
||||
self.term.add_line(win, '[saved]', attr=attr)
|
||||
|
||||
win.border()
|
||||
|
||||
@@ -3,12 +3,11 @@ from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import time
|
||||
import curses
|
||||
|
||||
from . import docs
|
||||
from .content import SubredditContent
|
||||
from .page import Page, PageController, logged_in
|
||||
from .objects import Navigator, Color, Command
|
||||
from .objects import Navigator, Command
|
||||
from .submission_page import SubmissionPage
|
||||
from .subscription_page import SubscriptionPage
|
||||
from .exceptions import TemporaryFileError
|
||||
@@ -265,50 +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')
|
||||
if row in valid_rows:
|
||||
self.term.add_line(win, text, row, 1, curses.A_BOLD)
|
||||
self.term.add_line(win, text, row, 1, attr)
|
||||
|
||||
row = n_title + offset
|
||||
if row in valid_rows:
|
||||
seen = (data['url_full'] in self.config.history)
|
||||
link_color = Color.MAGENTA if seen else Color.BLUE
|
||||
attr = curses.A_UNDERLINE | link_color
|
||||
if data['url_full'] in self.config.history:
|
||||
attr = self.term.attr('url_seen')
|
||||
else:
|
||||
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:
|
||||
self.term.add_line(win, '{score} '.format(**data), row, 1)
|
||||
text, attr = self.term.get_arrow(data['likes'])
|
||||
self.term.add_line(win, text, attr=attr)
|
||||
self.term.add_line(win, ' {created} '.format(**data))
|
||||
|
||||
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'])
|
||||
self.term.add_line(win, arrow, attr=attr)
|
||||
self.term.add_space(win)
|
||||
|
||||
attr = self.term.attr('created')
|
||||
self.term.add_line(win, '{created}'.format(**data), attr=attr)
|
||||
|
||||
if data['comments'] is not None:
|
||||
text, attr = '-', curses.A_BOLD
|
||||
self.term.add_line(win, text, attr=attr)
|
||||
self.term.add_line(win, ' {comments} '.format(**data))
|
||||
attr = self.term.attr('separator')
|
||||
self.term.add_space(win)
|
||||
self.term.add_line(win, '-', attr=attr)
|
||||
|
||||
attr = self.term.attr('comment_count')
|
||||
self.term.add_space(win)
|
||||
self.term.add_line(win, '{comments}'.format(**data), attr=attr)
|
||||
|
||||
if data['saved']:
|
||||
text, attr = '[saved]', Color.GREEN
|
||||
self.term.add_line(win, text, attr=attr)
|
||||
attr = self.term.attr('saved')
|
||||
self.term.add_space(win)
|
||||
self.term.add_line(win, '[saved]', attr=attr)
|
||||
|
||||
if data['stickied']:
|
||||
text, attr = '[stickied]', Color.GREEN
|
||||
self.term.add_line(win, text, attr=attr)
|
||||
attr = self.term.attr('stickied')
|
||||
self.term.add_space(win)
|
||||
self.term.add_line(win, '[stickied]', attr=attr)
|
||||
|
||||
if data['gold']:
|
||||
text, attr = self.term.guilded
|
||||
self.term.add_line(win, text, attr=attr)
|
||||
attr = self.term.attr('gold')
|
||||
self.term.add_space(win)
|
||||
self.term.add_line(win, self.term.guilded, attr=attr)
|
||||
|
||||
if data['nsfw']:
|
||||
text, attr = 'NSFW', (curses.A_BOLD | Color.RED)
|
||||
self.term.add_line(win, text, attr=attr)
|
||||
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:
|
||||
text = '{author}'.format(**data)
|
||||
self.term.add_line(win, text, row, 1, Color.GREEN)
|
||||
text = ' /r/{subreddit}'.format(**data)
|
||||
self.term.add_line(win, text, attr=Color.YELLOW)
|
||||
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')
|
||||
self.term.add_line(win, '/r/{subreddit}'.format(**data), attr=attr)
|
||||
|
||||
if data['flair']:
|
||||
text = ' {flair}'.format(**data)
|
||||
self.term.add_line(win, text, attr=Color.RED)
|
||||
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')
|
||||
for y in range(n_rows):
|
||||
self.term.addch(win, y, 0, str(' '), attr)
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import curses
|
||||
|
||||
from . import docs
|
||||
from .page import Page, PageController
|
||||
from .content import SubscriptionContent, SubredditContent
|
||||
from .objects import Color, Navigator, Command
|
||||
from .objects import Navigator, Command
|
||||
|
||||
|
||||
class SubscriptionController(PageController):
|
||||
@@ -95,10 +93,21 @@ class SubscriptionPage(Page):
|
||||
|
||||
row = offset
|
||||
if row in valid_rows:
|
||||
attr = curses.A_BOLD | Color.YELLOW
|
||||
if data['type'] == 'Multireddit':
|
||||
attr = self.term.attr('multireddit_name')
|
||||
else:
|
||||
attr = self.term.attr('subscription_name')
|
||||
self.term.add_line(win, '{name}'.format(**data), row, 1, attr)
|
||||
|
||||
row = offset + 1
|
||||
for row, text in enumerate(data['split_title'], start=row):
|
||||
if row in valid_rows:
|
||||
self.term.add_line(win, text, row, 1)
|
||||
if data['type'] == 'Multireddit':
|
||||
attr = self.term.attr('multireddit_text')
|
||||
else:
|
||||
attr = self.term.attr('subscription_text')
|
||||
self.term.add_line(win, text, row, 1, attr)
|
||||
|
||||
attr = self.term.attr('cursor')
|
||||
for y in range(n_rows):
|
||||
self.term.addch(win, y, 0, str(' '), attr)
|
||||
|
||||
105
rtv/terminal.py
105
rtv/terminal.py
@@ -20,9 +20,9 @@ from tempfile import NamedTemporaryFile
|
||||
import six
|
||||
from kitchen.text.display import textual_width_chop
|
||||
|
||||
from . import exceptions
|
||||
from . import mime_parsers
|
||||
from .objects import LoadScreen, Color
|
||||
from . import exceptions, mime_parsers
|
||||
from .theme import Theme
|
||||
from .objects import LoadScreen
|
||||
|
||||
try:
|
||||
# Fix only needed for versions prior to python 3.6
|
||||
@@ -46,16 +46,20 @@ class Terminal(object):
|
||||
MIN_HEIGHT = 10
|
||||
MIN_WIDTH = 20
|
||||
|
||||
# ASCII code
|
||||
# ASCII codes
|
||||
ESCAPE = 27
|
||||
RETURN = 10
|
||||
SPACE = 32
|
||||
|
||||
def __init__(self, stdscr, config):
|
||||
def __init__(self, stdscr, config, theme=None):
|
||||
|
||||
self.stdscr = stdscr
|
||||
self.config = config
|
||||
self.loader = LoadScreen(self)
|
||||
|
||||
self.theme = None
|
||||
self.set_theme(theme)
|
||||
|
||||
self._display = None
|
||||
self._mailcap_dict = mailcap.getcaps()
|
||||
self._term = os.environ.get('TERM')
|
||||
@@ -66,27 +70,19 @@ class Terminal(object):
|
||||
|
||||
@property
|
||||
def up_arrow(self):
|
||||
symbol = '^' if self.config['ascii'] else '▲'
|
||||
attr = curses.A_BOLD | Color.GREEN
|
||||
return symbol, attr
|
||||
return '^' if self.config['ascii'] else '▲'
|
||||
|
||||
@property
|
||||
def down_arrow(self):
|
||||
symbol = 'v' if self.config['ascii'] else '▼'
|
||||
attr = curses.A_BOLD | Color.RED
|
||||
return symbol, attr
|
||||
return 'v' if self.config['ascii'] else '▼'
|
||||
|
||||
@property
|
||||
def neutral_arrow(self):
|
||||
symbol = 'o' if self.config['ascii'] else '•'
|
||||
attr = curses.A_BOLD
|
||||
return symbol, attr
|
||||
return 'o' if self.config['ascii'] else '•'
|
||||
|
||||
@property
|
||||
def guilded(self):
|
||||
symbol = '*' if self.config['ascii'] else '✪'
|
||||
attr = curses.A_BOLD | Color.YELLOW
|
||||
return symbol, attr
|
||||
return '*' if self.config['ascii'] else '✪'
|
||||
|
||||
@property
|
||||
def vline(self):
|
||||
@@ -197,11 +193,11 @@ class Terminal(object):
|
||||
"""
|
||||
|
||||
if likes is None:
|
||||
return self.neutral_arrow
|
||||
return self.neutral_arrow, self.attr('neutral_vote')
|
||||
elif likes:
|
||||
return self.up_arrow
|
||||
return self.up_arrow, self.attr('upvote')
|
||||
else:
|
||||
return self.down_arrow
|
||||
return self.down_arrow, self.attr('downvote')
|
||||
|
||||
def clean(self, string, n_cols=None):
|
||||
"""
|
||||
@@ -278,7 +274,21 @@ class Terminal(object):
|
||||
params = [] if attr is None else [attr]
|
||||
window.addstr(row, col, text, *params)
|
||||
|
||||
def show_notification(self, message, timeout=None):
|
||||
@staticmethod
|
||||
def add_space(window):
|
||||
"""
|
||||
Shortcut for adding a single space to a window at the current position
|
||||
"""
|
||||
|
||||
row, col = window.getyx()
|
||||
_, max_cols = window.getmaxyx()
|
||||
if max_cols - col - 1 <= 0:
|
||||
# Trying to draw outside of the screen bounds
|
||||
return
|
||||
|
||||
window.addstr(row, col, ' ')
|
||||
|
||||
def show_notification(self, message, timeout=None, style='info'):
|
||||
"""
|
||||
Overlay a message box on the center of the screen and wait for input.
|
||||
|
||||
@@ -286,12 +296,17 @@ class Terminal(object):
|
||||
message (list or string): List of strings, one per line.
|
||||
timeout (float): Optional, maximum length of time that the message
|
||||
will be shown before disappearing.
|
||||
style (str): The theme element that will be applied to the
|
||||
notification window
|
||||
"""
|
||||
|
||||
assert style in ('info', 'warning', 'error', 'success')
|
||||
|
||||
if isinstance(message, six.string_types):
|
||||
message = message.splitlines()
|
||||
|
||||
n_rows, n_cols = self.stdscr.getmaxyx()
|
||||
v_offset, h_offset = self.stdscr.getbegyx()
|
||||
|
||||
box_width = max(len(m) for m in message) + 2
|
||||
box_height = len(message) + 2
|
||||
@@ -301,10 +316,11 @@ class Terminal(object):
|
||||
box_height = min(box_height, n_rows)
|
||||
message = message[:box_height-2]
|
||||
|
||||
s_row = (n_rows - box_height) // 2
|
||||
s_col = (n_cols - box_width) // 2
|
||||
s_row = (n_rows - box_height) // 2 + v_offset
|
||||
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.erase()
|
||||
window.border()
|
||||
|
||||
@@ -382,7 +398,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
|
||||
@@ -692,18 +708,22 @@ class Terminal(object):
|
||||
"""
|
||||
|
||||
n_rows, n_cols = self.stdscr.getmaxyx()
|
||||
ch, attr = str(' '), curses.A_BOLD | curses.A_REVERSE | Color.CYAN
|
||||
v_offset, h_offset = self.stdscr.getbegyx()
|
||||
ch, attr = str(' '), self.attr('prompt')
|
||||
prompt = self.clean(prompt, n_cols-1)
|
||||
|
||||
# Create a new window to draw the text at the bottom of the screen,
|
||||
# so we can erase it when we're done.
|
||||
prompt_win = curses.newwin(1, len(prompt)+1, n_rows-1, 0)
|
||||
s_row = v_offset + n_rows - 1
|
||||
s_col = h_offset
|
||||
prompt_win = curses.newwin(1, len(prompt) + 1, s_row, s_col)
|
||||
prompt_win.bkgd(ch, attr)
|
||||
self.add_line(prompt_win, prompt)
|
||||
prompt_win.refresh()
|
||||
|
||||
# Create a separate window for text input
|
||||
input_win = curses.newwin(1, n_cols-len(prompt), n_rows-1, len(prompt))
|
||||
s_col = h_offset + len(prompt)
|
||||
input_win = curses.newwin(1, n_cols - len(prompt), s_row, s_col)
|
||||
input_win.bkgd(ch, attr)
|
||||
input_win.refresh()
|
||||
|
||||
@@ -802,3 +822,34 @@ class Terminal(object):
|
||||
self.stdscr.touchwin()
|
||||
else:
|
||||
self.stdscr.clearok(True)
|
||||
|
||||
def attr(self, element):
|
||||
"""
|
||||
Shortcut for fetching the color + attribute code for an element.
|
||||
"""
|
||||
|
||||
return self.theme.get(element)
|
||||
|
||||
def set_theme(self, theme=None):
|
||||
"""
|
||||
Set the terminal theme. This is a stub for what will eventually
|
||||
support managing custom themes.
|
||||
|
||||
Check that the terminal supports the provided theme, and applies
|
||||
the theme to the terminal if possible.
|
||||
|
||||
If the terminal doesn't support the theme, this falls back to the
|
||||
default theme. The default theme only requires 8 colors so it
|
||||
should be compatible with any terminal that supports basic colors.
|
||||
"""
|
||||
monochrome = (not curses.has_colors())
|
||||
|
||||
if theme is None:
|
||||
theme = Theme(monochrome=monochrome)
|
||||
|
||||
theme.bind_curses()
|
||||
|
||||
# Apply the default color to the whole screen
|
||||
self.stdscr.bkgd(str(' '), theme.get('normal'))
|
||||
|
||||
self.theme = theme
|
||||
|
||||
113
rtv/theme.py
Normal file
113
rtv/theme.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
This file is a stub that contains the default RTV theme.
|
||||
|
||||
This will eventually be expanded to support loading/managing custom themes.
|
||||
"""
|
||||
|
||||
import curses
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
DEFAULT_THEME = {
|
||||
'normal': (-1, -1, curses.A_NORMAL),
|
||||
'bar_level_1': (curses.COLOR_MAGENTA, -1, curses.A_NORMAL),
|
||||
'bar_level_1.selected': (curses.COLOR_MAGENTA, -1, curses.A_REVERSE),
|
||||
'bar_level_2': (curses.COLOR_CYAN, -1, curses.A_NORMAL),
|
||||
'bar_level_2.selected': (curses.COLOR_CYAN, -1, curses.A_REVERSE),
|
||||
'bar_level_3': (curses.COLOR_GREEN, -1, curses.A_NORMAL),
|
||||
'bar_level_3.selected': (curses.COLOR_GREEN, -1, curses.A_REVERSE),
|
||||
'bar_level_4': (curses.COLOR_YELLOW, -1, curses.A_NORMAL),
|
||||
'bar_level_4.selected': (curses.COLOR_YELLOW, -1, curses.A_REVERSE),
|
||||
'comment_author': (curses.COLOR_BLUE, -1, curses.A_BOLD),
|
||||
'comment_author_self': (curses.COLOR_GREEN, -1, curses.A_BOLD),
|
||||
'comment_count': (-1, -1, curses.A_NORMAL),
|
||||
'comment_text': (-1, -1, curses.A_NORMAL),
|
||||
'created': (-1, -1, curses.A_NORMAL),
|
||||
'cursor': (-1, -1, curses.A_NORMAL),
|
||||
'cursor.selected': (-1, -1, curses.A_REVERSE),
|
||||
'downvote': (curses.COLOR_RED, -1, curses.A_BOLD),
|
||||
'gold': (curses.COLOR_YELLOW, -1, curses.A_BOLD),
|
||||
'help_bar': (curses.COLOR_CYAN, -1, curses.A_BOLD | curses.A_REVERSE),
|
||||
'hidden_comment_expand': (-1, -1, curses.A_BOLD),
|
||||
'hidden_comment_text': (-1, -1, curses.A_NORMAL),
|
||||
'multireddit_name': (curses.COLOR_YELLOW, -1, curses.A_BOLD),
|
||||
'multireddit_text': (-1, -1, curses.A_NORMAL),
|
||||
'neutral_vote': (-1, -1, curses.A_BOLD),
|
||||
'notice_info': (-1, -1, curses.A_NORMAL),
|
||||
'notice_loading': (-1, -1, curses.A_NORMAL),
|
||||
'notice_error': (-1, -1, curses.A_NORMAL),
|
||||
'notice_success': (-1, -1, curses.A_NORMAL),
|
||||
'nsfw': (curses.COLOR_RED, -1, curses.A_BOLD | curses.A_REVERSE),
|
||||
'order_bar': (curses.COLOR_YELLOW, -1, curses.A_BOLD),
|
||||
'order_bar.selected': (curses.COLOR_YELLOW, -1, curses.A_BOLD | curses.A_REVERSE),
|
||||
'prompt': (curses.COLOR_CYAN, -1, curses.A_BOLD | curses.A_REVERSE),
|
||||
'saved': (curses.COLOR_GREEN, -1, curses.A_NORMAL),
|
||||
'score': (-1, -1, curses.A_NORMAL),
|
||||
'separator': (-1, -1, curses.A_BOLD),
|
||||
'stickied': (curses.COLOR_GREEN, -1, curses.A_NORMAL),
|
||||
'subscription_name': (curses.COLOR_YELLOW, -1, curses.A_BOLD),
|
||||
'subscription_text': (-1, -1, curses.A_NORMAL),
|
||||
'submission_author': (curses.COLOR_GREEN, -1, curses.A_NORMAL),
|
||||
'submission_flair': (curses.COLOR_RED, -1, curses.A_NORMAL),
|
||||
'submission_subreddit': (curses.COLOR_YELLOW, -1, curses.A_NORMAL),
|
||||
'submission_text': (-1, -1, curses.A_NORMAL),
|
||||
'submission_title': (-1, -1, curses.A_BOLD),
|
||||
'title_bar': (curses.COLOR_CYAN, -1, curses.A_BOLD | curses.A_REVERSE),
|
||||
'upvote': (curses.COLOR_GREEN, -1, curses.A_BOLD),
|
||||
'url': (curses.COLOR_BLUE, -1, curses.A_UNDERLINE),
|
||||
'url_seen': (curses.COLOR_MAGENTA, -1, curses.A_UNDERLINE),
|
||||
'user_flair': (curses.COLOR_YELLOW, -1, curses.A_BOLD)
|
||||
}
|
||||
|
||||
|
||||
class Theme(object):
|
||||
|
||||
BAR_LEVELS = ['bar_level_1', 'bar_level_2', 'bar_level_3', 'bar_level_4']
|
||||
|
||||
def __init__(self, monochrome=True):
|
||||
|
||||
self.monochrome = monochrome
|
||||
self._modifier = None
|
||||
self._elements = {}
|
||||
self._color_pairs = {}
|
||||
|
||||
def bind_curses(self):
|
||||
|
||||
if self.monochrome:
|
||||
# Skip initializing the colors and just use the attributes
|
||||
self._elements = {key: val[2] for key, val in DEFAULT_THEME.items()}
|
||||
return
|
||||
|
||||
# Shortcut for the default fg/bg
|
||||
self._color_pairs[(-1, -1)] = curses.A_NORMAL
|
||||
|
||||
for key, (fg, bg, attr) in DEFAULT_THEME.items():
|
||||
# Register the color pair for the element
|
||||
if (fg, bg) not in self._color_pairs:
|
||||
index = len(self._color_pairs) + 1
|
||||
curses.init_pair(index, fg, bg)
|
||||
self._color_pairs[(fg, bg)] = curses.color_pair(index)
|
||||
|
||||
self._elements[key] = self._color_pairs[(fg, bg)] | attr
|
||||
|
||||
def get(self, element, modifier=None):
|
||||
|
||||
modifier = modifier or self._modifier
|
||||
if modifier:
|
||||
modified_element = '{0}.{1}'.format(element, modifier)
|
||||
if modified_element in self._elements:
|
||||
return self._elements[modified_element]
|
||||
|
||||
return self._elements[element]
|
||||
|
||||
@contextmanager
|
||||
def set_modifier(self, modifier=None):
|
||||
|
||||
# This case is undefined if the context manager is nested
|
||||
assert self._modifier is None
|
||||
|
||||
self._modifier = modifier
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self._modifier = None
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
# TODO: Add a new test for this
|
||||
# assert not window.subwin.chgat.called
|
||||
|
||||
# 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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,14 +20,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, \
|
||||
|
||||
Reference in New Issue
Block a user