Almost functional.

This commit is contained in:
Michael Lazar
2016-02-09 02:56:55 -08:00
parent 2f093e47a8
commit 181507d9bb
8 changed files with 121 additions and 75 deletions

View File

@@ -26,6 +26,7 @@ _logger = logging.getLogger(__name__)
# ptrace_scope to 0 in /etc/sysctl.d/10-ptrace.conf.
# http://blog.mellenthin.de/archives/2010/10/18/gdb-attach-fails
def main():
"Main entry point"
@@ -38,12 +39,13 @@ def main():
sys.stdout.write('\x1b]2;{0}\x07'.format(title))
args = Config.get_args()
fargs = Config.get_file(args.get('config'))
fargs, bindings = Config.get_file(args.get('config'))
# Apply the file config first, then overwrite with any command line args
config = Config()
config.update(**fargs)
config.update(**args)
config.keymap.update(**bindings)
# Copy the default config file and quit
if config['copy_config']:

View File

@@ -1,19 +1,17 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
import os
import curses
import codecs
import shutil
import argparse
from curses import ascii
from functools import partial
import six
from six.moves import configparser
from . import docs, __version__
from .objects import KeyMap
PACKAGE = os.path.dirname(__file__)
HOME = os.path.expanduser('~')
@@ -115,7 +113,10 @@ class Config(object):
self.history_file = history_file
self.token_file = token_file
self.config = kwargs
self.default = self.get_file(DEFAULT_CONFIG)
default, bindings = self.get_file(DEFAULT_CONFIG)
self.default = default
self.keymap = KeyMap(bindings)
# `refresh_token` and `history` are saved/loaded at separate locations,
# so they are treated differently from the rest of the config options.
@@ -198,8 +199,8 @@ class Config(object):
return cls._parse_rtv_file(config)
@classmethod
def _parse_rtv_file(cls, config):
@staticmethod
def _parse_rtv_file(config):
rtv = {}
if config.has_section('rtv'):
@@ -213,7 +214,6 @@ class Config(object):
'oauth_redirect_port': partial(config.getint, 'rtv'),
'oauth_scope': lambda x: rtv[x].split(',')
}
for key, func in params.items():
if key in rtv:
rtv[key] = func(key)
@@ -223,30 +223,10 @@ class Config(object):
bindings = dict(config.items('bindings'))
for name, keys in bindings.items():
bindings[name] = [cls._parse_key(key) for key in keys.split(',')]
bindings[name] = [key.strip() for key in keys.split(',')]
return rtv, bindings
@staticmethod
def _parse_key(key):
"""
Parse a key represented by a string return its character code.
"""
key = key.strip()
if re.match('[<]KEY_.*[>]', key):
# Curses control character
return getattr(curses, key[1:-1])
elif re.match('[<].*[>]', key):
# Ascii control character
return getattr(ascii, key[1:-1])
elif key.startswith('0x'):
# Ascii hex code
return int(key, 16)
else:
# Ascii character
return ord(key)
@staticmethod
def _ensure_filepath(filename):
"""

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
import os
import time
import curses
@@ -9,7 +10,9 @@ import inspect
import weakref
import logging
import threading
from curses import ascii
from contextlib import contextmanager
from collections import defaultdict
import six
import praw
@@ -502,6 +505,11 @@ class Controller(object):
>>> def func(self, *args)
>>> ...
Register a KeyBinding that can be defined later by the config file
>>> @Controller.register(KeyMap.UPVOTE)
>>> def upvote(self, *args)
>> ...
Register a default behavior by using `None`.
>>> @Controller.register(None)
>>> def default_func(self, *args)
@@ -515,13 +523,22 @@ class Controller(object):
character_map = {}
def __init__(self, instance):
def __init__(self, instance, keymap=None):
self.instance = instance
# Build a list of parent controllers that follow the object's MRO to
# check if any parent controllers have registered the keypress
# Build a list of parent controllers that follow the object's MRO
# to check if any parent controllers have registered the keypress
self.parents = inspect.getmro(type(self))[:-1]
# Update the character map with the bindings defined in the keymap
if not keymap:
for controller in [self] + self.parents:
for binding, func in controller.character_map.items():
if isinstance(binding, KeyBinding):
# TODO: Raise error if trying to overwrite a char
mappings = {char: func for char in keymap.get(binding)}
self.character_map.update(mappings)
def trigger(self, char, *args, **kwargs):
if isinstance(char, six.string_types) and len(char) == 1:
@@ -551,4 +568,53 @@ class Controller(object):
else:
cls.character_map[char] = f
return f
return inner
return inner
class KeyBinding(object):
def __init__(self, val):
self.val = val.upper()
class KeyMapMeta(type):
def __getattr__(cls, key):
return KeyBinding(key)
@six.add_metaclass(KeyMapMeta)
class KeyMap(object):
def __init__(self, bindings):
self._bindings = defaultdict(list)
self.update(bindings)
def update(self, **bindings):
for command, keys in bindings:
binding = KeyBinding(command)
self._bindings[binding] = [self._parse_key(key) for key in keys]
def get(self, binding):
return self._bindings[binding]
@staticmethod
def _parse_key(key):
"""
Parse a key represented by a string and return its character code.
"""
if isinstance(key, int):
return key
elif re.match('[<]KEY_.*[>]', key):
# Curses control character
return getattr(curses, key[1:-1])
elif re.match('[<].*[>]', key):
# Ascii control character
return getattr(ascii, key[1:-1])
elif key.startswith('0x'):
# Ascii hex code
return int(key, 16)
else:
# Ascii character
return ord(key)

View File

@@ -9,7 +9,7 @@ from functools import wraps
from kitchen.text.display import textual_width
from . import docs
from .objects import Controller, Color
from .objects import Controller, Color, KeyMap
def logged_in(f):
@@ -68,60 +68,60 @@ class Page(object):
ch = self.term.stdscr.getch()
self.controller.trigger(ch)
@PageController.register('q')
@PageController.register(KeyMap.EXIT)
def exit(self):
if self.term.prompt_y_or_n('Do you really want to quit? (y/n): '):
sys.exit()
@PageController.register('Q')
@PageController.register(KeyMap.FORCE_EXIT)
def force_exit(self):
sys.exit()
@PageController.register('?')
@PageController.register(KeyMap.HELP)
def show_help(self):
self.term.show_notification(docs.HELP.strip().splitlines())
@PageController.register('1')
@PageController.register(KeyMap.SORT_HOT)
def sort_content_hot(self):
self.refresh_content(order='hot')
@PageController.register('2')
@PageController.register(KeyMap.SORT_TOP)
def sort_content_top(self):
self.refresh_content(order='top')
@PageController.register('3')
@PageController.register(KeyMap.SORT_RISING)
def sort_content_rising(self):
self.refresh_content(order='rising')
@PageController.register('4')
@PageController.register(KeyMap.SORT_NEW)
def sort_content_new(self):
self.refresh_content(order='new')
@PageController.register('5')
@PageController.register(KeyMap.SORT_CONTROVERSIAL)
def sort_content_controversial(self):
self.refresh_content(order='controversial')
@PageController.register(curses.KEY_UP, 'k')
@PageController.register(KeyMap.MOVE_UP)
def move_cursor_up(self):
self._move_cursor(-1)
self.clear_input_queue()
@PageController.register(curses.KEY_DOWN, 'j')
@PageController.register(KeyMap.MOVE_DOWN)
def move_cursor_down(self):
self._move_cursor(1)
self.clear_input_queue()
@PageController.register('m', curses.KEY_PPAGE)
@PageController.register(KeyMap.PAGE_UP)
def move_page_up(self):
self._move_page(-1)
self.clear_input_queue()
@PageController.register('n', curses.KEY_NPAGE)
@PageController.register(KeyMap.PAGE_DOWN)
def move_page_down(self):
self._move_page(1)
self.clear_input_queue()
@PageController.register('a')
@PageController.register(KeyMap.UPVOTE)
@logged_in
def upvote(self):
data = self.content.get(self.nav.absolute_index)
@@ -138,7 +138,7 @@ class Page(object):
if not self.term.loader.exception:
data['likes'] = True
@PageController.register('z')
@PageController.register(KeyMap.DOWNVOTE)
@logged_in
def downvote(self):
data = self.content.get(self.nav.absolute_index)
@@ -155,7 +155,7 @@ class Page(object):
if not self.term.loader.exception:
data['likes'] = None
@PageController.register('u')
@PageController.register(KeyMap.LOGIN)
def login(self):
"""
Prompt to log into the user's account, or log out of the current
@@ -169,7 +169,7 @@ class Page(object):
else:
self.oauth.authorize()
@PageController.register('d')
@PageController.register(KeyMap.DELETE)
@logged_in
def delete_item(self):
"""
@@ -193,7 +193,7 @@ class Page(object):
if self.term.loader.exception is None:
self.refresh_content()
@PageController.register('e')
@PageController.register(KeyMap.EDIT)
@logged_in
def edit(self):
"""
@@ -228,7 +228,7 @@ class Page(object):
if self.term.loader.exception is None:
self.refresh_content()
@PageController.register('i')
@PageController.register(KeyMap.INBOX)
@logged_in
def get_inbox(self):
"""

View File

@@ -100,7 +100,7 @@ INBOX = i
REFRESH = r, <KEY_F5>
; Submission page
SUBMISSION_TOGGLE_COMMENTS = l, 0x20, <KEY_RIGHT>
SUBMISSION_TOGGLE_COMMENT = l, 0x20, <KEY_RIGHT>
SUBMISSION_OPEN_IN_BROWSER = o, <LF>, <KEY_ENTER>
SUBMISSION_POST = c
SUBMISSION_EXIT = h, <KEY_LEFT>
@@ -111,6 +111,7 @@ SUBREDDIT_PROMPT = /
SUBREDDIT_POST = c
SUBREDDIT_OPEN = l, <KEY_RIGHT>
SUBREDDIT_OPEN_IN_BROWSER = o, <LF>, <KEY_ENTER>, <KEY_ENTER>
SUBREDDIT_OPEN_SUBSCRIPTIONS = s
; Subscription page
SUBSCRIPTION_SELECT = l, <LF>, <KEY_ENTER>, <KEY_RIGHT>

View File

@@ -7,8 +7,7 @@ import curses
from . import docs
from .content import SubmissionContent
from .page import Page, PageController, logged_in
from .objects import Navigator, Color
from .terminal import Terminal
from .objects import Navigator, Color, KeyMap
class SubmissionController(PageController):
@@ -29,7 +28,7 @@ class SubmissionPage(Page):
# Start at the submission post, which is indexed as -1
self.nav = Navigator(self.content.get, page_index=-1)
@SubmissionController.register(curses.KEY_RIGHT, 'l', ' ')
@SubmissionController.register(KeyMap.SUBMISSION_TOGGLE_COMMENT)
def toggle_comment(self):
"Toggle the selected comment tree between visible and hidden"
@@ -41,13 +40,13 @@ class SubmissionPage(Page):
# causes the cursor index to go out of bounds.
self.nav.page_index, self.nav.cursor_index = current_index, 0
@SubmissionController.register(curses.KEY_LEFT, 'h')
@SubmissionController.register(KeyMap.SUBMISSION_EXIT)
def exit_submission(self):
"Close the submission and return to the subreddit page"
self.active = False
@SubmissionController.register(curses.KEY_F5, 'r')
@SubmissionController.register(KeyMap.REFRESH)
def refresh_content(self, order=None, name=None):
"Re-download comments and reset the page index"
@@ -60,7 +59,7 @@ class SubmissionPage(Page):
if not self.term.loader.exception:
self.nav = Navigator(self.content.get, page_index=-1)
@SubmissionController.register(curses.KEY_ENTER, Terminal.RETURN, 'o')
@SubmissionController.register(KeyMap.SUBMISSION_OPEN_IN_BROWSER)
def open_link(self):
"Open the selected item with the webbrowser"
@@ -71,7 +70,7 @@ class SubmissionPage(Page):
else:
self.term.flash()
@SubmissionController.register('c')
@SubmissionController.register(KeyMap.SUBMISSION_POST)
@logged_in
def add_comment(self):
"""
@@ -114,7 +113,7 @@ class SubmissionPage(Page):
if not self.term.loader.exception:
self.refresh_content()
@SubmissionController.register('d')
@SubmissionController.register(KeyMap.DELETE)
@logged_in
def delete_comment(self):
"Delete a comment as long as it is not the current submission"

