Browse Source

Added missing file in previous commit

master
John ShaggyTwoDope Jenkins 5 years ago
parent
commit
31462b1e98
65 changed files with 2558 additions and 0 deletions
  1. 22
    0
      rtv-git/PKGBUILD
  2. BIN
      rtv-git/pkg/rtv-git/.MTREE
  3. 27
    0
      rtv-git/pkg/rtv-git/.PKGINFO
  4. 10
    0
      rtv-git/pkg/rtv-git/usr/bin/rtv
  5. 138
    0
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv-1.1.2-py3.4.egg-info/PKG-INFO
  6. 23
    0
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv-1.1.2-py3.4.egg-info/SOURCES.txt
  7. 1
    0
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv-1.1.2-py3.4.egg-info/dependency_links.txt
  8. 3
    0
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv-1.1.2-py3.4.egg-info/entry_points.txt
  9. 3
    0
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv-1.1.2-py3.4.egg-info/requires.txt
  10. 1
    0
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv-1.1.2-py3.4.egg-info/top_level.txt
  11. 6
    0
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__init__.py
  12. 111
    0
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__main__.py
  13. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/__init__.cpython-34.pyc
  14. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/__init__.cpython-34.pyo
  15. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/__main__.cpython-34.pyc
  16. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/__main__.cpython-34.pyo
  17. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/__version__.cpython-34.pyc
  18. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/__version__.cpython-34.pyo
  19. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/config.cpython-34.pyc
  20. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/config.cpython-34.pyo
  21. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/content.cpython-34.pyc
  22. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/content.cpython-34.pyo
  23. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/curses_helpers.cpython-34.pyc
  24. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/curses_helpers.cpython-34.pyo
  25. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/docs.cpython-34.pyc
  26. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/docs.cpython-34.pyo
  27. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/exceptions.cpython-34.pyc
  28. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/exceptions.cpython-34.pyo
  29. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/helpers.cpython-34.pyc
  30. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/helpers.cpython-34.pyo
  31. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/page.cpython-34.pyc
  32. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/page.cpython-34.pyo
  33. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/submission.cpython-34.pyc
  34. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/submission.cpython-34.pyo
  35. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/subreddit.cpython-34.pyc
  36. BIN
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/subreddit.cpython-34.pyo
  37. 1
    0
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__version__.py
  38. 5
    0
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/config.py
  39. 337
    0
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/content.py
  40. 285
    0
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/curses_helpers.py
  41. 53
    0
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/docs.py
  42. 23
    0
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/exceptions.py
  43. 164
    0
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/helpers.py
  44. 357
    0
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/page.py
  45. 243
    0
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/submission.py
  46. 144
    0
      rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/subreddit.py
  47. BIN
      rtv-git/rtv-git-r226.6d1ac5d-1.src.tar.gz
  48. BIN
      rtv-git/rtv-git-r226.ca41b77-1-any.pkg.tar.xz
  49. 44
    0
      rtv-git/rtv/FETCH_HEAD
  50. 1
    0
      rtv-git/rtv/HEAD
  51. 8
    0
      rtv-git/rtv/config
  52. 1
    0
      rtv-git/rtv/description
  53. 15
    0
      rtv-git/rtv/hooks/applypatch-msg.sample
  54. 24
    0
      rtv-git/rtv/hooks/commit-msg.sample
  55. 8
    0
      rtv-git/rtv/hooks/post-update.sample
  56. 14
    0
      rtv-git/rtv/hooks/pre-applypatch.sample
  57. 49
    0
      rtv-git/rtv/hooks/pre-commit.sample
  58. 53
    0
      rtv-git/rtv/hooks/pre-push.sample
  59. 169
    0
      rtv-git/rtv/hooks/pre-rebase.sample
  60. 36
    0
      rtv-git/rtv/hooks/prepare-commit-msg.sample
  61. 128
    0
      rtv-git/rtv/hooks/update.sample
  62. 6
    0
      rtv-git/rtv/info/exclude
  63. BIN
      rtv-git/rtv/objects/pack/pack-021cc7b98d3db2a0e05722ec1bcbf305483d90c7.idx
  64. BIN
      rtv-git/rtv/objects/pack/pack-021cc7b98d3db2a0e05722ec1bcbf305483d90c7.pack
  65. 45
    0
      rtv-git/rtv/packed-refs

+ 22
- 0
rtv-git/PKGBUILD View File

@@ -0,0 +1,22 @@
# Maintainer: John Jenkins twodopeshaggy@gmail.com

pkgname=rtv-git
pkgver=r226.6d1ac5d
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=('git+https://github.com/michael-lazar/rtv.git')
sha256sums=('SKIP')

