Merge remote-tracking branch 'upstream/master'
This commit is contained in:
@@ -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
64
CONTROLS.rst
Normal 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
|
||||
223
README.rst
223
README.rst
@@ -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
12
rtv.1
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
__version__ = '1.8.0'
|
||||
__version__ = '1.8.1'
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
18
rtv/docs.py
18
rtv/docs.py
@@ -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
|
||||
"""
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
122
rtv/objects.py
122
rtv/objects.py
@@ -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)
|
||||
42
rtv/page.py
42
rtv/page.py
@@ -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):
|
||||
"""
|
||||
|
||||
72
rtv/rtv.cfg
72
rtv/rtv.cfg
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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+
|
||||
|
||||
@@ -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
179
tests/cassettes/test_submission_pager.yaml
Normal file
179
tests/cassettes/test_submission_pager.yaml
Normal 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
|
||||
@@ -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():
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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(_):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user