Squashed commit of the following:
Updated the supported python versions list.
Fixed regression in displaying xposts. #173.
Fixing a few style things.
Added a more robust test for the tornado handler.
Trying without pytest-cov
Updated travis for coverage.
Remove python 3.2 support because no unicode literals, following what praw supports.
"Side effect is not iterable."
Added requirements for travis.
Renamed travis file correctly.
Adding test configurations, got tox working.
Adding vcr cassettes to the repo.
Renamed requirements files.
Split up tests and cleaned up test names.
Tests done, still one failure.
Treat cassettes as binary to prevent bad merging.
Fixed a few broken tests.
Added a timeout to notifications.
Prepping subreddit page.
Finished submission page tests.
Working on submission tests.
Fixed vcr matching on urls with params, started submission tests.
Log cleanup.
Still trying to fix a broken test.
-Fixed a few pytest bugs and tweaked logging.
Still working on subscription tests.
Finished page tests, on to subscription page.
Finished content tests and starting page tests.
Added the test refresh-token file to gitignore.
Moved functional test file out of the repository.
Continuing work on subreddit content tests.
Tests now match module names, cassettes are split into individual tests for faster loading.
Linter fixes.
Cleanup.
Added support for nested loaders.
Added pytest options, starting subreddit content tests.
Back on track with loader, continuing content tests.
Finishing submission content tests and discovered snag with loader exception handling.
VCR up and running, continuing to implement content tests.
Playing around with vcr.py
Moved helper functions into terminal and new objects.py
Fixed a few broken tests.
Working on navigator tests.
Reorganizing some things.
Mocked webbrowser._tryorder for terminal test.
Completed oauth tests.
Progress on the oauth tests.
Working on adding fake tornado request.
Starting on OAuth tool tests.
Finished curses helpers tests.
Still working on curses helpers tests.
Almost finished with tests on curses helpers.
Adding tests and working on mocking stdscr.
Starting to add tests for curses functions.
Merge branch 'future_work' of https://github.com/michael-lazar/rtv into future_work
Refactoring controller, still in progress.
Renamed auth handler.
Rename CursesHelper to CursesBase.
Added temporary file with a possible template for func testing.
Mixup between basename and dirname.
Merge branch 'future_work' of https://github.com/michael-lazar/rtv into future_work
py3 compatability for mock.
Beginning to refactor the curses session.
Started adding tests, improved unicode handling in the config.
Cleanup, fixed a few typos.
Major refactor, almost done!.
Started a config class.
Merge branch 'master' into future_work
The editor now handles unicode characters in all situations.
Fixed a few typos from previous commits.
__main__.py formatting.
Cleaned up history logic and moved to the config file.
This commit is contained in:
516
rtv/page.py
516
rtv/page.py
@@ -1,257 +1,47 @@
|
||||
import curses
|
||||
import time
|
||||
import six
|
||||
import sys
|
||||
import logging
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
import time
|
||||
import curses
|
||||
from functools import wraps
|
||||
|
||||
import praw.errors
|
||||
import requests
|
||||
from kitchen.text.display import textual_width
|
||||
|
||||
from .helpers import open_editor
|
||||
from .curses_helpers import (Color, show_notification, show_help, prompt_input,
|
||||
add_line)
|
||||
from .docs import COMMENT_EDIT_FILE, SUBMISSION_FILE
|
||||
|
||||
__all__ = ['Navigator', 'BaseController', 'BasePage']
|
||||
_logger = logging.getLogger(__name__)
|
||||
from . import docs
|
||||
from .objects import Controller, Color
|
||||
|
||||
|
||||
class Navigator(object):
|
||||
def logged_in(f):
|
||||
"""
|
||||
Handles math behind cursor movement and screen paging.
|
||||
Decorator for Page methods that require the user to be authenticated.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
valid_page_cb,
|
||||
page_index=0,
|
||||
cursor_index=0,
|
||||
inverted=False):
|
||||
|
||||
self.page_index = page_index
|
||||
self.cursor_index = cursor_index
|
||||
self.inverted = inverted
|
||||
self._page_cb = valid_page_cb
|
||||
self._header_window = None
|
||||
self._content_window = None
|
||||
|
||||
@property
|
||||
def step(self):
|
||||
return 1 if not self.inverted else -1
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
return (self.page_index, self.cursor_index, self.inverted)
|
||||
|
||||
@property
|
||||
def absolute_index(self):
|
||||
return self.page_index + (self.step * self.cursor_index)
|
||||
|
||||
def move(self, direction, n_windows):
|
||||
"Move the cursor down (positive direction) or up (negative direction)"
|
||||
|
||||
valid, redraw = True, False
|
||||
|
||||
forward = ((direction * self.step) > 0)
|
||||
|
||||
if forward:
|
||||
if self.page_index < 0:
|
||||
if self._is_valid(0):
|
||||
# Special case - advance the page index if less than zero
|
||||
self.page_index = 0
|
||||
self.cursor_index = 0
|
||||
redraw = True
|
||||
else:
|
||||
valid = False
|
||||
else:
|
||||
self.cursor_index += 1
|
||||
if not self._is_valid(self.absolute_index):
|
||||
# Move would take us out of bounds
|
||||
self.cursor_index -= 1
|
||||
valid = False
|
||||
elif self.cursor_index >= (n_windows - 1):
|
||||
# Flip the orientation and reset the cursor
|
||||
self.flip(self.cursor_index)
|
||||
self.cursor_index = 0
|
||||
redraw = True
|
||||
else:
|
||||
if self.cursor_index > 0:
|
||||
self.cursor_index -= 1
|
||||
else:
|
||||
self.page_index -= self.step
|
||||
if self._is_valid(self.absolute_index):
|
||||
# We have reached the beginning of the page - move the
|
||||
# index
|
||||
redraw = True
|
||||
else:
|
||||
self.page_index += self.step
|
||||
valid = False # Revert
|
||||
|
||||
return valid, redraw
|
||||
|
||||
def move_page(self, direction, n_windows):
|
||||
"""
|
||||
Move page down (positive direction) or up (negative direction).
|
||||
"""
|
||||
|
||||
# top of subreddit/submission page or only one
|
||||
# submission/reply on the screen: act as normal move
|
||||
if (self.absolute_index < 0) | (n_windows == 0):
|
||||
valid, redraw = self.move(direction, n_windows)
|
||||
else:
|
||||
# first page
|
||||
if self.absolute_index < n_windows and direction < 0:
|
||||
self.page_index = -1
|
||||
self.cursor_index = 0
|
||||
self.inverted = False
|
||||
|
||||
# not submission mode: starting index is 0
|
||||
if not self._is_valid(self.absolute_index):
|
||||
self.page_index = 0
|
||||
valid = True
|
||||
else:
|
||||
# flip to the direction of movement
|
||||
if ((direction > 0) & (self.inverted is True))\
|
||||
| ((direction < 0) & (self.inverted is False)):
|
||||
self.page_index += (self.step * (n_windows-1))
|
||||
self.inverted = not self.inverted
|
||||
self.cursor_index \
|
||||
= (n_windows-(direction < 0)) - self.cursor_index
|
||||
|
||||
valid = False
|
||||
adj = 0
|
||||
# check if reached the bottom
|
||||
while not valid:
|
||||
n_move = n_windows - adj
|
||||
if n_move == 0:
|
||||
break
|
||||
|
||||
self.page_index += n_move * direction
|
||||
valid = self._is_valid(self.absolute_index)
|
||||
if not valid:
|
||||
self.page_index -= n_move * direction
|
||||
adj += 1
|
||||
|
||||
redraw = True
|
||||
|
||||
return valid, redraw
|
||||
|
||||
def flip(self, n_windows):
|
||||
"Flip the orientation of the page"
|
||||
|
||||
self.page_index += (self.step * n_windows)
|
||||
self.cursor_index = n_windows
|
||||
self.inverted = not self.inverted
|
||||
|
||||
def _is_valid(self, page_index):
|
||||
"Check if a page index will cause entries to fall outside valid range"
|
||||
|
||||
try:
|
||||
self._page_cb(page_index)
|
||||
except IndexError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
@wraps(f)
|
||||
def wrapped_method(self, *args, **kwargs):
|
||||
if not self.reddit.is_oauth_session():
|
||||
self.term.show_notification('Not logged in')
|
||||
return
|
||||
return f(self, *args, **kwargs)
|
||||
return wrapped_method
|
||||
|
||||
|
||||
class SafeCaller(object):
|
||||
|
||||
def __init__(self, window):
|
||||
self.window = window
|
||||
self.catch = True
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, e, exc_tb):
|
||||
|
||||
if self.catch:
|
||||
if isinstance(e, praw.errors.APIException):
|
||||
message = ['Error: {}'.format(e.error_type), e.message]
|
||||
show_notification(self.window, message)
|
||||
_logger.exception(e)
|
||||
return True
|
||||
elif isinstance(e, praw.errors.ClientException):
|
||||
message = ['Error: Client Exception', e.message]
|
||||
show_notification(self.window, message)
|
||||
_logger.exception(e)
|
||||
return True
|
||||
elif isinstance(e, requests.HTTPError):
|
||||
show_notification(self.window, ['Unexpected Error'])
|
||||
_logger.exception(e)
|
||||
return True
|
||||
elif isinstance(e, requests.ConnectionError):
|
||||
show_notification(self.window, ['Unexpected Error'])
|
||||
_logger.exception(e)
|
||||
return True
|
||||
class PageController(Controller):
|
||||
character_map = {}
|
||||
|
||||
|
||||
class BaseController(object):
|
||||
"""
|
||||
Event handler for triggering functions with curses keypresses.
|
||||
class Page(object):
|
||||
|
||||
Register a keystroke to a class method using the @egister decorator.
|
||||
#>>> @Controller.register('a', 'A')
|
||||
#>>> def func(self, *args)
|
||||
def __init__(self, reddit, term, config, oauth):
|
||||
|
||||
Register a default behavior by using `None`.
|
||||
#>>> @Controller.register(None)
|
||||
#>>> def default_func(self, *args)
|
||||
|
||||
Bind the controller to a class instance and trigger a key. Additional
|
||||
arguments will be passed to the function.
|
||||
#>>> controller = Controller(self)
|
||||
#>>> controller.trigger('a', *args)
|
||||
"""
|
||||
|
||||
character_map = {None: (lambda *args, **kwargs: None)}
|
||||
|
||||
def __init__(self, instance):
|
||||
self.instance = instance
|
||||
|
||||
def trigger(self, char, *args, **kwargs):
|
||||
|
||||
if isinstance(char, six.string_types) and len(char) == 1:
|
||||
char = ord(char)
|
||||
|
||||
func = self.character_map.get(char)
|
||||
if func is None:
|
||||
func = BaseController.character_map.get(char)
|
||||
if func is None:
|
||||
func = self.character_map.get(None)
|
||||
if func is None:
|
||||
func = BaseController.character_map.get(None)
|
||||
return func(self.instance, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def register(cls, *chars):
|
||||
def wrap(f):
|
||||
for char in chars:
|
||||
if isinstance(char, six.string_types) and len(char) == 1:
|
||||
cls.character_map[ord(char)] = f
|
||||
else:
|
||||
cls.character_map[char] = f
|
||||
return f
|
||||
return wrap
|
||||
|
||||
|
||||
class BasePage(object):
|
||||
"""
|
||||
Base terminal viewer incorporates a cursor to navigate content
|
||||
"""
|
||||
|
||||
MIN_HEIGHT = 10
|
||||
MIN_WIDTH = 20
|
||||
|
||||
def __init__(self, stdscr, reddit, content, oauth, **kwargs):
|
||||
|
||||
self.stdscr = stdscr
|
||||
self.reddit = reddit
|
||||
self.content = content
|
||||
self.term = term
|
||||
self.config = config
|
||||
self.oauth = oauth
|
||||
self.nav = Navigator(self.content.get, **kwargs)
|
||||
self.content = None
|
||||
self.nav = None
|
||||
self.controller = None
|
||||
|
||||
self.active = True
|
||||
self._header_window = None
|
||||
self._content_window = None
|
||||
self._subwindows = None
|
||||
@@ -260,101 +50,114 @@ class BasePage(object):
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def draw_item(window, data, inverted):
|
||||
def _draw_item(window, data, inverted):
|
||||
raise NotImplementedError
|
||||
|
||||
@BaseController.register('q')
|
||||
def loop(self):
|
||||
"""
|
||||
Main control loop runs the following steps:
|
||||
1. Re-draw the screen
|
||||
2. Wait for user to press a key (includes terminal resizing)
|
||||
3. Trigger the method registered to the input key
|
||||
|
||||
The loop will run until self.active is set to False from within one of
|
||||
the methods.
|
||||
"""
|
||||
|
||||
self.active = True
|
||||
while self.active:
|
||||
self.draw()
|
||||
ch = self.term.stdscr.getch()
|
||||
self.controller.trigger(ch)
|
||||
|
||||
@PageController.register('q')
|
||||
def exit(self):
|
||||
"""
|
||||
Prompt to exit the application.
|
||||
"""
|
||||
|
||||
ch = prompt_input(self.stdscr, "Do you really want to quit? (y/n): ")
|
||||
if ch == 'y':
|
||||
if self.term.prompt_y_or_n('Do you really want to quit? (y/n): '):
|
||||
sys.exit()
|
||||
elif ch != 'n':
|
||||
curses.flash()
|
||||
|
||||
@BaseController.register('Q')
|
||||
@PageController.register('Q')
|
||||
def force_exit(self):
|
||||
sys.exit()
|
||||
|
||||
@BaseController.register('?')
|
||||
def help(self):
|
||||
show_help(self._content_window)
|
||||
@PageController.register('?')
|
||||
def show_help(self):
|
||||
self.term.show_notification(docs.HELP.strip().splitlines())
|
||||
|
||||
@BaseController.register('1')
|
||||
@PageController.register('1')
|
||||
def sort_content_hot(self):
|
||||
self.refresh_content(order='hot')
|
||||
|
||||
@BaseController.register('2')
|
||||
@PageController.register('2')
|
||||
def sort_content_top(self):
|
||||
self.refresh_content(order='top')
|
||||
|
||||
@BaseController.register('3')
|
||||
@PageController.register('3')
|
||||
def sort_content_rising(self):
|
||||
self.refresh_content(order='rising')
|
||||
|
||||
@BaseController.register('4')
|
||||
@PageController.register('4')
|
||||
def sort_content_new(self):
|
||||
self.refresh_content(order='new')
|
||||
|
||||
@BaseController.register('5')
|
||||
@PageController.register('5')
|
||||
def sort_content_controversial(self):
|
||||
self.refresh_content(order='controversial')
|
||||
|
||||
@BaseController.register(curses.KEY_UP, 'k')
|
||||
@PageController.register(curses.KEY_UP, 'k')
|
||||
def move_cursor_up(self):
|
||||
self._move_cursor(-1)
|
||||
self.clear_input_queue()
|
||||
|
||||
@BaseController.register(curses.KEY_DOWN, 'j')
|
||||
@PageController.register(curses.KEY_DOWN, 'j')
|
||||
def move_cursor_down(self):
|
||||
self._move_cursor(1)
|
||||
self.clear_input_queue()
|
||||
|
||||
@BaseController.register('n', curses.KEY_NPAGE)
|
||||
def move_page_down(self):
|
||||
self._move_page(1)
|
||||
self.clear_input_queue()
|
||||
|
||||
@BaseController.register('m', curses.KEY_PPAGE)
|
||||
@PageController.register('m', curses.KEY_PPAGE)
|
||||
def move_page_up(self):
|
||||
self._move_page(-1)
|
||||
self.clear_input_queue()
|
||||
|
||||
@BaseController.register('a')
|
||||
@PageController.register('n', curses.KEY_NPAGE)
|
||||
def move_page_down(self):
|
||||
self._move_page(1)
|
||||
self.clear_input_queue()
|
||||
|
||||
@PageController.register('a')
|
||||
@logged_in
|
||||
def upvote(self):
|
||||
data = self.content.get(self.nav.absolute_index)
|
||||
try:
|
||||
if 'likes' not in data:
|
||||
pass
|
||||
elif data['likes']:
|
||||
if 'likes' not in data:
|
||||
self.term.flash()
|
||||
elif data['likes']:
|
||||
with self.term.loader():
|
||||
data['object'].clear_vote()
|
||||
if not self.term.loader.exception:
|
||||
data['likes'] = None
|
||||
else:
|
||||
else:
|
||||
with self.term.loader():
|
||||
data['object'].upvote()
|
||||
if not self.term.loader.exception:
|
||||
data['likes'] = True
|
||||
except praw.errors.LoginOrScopeRequired:
|
||||
show_notification(self.stdscr, ['Not logged in'])
|
||||
|
||||
@BaseController.register('z')
|
||||
@PageController.register('z')
|
||||
@logged_in
|
||||
def downvote(self):
|
||||
data = self.content.get(self.nav.absolute_index)
|
||||
try:
|
||||
if 'likes' not in data:
|
||||
pass
|
||||
elif data['likes'] or data['likes'] is None:
|
||||
if 'likes' not in data:
|
||||
self.term.flash()
|
||||
elif data['likes'] or data['likes'] is None:
|
||||
with self.term.loader():
|
||||
data['object'].downvote()
|
||||
if not self.term.loader.exception:
|
||||
data['likes'] = False
|
||||
else:
|
||||
else:
|
||||
with self.term.loader():
|
||||
data['object'].clear_vote()
|
||||
if not self.term.loader.exception:
|
||||
data['likes'] = None
|
||||
|
||||
except praw.errors.LoginOrScopeRequired:
|
||||
show_notification(self.stdscr, ['Not logged in'])
|
||||
|
||||
@BaseController.register('u')
|
||||
@PageController.register('u')
|
||||
def login(self):
|
||||
"""
|
||||
Prompt to log into the user's account, or log out of the current
|
||||
@@ -362,138 +165,105 @@ class BasePage(object):
|
||||
"""
|
||||
|
||||
if self.reddit.is_oauth_session():
|
||||
ch = prompt_input(self.stdscr, "Log out? (y/n): ")
|
||||
if ch == 'y':
|
||||
if self.term.prompt_y_or_n('Log out? (y/n): '):
|
||||
self.oauth.clear_oauth_data()
|
||||
show_notification(self.stdscr, ['Logged out'])
|
||||
elif ch != 'n':
|
||||
curses.flash()
|
||||
self.term.show_notification('Logged out')
|
||||
else:
|
||||
self.oauth.authorize()
|
||||
|
||||
@BaseController.register('d')
|
||||
def delete(self):
|
||||
@PageController.register('d')
|
||||
@logged_in
|
||||
def delete_item(self):
|
||||
"""
|
||||
Delete a submission or comment.
|
||||
"""
|
||||
|
||||
if not self.reddit.is_oauth_session():
|
||||
show_notification(self.stdscr, ['Not logged in'])
|
||||
return
|
||||
|
||||
data = self.content.get(self.nav.absolute_index)
|
||||
if data.get('author') != self.reddit.user.name:
|
||||
curses.flash()
|
||||
self.term.flash()
|
||||
return
|
||||
|
||||
prompt = 'Are you sure you want to delete this? (y/n): '
|
||||
char = prompt_input(self.stdscr, prompt)
|
||||
if char != 'y':
|
||||
show_notification(self.stdscr, ['Aborted'])
|
||||
if not self.term.prompt_y_or_n(prompt):
|
||||
self.term.show_notification('Aborted')
|
||||
return
|
||||
|
||||
with self.safe_call as s:
|
||||
with self.loader(message='Deleting', delay=0):
|
||||
data['object'].delete()
|
||||
time.sleep(2.0)
|
||||
s.catch = False
|
||||
with self.term.loader(message='Deleting', delay=0):
|
||||
data['object'].delete()
|
||||
# Give reddit time to process the request
|
||||
time.sleep(2.0)
|
||||
if self.term.loader.exception is None:
|
||||
self.refresh_content()
|
||||
|
||||
@BaseController.register('e')
|
||||
@PageController.register('e')
|
||||
@logged_in
|
||||
def edit(self):
|
||||
"""
|
||||
Edit a submission or comment.
|
||||
"""
|
||||
|
||||
if not self.reddit.is_oauth_session():
|
||||
show_notification(self.stdscr, ['Not logged in'])
|
||||
return
|
||||
|
||||
data = self.content.get(self.nav.absolute_index)
|
||||
if data.get('author') != self.reddit.user.name:
|
||||
curses.flash()
|
||||
self.term.flash()
|
||||
return
|
||||
|
||||
if data['type'] == 'Submission':
|
||||
subreddit = self.reddit.get_subreddit(self.content.name)
|
||||
content = data['text']
|
||||
info = SUBMISSION_FILE.format(content=content, name=subreddit)
|
||||
info = docs.SUBMISSION_EDIT_FILE.format(
|
||||
content=content, name=subreddit)
|
||||
elif data['type'] == 'Comment':
|
||||
content = data['body']
|
||||
info = COMMENT_EDIT_FILE.format(content=content)
|
||||
info = docs.COMMENT_EDIT_FILE.format(content=content)
|
||||
else:
|
||||
curses.flash()
|
||||
self.term.flash()
|
||||
return
|
||||
|
||||
text = open_editor(info)
|
||||
text = self.term.open_editor(info)
|
||||
if text == content:
|
||||
show_notification(self.stdscr, ['Aborted'])
|
||||
self.term.show_notification('Aborted')
|
||||
return
|
||||
|
||||
with self.safe_call as s:
|
||||
with self.loader(message='Editing', delay=0):
|
||||
data['object'].edit(text)
|
||||
time.sleep(2.0)
|
||||
s.catch = False
|
||||
with self.term.loader(message='Editing', delay=0):
|
||||
data['object'].edit(text)
|
||||
time.sleep(2.0)
|
||||
if self.term.loader.exception is None:
|
||||
self.refresh_content()
|
||||
|
||||
@BaseController.register('i')
|
||||
@PageController.register('i')
|
||||
@logged_in
|
||||
def get_inbox(self):
|
||||
"""
|
||||
Checks the inbox for unread messages and displays a notification.
|
||||
"""
|
||||
|
||||
if not self.reddit.is_oauth_session():
|
||||
show_notification(self.stdscr, ['Not logged in'])
|
||||
return
|
||||
|
||||
inbox = len(list(self.reddit.get_unread(limit=1)))
|
||||
try:
|
||||
if inbox > 0:
|
||||
show_notification(self.stdscr, ['New Messages'])
|
||||
elif inbox == 0:
|
||||
show_notification(self.stdscr, ['No New Messages'])
|
||||
except praw.errors.LoginOrScopeRequired:
|
||||
show_notification(self.stdscr, ['Not Logged In'])
|
||||
message = 'New Messages' if inbox > 0 else 'No New Messages'
|
||||
self.term.show_notification(message)
|
||||
|
||||
def clear_input_queue(self):
|
||||
"""
|
||||
Clear excessive input caused by the scroll wheel or holding down a key
|
||||
"""
|
||||
|
||||
self.stdscr.nodelay(1)
|
||||
while self.stdscr.getch() != -1:
|
||||
continue
|
||||
self.stdscr.nodelay(0)
|
||||
|
||||
@property
|
||||
def safe_call(self):
|
||||
"""
|
||||
Wrap praw calls with extended error handling.
|
||||
If a PRAW related error occurs inside of this context manager, a
|
||||
notification will be displayed on the screen instead of the entire
|
||||
application shutting down. This function will return a callback that
|
||||
can be used to check the status of the call.
|
||||
|
||||
Usage:
|
||||
#>>> with self.safe_call as s:
|
||||
#>>> self.reddit.submit(...)
|
||||
#>>> s.catch = False
|
||||
#>>> on_success()
|
||||
"""
|
||||
return SafeCaller(self.stdscr)
|
||||
with self.term.no_delay():
|
||||
while self.term.getch() != -1:
|
||||
continue
|
||||
|
||||
def draw(self):
|
||||
|
||||
n_rows, n_cols = self.stdscr.getmaxyx()
|
||||
if n_rows < self.MIN_HEIGHT or n_cols < self.MIN_WIDTH:
|
||||
window = self.term.stdscr
|
||||
n_rows, n_cols = window.getmaxyx()
|
||||
if n_rows < self.term.MIN_HEIGHT or n_cols < self.term.MIN_WIDTH:
|
||||
# TODO: Will crash when you try to navigate if the terminal is too
|
||||
# small at startup because self._subwindows will never be populated
|
||||
return
|
||||
|
||||
# Note: 2 argument form of derwin breaks PDcurses on Windows 7!
|
||||
self._header_window = self.stdscr.derwin(1, n_cols, 0, 0)
|
||||
self._content_window = self.stdscr.derwin(n_rows - 1, n_cols, 1, 0)
|
||||
self._header_window = window.derwin(1, n_cols, 0, 0)
|
||||
self._content_window = window.derwin(n_rows - 1, n_cols, 1, 0)
|
||||
|
||||
self.stdscr.erase()
|
||||
window.erase()
|
||||
self._draw_header()
|
||||
self._draw_content()
|
||||
self._add_cursor()
|
||||
@@ -503,20 +273,26 @@ class BasePage(object):
|
||||
n_rows, n_cols = self._header_window.getmaxyx()
|
||||
|
||||
self._header_window.erase()
|
||||
attr = curses.A_REVERSE | curses.A_BOLD | Color.CYAN
|
||||
self._header_window.bkgd(' ', attr)
|
||||
# curses.bkgd expects bytes in py2 and unicode in py3
|
||||
ch, attr = str(' '), curses.A_REVERSE | curses.A_BOLD | Color.CYAN
|
||||
self._header_window.bkgd(ch, attr)
|
||||
|
||||
sub_name = self.content.name.replace('/r/front', 'Front Page')
|
||||
add_line(self._header_window, sub_name, 0, 0)
|
||||
self.term.add_line(self._header_window, sub_name, 0, 0)
|
||||
if self.content.order is not None:
|
||||
add_line(self._header_window, ' [{}]'.format(self.content.order))
|
||||
order = ' [{}]'.format(self.content.order)
|
||||
self.term.add_line(self._header_window, order)
|
||||
|
||||
if 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
|
||||
|
||||
username = self.reddit.user.name
|
||||
s_col = (n_cols - textual_width(username) - 1)
|
||||
s_col = (n_cols - width(username) - 1)
|
||||
# Only print username if it fits in the empty space on the right
|
||||
if (s_col - 1) >= textual_width(sub_name):
|
||||
add_line(self._header_window, username, 0, s_col)
|
||||
if (s_col - 1) >= width(sub_name):
|
||||
self.term.add_line(self._header_window, username, 0, s_col)
|
||||
|
||||
self._header_window.refresh()
|
||||
|
||||
@@ -543,7 +319,7 @@ class BasePage(object):
|
||||
start = current_row - window_rows if inverted else current_row
|
||||
subwindow = self._content_window.derwin(
|
||||
window_rows, window_cols, start, data['offset'])
|
||||
attr = self.draw_item(subwindow, data, inverted)
|
||||
attr = self._draw_item(subwindow, data, inverted)
|
||||
self._subwindows.append((subwindow, attr))
|
||||
available_rows -= (window_rows + 1) # Add one for the blank line
|
||||
current_row += step * (window_rows + 1)
|
||||
@@ -571,7 +347,7 @@ class BasePage(object):
|
||||
self._remove_cursor()
|
||||
valid, redraw = self.nav.move(direction, len(self._subwindows))
|
||||
if not valid:
|
||||
curses.flash()
|
||||
self.term.flash()
|
||||
|
||||
# Note: ACS_VLINE doesn't like changing the attribute,
|
||||
# so always redraw.
|
||||
@@ -582,14 +358,14 @@ class BasePage(object):
|
||||
self._remove_cursor()
|
||||
valid, redraw = self.nav.move_page(direction, len(self._subwindows)-1)
|
||||
if not valid:
|
||||
curses.flash()
|
||||
self.term.flash()
|
||||
|
||||
# Note: ACS_VLINE doesn't like changing the attribute,
|
||||
# so always redraw.
|
||||
self._draw_content()
|
||||
self._add_cursor()
|
||||
|
||||
def _edit_cursor(self, attribute=None):
|
||||
def _edit_cursor(self, attribute):
|
||||
|
||||
# Don't allow the cursor to go below page index 0
|
||||
if self.nav.absolute_index < 0:
|
||||
@@ -599,7 +375,7 @@ class BasePage(object):
|
||||
# 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
|
||||
self.nav.cursor_index = len(self._subwindows) - 1
|
||||
|
||||
window, attr = self._subwindows[self.nav.cursor_index]
|
||||
if attr is not None:
|
||||
@@ -609,4 +385,4 @@ class BasePage(object):
|
||||
for row in range(n_rows):
|
||||
window.chgat(row, 0, 1, attribute)
|
||||
|
||||
window.refresh()
|
||||
window.refresh()
|
||||
Reference in New Issue
Block a user