pkgver() {
cd "$srcdir/rtv"
printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}

package() {
cd "$srcdir/rtv"
python setup.py install --root="$pkgdir/" --optimize=1
}

BIN
rtv-git/pkg/rtv-git/.MTREE View File


+ 27
- 0
rtv-git/pkg/rtv-git/.PKGINFO View File

@@ -0,0 +1,27 @@
# Generated by makepkg 4.2.1
# using fakeroot version 1.20.2
# Thu Apr 2 03:20:06 UTC 2015
pkgname = rtv-git
pkgver = r226.ca41b77-1
pkgdesc = Browse Reddit from your terminal
url = https://github.com/michael-lazar/rtv
builddate = 1427944806
packager = Unknown Packager
size = 201728
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
- 0
rtv-git/pkg/rtv-git/usr/bin/rtv View File

@@ -0,0 +1,10 @@
#!/bin/python
# EASY-INSTALL-ENTRY-SCRIPT: 'rtv==1.1.2','console_scripts','rtv'
__requires__ = 'rtv==1.1.2'
import sys
from pkg_resources import load_entry_point

if __name__ == '__main__':
sys.exit(
load_entry_point('rtv==1.1.2', 'console_scripts', 'rtv')()
)

+ 138
- 0
rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv-1.1.2-py3.4.egg-info/PKG-INFO View File

@@ -0,0 +1,138 @@
Metadata-Version: 1.1
Name: rtv
Version: 1.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
:``?``: Show the help message
:``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
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
**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``: Comment/reply on the selected item
-------------
Configuration
-------------
RTV will read a configuration file located at **~/.rtv**.
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:
**~/.rtv**
.. code-block:: ini
[rtv]
username=MyUsername
password=MySecretPassword
# Default subreddit
subreddit=CollegeBasketball
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
- 0
rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv-1.1.2-py3.4.egg-info/SOURCES.txt View File

@@ -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
- 0
rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv-1.1.2-py3.4.egg-info/dependency_links.txt View File

@@ -0,0 +1 @@


+ 3
- 0
rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv-1.1.2-py3.4.egg-info/entry_points.txt View File

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


+ 3
- 0
rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv-1.1.2-py3.4.egg-info/requires.txt View File

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

+ 1
- 0
rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv-1.1.2-py3.4.egg-info/top_level.txt View File

@@ -0,0 +1 @@
rtv

+ 6
- 0
rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__init__.py View File

@@ -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'

+ 111
- 0
rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__main__.py View File

@@ -0,0 +1,111 @@
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 *

__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_path = os.path.join(os.path.expanduser('~'), '.rtv')
config = configparser.ConfigParser()
config.read(config_path)

defaults = {}
if config.has_section('rtv'):
defaults = dict(config.items('rtv'))

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()

# 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-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__pycache__/__init__.cpython-34.pyc View File


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


+ 1
- 0
rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/__version__.py View File

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

+ 5
- 0
rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/config.py View File

@@ -0,0 +1,5 @@
"""
Global configuration settings
"""

unicode = False

+ 337
- 0
rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/content.py View File

@@ -0,0 +1,337 @@
import textwrap

import praw
import requests

from .exceptions import SubmissionError, SubredditError
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

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

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']) + len(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):

"""
Grabs 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 = []

@classmethod
def from_name(cls, reddit, name, loader, order='hot', search=None):

if name is None:
name = 'front'

name = name.strip(' /') # Strip leading and trailing backslashes
if name.startswith('r/'):
name = name[2:]

# Grab the display type e.g. "python/new"
if '/' in name:
name, order = name.split('/')

if order == 'hot':
display_name = '/r/{}'.format(name)
else:
display_name = '/r/{}/{}'.format(name, order)
if name == 'front':
if search:
submissions = reddit.search(search, None, order)
elif order == 'hot':
submissions = reddit.get_front_page(limit=None)
elif order == 'top':
submissions = reddit.get_top(limit=None)
elif order == 'rising':
submissions = reddit.get_rising(limit=None)
elif order == 'new':
submissions = reddit.get_new(limit=None)
elif order == 'controversial':
submissions = reddit.get_controversial(limit=None)
else:
raise SubredditError(display_name)
else:
subreddit = reddit.get_subreddit(name)
if search:
submissions = reddit.search(search, name, order)
elif order == 'hot':
submissions = subreddit.get_hot(limit=None)
elif order == 'top':
submissions = subreddit.get_top(limit=None)
elif order == 'rising':
submissions = subreddit.get_rising(limit=None)
elif order == 'new':
submissions = subreddit.get_new(limit=None)
elif order == 'controversial':
submissions = subreddit.get_controversial(limit=None)
else:
raise SubredditError(display_name)

# 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.
content = cls(display_name, submissions, loader)
try:
content.get(0)
except (praw.errors.APIException, requests.HTTPError,
praw.errors.RedirectException):
raise SubredditError(display_name)

return content

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 < 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

+ 285
- 0
rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/curses_helpers.py View File

@@ -0,0 +1,285 @@
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')


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

# Make sure the window is large enough to fit the message
if (box_width > n_cols) or (box_height > n_rows):
curses.flash()
return

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.addstr(index, 1, line)
window.refresh()
stdscr.getch()

window.clear()
window = None
stdscr.refresh()


def show_help(stdscr):
"""
Overlay a message box with the help screen.
"""
show_notification(stdscr, HELP.split("\n"))


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()

+ 53
- 0
rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/docs.py View File

@@ -0,0 +1,53 @@
from .__version__ import __version__

__all__ = ['AGENT', 'SUMMARY', 'AUTH', 'CONTROLS', 'HELP']

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
`r` : Refresh the current page
`q` : Quit the program
`ENTER` or `o` : Open the selected item in the default web browser
`?` : Show this help message

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

Submission Mode
`LEFT` or `h` : Return to subreddit mode
`RIGHT` or `l` : Fold the selected comment, or load additional comments
`c` : Comment/reply 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}
"""

