Merge remote-tracking branch 'upstream/master'

This commit is contained in:
David Foucher
2016-03-15 00:25:34 +01:00
26 changed files with 3533 additions and 1573 deletions

View File

@@ -2,6 +2,7 @@
RTV Changelog
=============
.. _1.8.1: http://github.com/michael-lazar/rtv/releases/tag/v1.8.1
.. _1.8.0: http://github.com/michael-lazar/rtv/releases/tag/v1.8.0
.. _1.7.0: http://github.com/michael-lazar/rtv/releases/tag/v1.7.0
.. _1.6.1: http://github.com/michael-lazar/rtv/releases/tag/v1.6.1
@@ -15,10 +16,27 @@ RTV Changelog
.. _1.2.1: http://github.com/michael-lazar/rtv/releases/tag/v1.2.1
.. _1.2: http://github.com/michael-lazar/rtv/releases/tag/v1.2
-------------------
1.8.1_ (2016-03-01)
-------------------
Features
------------------
1.8.0_ (2015-12-20
------------------
* All keys are now rebindable through the config.
* New bindings - ctrl-d and ctrl-u for page up / page down.
* Added tag for stickied posts and comments.
* Added bullet between timestamp and comment count.
Bugfixes
* Links starting with np.reddit.com no longer return `Forbidden`.
Documentation
* Updated README.
-------------------
1.8.0_ (2015-12-20)
-------------------
Features
* A banner on the top of the page now displays the selected page sort order.

64
CONTROLS.rst Normal file
View File

@@ -0,0 +1,64 @@
========
Controls
========
.. image:: http://i.imgur.com/xDUQ03C.png
--------------
Basic Commands
--------------
:``j``/``k`` or ``▲``/``▼``: Move the cursor up/down
:``m``/``n`` or ``PgUp``/``PgDn``: Jump to the previous/next page
:``1-5``: Toggle post order (*hot*, *top*, *rising*, *new*, *controversial*)
:``r`` or ``F5``: Refresh page content
:``u``: Log in or switch accounts
:``?``: Show the help screen
:``q``/``Q``: Quit/Force quit
----------------------
Authenticated Commands
----------------------
Some actions require that you be logged in to your reddit account.
You can log in by pressing ``u`` while inside of the program.
Once you are logged in your username will appear in the top-right corner of the screen.
:``a``/``z``: Upvote/downvote
:``c``: Compose a new post or comment
:``e``: Edit an existing post or comment
:``d``: Delete an existing post or comment
:``i``: Display new messages prompt
:``s``: View a list of subscribed subreddits
:``w``: Save a submission
--------------
Subreddit Mode
--------------
In subreddit mode you can browse through the top submissions on either the front page or a specific subreddit.
:``l`` or ``►``: Enter the selected submission
:``o`` or ``ENTER``: Open the submission link with your web browser
:``/``: Open a prompt to switch subreddits
:``f``: Open a prompt to search the current subreddit
The ``/`` prompt accepts subreddits in the following formats
* ``/r/python``
* ``/r/python/new``
* ``/r/python+linux`` supports multireddits
* ``/r/front`` will redirect to the front page
* ``/r/me`` will display your submissions
* ``/r/saved`` will display your saved submission
---------------
Submission Mode
---------------
In submission mode you can view the self text for a submission and browse comments.
:``h`` or ``◄``: Return to the subreddit
:``l`` or ``►``: Open the selected comment in a new window
:``o`` or ``ENTER``: Open the comment permalink with your web browser
:``SPACE``: Fold the selected comment, or load additional comments

View File

@@ -2,14 +2,14 @@
RTV: Reddit Terminal Viewer
===========================
RTV is an application that allows you to view and interact with reddit from your terminal.
It is compatible with *most* terminal emulators on Linux and OSX.
| RTV allows you to view and interact with reddit from your terminal.
| It's compatible with *most* terminal emulators on Linux and OSX.
.. image:: http://i.imgur.com/Ek13lqM.png
RTV is built in **python** using the **curses** library.
`DEMO <https://asciinema.org/a/31609?speed=2&autoplay=1>`_
`Demo <https://asciinema.org/a/31609?speed=2&autoplay=1>`_
RTV is built in **python** using the **curses** library.
---------------
@@ -19,174 +19,113 @@ RTV is built in **python** using the **curses** library.
* `Installation`_
* `Usage`_
* `Configuration`_
* `Settings`_
* `FAQ`_
* `Changelog`_
* `Contributors`_
* `License`_
============
Installation
============
Install using pip
Install using pip...
.. code-block:: bash
$ sudo pip install rtv
$ pip install rtv
Or clone the repository
or clone the repository.
.. code-block:: bash
$ git clone https://github.com/michael-lazar/rtv.git
$ cd rtv
$ sudo python3 setup.py install
The installation will place a script in the system path
.. code-block:: bash
$ rtv
$ rtv --help
See the `FAQ`_ to troubleshoot common installation problems
$ python3 setup.py install
=====
Usage
=====
RTV supports browsing both subreddits and submission comments.
Navigating is simple and intuitive.
Move the cursor using either the arrow keys or *Vim* style movement.
Move **up** and **down** to scroll through the page.
Move **right** to view the selected submission, and **left** to exit the submission.
--------------
Basic Commands
--------------
:``j``/``k`` or ``▲``/``▼``: Move the cursor up/down
:``m``/``n`` or ``PgUp``/``PgDn``: Jump to the previous/next page
:``1-5``: Toggle post order (*hot*, *top*, *rising*, *new*, *controversial*)
:``r`` or ``F5``: Refresh page content
:``u``: Log in or switch accounts
:``?``: Show the help screen
:``q``/``Q``: Quit/Force quit
----------------------
Authenticated Commands
----------------------
Some actions require that you be logged in to your reddit account.
You can log in by pressing ``u`` while inside of the program.
Once you are logged in your username will appear in the top-right corner of the screen.
:``a``/``z``: Upvote/downvote
:``c``: Compose a new post or comment
:``e``: Edit an existing post or comment
:``d``: Delete an existing post or comment
:``i``: Display new messages prompt
:``s``: View a list of subscribed subreddits
:``w``: View a list of your saved posts
--------------
Subreddit Mode
--------------
In subreddit mode you can browse through the top submissions on either the front page or a specific subreddit.
:``l`` or ``►``: Enter the selected submission
:``o`` or ``ENTER``: Open the submission link with your web browser
:``/``: Open a prompt to switch subreddits
:``f``: Open a prompt to search the current subreddit
The ``/`` prompt accepts subreddits in the following formats
* ``/r/python``
* ``/r/python/new``
* ``/r/python+linux`` supports multireddits
* ``/r/front`` will redirect to the front page
* ``/r/me`` will display your submissions
* ``/r/saved`` will display your saved posts/comments
---------------
Submission Mode
---------------
In submission mode you can view the self text for a submission and browse comments.
:``h`` or ``◄``: Return to the subreddit
:``o`` or ``ENTER``: Open the comment permalink with your web browser
:``SPACE``: Fold the selected comment, or load additional comments
=======
Key Map
=======
.. image:: http://i.imgur.com/xDUQ03C.png
=============
Configuration
=============
------
Editor
------
RTV allows users to compose comments and replies using their preferred text editor (**vi**, **nano**, **gedit**, etc).
You can specify which text editor you would like to use by setting the ``$RTV_EDITOR`` environment variable.
To run the program, type
.. code-block:: bash
$ export RTV_EDITOR=gedit
$ rtv --help
If no editor is specified, RTV will fallback to the system's default ``$EDITOR``, and finally to ``nano``.
--------
Controls
--------
-----------
Web Browser
-----------
Move the cursor using either the arrow keys or *Vim* style movement
RTV has the capability to open links inside of your web browser.
By default RTV will use the system's browser.
On most systems this corresponds to a graphical browser such as Firefox or Chrome.
If you prefer to stay in the terminal, use ``$BROWSER`` to specify a console-based web browser.
`w3m <http://w3m.sourceforge.net/>`_, `lynx <http://lynx.isc.org/>`_, and `elinks <http://elinks.or.cz/>`_ are all good choices.
- Press **up** and **down** to scroll through submissions.
- Press **right** to view the selected submission and **left** to return.
- Press **?** to open the help screen.
.. code-block:: bash
$ export BROWSER=w3m
See `CONTROLS.rst <https://github.com/michael-lazar/rtv/blob/master/CONTROLS.rst>`_ for the complete list of available commands.
--------------
Authentication
--------------
RTV uses OAuth to facilitate logging into your reddit user account [#]_. The login process follows these steps:
RTV enables you to login to your reddit account in order to perform actions like voting and leave comments.
The login process uses OAuth [#]_ and follows these steps:
1. You initiate a login by pressing the ``u`` key.
2. You're redirected to a webbrowser where reddit will ask you to login and authorize RTV.
3. RTV uses the generated token to login on your behalf.
4. The token is stored on your computer at ``{HOME}/.config/rtv/refresh-token`` for future sessions. You can disable this behavior by setting ``persistent=False`` in your RTV config.
1. Initiate a login by pressing the ``u`` key.
2. Open a new webpage where reddit will ask you to authorize the application.
3. Click **Accept**.
Note that RTV no longer allows you to input your username/password directly. This method of cookie based authentication has been deprecated by reddit and will not be supported in future releases [#]_.
RTV will retrieve an auth token with your information and store it locally in ``{HOME}/.config/rtv/refresh-token``.
You can disable storing the token by setting ``persistent=False`` in the config.
Note that RTV no longer allows you to input your username/password directly. This method of cookie based authentication has been deprecated by reddit [#]_.
.. [#] `<https://github.com/reddit/reddit/wiki/OAuth2>`_
.. [#] `<https://www.reddit.com/r/redditdev/comments/2ujhkr/important_api_licensing_terms_clarified/>`_
-----------
Config File
-----------
========
Settings
========
RTV stores configuration settings in ``{HOME}/.config/rtv/rtv.cfg``.
You can auto-generate the config file by running
-------------
Configuration
-------------
Configuration settings are stored in ``{HOME}/.config/rtv/rtv.cfg``.
Auto-generate the config file by running
.. code-block:: bash
$ rtv --copy-config
See the `default config <https://github.com/michael-lazar/rtv/blob/master/rtv/rtv.cfg>`_ to view descriptions for each setting.
See the `default config <https://github.com/michael-lazar/rtv/blob/master/rtv/rtv.cfg>`_ for the full list of settings.
------
Editor
------
You can compose posts and reply to comments using your preferred text editor.
Set the editor by changing ``$RTV_EDITOR`` in your environment.
.. code-block:: bash
$ export RTV_EDITOR=gedit
If not specified, the default system ``$EDITOR`` (or *nano*) will be used.
-----------
Web Browser
-----------
You can open submission links using your web browser.
On most systems the default web browser will open in a new window.
If you prefer the complete terminal experience, set ``$BROWSER`` to a console-based web browser.
.. code-block:: bash
$ export BROWSER=w3m
`w3m <http://w3m.sourceforge.net/>`_, `lynx <http://lynx.isc.org/>`_, and `elinks <http://elinks.or.cz/>`_ are all good choices.
===
FAQ
@@ -214,25 +153,39 @@ How do I run the repository code directly?
.. code-block:: bash
$ cd ~/rtv_project
$ python3 -m pip install -r requirements.py3.txt
$ python3 -m rtv
How do I run the tests?
This project uses `pytest <http://pytest.org/>`_ and `VCR.py <https://vcrpy.readthedocs.org/>`_.
.. code-block:: bash
$ pip3 install pytest
$ # The pip release for VCR.py is out-of-date
$ pip3 install git+https://github.com/kevin1024/vcrpy.git
$ cd ~/rtv_project
$ # Run the full suite
$ PYTHONPATH=. py.test
$ # or a single test
$ PYTHONPATH=. py.test tests/test_config.py::test_copy_default_config
VCR.py will record HTTP requests made during the test run and store
them in *tests/cassettes/*. By default these cassettes are read-only,
if you would like to record new cassettes you must provide your own refresh token.
.. code-block:: bash
$ PYTHONPATH=. py.test --record-mode=once --refresh-token=~/.config/rtv/refresh-token
=========
Changelog
=========
Please see `CHANGELOG.rst <https://github.com/michael-lazar/rtv/blob/master/CHANGELOG.rst>`_.
============
Contributors
============
Please see `CONTRIBUTORS.rst <https://github.com/michael-lazar/rtv/blob/master/CONTRIBUTORS.rst>`_.
=======
License
=======
Please see `LICENSE <https://github.com/michael-lazar/rtv/blob/master/LICENSE>`_.
This project is distributed under the `MIT <https://github.com/michael-lazar/rtv/blob/master/LICENSE>`_ license.
.. |python| image:: https://img.shields.io/badge/python-2.7%2C%203.5-blue.svg

12
rtv.1
View File

@@ -1,4 +1,4 @@
.TH "RTV" "1" "December 20, 2015" "Version 1.8.0" "Usage and Commands"
.TH "RTV" "1" "March 02, 2016" "Version 1.8.1" "Usage and Commands"
.SH NAME
RTV - Reddit Terminal Viewer
.SH SYNOPSIS
@@ -49,8 +49,10 @@ Copy the default configuration to {HOME}/.config/rtv/rtv.cfg
.SH CONTROLS
Navigate between posts by using the arrow keys or vim-style `hjkl` movement.
You can view the full list of commands by pressing the \fB?\fR key inside of the program.
Move the cursor using either the arrow keys or Vim-style movement.
- Press \fBup\fR and \fBdown\fR to scroll through submissions.
- Press \fBright\fR to view the selected submission and \fBleft\fR to return.
- Press \fB?\fR to open the help screen.
.SH FILES
.TP
.BR $XDG_CONFIG_HOME/rtv/rtv.cfg
@@ -72,10 +74,8 @@ Specifies which webbrowser RTV will attempt to use when opening links.
This can be set to a terminal browser (w3m, lynx, elinks, etc.) for a true
terminal experience. RTV will fallback to the system's default browser.
.SH AUTHOR
Man page written by Johnathan "ShaggyTwoDope" Jenkins <twodopeshaggy@gmail.com> (2015).
Michael Lazar <lazar.michael22@gmail.com> (2016).
.SH BUGS
Report bugs to \fIhttps://github.com/michael-lazar/rtv/issues\fR
.SH LICENSE
The MIT License (MIT)
.PP
(c) 2015 Michael Lazar

View File

@@ -14,6 +14,7 @@ from .oauth import OAuthHelper
from .terminal import Terminal
from .objects import curses_session
from .subreddit import SubredditPage
from .exceptions import ConfigError
from .__version__ import __version__
@@ -39,13 +40,17 @@ def main():
sys.stdout.write('\x1b]2;{0}\x07'.format(title))
args = Config.get_args()
fargs = Config.get_file(args.get('config'))
fargs, bindings = Config.get_file(args.get('config'))
# Apply the file config first, then overwrite with any command line args
config = Config()
config.update(**fargs)
config.update(**args)
# If key bindings are supplied in the config file, overwrite the defaults
if bindings:
config.keymap.set_bindings(bindings)
# Copy the default config file and quit
if config['copy_config']:
copy_default_config()
@@ -60,6 +65,14 @@ def main():
config.delete_refresh_token()
if config['log']:
# Log request headers to the file (print hack only works on python 3.x)
# from http import client
# _http_logger = logging.getLogger('http.client')
# client.HTTPConnection.debuglevel = 2
# def print_to_file(*args, **_):
# if args[0] != "header:":
# _http_logger.info(' '.join(args))
# client.print = print_to_file
logging.basicConfig(level=logging.DEBUG, filename=config['log'])
else:
# Add an empty handler so the logger doesn't complain
@@ -94,6 +107,9 @@ def main():
# Launch the subreddit page
page.loop()
except ConfigError as e:
_logger.exception(e)
print(e)
except Exception as e:
_logger.exception(e)
raise

View File

@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
__version__ = '1.8.0'
__version__ = '1.8.1'

View File

@@ -11,6 +11,7 @@ import six
from six.moves import configparser
from . import docs, __version__
from .objects import KeyMap
PACKAGE = os.path.dirname(__file__)
HOME = os.path.expanduser('~')
@@ -26,7 +27,7 @@ def build_parser():
parser = argparse.ArgumentParser(
prog='rtv', description=docs.SUMMARY,
epilog=docs.CONTROLS+docs.HELP,
epilog=docs.CONTROLS,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument(
'-V', '--version', action='version', version='rtv '+__version__)
@@ -112,7 +113,10 @@ class Config(object):
self.history_file = history_file
self.token_file = token_file
self.config = kwargs
self.default = self.get_file(DEFAULT_CONFIG)
default, bindings = self.get_file(DEFAULT_CONFIG)
self.default = default
self.keymap = KeyMap(bindings)
# `refresh_token` and `history` are saved/loaded at separate locations,
# so they are treated differently from the rest of the config options.
@@ -198,9 +202,9 @@ class Config(object):
@staticmethod
def _parse_rtv_file(config):
out = {}
rtv = {}
if config.has_section('rtv'):
out = dict(config.items('rtv'))
rtv = dict(config.items('rtv'))
params = {
'ascii': partial(config.getboolean, 'rtv'),
@@ -208,13 +212,20 @@ class Config(object):
'persistent': partial(config.getboolean, 'rtv'),
'history_size': partial(config.getint, 'rtv'),
'oauth_redirect_port': partial(config.getint, 'rtv'),
'oauth_scope': lambda x: out[x].split(',')
'oauth_scope': lambda x: rtv[x].split(',')
}
for key, func in params.items():
if key in out:
out[key] = func(key)
return out
if key in rtv:
rtv[key] = func(key)
bindings = {}
if config.has_section('bindings'):
bindings = dict(config.items('bindings'))
for name, keys in bindings.items():
bindings[name] = [key.strip() for key in keys.split(',')]
return rtv, bindings
@staticmethod
def _ensure_filepath(filename):

View File

@@ -257,7 +257,10 @@ class SubmissionContent(Content):
def from_url(cls, reddit, url, loader, indent_size=2, max_indent_level=8,
order=None):
url = url.replace('http:', 'https:')
url = url.replace('http:', 'https:') # Reddit forces SSL
# Sometimes reddit will return a 403 FORBIDDEN when trying to access an
# np link while using OAUTH. Cause is unknown.
url = url.replace('https://np.', 'https://www.')
submission = reddit.get_submission(url, comment_sort=order)
return cls(submission, loader, indent_size, max_indent_level, order)

View File

@@ -12,17 +12,12 @@ terminal window.
"""
CONTROLS = """
Controls
--------
RTV currently supports browsing both subreddits and individual submissions.
In each mode the controls are slightly different. In subreddit mode you can
browse through the top submissions on either the front page or a specific
subreddit. In submission mode you can view the self text for a submission and
browse comments.
Move the cursor using either the arrow keys or *Vim* style movement.
Press `?` to open the help screen.
"""
HELP = """
Basic Commands
[Basic Commands]
`j/k` or `UP/DOWN` : Move the cursor up/down
`m/n` or `PgUp/PgDn`: Jump to the previous/next page
`o` or `ENTER` : Open the selected item as a webpage
@@ -32,7 +27,7 @@ Basic Commands
`?` : Show the help screen
`q/Q` : Quit/Force quit
Authenticated Commands
[Authenticated Commands]
`a/z` : Upvote/downvote
`w` : Save/unsave a post
`c` : Compose a new post or comment
@@ -41,13 +36,14 @@ Authenticated Commands
`i` : Display new messages prompt
`s` : Open/close subscribed subreddits list
Subreddit Mode
[Subreddit Mode]
`l` or `RIGHT` : Enter the selected submission
`/` : Open a prompt to switch subreddits
`f` : Open a prompt to search the current subreddit
Submission Mode
[Submission Mode]
`h` or `LEFT` : Return to subreddit mode
`l` or `RIGHT` : Open the selected comment in a new window
`SPACE` : Fold the selected comment, or load additional comments
"""

View File

@@ -6,6 +6,10 @@ class EscapeInterrupt(Exception):
"Signal that the ESC key has been pressed"
class ConfigError(Exception):
"There was a problem with the configuration"
class RTVError(Exception):
"Base RTV error class"

View File

@@ -1,14 +1,16 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
import os
import time
import curses
import signal
import inspect
import weakref
import logging
import threading
import curses
import curses.ascii
from contextlib import contextmanager
import six
@@ -502,6 +504,11 @@ class Controller(object):
>>> def func(self, *args)
>>> ...
Register a KeyBinding that can be defined later by the config file
>>> @Controller.register(Command("UPVOTE"))
>>> def upvote(self, *args)
>> ...
Register a default behavior by using `None`.
>>> @Controller.register(None)
>>> def default_func(self, *args)
@@ -515,13 +522,34 @@ class Controller(object):
character_map = {}
def __init__(self, instance):
def __init__(self, instance, keymap=None):
self.instance = instance
# Build a list of parent controllers that follow the object's MRO to
# check if any parent controllers have registered the keypress
# Build a list of parent controllers that follow the object's MRO
# to check if any parent controllers have registered the keypress
self.parents = inspect.getmro(type(self))[:-1]
if not keymap:
return
# Go through the controller and all of it's parents and look for
# Command objects in the character map. Use the keymap the lookup the
# keys associated with those command objects and add them to the
# character map.
for controller in self.parents:
for command, func in controller.character_map.copy().items():
if isinstance(command, Command):
for key in keymap.get(command):
val = keymap.parse(key)
# Check if the key is already programmed to trigger a
# different function.
if controller.character_map.get(val, func) != func:
raise exceptions.ConfigError(
"Invalid configuration! `%s` is bound to "
"duplicate commands in the "
"%s" % (key, controller.__name__))
controller.character_map[val] = func
def trigger(self, char, *args, **kwargs):
if isinstance(char, six.string_types) and len(char) == 1:
@@ -552,3 +580,89 @@ class Controller(object):
cls.character_map[char] = f
return f
return inner
class Command(object):
"""
Minimal class that should be used to wrap abstract commands that may be
implemented as one or more physical keystrokes.
E.g. Command("REFRESH") can be represented by the KeyMap to be triggered
by either `r` or `F5`
"""
def __init__(self, val):
self.val = val.upper()
def __repr__(self):
return 'Command(%s)' % self.val
def __eq__(self, other):
return repr(self) == repr(other)
def __ne__(self, other):
return not self == other
def __hash__(self):
return hash(repr(self))
class KeyMap(object):
"""
Mapping between commands and the keys that they represent.
"""
def __init__(self, bindings):
self._keymap = None
self.set_bindings(bindings)
def set_bindings(self, bindings):
# Clear the keymap before applying the bindings to avoid confusion.
# If a user defines custom bindings in their config file, they must
# explicitly define ALL of the bindings.
self._keymap = {}
for command, keys in bindings.items():
if not isinstance(command, Command):
command = Command(command)
self._keymap[command] = keys
def get(self, command):
if not isinstance(command, Command):
command = Command(command)
try:
return self._keymap[command]
except KeyError:
raise exceptions.ConfigError('Invalid configuration! `%s` key is '
'undefined' % command.val)
@staticmethod
def parse(key):
"""
Parse a key represented by a string and return its character code.
"""
try:
if isinstance(key, int):
return key
elif re.match('[<]KEY_.*[>]', key):
# Curses control character
return getattr(curses, key[1:-1])
elif re.match('[<].*[>]', key):
# Ascii control character
return getattr(curses.ascii, key[1:-1])
elif key.startswith('0x'):
# Ascii hex code
return int(key, 16)
else:
# Ascii character
code = ord(key)
if 0 <= code <= 255:
return code
# Python 3.3 has a curses.get_wch() function that we can use
# for unicode keys, but Python 2.7 is limited to ascii.
raise exceptions.ConfigError('Invalid configuration! `%s` is '
'not in the ascii range' % key)
except (AttributeError, ValueError, TypeError):
raise exceptions.ConfigError('Invalid configuration! "%s" is not a '
'valid key' % key)

View File

@@ -9,7 +9,7 @@ from functools import wraps
from kitchen.text.display import textual_width
from . import docs
from .objects import Controller, Color
from .objects import Controller, Color, Command
def logged_in(f):
@@ -68,60 +68,60 @@ class Page(object):
ch = self.term.stdscr.getch()
self.controller.trigger(ch)
@PageController.register('q')
@PageController.register(Command('EXIT'))
def exit(self):
if self.term.prompt_y_or_n('Do you really want to quit? (y/n): '):
sys.exit()
@PageController.register('Q')
@PageController.register(Command('FORCE_EXIT'))
def force_exit(self):
sys.exit()
@PageController.register('?')
@PageController.register(Command('HELP'))
def show_help(self):
self.term.show_notification(docs.HELP.strip().splitlines())
self.term.show_notification(docs.HELP.strip('\n').splitlines())
@PageController.register('1')
@PageController.register(Command('SORT_HOT'))
def sort_content_hot(self):
self.refresh_content(order='hot')
@PageController.register('2')
@PageController.register(Command('SORT_TOP'))
def sort_content_top(self):
self.refresh_content(order='top')
@PageController.register('3')
@PageController.register(Command('SORT_RISING'))
def sort_content_rising(self):
self.refresh_content(order='rising')
@PageController.register('4')
@PageController.register(Command('SORT_NEW'))
def sort_content_new(self):
self.refresh_content(order='new')
@PageController.register('5')
@PageController.register(Command('SORT_CONTROVERSIAL'))
def sort_content_controversial(self):
self.refresh_content(order='controversial')
@PageController.register(curses.KEY_UP, 'k')
@PageController.register(Command('MOVE_UP'))
def move_cursor_up(self):
self._move_cursor(-1)
self.clear_input_queue()
@PageController.register(curses.KEY_DOWN, 'j')
@PageController.register(Command('MOVE_DOWN'))
def move_cursor_down(self):
self._move_cursor(1)
self.clear_input_queue()
@PageController.register('m', curses.KEY_PPAGE)
@PageController.register(Command('PAGE_UP'))
def move_page_up(self):
self._move_page(-1)
self.clear_input_queue()
@PageController.register('n', curses.KEY_NPAGE)
@PageController.register(Command('PAGE_DOWN'))
def move_page_down(self):
self._move_page(1)
self.clear_input_queue()
@PageController.register('a')
@PageController.register(Command('UPVOTE'))
@logged_in
def upvote(self):
data = self.content.get(self.nav.absolute_index)
@@ -138,7 +138,7 @@ class Page(object):
if not self.term.loader.exception:
data['likes'] = True
@PageController.register('z')
@PageController.register(Command('DOWNVOTE'))
@logged_in
def downvote(self):
data = self.content.get(self.nav.absolute_index)
@@ -155,7 +155,7 @@ class Page(object):
if not self.term.loader.exception:
data['likes'] = None
@PageController.register('w')
@PageController.register(Command('SAVE'))
@logged_in
def save(self):
data = self.content.get(self.nav.absolute_index)
@@ -172,7 +172,7 @@ class Page(object):
if not self.term.loader.exception:
data['saved'] = False
@PageController.register('u')
@PageController.register(Command('LOGIN'))
def login(self):
"""
Prompt to log into the user's account, or log out of the current
@@ -186,7 +186,7 @@ class Page(object):
else:
self.oauth.authorize()
@PageController.register('d')
@PageController.register(Command('DELETE'))
@logged_in
def delete_item(self):
"""
@@ -210,7 +210,7 @@ class Page(object):
if self.term.loader.exception is None:
self.refresh_content()
@PageController.register('e')
@PageController.register(Command('EDIT'))
@logged_in
def edit(self):
"""
@@ -245,7 +245,7 @@ class Page(object):
if self.term.loader.exception is None:
self.refresh_content()
@PageController.register('i')
@PageController.register(Command('INBOX'))
@logged_in
def get_inbox(self):
"""

View File

@@ -50,3 +50,75 @@ oauth_redirect_port = 65000
; Access permissions that will be requested.
oauth_scope = edit,history,identity,mysubreddits,privatemessages,read,report,save,submit,subscribe,vote
[bindings]
##############
# Key Bindings
##############
; If you would like to define custom bindings, copy this section into your
; config file with the [bindings] heading. All commands must be bound to at
; least one key for the config to be valid.
;
; 1.) Plain keys can be represented by either uppercase/lowercase characters
; or the hexadecimal numbers referring their ascii codes. For reference, see
; https://en.wikipedia.org/wiki/ASCII#ASCII_printable_code_chart
; e.g. Q, q, 1, ?
; e.g. 0x20 (space), 0x3c (less-than sign)
;
; 2.) Special ascii control codes should be surrounded with <>. For reference,
; see https://en.wikipedia.org/wiki/ASCII#ASCII_control_code_chart
; e.g. <LF> (enter), <ESC> (escape)
;
; 3.) Other special keys are defined by curses, they should be surrounded by <>
; and prefixed with KEY_. For reference, see
; https://docs.python.org/2/library/curses.html#constants
; e.g. <KEY_LEFT> (left arrow), <KEY_F5>, <KEY_NPAGE> (page down)
;
; Notes:
; - Curses <KEY_ENTER> is unreliable and should always be used in conjunction
; with <LF>.
; - Use 0x20 for the space key.
; - A subset of Ctrl modifiers are available through the ascii control codes.
; For example, Ctrl-D will trigger an <EOT> signal. See the table above for
; a complete reference.
; Base page
EXIT = q
FORCE_EXIT = Q
HELP = ?
SORT_HOT = 1
SORT_TOP = 2
SORT_RISING = 3
SORT_NEW = 4
SORT_CONTROVERSIAL = 5
MOVE_UP = k, <KEY_UP>
MOVE_DOWN = j, <KEY_DOWN>
PAGE_UP = m, <KEY_PPAGE>, <NAK>
PAGE_DOWN = n, <KEY_NPAGE>, <EOT>
UPVOTE = a
DOWNVOTE = z
LOGIN = u
DELETE = d
EDIT = e
INBOX = i
REFRESH = r, <KEY_F5>
SAVE = w
; Submission page
SUBMISSION_TOGGLE_COMMENT = 0x20
SUBMISSION_OPEN_IN_BROWSER = o, <LF>, <KEY_ENTER>
SUBMISSION_POST = c
SUBMISSION_EXIT = h, <KEY_LEFT>
SUBMISSION_OPEN_IN_PAGER = l, <KEY_RIGHT>
; Subreddit page
SUBREDDIT_SEARCH = f
SUBREDDIT_PROMPT = /
SUBREDDIT_POST = c
SUBREDDIT_OPEN = l, <KEY_RIGHT>
SUBREDDIT_OPEN_IN_BROWSER = o, <LF>, <KEY_ENTER>, <KEY_ENTER>
SUBREDDIT_OPEN_SUBSCRIPTIONS = s
; Subscription page
SUBSCRIPTION_SELECT = l, <LF>, <KEY_ENTER>, <KEY_RIGHT>
SUBSCRIPTION_EXIT = h, s, <ESC>, <KEY_LEFT>

View File

@@ -7,8 +7,7 @@ import curses
from . import docs
from .content import SubmissionContent
from .page import Page, PageController, logged_in
from .objects import Navigator, Color
from .terminal import Terminal
from .objects import Navigator, Color, Command
class SubmissionController(PageController):
@@ -20,15 +19,15 @@ class SubmissionPage(Page):
def __init__(self, reddit, term, config, oauth, url=None, submission=None):
super(SubmissionPage, self).__init__(reddit, term, config, oauth)
self.controller = SubmissionController(self, keymap=config.keymap)
if url:
self.content = SubmissionContent.from_url(reddit, url, term.loader)
else:
self.content = SubmissionContent(submission, term.loader)
self.controller = SubmissionController(self)
# Start at the submission post, which is indexed as -1
self.nav = Navigator(self.content.get, page_index=-1)
@SubmissionController.register(curses.KEY_RIGHT, 'l', ' ')
@SubmissionController.register(Command('SUBMISSION_TOGGLE_COMMENT'))
def toggle_comment(self):
"Toggle the selected comment tree between visible and hidden"
@@ -40,13 +39,13 @@ class SubmissionPage(Page):
# causes the cursor index to go out of bounds.
self.nav.page_index, self.nav.cursor_index = current_index, 0
@SubmissionController.register(curses.KEY_LEFT, 'h')
@SubmissionController.register(Command('SUBMISSION_EXIT'))
def exit_submission(self):
"Close the submission and return to the subreddit page"
self.active = False
@SubmissionController.register(curses.KEY_F5, 'r')
@SubmissionController.register(Command('REFRESH'))
def refresh_content(self, order=None, name=None):
"Re-download comments and reset the page index"
@@ -59,7 +58,7 @@ class SubmissionPage(Page):
if not self.term.loader.exception:
self.nav = Navigator(self.content.get, page_index=-1)
@SubmissionController.register(curses.KEY_ENTER, Terminal.RETURN, 'o')
@SubmissionController.register(Command('SUBMISSION_OPEN_IN_BROWSER'))
def open_link(self):
"Open the selected item with the webbrowser"
@@ -70,7 +69,20 @@ class SubmissionPage(Page):
else:
self.term.flash()
@SubmissionController.register('c')
@SubmissionController.register(Command('SUBMISSION_OPEN_IN_PAGER'))
def open_pager(self):
"Open the selected item with the system's pager"
data = self.content.get(self.nav.absolute_index)
if data['type'] == 'Submission':
text = '\n\n'.join((data['permalink'], data['text']))
self.term.open_pager(text)
elif data['type'] == 'Comment':
text = '\n\n'.join((data['permalink'], data['body']))
self.term.open_pager(text)
else:
self.term.flash()
@SubmissionController.register(Command('SUBMISSION_POST'))
@logged_in
def add_comment(self):
"""
@@ -113,7 +125,7 @@ class SubmissionPage(Page):
if not self.term.loader.exception:
self.refresh_content()
@SubmissionController.register('d')
@SubmissionController.register(Command('DELETE'))
@logged_in
def delete_comment(self):
"Delete a comment as long as it is not the current submission"

View File

@@ -7,10 +7,9 @@ import curses
from . import docs
from .content import SubredditContent
from .page import Page, PageController, logged_in
from .objects import Navigator, Color
from .objects import Navigator, Color, Command
from .submission import SubmissionPage
from .subscription import SubscriptionPage
from .terminal import Terminal
class SubredditController(PageController):
@@ -27,11 +26,11 @@ class SubredditPage(Page):
"""
super(SubredditPage, self).__init__(reddit, term, config, oauth)
self.controller = SubredditController(self, keymap=config.keymap)
self.content = SubredditContent.from_name(reddit, name, term.loader)
self.controller = SubredditController(self)
self.nav = Navigator(self.content.get)
@SubredditController.register(curses.KEY_F5, 'r')
@SubredditController.register(Command('REFRESH'))
def refresh_content(self, order=None, name=None):
"Re-download all submissions and reset the page index"
@@ -49,7 +48,7 @@ class SubredditPage(Page):
if not self.term.loader.exception:
self.nav = Navigator(self.content.get)
@SubredditController.register('f')
@SubredditController.register(Command('SUBREDDIT_SEARCH'))
def search_subreddit(self, name=None):
"Open a prompt to search the given subreddit"
@@ -65,7 +64,7 @@ class SubredditPage(Page):
if not self.term.loader.exception:
self.nav = Navigator(self.content.get)
@SubredditController.register('/')
@SubredditController.register(Command('SUBREDDIT_PROMPT'))
def prompt_subreddit(self):
"Open a prompt to navigate to a different subreddit"
@@ -73,7 +72,7 @@ class SubredditPage(Page):
if name is not None:
self.refresh_content(order='ignore', name=name)
@SubredditController.register(curses.KEY_RIGHT, 'l')
@SubredditController.register(Command('SUBREDDIT_OPEN'))
def open_submission(self, url=None):
"Select the current submission to view posts"
@@ -93,7 +92,7 @@ class SubredditPage(Page):
if data.get('url_type') == 'selfpost':
self.config.history.add(data['url_full'])
@SubredditController.register(curses.KEY_ENTER, Terminal.RETURN, 'o')
@SubredditController.register(Command('SUBREDDIT_OPEN_IN_BROWSER'))
def open_link(self):
"Open a link with the webbrowser"
@@ -107,7 +106,7 @@ class SubredditPage(Page):
self.term.open_browser(data['url_full'])
self.config.history.add(data['url_full'])
@SubredditController.register('c')
@SubredditController.register(Command('SUBREDDIT_POST'))
@logged_in
def post_submission(self):
"Post a new submission to the given subreddit"
@@ -145,7 +144,7 @@ class SubredditPage(Page):
self.refresh_content()
@SubredditController.register('s')
@SubredditController.register(Command('SUBREDDIT_OPEN_SUBSCRIPTIONS'))
@logged_in
def open_subscriptions(self):
"Open user subscriptions page"
@@ -190,7 +189,10 @@ class SubredditPage(Page):
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} {comments} '.format(**data))
self.term.add_line(win, ' {created} '.format(**data))
text, attr = self.term.timestamp_sep
self.term.add_line(win, text, attr=attr)
self.term.add_line(win, ' {comments} '.format(**data))
if data['saved']:
text, attr = self.term.saved

View File

@@ -5,8 +5,7 @@ import curses
from .page import Page, PageController
from .content import SubscriptionContent
from .objects import Color, Navigator
from .terminal import Terminal
from .objects import Color, Navigator, Command
class SubscriptionController(PageController):
@@ -18,12 +17,12 @@ class SubscriptionPage(Page):
def __init__(self, reddit, term, config, oauth):
super(SubscriptionPage, self).__init__(reddit, term, config, oauth)
self.controller = SubscriptionController(self, keymap=config.keymap)
self.content = SubscriptionContent.from_user(reddit, term.loader)
self.controller = SubscriptionController(self)
self.nav = Navigator(self.content.get)
self.subreddit_data = None
@SubscriptionController.register(curses.KEY_F5, 'r')
@SubscriptionController.register(Command('REFRESH'))
def refresh_content(self, order=None, name=None):
"Re-download all subscriptions and reset the page index"
@@ -38,15 +37,14 @@ class SubscriptionPage(Page):
if not self.term.loader.exception:
self.nav = Navigator(self.content.get)
@SubscriptionController.register(curses.KEY_ENTER, Terminal.RETURN,
curses.KEY_RIGHT, 'l')
@SubscriptionController.register(Command('SUBSCRIPTION_SELECT'))
def select_subreddit(self):
"Store the selected subreddit and return to the subreddit page"
self.subreddit_data = self.content.get(self.nav.absolute_index)
self.active = False
@SubscriptionController.register(curses.KEY_LEFT, Terminal.ESCAPE, 'h', 's')
@SubscriptionController.register(Command('SUBSCRIPTION_EXIT'))
def close_subscriptions(self):
"Close subscriptions and return to the subreddit page"

View File

@@ -35,6 +35,7 @@ class Terminal(object):
# ASCII code
ESCAPE = 27
RETURN = 10
SPACE = 32
def __init__(self, stdscr, ascii=False):
@@ -57,6 +58,12 @@ class Terminal(object):
@property
def neutral_arrow(self):
symbol = '>' if self.ascii else ''
attr = curses.A_BOLD
return symbol, attr
@property
def timestamp_sep(self):
symbol = 'o' if self.ascii else ''
attr = curses.A_BOLD
return symbol, attr
@@ -356,6 +363,26 @@ class Terminal(object):
with self.suspend():
webbrowser.open_new_tab(url)
def open_pager(self, data):
"""
View a long block of text using the system's default pager.
The data string will be piped directly to the pager.
"""
pager = os.getenv('PAGER') or 'less'
try:
with self.suspend():
p = subprocess.Popen([pager], stdin=subprocess.PIPE)
p.stdin.write(self.clean(data))
p.stdin.close()
try:
p.wait()
except KeyboardInterrupt:
p.terminate()
except OSError:
self.show_notification('Could not open pager %s' % pager)
def open_editor(self, data=''):
"""
Open a temporary file using the system's default editor.
@@ -366,16 +393,19 @@ class Terminal(object):
"""
with NamedTemporaryFile(prefix='rtv-', suffix='.txt', mode='wb') as fp:
fp.write(codecs.encode(data, 'utf-8'))
fp.write(self.clean(data))
fp.flush()
editor = os.getenv('RTV_EDITOR') or os.getenv('EDITOR') or 'nano'
try:
with self.suspend():
subprocess.Popen([editor, fp.name]).wait()
p = subprocess.Popen([editor, fp.name])
try:
p.wait()
except KeyboardInterrupt:
p.terminate()
except OSError:
raise exceptions.ProgramError(
'Could not open file with %s' % editor)
self.show_notification('Could not open file with %s' % editor)
# Open a second file object to read. This appears to be necessary
# in order to read the changes made by some editors (gedit). w+

View File

@@ -8,8 +8,10 @@ RTV - Reddit Terminal Viewer
.SH OPTIONS
{options}
.SH CONTROLS
Navigate between posts by using the arrow keys or vim-style `hjkl` movement.
You can view the full list of commands by pressing the \fB?\fR key inside of the program.
Move the cursor using either the arrow keys or Vim-style movement.
- Press \fBup\fR and \fBdown\fR to scroll through submissions.
- Press \fBright\fR to view the selected submission and \fBleft\fR to return.
- Press \fB?\fR to open the help screen.
.SH FILES
.TP
.BR $XDG_CONFIG_HOME/rtv/rtv.cfg
@@ -23,18 +25,16 @@ for future sessions. You can disable this behavior by setting the option
.SH ENVIRONMENT
.TP
.BR RTV_EDITOR
Specifies which text editor RTV will attempt to use when editing comments and
posts. RTV will fallback to \fI$EDITOR\fR if the editor is unspecified.
Text editor to use when editing comments and submissions. Will fallback to
\fI$EDITOR\fR.
.TP
.BR BROWSER
Specifies which webbrowser RTV will attempt to use when opening links.
This can be set to a terminal browser (w3m, lynx, elinks, etc.) for a true
terminal experience. RTV will fallback to the system's default browser.
Web browser to use when opening links.
.BR PAGER
Pager to use when expanding individual comments and submissions.
.SH AUTHOR
Man page written by Johnathan "ShaggyTwoDope" Jenkins <twodopeshaggy@gmail.com> (2015).
Michael Lazar <lazar.michael22@gmail.com> (2016).
.SH BUGS
Report bugs to \fIhttps://github.com/michael-lazar/rtv/issues\fR
.SH LICENSE
{license}
.PP
{copyright}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,179 @@
interactions:
- request:
body: null
headers:
Accept: ['*/*']
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
User-Agent: [rtv test suite PRAW/3.3.0 Python/3.4.0 b'Linux-3.13.0-24-generic-x86_64-with-Ubuntu-14.04-trusty']
method: GET
uri: https://www.reddit.com/r/Python/comments/2xmo63.json
response:
body:
string: !!binary |
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
headers:
CF-RAY: [27e2870df58d0701-SJC]
Connection: [keep-alive]
Content-Encoding: [gzip]
Content-Length: ['7681']
Content-Type: [application/json; charset=UTF-8]
Date: ['Fri, 04 Mar 2016 04:19:01 GMT']
Server: [cloudflare-nginx]
Set-Cookie: ['__cfduid=d74b0638b5cdf1ae3e52258dd917f74761457065141; expires=Sat,
04-Mar-17 04:19:01 GMT; path=/; domain=.reddit.com; HttpOnly', 'loid=FIHszkGB7O46d84R4x;
Domain=reddit.com; Max-Age=63071999; Path=/; expires=Sun, 04-Mar-2018 04:19:01
GMT; secure', 'loidcreated=2016-03-04T04%3A19%3A01.483Z; Domain=reddit.com;
Max-Age=63071999; Path=/; expires=Sun, 04-Mar-2018 04:19:01 GMT; secure']
Strict-Transport-Security: [max-age=15552000; includeSubDomains; preload]
Vary: [accept-encoding]
X-Moose: [majestic]
access-control-allow-origin: ['*']
access-control-expose-headers: ['X-Reddit-Tracking, X-Moose']
cache-control: ['max-age=0, must-revalidate']
set-cookie: ['__cfduid=d74b0638b5cdf1ae3e52258dd917f74761457065141; expires=Sat,
04-Mar-17 04:19:01 GMT; path=/; domain=.reddit.com; HttpOnly', 'loid=FIHszkGB7O46d84R4x;
Domain=reddit.com; Max-Age=63071999; Path=/; expires=Sun, 04-Mar-2018 04:19:01
GMT; secure', 'loidcreated=2016-03-04T04%3A19%3A01.483Z; Domain=reddit.com;
Max-Age=63071999; Path=/; expires=Sun, 04-Mar-2018 04:19:01 GMT; secure']
x-content-type-options: [nosniff]
x-frame-options: [SAMEORIGIN]
x-reddit-tracking: ['https://pixel.redditmedia.com/pixel/of_destiny.png?v=WFLujq0%2BidDL%2F5%2FEeUPNp5nimP4wqGpVG5FP8Mx8Jg0%2Bh%2FCnbI6mePESqBT0oQ36bwCzJoaTgDU%3D']
x-ua-compatible: [IE=edge]
x-xss-protection: [1; mode=block]
status: {code: 200, message: OK}
version: 1

View File

@@ -88,19 +88,35 @@ def test_config_from_file():
'link': 'https://reddit.com/permalink •',
'subreddit': 'cfb'}
bindings = {
'REFRESH': 'r, <KEY_F5>',
'UPVOTE': ''}
with NamedTemporaryFile(suffix='.cfg') as fp:
fargs = Config.get_file(filename=fp.name)
fargs, fbindings = Config.get_file(filename=fp.name)
config = Config(**fargs)
config.keymap.set_bindings(fbindings)
assert config.config == {}
assert config.keymap._keymap == {}
# [rtv]
rows = ['{0}={1}'.format(key, val) for key, val in args.items()]
data = '\n'.join(['[rtv]'] + rows)
fp.write(codecs.encode(data, 'utf-8'))
# [bindings]
rows = ['{0}={1}'.format(key, val) for key, val in bindings.items()]
data = '\n'.join(['', '', '[bindings]'] + rows)
fp.write(codecs.encode(data, 'utf-8'))
fp.flush()
fargs = Config.get_file(filename=fp.name)
fargs, fbindings = Config.get_file(filename=fp.name)
config.update(**fargs)
config.keymap.set_bindings(fbindings)
assert config.config == args
assert config.keymap.get('REFRESH') == ['r', '<KEY_F5>']
assert config.keymap.get('UPVOTE') == ['']
def test_config_refresh_token():

View File

@@ -143,7 +143,7 @@ def test_content_submission_load_more_comments(reddit, terminal):
assert content.get(390)['type'] == 'Comment'
def test_content_submission_from_url(reddit, terminal):
def test_content_submission_from_url(reddit, oauth, refresh_token, terminal):
url = 'https://www.reddit.com/r/AskReddit/comments/2np694/'
SubmissionContent.from_url(reddit, url, terminal.loader)
@@ -159,6 +159,14 @@ def test_content_submission_from_url(reddit, terminal):
SubmissionContent.from_url(reddit, url[:-2], terminal.loader)
assert isinstance(terminal.loader.exception, praw.errors.NotFound)
# np.* urls should not raise a 403 error when logged into oauth
oauth.config.refresh_token = refresh_token
oauth.authorize()
url = 'https://np.reddit.com//r/LifeProTips/comments/441hsf//czmp112.json'
with terminal.loader():
SubmissionContent.from_url(reddit, url, terminal.loader)
assert not terminal.loader.exception
def test_content_subreddit_initialize(reddit, terminal):

View File

@@ -4,10 +4,13 @@ from __future__ import unicode_literals
import time
import curses
import six
import pytest
import requests
from rtv.objects import Color, Controller, Navigator, curses_session
from rtv import exceptions
from rtv.objects import Color, Controller, Navigator, Command, KeyMap, \
curses_session
try:
from unittest import mock
@@ -246,6 +249,94 @@ def test_objects_controller():
assert controller_c.trigger('3') is None
def test_objects_controller_command():
class ControllerA(Controller):
character_map = {}
class ControllerB(ControllerA):
character_map = {}
@ControllerA.register(Command('REFRESH'))
def call_page(_):
return 'a1'
@ControllerA.register(Command('UPVOTE'))
def call_page(_):
return 'a2'
@ControllerB.register(Command('REFRESH'))
def call_page(_):
return 'b1'
# Two commands aren't allowed to share keys
keymap = KeyMap({'REFRESH': [0x10, 0x11], 'UPVOTE': [0x11, 0x12]})
with pytest.raises(exceptions.ConfigError) as e:
ControllerA(None, keymap=keymap)
assert 'ControllerA' in six.text_type(e)
# Reset the character map
ControllerA.character_map = {Command('REFRESH'): 0, Command('UPVOTE'): 0}
# All commands must be defined in the keymap
keymap = KeyMap({'REFRESH': [0x10]})
with pytest.raises(exceptions.ConfigError) as e:
ControllerB(None, keymap=keymap)
assert 'UPVOTE' in six.text_type(e)
def test_objects_command():
c1 = Command("REFRESH")
c2 = Command("refresh")
c3 = Command("EXIT")
assert c1 == c2
assert c1 != c3
keymap = {c1: None, c2: None, c3: None}
assert len(keymap) == 2
assert c1 in keymap
assert c2 in keymap
assert c3 in keymap
def test_objects_keymap():
bindings = {
'refresh': ['a', 0x12, '<LF>', '<KEY_UP>'],
'exit': [],
Command('UPVOTE'): ['b', '<KEY_F5>']
}
keymap = KeyMap(bindings)
assert keymap.get(Command('REFRESH')) == ['a', 0x12, '<LF>', '<KEY_UP>']
assert keymap.get(Command('exit')) == []
assert keymap.get('upvote') == ['b', '<KEY_F5>']
with pytest.raises(exceptions.ConfigError) as e:
keymap.get('downvote')
assert 'DOWNVOTE' in six.text_type(e)
# Updating the bindings wipes out the old ones
bindings = {'refresh': ['a', 0x12, '<LF>', '<KEY_UP>']}
keymap.set_bindings(bindings)
assert keymap.get('refresh')
with pytest.raises(exceptions.ConfigError) as e:
keymap.get('upvote')
assert 'UPVOTE' in six.text_type(e)
# Strings should be parsed correctly into keys
assert KeyMap.parse('a') == 97
assert KeyMap.parse(0x12) == 18
assert KeyMap.parse('<LF>') == 10
assert KeyMap.parse('<KEY_UP>') == 259
assert KeyMap.parse('<KEY_F5>') == 269
for key in ('', None, '<lf>', '<DNS>', '<KEY_UD>', ''):
with pytest.raises(exceptions.ConfigError) as e:
keymap.parse(key)
assert six.text_type(key) in six.text_type(e)
def test_objects_navigator_properties():
def valid_page_cb(_):

View File

@@ -38,7 +38,7 @@ def test_page_logged_in(terminal):
def test_page_unauthenticated(reddit, terminal, config, oauth):
page = Page(reddit, terminal, config, oauth)
page.controller = PageController(page)
page.controller = PageController(page, keymap=config.keymap)
with mock.patch.object(page, 'refresh_content'), \
mock.patch.object(page, 'content'), \
mock.patch.object(page, 'nav'), \
@@ -72,7 +72,7 @@ def test_page_unauthenticated(reddit, terminal, config, oauth):
# Show help
page.controller.trigger('?')
message = 'Basic Commands'.encode('utf-8')
message = '[Basic Commands]'.encode('utf-8')
terminal.stdscr.subwin.addstr.assert_any_call(1, 1, message)
# Sort content
@@ -104,7 +104,7 @@ def test_page_unauthenticated(reddit, terminal, config, oauth):
def test_page_authenticated(reddit, terminal, config, oauth, refresh_token):
page = Page(reddit, terminal, config, oauth)
page.controller = PageController(page)
page.controller = PageController(page, keymap=config.keymap)
config.refresh_token = refresh_token
# Login

View File

@@ -94,6 +94,23 @@ def test_submission_open(submission_page, terminal):
assert terminal.open_browser.called
def test_submission_pager(submission_page, terminal):
# View a submission with the pager
with mock.patch.object(terminal, 'open_pager'):
submission_page.controller.trigger('l')
assert terminal.open_pager.called
# Move down to the first comment
with mock.patch.object(submission_page, 'clear_input_queue'):
submission_page.controller.trigger('j')
# View a comment with the pager
with mock.patch.object(terminal, 'open_pager'):
submission_page.controller.trigger('l')
assert terminal.open_pager.called
def test_submission_vote(submission_page, refresh_token):
# Log in

View File

@@ -309,3 +309,26 @@ def test_open_browser(terminal):
open_new_tab.assert_called_with(url)
assert curses.endwin.called
assert curses.doupdate.called
def test_open_pager(terminal, stdscr):
data = "Hello World!"
def side_effect(args, stdin=None):
assert stdin is not None
raise OSError
with mock.patch('subprocess.Popen', autospec=True) as Popen, \
mock.patch.dict('os.environ', {'PAGER': 'fake'}):
Popen.return_value.stdin = mock.Mock()
terminal.open_pager(data)
assert Popen.called
assert not stdscr.addstr.called
# Raise an OS error
Popen.side_effect = side_effect
terminal.open_pager(data)
message = 'Could not open pager fake'.encode('ascii')
assert stdscr.addstr.called_with(0, 0, message)