sync with 1.10.0

This commit is contained in:
David Foucher
2016-07-17 13:11:17 +02:00
34 changed files with 6118 additions and 186 deletions

View File

@@ -1,4 +1,30 @@
# -*- coding: utf-8 -*-
r"""
________ __________________________
___ __ \__________ /_____ /__(_)_ /_
__ /_/ / _ \ __ /_ __ /__ /_ __/
_ _, _// __/ /_/ / / /_/ / _ / / /_
/_/ |_| \___/\__,_/ \__,_/ /_/ \__/
________ _____ ______
___ __/__________________ ______(_)____________ ___ /
__ / _ _ \_ ___/_ __ `__ \_ /__ __ \ __ `/_ /
_ / / __/ / _ / / / / / / _ / / / /_/ /_ /
/_/ \___//_/ /_/ /_/ /_//_/ /_/ /_/\__,_/ /_/
___ ______
__ | / /__(_)_______ ______________
__ | / /__ /_ _ \_ | /| / / _ \_ ___/
__ |/ / _ / / __/_ |/ |/ // __/ /
_____/ /_/ \___/____/|__/ \___//_/
(RTV)
"""
from __future__ import unicode_literals
from .__version__ import __version__
@@ -6,4 +32,4 @@ from .__version__ import __version__
__title__ = 'Reddit Terminal Viewer'
__author__ = 'Michael Lazar'
__license__ = 'The MIT License (MIT)'
__copyright__ = '(c) 2015 Michael Lazar'
__copyright__ = '(c) 2016 Michael Lazar'

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import sys
import locale
import logging
@@ -12,7 +13,7 @@ from . import docs
from .config import Config, copy_default_config
from .oauth import OAuthHelper
from .terminal import Terminal
from .objects import curses_session
from .objects import curses_session, Color
from .subreddit import SubredditPage
from .exceptions import ConfigError
from .__version__ import __version__
@@ -29,15 +30,17 @@ _logger = logging.getLogger(__name__)
def main():
"Main entry point"
"""Main entry point"""
# Squelch SSL warnings
logging.captureWarnings(True)
locale.setlocale(locale.LC_ALL, '')
# Set the terminal title
title = 'rtv {0}'.format(__version__)
sys.stdout.write('\x1b]2;{0}\x07'.format(title))
if os.getenv('DISPLAY'):
title = 'rtv {0}'.format(__version__)
sys.stdout.write('\x1b]2;{0}\x07'.format(title))
sys.stdout.flush()
args = Config.get_args()
fargs, bindings = Config.get_file(args.get('config'))
@@ -73,7 +76,19 @@ def main():
# if args[0] != "header:":
# _http_logger.info(' '.join(args))
# client.print = print_to_file
logging.basicConfig(level=logging.DEBUG, filename=config['log'])
logging.basicConfig(
level=logging.DEBUG,
filename=config['log'],
format='%(asctime)s:%(levelname)s:%(filename)s:%(lineno)d:%(message)s')
_logger.info('Starting new session, RTV v%s', __version__)
env = [
('$DISPLAY', os.getenv('DISPLAY')),
('$XDG_CONFIG_HOME', os.getenv('XDG_CONFIG_HOME')),
('$BROWSER', os.getenv('BROWSER')),
('$PAGER', os.getenv('PAGER')),
('$RTV_EDITOR', os.getenv('RTV_EDITOR')),
('$RTV_URLVIEWER', os.getenv('RTV_URLVIEWER'))]
_logger.info('Environment: %s', env)
else:
# Add an empty handler so the logger doesn't complain
logging.root.addHandler(logging.NullHandler())
@@ -83,6 +98,11 @@ def main():
try:
with curses_session() as stdscr:
# Initialize global color-pairs with curses
if not config['monochrome']:
Color.init()
term = Terminal(stdscr, config['ascii'])
with term.loader('Initializing', catch_exception=False):
reddit = praw.Reddit(user_agent=user_agent,
@@ -124,4 +144,4 @@ def main():
# Explicitly close file descriptors opened by Tornado's IOLoop
tornado.ioloop.IOLoop.current().close(all_fds=True)
sys.exit(main())
sys.exit(main())

View File

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

View File

@@ -33,19 +33,22 @@ def build_parser():
'-V', '--version', action='version', version='rtv '+__version__)
parser.add_argument(
'-s', dest='subreddit',
help='name of the subreddit that will be opened on start')
help='Name of the subreddit that will be opened on start')
parser.add_argument(
'-l', dest='link',
help='full URL of a submission that will be opened on start')
help='Full URL of a submission that will be opened on start')
parser.add_argument(
'--log', metavar='FILE', action='store',
help='log HTTP requests to the given file')
help='Log HTTP requests to the given file')
parser.add_argument(
'--config', metavar='FILE', action='store',
help='Load configuration settings from the given file')
parser.add_argument(
'--ascii', action='store_const', const=True,
help='enable ascii-only mode')
help='Enable ascii-only mode')
parser.add_argument(
'--monochrome', action='store_const', const=True,
help='Disable color')
parser.add_argument(
'--non-persistent', dest='persistent', action='store_const',
const=False,
@@ -79,6 +82,7 @@ def copy_default_config(filename=CONFIG):
print('Copying default settings to %s' % filename)
shutil.copy(DEFAULT_CONFIG, filename)
os.chmod(filename, 0o664)
class OrderedSet(object):
@@ -208,6 +212,7 @@ class Config(object):
params = {
'ascii': partial(config.getboolean, 'rtv'),
'monochrome': partial(config.getboolean, 'rtv'),
'clear_auth': partial(config.getboolean, 'rtv'),
'persistent': partial(config.getboolean, 'rtv'),
'history_size': partial(config.getint, 'rtv'),

View File

@@ -6,6 +6,7 @@ from datetime import datetime
import six
import praw
from praw.errors import InvalidSubreddit
from kitchen.text.display import wrap
from . import exceptions
@@ -96,6 +97,7 @@ class Content(object):
data['type'] = 'MoreComments'
data['count'] = comment.count
data['body'] = 'More comments'
data['hidden'] = True
else:
author = getattr(comment, 'author', '[deleted]')
name = getattr(author, 'name', '[deleted]')
@@ -118,6 +120,7 @@ class Content(object):
data['gold'] = comment.gilded > 0
data['permalink'] = permalink
data['stickied'] = stickied
data['hidden'] = False
data['saved'] = comment.saved
return data
@@ -129,10 +132,11 @@ class Content(object):
displayed through the terminal.
Definitions:
permalink - Full URL to the submission comments.
url_full - Link that the submission points to.
url - URL that is displayed on the subreddit page, may be
"selfpost" or "x-post" or a link.
permalink - URL to the reddit page with submission comments.
url_full - URL that the submission points to.
url - URL that will be displayed on the subreddit page, may be
"selfpost", "x-post submission", "x-post subreddit", or an
external link.
"""
reddit_link = re.compile(
@@ -148,8 +152,7 @@ class Content(object):
data['text'] = sub.selftext
data['created'] = cls.humanize_timestamp(sub.created_utc)
data['comments'] = '{0} comments'.format(sub.num_comments)
data['score'] = '{0} pts'.format(
'-' if sub.hide_score else sub.score)
data['score'] = '{0} pts'.format('-' if sub.hide_score else sub.score)
data['author'] = name
data['permalink'] = sub.permalink
data['subreddit'] = six.text_type(sub.subreddit)
@@ -159,6 +162,8 @@ class Content(object):
data['gold'] = sub.gilded > 0
data['nsfw'] = sub.over_18
data['stickied'] = sub.stickied
data['hidden'] = False
data['xpost_subreddit'] = None
data['index'] = None # This is filled in later by the method caller
data['saved'] = sub.saved
@@ -168,8 +173,13 @@ class Content(object):
elif reddit_link.match(sub.url):
# Strip the subreddit name from the permalink to avoid having
# submission.subreddit.url make a separate API call
data['url'] = 'self.{0}'.format(sub.url.split('/')[4])
data['url_type'] = 'x-post'
url_parts = sub.url.split('/')
data['xpost_subreddit'] = url_parts[4]
data['url'] = 'self.{0}'.format(url_parts[4])
if 'comments' in url_parts:
data['url_type'] = 'x-post submission'
else:
data['url_type'] = 'x-post subreddit'
else:
data['url'] = sub.url
data['url_type'] = 'external'
@@ -324,7 +334,8 @@ class SubmissionContent(Content):
'cache': cache,
'count': count,
'level': data['level'],
'body': 'Hidden'}
'body': 'Hidden',
'hidden': True}
self._comment_data[index:index + len(cache)] = [comment]
@@ -409,6 +420,11 @@ class SubredditContent(Content):
submissions = reddit.search(query, subreddit=name, sort=order)
else:
if name == '':
# Praw does not correctly handle empty strings
# https://github.com/praw-dev/praw/issues/615
raise InvalidSubreddit()
if name == 'front':
dispatch = {
None: reddit.get_front_page,
@@ -420,6 +436,9 @@ class SubredditContent(Content):
}
else:
subreddit = reddit.get_subreddit(name)
# For special subreddits like /r/random we want to replace the
# display name with the one returned by the request.
display_name = '/r/{0}'.format(subreddit.display_name)
dispatch = {
None: subreddit.get_hot,
'hot': subreddit.get_hot,
@@ -456,9 +475,10 @@ class SubredditContent(Content):
data = self.strip_praw_submission(submission)
except:
continue
data['index'] = index
data['index'] = len(self._submission_data) + 1
# Add the post number to the beginning of the title
data['title'] = '{0}. {1}'.format(index+1, data.get('title'))
data['title'] = '{0}. {1}'.format(data['index'], data['title'])
self._submission_data.append(data)
# Modifies the original dict, faster than copying

View File

@@ -45,6 +45,7 @@ HELP = """
`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
`b` : Display URLs with urlview
"""
COMMENT_FILE = """

View File

@@ -35,4 +35,8 @@ class ProgramError(RTVError):
class BrowserError(RTVError):
"Could not open a web browser tab"
"Could not open a web browser tab"
class TemporaryFileError(RTVError):
"Indicates that an error has occurred and the file should not be deleted"

View File

@@ -62,7 +62,8 @@ def curses_session():
# Hide the blinking cursor
curses.curs_set(0)
Color.init()
# Assign the terminal's default (background) color to code -1
curses.use_default_colors()
yield stdscr
@@ -254,7 +255,6 @@ class LoadScreen(object):
class Color(object):
"""
Color attributes for curses.
"""
@@ -286,9 +286,6 @@ class Color(object):
curses color pairs can be accessed directly through class attributes.
"""
# Assign the terminal's default (background) color to code -1
curses.use_default_colors()
for index, (attr, code) in enumerate(cls._colors.items(), start=1):
curses.init_pair(index, code[0], code[1])
setattr(cls, attr, curses.color_pair(index))
@@ -318,7 +315,8 @@ class Navigator(object):
valid_page_cb,
page_index=0,
cursor_index=0,
inverted=False):
inverted=False,
top_item_height=None):
"""
Params:
valid_page_callback (func): This function, usually `Content.get`,
@@ -334,11 +332,16 @@ class Navigator(object):
inverted - The page is drawn from the bottom of the screen,
starting with the page index, up to the top of the
screen.
top_item_height (int): If this is set to a non-null value
The number of columns that the top-most item
should utilize if non-inverted. This is used for a special mode
where all items are drawn non-inverted except for the top one.
"""
self.page_index = page_index
self.cursor_index = cursor_index
self.inverted = inverted
self.top_item_height = top_item_height
self._page_cb = valid_page_cb
@property
@@ -399,15 +402,21 @@ class Navigator(object):
# Flip the orientation and reset the cursor
self.flip(self.cursor_index)
self.cursor_index = 0
self.top_item_height = None
redraw = True
else:
if self.cursor_index > 0:
self.cursor_index -= 1
if self.top_item_height and self.cursor_index == 0:
# Selecting the partially displayed item
self.top_item_height = None
redraw = True
else:
self.page_index -= self.step
if self._is_valid(self.absolute_index):
# We have reached the beginning of the page - move the
# index
self.top_item_height = None
redraw = True
else:
self.page_index += self.step
@@ -481,6 +490,7 @@ class Navigator(object):
self.page_index += (self.step * n_windows)
self.cursor_index = n_windows
self.inverted = not self.inverted
self.top_item_height = None
def _is_valid(self, page_index):
"""

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import sys
import time
import curses
@@ -10,6 +11,8 @@ from kitchen.text.display import textual_width
from . import docs
from .objects import Controller, Color, Command
from .exceptions import TemporaryFileError
from .__version__ import __version__
def logged_in(f):
@@ -234,16 +237,19 @@ class Page(object):
self.term.flash()
return
text = self.term.open_editor(info)
if text == content:
self.term.show_notification('Canceled')
return
with self.term.open_editor(info) as text:
if text == content:
self.term.show_notification('Canceled')
return
with self.term.loader('Editing', delay=0):
data['object'].edit(text)
time.sleep(2.0)
if self.term.loader.exception is None:
self.refresh_content()
with self.term.loader('Editing', delay=0):
data['object'].edit(text)
time.sleep(2.0)
if self.term.loader.exception is None:
self.refresh_content()
else:
raise TemporaryFileError()
@PageController.register(Command('INBOX'))
@logged_in
@@ -291,9 +297,22 @@ class Page(object):
ch, attr = str(' '), curses.A_REVERSE | curses.A_BOLD | Color.CYAN
window.bkgd(ch, attr)
sub_name = self.content.name.replace('/r/front', 'Front Page')
sub_name = self.content.name
sub_name = sub_name.replace('/r/front', 'Front Page')
sub_name = sub_name.replace('/r/me', 'My Submissions')
self.term.add_line(window, sub_name, 0, 0)
# Set the terminal title
if len(sub_name) > 50:
title = sub_name.strip('/').rsplit('/', 1)[1].replace('_', ' ')
else:
title = sub_name
if os.getenv('DISPLAY'):
title += ' - rtv {0}'.format(__version__)
sys.stdout.write('\x1b]2;{0}\x07'.format(title))
sys.stdout.flush()
if self.reddit.user is not None:
# The starting position of the name depends on if we're converting
# to ascii or not
@@ -344,29 +363,46 @@ class Page(object):
# If not inverted, align the first submission with the top and draw
# downwards. If inverted, align the first submission with the bottom
# and draw upwards.
cancel_inverted = True
current_row = (win_n_rows - 1) if inverted else 0
available_rows = (win_n_rows - 1) if inverted else win_n_rows
top_item_height = None if inverted else self.nav.top_item_height
for data in self.content.iterate(page_index, step, win_n_cols - 2):
subwin_n_rows = min(available_rows, data['n_rows'])
subwin_inverted = inverted
if top_item_height is not None:
# Special case: draw the page as non-inverted, except for the
# top element. This element will be drawn as inverted with a
# restricted height
subwin_n_rows = min(subwin_n_rows, top_item_height)
subwin_inverted = True
top_item_height = None
subwin_n_cols = win_n_cols - data['offset']
start = current_row - subwin_n_rows if inverted else current_row
subwindow = window.derwin(
subwin_n_rows, subwin_n_cols, start, data['offset'])
attr = self._draw_item(subwindow, data, inverted)
attr = self._draw_item(subwindow, data, subwin_inverted)
self._subwindows.append((subwindow, attr))
available_rows -= (subwin_n_rows + 1) # Add one for the blank line
current_row += step * (subwin_n_rows + 1)
if available_rows <= 0:
# Indicate the page is full and we can keep the inverted screen.
cancel_inverted = False
break
else:
# If the page is not full we need to make sure that it is NOT
if len(self._subwindows) == 1:
# Never draw inverted if only one subwindow. The top of the
# subwindow should always be aligned with the top of the screen.
cancel_inverted = True
if cancel_inverted and self.nav.inverted:
# In some cases we need to make sure that the screen is NOT
# inverted. Unfortunately, this currently means drawing the whole
# page over again. Could not think of a better way to pre-determine
# if the content will fill up the page, given that it is dependent
# on the size of the terminal.
if self.nav.inverted:
self.nav.flip((len(self._subwindows) - 1))
self._draw_content()
self.nav.flip((len(self._subwindows) - 1))
self._draw_content()
self._row = n_rows

View File

@@ -13,6 +13,9 @@
; This may be necessary for compatibility with some terminal browsers.
ascii = False
; Turn on monochrome mode to disable color.
monochrome = False
; Enable debugging by logging all HTTP requests and errors to the given file.
;log = /tmp/rtv.log
@@ -110,6 +113,7 @@ SUBMISSION_OPEN_IN_BROWSER = o, <LF>, <KEY_ENTER>
SUBMISSION_POST = c
SUBMISSION_EXIT = h, <KEY_LEFT>
SUBMISSION_OPEN_IN_PAGER = l, <KEY_RIGHT>
SUBMISSION_OPEN_IN_URLVIEWER = b
; Subreddit page
SUBREDDIT_SEARCH = f

View File

@@ -8,6 +8,7 @@ from . import docs
from .content import SubmissionContent
from .page import Page, PageController, logged_in
from .objects import Navigator, Color, Command
from .exceptions import TemporaryFileError
class SubmissionController(PageController):
@@ -33,11 +34,19 @@ class SubmissionPage(Page):
current_index = self.nav.absolute_index
self.content.toggle(current_index)
# This logic handles a display edge case after a comment toggle. We
# want to make sure that when we re-draw the page, the cursor stays at
# its current absolute position on the screen. In order to do this,
# apply a fixed offset if, while inverted, we either try to hide the
# bottom comment or toggle any of the middle comments.
if self.nav.inverted:
# Reset the navigator so that the cursor is at the bottom of the
# page. This is a workaround to handle if folding the comment
# causes the cursor index to go out of bounds.
self.nav.page_index, self.nav.cursor_index = current_index, 0
data = self.content.get(current_index)
if data['hidden'] or self.nav.cursor_index != 0:
window = self._subwindows[-1][0]
n_rows, _ = window.getmaxyx()
self.nav.flip(len(self._subwindows) - 1)
self.nav.top_item_height = n_rows
@SubmissionController.register(Command('SUBMISSION_EXIT'))
def exit_submission(self):
@@ -113,17 +122,20 @@ class SubmissionPage(Page):
type=data['type'].lower(),
content=content)
comment = self.term.open_editor(comment_info)
if not comment:
self.term.show_notification('Canceled')
return
with self.term.open_editor(comment_info) as comment:
if not comment:
self.term.show_notification('Canceled')
return
with self.term.loader('Posting', delay=0):
reply(comment)
# Give reddit time to process the submission
time.sleep(2.0)
if not self.term.loader.exception:
self.refresh_content()
with self.term.loader('Posting', delay=0):
reply(comment)
# Give reddit time to process the submission
time.sleep(2.0)
if self.term.loader.exception is None:
self.refresh_content()
else:
raise TemporaryFileError()
@SubmissionController.register(Command('DELETE'))
@logged_in
@@ -135,6 +147,15 @@ class SubmissionPage(Page):
else:
self.term.flash()
@SubmissionController.register(Command('SUBMISSION_OPEN_IN_URLVIEWER'))
def comment_urlview(self):
data = self.content.get(self.nav.absolute_index)
comment = data.get('body', '')
if comment:
self.term.open_urlview(comment)
else:
self.term.flash()
def _draw_item(self, win, data, inverted):
if data['type'] == 'MoreComments':
@@ -155,6 +176,16 @@ class SubmissionPage(Page):
valid_rows = range(0, n_rows)
offset = 0 if not inverted else -(data['n_rows'] - n_rows)
# If there isn't enough space to fit the comment body on the screen,
# replace the last line with a notification.
split_body = data['split_body']
if data['n_rows'] > n_rows:
# Only when there is a single comment on the page and not inverted
if not inverted and len(self._subwindows) == 0:
cutoff = data['n_rows'] - n_rows + 1
split_body = split_body[:-cutoff]
split_body.append('(Not enough space to display)')
row = offset
if row in valid_rows:
@@ -182,7 +213,7 @@ class SubmissionPage(Page):
text, attr = self.term.saved
self.term.add_line(win, text, attr=attr)
for row, text in enumerate(data['split_body'], start=offset+1):
for row, text in enumerate(split_body, start=offset+1):
if row in valid_rows:
self.term.add_line(win, text, row, 1)
@@ -201,7 +232,8 @@ class SubmissionPage(Page):
n_cols -= 1
self.term.add_line(win, '{body}'.format(**data), 0, 1)
self.term.add_line(win, ' [{count}]'.format(**data), attr=curses.A_BOLD)
self.term.add_line(
win, ' [{count}]'.format(**data), attr=curses.A_BOLD)
attr = Color.get_level(data['level'])
self.term.addch(win, 0, 0, self.term.vline, attr)

View File

@@ -10,6 +10,7 @@ from .page import Page, PageController, logged_in
from .objects import Navigator, Color, Command
from .submission import SubmissionPage
from .subscription import SubscriptionPage
from .exceptions import TemporaryFileError
class SubredditController(PageController):
@@ -99,7 +100,9 @@ class SubredditPage(Page):
data = self.content.get(self.nav.absolute_index)
if data['url_type'] == 'selfpost':
self.open_submission()
elif data['url_type'] == 'x-post':
elif data['url_type'] == 'x-post subreddit':
self.refresh_content(order='ignore', name=data['xpost_subreddit'])
elif data['url_type'] == 'x-post submission':
self.open_submission(url=data['url_full'])
self.config.history.add(data['url_full'])
else:
@@ -118,31 +121,35 @@ class SubredditPage(Page):
return
submission_info = docs.SUBMISSION_FILE.format(name=name)
text = self.term.open_editor(submission_info)
if not text or '\n' not in text:
self.term.show_notification('Canceled')
return
with self.term.open_editor(submission_info) as text:
if not text:
self.term.show_notification('Canceled')
return
elif '\n' not in text:
self.term.show_notification('Missing body')
return
title, content = text.split('\n', 1)
with self.term.loader('Posting', delay=0):
submission = self.reddit.submit(name, title, text=content,
raise_captcha_exception=True)
# Give reddit time to process the submission
time.sleep(2.0)
if self.term.loader.exception:
return
title, content = text.split('\n', 1)
with self.term.loader('Posting', delay=0):
submission = self.reddit.submit(name, title, text=content,
raise_captcha_exception=True)
# Give reddit time to process the submission
time.sleep(2.0)
if self.term.loader.exception:
raise TemporaryFileError()
# Open the newly created post
with self.term.loader('Loading submission'):
page = SubmissionPage(
self.reddit, self.term, self.config, self.oauth,
submission=submission)
if self.term.loader.exception:
return
if not self.term.loader.exception:
# Open the newly created post
with self.term.loader('Loading submission'):
page = SubmissionPage(
self.reddit, self.term, self.config, self.oauth,
submission=submission)
if self.term.loader.exception:
return
page.loop()
page.loop()
self.refresh_content()
self.refresh_content()
@SubredditController.register(Command('SUBREDDIT_OPEN_SUBSCRIPTIONS'))
@logged_in

View File

@@ -6,12 +6,14 @@ import sys
import time
import codecs
import curses
import logging
import tempfile
import webbrowser
import subprocess
import curses.ascii
from curses import textpad
from datetime import datetime
from contextlib import contextmanager
from tempfile import NamedTemporaryFile
import six
from kitchen.text.display import textual_width_chop
@@ -27,6 +29,9 @@ except ImportError:
unescape = html_parser.HTMLParser().unescape
_logger = logging.getLogger(__name__)
class Terminal(object):
MIN_HEIGHT = 10
@@ -58,13 +63,13 @@ class Terminal(object):
@property
def neutral_arrow(self):
symbol = '>' if self.ascii else ''
symbol = 'o' if self.ascii else ''
attr = curses.A_BOLD
return symbol, attr
@property
def timestamp_sep(self):
symbol = 'o' if self.ascii else ''
symbol = '-'
attr = curses.A_BOLD
return symbol, attr
@@ -100,7 +105,14 @@ class Terminal(object):
if self._display is None:
if sys.platform == 'darwin':
# OSX doesn't always set DISPLAY so we can't use this to check
display = True
# Note: Disabling for now, with the hope that if this
# is a widespread issue then people will complain and we can
# come up with a better solution. Checking for $DISPLAY is
# used extensively in mailcap files, so it really *should* be
# set properly. I don't have a mac anymore so I can't test.
# display = True
display = bool(os.environ.get("DISPLAY"))
else:
display = bool(os.environ.get("DISPLAY"))
@@ -350,7 +362,8 @@ class Terminal(object):
'Browser exited with status=%s' % code)
time.sleep(0.01)
else:
raise exceptions.BrowserError('Timeout opening browser')
raise exceptions.BrowserError(
'Timeout opening browser')
finally:
# Can't check the loader exception because the oauth module
# supersedes this loader and we need to always kill the
@@ -383,37 +396,75 @@ class Terminal(object):
except OSError:
self.show_notification('Could not open pager %s' % pager)
@contextmanager
def open_editor(self, data=''):
"""
Open a temporary file using the system's default editor.
Open a file for editing using the system's default editor.
The data string will be written to the file before opening. This
function will block until the editor has closed. At that point the file
will be read and and lines starting with '#' will be stripped.
After the file has been altered, the text will be read back and lines
starting with '#' will be stripped. If an error occurs inside of the
context manager, the file will be preserved. Otherwise, the file will
be deleted when the context manager closes.
Params:
data (str): If provided, text will be written to the file before
opening it with the editor.
Returns:
text (str): The text that the user entered into the editor.
"""
with NamedTemporaryFile(prefix='rtv-', suffix='.txt', mode='wb') as fp:
fp.write(self.clean(data))
fp.flush()
editor = os.getenv('RTV_EDITOR') or os.getenv('EDITOR') or 'nano'
filename = 'rtv_{:%Y%m%d_%H%M%S}.txt'.format(datetime.now())
filepath = os.path.join(tempfile.gettempdir(), filename)
with codecs.open(filepath, 'w', 'utf-8') as fp:
fp.write(data)
_logger.info('File created: %s', filepath)
editor = os.getenv('RTV_EDITOR') or os.getenv('EDITOR') or 'nano'
try:
with self.suspend():
p = subprocess.Popen([editor, filepath])
try:
p.wait()
except KeyboardInterrupt:
p.terminate()
except OSError:
self.show_notification('Could not open file with %s' % editor)
with codecs.open(filepath, 'r', 'utf-8') as fp:
text = ''.join(line for line in fp if not line.startswith('#'))
text = text.rstrip()
try:
yield text
except exceptions.TemporaryFileError:
# All exceptions will cause the file to *not* be removed, but these
# ones should also be swallowed
_logger.info('Caught TemporaryFileError')
self.show_notification('Post saved as: %s', filepath)
else:
# If no errors occurred, try to remove the file
try:
with self.suspend():
p = subprocess.Popen([editor, fp.name])
try:
p.wait()
except KeyboardInterrupt:
p.terminate()
os.remove(filepath)
except OSError:
self.show_notification('Could not open file with %s' % editor)
_logger.warning('Could not delete: %s', filepath)
else:
_logger.info('File deleted: %s', filepath)
# Open a second file object to read. This appears to be necessary
# in order to read the changes made by some editors (gedit). w+
# mode does not work!
with codecs.open(fp.name, 'r', 'utf-8') as fp2:
text = ''.join(line for line in fp2 if not line.startswith('#'))
text = text.rstrip()
return text
def open_urlview(self, data):
urlview = os.getenv('RTV_URLVIEWER') or 'urlview'
try:
with self.suspend():
p = subprocess.Popen([urlview],
stdin=subprocess.PIPE)
try:
p.communicate(input=six.b(data))
except KeyboardInterrupt:
p.terminate()
except OSError:
self.show_notification(
'Could not open urls with {}'.format(urlview))
def text_input(self, window, allow_resize=False):
"""