+ 23
- 0
rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/exceptions.py View File

@@ -0,0 +1,23 @@
class SubmissionError(Exception):
"""Submission could not be loaded"""

def __init__(self, url):
self.url = url


class SubredditError(Exception):
"""Subreddit could not be reached"""

def __init__(self, name):
self.name = name


class ProgramError(Exception):
"""Problem executing an external program"""

def __init__(self, name):
self.name = name


class EscapeInterrupt(Exception):
"""Signal that the ESC key has been pressed"""

+ 164
- 0
rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/helpers.py View File

@@ -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)

+ 357
- 0
rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/page.py View File

@@ -0,0 +1,357 @@
import curses
import six
import sys

import praw.errors

from .helpers import clean
from .curses_helpers import Color, show_notification, show_help, text_input

__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):

if isinstance(char, six.string_types) and len(char) == 1:
char = ord(char)

func = self.character_map.get(char)
if func is None:
func = BaseController.character_map.get(char)
if func is None:
func = self.character_map.get(None)
if func is None:
func = BaseController.character_map.get(None)
return func(self.instance, *args, **kwargs)

@classmethod
def register(cls, *chars):
def wrap(f):
for char in chars:
if isinstance(char, six.string_types) and len(char) == 1:
cls.character_map[ord(char)] = f
else:
cls.character_map[char] = f
return f
return wrap


class BasePage(object):

"""
Base terminal viewer incorperates a cursor to navigate content
"""

MIN_HEIGHT = 10
MIN_WIDTH = 20

def __init__(self, stdscr, reddit, content, **kwargs):

self.stdscr = stdscr
self.reddit = reddit
self.content = content
self.nav = Navigator(self.content.get, **kwargs)

self._header_window = None
self._content_window = None
self._subwindows = None

@BaseController.register('q')
def exit(self):
sys.exit()

@BaseController.register('?')
def help(self):
show_help(self.stdscr)

@BaseController.register(curses.KEY_UP, 'k')
def move_cursor_up(self):
self._move_cursor(-1)
self.clear_input_queue()

@BaseController.register(curses.KEY_DOWN, 'j')
def move_cursor_down(self):
self._move_cursor(1)
self.clear_input_queue()

def clear_input_queue(self):
"Clear excessive input caused by the scroll wheel or holding down a key"
self.stdscr.nodelay(1)
while self.stdscr.getch() != -1:
continue
self.stdscr.nodelay(0)

@BaseController.register('a')
def upvote(self):
data = self.content.get(self.nav.absolute_index)
try:
if 'likes' not in data:
pass
elif data['likes']:
data['object'].clear_vote()
data['likes'] = None
else:
data['object'].upvote()
data['likes'] = True
except praw.errors.LoginOrScopeRequired:
show_notification(self.stdscr, ['Login to vote'])

@BaseController.register('z')
def downvote(self):
data = self.content.get(self.nav.absolute_index)
try:
if 'likes' not in data:
pass
if data['likes'] is False:
data['object'].clear_vote()
data['likes'] = None
else:
data['object'].downvote()
data['likes'] = False
except praw.errors.LoginOrScopeRequired:
show_notification(self.stdscr, ['Login to vote'])

def prompt_input(self, prompt):
"""Prompt the user for input"""
attr = curses.A_BOLD | Color.CYAN
n_rows, n_cols = self.stdscr.getmaxyx()
self.stdscr.addstr(n_rows - 1, 0, prompt, attr)
self.stdscr.refresh()
window = self.stdscr.derwin(1, n_cols - len(prompt),
n_rows - 1, len(prompt))
window.attrset(attr)

