Browse Source

fixed conflicts (LOL merge -s ours; push -f)

master
John ShaggyTwoDope Jenkins 7 years ago
parent
commit
ad2c2c2b1c
  1. 17
      rtv/PKGBUILD
  2. BIN
      rtv/pkg/rtv/.MTREE
  3. 27
      rtv/pkg/rtv/.PKGINFO
  4. 10
      rtv/pkg/rtv/usr/bin/rtv
  5. 149
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv-1.2-py3.4.egg-info/PKG-INFO
  6. 23
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv-1.2-py3.4.egg-info/SOURCES.txt
  7. 1
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv-1.2-py3.4.egg-info/dependency_links.txt
  8. 3
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv-1.2-py3.4.egg-info/entry_points.txt
  9. 3
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv-1.2-py3.4.egg-info/requires.txt
  10. 1
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv-1.2-py3.4.egg-info/top_level.txt
  11. 6
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__init__.py
  12. 133
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__main__.py
  13. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/__init__.cpython-34.pyc
  14. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/__init__.cpython-34.pyo
  15. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/__main__.cpython-34.pyc
  16. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/__main__.cpython-34.pyo
  17. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/__version__.cpython-34.pyc
  18. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/__version__.cpython-34.pyo
  19. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/config.cpython-34.pyc
  20. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/config.cpython-34.pyo
  21. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/content.cpython-34.pyc
  22. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/content.cpython-34.pyo
  23. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/curses_helpers.cpython-34.pyc
  24. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/curses_helpers.cpython-34.pyo
  25. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/docs.cpython-34.pyc
  26. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/docs.cpython-34.pyo
  27. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/exceptions.cpython-34.pyc
  28. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/exceptions.cpython-34.pyo
  29. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/helpers.cpython-34.pyc
  30. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/helpers.cpython-34.pyo
  31. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/page.cpython-34.pyc
  32. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/page.cpython-34.pyo
  33. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/submission.cpython-34.pyc
  34. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/submission.cpython-34.pyo
  35. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/subreddit.cpython-34.pyc
  36. BIN
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/subreddit.cpython-34.pyo
  37. 1
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__version__.py
  38. 5
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/config.py
  39. 322
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/content.py
  40. 289
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/curses_helpers.py
  41. 68
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/docs.py
  42. 31
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/exceptions.py
  43. 164
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/helpers.py
  44. 399
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/page.py
  45. 268
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/submission.py
  46. 209
      rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/subreddit.py
  47. BIN
      rtv/rtv-1.2-1-any.pkg.tar.xz
  48. BIN
      rtv/rtv-1.2-1.src.tar.gz
  49. BIN
      rtv/v1.2.tar.gz

