diff --git a/README.rst b/README.rst index 4aa9083..59c68bc 100644 --- a/README.rst +++ b/README.rst @@ -28,7 +28,7 @@ Installation Install using pip .. code-block:: bash - + $ sudo pip install rtv Or clone the repository @@ -47,12 +47,12 @@ The installation will place a script in the system path $ rtv --help ===== -Usage +Usage ===== RTV supports browsing both subreddits and submission comments. -Navigating is simple and intuitive. +Navigating is simple and intuitive. Move the cursor using either the arrow keys or *Vim* style movement. Move **up** and **down** to scroll through the page. Move **right** to view the selected submission, and **left** to exit the submission. @@ -84,6 +84,7 @@ Once you are logged in your username will appear in the top-right corner of the :``c``: Compose a new post or comment :``e``: Edit an existing post or comment :``d``: Delete an existing post or comment +:``s``: Open/close subscribed subreddits list -------------- Subreddit Mode @@ -96,7 +97,7 @@ In subreddit mode you can browse through the top submissions on either the front :``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 @@ -126,7 +127,7 @@ You can specify which text editor you would like to use by setting the ``$RTV_ED .. code-block:: bash $ export RTV_EDITOR=gedit - + If no editor is specified, RTV will fallback to the system's default ``$EDITOR``, and finally to ``nano``. ----------- diff --git a/rtv/__main__.py b/rtv/__main__.py index af50d7d..8d6e561 100644 --- a/rtv/__main__.py +++ b/rtv/__main__.py @@ -10,7 +10,7 @@ import praw.errors from six.moves import configparser from . import config -from .exceptions import SubmissionError, SubredditError, ProgramError +from .exceptions import SubmissionError, SubredditError, SubscriptionError, ProgramError from .curses_helpers import curses_session from .submission import SubmissionPage from .subreddit import SubredditPage diff --git a/rtv/content.py b/rtv/content.py index a8082e6..afa64bd 100644 --- a/rtv/content.py +++ b/rtv/content.py @@ -4,10 +4,10 @@ import praw import requests import re -from .exceptions import SubmissionError, SubredditError, AccountError +from .exceptions import SubmissionError, SubredditError, SubscriptionError, AccountError from .helpers import humanize_timestamp, wrap_text, strip_subreddit_url -__all__ = ['SubredditContent', 'SubmissionContent'] +__all__ = ['SubredditContent', 'SubmissionContent', 'SubscriptionContent'] _logger = logging.getLogger(__name__) @@ -149,6 +149,20 @@ class BaseContent(object): return data + @staticmethod + def strip_praw_subscription(subscription): + """ + Parse through a subscription and return a dict with data ready to be + displayed through the terminal. + """ + + data = {} + data['object'] = subscription + data['type'] = 'Subscription' + data['name'] = "/r/" + subscription.display_name + data['title'] = subscription.title + + return data class SubmissionContent(BaseContent): """ @@ -369,3 +383,49 @@ class SubredditContent(BaseContent): data['offset'] = 0 return data + +class SubscriptionContent(BaseContent): + + def __init__(self, subscriptions, loader): + + self.name = "Subscriptions" + self.order = None + self._loader = loader + self._subscriptions = subscriptions + self._subscription_data = [] + + @classmethod + def from_user(cls, reddit, loader): + try: + with loader(): + subscriptions = reddit.get_my_subreddits(limit=None) + except praw.errors.APIException: + raise SubscriptionError() + + return cls(subscriptions, loader) + + def get(self, index, n_cols=70): + """ + Grab the `i`th subscription, with the title field formatted to fit inside + of a window of width `n_cols` + """ + + if index < 0: + raise IndexError + + while index >= len(self._subscription_data): + try: + with self._loader(): + subscription = next(self._subscriptions) + except StopIteration: + raise IndexError + else: + data = self.strip_praw_subscription(subscription) + self._subscription_data.append(data) + + data = self._subscription_data[index] + data['split_title'] = wrap_text(data['title'], width=n_cols) + data['n_rows'] = len(data['split_title']) + 1 + data['offset'] = 0 + + return data diff --git a/rtv/curses_helpers.py b/rtv/curses_helpers.py index f9489fd..0d48684 100644 --- a/rtv/curses_helpers.py +++ b/rtv/curses_helpers.py @@ -24,7 +24,7 @@ ESCAPE = 27 def get_gold(): """ - Return the guilded symbol. + Return the gilded symbol. """ symbol = u'\u272A' if config.unicode else '*' diff --git a/rtv/docs.py b/rtv/docs.py index f25e178..8775728 100644 --- a/rtv/docs.py +++ b/rtv/docs.py @@ -42,6 +42,7 @@ Authenticated Commands `c` : Compose a new post or comment `e` : Edit an existing post or comment `d` : Delete an existing post or comment + `s` : Open/close subscribed subreddits list Subreddit Mode `l` or `RIGHT` : Enter the selected submission diff --git a/rtv/exceptions.py b/rtv/exceptions.py index dece7fe..80fb76f 100644 --- a/rtv/exceptions.py +++ b/rtv/exceptions.py @@ -24,6 +24,10 @@ class SubredditError(RTVError): self.name = name +class SubscriptionError(RTVError): + "Subscriptions could not be fetched" + + class ProgramError(RTVError): "Problem executing an external program" diff --git a/rtv/subreddit.py b/rtv/subreddit.py index ed324bc..a869a3b 100644 --- a/rtv/subreddit.py +++ b/rtv/subreddit.py @@ -8,6 +8,7 @@ import requests from .exceptions import SubredditError, AccountError from .page import BasePage, Navigator, BaseController from .submission import SubmissionPage +from .subscriptions import SubscriptionPage from .content import SubredditContent from .helpers import open_browser, open_editor, strip_subreddit_url from .docs import SUBMISSION_FILE @@ -164,6 +165,23 @@ class SubredditPage(BasePage): page.loop() self.refresh_content() + @SubredditController.register('s') + def open_subscriptions(self): + "Open user subscriptions page" + + if not self.reddit.is_logged_in(): + show_notification(self.stdscr, ['Not logged in']) + return + + # Open subscriptions page + page = SubscriptionPage(self.stdscr, self.reddit) + page.loop() + + # When user has chosen a subreddit in the subscriptions list, + # refresh content with the selected subreddit + if page.selected_subreddit_data is not None: + self.refresh_content(name=page.selected_subreddit_data['name']) + @staticmethod def draw_item(win, data, inverted=False): diff --git a/rtv/subscriptions.py b/rtv/subscriptions.py new file mode 100644 index 0000000..8ef2094 --- /dev/null +++ b/rtv/subscriptions.py @@ -0,0 +1,73 @@ +import curses +import sys +import time +import logging + +from .content import SubscriptionContent +from .page import BasePage, Navigator, BaseController +from .curses_helpers import (Color, LoadScreen, add_line) + +__all__ = ['SubscriptionController', 'SubscriptionPage'] +_logger = logging.getLogger(__name__) + +class SubscriptionController(BaseController): + character_map = {} + +class SubscriptionPage(BasePage): + + def __init__(self, stdscr, reddit): + + self.controller = SubscriptionController(self) + self.loader = LoadScreen(stdscr) + self.selected_subreddit_data = None + + content = SubscriptionContent.from_user(reddit, self.loader) + super(SubscriptionPage, self).__init__(stdscr, reddit, content) + + def loop(self): + "Main control loop" + + self.active = True + while self.active: + self.draw() + cmd = self.stdscr.getch() + self.controller.trigger(cmd) + + @SubscriptionController.register(curses.KEY_F5, 'r') + def refresh_content(self): + "Re-download all subscriptions and reset the page index" + + self.content = SubscriptionContent.get_list(self.reddit, self.loader) + self.nav = Navigator(self.content.get) + + @SubscriptionController.register(curses.KEY_ENTER, 10, curses.KEY_RIGHT) + def store_selected_subreddit(self): + "Store the selected subreddit and return to the subreddit page" + + self.selected_subreddit_data = self.content.get(self.nav.absolute_index) + self.active = False + + @SubscriptionController.register(curses.KEY_LEFT, 'h', 's') + def close_subscriptions(self): + "Close subscriptions and return to the subreddit page" + + self.active = False + + @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) + + row = offset + if row in valid_rows: + attr = curses.A_BOLD | Color.YELLOW + add_line(win, u'{name}'.format(**data), row, 1, attr) + + row = offset + 1 + for row, text in enumerate(data['split_title'], start=row): + if row in valid_rows: + add_line(win, text, row, 1) \ No newline at end of file