out = text_input(window)
return out

def draw(self):

n_rows, n_cols = self.stdscr.getmaxyx()
if n_rows < self.MIN_HEIGHT or n_cols < self.MIN_WIDTH:
return

# Note: 2 argument form of derwin breaks PDcurses on Windows 7!
self._header_window = self.stdscr.derwin(1, n_cols, 0, 0)
self._content_window = self.stdscr.derwin(n_rows - 1, n_cols, 1, 0)

self.stdscr.erase()
self._draw_header()
self._draw_content()
self._add_cursor()

@staticmethod
def draw_item(window, data, inverted):
raise NotImplementedError

def _draw_header(self):

n_rows, n_cols = self._header_window.getmaxyx()

self._header_window.erase()
attr = curses.A_REVERSE | curses.A_BOLD | Color.CYAN
self._header_window.bkgd(' ', attr)

sub_name = self.content.name.replace('/r/front', 'Front Page ')
self._header_window.addnstr(0, 0, clean(sub_name), n_cols - 1)

if self.reddit.user is not None:
username = self.reddit.user.name
s_col = (n_cols - len(username) - 1)
# Only print the username if it fits in the empty space on the
# right
if (s_col - 1) >= len(sub_name):
n = (n_cols - s_col - 1)
self._header_window.addnstr(0, s_col, clean(username), n)

self._header_window.refresh()

def _draw_content(self):
"""
Loop through submissions and fill up the content page.
"""

n_rows, n_cols = self._content_window.getmaxyx()
self._content_window.erase()
self._subwindows = []

page_index, cursor_index, inverted = self.nav.position
step = self.nav.step

# If not inverted, align the first submission with the top and draw
# downwards. If inverted, align the first submission with the bottom
# and draw upwards.
current_row = (n_rows - 1) if inverted else 0
available_rows = (n_rows - 1) if inverted else n_rows
for data in self.content.iterate(page_index, step, n_cols - 2):
window_rows = min(available_rows, data['n_rows'])
window_cols = n_cols - data['offset']
start = current_row - window_rows if inverted else current_row
subwindow = self._content_window.derwin(
window_rows, window_cols, start, data['offset'])
attr = self.draw_item(subwindow, data, inverted)
self._subwindows.append((subwindow, attr))
available_rows -= (window_rows + 1) # Add one for the blank line
current_row += step * (window_rows + 1)
if available_rows <= 0:
break
else:
# If the page is not full we need to make sure that it is NOT
# inverted. Unfortunately, this currently means drawing the whole
# page over again. Could not think of a better way to pre-determine
# if the content will fill up the page, given that it is dependent
# on the size of the terminal.
if self.nav.inverted:
self.nav.flip((len(self._subwindows) - 1))
self._draw_content()

self._content_window.refresh()

def _add_cursor(self):
self._edit_cursor(curses.A_REVERSE)

def _remove_cursor(self):
self._edit_cursor(curses.A_NORMAL)

def _move_cursor(self, direction):

self._remove_cursor()

valid, redraw = self.nav.move(direction, len(self._subwindows))
if not valid:
curses.flash()

# Note: ACS_VLINE doesn't like changing the attribute, so always redraw.
# if redraw: self._draw_content()
self._draw_content()
self._add_cursor()

def _edit_cursor(self, attribute=None):

# Don't allow the cursor to go below page index 0
if self.nav.absolute_index < 0:
return

window, attr = self._subwindows[self.nav.cursor_index]
if attr is not None:
attribute |= attr

n_rows, _ = window.getmaxyx()
for row in range(n_rows):
window.chgat(row, 0, 1, attribute)

window.refresh()

+ 243
- 0
rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/submission.py View File

@@ -0,0 +1,243 @@
import curses
import sys
import time

import praw.errors

from .content import SubmissionContent
from .page import BasePage, Navigator, BaseController
from .helpers import clean, open_browser, open_editor
from .curses_helpers import (BULLET, UARROW, DARROW, Color, LoadScreen,
show_notification, text_input)
from .docs import COMMENT_FILE

__all__ = ['SubmissionController', 'SubmissionPage']


class SubmissionController(BaseController):
character_map = {}


class SubmissionPage(BasePage):

def __init__(self, stdscr, reddit, url=None, submission=None):

self.controller = SubmissionController(self)
self.loader = LoadScreen(stdscr)
if url is not None:
content = SubmissionContent.from_url(reddit, url, self.loader)
elif submission is not None:
content = SubmissionContent(submission, self.loader)
else:
raise ValueError('Must specify url or submission')