17
rtv/PKGBUILD

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
# Maintainer: John Jenkins twodopeshaggy@gmail.com
pkgname=rtv
pkgver=1.2
pkgrel=1
pkgdesc="Browse Reddit from your terminal"
arch=('any')
url="https://github.com/michael-lazar/rtv"
license=('MIT')
depends=('ncurses' 'python' 'python-six' 'python-requests' 'python-praw' 'python-setuptools')
source=(https://github.com/michael-lazar/rtv/archive/v$pkgver.tar.gz)
md5sums=('b67388a428ae06e45e8522b0f56037b1')
package() {
cd "$srcdir/$pkgname-$pkgver"
python setup.py install --root="$pkgdir/" --optimize=1
}

BIN
rtv/pkg/rtv/.MTREE

Binary file not shown.

27
rtv/pkg/rtv/.PKGINFO

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
# Generated by makepkg 4.2.1
# using fakeroot version 1.20.2
# Mon Apr 6 02:40:49 UTC 2015
pkgname = rtv
pkgver = 1.2-1
pkgdesc = Browse Reddit from your terminal
url = https://github.com/michael-lazar/rtv
builddate = 1428288049
packager = Unknown Packager
size = 217088
arch = any
license = MIT
depend = ncurses
depend = python
depend = python-six
depend = python-requests
depend = python-praw
depend = python-setuptools
makepkgopt = strip
makepkgopt = docs
makepkgopt = !libtool
makepkgopt = !staticlibs
makepkgopt = emptydirs
makepkgopt = zipman
makepkgopt = purge
makepkgopt = !upx
makepkgopt = !debug

10
rtv/pkg/rtv/usr/bin/rtv

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
#!/bin/python
# EASY-INSTALL-ENTRY-SCRIPT: 'rtv==1.2','console_scripts','rtv'
__requires__ = 'rtv==1.2'
import sys
from pkg_resources import load_entry_point
if __name__ == '__main__':
sys.exit(
load_entry_point('rtv==1.2', 'console_scripts', 'rtv')()
)

149
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv-1.2-py3.4.egg-info/PKG-INFO

@ -0,0 +1,149 @@ @@ -0,0 +1,149 @@
Metadata-Version: 1.1
Name: rtv
Version: 1.2
Summary: A simple terminal viewer for Reddit (Reddit Terminal Viewer)
Home-page: https://github.com/michael-lazar/rtv
Author: Michael Lazar
Author-email: lazar.michael22@gmail.com
License: MIT
Description: .. image:: https://pypip.in/version/rtv/badge.svg?text=version&style=flat
:target: https://pypi.python.org/pypi/rtv/
:alt: Latest Version
.. image:: https://pypip.in/py_versions/rtv/badge.svg?style=flat
:target: https://pypi.python.org/pypi/rtv/
:alt: Supported Python versions
======================
Reddit Terminal Viewer
======================
Browse Reddit from your terminal
.. image:: http://i.imgur.com/W1hxqCt.png
RTV is built in **python** using the **curses** library, and is compatible with *most* terminal emulators on Linux and OS X.
-------------
Update (v1.1)
-------------
Users can now post comments!
.. image:: http://i.imgur.com/twls7iM.png
------------
Installation
------------
Install using pip
.. code-block:: bash
$ sudo pip install rtv
Or clone the repository
.. code-block:: bash
$ git clone https://github.com/michael-lazar/rtv.git
$ cd rtv
$ sudo python setup.py install
The installation will place a script in the system path
.. code-block:: bash
$ rtv
$ rtv --help
-----
Usage
-----
RTV currently supports browsing both subreddits and individual submissions. In each mode the controls are slightly different.
**Global Commands**
:``▲``/``▼`` or ``j``/``k``: Scroll to the prev/next item
:``a``/``z``: Upvote/downvote the selected item
:``ENTER`` or ``o``: Open the selected item in the default web browser
:``r``: Refresh the current page
:``u``: Login and logout of your user account
:``?``: Show the help screen
:``q``: Quit
**Subreddit Mode**
In subreddit mode you can browse through the top submissions on either the front page or a specific subreddit.
:``►`` or ``l``: View comments for the selected submission
:``/``: Open a prompt to switch subreddits
:``f``: Open a prompt to search the current subreddit
:``p``: Post a new submission to 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
**Submission Mode**
In submission mode you can view the self text for a submission and browse comments.
:``◄`` or ``h``: Return to subreddit mode
:``►`` or ``l``: Fold the selected comment, or load additional comments
:``c``: Post a new comment on the selected item
-------------
Configuration
-------------
RTV will read a configuration file located at ``$XDG_CONFIG_HOME/rtv/rtv.cfg`` or ``~/.config/rtv/rtv.cfg`` if ``$XDG_CONFIG_HOME`` is not set.
This can be used to avoid having to re-enter login credentials every time the program is launched.
Each line in the file will replace the corresponding default argument in the launch script.
Example config:
.. code-block:: ini
[rtv]
username=MyUsername
password=MySecretPassword
# Log file location
log=/tmp/rtv.log
# Default subreddit
subreddit=CollegeBasketball
# Default submission link - will be opened every time the program starts
# link=http://www.reddit.com/r/CollegeBasketball/comments/31irjq
# Enable unicode characters (experimental)
# This is known to be unstable with east asian wide character sets
# unicode=true
RTV allows users to compose comments and replys using their preferred text editor (**vi**, **nano**, **gedit**, etc).
Set the environment variable ``RTV_EDITOR`` to specify which editor the program should use.
.. code-block:: bash
$ export RTV_EDITOR=gedit
Keywords: reddit terminal praw curses
Platform: UNKNOWN
Classifier: Intended Audience :: End Users/Desktop
Classifier: Environment :: Console :: Curses
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: POSIX
Classifier: Natural Language :: English
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Terminals
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content :: Message Boards
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content :: News/Diary

23
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv-1.2-py3.4.egg-info/SOURCES.txt

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
MANIFEST.in
README.rst
setup.cfg
setup.py
version.py
rtv/__init__.py
rtv/__main__.py
rtv/__version__.py
rtv/config.py
rtv/content.py
rtv/curses_helpers.py
rtv/docs.py
rtv/exceptions.py
rtv/helpers.py
rtv/page.py
rtv/submission.py
rtv/subreddit.py
rtv.egg-info/PKG-INFO
rtv.egg-info/SOURCES.txt
rtv.egg-info/dependency_links.txt
rtv.egg-info/entry_points.txt
rtv.egg-info/requires.txt
rtv.egg-info/top_level.txt

1
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv-1.2-py3.4.egg-info/dependency_links.txt

@ -0,0 +1 @@ @@ -0,0 +1 @@

3
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv-1.2-py3.4.egg-info/entry_points.txt

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
[console_scripts]
rtv = rtv.__main__:main

3
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv-1.2-py3.4.egg-info/requires.txt

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
praw>=2.1.6
six
requests

1
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv-1.2-py3.4.egg-info/top_level.txt

@ -0,0 +1 @@ @@ -0,0 +1 @@
rtv

6
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__init__.py

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
from .__version__ import __version__
__title__ = 'Reddit Terminal Viewer'
__author__ = 'Michael Lazar'
__license__ = 'The MIT License (MIT)'
__copyright__ = '(c) 2015 Michael Lazar'

133
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__main__.py

@ -0,0 +1,133 @@ @@ -0,0 +1,133 @@
import os
import sys
import argparse
import locale
import logging
import requests
import praw
import praw.errors
from six.moves import configparser
from . import config
from .exceptions import SubmissionError, SubredditError, ProgramError
from .curses_helpers import curses_session
from .submission import SubmissionPage
from .subreddit import SubredditPage
from .docs import *
from .__version__ import __version__
__all__ = []
def load_config():
"""
Search for a configuration file at the location ~/.rtv and attempt to load
saved settings for things like the username and password.
"""
config = configparser.ConfigParser()
HOME = os.path.expanduser('~')
XDG_CONFIG_HOME = os.getenv('XDG_CONFIG_HOME', os.path.join(HOME, '.config'))
config_paths = [
os.path.join(XDG_CONFIG_HOME, 'rtv', 'rtv.cfg'),
os.path.join(HOME, '.rtv')
]
# read only the first existing config file
for config_path in config_paths:
if os.path.exists(config_path):
config.read(config_path)
break
defaults = {}
if config.has_section('rtv'):
defaults = dict(config.items('rtv'))
if 'unicode' in defaults:
defaults['unicode'] = config.getboolean('rtv', 'unicode')
return defaults
def command_line():
parser = argparse.ArgumentParser(
prog='rtv', description=SUMMARY,
epilog=CONTROLS + HELP,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('-s', dest='subreddit', help='subreddit name')
parser.add_argument('-l', dest='link', help='full link to a submission')
parser.add_argument('--unicode', action='store_true',
help='enable unicode (experimental)')
parser.add_argument('--log', metavar='FILE', action='store',
help='Log HTTP requests')
group = parser.add_argument_group('authentication (optional)', AUTH)
group.add_argument('-u', dest='username', help='reddit username')
group.add_argument('-p', dest='password', help='reddit password')
args = parser.parse_args()
return args
def main():
"Main entry point"
# logging.basicConfig(level=logging.DEBUG, filename='rtv.log')
locale.setlocale(locale.LC_ALL, '')
args = command_line()
local_config = load_config()
# set the terminal title
title = 'rtv {0}'.format(__version__)
if os.name == 'nt':
os.system('title {0}'.format(title))
else:
sys.stdout.write("\x1b]2;{0}\x07".format(title))
# Fill in empty arguments with config file values. Paramaters explicitly
# typed on the command line will take priority over config file params.
for key, val in local_config.items():
if getattr(args, key, None) is None:
setattr(args, key, val)
config.unicode = args.unicode
if args.log:
logging.basicConfig(level=logging.DEBUG, filename=args.log)
try:
print('Connecting...')
reddit = praw.Reddit(user_agent=AGENT)
reddit.config.decode_html_entities = True
if args.username:
# PRAW will prompt for password if it is None
reddit.login(args.username, args.password)
with curses_session() as stdscr:
if args.link:
page = SubmissionPage(stdscr, reddit, url=args.link)
page.loop()
page = SubredditPage(stdscr, reddit, args.subreddit)
page.loop()
except praw.errors.InvalidUserPass:
print('Invalid password for username: {}'.format(args.username))
except requests.ConnectionError:
print('Connection timeout')
except requests.HTTPError:
print('HTTP Error: 404 Not Found')
except SubmissionError as e:
print('Could not reach submission URL: {}'.format(e.url))
except SubredditError as e:
print('Could not reach subreddit: {}'.format(e.name))
except ProgramError as e:
print('Error: could not open file with program "{}", '
'try setting RTV_EDITOR'.format(e.name))
except KeyboardInterrupt:
return
sys.exit(main())

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/__init__.cpython-34.pyc

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/__init__.cpython-34.pyo

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/__main__.cpython-34.pyc

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/__main__.cpython-34.pyo

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/__version__.cpython-34.pyc

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/__version__.cpython-34.pyo

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/config.cpython-34.pyc

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/config.cpython-34.pyo

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/content.cpython-34.pyc

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/content.cpython-34.pyo

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/curses_helpers.cpython-34.pyc

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/curses_helpers.cpython-34.pyo

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/docs.cpython-34.pyc

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/docs.cpython-34.pyo

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/exceptions.cpython-34.pyc

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/exceptions.cpython-34.pyo

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/helpers.cpython-34.pyc

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/helpers.cpython-34.pyo

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/page.cpython-34.pyc

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/page.cpython-34.pyo

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/submission.cpython-34.pyc

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/submission.cpython-34.pyo

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/subreddit.cpython-34.pyc

Binary file not shown.

BIN
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__pycache__/subreddit.cpython-34.pyo

Binary file not shown.

1
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/__version__.py

@ -0,0 +1 @@ @@ -0,0 +1 @@
__version__ = '1.2'

5
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/config.py

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
"""
Global configuration settings
"""
unicode = False

322
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/content.py

@ -0,0 +1,322 @@ @@ -0,0 +1,322 @@
import textwrap
import praw
import requests
from .exceptions import SubmissionError, SubredditError, AccountError
from .helpers import humanize_timestamp, wrap_text, strip_subreddit_url
__all__ = ['SubredditContent', 'SubmissionContent']
class BaseContent(object):
def get(self, index, n_cols):
raise NotImplementedError
def iterate(self, index, step, n_cols):
while True:
if step < 0 and index < 0:
# Hack to prevent displaying negative indicies if iterating in
# the negative direction.
break
try:
yield self.get(index, n_cols=n_cols)
except IndexError:
break
index += step
@staticmethod
def flatten_comments(comments, root_level=0):
"""
Flatten a PRAW comment tree while preserving the nested level of each
comment via the `nested_level` attribute.
"""
stack = comments[:]
for item in stack:
item.nested_level = root_level
retval = []
while stack:
item = stack.pop(0)
if isinstance(item, praw.objects.MoreComments) and (
item.count == 0):
continue
nested = getattr(item, 'replies', None)
if nested:
for n in nested:
n.nested_level = item.nested_level + 1
stack[0:0] = nested
retval.append(item)
return retval
@staticmethod
def strip_praw_comment(comment):
"""
Parse through a submission comment and return a dict with data ready to
be displayed through the terminal.
"""
data = {}
data['object'] = comment
data['level'] = comment.nested_level
if isinstance(comment, praw.objects.MoreComments):
data['type'] = 'MoreComments'
data['count'] = comment.count
data['body'] = 'More comments'.format(comment.count)
else:
data['type'] = 'Comment'
data['body'] = comment.body
data['created'] = humanize_timestamp(comment.created_utc)
data['score'] = '{} pts'.format(comment.score)
author = getattr(comment, 'author')
data['author'] = (author.name if author else '[deleted]')
sub_author = getattr(comment.submission.author, 'name')
data['is_author'] = (data['author'] == sub_author)
flair = comment.author_flair_text
data['flair'] = (flair if flair else '')
data['likes'] = comment.likes
data['gold'] = comment.gilded > 0
return data
@staticmethod
def strip_praw_submission(sub):
"""
Parse through a submission and return a dict with data ready to be
displayed through the terminal.
"""
is_selfpost = lambda s: s.startswith('http://www.reddit.com/r/')
data = {}
data['object'] = sub
data['type'] = 'Submission'
data['title'] = sub.title
data['text'] = sub.selftext
data['created'] = humanize_timestamp(sub.created_utc)
data['comments'] = '{} comments'.format(sub.num_comments)
data['score'] = '{} pts'.format(sub.score)
author = getattr(sub, 'author')
data['author'] = (author.name if author else '[deleted]')
data['permalink'] = sub.permalink
data['subreddit'] = strip_subreddit_url(sub.permalink)
data['flair'] = (sub.link_flair_text if sub.link_flair_text else '')
data['url_full'] = sub.url
data['url'] = ('selfpost' if is_selfpost(sub.url) else sub.url)
data['likes'] = sub.likes
data['gold'] = sub.gilded > 0
return data
class SubmissionContent(BaseContent):
"""
Grab a submission from PRAW and lazily store comments to an internal
list for repeat access.
"""
def __init__(self, submission, loader, indent_size=2, max_indent_level=4):
self.indent_size = indent_size
self.max_indent_level = max_indent_level
self._loader = loader
self._submission = submission
self._submission_data = self.strip_praw_submission(self._submission)
self.name = self._submission_data['permalink']
comments = self.flatten_comments(self._submission.comments)
self._comment_data = [self.strip_praw_comment(c) for c in comments]
@classmethod
def from_url(cls, reddit, url, loader, indent_size=2, max_indent_level=4):
try:
with loader():
submission = reddit.get_submission(url, comment_sort='hot')
except praw.errors.APIException:
raise SubmissionError(url)
return cls(submission, loader, indent_size, max_indent_level)
def get(self, index, n_cols=70):
"""
Grab the `i`th submission, with the title field formatted to fit inside
of a window of width `n`
"""
if index < -1:
raise IndexError
elif index == -1:
data = self._submission_data
data['split_title'] = textwrap.wrap(data['title'], width=n_cols -2)
data['split_text'] = wrap_text(data['text'], width=n_cols - 2)
data['n_rows'] = len(data['split_title'] + data['split_text']) + 5
data['offset'] = 0
else:
data = self._comment_data[index]
indent_level = min(data['level'], self.max_indent_level)
data['offset'] = indent_level * self.indent_size
if data['type'] == 'Comment':
width = n_cols - data['offset']
data['split_body'] = wrap_text(data['body'], width=width)
data['n_rows'] = len(data['split_body']) + 1
else:
data['n_rows'] = 1
return data
def toggle(self, index, n_cols=70):
"""
Toggle the state of the object at the given index.
If it is a comment, pack it into a hidden comment.
If it is a hidden comment, unpack it.
If it is more comments, load the comments.
"""
data = self.get(index)
if data['type'] == 'Submission':
# Can't hide the submission!
pass
elif data['type'] == 'Comment':
cache = [data]
count = 1
for d in self.iterate(index + 1, 1, n_cols):
if d['level'] <= data['level']:
break
count += d.get('count', 1)
cache.append(d)
comment = {}
comment['type'] = 'HiddenComment'
comment['cache'] = cache
comment['count'] = count
comment['level'] = data['level']
comment['body'] = 'Hidden'.format(count)
self._comment_data[index:index + len(cache)] = [comment]
elif data['type'] == 'HiddenComment':
self._comment_data[index:index + 1] = data['cache']
elif data['type'] == 'MoreComments':
with self._loader():
comments = data['object'].comments(update=False)
comments = self.flatten_comments(comments,
root_level=data['level'])
comment_data = [self.strip_praw_comment(c) for c in comments]
self._comment_data[index:index + 1] = comment_data
else:
raise ValueError('% type not recognized' % data['type'])
class SubredditContent(BaseContent):
"""
Grab a subreddit from PRAW and lazily stores submissions to an internal
list for repeat access.
"""
def __init__(self, name, submissions, loader):
self.name = name
self._loader = loader
self._submissions = submissions
self._submission_data = []
# Verify that content exists for the given submission generator.
# This is necessary because PRAW loads submissions lazily, and
# there is is no other way to check things like multireddits that
# don't have a real corresponding subreddit object.
try:
self.get(0)
except (praw.errors.APIException, requests.HTTPError,
praw.errors.RedirectException):
raise SubredditError(display_name)
@classmethod
def from_name(cls, reddit, name, loader, order='hot', query=None):
if order not in ['hot', 'top', 'rising', 'new', 'controversial']:
raise SubredditError(display_name)
name = name.strip(' /') # Strip leading and trailing backslashes
if name.startswith('r/'):
name = name[2:]
# Grab the display order e.g. "python/new"
if '/' in name:
name, order = name.split('/')
display_name = display_name = '/r/{}'.format(name)
if order != 'hot':
display_name += '/{}'.format(order)
if name == 'me':
if not reddit.is_logged_in():
raise AccountError
else:
submissions = reddit.user.get_submitted(sort=order)
elif query:
if name == 'front':
submissions = reddit.search(query, subreddit=None, sort=order)
else:
submissions = reddit.search(query, subreddit=name, sort=order)
else:
if name == 'front':
dispatch = {
'hot': reddit.get_front_page,
'top': reddit.get_top,
'rising': reddit.get_rising,
'new': reddit.get_new,
'controversial': reddit.get_controversial,
}
else:
subreddit = reddit.get_subreddit(name)
dispatch = {
'hot': subreddit.get_hot,
'top': subreddit.get_top,
'rising': subreddit.get_rising,
'new': subreddit.get_new,
'controversial': subreddit.get_controversial,
}
submissions = dispatch[order](limit=None)
return cls(display_name, submissions, loader)
def get(self, index, n_cols=70):
"""
Grab the `i`th submission, with the title field formatted to fit inside
of a window of width `n_cols`
"""
if index < 0:
raise IndexError
while index >= len(self._submission_data):
try:
with self._loader():
submission = next(self._submissions)
except StopIteration:
raise IndexError
else:
data = self.strip_praw_submission(submission)
self._submission_data.append(data)
# Modifies the original dict, faster than copying
data = self._submission_data[index]
data['split_title'] = textwrap.wrap(data['title'], width=n_cols)
data['n_rows'] = len(data['split_title']) + 3
data['offset'] = 0
return data

289
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/curses_helpers.py

@ -0,0 +1,289 @@ @@ -0,0 +1,289 @@
import os
import time
import threading
import curses
from curses import textpad, ascii
from contextlib import contextmanager
from .docs import HELP
from .helpers import strip_textpad
from .exceptions import EscapeInterrupt
__all__ = ['ESCAPE', 'UARROW', 'DARROW', 'BULLET', 'show_notification',
'show_help', 'LoadScreen', 'Color', 'text_input', 'curses_session']
ESCAPE = 27
# Curses does define constants for these (e.g. curses.ACS_BULLET)
# However, they rely on using the curses.addch() function, which has been
# found to be buggy and a PITA to work with. By defining them as unicode
# points they can be added via the more reliable curses.addstr().
# http://bugs.python.org/issue21088
UARROW = u'\u25b2'.encode('utf-8')
DARROW = u'\u25bc'.encode('utf-8')
BULLET = u'\u2022'.encode('utf-8')
GOLD = u'\u272A'.encode('utf-8')
def show_notification(stdscr, message):
"""
Overlay a message box on the center of the screen and wait for user input.
Params:
message (list): List of strings, one per line.
"""
n_rows, n_cols = stdscr.getmaxyx()
box_width = max(map(len, message)) + 2
box_height = len(message) + 2
# Cut off the lines of the message that don't fit on the screen
box_width = min(box_width, n_cols)
box_height = min(box_height, n_rows)
message = message[:box_height-2]
s_row = (n_rows - box_height) // 2
s_col = (n_cols - box_width) // 2
window = stdscr.derwin(box_height, box_width, s_row, s_col)
window.erase()
window.border()
for index, line in enumerate(message, start=1):
window.addnstr(index, 1, line, box_width - 2)
window.refresh()
ch = stdscr.getch()
window.clear()
window = None
stdscr.refresh()
return ch
def show_help(stdscr):
"""
Overlay a message box with the help screen.
"""
show_notification(stdscr, HELP.splitlines())
class LoadScreen(object):
"""
Display a loading dialog while waiting for a blocking action to complete.
This class spins off a seperate thread to animate the loading screen in the
background.
Usage:
#>>> loader = LoadScreen(stdscr)
#>>> with loader(...):
#>>> blocking_request(...)
"""
def __init__(self, stdscr):
self._stdscr = stdscr
self._args = None
self._animator = None
self._is_running = None
def __call__(
self,
delay=0.5,
interval=0.4,
message='Downloading',
trail='...'):
"""
Params:
delay (float): Length of time that the loader will wait before
printing on the screen. Used to prevent flicker on pages that
load very fast.
interval (float): Length of time between each animation frame.
message (str): Message to display
trail (str): Trail of characters that will be animated by the
loading screen.
"""
self._args = (delay, interval, message, trail)
return self
def __enter__(self):
self._animator = threading.Thread(target=self.animate, args=self._args)
self._animator.daemon = True
self._is_running = True
self._animator.start()
def __exit__(self, exc_type, exc_val, exc_tb):
self._is_running = False
self._animator.join()
def animate(self, delay, interval, message, trail):
start = time.time()
while (time.time() - start) < delay:
if not self._is_running:
return
message_len = len(message) + len(trail)
n_rows, n_cols = self._stdscr.getmaxyx()
s_row = (n_rows - 3) // 2
s_col = (n_cols - message_len - 1) // 2
window = self._stdscr.derwin(3, message_len + 2, s_row, s_col)
while True:
for i in range(len(trail) + 1):
if not self._is_running:
window.clear()
window = None
self._stdscr.refresh()
return
window.erase()
window.border()
window.addstr(1, 1, message + trail[:i])
window.refresh()
time.sleep(interval)
class Color(object):
"""
Color attributes for curses.
"""
_colors = {
'RED': (curses.COLOR_RED, -1),
'GREEN': (curses.COLOR_GREEN, -1),
'YELLOW': (curses.COLOR_YELLOW, -1),
'BLUE': (curses.COLOR_BLUE, -1),
'MAGENTA': (curses.COLOR_MAGENTA, -1),
'CYAN': (curses.COLOR_CYAN, -1),
'WHITE': (curses.COLOR_WHITE, -1),
}
@classmethod
def init(cls):
"""
Initialize color pairs inside of curses using the default background.
This should be called once during the curses initial setup. Afterwards,
curses color pairs can be accessed directly through class attributes.
"""
# Assign the terminal's default (background) color to code -1
curses.use_default_colors()
for index, (attr, code) in enumerate(cls._colors.items(), start=1):
curses.init_pair(index, code[0], code[1])
setattr(cls, attr, curses.color_pair(index))
@classmethod
def get_level(cls, level):
levels = [cls.MAGENTA, cls.CYAN, cls.GREEN, cls.YELLOW]
return levels[level % len(levels)]
def text_input(window, allow_resize=True):
"""
Transform a window into a text box that will accept user input and loop
until an escape sequence is entered.
If enter is pressed, return the input text as a string.
If escape is pressed, return None.
"""
window.clear()
# Set cursor mode to 1 because 2 doesn't display on some terminals
curses.curs_set(1)
# Turn insert_mode off to avoid the recursion error described here
# http://bugs.python.org/issue13051
textbox = textpad.Textbox(window, insert_mode=False)
textbox.stripspaces = 0
def validate(ch):
"Filters characters for special key sequences"
if ch == ESCAPE:
raise EscapeInterrupt
if (not allow_resize) and (ch == curses.KEY_RESIZE):
raise EscapeInterrupt
# Fix backspace for iterm
if ch == ascii.DEL:
ch = curses.KEY_BACKSPACE
return ch
# Wrapping in an exception block so that we can distinguish when the user
# hits the return character from when the user tries to back out of the
# input.
try:
out = textbox.edit(validate=validate)
except EscapeInterrupt:
out = None
curses.curs_set(0)
return strip_textpad(out)
@contextmanager
def curses_session():
"""
Setup terminal and initialize curses.
"""
try:
# Curses must wait for some time after the Escape key is pressed to
# check if it is the beginning of an escape sequence indicating a
# special key. The default wait time is 1 second, which means that
# getch() will not return the escape key (27) until a full second
# after it has been pressed.
# Turn this down to 25 ms, which is close to what VIM uses.
# http://stackoverflow.com/questions/27372068
os.environ['ESCDELAY'] = '25'
# Initialize curses
stdscr = curses.initscr()
# Turn off echoing of keys, and enter cbreak mode,
# where no buffering is performed on keyboard input
curses.noecho()
curses.cbreak()
# In keypad mode, escape sequences for special keys
# (like the cursor keys) will be interpreted and
# a special value like curses.KEY_LEFT will be returned
stdscr.keypad(1)
# Start color, too. Harmless if the terminal doesn't have
# color; user can test with has_color() later on. The try/catch
# works around a minor bit of over-conscientiousness in the curses
# module -- the error return from C start_color() is ignorable.
try:
curses.start_color()
except:
pass
Color.init()
# Hide blinking cursor
curses.curs_set(0)
yield stdscr
finally:
if stdscr is not None:
stdscr.keypad(0)
curses.echo()
curses.nocbreak()
curses.endwin()

68
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/docs.py

@ -0,0 +1,68 @@ @@ -0,0 +1,68 @@
from .__version__ import __version__
__all__ = ['AGENT', 'SUMMARY', 'AUTH', 'CONTROLS', 'HELP', 'COMMENT_FILE',
'SUBMISSION_FILE']
AGENT = """\
desktop:https://github.com/michael-lazar/rtv:{} (by /u/civilization_phaze_3)\
""".format(__version__)
SUMMARY = """
Reddit Terminal Viewer is a lightweight browser for www.reddit.com built into a
terminal window.
"""
AUTH = """\
Authenticating is required to vote and leave comments. If only a username is
given, the program will display a secure prompt to enter a password.
"""
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.
"""
HELP = """
Global Commands
`UP/DOWN` or `j/k` : Scroll to the prev/next item
`a/z` : Upvote/downvote the selected item
`ENTER` or `o` : Open the selected item in the default web browser
`r` : Refresh the current page
`u` : Login/logout of your user account
`?` : Show this help message
`q` : Quit the program
Subreddit Mode
`RIGHT` or `l` : View comments for the selected submission
`/` : Open a prompt to switch subreddits
`f` : Open a prompt to search the current subreddit
`p` : Post a new submission to the current subreddit
Submission Mode
`LEFT` or `h` : Return to subreddit mode
`RIGHT` or `l` : Fold the selected comment, or load additional comments
`c` : Post a new comment on the selected item
"""
COMMENT_FILE = """
# Please enter a comment. Lines starting with '#' will be ignored,
# and an empty message aborts the comment.
#
# Replying to {author}'s {type}
{content}
"""
SUBMISSION_FILE = """
# Please enter your submission. Lines starting with '#' will be ignored,
# and an empty field aborts the submission.
#
# The first line will be interpreted as the title
# The following lines will be interpreted as the content
#
# Posting to /r/{name}
"""

31
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/exceptions.py

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
class EscapeInterrupt(Exception):
"Signal that the ESC key has been pressed"
class RTVError(Exception):
"Base RTV error class"
class AccountError(RTVError):
"Could not access user account"
class SubmissionError(RTVError):
"Submission could not be loaded"
def __init__(self, url):
self.url = url
class SubredditError(RTVError):
"Subreddit could not be reached"
def __init__(self, name):
self.name = name
class ProgramError(RTVError):
"Problem executing an external program"
def __init__(self, name):
self.name = name

164
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/helpers.py

@ -0,0 +1,164 @@ @@ -0,0 +1,164 @@
import sys
import os
import textwrap
import subprocess
from datetime import datetime
from tempfile import NamedTemporaryFile
from . import config
from .exceptions import ProgramError
__all__ = ['open_browser', 'clean', 'wrap_text', 'strip_textpad',
'strip_subreddit_url', 'humanize_timestamp', 'open_editor']
def open_editor(data=''):
"""
Open a temporary file using the system's default editor.
The data string will be written to the file before opening. This function
will block until the editor has closed. At that point the file will be
read and and lines starting with '#' will be stripped.
"""
with NamedTemporaryFile(prefix='rtv-', suffix='.txt', mode='w') as fp:
fp.write(data)
fp.flush()
editor = os.getenv('RTV_EDITOR') or os.getenv('EDITOR') or 'nano'
try:
subprocess.Popen([editor, fp.name]).wait()
except OSError as e:
raise ProgramError(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+ mode does
# not work!
with open(fp.name) as fp2:
text = ''.join(line for line in fp2 if not line.startswith('#'))
text = text.rstrip()
return text
def open_browser(url):
"""
Call webbrowser.open_new_tab(url) and redirect stdout/stderr to devnull.
This is a workaround to stop firefox from spewing warning messages to the
console. See http://bugs.python.org/issue22277 for a better description
of the problem.
"""
command = "import webbrowser; webbrowser.open_new_tab('%s')" % url
args = [sys.executable, '-c', command]
with open(os.devnull, 'ab+', 0) as null:
subprocess.check_call(args, stdout=null, stderr=null)
def clean(string):
"""
Required reading!
http://nedbatchelder.com/text/unipain.html
Python 2 input string will be a unicode type (unicode code points). Curses
will accept unicode if all of the points are in the ascii range. However, if
any of the code points are not valid ascii curses will throw a
UnicodeEncodeError: 'ascii' codec can't encode character, ordinal not in
range(128). If we encode the unicode to a utf-8 byte string and pass that to
curses, it will render correctly.
Python 3 input string will be a string type (unicode code points). Curses
will accept that in all cases. However, the n character count in addnstr
will not be correct. If code points are passed to addnstr, curses will treat
each code point as one character and will not account for wide characters.
If utf-8 is passed in, addnstr will treat each 'byte' as a single character.
"""
encoding = 'utf-8' if config.unicode else 'ascii'
string = string.encode(encoding, 'replace')
return string
def wrap_text(text, width):
"""
Wrap text paragraphs to the given character width while preserving newlines.
"""
out = []
for paragraph in text.splitlines():
# Wrap returns an empty list when paragraph is a newline. In order to
# preserve newlines we substitute a list containing an empty string.
lines = textwrap.wrap(paragraph, width=width) or ['']
out.extend(lines)
return out
def strip_textpad(text):
"""
Attempt to intelligently strip excess whitespace from the output of a
curses textpad.
"""
if text is None:
return text
# Trivial case where the textbox is only one line long.
if '\n' not in text:
return text.rstrip()
# Allow one space at the end of the line. If there is more than one space,
# assume that a newline operation was intended by the user
stack, current_line = [], ''
for line in text.split('\n'):
if line.endswith(' '):
stack.append(current_line + line.rstrip())
current_line = ''
else:
current_line += line
stack.append(current_line)
# Prune empty lines at the bottom of the textbox.
for item in stack[::-1]:
if len(item) == 0:
stack.pop()
else:
break
out = '\n'.join(stack)
return out
def strip_subreddit_url(permalink):
"""
Strip a subreddit name from the subreddit's permalink.
This is used to avoid submission.subreddit.url making a seperate API call.
"""
subreddit = permalink.split('/')[4]
return '/r/{}'.format(subreddit)
def humanize_timestamp(utc_timestamp, verbose=False):
"""
Convert a utc timestamp into a human readable relative-time.
"""
timedelta = datetime.utcnow() - datetime.utcfromtimestamp(utc_timestamp)
seconds = int(timedelta.total_seconds())
if seconds < 60:
return 'moments ago' if verbose else '0min'
minutes = seconds // 60
if minutes < 60:
return ('%d minutes ago' % minutes) if verbose else ('%dmin' % minutes)
hours = minutes // 60
if hours < 24:
return ('%d hours ago' % hours) if verbose else ('%dhr' % hours)
days = hours // 24
if days < 30:
return ('%d days ago' % days) if verbose else ('%dday' % days)
months = days // 30.4
if months < 12:
return ('%d months ago' % months) if verbose else ('%dmonth' % months)
years = months // 12
return ('%d years ago' % years) if verbose else ('%dyr' % years)

399
rtv/pkg/rtv/usr/lib/python3.4/site-packages/rtv/page.py

@ -0,0 +1,399 @@ @@ -0,0 +1,399 @@
import curses
import six
import sys
import praw.errors
from .helpers import clean
from .curses_helpers import Color, show_notification, show_help, text_input
from .docs import AGENT
__all__ = ['Navigator']
class Navigator(object):
"""
Handles math behind cursor movement and screen paging.
"""
def __init__(
self,
valid_page_cb,
page_index=0,
cursor_index=0,
inverted=False):
self.page_index = page_index
self.cursor_index = cursor_index
self.inverted = inverted
self._page_cb = valid_page_cb
self._header_window = None
self._content_window = None
@property
def step(self):
return 1 if not self.inverted else -1
@property
def position(self):
return (self.page_index, self.cursor_index, self.inverted)
@property
def absolute_index(self):
return self.page_index + (self.step * self.cursor_index)
def move(self, direction, n_windows):
"Move the cursor down (positive direction) or up (negative direction)"
valid, redraw = True, False
forward = ((direction * self.step) > 0)
if forward:
if self.page_index < 0:
if self._is_valid(0):
# Special case - advance the page index if less than zero
self.page_index = 0
self.cursor_index = 0
redraw = True
else:
valid = False
else:
self.cursor_index += 1
if not self._is_valid(self.absolute_index):
# Move would take us out of bounds
self.cursor_index -= 1
valid = False
elif self.cursor_index >= (n_windows - 1):
# Flip the orientation and reset the cursor
self.flip(self.cursor_index)
self.cursor_index = 0
redraw = True
else:
if self.cursor_index > 0:
self.cursor_index -= 1
else:
self.page_index -= self.step
if self._is_valid(self.absolute_index):
# We have reached the beginning of the page - move the
# index
redraw = True
else:
self.page_index += self.step
valid = False # Revert
return valid, redraw
def flip(self, n_windows):
"Flip the orientation of the page"
self.page_index += (self.step * n_windows)
self.cursor_index = n_windows
self.inverted = not self.inverted
def _is_valid(self, page_index):
"Check if a page index will cause entries to fall outside valid range"
try:
self._page_cb(page_index)
except IndexError:
return False
else:
return True
class BaseController(object):
"""
Event handler for triggering functions with curses keypresses.
Register a keystroke to a class method using the @egister decorator.
#>>> @Controller.register('a', 'A')
#>>> def func(self, *args)
Register a default behavior by using `None`.
#>>> @Controller.register(None)
#>>> def default_func(self, *args)
Bind the controller to a class instance and trigger a key. Additional
arguments will be passed to the function.
#>>> controller = Controller(self)
#>>> controller.trigger('a', *args)
"""
character_map = {None: (lambda *args, **kwargs: None)}
def __init__(self, instance):
self.instance = instance
def trigger(self, char, *args, **kwargs):