diff --git a/README.rst b/README.rst index 873bb53..85f21f4 100644 --- a/README.rst +++ b/README.rst @@ -59,7 +59,7 @@ RTV currently supports browsing both subreddits and individual submissions. In e :``▲``/``▼`` or ``j``/``k``: Scroll to the prev/next item :``a``/``z``: Upvote/downvote the selected item -:``o``: Open the selected item in the default web browser +:``ENTER`` or ``o``: Open the selected item in the default web browser :``r``: Refresh the current page :``?``: Show the help message :``q``: Quit diff --git a/rtv/docs.py b/rtv/docs.py index 524ee66..4089ee7 100644 --- a/rtv/docs.py +++ b/rtv/docs.py @@ -32,7 +32,7 @@ Global Commands `a/z` : Upvote/downvote the selected item `r` : Refresh the current page `q` : Quit the program - `o` : Open the selected item in the default web browser + `ENTER` or `o` : Open the selected item in the default web browser `?` : Show this help message Subreddit Mode diff --git a/rtv/page.py b/rtv/page.py index 455d9c4..d1f31eb 100644 --- a/rtv/page.py +++ b/rtv/page.py @@ -1,9 +1,11 @@ import curses +import six +import sys import praw.errors from .helpers import clean -from .curses_helpers import Color, show_notification +from .curses_helpers import Color, show_notification, show_help __all__ = ['Navigator'] @@ -96,6 +98,55 @@ class Navigator(object): 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 @@ -115,11 +166,23 @@ class BasePage(object): 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" @@ -128,8 +191,8 @@ class BasePage(object): 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: @@ -143,8 +206,8 @@ class BasePage(object): 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: diff --git a/rtv/submission.py b/rtv/submission.py index 5e8198c..a854ddd 100644 --- a/rtv/submission.py +++ b/rtv/submission.py @@ -5,20 +5,25 @@ import time import praw.errors from .content import SubmissionContent -from .page import BasePage, Navigator +from .page import BasePage, Navigator, BaseController from .helpers import clean, open_browser, open_editor from .curses_helpers import (BULLET, UARROW, DARROW, Color, LoadScreen, - show_help, show_notification, text_input) + show_notification, text_input) from .docs import COMMENT_FILE -__all__ = ['SubmissionPage'] +__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: @@ -30,59 +35,14 @@ class SubmissionPage(BasePage): page_index=-1) def loop(self): - - self.draw() - - while True: + self.active = True + while self.active: + self.draw() cmd = self.stdscr.getch() + self.controller.trigger(cmd) - if cmd in (curses.KEY_UP, ord('k')): - self.move_cursor_up() - self.clear_input_queue() - - elif cmd in (curses.KEY_DOWN, ord('j')): - self.move_cursor_down() - self.clear_input_queue() - - elif cmd in (curses.KEY_RIGHT, curses.KEY_ENTER, ord('l')): - self.toggle_comment() - self.draw() - - elif cmd in (curses.KEY_LEFT, ord('h')): - break - - elif cmd == ord('o'): - self.open_link() - self.draw() - - elif cmd in (curses.KEY_F5, ord('r')): - self.refresh_content() - self.draw() - - elif cmd == ord('c'): - self.add_comment() - self.draw() - - elif cmd == ord('?'): - show_help(self.stdscr) - self.draw() - - elif cmd == ord('a'): - self.upvote() - self.draw() - - elif cmd == ord('z'): - self.downvote() - self.draw() - - elif cmd == ord('q'): - sys.exit() - - elif cmd == curses.KEY_RESIZE: - self.draw() - + @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: @@ -91,19 +51,24 @@ class SubmissionPage(BasePage): # cursor index to go out of bounds. self.nav.page_index, self.nav.cursor_index = current_index, 0 - def refresh_content(self): + @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. diff --git a/rtv/subreddit.py b/rtv/subreddit.py index 8e6b47a..b811a07 100644 --- a/rtv/subreddit.py +++ b/rtv/subreddit.py @@ -1,81 +1,43 @@ import curses -import sys import requests from .exceptions import SubredditError -from .page import BasePage, Navigator +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, - text_input, show_notification, show_help) + text_input, show_notification) -__all__ = ['opened_links', 'SubredditPage'] +__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): - - self.draw() - while True: + self.draw() cmd = self.stdscr.getch() + self.controller.trigger(cmd) - if cmd in (curses.KEY_UP, ord('k')): - self.move_cursor_up() - self.clear_input_queue() - - elif cmd in (curses.KEY_DOWN, ord('j')): - self.move_cursor_down() - self.clear_input_queue() - - elif cmd in (curses.KEY_RIGHT, curses.KEY_ENTER, ord('l')): - self.open_submission() - self.draw() - - elif cmd == ord('o'): - self.open_link() - self.draw() - - elif cmd in (curses.KEY_F5, ord('r')): - self.refresh_content() - self.draw() - - elif cmd == ord('?'): - show_help(self.stdscr) - self.draw() - - elif cmd == ord('a'): - self.upvote() - self.draw() - - elif cmd == ord('z'): - self.downvote() - self.draw() - - elif cmd == ord('q'): - sys.exit() - - elif cmd == curses.KEY_RESIZE: - self.draw() - - elif cmd == ord('/'): - self.prompt_subreddit() - self.draw() - + @SubredditController.register(curses.KEY_F5, 'r') def refresh_content(self, name=None): - name = name or self.content.name try: @@ -88,6 +50,7 @@ class SubredditPage(BasePage): else: self.nav = Navigator(self.content.get) + @SubredditController.register('/') def prompt_subreddit(self): "Open a prompt to type in a new subreddit" @@ -103,6 +66,7 @@ class SubredditPage(BasePage): if out is not None: self.refresh_content(name=out) + @SubredditController.register(curses.KEY_RIGHT, 'l') def open_submission(self): "Select the current submission to view posts" @@ -114,6 +78,7 @@ class SubredditPage(BasePage): 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"