super(SubmissionPage, self).__init__(stdscr, reddit, content,
page_index=-1)

def loop(self):
self.active = True
while self.active:
self.draw()
cmd = self.stdscr.getch()
self.controller.trigger(cmd)

@SubmissionController.register(curses.KEY_RIGHT, 'l')
def toggle_comment(self):
current_index = self.nav.absolute_index
self.content.toggle(current_index)
if self.nav.inverted:
# Reset the page so that the bottom is at the cursor position.
# This is a workaround to handle if folding the 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')
def exit_submission(self):
self.active = False

@SubmissionController.register(curses.KEY_F5, 'r')
def refresh_content(self):
url = self.content.name
self.content = SubmissionContent.from_url(
self.reddit,
url,
self.loader)
self.nav = Navigator(self.content.get, page_index=-1)

@SubmissionController.register(curses.KEY_ENTER, 10, 'o')
def open_link(self):
# Always open the page for the submission
# May want to expand at some point to open comment permalinks
url = self.content.get(-1)['permalink']
open_browser(url)

@SubmissionController.register('c')
def add_comment(self):
"""
Add a comment on the submission if a header is selected.
Reply to a comment if the comment is selected.
"""
if not self.reddit.is_logged_in():
show_notification(self.stdscr, ["Login to reply"])
return

data = self.content.get(self.nav.absolute_index)
if data['type'] == 'Submission':
content = data['text']
elif data['type'] == 'Comment':
content = data['body']
else:
curses.flash()
return

# Comment out every line of the content
content = '\n'.join(['# |' + line for line in content.split('\n')])
comment_info = COMMENT_FILE.format(
author=data['author'],
type=data['type'].lower(),
content=content)

curses.endwin()
comment_text = open_editor(comment_info)
curses.doupdate()

if not comment_text:
curses.flash()
return
try:
if data['type'] == 'Submission':
data['object'].add_comment(comment_text)
else:
data['object'].reply(comment_text)
except praw.errors.APIException as e:
show_notification(self.stdscr, [e.message])
else:
time.sleep(0.5)
self.refresh_content()

def draw_item(self, win, data, inverted=False):

if data['type'] == 'MoreComments':
return self.draw_more_comments(win, data)
elif data['type'] == 'HiddenComment':
return self.draw_more_comments(win, data)
elif data['type'] == 'Comment':
return self.draw_comment(win, data, inverted=inverted)
else:
return self.draw_submission(win, data)

@staticmethod
def draw_comment(win, data, inverted=False):

n_rows, n_cols = win.getmaxyx()
n_cols -= 1

# Handle the case where the window is not large enough to fit the text.
valid_rows = range(0, n_rows)
offset = 0 if not inverted else -(data['n_rows'] - n_rows)

row = offset
if row in valid_rows:

text = clean('{author} '.format(**data))
attr = curses.A_BOLD
attr |= (Color.BLUE if not data['is_author'] else Color.GREEN)
win.addnstr(row, 1, text, n_cols - 1, attr)

if data['flair']:
text = clean('{flair} '.format(**data))
attr = curses.A_BOLD | Color.YELLOW
win.addnstr(text, n_cols - win.getyx()[1], attr)

if data['likes'] is None:
text, attr = BULLET, curses.A_BOLD
elif data['likes']:
text, attr = UARROW, (curses.A_BOLD | Color.GREEN)
else:
text, attr = DARROW, (curses.A_BOLD | Color.RED)
win.addnstr(text, n_cols - win.getyx()[1], attr)

text = clean(' {score} {created}'.format(**data))
win.addnstr(text, n_cols - win.getyx()[1])

n_body = len(data['split_body'])
for row, text in enumerate(data['split_body'], start=offset + 1):
if row in valid_rows:
text = clean(text)
win.addnstr(row, 1, text, n_cols - 1)

# Unfortunately vline() doesn't support custom color so we have to
# build it one segment at a time.
attr = Color.get_level(data['level'])
for y in range(n_rows):
x = 0
# http://bugs.python.org/issue21088
if (sys.version_info.major,
sys.version_info.minor,
sys.version_info.micro) == (3, 4, 0):
x, y = y, x

win.addch(y, x, curses.ACS_VLINE, attr)

return (attr | curses.ACS_VLINE)

@staticmethod
def draw_more_comments(win, data):

n_rows, n_cols = win.getmaxyx()
n_cols -= 1

text = clean('{body}'.format(**data))
win.addnstr(0, 1, text, n_cols - 1)
text = clean(' [{count}]'.format(**data))
win.addnstr(text, n_cols - win.getyx()[1], curses.A_BOLD)