View File

@@ -7,7 +7,7 @@ import curses
from . import docs
from .content import SubredditContent
from .page import Page, PageController, logged_in
from .objects import Navigator, Color
from .objects import Navigator, Color, KeyMap
from .submission import SubmissionPage
from .subscription import SubscriptionPage
from .terminal import Terminal
@@ -31,7 +31,7 @@ class SubredditPage(Page):
self.controller = SubredditController(self)
self.nav = Navigator(self.content.get)
@SubredditController.register(curses.KEY_F5, 'r')
@SubredditController.register(KeyMap.REFRESH)
def refresh_content(self, order=None, name=None):
"Re-download all submissions and reset the page index"
@@ -49,7 +49,7 @@ class SubredditPage(Page):
if not self.term.loader.exception:
self.nav = Navigator(self.content.get)
@SubredditController.register('f')
@SubredditController.register(KeyMap.SUBREDDIT_SEARCH)
def search_subreddit(self, name=None):
"Open a prompt to search the given subreddit"
@@ -65,7 +65,7 @@ class SubredditPage(Page):
if not self.term.loader.exception:
self.nav = Navigator(self.content.get)
@SubredditController.register('/')
@SubredditController.register(KeyMap.SUBREDDIT_PROMPT)
def prompt_subreddit(self):
"Open a prompt to navigate to a different subreddit"
@@ -73,7 +73,7 @@ class SubredditPage(Page):
if name is not None:
self.refresh_content(order='ignore', name=name)
@SubredditController.register(curses.KEY_RIGHT, 'l')
@SubredditController.register(KeyMap.SUBREDDIT_OPEN)
def open_submission(self, url=None):
"Select the current submission to view posts"
@@ -93,7 +93,7 @@ class SubredditPage(Page):
if data.get('url_type') == 'selfpost':
self.config.history.add(data['url_full'])
@SubredditController.register(curses.KEY_ENTER, Terminal.RETURN, 'o')
@SubredditController.register(KeyMap.SUBREDDIT_OPEN_IN_BROWSER)
def open_link(self):
"Open a link with the webbrowser"
@@ -107,7 +107,7 @@ class SubredditPage(Page):
self.term.open_browser(data['url_full'])
self.config.history.add(data['url_full'])
@SubredditController.register('c')
@SubredditController.register(KeyMap.SUBREDDIT_POST)
@logged_in
def post_submission(self):
"Post a new submission to the given subreddit"
@@ -145,7 +145,7 @@ class SubredditPage(Page):
self.refresh_content()
@SubredditController.register('s')
@SubredditController.register(KeyMap.SUBREDDIT_OPEN_SUBSCRIPTIONS)
@logged_in
def open_subscriptions(self):
"Open user subscriptions page"

