diff --git a/README.rst b/README.rst index 020a3b9..8505e40 100644 --- a/README.rst +++ b/README.rst @@ -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. diff --git a/rtv/__main__.py b/rtv/__main__.py index c571874..628e570 100644 --- a/rtv/__main__.py +++ b/rtv/__main__.py @@ -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 diff --git a/rtv/content.py b/rtv/content.py index a42fe23..45f88ae 100644 --- a/rtv/content.py +++ b/rtv/content.py @@ -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 \ No newline at end of file diff --git a/rtv/curses_helpers.py b/rtv/curses_helpers.py index d6337ca..f8142fd 100644 --- a/rtv/curses_helpers.py +++ b/rtv/curses_helpers.py @@ -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): diff --git a/rtv/docs.py b/rtv/docs.py index 33002fe..b06dc1f 100644 --- a/rtv/docs.py +++ b/rtv/docs.py @@ -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} -""" +""" \ No newline at end of file diff --git a/rtv/exceptions.py b/rtv/exceptions.py index 9f2e61c..dece7fe 100644 --- a/rtv/exceptions.py +++ b/rtv/exceptions.py @@ -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""" diff --git a/rtv/page.py b/rtv/page.py index 70d13f6..a708624 100644 --- a/rtv/page.py +++ b/rtv/page.py @@ -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() \ No newline at end of file diff --git a/rtv/submission.py b/rtv/submission.py index db32633..3712932 100644 --- a/rtv/submission.py +++ b/rtv/submission.py @@ -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() \ No newline at end of file diff --git a/rtv/subreddit.py b/rtv/subreddit.py index 14580e9..5eb3037 100644 --- a/rtv/subreddit.py +++ b/rtv/subreddit.py @@ -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) \ No newline at end of file