# Unfortunately vline() doesn't support custom color so we have to
# build it one segment at a time.
attr = Color.get_level(data['level'])
win.addch(0, 0, curses.ACS_VLINE, attr)

return (attr | curses.ACS_VLINE)

@staticmethod
def draw_submission(win, data):

n_rows, n_cols = win.getmaxyx()
n_cols -= 3 # one for each side of the border + one for offset

for row, text in enumerate(data['split_title'], start=1):
text = clean(text)
win.addnstr(row, 1, text, n_cols, curses.A_BOLD)

row = len(data['split_title']) + 1
attr = curses.A_BOLD | Color.GREEN
text = clean('{author}'.format(**data))
win.addnstr(row, 1, text, n_cols, attr)
attr = curses.A_BOLD | Color.YELLOW
text = clean(' {flair}'.format(**data))
win.addnstr(text, n_cols - win.getyx()[1], attr)
text = clean(' {created} {subreddit}'.format(**data))
win.addnstr(text, n_cols - win.getyx()[1])

row = len(data['split_title']) + 2
attr = curses.A_UNDERLINE | Color.BLUE
text = clean('{url}'.format(**data))
win.addnstr(row, 1, text, n_cols, attr)
offset = len(data['split_title']) + 3

# Cut off text if there is not enough room to display the whole post
split_text = data['split_text']
if data['n_rows'] > n_rows:
cutoff = data['n_rows'] - n_rows + 1
split_text = split_text[:-cutoff]
split_text.append('(Not enough space to display)')

for row, text in enumerate(split_text, start=offset):
text = clean(text)
win.addnstr(row, 1, text, n_cols)

row = len(data['split_title']) + len(split_text) + 3
text = clean('{score} {comments}'.format(**data))
win.addnstr(row, 1, text, n_cols, curses.A_BOLD)

win.border()

+ 144
- 0
rtv-git/pkg/rtv-git/usr/lib/python3.4/site-packages/rtv/subreddit.py View File

@@ -0,0 +1,144 @@
import curses

import requests

from .exceptions import SubredditError
from .page import BasePage, Navigator, BaseController
from .submission import SubmissionPage
from .content import SubredditContent
from .helpers import clean, open_browser
from .curses_helpers import (BULLET, UARROW, DARROW, Color, LoadScreen,
show_notification)

__all__ = ['opened_links', 'SubredditController', 'SubredditPage']

# Used to keep track of browsing history across the current session
opened_links = set()


class SubredditController(BaseController):
character_map = {}


class SubredditPage(BasePage):

def __init__(self, stdscr, reddit, name):

self.controller = SubredditController(self)
self.loader = LoadScreen(stdscr)

content = SubredditContent.from_name(reddit, name, self.loader)
super(SubredditPage, self).__init__(stdscr, reddit, content)

def loop(self):
while True:
self.draw()
cmd = self.stdscr.getch()
self.controller.trigger(cmd)

@SubredditController.register(curses.KEY_F5, 'r')
def refresh_content(self, name=None):
name = name or self.content.name

try:
self.content = SubredditContent.from_name(
self.reddit, name, self.loader)
except SubredditError:
show_notification(self.stdscr, ['Invalid subreddit'])
except requests.HTTPError:
show_notification(self.stdscr, ['Could not reach subreddit'])
else:
self.nav = Navigator(self.content.get)

@SubredditController.register('f')
def search_subreddit(self, name=None):
"""Open a prompt to search the subreddit"""
name = name or self.content.name
prompt = 'Search this Subreddit: '
search = self.prompt_input(prompt)
if search is not None:
try:
self.nav.cursor_index = 0
self.content = SubredditContent.from_name(self.reddit, name,
self.loader, search=search)
except IndexError: # if there are no submissions
show_notification(self.stdscr, ['No results found'])

@SubredditController.register('/')
def prompt_subreddit(self):
"""Open a prompt to type in a new subreddit"""
prompt = 'Enter Subreddit: /r/'
name = self.prompt_input(prompt)
if name is not None:
self.refresh_content(name=name)

@SubredditController.register(curses.KEY_RIGHT, 'l')
def open_submission(self):
"Select the current submission to view posts"

data = self.content.get(self.nav.absolute_index)
page = SubmissionPage(self.stdscr, self.reddit, url=data['permalink'])
page.loop()

if data['url'] == 'selfpost':
global opened_links
opened_links.add(data['url_full'])

@SubredditController.register(curses.KEY_ENTER, 10, 'o')
def open_link(self):
"Open a link with the webbrowser"

url = self.content.get(self.nav.absolute_index)['url_full']
open_browser(url)

global opened_links
opened_links.add(url)

@staticmethod
def draw_item(win, data, inverted=False):

