Merge remote-tracking branch 'upstream/master'

This commit is contained in:
David Foucher
2016-03-15 00:25:34 +01:00
26 changed files with 3533 additions and 1573 deletions

View File

@@ -14,6 +14,7 @@ from .oauth import OAuthHelper
from .terminal import Terminal
from .objects import curses_session
from .subreddit import SubredditPage
from .exceptions import ConfigError
from .__version__ import __version__
@@ -39,13 +40,17 @@ 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)
# If key bindings are supplied in the config file, overwrite the defaults
if bindings:
config.keymap.set_bindings(bindings)
# Copy the default config file and quit
if config['copy_config']:
copy_default_config()
@@ -60,6 +65,14 @@ def main():
config.delete_refresh_token()
if config['log']:
# Log request headers to the file (print hack only works on python 3.x)
# from http import client
# _http_logger = logging.getLogger('http.client')
# client.HTTPConnection.debuglevel = 2
# def print_to_file(*args, **_):
# if args[0] != "header:":
# _http_logger.info(' '.join(args))
# client.print = print_to_file
logging.basicConfig(level=logging.DEBUG, filename=config['log'])
else:
# Add an empty handler so the logger doesn't complain
@@ -94,6 +107,9 @@ def main():
# Launch the subreddit page
page.loop()
except ConfigError as e:
_logger.exception(e)
print(e)
except Exception as e:
_logger.exception(e)
raise

View File

@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
__version__ = '1.8.0'
__version__ = '1.8.1'

View File