View File

@@ -5,8 +5,7 @@ import curses
from .page import Page, PageController
from .content import SubscriptionContent
from .objects import Color, Navigator
from .terminal import Terminal
from .objects import Color, Navigator, KeyMap
class SubscriptionController(PageController):
@@ -23,7 +22,7 @@ class SubscriptionPage(Page):
self.nav = Navigator(self.content.get)
self.subreddit_data = None
@SubscriptionController.register(curses.KEY_F5, 'r')
@SubscriptionController.register(KeyMap.REFRESH)
def refresh_content(self, order=None, name=None):
"Re-download all subscriptions and reset the page index"
@@ -38,15 +37,14 @@ class SubscriptionPage(Page):
if not self.term.loader.exception:
self.nav = Navigator(self.content.get)
@SubscriptionController.register(curses.KEY_ENTER, Terminal.RETURN,
curses.KEY_RIGHT, 'l')
@SubscriptionController.register(KeyMap.SUBSCRIPTION_SELECT)
def select_subreddit(self):
"Store the selected subreddit and return to the subreddit page"
self.subreddit_data = self.content.get(self.nav.absolute_index)
self.active = False
@SubscriptionController.register(curses.KEY_LEFT, Terminal.ESCAPE, 'h', 's')
@SubscriptionController.register(KeyMap.SUBSCRIPTION_CLOSE)
def close_subscriptions(self):
"Close subscriptions and return to the subreddit page"