n_rows, n_cols = win.getmaxyx()
n_cols -= 1 # Leave space for the cursor in the first column

# Handle the case where the window is not large enough to fit the data.
valid_rows = range(0, n_rows)
offset = 0 if not inverted else -(data['n_rows'] - n_rows)

n_title = len(data['split_title'])
for row, text in enumerate(data['split_title'], start=offset):
if row in valid_rows:
text = clean(text)
win.addnstr(row, 1, text, n_cols - 1, curses.A_BOLD)

row = n_title + offset
if row in valid_rows:
seen = (data['url_full'] in opened_links)
link_color = Color.MAGENTA if seen else Color.BLUE
attr = curses.A_UNDERLINE | link_color
text = clean('{url}'.format(**data))
win.addnstr(row, 1, text, n_cols - 1, attr)

row = n_title + offset + 1
if row in valid_rows:
text = clean('{score} '.format(**data))
win.addnstr(row, 1, text, n_cols - 1)

if data['likes'] is None:
text, attr = BULLET, curses.A_BOLD
elif data['likes']:
text, attr = UARROW, curses.A_BOLD | Color.GREEN
else:
text, attr = DARROW, curses.A_BOLD | Color.RED
win.addnstr(text, n_cols - win.getyx()[1], attr)

text = clean(' {created} {comments}'.format(**data))
win.addnstr(text, n_cols - win.getyx()[1])

row = n_title + offset + 2
if row in valid_rows:
text = clean('{author}'.format(**data))
win.addnstr(row, 1, text, n_cols - 1, curses.A_BOLD)
text = clean(' {subreddit}'.format(**data))
win.addnstr(text, n_cols - win.getyx()[1], Color.YELLOW)
text = clean(' {flair}'.format(**data))
win.addnstr(text, n_cols - win.getyx()[1], Color.RED)

BIN
rtv-git/rtv-git-r226.6d1ac5d-1.src.tar.gz View File


BIN
rtv-git/rtv-git-r226.ca41b77-1-any.pkg.tar.xz View File


+ 44
- 0
rtv-git/rtv/FETCH_HEAD View File

