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
:``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

View File

@@ -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

View File

@@ -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:

View File

@@ -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.

View File

@@ -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"