Merge pull request #52 from Brobin/command_controller

Command Controllers and Command decorators
This commit is contained in:
michael-lazar
2015-03-30 21:52:56 -07:00
5 changed files with 104 additions and 111 deletions

View File

@@ -59,7 +59,7 @@ RTV currently supports browsing both subreddits and individual submissions. In e
:``▲``/``▼`` or ``j``/``k``: Scroll to the prev/next item :``▲``/``▼`` or ``j``/``k``: Scroll to the prev/next item
:``a``/``z``: Upvote/downvote the selected 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 :``r``: Refresh the current page
:``?``: Show the help message :``?``: Show the help message
:``q``: Quit :``q``: Quit

View File

@@ -32,7 +32,7 @@ Global Commands
`a/z` : Upvote/downvote the selected item `a/z` : Upvote/downvote the selected item
`r` : Refresh the current page `r` : Refresh the current page
`q` : Quit the program `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 `?` : Show this help message
Subreddit Mode Subreddit Mode

View File

@@ -1,9 +1,11 @@
import curses import curses
import six
import sys
import praw.errors import praw.errors
from .helpers import clean from .helpers import clean
from .curses_helpers import Color, show_notification from .curses_helpers import Color, show_notification, show_help
__all__ = ['Navigator'] __all__ = ['Navigator']
@@ -96,6 +98,55 @@ class Navigator(object):
return True 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): class BasePage(object):
""" """
Base terminal viewer incorperates a cursor to navigate content Base terminal viewer incorperates a cursor to navigate content
@@ -115,11 +166,23 @@ class BasePage(object):
self._content_window = None self._content_window = None
self._subwindows = 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): def move_cursor_up(self):
self._move_cursor(-1) self._move_cursor(-1)
self.clear_input_queue()
@BaseController.register(curses.KEY_DOWN, 'j')
def move_cursor_down(self): def move_cursor_down(self):
self._move_cursor(1) self._move_cursor(1)
self.clear_input_queue()
def clear_input_queue(self): def clear_input_queue(self):
"Clear excessive input caused by the scroll wheel or holding down a key" "Clear excessive input caused by the scroll wheel or holding down a key"
@@ -128,8 +191,8 @@ class BasePage(object):
continue continue
self.stdscr.nodelay(0) self.stdscr.nodelay(0)
@BaseController.register('a')
def upvote(self): def upvote(self):
data = self.content.get(self.nav.absolute_index) data = self.content.get(self.nav.absolute_index)
try: try:
if 'likes' not in data: if 'likes' not in data:
@@ -143,8 +206,8 @@ class BasePage(object):
except praw.errors.LoginOrScopeRequired: except praw.errors.LoginOrScopeRequired:
show_notification(self.stdscr, ['Login to vote']) show_notification(self.stdscr, ['Login to vote'])
@BaseController.register('z')
def downvote(self): def downvote(self):
data = self.content.get(self.nav.absolute_index) data = self.content.get(self.nav.absolute_index)
try: try:
if 'likes' not in data: if 'likes' not in data:

View File

@@ -5,20 +5,25 @@ import time
import praw.errors import praw.errors
from .content import SubmissionContent from .content import SubmissionContent
from .page import BasePage, Navigator from .page import BasePage, Navigator, BaseController
from .helpers import clean, open_browser, open_editor from .helpers import clean, open_browser, open_editor
from .curses_helpers import (BULLET, UARROW, DARROW, Color, LoadScreen, from .curses_helpers import (BULLET, UARROW, DARROW, Color, LoadScreen,
show_help, show_notification, text_input) show_notification, text_input)
from .docs import COMMENT_FILE from .docs import COMMENT_FILE
__all__ = ['SubmissionPage'] __all__ = ['SubmissionController', 'SubmissionPage']
class SubmissionController(BaseController):
character_map = {}
class SubmissionPage(BasePage): class SubmissionPage(BasePage):
def __init__(self, stdscr, reddit, url=None, submission=None): def __init__(self, stdscr, reddit, url=None, submission=None):
self.controller = SubmissionController(self)
self.loader = LoadScreen(stdscr) self.loader = LoadScreen(stdscr)
if url is not None: if url is not None:
content = SubmissionContent.from_url(reddit, url, self.loader) content = SubmissionContent.from_url(reddit, url, self.loader)
elif submission is not None: elif submission is not None:
@@ -30,59 +35,14 @@ class SubmissionPage(BasePage):
page_index=-1) page_index=-1)
def loop(self): def loop(self):
self.active = True
while self.active:
self.draw() self.draw()
while True:
cmd = self.stdscr.getch() cmd = self.stdscr.getch()
self.controller.trigger(cmd)
if cmd in (curses.KEY_UP, ord('k')): @SubmissionController.register(curses.KEY_RIGHT, 'l')
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()
def toggle_comment(self): def toggle_comment(self):
current_index = self.nav.absolute_index current_index = self.nav.absolute_index
self.content.toggle(current_index) self.content.toggle(current_index)
if self.nav.inverted: if self.nav.inverted:
@@ -91,19 +51,24 @@ class SubmissionPage(BasePage):
# cursor index to go out of bounds. # cursor index to go out of bounds.
self.nav.page_index, self.nav.cursor_index = current_index, 0 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 url = self.content.name
self.content = SubmissionContent.from_url(self.reddit, url, self.loader) self.content = SubmissionContent.from_url(self.reddit, url, self.loader)
self.nav = Navigator(self.content.get, page_index=-1) self.nav = Navigator(self.content.get, page_index=-1)
@SubmissionController.register(curses.KEY_ENTER, 10, 'o')
def open_link(self): def open_link(self):
# Always open the page for the submission # Always open the page for the submission
# May want to expand at some point to open comment permalinks # May want to expand at some point to open comment permalinks
url = self.content.get(-1)['permalink'] url = self.content.get(-1)['permalink']
open_browser(url) open_browser(url)
@SubmissionController.register('c')
def add_comment(self): def add_comment(self):
""" """
Add a comment on the submission if a header is selected. Add a comment on the submission if a header is selected.

View File

@@ -1,81 +1,43 @@
import curses import curses
import sys
import requests import requests
from .exceptions import SubredditError from .exceptions import SubredditError
from .page import BasePage, Navigator from .page import BasePage, Navigator, BaseController
from .submission import SubmissionPage from .submission import SubmissionPage
from .content import SubredditContent from .content import SubredditContent
from .helpers import clean, open_browser from .helpers import clean, open_browser
from .curses_helpers import (BULLET, UARROW, DARROW, Color, LoadScreen, 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 # Used to keep track of browsing history across the current session
opened_links = set() opened_links = set()
class SubredditController(BaseController):
character_map = {}
class SubredditPage(BasePage): class SubredditPage(BasePage):
def __init__(self, stdscr, reddit, name): def __init__(self, stdscr, reddit, name):
self.controller = SubredditController(self)
self.loader = LoadScreen(stdscr) self.loader = LoadScreen(stdscr)
content = SubredditContent.from_name(reddit, name, self.loader) content = SubredditContent.from_name(reddit, name, self.loader)
super(SubredditPage, self).__init__(stdscr, reddit, content) super(SubredditPage, self).__init__(stdscr, reddit, content)
def loop(self): def loop(self):
self.draw()
while True: while True:
self.draw()
cmd = self.stdscr.getch() cmd = self.stdscr.getch()
self.controller.trigger(cmd)
if cmd in (curses.KEY_UP, ord('k')): @SubredditController.register(curses.KEY_F5, 'r')
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()
def refresh_content(self, name=None): def refresh_content(self, name=None):
name = name or self.content.name name = name or self.content.name
try: try:
@@ -88,6 +50,7 @@ class SubredditPage(BasePage):
else: else:
self.nav = Navigator(self.content.get) self.nav = Navigator(self.content.get)
@SubredditController.register('/')
def prompt_subreddit(self): def prompt_subreddit(self):
"Open a prompt to type in a new subreddit" "Open a prompt to type in a new subreddit"
@@ -103,6 +66,7 @@ class SubredditPage(BasePage):
if out is not None: if out is not None:
self.refresh_content(name=out) self.refresh_content(name=out)
@SubredditController.register(curses.KEY_RIGHT, 'l')
def open_submission(self): def open_submission(self):
"Select the current submission to view posts" "Select the current submission to view posts"
@@ -114,6 +78,7 @@ class SubredditPage(BasePage):
global opened_links global opened_links
opened_links.add(data['url_full']) opened_links.add(data['url_full'])
@SubredditController.register(curses.KEY_ENTER, 10, 'o')
def open_link(self): def open_link(self):
"Open a link with the webbrowser" "Open a link with the webbrowser"