@@ -0,0 +1,44 @@
c4d97f939fad823f6575a9e812f3dc54fad3ad5b not-for-merge branch 'controller' of https://github.com/michael-lazar/rtv
ca41b77d83abc415cd60060c43b65a3580d24ed4 not-for-merge branch 'master' of https://github.com/michael-lazar/rtv
d5f68215cdbc5906c19a64c0bab2e3b737a9bb29 not-for-merge branch 'pypi' of https://github.com/michael-lazar/rtv
7c7606d288b0ec306577cb1c79b3ea9cb90e4caf not-for-merge 'refs/pull/1/head' of https://github.com/michael-lazar/rtv
98fc02e28e0147509ddfe4e601a97859a0e47cfc not-for-merge 'refs/pull/11/head' of https://github.com/michael-lazar/rtv
6a5da9f1ec1305ce2fdf4a1e3ec3f90afc6772cb not-for-merge 'refs/pull/13/head' of https://github.com/michael-lazar/rtv
9fcb16b3e48467566140db4a37817cbe82116eb9 not-for-merge 'refs/pull/14/head' of https://github.com/michael-lazar/rtv
b5bfd40b1a0e1119cdbd7e6d5b13b11fcb260fc5 not-for-merge 'refs/pull/16/head' of https://github.com/michael-lazar/rtv
f1d4b5909b8490551b6fc8ff70cc322cab42ce48 not-for-merge 'refs/pull/16/merge' of https://github.com/michael-lazar/rtv
cb5d730d39943b130abec75d4c8c20e54ec7ac54 not-for-merge 'refs/pull/18/head' of https://github.com/michael-lazar/rtv
505a49552dd3f016ec4993f237a7bbe2b407b9cf not-for-merge 'refs/pull/18/merge' of https://github.com/michael-lazar/rtv
73af45f4a20cda0392dad41dd3e6f37d4c470ec3 not-for-merge 'refs/pull/19/head' of https://github.com/michael-lazar/rtv
f25ab62d48f62c027e1f8d6424d0d2ddfccd4fa2 not-for-merge 'refs/pull/20/head' of https://github.com/michael-lazar/rtv
f08693988fd087a316456306459f114e637b4be6 not-for-merge 'refs/pull/22/head' of https://github.com/michael-lazar/rtv
09ed21b75c7bf0291b163b8a72c5da62272b2f12 not-for-merge 'refs/pull/29/head' of https://github.com/michael-lazar/rtv
3379a99e0d9502d9205dc30e69fc56f585dc7a25 not-for-merge 'refs/pull/30/head' of https://github.com/michael-lazar/rtv
af86a3ad67747ea42fe2ed6a40d582c10fce319e not-for-merge 'refs/pull/31/head' of https://github.com/michael-lazar/rtv
abadbd7037503973ce009b7c5e8c79150b1c375d not-for-merge 'refs/pull/33/head' of https://github.com/michael-lazar/rtv
34dfa53b9f4f05a8afd051eda55dc4c79371b6dc not-for-merge 'refs/pull/34/head' of https://github.com/michael-lazar/rtv
9744641d32e971f50aa90ce8f564ead70f9a1788 not-for-merge 'refs/pull/37/head' of https://github.com/michael-lazar/rtv
2a21064438cb97d633696b7ea2a1d3a42a35fc76 not-for-merge 'refs/pull/37/merge' of https://github.com/michael-lazar/rtv
da851ec0485f22e3e3df0332ce55c0ee650a9d9c not-for-merge 'refs/pull/38/head' of https://github.com/michael-lazar/rtv
60dfc858552940d7902812e316f16383b811aa88 not-for-merge 'refs/pull/40/head' of https://github.com/michael-lazar/rtv
eea306a980461d1aefff908aded5a78decaf1b36 not-for-merge 'refs/pull/41/head' of https://github.com/michael-lazar/rtv
571e1e82c57519b661b1a7bdc02a541985c34f4d not-for-merge 'refs/pull/43/head' of https://github.com/michael-lazar/rtv
15aba8b0eb91d5182cfa459ec516ed774834dc61 not-for-merge 'refs/pull/44/head' of https://github.com/michael-lazar/rtv
32fd689544397653c8e0eca58d5d082a12075057 not-for-merge 'refs/pull/45/head' of https://github.com/michael-lazar/rtv
7d461ffc58be34b4bbf671eddb5f5899e630ab38 not-for-merge 'refs/pull/46/head' of https://github.com/michael-lazar/rtv
80cdf036ef3051c326ddcb19c193c0088fab09c8 not-for-merge 'refs/pull/48/head' of https://github.com/michael-lazar/rtv
601336ad37f4d4d357af8e3e02e3cb19bfb8696f not-for-merge 'refs/pull/49/head' of https://github.com/michael-lazar/rtv
bf512d22f5dac79f355adfee20fe827bff7d55cd not-for-merge 'refs/pull/49/merge' of https://github.com/michael-lazar/rtv
e33c4315126dcd3d5c05584f2815c9c5f4a680d0 not-for-merge 'refs/pull/51/head' of https://github.com/michael-lazar/rtv
bdecf095ef593745b8a725b0eec6511d03587642 not-for-merge 'refs/pull/51/merge' of https://github.com/michael-lazar/rtv
e31997d392b10e763a52f26f4ad19dbdfefd8a4e not-for-merge 'refs/pull/52/head' of https://github.com/michael-lazar/rtv
ac98f564a38ad61fe93a18dd802332af7383c2da not-for-merge 'refs/pull/53/head' of https://github.com/michael-lazar/rtv
bdce9cb4b5484db9abda70f908798f4d7a2c7da0 not-for-merge 'refs/pull/54/head' of https://github.com/michael-lazar/rtv
37c43559df699d7119df9e83d8c39bbd9194d63f not-for-merge 'refs/pull/56/head' of https://github.com/michael-lazar/rtv
49977151d1f0e3ca6b14eb1e63bf7380021c76ce not-for-merge 'refs/pull/58/head' of https://github.com/michael-lazar/rtv
9c7d8a34be8861eaca6b8335676a79af0d373342 not-for-merge 'refs/pull/58/merge' of https://github.com/michael-lazar/rtv
edcee5c82a14ecfc6d9eca039f06580c1165c147 not-for-merge 'refs/pull/59/head' of https://github.com/michael-lazar/rtv
cac1a41951439825c40274ac3c6aee638e2c4da9 not-for-merge 'refs/pull/59/merge' of https://github.com/michael-lazar/rtv
dd1aa1bd4fac1ffe1c0ac72c84949cc6545a5ce4 not-for-merge 'refs/pull/60/head' of https://github.com/michael-lazar/rtv
59b657de37a26d7c86fe47a517b60b576e11186a not-for-merge 'refs/pull/61/head' of https://github.com/michael-lazar/rtv
8558475ae4b4f58c34a3f25a324ea8c917a35ac7 not-for-merge 'refs/pull/61/merge' of https://github.com/michael-lazar/rtv

+ 1
- 0
rtv-git/rtv/HEAD View File

@@ -0,0 +1 @@
ref: refs/heads/master

+ 8
- 0
rtv-git/rtv/config View File

<
@@ -0,0 +1,8 @@
[core]
repositoryformatversion = 0
filemode = true