From 346ba424f2a42aa037ec5b8edee77a6b78427bd0 Mon Sep 17 00:00:00 2001 From: Tobin Date: Mon, 30 Mar 2015 16:58:01 -0500 Subject: [PATCH 1/9] added base controller for command decorators --- rtv/page.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/rtv/page.py b/rtv/page.py index 455d9c4..5a3012e 100644 --- a/rtv/page.py +++ b/rtv/page.py @@ -96,6 +96,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 = Controller.character_map.get(char) + if func is None: + func = self.character_map.get(None) + if func is None: + func = Controller.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 From 6fcabe36e3521553c2fa548d1bd7fa2080a3d084 Mon Sep 17 00:00:00 2001 From: Tobin Date: Mon, 30 Mar 2015 17:18:29 -0500 Subject: [PATCH 2/9] implemented controllers for key input --- rtv/page.py | 19 ++++++++++--- rtv/submission.py | 68 ++++++++++------------------------------------- rtv/subreddit.py | 62 ++++++++++-------------------------------- 3 files changed, 43 insertions(+), 106 deletions(-) diff --git a/rtv/page.py b/rtv/page.py index 5a3012e..a4f329b 100644 --- a/rtv/page.py +++ b/rtv/page.py @@ -1,4 +1,5 @@ import curses +import six import praw.errors @@ -126,11 +127,11 @@ class BaseController(object): func = self.character_map.get(char) if func is None: - func = Controller.character_map.get(char) + func = BaseController.character_map.get(char) if func is None: func = self.character_map.get(None) if func is None: - func = Controller.character_map.get(None) + func = BaseController.character_map.get(None) return func(self.instance, *args, **kwargs) @classmethod @@ -164,9 +165,19 @@ 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) + @BaseController.register(curses.KEY_DOWN, 'j') def move_cursor_down(self): self._move_cursor(1) @@ -177,8 +188,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: @@ -192,8 +203,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..0c35bda 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) 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,13 @@ class SubmissionPage(BasePage): page_index=-1) 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.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 +50,20 @@ class SubmissionPage(BasePage): # cursor index to go out of bounds. self.nav.page_index, self.nav.cursor_index = current_index, 0 + @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, '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..a75b8da 100644 --- a/rtv/subreddit.py +++ b/rtv/subreddit.py @@ -4,78 +4,41 @@ 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) -__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 +51,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 +67,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 +79,7 @@ class SubredditPage(BasePage): global opened_links opened_links.add(data['url_full']) + @SubredditController.register(curses.KEY_ENTER, 'o') def open_link(self): "Open a link with the webbrowser" From bd38e6a28ed823c692c50f8eccb30c03dc6d369f Mon Sep 17 00:00:00 2001 From: Tobin Date: Mon, 30 Mar 2015 17:23:38 -0500 Subject: [PATCH 3/9] added character value for enter because curses.KEY_ENTER is buggy --- rtv/submission.py | 9 +++++++-- rtv/subreddit.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/rtv/submission.py b/rtv/submission.py index 0c35bda..29a329f 100644 --- a/rtv/submission.py +++ b/rtv/submission.py @@ -35,7 +35,8 @@ class SubmissionPage(BasePage): page_index=-1) def loop(self): - while True: + self.active = True + while self.active: self.draw() cmd = self.stdscr.getch() self.controller.trigger(cmd) @@ -50,13 +51,17 @@ class SubmissionPage(BasePage): # 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, 'o') + @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 diff --git a/rtv/subreddit.py b/rtv/subreddit.py index a75b8da..0ba375e 100644 --- a/rtv/subreddit.py +++ b/rtv/subreddit.py @@ -79,7 +79,7 @@ class SubredditPage(BasePage): global opened_links opened_links.add(data['url_full']) - @SubredditController.register(curses.KEY_ENTER, 'o') + @SubredditController.register(curses.KEY_ENTER, 10, 'o') def open_link(self): "Open a link with the webbrowser" From c2bf9a88ed47d8d1b2470655ab60f860b1b414bf Mon Sep 17 00:00:00 2001 From: Tobin Date: Mon, 30 Mar 2015 17:26:19 -0500 Subject: [PATCH 4/9] updated docs for enter key binding --- README.rst | 2 +- rtv/docs.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 From 69a346abe8a1150f4db3eadebeb8a684885002a0 Mon Sep 17 00:00:00 2001 From: Tobin Date: Mon, 30 Mar 2015 18:36:01 -0500 Subject: [PATCH 5/9] missing import statement --- rtv/page.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rtv/page.py b/rtv/page.py index a4f329b..ed01a3f 100644 --- a/rtv/page.py +++ b/rtv/page.py @@ -1,5 +1,6 @@ import curses import six +import sys import praw.errors From 3ad9a0c9f48259be38e7683562cbe5a14cfd7351 Mon Sep 17 00:00:00 2001 From: Tobin Date: Mon, 30 Mar 2015 23:16:59 -0500 Subject: [PATCH 6/9] remove unused sys import --- rtv/subreddit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rtv/subreddit.py b/rtv/subreddit.py index 0ba375e..366bde0 100644 --- a/rtv/subreddit.py +++ b/rtv/subreddit.py @@ -1,5 +1,4 @@ import curses -import sys import requests From 9a4a0b59ea19cf79ee4ae3c5a614211b00a2d3b7 Mon Sep 17 00:00:00 2001 From: Tobin Date: Mon, 30 Mar 2015 23:19:23 -0500 Subject: [PATCH 7/9] missing show_help import --- rtv/page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtv/page.py b/rtv/page.py index ed01a3f..7567896 100644 --- a/rtv/page.py +++ b/rtv/page.py @@ -5,7 +5,7 @@ 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'] From 5d72126fa530bcbb447dfabac8722aeba1edd891 Mon Sep 17 00:00:00 2001 From: Tobin Date: Mon, 30 Mar 2015 23:37:50 -0500 Subject: [PATCH 8/9] clear input queue when moving cursor --- rtv/page.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rtv/page.py b/rtv/page.py index 7567896..d1f31eb 100644 --- a/rtv/page.py +++ b/rtv/page.py @@ -177,10 +177,12 @@ class BasePage(object): @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" From e31997d392b10e763a52f26f4ad19dbdfefd8a4e Mon Sep 17 00:00:00 2001 From: Tobin Date: Mon, 30 Mar 2015 23:48:43 -0500 Subject: [PATCH 9/9] more unused imports --- rtv/submission.py | 2 +- rtv/subreddit.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rtv/submission.py b/rtv/submission.py index 29a329f..a854ddd 100644 --- a/rtv/submission.py +++ b/rtv/submission.py @@ -8,7 +8,7 @@ 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_help, show_notification, text_input) + show_notification, text_input) from .docs import COMMENT_FILE __all__ = ['SubmissionController', 'SubmissionPage'] diff --git a/rtv/subreddit.py b/rtv/subreddit.py index 366bde0..b811a07 100644 --- a/rtv/subreddit.py +++ b/rtv/subreddit.py @@ -8,7 +8,7 @@ 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', 'SubredditController', 'SubredditPage']