Docstring wordings, refactored /r/me handling, updated README.

This commit is contained in:
Michael Lazar
2015-04-05 17:11:25 -07:00
parent cd6047b5a9
commit 9ee21fe1ce
9 changed files with 155 additions and 145 deletions

View File

@@ -61,7 +61,8 @@ RTV currently supports browsing both subreddits and individual submissions. In e
:``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
:``u``: Login and logout of your user account
:``?``: Show the help screen
:``q``: Quit
**Subreddit Mode**
@@ -71,7 +72,7 @@ In subreddit mode you can browse through the top submissions on either the front
:``►`` 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 Submission to the current subreddit
:``p``: Post a new submission to the current subreddit
The ``/`` prompt accepts subreddits in the following formats
@@ -79,7 +80,7 @@ The ``/`` prompt accepts subreddits in the following formats
* ``/r/python/new``
* ``/r/python+linux`` supports multireddits
* ``/r/front`` will redirect to the front page
* ``/r/me`` will show you your submissions on all subs
* ``/r/me`` will display your submissions
**Submission Mode**
@@ -87,7 +88,7 @@ In submission mode you can view the self text for a submission and browse commen
:``◄`` or ``h``: Return to subreddit mode
:``►`` or ``l``: Fold the selected comment, or load additional comments
:``c``: Comment/reply on the selected item
:``c``: Post a new comment on the selected item
-------------
Configuration
@@ -104,10 +105,20 @@ Example config:
[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.

View File

@@ -45,6 +45,9 @@ def load_config():
if config.has_section('rtv'):
defaults = dict(config.items('rtv'))
if 'unicode' in defaults:
defaults['unicode'] = config.getboolean('rtv', 'unicode')
return defaults

View File

@@ -3,7 +3,7 @@ import textwrap
import praw
import requests
from .exceptions import SubmissionError, SubredditError
from .exceptions import SubmissionError, SubredditError, AccountError
from .helpers import humanize_timestamp, wrap_text, strip_subreddit_url
__all__ = ['SubredditContent', 'SubmissionContent']
@@ -114,18 +114,12 @@ class BaseContent(object):
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):
def __init__(self, submission, loader, indent_size=2, max_indent_level=4):
self.indent_size = indent_size
self.max_indent_level = max_indent_level
@@ -138,13 +132,7 @@ class SubmissionContent(BaseContent):
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):
def from_url(cls, reddit, url, loader, indent_size=2, max_indent_level=4):
try:
with loader():
@@ -165,10 +153,9 @@ class SubmissionContent(BaseContent):
elif index == -1:
data = self._submission_data
data['split_title'] = textwrap.wrap(data['title'],
width=n_cols -2)
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['n_rows'] = len(data['split_title'] + data['split_text']) + 5
data['offset'] = 0
else:
@@ -233,9 +220,8 @@ class SubmissionContent(BaseContent):
class SubredditContent(BaseContent):
"""
Grabs a subreddit from PRAW and lazily stores submissions to an internal
Grab a subreddit from PRAW and lazily stores submissions to an internal
list for repeat access.
"""
@@ -251,7 +237,7 @@ class SubredditContent(BaseContent):
# there is is no other way to check things like multireddits that
# don't have a real corresponding subreddit object.
try:
content.get(0)
self.get(0)
except (praw.errors.APIException, requests.HTTPError,
praw.errors.RedirectException):
raise SubredditError(display_name)
@@ -259,8 +245,6 @@ class SubredditContent(BaseContent):
@classmethod
def from_name(cls, reddit, name, loader, order='hot', query=None):
name = name if name else 'front'
if order not in ['hot', 'top', 'rising', 'new', 'controversial']:
raise SubredditError(display_name)
@@ -268,7 +252,7 @@ class SubredditContent(BaseContent):
if name.startswith('r/'):
name = name[2:]
# Grab the display type e.g. "python/new"
# Grab the display order e.g. "python/new"
if '/' in name:
name, order = name.split('/')
@@ -276,57 +260,50 @@ class SubredditContent(BaseContent):
if order != 'hot':
display_name += '/{}'.format(order)
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
}
if name == 'me':
if not self.reddit.is_logged_in():
raise AccountError
else:
submissions = reddit.user.get_submitted(sort=order)
if query:
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)
@classmethod
def from_redditor(cls, reddit, loader, order='new'):
submissions = reddit.user.get_submitted(sort=order)
display_name = '/r/me'
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`
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)
@@ -342,4 +319,4 @@ class SubredditContent(BaseContent):
data['n_rows'] = len(data['split_title']) + 3
data['offset'] = 0
return data
return data

View File

@@ -66,8 +66,11 @@ def show_help(stdscr):
"""
Overlay a message box with the help screen.
"""
show_notification(stdscr, HELP.split("\n"))
curses.endwin()
print(HELP)
raw_input('Press Enter to continue')
curses.doupdate()
class LoadScreen(object):

View File

@@ -2,7 +2,9 @@ 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__)
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
@@ -28,22 +30,22 @@ 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
`u` : Log in
`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 Submission to 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` : Comment/reply on the selected item
`c` : Post a new comment on the selected item
"""
COMMENT_FILE = """
@@ -59,7 +61,7 @@ SUBMISSION_FILE = """
# and an empty field aborts the submission.
#
# The first line will be interpreted as the title
# Following lines will be interpreted as the content
# The following lines will be interpreted as the content
#
# Posting to /r/{name}
"""
"""

View File

@@ -1,23 +1,31 @@
class SubmissionError(Exception):
"""Submission could not be loaded"""
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(Exception):
"""Subreddit could not be reached"""
class SubredditError(RTVError):
"Subreddit could not be reached"
def __init__(self, name):
self.name = name
class ProgramError(Exception):
"""Problem executing an external program"""
class ProgramError(RTVError):
"Problem executing an external program"
def __init__(self, name):
self.name = name
class EscapeInterrupt(Exception):
"""Signal that the ESC key has been pressed"""

View File

@@ -12,7 +12,6 @@ __all__ = ['Navigator']
class Navigator(object):
"""
Handles math behind cursor movement and screen paging.
"""
@@ -87,6 +86,7 @@ class Navigator(object):
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
@@ -103,7 +103,6 @@ class Navigator(object):
class BaseController(object):
"""
Event handler for triggering functions with curses keypresses.
@@ -153,7 +152,6 @@ class BaseController(object):
class BasePage(object):
"""
Base terminal viewer incorperates a cursor to navigate content
"""
@@ -192,6 +190,7 @@ class BasePage(object):
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
@@ -210,7 +209,7 @@ class BasePage(object):
data['object'].upvote()
data['likes'] = True
except praw.errors.LoginOrScopeRequired:
show_notification(self.stdscr, ['Login to vote'])
show_notification(self.stdscr, ['Not logged in'])
@BaseController.register('z')
def downvote(self):
@@ -225,13 +224,13 @@ class BasePage(object):
data['object'].downvote()
data['likes'] = False
except praw.errors.LoginOrScopeRequired:
show_notification(self.stdscr, ['Login to vote'])
show_notification(self.stdscr, ['Not logged in'])
@BaseController.register('u')
def login(self):
"""
Prompt to log into the user's account. Log out if the user is already
logged in.
Prompt to log into the user's account, or log out of the current
account.
"""
if self.reddit.is_logged_in():
@@ -252,9 +251,7 @@ class BasePage(object):
show_notification(self.stdscr, ['Logged in'])
def logout(self):
"""
Prompt to log out of the user's account.
"""
"Prompt to log out of the user's account."
ch = self.prompt_input("Log out? (y/n):")
if ch == 'y':
@@ -264,7 +261,8 @@ class BasePage(object):
curses.flash()
def prompt_input(self, prompt, hide=False):
"""Prompt the user for input"""
"Prompt the user for input"
attr = curses.A_BOLD | Color.CYAN
n_rows, n_cols = self.stdscr.getmaxyx()
@@ -397,4 +395,4 @@ class BasePage(object):
for row in range(n_rows):
window.chgat(row, 0, 1, attribute)
window.refresh()
window.refresh()

View File

@@ -24,9 +24,9 @@ class SubmissionPage(BasePage):
self.controller = SubmissionController(self)
self.loader = LoadScreen(stdscr)
if url is not None:
if url:
content = SubmissionContent.from_url(reddit, url, self.loader)
elif submission is not None:
elif submission:
content = SubmissionContent(submission, self.loader)
else:
raise ValueError('Must specify url or submission')
@@ -35,6 +35,8 @@ class SubmissionPage(BasePage):
content, page_index=-1)
def loop(self):
"Main control loop"
self.active = True
while self.active:
self.draw()
@@ -43,6 +45,8 @@ class SubmissionPage(BasePage):
@SubmissionController.register(curses.KEY_RIGHT, 'l')
def toggle_comment(self):
"Toggle the selected comment tree between visible and hidden"
current_index = self.nav.absolute_index
self.content.toggle(current_index)
if self.nav.inverted:
@@ -53,20 +57,24 @@ class SubmissionPage(BasePage):
@SubmissionController.register(curses.KEY_LEFT, 'h')
def exit_submission(self):
"Close the submission and return to the subreddit page"
self.active = False
@SubmissionController.register(curses.KEY_F5, 'r')
def refresh_content(self):
url = self.content.name
"Re-download comments reset the page index"
self.content = SubmissionContent.from_url(
self.reddit,
url,
self.content.name,
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
"Open the current submission page with the webbrowser"
# May want to expand at some point to open comment permalinks
url = self.content.get(-1)['permalink']
open_browser(url)
@@ -74,11 +82,12 @@ class SubmissionPage(BasePage):
@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.
Add a top-level comment if the submission is selected, or reply to the
selected comment.
"""
if not self.reddit.is_logged_in():
show_notification(self.stdscr, ["Login to reply"])
show_notification(self.stdscr, ['Not logged in'])
return
data = self.content.get(self.nav.absolute_index)
@@ -100,19 +109,19 @@ class SubmissionPage(BasePage):
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])
except praw.errors.APIException:
curses.flash()
else:
time.sleep(0.5)
time.sleep(2.0)
self.refresh_content()
def draw_item(self, win, data, inverted=False):
@@ -248,4 +257,4 @@ class SubmissionPage(BasePage):
text, attr = GOLD, (curses.A_BOLD | Color.YELLOW)
win.addnstr(text, n_cols - win.getyx()[1], attr)
win.border()
win.border()

View File

@@ -3,7 +3,7 @@ import time
import requests
import praw
from .exceptions import SubredditError
from .exceptions import SubredditError, AccountError
from .page import BasePage, Navigator, BaseController
from .submission import SubmissionPage
from .content import SubredditContent
@@ -33,6 +33,8 @@ class SubredditPage(BasePage):
super(SubredditPage, self).__init__(stdscr, reddit, content)
def loop(self):
"Main control loop"
while True:
self.draw()
cmd = self.stdscr.getch()
@@ -40,13 +42,14 @@ class SubredditPage(BasePage):
@SubredditController.register(curses.KEY_F5, 'r')
def refresh_content(self, name=None):
"Re-download all submissions and reset the page index"
name = name or self.content.name
if name == 'me' or name == '/r/me':
self.redditor_profile()
return
try:
self.content = SubredditContent.from_name(
self.reddit, name, self.loader)
except AccountError:
show_notification(self.stdscr, ['Not logged in'])
except SubredditError:
show_notification(self.stdscr, ['Invalid subreddit'])
except requests.HTTPError:
@@ -56,36 +59,30 @@ class SubredditPage(BasePage):
@SubredditController.register('f')
def search_subreddit(self, name=None):
"""Open a prompt to search the subreddit"""
"Open a prompt to search the given subreddit"
name = name or self.content.name
prompt = 'Search this Subreddit: '
prompt = 'Search:'
query = self.prompt_input(prompt)
if query is not None:
try:
self.nav.cursor_index = 0
self.content = SubredditContent.from_name(self.reddit, name,
self.loader, query=query)
except IndexError: # if there are no submissions
show_notification(self.stdscr, ['No results found'])
if query is None:
return
try:
self.content = SubredditContent.from_name(
self.reddit, name, self.loader, query=query)
except IndexError: # if there are no submissions
show_notification(self.stdscr, ['No results found'])
else:
self.nav = Navigator(self.content.get)
@SubredditController.register('/')
def prompt_subreddit(self):
"""Open a prompt to type in a new subreddit"""
"Open a prompt to navigate to a different subreddit"
prompt = 'Enter Subreddit: /r/'
name = self.prompt_input(prompt)
if name is not None:
self.refresh_content(name=name)
def redditor_profile(self):
if self.reddit.is_logged_in():
try:
self.content = SubredditContent.from_redditor(
self.reddit, self.loader)
except requests.HTTPError:
show_notification(self.stdscr, ['Could not reach subreddit'])
else:
show_notification(self.stdscr, ['Log in to view your submissions'])
@SubredditController.register(curses.KEY_RIGHT, 'l')
def open_submission(self):
"Select the current submission to view posts"
@@ -110,19 +107,18 @@ class SubredditPage(BasePage):
@SubredditController.register('p')
def post_submission(self):
# Abort if user isn't logged in
"Post a new submission to the given subreddit"
if not self.reddit.is_logged_in():
show_notification(self.stdscr, ['Login to reply'])
show_notification(self.stdscr, ['Not logged in'])
return
subreddit = self.reddit.get_subreddit(self.content.name)
# Make sure it is a valid subreddit for submission
# Strips the subreddit to just the name
# Make sure it is a valid subreddit for submission
subreddit = self.reddit.get_subreddit(self.content.name)
sub = str(subreddit).split('/')[2]
if '+' in sub or sub == 'all' or sub == 'front':
message = 'Can\'t post to /r/{0}'.format(sub)
show_notification(self.stdscr, [message])
if '+' in sub or sub in ('all', 'front', 'me'):
show_notification(self.stdscr, ['Invalid subreddit'])
return
# Open the submission window
@@ -131,19 +127,22 @@ class SubredditPage(BasePage):
submission_text = open_editor(submission_info)
curses.doupdate()
# Abort if there is no content
# Validate the submission content
if not submission_text:
curses.flash()
return
if '\n' not in submission_text:
show_notification(self.stdscr, ['No content'])
return
try:
title, content = submission_text.split('\n', 1)
self.reddit.submit(sub, title, text=content)
except praw.errors.APIException as e:
show_notification(self.stdscr, [e.message])
except ValueError:
show_notification(self.stdscr, ['No post content! Post aborted.'])
except praw.errors.APIException:
curses.flash()
else:
time.sleep(0.5)
time.sleep(2.0)
self.refresh_content()
@staticmethod
@@ -197,4 +196,4 @@ class SubredditPage(BasePage):
text = clean(u' {subreddit}'.format(**data))
win.addnstr(text, n_cols - win.getyx()[1], Color.YELLOW)
text = clean(u' {flair}'.format(**data))
win.addnstr(text, n_cols - win.getyx()[1], Color.RED)
win.addnstr(text, n_cols - win.getyx()[1], Color.RED)