@@ -11,6 +11,7 @@ import six
from six.moves import configparser
from . import docs, __version__
from .objects import KeyMap
PACKAGE = os.path.dirname(__file__)
HOME = os.path.expanduser('~')
@@ -26,7 +27,7 @@ def build_parser():
parser = argparse.ArgumentParser(
prog='rtv', description=docs.SUMMARY,
epilog=docs.CONTROLS+docs.HELP,
epilog=docs.CONTROLS,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument(
'-V', '--version', action='version', version='rtv '+__version__)
@@ -112,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,9 +202,9 @@ class Config(object):
@staticmethod
def _parse_rtv_file(config):
out = {}
rtv = {}
if config.has_section('rtv'):
out = dict(config.items('rtv'))
rtv = dict(config.items('rtv'))
params = {
'ascii': partial(config.getboolean, 'rtv'),
@@ -208,13 +212,20 @@ class Config(object):
'persistent': partial(config.getboolean, 'rtv'),
'history_size': partial(config.getint, 'rtv'),
'oauth_redirect_port': partial(config.getint, 'rtv'),
'oauth_scope': lambda x: out[x].split(',')
'oauth_scope': lambda x: rtv[x].split(',')
}
for key, func in params.items():
if key in out:
out[key] = func(key)
return out
if key in rtv:
rtv[key] = func(key)
bindings = {}
if config.has_section('bindings'):
bindings = dict(config.items('bindings'))
for name, keys in bindings.items():
bindings[name] = [key.strip() for key in keys.split(',')]
return rtv, bindings
@staticmethod
def _ensure_filepath(filename):

View File

@@ -257,7 +257,10 @@ class SubmissionContent(Content):
def from_url(cls, reddit, url, loader, indent_size=2, max_indent_level=8,
order=None):
url = url.replace('http:', 'https:')
url = url.replace('http:', 'https:') # Reddit forces SSL
# Sometimes reddit will return a 403 FORBIDDEN when trying to access an
# np link while using OAUTH. Cause is unknown.
url = url.replace('https://np.', 'https://www.')
submission = reddit.get_submission(url, comment_sort=order)
return cls(submission, loader, indent_size, max_indent_level, order)

View File

@@ -12,17 +12,12 @@ terminal window.
"""
CONTROLS = """
Controls
--------
RTV currently supports browsing both subreddits and individual submissions.
In each mode the controls are slightly different. In subreddit mode you can
browse through the top submissions on either the front page or a specific
subreddit. In submission mode you can view the self text for a submission and
browse comments.
Move the cursor using either the arrow keys or *Vim* style movement.
Press `?` to open the help screen.
"""
HELP = """
Basic Commands
[Basic Commands]
`j/k` or `UP/DOWN` : Move the cursor up/down
`m/n` or `PgUp/PgDn`: Jump to the previous/next page
`o` or `ENTER` : Open the selected item as a webpage
@@ -32,7 +27,7 @@ Basic Commands
`?` : Show the help screen
`q/Q` : Quit/Force quit
Authenticated Commands
[Authenticated Commands]
`a/z` : Upvote/downvote
`w` : Save/unsave a post
`c` : Compose a new post or comment
@@ -41,13 +36,14 @@ Authenticated Commands
`i` : Display new messages prompt
`s` : Open/close subscribed subreddits list
Subreddit Mode
[Subreddit Mode]
`l` or `RIGHT` : Enter the selected submission
`/` : Open a prompt to switch subreddits
`f` : Open a prompt to search the current subreddit
Submission Mode
[Submission Mode]
`h` or `LEFT` : Return to subreddit mode
`l` or `RIGHT` : Open the selected comment in a new window
`SPACE` : Fold the selected comment, or load additional comments
"""

View File

@@ -6,6 +6,10 @@ class EscapeInterrupt(Exception):
"Signal that the ESC key has been pressed"
class ConfigError(Exception):
"There was a problem with the configuration"
class RTVError(Exception):
"Base RTV error class"

View File

@@ -1,14 +1,16 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
import os
import time
import curses
import signal
import inspect
import weakref
import logging
import threading
import curses
import curses.ascii
from contextlib import contextmanager
import six
@@ -502,6 +504,11 @@ class Controller(object):
>>> def func(self, *args)
>>> ...
Register a KeyBinding that can be defined later by the config file
>>> @Controller.register(Command("UPVOTE"))
>>> def upvote(self, *args)
>> ...
Register a default behavior by using `None`.
>>> @Controller.register(None)
>>> def default_func(self, *args)
@@ -515,13 +522,34 @@ 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]
if not keymap:
return
# Go through the controller and all of it's parents and look for
# Command objects in the character map. Use the keymap the lookup the
# keys associated with those command objects and add them to the
# character map.
for controller in self.parents:
for command, func in controller.character_map.copy().items():
if isinstance(command, Command):
for key in keymap.get(command):
val = keymap.parse(key)
# Check if the key is already programmed to trigger a
# different function.
if controller.character_map.get(val, func) != func:
raise exceptions.ConfigError(
"Invalid configuration! `%s` is bound to "
"duplicate commands in the "
"%s" % (key, controller.__name__))
controller.character_map[val] = func
def trigger(self, char, *args, **kwargs):
if isinstance(char, six.string_types) and len(char) == 1:
@@ -551,4 +579,90 @@ class Controller(object):
else:
cls.character_map[char] = f
return f
return inner
return inner
class Command(object):
"""
Minimal class that should be used to wrap abstract commands that may be
implemented as one or more physical keystrokes.
E.g. Command("REFRESH") can be represented by the KeyMap to be triggered
by either `r` or `F5`
"""
def __init__(self, val):
self.val = val.upper()
def __repr__(self):
return 'Command(%s)' % self.val
def __eq__(self, other):
return repr(self) == repr(other)
def __ne__(self, other):
return not self == other
def __hash__(self):
return hash(repr(self))
class KeyMap(object):
"""
Mapping between commands and the keys that they represent.
"""
def __init__(self, bindings):
self._keymap = None
self.set_bindings(bindings)
def set_bindings(self, bindings):
# Clear the keymap before applying the bindings to avoid confusion.
# If a user defines custom bindings in their config file, they must
# explicitly define ALL of the bindings.
self._keymap = {}
for command, keys in bindings.items():
if not isinstance(command, Command):
command = Command(command)
self._keymap[command] = keys
def get(self, command):
if not isinstance(command, Command):
command = Command(command)
try:
return self._keymap[command]
except KeyError:
raise exceptions.ConfigError('Invalid configuration! `%s` key is '
'undefined' % command.val)
@staticmethod
def parse(key):
"""
Parse a key represented by a string and return its character code.
"""
try:
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(curses.ascii, key[1:-1])
elif key.startswith('0x'):
# Ascii hex code
return int(key, 16)
else:
# Ascii character
code = ord(key)
if 0 <= code <= 255:
return code
# Python 3.3 has a curses.get_wch() function that we can use
# for unicode keys, but Python 2.7 is limited to ascii.
raise exceptions.ConfigError('Invalid configuration! `%s` is '
'not in the ascii range' % key)
except (AttributeError, ValueError, TypeError):
raise exceptions.ConfigError('Invalid configuration! "%s" is not a '
'valid key' % 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, Command
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(Command('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(Command('FORCE_EXIT'))
def force_exit(self):
sys.exit()
@PageController.register('?')
@PageController.register(Command('HELP'))
def show_help(self):
self.term.show_notification(docs.HELP.strip().splitlines())
self.term.show_notification(docs.HELP.strip('\n').splitlines())
@PageController.register('1')
@PageController.register(Command('SORT_HOT'))
def sort_content_hot(self):
self.refresh_content(order='hot')
@PageController.register('2')
@PageController.register(Command('SORT_TOP'))
def sort_content_top(self):
self.refresh_content(order='top')
@PageController.register('3')
@PageController.register(Command('SORT_RISING'))
def sort_content_rising(self):
self.refresh_content(order='rising')
@PageController.register('4')
@PageController.register(Command('SORT_NEW'))
def sort_content_new(self):
self.refresh_content(order='new')
@PageController.register('5')
@PageController.register(Command('SORT_CONTROVERSIAL'))
def sort_content_controversial(self):
self.refresh_content(order='controversial')
@PageController.register(curses.KEY_UP, 'k')
@PageController.register(Command('MOVE_UP'))
def move_cursor_up(self):
self._move_cursor(-1)
self.clear_input_queue()
@PageController.register(curses.KEY_DOWN, 'j')
@PageController.register(Command('MOVE_DOWN'))
def move_cursor_down(self):
self._move_cursor(1)
self.clear_input_queue()
@PageController.register('m', curses.KEY_PPAGE)
@PageController.register(Command('PAGE_UP'))
def move_page_up(self):
self._move_page(-1)
self.clear_input_queue()
@PageController.register('n', curses.KEY_NPAGE)
@PageController.register(Command('PAGE_DOWN'))
def move_page_down(self):
self._move_page(1)
self.clear_input_queue()
@PageController.register('a')
@PageController.register(Command('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(Command('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('w')
@PageController.register(Command('SAVE'))
@logged_in
def save(self):
data = self.content.get(self.nav.absolute_index)
@@ -172,7 +172,7 @@ class Page(object):
if not self.term.loader.exception:
data['saved'] = False
@PageController.register('u')
@PageController.register(Command('LOGIN'))
def login(self):
"""
Prompt to log into the user's account, or log out of the current
@@ -186,7 +186,7 @@ class Page(object):
else:
self.oauth.authorize()
@PageController.register('d')
@PageController.register(Command('DELETE'))
@logged_in
def delete_item(self):
"""
@@ -210,7 +210,7 @@ class Page(object):
if self.term.loader.exception is None:
self.refresh_content()
@PageController.register('e')
@PageController.register(Command('EDIT'))
@logged_in
def edit(self):
"""
@@ -245,7 +245,7 @@ class Page(object):
if self.term.loader.exception is None:
self.refresh_content()
@PageController.register('i')
@PageController.register(Command('INBOX'))
@logged_in
def get_inbox(self):
"""

View File

@@ -49,4 +49,76 @@ oauth_redirect_uri = http://127.0.0.1:65000/
oauth_redirect_port = 65000
; Access permissions that will be requested.
oauth_scope = edit,history,identity,mysubreddits,privatemessages,read,report,save,submit,subscribe,vote
oauth_scope = edit,history,identity,mysubreddits,privatemessages,read,report,save,submit,subscribe,vote
[bindings]
##############
# Key Bindings
##############
; If you would like to define custom bindings, copy this section into your
; config file with the [bindings] heading. All commands must be bound to at
; least one key for the config to be valid.
;
; 1.) Plain keys can be represented by either uppercase/lowercase characters
; or the hexadecimal numbers referring their ascii codes. For reference, see
; https://en.wikipedia.org/wiki/ASCII#ASCII_printable_code_chart
; e.g. Q, q, 1, ?
; e.g. 0x20 (space), 0x3c (less-than sign)
;
; 2.) Special ascii control codes should be surrounded with <>. For reference,
; see https://en.wikipedia.org/wiki/ASCII#ASCII_control_code_chart
; e.g. <LF> (enter), <ESC> (escape)
;
; 3.) Other special keys are defined by curses, they should be surrounded by <>
; and prefixed with KEY_. For reference, see
; https://docs.python.org/2/library/curses.html#constants
; e.g. <KEY_LEFT> (left arrow), <KEY_F5>, <KEY_NPAGE> (page down)
;
; Notes:
; - Curses <KEY_ENTER> is unreliable and should always be used in conjunction
; with <LF>.
; - Use 0x20 for the space key.
; - A subset of Ctrl modifiers are available through the ascii control codes.
; For example, Ctrl-D will trigger an <EOT> signal. See the table above for
; a complete reference.
; Base page
EXIT = q
FORCE_EXIT = Q
HELP = ?
SORT_HOT = 1
SORT_TOP = 2
SORT_RISING = 3
SORT_NEW = 4
SORT_CONTROVERSIAL = 5
MOVE_UP = k, <KEY_UP>
MOVE_DOWN = j, <KEY_DOWN>
PAGE_UP = m, <KEY_PPAGE>, <NAK>
PAGE_DOWN = n, <KEY_NPAGE>, <EOT>
UPVOTE = a
DOWNVOTE = z
LOGIN = u
DELETE = d
EDIT = e
INBOX = i
REFRESH = r, <KEY_F5>
SAVE = w
; Submission page
SUBMISSION_TOGGLE_COMMENT = 0x20
SUBMISSION_OPEN_IN_BROWSER = o, <LF>, <KEY_ENTER>
SUBMISSION_POST = c
SUBMISSION_EXIT = h, <KEY_LEFT>
SUBMISSION_OPEN_IN_PAGER = l, <KEY_RIGHT>
; Subreddit page
SUBREDDIT_SEARCH = f
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>
SUBSCRIPTION_EXIT = h, s, <ESC>, <KEY_LEFT>

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, Command
class SubmissionController(PageController):
@@ -20,15 +19,15 @@ class SubmissionPage(Page):
def __init__(self, reddit, term, config, oauth, url=None, submission=None):
super(SubmissionPage, self).__init__(reddit, term, config, oauth)
self.controller = SubmissionController(self, keymap=config.keymap)
if url:
self.content = SubmissionContent.from_url(reddit, url, term.loader)
else:
self.content = SubmissionContent(submission, term.loader)
self.controller = SubmissionController(self)
# 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(Command('SUBMISSION_TOGGLE_COMMENT'))
def toggle_comment(self):
"Toggle the selected comment tree between visible and hidden"
@@ -40,13 +39,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(Command('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(Command('REFRESH'))
def refresh_content(self, order=None, name=None):
"Re-download comments and reset the page index"
@@ -59,7 +58,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(Command('SUBMISSION_OPEN_IN_BROWSER'))
def open_link(self):
"Open the selected item with the webbrowser"
@@ -70,7 +69,20 @@ class SubmissionPage(Page):
else:
self.term.flash()
@SubmissionController.register('c')
@SubmissionController.register(Command('SUBMISSION_OPEN_IN_PAGER'))
def open_pager(self):
"Open the selected item with the system's pager"
data = self.content.get(self.nav.absolute_index)
if data['type'] == 'Submission':
text = '\n\n'.join((data['permalink'], data['text']))
self.term.open_pager(text)
elif data['type'] == 'Comment':
text = '\n\n'.join((data['permalink'], data['body']))
self.term.open_pager(text)
else:
self.term.flash()
@SubmissionController.register(Command('SUBMISSION_POST'))
@logged_in
def add_comment(self):
"""
@@ -113,7 +125,7 @@ class SubmissionPage(Page):
if not self.term.loader.exception:
self.refresh_content()
@SubmissionController.register('d')
@SubmissionController.register(Command('DELETE'))
@logged_in
def delete_comment(self):
"Delete a comment as long as it is not the current submission"

View File

@@ -7,10 +7,9 @@ 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, Command
from .submission import SubmissionPage
from .subscription import SubscriptionPage
from .terminal import Terminal
class SubredditController(PageController):
@@ -27,11 +26,11 @@ class SubredditPage(Page):
"""
super(SubredditPage, self).__init__(reddit, term, config, oauth)
self.controller = SubredditController(self, keymap=config.keymap)
self.content = SubredditContent.from_name(reddit, name, term.loader)
self.controller = SubredditController(self)
self.nav = Navigator(self.content.get)
@SubredditController.register(curses.KEY_F5, 'r')
@SubredditController.register(Command('REFRESH'))
def refresh_content(self, order=None, name=None):
"Re-download all submissions and reset the page index"
@@ -49,7 +48,7 @@ class SubredditPage(Page):
if not self.term.loader.exception:
self.nav = Navigator(self.content.get)
@SubredditController.register('f')
@SubredditController.register(Command('SUBREDDIT_SEARCH'))
def search_subreddit(self, name=None):
"Open a prompt to search the given subreddit"
@@ -65,7 +64,7 @@ class SubredditPage(Page):
if not self.term.loader.exception:
self.nav = Navigator(self.content.get)
@SubredditController.register('/')
@SubredditController.register(Command('SUBREDDIT_PROMPT'))
def prompt_subreddit(self):
"Open a prompt to navigate to a different subreddit"
@@ -73,7 +72,7 @@ class SubredditPage(Page):
if name is not None:
self.refresh_content(order='ignore', name=name)
@SubredditController.register(curses.KEY_RIGHT, 'l')
@SubredditController.register(Command('SUBREDDIT_OPEN'))
def open_submission(self, url=None):
"Select the current submission to view posts"
@@ -93,7 +92,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(Command('SUBREDDIT_OPEN_IN_BROWSER'))
def open_link(self):
"Open a link with the webbrowser"
@@ -107,7 +106,7 @@ class SubredditPage(Page):
self.term.open_browser(data['url_full'])
self.config.history.add(data['url_full'])
@SubredditController.register('c')
@SubredditController.register(Command('SUBREDDIT_POST'))
@logged_in
def post_submission(self):
"Post a new submission to the given subreddit"
@@ -145,7 +144,7 @@ class SubredditPage(Page):
self.refresh_content()
@SubredditController.register('s')
@SubredditController.register(Command('SUBREDDIT_OPEN_SUBSCRIPTIONS'))
@logged_in
def open_subscriptions(self):
"Open user subscriptions page"
@@ -190,7 +189,10 @@ class SubredditPage(Page):
self.term.add_line(win, '{score} '.format(**data), row, 1)
text, attr = self.term.get_arrow(data['likes'])
self.term.add_line(win, text, attr=attr)
self.term.add_line(win, ' {created} {comments} '.format(**data))
self.term.add_line(win, ' {created} '.format(**data))
text, attr = self.term.timestamp_sep
self.term.add_line(win, text, attr=attr)
self.term.add_line(win, ' {comments} '.format(**data))
if data['saved']:
text, attr = self.term.saved

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, Command
class SubscriptionController(PageController):
@@ -18,12 +17,12 @@ class SubscriptionPage(Page):
def __init__(self, reddit, term, config, oauth):
super(SubscriptionPage, self).__init__(reddit, term, config, oauth)
self.controller = SubscriptionController(self, keymap=config.keymap)
self.content = SubscriptionContent.from_user(reddit, term.loader)
self.controller = SubscriptionController(self)
self.nav = Navigator(self.content.get)
self.subreddit_data = None
@SubscriptionController.register(curses.KEY_F5, 'r')
@SubscriptionController.register(Command('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(Command('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(Command('SUBSCRIPTION_EXIT'))
def close_subscriptions(self):
"Close subscriptions and return to the subreddit page"

View File

@@ -35,6 +35,7 @@ class Terminal(object):
# ASCII code
ESCAPE = 27
RETURN = 10
SPACE = 32
def __init__(self, stdscr, ascii=False):
@@ -57,6 +58,12 @@ class Terminal(object):
@property
def neutral_arrow(self):
symbol = '>' if self.ascii else ''
attr = curses.A_BOLD
return symbol, attr
@property
def timestamp_sep(self):
symbol = 'o' if self.ascii else ''
attr = curses.A_BOLD
return symbol, attr
@@ -356,6 +363,26 @@ class Terminal(object):
with self.suspend():
webbrowser.open_new_tab(url)
def open_pager(self, data):
"""
View a long block of text using the system's default pager.
The data string will be piped directly to the pager.
"""
pager = os.getenv('PAGER') or 'less'
try:
with self.suspend():
p = subprocess.Popen([pager], stdin=subprocess.PIPE)
p.stdin.write(self.clean(data))
p.stdin.close()
try:
p.wait()
except KeyboardInterrupt:
p.terminate()
except OSError:
self.show_notification('Could not open pager %s' % pager)
def open_editor(self, data=''):
"""
Open a temporary file using the system's default editor.
@@ -366,16 +393,19 @@ class Terminal(object):
"""
with NamedTemporaryFile(prefix='rtv-', suffix='.txt', mode='wb') as fp:
fp.write(codecs.encode(data, 'utf-8'))
fp.write(self.clean(data))
fp.flush()
editor = os.getenv('RTV_EDITOR') or os.getenv('EDITOR') or 'nano'
try:
with self.suspend():
subprocess.Popen([editor, fp.name]).wait()
p = subprocess.Popen([editor, fp.name])
try:
p.wait()
except KeyboardInterrupt:
p.terminate()
except OSError:
raise exceptions.ProgramError(
'Could not open file with %s' % editor)
self.show_notification('Could not open file with %s' % editor)
# Open a second file object to read. This appears to be necessary
# in order to read the changes made by some editors (gedit). w+