Large commit to add support for browsing the inbox
This commit is contained in:
@@ -227,11 +227,11 @@ def main():
|
||||
# headers to avoid a 429 response from reddit.com
|
||||
url = requests.head(config['link'], headers=reddit.http.headers,
|
||||
allow_redirects=True).url
|
||||
|
||||
page.open_submission(url=url)
|
||||
|
||||
# Launch the subreddit page
|
||||
page.loop()
|
||||
while page:
|
||||
page = page.loop()
|
||||
|
||||
except ConfigError as e:
|
||||
_logger.exception(e)
|
||||
|
||||
191
rtv/content.py
191
rtv/content.py
@@ -164,7 +164,7 @@ class Content(object):
|
||||
data['saved'] = comment.saved
|
||||
if comment.edited:
|
||||
data['edited'] = '(edit {})'.format(
|
||||
cls.humanize_timestamp(comment.edited))
|
||||
cls.humanize_timestamp(comment.edited))
|
||||
else:
|
||||
data['edited'] = ''
|
||||
else:
|
||||
@@ -198,7 +198,7 @@ class Content(object):
|
||||
data['hidden'] = False
|
||||
if comment.edited:
|
||||
data['edited'] = '(edit {})'.format(
|
||||
cls.humanize_timestamp(comment.edited))
|
||||
cls.humanize_timestamp(comment.edited))
|
||||
else:
|
||||
data['edited'] = ''
|
||||
|
||||
@@ -249,9 +249,9 @@ class Content(object):
|
||||
data['saved'] = sub.saved
|
||||
if sub.edited:
|
||||
data['edited'] = '(edit {})'.format(
|
||||
cls.humanize_timestamp(sub.edited))
|
||||
cls.humanize_timestamp(sub.edited))
|
||||
data['edited_long'] = '(edit {})'.format(
|
||||
cls.humanize_timestamp(sub.edited, True))
|
||||
cls.humanize_timestamp(sub.edited, True))
|
||||
else:
|
||||
data['edited'] = ''
|
||||
data['edited_long'] = ''
|
||||
@@ -295,6 +295,52 @@ class Content(object):
|
||||
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def strip_praw_message(cls, msg):
|
||||
"""
|
||||
Parse through a message and return a dict with data ready to be
|
||||
displayed through the terminal. Messages can be of either type
|
||||
praw.objects.Message or praw.object.Comment. The comments returned will
|
||||
contain special fields unique to messages and can't be parsed as normal
|
||||
comment objects.
|
||||
"""
|
||||
author = getattr(msg, 'author', None)
|
||||
|
||||
data = {}
|
||||
data['object'] = msg
|
||||
|
||||
if isinstance(msg, praw.objects.Message):
|
||||
data['type'] = 'Message'
|
||||
data['level'] = msg.nested_level
|
||||
data['distinguished'] = msg.distinguished
|
||||
data['permalink'] = None
|
||||
data['submission_permalink'] = None
|
||||
data['subreddit_name'] = None
|
||||
data['link_title'] = None
|
||||
data['context'] = None
|
||||
else:
|
||||
data['type'] = 'InboxComment'
|
||||
data['level'] = 0
|
||||
data['distinguished'] = None
|
||||
data['permalink'] = msg._fast_permalink
|
||||
data['submission_permalink'] = '/'.join(data['permalink'].split('/')[:-2])
|
||||
data['subreddit_name'] = msg.subreddit_name_prefixed
|
||||
data['link_title'] = msg.link_title
|
||||
data['context'] = msg.context
|
||||
|
||||
data['id'] = msg.id
|
||||
data['subject'] = msg.subject
|
||||
data['body'] = msg.body
|
||||
data['html'] = msg.body_html
|
||||
data['created'] = cls.humanize_timestamp(msg.created_utc)
|
||||
data['created_long'] = cls.humanize_timestamp(msg.created_utc, True)
|
||||
data['recipient'] = msg.dest
|
||||
data['distinguished'] = msg.distinguished
|
||||
data['author'] = author.name if author else '[deleted]'
|
||||
data['is_new'] = msg.new
|
||||
data['was_comment'] = msg.was_comment
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def humanize_timestamp(utc_timestamp, verbose=False):
|
||||
"""
|
||||
@@ -306,20 +352,50 @@ class Content(object):
|
||||
seconds = int(timedelta.total_seconds())
|
||||
if seconds < 60:
|
||||
return 'moments ago' if verbose else '0min'
|
||||
|
||||
minutes = seconds // 60
|
||||
if minutes < 60:
|
||||
return '%d minutes ago' % minutes if verbose else '%dmin' % minutes
|
||||
if verbose and minutes == 1:
|
||||
return '1 minutes ago'
|
||||
elif verbose:
|
||||
return '%d minutes ago' % minutes
|
||||
else:
|
||||
return '%dmin' % minutes
|
||||
|
||||
hours = minutes // 60
|
||||
if hours < 24:
|
||||
return '%d hours ago' % hours if verbose else '%dhr' % hours
|
||||
if verbose and hours == 1:
|
||||
return '1 hour ago'
|
||||
elif verbose:
|
||||
return '%d hours ago' % hours
|
||||
else:
|
||||
return '%dhr' % hours
|
||||
|
||||
days = hours // 24
|
||||
if days < 30:
|
||||
return '%d days ago' % days if verbose else '%dday' % days
|
||||
if verbose and days == 1:
|
||||
return '1 day ago'
|
||||
elif verbose:
|
||||
return '%d days ago' % days
|
||||
else:
|
||||
return '%dday' % days
|
||||
|
||||
months = days // 30.4
|
||||
if months < 12:
|
||||
return '%d months ago' % months if verbose else '%dmonth' % months
|
||||
if verbose and months == 1:
|
||||
return '1 month ago'
|
||||
elif verbose:
|
||||
return '%d months ago' % months
|
||||
else:
|
||||
return '%dmonth' % months
|
||||
|
||||
years = months // 12
|
||||
return '%d years ago' % years if verbose else '%dyr' % years
|
||||
if verbose and years == 1:
|
||||
return '1 year ago'
|
||||
elif verbose:
|
||||
return '%d years ago' % years
|
||||
else:
|
||||
return '%dyr' % years
|
||||
|
||||
@staticmethod
|
||||
def wrap_text(text, width):
|
||||
@@ -380,10 +456,18 @@ class SubmissionContent(Content):
|
||||
def from_url(cls, reddit, url, loader, indent_size=2, max_indent_level=8,
|
||||
order=None, max_comment_cols=120):
|
||||
|
||||
url = url.replace('http:', 'https:') # Reddit forces SSL
|
||||
# Reddit forces SSL
|
||||
url = url.replace('http:', 'https:')
|
||||
|
||||
# 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.')
|
||||
|
||||
# Sometimes reddit will return internal links like "context" as
|
||||
# relative URLs.
|
||||
if url.startswith('/'):
|
||||
url = 'https://www.reddit.com' + url
|
||||
|
||||
submission = reddit.get_submission(url, comment_sort=order)
|
||||
return cls(submission, loader, indent_size, max_indent_level, order,
|
||||
max_comment_cols)
|
||||
@@ -854,6 +938,92 @@ class SubscriptionContent(Content):
|
||||
return data
|
||||
|
||||
|
||||
class InboxContent(Content):
|
||||
|
||||
def __init__(self, order, content_generator, loader,
|
||||
indent_size=2, max_indent_level=8):
|
||||
|
||||
self.name = 'My Inbox'
|
||||
self.order = order
|
||||
self.query = None
|
||||
self.indent_size = indent_size
|
||||
self.max_indent_level = max_indent_level
|
||||
self._loader = loader
|
||||
self._content_generator = content_generator
|
||||
self._content_data = []
|
||||
|
||||
try:
|
||||
self.get(0)
|
||||
except IndexError:
|
||||
if order == 'all':
|
||||
raise exceptions.InboxError('Empty Inbox')
|
||||
else:
|
||||
raise exceptions.InboxError('Empty Inbox [%s]' % order)
|
||||
|
||||
@classmethod
|
||||
def from_user(cls, reddit, loader, order='all'):
|
||||
if order == 'all':
|
||||
items = reddit.get_inbox(limit=None)
|
||||
elif order == 'unread':
|
||||
items = reddit.get_unread(limit=None)
|
||||
elif order == 'messages':
|
||||
items = reddit.get_messages(limit=None)
|
||||
elif order == 'comments':
|
||||
items = reddit.get_comment_replies(limit=None)
|
||||
elif order == 'posts':
|
||||
items = reddit.get_post_replies(limit=None)
|
||||
elif order == 'mentions':
|
||||
items = reddit.get_mentions(limit=None)
|
||||
elif order == 'sent':
|
||||
items = reddit.get_sent(limit=None)
|
||||
else:
|
||||
raise exceptions.InboxError('Invalid order %s' % order)
|
||||
|
||||
return cls(order, items, loader)
|
||||
|
||||
@property
|
||||
def range(self):
|
||||
return 0, len(self._content_data) - 1
|
||||
|
||||
def get(self, index, n_cols=70):
|
||||
"""
|
||||
Grab the `i`th object, with the title field formatted to fit
|
||||
inside of a window of width `n_cols`
|
||||
"""
|
||||
|
||||
if index < 0:
|
||||
raise IndexError
|
||||
|
||||
while index >= len(self._content_data):
|
||||
try:
|
||||
with self._loader('Loading content'):
|
||||
item = next(self._content_generator)
|
||||
if self._loader.exception:
|
||||
raise IndexError
|
||||
except StopIteration:
|
||||
raise IndexError
|
||||
else:
|
||||
if isinstance(item, praw.objects.Message):
|
||||
# Message chains can be treated like comment trees
|
||||
for child_message in self.flatten_comments([item]):
|
||||
data = self.strip_praw_message(child_message)
|
||||
self._content_data.append(data)
|
||||
else:
|
||||
# Comments also return children, but we don't display them
|
||||
# in the Inbox page so they don't need to be parsed here.
|
||||
data = self.strip_praw_message(item)
|
||||
self._content_data.append(data)
|
||||
|
||||
data = self._content_data[index]
|
||||
indent_level = min(data['level'], self.max_indent_level)
|
||||
data['h_offset'] = indent_level * self.indent_size
|
||||
width = n_cols - data['h_offset']
|
||||
data['split_body'] = self.wrap_text(data['body'], width=width)
|
||||
data['n_rows'] = len(data['split_body']) + 2
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class RequestHeaderRateLimiter(DefaultHandler):
|
||||
"""Custom PRAW request handler for rate-limiting requests.
|
||||
|
||||
@@ -952,7 +1122,6 @@ class RequestHeaderRateLimiter(DefaultHandler):
|
||||
"""Remove items from cache matching URLs.
|
||||
|
||||
Return the number of items removed.
|
||||
|
||||
"""
|
||||
if isinstance(urls, six.text_type):
|
||||
urls = [urls]
|
||||
|
||||
41
rtv/docs.py
41
rtv/docs.py
@@ -55,20 +55,21 @@ https://github.com/michael-lazar/rtv
|
||||
Q : Force quit
|
||||
a : Upvote
|
||||
z : Downvote
|
||||
c : Compose a new submission/comment
|
||||
c : Compose a new submission/comment/reply
|
||||
C : Compose a new private message
|
||||
e : Edit a submission/comment
|
||||
d : Delete a submission/comment
|
||||
i : Display new messages
|
||||
i : View inbox
|
||||
s : Show subscribed subreddits
|
||||
S : Show subscribed multireddits
|
||||
w : Save a submission/comment
|
||||
w : Save submission/comment, or mark message as read
|
||||
l : View comments, or open comment in pager
|
||||
h : Return to subreddit
|
||||
o : Open the submission or comment url
|
||||
SPACE : Hide a submission, or fold/expand the selected comment tree
|
||||
b : Display urls with urlview
|
||||
y : Copy submission permalink to clipboard
|
||||
Y : Copy submission or comment urls to clipboard
|
||||
h : Return to the previous page
|
||||
o : Open the submission/comment URL
|
||||
SPACE : Hide submission, or fold/expand the selected comment tree
|
||||
b : Send submission/comment URLs to a urlview program
|
||||
y : Copy submission permalink to the clipboard
|
||||
Y : Copy submission/comment URLs to the clipboard
|
||||
F2 : Cycle to previous theme
|
||||
F3 : Cycle to next theme
|
||||
|
||||
@@ -105,6 +106,10 @@ BANNER_SEARCH = """
|
||||
[1]relevance [2]top [3]comments [4]new
|
||||
"""
|
||||
|
||||
BANNER_INBOX = """
|
||||
[1]all [2]unread [3]messages [4]comments [5]posts [6]mentions [7]sent
|
||||
"""
|
||||
|
||||
FOOTER_SUBREDDIT = """
|
||||
[?]Help [q]Quit [l]Comments [/]Prompt [u]Login [o]Open [c]Post [a/z]Vote [r]Refresh
|
||||
"""
|
||||
@@ -114,12 +119,16 @@ FOOTER_SUBMISSION = """
|
||||
"""
|
||||
|
||||
FOOTER_SUBSCRIPTION = """
|
||||
[?]Help [q]Quit [h]Return [l]Select
|
||||
[?]Help [q]Quit [h]Return [l]Select Subreddit [r]Refresh
|
||||
"""
|
||||
|
||||
FOOTER_INBOX = """
|
||||
[?]Help [l]View Context [o]Open Submission [c]Reply [w]Mark Read [r]Refresh
|
||||
"""
|
||||
|
||||
TOKEN = "INSTRUCTIONS"
|
||||
|
||||
COMMENT_FILE = """<!--{token}
|
||||
REPLY_FILE = """<!--{token}
|
||||
Replying to {{author}}'s {{type}}:
|
||||
{{content}}
|
||||
|
||||
@@ -154,6 +163,16 @@ The submission is shown below, update it and save the file.
|
||||
{{content}}
|
||||
""".format(token=TOKEN)
|
||||
|
||||
MESSAGE_FILE = """<!--{token}
|
||||
Compose a new private message
|
||||
|
||||
Enter your message below this instruction block:
|
||||
- The first line should contain the recipient's reddit name
|
||||
- The second line should contain the message subject
|
||||
- Subsequent lines will be interpreted as the message body
|
||||
{token}-->
|
||||
""".format(token=TOKEN)
|
||||
|
||||
OAUTH_ACCESS_DENIED = """\
|
||||
<h1 style="color: red">Access Denied</h1><hr>
|
||||
<p><span style="font-weight: bold">Reddit Terminal Viewer</span> was
|
||||
|
||||
@@ -39,6 +39,10 @@ class SubscriptionError(RTVError):
|
||||
"Content could not be fetched"
|
||||
|
||||
|
||||
class InboxError(RTVError):
|
||||
"Content could not be fetched"
|
||||
|
||||
|
||||
class ProgramError(RTVError):
|
||||
"Problem executing an external program"
|
||||
|
||||
|
||||
204
rtv/inbox_page.py
Normal file
204
rtv/inbox_page.py
Normal file
@@ -0,0 +1,204 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from . import docs
|
||||
from .content import InboxContent
|
||||
from .page import Page, PageController, logged_in
|
||||
from .objects import Navigator, Command
|
||||
|
||||
|
||||
class InboxController(PageController):
|
||||
character_map = {}
|
||||
|
||||
|
||||
class InboxPage(Page):
|
||||
BANNER = docs.BANNER_INBOX
|
||||
FOOTER = docs.FOOTER_INBOX
|
||||
|
||||
name = 'inbox'
|
||||
|
||||
def __init__(self, reddit, term, config, oauth, content_type='all'):
|
||||
super(InboxPage, self).__init__(reddit, term, config, oauth)
|
||||
|
||||
self.controller = InboxController(self, keymap=config.keymap)
|
||||
self.content = InboxContent.from_user(reddit, term.loader, content_type)
|
||||
self.nav = Navigator(self.content.get)
|
||||
self.content_type = content_type
|
||||
|
||||
def handle_selected_page(self):
|
||||
"""
|
||||
Open the subscription and submission pages subwindows, but close the
|
||||
current page if any other type of page is selected.
|
||||
"""
|
||||
if not self.selected_page:
|
||||
pass
|
||||
if self.selected_page.name in ('subscription', 'submission'):
|
||||
# Launch page in a subwindow
|
||||
self.selected_page = self.selected_page.loop()
|
||||
elif self.selected_page.name in ('subreddit', 'inbox'):
|
||||
# Replace the current page
|
||||
self.active = False
|
||||
else:
|
||||
raise RuntimeError(self.selected_page.name)
|
||||
|
||||
@logged_in
|
||||
def refresh_content(self, order=None, name=None):
|
||||
"""
|
||||
Re-download all inbox content and reset the page index
|
||||
"""
|
||||
self.content_type = order or self.content_type
|
||||
|
||||
with self.term.loader():
|
||||
self.content = InboxContent.from_user(
|
||||
self.reddit, self.term.loader, self.content_type)
|
||||
if not self.term.loader.exception:
|
||||
self.nav = Navigator(self.content.get)
|
||||
|
||||
@InboxController.register(Command('SORT_1'))
|
||||
def load_content_inbox(self):
|
||||
self.refresh_content(order='all')
|
||||
|
||||
@InboxController.register(Command('SORT_2'))
|
||||
def load_content_unread_messages(self):
|
||||
self.refresh_content(order='unread')
|
||||
|
||||
@InboxController.register(Command('SORT_3'))
|
||||
def load_content_messages(self):
|
||||
self.refresh_content(order='messages')
|
||||
|
||||
@InboxController.register(Command('SORT_4'))
|
||||
def load_content_comment_replies(self):
|
||||
self.refresh_content(order='comments')
|
||||
|
||||
@InboxController.register(Command('SORT_5'))
|
||||
def load_content_post_replies(self):
|
||||
self.refresh_content(order='posts')
|
||||
|
||||
@InboxController.register(Command('SORT_6'))
|
||||
def load_content_username_mentions(self):
|
||||
self.refresh_content(order='mentions')
|
||||
|
||||
@InboxController.register(Command('SORT_7'))
|
||||
def load_content_sent_messages(self):
|
||||
self.refresh_content(order='sent')
|
||||
|
||||
@InboxController.register(Command('INBOX_MARK_READ'))
|
||||
@logged_in
|
||||
def mark_seen(self):
|
||||
"""
|
||||
Mark the selected message or comment as seen.
|
||||
"""
|
||||
data = self.get_selected_item()
|
||||
if data['is_new']:
|
||||
with self.term.loader('Marking as read'):
|
||||
data['object'].mark_as_read()
|
||||
if not self.term.loader.exception:
|
||||
data['is_new'] = False
|
||||
else:
|
||||
with self.term.loader('Marking as unread'):
|
||||
data['object'].mark_as_unread()
|
||||
if not self.term.loader.exception:
|
||||
data['is_new'] = True
|
||||
|
||||
@InboxController.register(Command('INBOX_REPLY'))
|
||||
@logged_in
|
||||
def inbox_reply(self):
|
||||
"""
|
||||
Reply to the selected private message or comment from the inbox.
|
||||
"""
|
||||
self.reply()
|
||||
|
||||
@InboxController.register(Command('INBOX_EXIT'))
|
||||
def close_inbox(self):
|
||||
"""
|
||||
Close inbox and return to the previous page.
|
||||
"""
|
||||
self.active = False
|
||||
|
||||
@InboxController.register(Command('INBOX_VIEW_CONTEXT'))
|
||||
@logged_in
|
||||
def view_context(self):
|
||||
"""
|
||||
View the context surrounding the selected comment.
|
||||
"""
|
||||
url = self.get_selected_item().get('context')
|
||||
if url:
|
||||
self.selected_page = self.open_submission_page(url)
|
||||
|
||||
@InboxController.register(Command('INBOX_OPEN_SUBMISSION'))
|
||||
@logged_in
|
||||
def open_submission(self):
|
||||
"""
|
||||
Open the full submission and comment tree for the selected comment.
|
||||
"""
|
||||
url = self.get_selected_item().get('submission_permalink')
|
||||
if url:
|
||||
self.selected_page = self.open_submission_page(url)
|
||||
|
||||
def _draw_item(self, win, data, inverted):
|
||||
|
||||
n_rows, n_cols = win.getmaxyx()
|
||||
n_cols -= 1 # Leave space for the cursor in the first column
|
||||
|
||||
# Handle the case where the window is not large enough to fit the data.
|
||||
valid_rows = range(0, n_rows)
|
||||
offset = 0 if not inverted else -(data['n_rows'] - n_rows)
|
||||
|
||||
row = offset
|
||||
if row in valid_rows:
|
||||
if data['is_new']:
|
||||
attr = self.term.attr('New')
|
||||
self.term.add_line(win, '[new]', row, 1, attr)
|
||||
self.term.add_space(win)
|
||||
|
||||
attr = self.term.attr('MessageSubject')
|
||||
self.term.add_line(win, '{subject}'.format(**data), attr=attr)
|
||||
self.term.add_space(win)
|
||||
else:
|
||||
attr = self.term.attr('MessageSubject')
|
||||
self.term.add_line(win, '{subject}'.format(**data), row, 1, attr)
|
||||
self.term.add_space(win)
|
||||
|
||||
if data['link_title']:
|
||||
attr = self.term.attr('MessageLink')
|
||||
self.term.add_line(win, '{link_title}'.format(**data), attr=attr)
|
||||
|
||||
row = offset + 1
|
||||
if row in valid_rows:
|
||||
# reddit.user might be ``None`` if the user logs out while viewing
|
||||
# this page
|
||||
if data['author'] == getattr(self.reddit.user, 'name', None):
|
||||
self.term.add_line(win, 'to ', row, 1)
|
||||
text = '{recipient}'.format(**data)
|
||||
else:
|
||||
self.term.add_line(win, 'from ', row, 1)
|
||||
text = '{author}'.format(**data)
|
||||
attr = self.term.attr('MessageAuthor')
|
||||
self.term.add_line(win, text, attr=attr)
|
||||
self.term.add_space(win)
|
||||
|
||||
if data['distinguished']:
|
||||
attr = self.term.attr('Distinguished')
|
||||
text = '[{distinguished}]'.format(**data)
|
||||
self.term.add_line(win, text, attr=attr)
|
||||
self.term.add_space(win)
|
||||
|
||||
attr = self.term.attr('Created')
|
||||
text = 'sent {created_long}'.format(**data)
|
||||
self.term.add_line(win, text, attr=attr)
|
||||
self.term.add_space(win)
|
||||
|
||||
if data['subreddit_name']:
|
||||
attr = self.term.attr('MessageSubreddit')
|
||||
text = 'via {subreddit_name}'.format(**data)
|
||||
self.term.add_line(win, text, attr=attr)
|
||||
self.term.add_space(win)
|
||||
|
||||
attr = self.term.attr('MessageText')
|
||||
for row, text in enumerate(data['split_body'], start=offset + 2):
|
||||
if row in valid_rows:
|
||||
self.term.add_line(win, text, row, 1, attr=attr)
|
||||
|
||||
attr = self.term.attr('CursorBlock')
|
||||
for y in range(n_rows):
|
||||
self.term.addch(win, y, 0, str(' '), attr)
|
||||
330
rtv/page.py
330
rtv/page.py
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
@@ -43,7 +44,6 @@ class Page(object):
|
||||
FOOTER = None
|
||||
|
||||
def __init__(self, reddit, term, config, oauth):
|
||||
|
||||
self.reddit = reddit
|
||||
self.term = term
|
||||
self.config = config
|
||||
@@ -54,6 +54,7 @@ class Page(object):
|
||||
self.copy_to_clipboard = copy
|
||||
|
||||
self.active = True
|
||||
self.selected_page = None
|
||||
self._row = 0
|
||||
self._subwindows = None
|
||||
|
||||
@@ -64,6 +65,9 @@ class Page(object):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_selected_item(self):
|
||||
"""
|
||||
Return the content dictionary that is currently selected by the cursor.
|
||||
"""
|
||||
return self.content.get(self.nav.absolute_index)
|
||||
|
||||
def loop(self):
|
||||
@@ -72,34 +76,91 @@ class Page(object):
|
||||
1. Re-draw the screen
|
||||
2. Wait for user to press a key (includes terminal resizing)
|
||||
3. Trigger the method registered to the input key
|
||||
4. Check if there are any nested pages that need to be looped over
|
||||
|
||||
The loop will run until self.active is set to False from within one of
|
||||
the methods.
|
||||
"""
|
||||
|
||||
self.active = True
|
||||
|
||||
# This needs to be called once before the main loop, in case a subpage
|
||||
# was pre-selected before the loop started. This happens in __main__.py
|
||||
# with ``page.open_submission(url=url)``
|
||||
while self.selected_page and self.active:
|
||||
self.handle_selected_page()
|
||||
|
||||
while self.active:
|
||||
self.draw()
|
||||
ch = self.term.stdscr.getch()
|
||||
self.controller.trigger(ch)
|
||||
|
||||
while self.selected_page and self.active:
|
||||
self.handle_selected_page()
|
||||
|
||||
return self.selected_page
|
||||
|
||||
def handle_selected_page(self):
|
||||
"""
|
||||
Some commands will result in an action that causes a new page to open.
|
||||
Examples include selecting a submission, viewing subscribed subreddits,
|
||||
or opening the user's inbox. With these commands, the newly selected
|
||||
page will be pre-loaded and stored in ``self.selected_page`` variable.
|
||||
It's up to each page type to determine what to do when another page is
|
||||
selected.
|
||||
|
||||
- It can start a nested page.loop(). This would allow the user to
|
||||
return to their previous screen after exiting the sub-page. For
|
||||
example, this is what happens when opening an individual submission
|
||||
from within a subreddit page. When the submission is closed, the
|
||||
user resumes the subreddit that they were previously viewing.
|
||||
|
||||
- It can close the current self.loop() and bubble the selected page up
|
||||
one level in the loop stack. For example, this is what happens when
|
||||
the user opens their subscriptions and selects a subreddit. The
|
||||
subscription page loop is closed and the selected subreddit is
|
||||
bubbled up to the root level loop.
|
||||
|
||||
Care should be taken to ensure the user can never enter an infinite
|
||||
nested loop, as this could lead to memory leaks and recursion errors.
|
||||
|
||||
# Example of an unsafe nested loop
|
||||
subreddit_page.loop()
|
||||
-> submission_page.loop()
|
||||
-> subreddit_page.loop()
|
||||
-> submission_page.loop()
|
||||
...
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@PageController.register(Command('REFRESH'))
|
||||
def reload_page(self):
|
||||
"""
|
||||
Clear the PRAW cache to force the page the re-fetch content from reddit.
|
||||
"""
|
||||
self.reddit.handler.clear_cache()
|
||||
self.refresh_content()
|
||||
|
||||
@PageController.register(Command('EXIT'))
|
||||
def exit(self):
|
||||
"""
|
||||
Prompt and exit the application.
|
||||
"""
|
||||
if self.term.prompt_y_or_n('Do you really want to quit? (y/n): '):
|
||||
sys.exit()
|
||||
|
||||
@PageController.register(Command('FORCE_EXIT'))
|
||||
def force_exit(self):
|
||||
"""
|
||||
Immediately exit the application.
|
||||
"""
|
||||
sys.exit()
|
||||
|
||||
@PageController.register(Command('PREVIOUS_THEME'))
|
||||
def previous_theme(self):
|
||||
|
||||
"""
|
||||
Cycle to preview the previous theme from the internal list of themes.
|
||||
"""
|
||||
theme = self.term.theme_list.previous(self.term.theme)
|
||||
while not self.term.check_theme(theme):
|
||||
theme = self.term.theme_list.previous(theme)
|
||||
@@ -111,7 +172,9 @@ class Page(object):
|
||||
|
||||
@PageController.register(Command('NEXT_THEME'))
|
||||
def next_theme(self):
|
||||
|
||||
"""
|
||||
Cycle to preview the next theme from the internal list of themes.
|
||||
"""
|
||||
theme = self.term.theme_list.next(self.term.theme)
|
||||
while not self.term.check_theme(theme):
|
||||
theme = self.term.theme_list.next(theme)
|
||||
@@ -123,36 +186,57 @@ class Page(object):
|
||||
|
||||
@PageController.register(Command('HELP'))
|
||||
def show_help(self):
|
||||
"""
|
||||
Open the help documentation in the system pager.
|
||||
"""
|
||||
self.term.open_pager(docs.HELP.strip())
|
||||
|
||||
@PageController.register(Command('MOVE_UP'))
|
||||
def move_cursor_up(self):
|
||||
"""
|
||||
Move the cursor up one selection.
|
||||
"""
|
||||
self._move_cursor(-1)
|
||||
self.clear_input_queue()
|
||||
|
||||
@PageController.register(Command('MOVE_DOWN'))
|
||||
def move_cursor_down(self):
|
||||
"""
|
||||
Move the cursor down one selection.
|
||||
"""
|
||||
self._move_cursor(1)
|
||||
self.clear_input_queue()
|
||||
|
||||
@PageController.register(Command('PAGE_UP'))
|
||||
def move_page_up(self):
|
||||
"""
|
||||
Move the cursor up approximately the number of entries on the page.
|
||||
"""
|
||||
self._move_page(-1)
|
||||
self.clear_input_queue()
|
||||
|
||||
@PageController.register(Command('PAGE_DOWN'))
|
||||
def move_page_down(self):
|
||||
"""
|
||||
Move the cursor down approximately the number of entries on the page.
|
||||
"""
|
||||
self._move_page(1)
|
||||
self.clear_input_queue()
|
||||
|
||||
@PageController.register(Command('PAGE_TOP'))
|
||||
def move_page_top(self):
|
||||
"""
|
||||
Move the cursor to the first item on the page.
|
||||
"""
|
||||
self.nav.page_index = self.content.range[0]
|
||||
self.nav.cursor_index = 0
|
||||
self.nav.inverted = False
|
||||
|
||||
@PageController.register(Command('PAGE_BOTTOM'))
|
||||
def move_page_bottom(self):
|
||||
"""
|
||||
Move the cursor to the last item on the page.
|
||||
"""
|
||||
self.nav.page_index = self.content.range[1]
|
||||
self.nav.cursor_index = 0
|
||||
self.nav.inverted = True
|
||||
@@ -160,6 +244,9 @@ class Page(object):
|
||||
@PageController.register(Command('UPVOTE'))
|
||||
@logged_in
|
||||
def upvote(self):
|
||||
"""
|
||||
Upvote the currently selected item.
|
||||
"""
|
||||
data = self.get_selected_item()
|
||||
if 'likes' not in data:
|
||||
self.term.flash()
|
||||
@@ -179,6 +266,9 @@ class Page(object):
|
||||
@PageController.register(Command('DOWNVOTE'))
|
||||
@logged_in
|
||||
def downvote(self):
|
||||
"""
|
||||
Downvote the currently selected item.
|
||||
"""
|
||||
data = self.get_selected_item()
|
||||
if 'likes' not in data:
|
||||
self.term.flash()
|
||||
@@ -198,6 +288,9 @@ class Page(object):
|
||||
@PageController.register(Command('SAVE'))
|
||||
@logged_in
|
||||
def save(self):
|
||||
"""
|
||||
Mark the currently selected item as saved through the reddit API.
|
||||
"""
|
||||
data = self.get_selected_item()
|
||||
if 'saved' not in data:
|
||||
self.term.flash()
|
||||
@@ -218,7 +311,6 @@ class Page(object):
|
||||
Prompt to log into the user's account, or log out of the current
|
||||
account.
|
||||
"""
|
||||
|
||||
if self.reddit.is_oauth_session():
|
||||
ch = self.term.show_notification('Log out? (y/n)')
|
||||
if ch in (ord('y'), ord('Y')):
|
||||
@@ -227,13 +319,64 @@ class Page(object):
|
||||
else:
|
||||
self.oauth.authorize()
|
||||
|
||||
def reply(self):
|
||||
"""
|
||||
Reply to the selected item. This is a utility method and should not
|
||||
be bound to a key directly.
|
||||
|
||||
Item type:
|
||||
Submission - add a top level comment
|
||||
Comment - add a comment reply
|
||||
Message - reply to a private message
|
||||
"""
|
||||
data = self.get_selected_item()
|
||||
|
||||
if data['type'] == 'Submission':
|
||||
body = data['text']
|
||||
description = 'submission'
|
||||
reply = data['object'].add_comment
|
||||
elif data['type'] in ('Comment', 'InboxComment'):
|
||||
body = data['body']
|
||||
description = 'comment'
|
||||
reply = data['object'].reply
|
||||
elif data['type'] == 'Message':
|
||||
body = data['body']
|
||||
description = 'private message'
|
||||
reply = data['object'].reply
|
||||
else:
|
||||
self.term.flash()
|
||||
return
|
||||
|
||||
# Construct the text that will be displayed in the editor file.
|
||||
# The post body will be commented out and added for reference
|
||||
lines = [' |' + line for line in body.split('\n')]
|
||||
content = '\n'.join(lines)
|
||||
comment_info = docs.REPLY_FILE.format(
|
||||
author=data['author'],
|
||||
type=description,
|
||||
content=content)
|
||||
|
||||
with self.term.open_editor(comment_info) as comment:
|
||||
if not comment:
|
||||
self.term.show_notification('Canceled')
|
||||
return
|
||||
|
||||
with self.term.loader('Posting {}'.format(description), delay=0):
|
||||
reply(comment)
|
||||
# Give reddit time to process the submission
|
||||
time.sleep(2.0)
|
||||
|
||||
if self.term.loader.exception is None:
|
||||
self.reload_page()
|
||||
else:
|
||||
raise TemporaryFileError()
|
||||
|
||||
@PageController.register(Command('DELETE'))
|
||||
@logged_in
|
||||
def delete_item(self):
|
||||
"""
|
||||
Delete a submission or comment.
|
||||
"""
|
||||
|
||||
data = self.get_selected_item()
|
||||
if data.get('author') != self.reddit.user.name:
|
||||
self.term.flash()
|
||||
@@ -248,6 +391,7 @@ class Page(object):
|
||||
data['object'].delete()
|
||||
# Give reddit time to process the request
|
||||
time.sleep(2.0)
|
||||
|
||||
if self.term.loader.exception is None:
|
||||
self.reload_page()
|
||||
|
||||
@@ -257,7 +401,6 @@ class Page(object):
|
||||
"""
|
||||
Edit a submission or comment.
|
||||
"""
|
||||
|
||||
data = self.get_selected_item()
|
||||
if data.get('author') != self.reddit.user.name:
|
||||
self.term.flash()
|
||||
@@ -289,20 +432,52 @@ class Page(object):
|
||||
else:
|
||||
raise TemporaryFileError()
|
||||
|
||||
@PageController.register(Command('INBOX'))
|
||||
@PageController.register(Command('PRIVATE_MESSAGE'))
|
||||
@logged_in
|
||||
def get_inbox(self):
|
||||
def send_private_message(self):
|
||||
"""
|
||||
Checks the inbox for unread messages and displays a notification.
|
||||
Send a new private message to another user.
|
||||
"""
|
||||
message_info = docs.MESSAGE_FILE
|
||||
with self.term.open_editor(message_info) as text:
|
||||
if not text:
|
||||
self.term.show_notification('Canceled')
|
||||
return
|
||||
|
||||
with self.term.loader('Loading'):
|
||||
messages = self.reddit.get_unread(limit=1)
|
||||
inbox = len(list(messages))
|
||||
parts = text.split('\n', 2)
|
||||
if len(parts) == 1:
|
||||
self.term.show_notification('Missing message subject')
|
||||
return
|
||||
elif len(parts) == 2:
|
||||
self.term.show_notification('Missing message body')
|
||||
return
|
||||
|
||||
if self.term.loader.exception is None:
|
||||
message = 'New Messages' if inbox > 0 else 'No New Messages'
|
||||
self.term.show_notification(message)
|
||||
recipient, subject, message = parts
|
||||
recipient = recipient.strip()
|
||||
subject = subject.strip()
|
||||
message = message.rstrip()
|
||||
|
||||
if not recipient:
|
||||
self.term.show_notification('Missing recipient')
|
||||
return
|
||||
elif not subject:
|
||||
self.term.show_notification('Missing message subject')
|
||||
return
|
||||
elif not message:
|
||||
self.term.show_notification('Missing message body')
|
||||
return
|
||||
|
||||
with self.term.loader('Sending message', delay=0):
|
||||
self.reddit.send_message(
|
||||
recipient, subject, message, raise_captcha_exception=True)
|
||||
# Give reddit time to process the message
|
||||
time.sleep(2.0)
|
||||
|
||||
if self.term.loader.exception:
|
||||
raise TemporaryFileError()
|
||||
else:
|
||||
self.term.show_notification('Message sent!')
|
||||
self.selected_page = self.open_inbox_page('sent')
|
||||
|
||||
def prompt_and_select_link(self):
|
||||
"""
|
||||
@@ -340,31 +515,23 @@ class Page(object):
|
||||
@PageController.register(Command('COPY_PERMALINK'))
|
||||
def copy_permalink(self):
|
||||
"""
|
||||
Copies submission permalink to OS clipboard
|
||||
Copy the submission permalink to OS clipboard
|
||||
"""
|
||||
|
||||
data = self.get_selected_item()
|
||||
url = data.get('permalink')
|
||||
if url is None:
|
||||
self.term.flash()
|
||||
return
|
||||
|
||||
try:
|
||||
self.copy_to_clipboard(url)
|
||||
except (ProgramError, OSError) as e:
|
||||
_logger.exception(e)
|
||||
self.term.show_notification(
|
||||
'Failed to copy permalink: {0}'.format(e))
|
||||
else:
|
||||
self.term.show_notification(
|
||||
'Copied permalink to clipboard', timeout=1)
|
||||
url = self.get_selected_item().get('permalink')
|
||||
self.copy_to_clipboard(url)
|
||||
|
||||
@PageController.register(Command('COPY_URL'))
|
||||
def copy_url(self):
|
||||
"""
|
||||
Copies link to OS clipboard
|
||||
Copy a link to OS clipboard
|
||||
"""
|
||||
url = self.prompt_and_select_link()
|
||||
self.copy_to_clipboard(url)
|
||||
|
||||
def copy_to_clipboard(self, url):
|
||||
"""
|
||||
Attempt to copy the selected URL to the user's clipboard
|
||||
"""
|
||||
if url is None:
|
||||
self.term.flash()
|
||||
return
|
||||
@@ -379,11 +546,104 @@ class Page(object):
|
||||
self.term.show_notification(
|
||||
['Copied to clipboard:', url], timeout=1)
|
||||
|
||||
@PageController.register(Command('SUBSCRIPTIONS'))
|
||||
@logged_in
|
||||
def subscriptions(self):
|
||||
"""
|
||||
View a list of the user's subscribed subreddits
|
||||
"""
|
||||
self.selected_page = self.open_subscription_page('subreddit')
|
||||
|
||||
@PageController.register(Command('MULTIREDDITS'))
|
||||
@logged_in
|
||||
def multireddits(self):
|
||||
"""
|
||||
View a list of the user's subscribed multireddits
|
||||
"""
|
||||
self.selected_page = self.open_subscription_page('multireddit')
|
||||
|
||||
@PageController.register(Command('PROMPT'))
|
||||
def prompt(self):
|
||||
"""
|
||||
Open a prompt to navigate to a different subreddit or comment"
|
||||
"""
|
||||
name = self.term.prompt_input('Enter page: /')
|
||||
if name:
|
||||
# Check if opening a submission url or a subreddit url
|
||||
# Example patterns for submissions:
|
||||
# comments/571dw3
|
||||
# /comments/571dw3
|
||||
# /r/pics/comments/571dw3/
|
||||
# https://www.reddit.com/r/pics/comments/571dw3/at_disneyland
|
||||
submission_pattern = re.compile(r'(^|/)comments/(?P<id>.+?)($|/)')
|
||||
|
||||
match = submission_pattern.search(name)
|
||||
if match:
|
||||
url = 'https://www.reddit.com/comments/{0}'.format(match.group('id'))
|
||||
self.selected_page = self.open_submission_page(url)
|
||||
else:
|
||||
self.selected_page = self.open_subreddit_page(name)
|
||||
|
||||
@PageController.register(Command('INBOX'))
|
||||
@logged_in
|
||||
def inbox(self):
|
||||
"""
|
||||
View the user's inbox.
|
||||
"""
|
||||
self.selected_page = self.open_inbox_page('all')
|
||||
|
||||
def open_inbox_page(self, content_type):
|
||||
"""
|
||||
Open an instance of the inbox page for the logged in user.
|
||||
"""
|
||||
from .inbox_page import InboxPage
|
||||
|
||||
with self.term.loader('Loading inbox'):
|
||||
page = InboxPage(self.reddit, self.term, self.config, self.oauth,
|
||||
content_type=content_type)
|
||||
if not self.term.loader.exception:
|
||||
return page
|
||||
|
||||
def open_subscription_page(self, content_type):
|
||||
"""
|
||||
Open an instance of the subscriptions page with the selected content.
|
||||
"""
|
||||
from .subscription_page import SubscriptionPage
|
||||
|
||||
with self.term.loader('Loading {0}s'.format(content_type)):
|
||||
page = SubscriptionPage(self.reddit, self.term, self.config,
|
||||
self.oauth, content_type=content_type)
|
||||
if not self.term.loader.exception:
|
||||
return page
|
||||
|
||||
def open_submission_page(self, url=None, submission=None):
|
||||
"""
|
||||
Open an instance of the submission page for the given submission URL.
|
||||
"""
|
||||
from .submission_page import SubmissionPage
|
||||
|
||||
with self.term.loader('Loading submission'):
|
||||
page = SubmissionPage(self.reddit, self.term, self.config,
|
||||
self.oauth, url=url, submission=submission)
|
||||
if not self.term.loader.exception:
|
||||
return page
|
||||
|
||||
def open_subreddit_page(self, name):
|
||||
"""
|
||||
Open an instance of the subreddit page for the given subreddit name.
|
||||
"""
|
||||
from .subreddit_page import SubredditPage
|
||||
|
||||
with self.term.loader('Loading subreddit'):
|
||||
page = SubredditPage(self.reddit, self.term, self.config,
|
||||
self.oauth, name)
|
||||
if not self.term.loader.exception:
|
||||
return page
|
||||
|
||||
def clear_input_queue(self):
|
||||
"""
|
||||
Clear excessive input caused by the scroll wheel or holding down a key
|
||||
"""
|
||||
|
||||
with self.term.no_delay():
|
||||
while self.term.getch() != -1:
|
||||
continue
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import time
|
||||
|
||||
from . import docs
|
||||
from .content import SubmissionContent, SubredditContent
|
||||
from .content import SubmissionContent
|
||||
from .page import Page, PageController, logged_in
|
||||
from .objects import Navigator, Command
|
||||
from .exceptions import TemporaryFileError
|
||||
|
||||
|
||||
class SubmissionController(PageController):
|
||||
@@ -16,10 +12,11 @@ class SubmissionController(PageController):
|
||||
|
||||
|
||||
class SubmissionPage(Page):
|
||||
|
||||
BANNER = docs.BANNER_SUBMISSION
|
||||
FOOTER = docs.FOOTER_SUBMISSION
|
||||
|
||||
name = 'submission'
|
||||
|
||||
def __init__(self, reddit, term, config, oauth, url=None, submission=None):
|
||||
super(SubmissionPage, self).__init__(reddit, term, config, oauth)
|
||||
|
||||
@@ -33,9 +30,44 @@ class SubmissionPage(Page):
|
||||
self.content = SubmissionContent(
|
||||
submission, term.loader,
|
||||
max_comment_cols=config['max_comment_cols'])
|
||||
|
||||
# Start at the submission post, which is indexed as -1
|
||||
self.nav = Navigator(self.content.get, page_index=-1)
|
||||
self.selected_subreddit = None
|
||||
|
||||
def handle_selected_page(self):
|
||||
"""
|
||||
Open the subscription page in a subwindow, but close the current page
|
||||
if any other type of page is selected.
|
||||
"""
|
||||
if not self.selected_page:
|
||||
pass
|
||||
elif self.selected_page.name == 'subscription':
|
||||
# Launch page in a subwindow
|
||||
self.selected_page = self.selected_page.loop()
|
||||
elif self.selected_page.name in ('subreddit', 'submission', 'inbox'):
|
||||
# Replace the current page
|
||||
self.active = False
|
||||
else:
|
||||
raise RuntimeError(self.selected_page.name)
|
||||
|
||||
def refresh_content(self, order=None, name=None):
|
||||
"""
|
||||
Re-download comments and reset the page index
|
||||
"""
|
||||
order = order or self.content.order
|
||||
url = name or self.content.name
|
||||
|
||||
# Hack to allow an order specified in the name by prompt_subreddit() to
|
||||
# override the current default
|
||||
if order == 'ignore':
|
||||
order = None
|
||||
|
||||
with self.term.loader('Refreshing page'):
|
||||
self.content = SubmissionContent.from_url(
|
||||
self.reddit, url, self.term.loader, order=order,
|
||||
max_comment_cols=self.config['max_comment_cols'])
|
||||
if not self.term.loader.exception:
|
||||
self.nav = Navigator(self.content.get, page_index=-1)
|
||||
|
||||
@SubmissionController.register(Command('SORT_1'))
|
||||
def sort_content_hot(self):
|
||||
@@ -62,7 +94,6 @@ class SubmissionPage(Page):
|
||||
"""
|
||||
Toggle the selected comment tree between visible and hidden
|
||||
"""
|
||||
|
||||
current_index = self.nav.absolute_index
|
||||
self.content.toggle(current_index)
|
||||
|
||||
@@ -84,57 +115,8 @@ class SubmissionPage(Page):
|
||||
"""
|
||||
Close the submission and return to the subreddit page
|
||||
"""
|
||||
|
||||
self.active = False
|
||||
|
||||
def refresh_content(self, order=None, name=None):
|
||||
"""
|
||||
Re-download comments and reset the page index
|
||||
"""
|
||||
|
||||
order = order or self.content.order
|
||||
url = name or self.content.name
|
||||
|
||||
# Hack to allow an order specified in the name by prompt_subreddit() to
|
||||
# override the current default
|
||||
if order == 'ignore':
|
||||
order = None
|
||||
|
||||
with self.term.loader('Refreshing page'):
|
||||
self.content = SubmissionContent.from_url(
|
||||
self.reddit, url, self.term.loader, order=order,
|
||||
max_comment_cols=self.config['max_comment_cols'])
|
||||
if not self.term.loader.exception:
|
||||
self.nav = Navigator(self.content.get, page_index=-1)
|
||||
|
||||
@SubmissionController.register(Command('PROMPT'))
|
||||
def prompt_subreddit(self):
|
||||
"""
|
||||
Open a prompt to navigate to a different subreddit
|
||||
"""
|
||||
|
||||
name = self.term.prompt_input('Enter page: /')
|
||||
if name is not None:
|
||||
# Check if opening a submission url or a subreddit url
|
||||
# Example patterns for submissions:
|
||||
# comments/571dw3
|
||||
# /comments/571dw3
|
||||
# /r/pics/comments/571dw3/
|
||||
# https://www.reddit.com/r/pics/comments/571dw3/at_disneyland
|
||||
submission_pattern = re.compile(r'(^|/)comments/(?P<id>.+?)($|/)')
|
||||
match = submission_pattern.search(name)
|
||||
if match:
|
||||
url = 'https://www.reddit.com/comments/{0}'
|
||||
self.refresh_content('ignore', url.format(match.group('id')))
|
||||
|
||||
else:
|
||||
with self.term.loader('Loading page'):
|
||||
content = SubredditContent.from_name(
|
||||
self.reddit, name, self.term.loader)
|
||||
if not self.term.loader.exception:
|
||||
self.selected_subreddit = content
|
||||
self.active = False
|
||||
|
||||
@SubmissionController.register(Command('SUBMISSION_OPEN_IN_BROWSER'))
|
||||
def open_link(self):
|
||||
"""
|
||||
@@ -161,7 +143,6 @@ class SubmissionPage(Page):
|
||||
"""
|
||||
Open the selected item with the system's pager
|
||||
"""
|
||||
|
||||
n_rows, n_cols = self.term.stdscr.getmaxyx()
|
||||
|
||||
if self.config['max_pager_cols'] is not None:
|
||||
@@ -182,46 +163,8 @@ class SubmissionPage(Page):
|
||||
def add_comment(self):
|
||||
"""
|
||||
Submit a reply to the selected item.
|
||||
|
||||
Selected item:
|
||||
Submission - add a top level comment
|
||||
Comment - add a comment reply
|
||||
"""
|
||||
|
||||
data = self.get_selected_item()
|
||||
if data['type'] == 'Submission':
|
||||
body = data['text']
|
||||
reply = data['object'].add_comment
|
||||
elif data['type'] == 'Comment':
|
||||
body = data['body']
|
||||
reply = data['object'].reply
|
||||
else:
|
||||
self.term.flash()
|
||||
return
|
||||
|
||||
# Construct the text that will be displayed in the editor file.
|
||||
# The post body will be commented out and added for reference
|
||||
lines = [' |' + line for line in body.split('\n')]
|
||||
content = '\n'.join(lines)
|
||||
comment_info = docs.COMMENT_FILE.format(
|
||||
author=data['author'],
|
||||
type=data['type'].lower(),
|
||||
content=content)
|
||||
|
||||
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 self.term.loader.exception is None:
|
||||
self.reload_page()
|
||||
else:
|
||||
raise TemporaryFileError()
|
||||
self.reply()
|
||||
|
||||
@SubmissionController.register(Command('DELETE'))
|
||||
@logged_in
|
||||
@@ -229,7 +172,6 @@ class SubmissionPage(Page):
|
||||
"""
|
||||
Delete the selected comment
|
||||
"""
|
||||
|
||||
if self.get_selected_item()['type'] == 'Comment':
|
||||
self.delete_item()
|
||||
else:
|
||||
@@ -240,7 +182,6 @@ class SubmissionPage(Page):
|
||||
"""
|
||||
Open the selected comment with the URL viewer
|
||||
"""
|
||||
|
||||
data = self.get_selected_item()
|
||||
comment = data.get('body') or data.get('text') or data.get('url_full')
|
||||
if comment:
|
||||
@@ -254,7 +195,6 @@ class SubmissionPage(Page):
|
||||
Move the cursor up to the comment's parent. If the comment is
|
||||
top-level, jump to the previous top-level comment.
|
||||
"""
|
||||
|
||||
cursor = self.nav.absolute_index
|
||||
if cursor > 0:
|
||||
level = max(self.content.get(cursor)['level'], 1)
|
||||
@@ -273,7 +213,6 @@ class SubmissionPage(Page):
|
||||
Jump to the next comment that's at the same level as the selected
|
||||
comment and shares the same parent.
|
||||
"""
|
||||
|
||||
cursor = self.nav.absolute_index
|
||||
if cursor >= 0:
|
||||
level = self.content.get(cursor)['level']
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import time
|
||||
|
||||
from . import docs
|
||||
from .content import SubredditContent
|
||||
from .page import Page, PageController, logged_in
|
||||
from .objects import Navigator, Command
|
||||
from .submission_page import SubmissionPage
|
||||
from .subscription_page import SubscriptionPage
|
||||
from .exceptions import TemporaryFileError
|
||||
|
||||
|
||||
@@ -18,10 +15,11 @@ class SubredditController(PageController):
|
||||
|
||||
|
||||
class SubredditPage(Page):
|
||||
|
||||
BANNER = docs.BANNER_SUBREDDIT
|
||||
FOOTER = docs.FOOTER_SUBREDDIT
|
||||
|
||||
name = 'subreddit'
|
||||
|
||||
def __init__(self, reddit, term, config, oauth, name):
|
||||
"""
|
||||
Params:
|
||||
@@ -34,11 +32,25 @@ class SubredditPage(Page):
|
||||
self.nav = Navigator(self.content.get)
|
||||
self.toggled_subreddit = None
|
||||
|
||||
def handle_selected_page(self):
|
||||
"""
|
||||
Open all selected pages in subwindows except other subreddit pages.
|
||||
"""
|
||||
if not self.selected_page:
|
||||
pass
|
||||
elif self.selected_page.name in ('subscription', 'submission', 'inbox'):
|
||||
# Launch page in a subwindow
|
||||
self.selected_page = self.selected_page.loop()
|
||||
elif self.selected_page.name == 'subreddit':
|
||||
# Replace the current page
|
||||
self.active = False
|
||||
else:
|
||||
raise RuntimeError(self.selected_page.name)
|
||||
|
||||
def refresh_content(self, order=None, name=None):
|
||||
"""
|
||||
Re-download all submissions and reset the page index
|
||||
"""
|
||||
|
||||
order = order or self.content.order
|
||||
|
||||
# Preserve the query if staying on the current page
|
||||
@@ -113,7 +125,6 @@ class SubredditPage(Page):
|
||||
"""
|
||||
Open a prompt to search the given subreddit
|
||||
"""
|
||||
|
||||
name = name or self.content.name
|
||||
|
||||
query = self.term.prompt_input('Search {0}: '.format(name))
|
||||
@@ -126,29 +137,6 @@ class SubredditPage(Page):
|
||||
if not self.term.loader.exception:
|
||||
self.nav = Navigator(self.content.get)
|
||||
|
||||
@SubredditController.register(Command('PROMPT'))
|
||||
def prompt_subreddit(self):
|
||||
"""
|
||||
Open a prompt to navigate to a different subreddit"
|
||||
"""
|
||||
|
||||
name = self.term.prompt_input('Enter page: /')
|
||||
if name is not None:
|
||||
# Check if opening a submission url or a subreddit url
|
||||
# Example patterns for submissions:
|
||||
# comments/571dw3
|
||||
# /comments/571dw3
|
||||
# /r/pics/comments/571dw3/
|
||||
# https://www.reddit.com/r/pics/comments/571dw3/at_disneyland
|
||||
submission_pattern = re.compile(r'(^|/)comments/(?P<id>.+?)($|/)')
|
||||
|
||||
match = submission_pattern.search(name)
|
||||
if match:
|
||||
submission_url = 'https://www.reddit.com/comments/{0}'
|
||||
self.open_submission(submission_url.format(match.group('id')))
|
||||
else:
|
||||
self.refresh_content(order='ignore', name=name)
|
||||
|
||||
@SubredditController.register(Command('SUBREDDIT_FRONTPAGE'))
|
||||
def show_frontpage(self):
|
||||
"""
|
||||
@@ -171,26 +159,13 @@ class SubredditPage(Page):
|
||||
"""
|
||||
Select the current submission to view posts.
|
||||
"""
|
||||
|
||||
data = {}
|
||||
if url is None:
|
||||
data = self.get_selected_item()
|
||||
url = data['permalink']
|
||||
if data.get('url_type') == 'selfpost':
|
||||
self.config.history.add(data['url_full'])
|
||||
|
||||
with self.term.loader('Loading submission'):
|
||||
page = SubmissionPage(
|
||||
self.reddit, self.term, self.config, self.oauth, url=url)
|
||||
if self.term.loader.exception:
|
||||
return
|
||||
|
||||
page.loop()
|
||||
|
||||
if data.get('url_type') == 'selfpost':
|
||||
self.config.history.add(data['url_full'])
|
||||
|
||||
if page.selected_subreddit is not None:
|
||||
self.content = page.selected_subreddit
|
||||
self.nav = Navigator(self.content.get)
|
||||
self.selected_page = self.open_submission_page(url)
|
||||
|
||||
@SubredditController.register(Command('SUBREDDIT_OPEN_IN_BROWSER'))
|
||||
def open_link(self):
|
||||
@@ -214,9 +189,8 @@ class SubredditPage(Page):
|
||||
@logged_in
|
||||
def post_submission(self):
|
||||
"""
|
||||
Post a new submission to the given subreddit
|
||||
Post a new submission to the given subreddit.
|
||||
"""
|
||||
|
||||
# Check that the subreddit can be submitted to
|
||||
name = self.content.name
|
||||
if '+' in name or name in ('/r/all', '/r/front', '/r/me', '/u/saved'):
|
||||
@@ -242,63 +216,8 @@ class SubredditPage(Page):
|
||||
raise TemporaryFileError()
|
||||
|
||||
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()
|
||||
|
||||
if page.selected_subreddit is not None:
|
||||
self.content = page.selected_subreddit
|
||||
self.nav = Navigator(self.content.get)
|
||||
else:
|
||||
self.reload_page()
|
||||
|
||||
@SubredditController.register(Command('SUBREDDIT_OPEN_SUBSCRIPTIONS'))
|
||||
@logged_in
|
||||
def open_subscriptions(self):
|
||||
"""
|
||||
Open user subscriptions page
|
||||
"""
|
||||
|
||||
with self.term.loader('Loading subscriptions'):
|
||||
page = SubscriptionPage(self.reddit, self.term, self.config,
|
||||
self.oauth, content_type='subreddit')
|
||||
if self.term.loader.exception:
|
||||
return
|
||||
|
||||
page.loop()
|
||||
|
||||
# When the user has chosen a subreddit in the subscriptions list,
|
||||
# refresh content with the selected subreddit
|
||||
if page.selected_subreddit is not None:
|
||||
self.content = page.selected_subreddit
|
||||
self.nav = Navigator(self.content.get)
|
||||
|
||||
@SubredditController.register(Command('SUBREDDIT_OPEN_MULTIREDDITS'))
|
||||
@logged_in
|
||||
def open_multireddit_subscriptions(self):
|
||||
"""
|
||||
Open user multireddit subscriptions page
|
||||
"""
|
||||
|
||||
with self.term.loader('Loading multireddits'):
|
||||
page = SubscriptionPage(self.reddit, self.term, self.config,
|
||||
self.oauth, content_type='multireddit')
|
||||
if self.term.loader.exception:
|
||||
return
|
||||
|
||||
page.loop()
|
||||
|
||||
# When the user has chosen a subreddit in the subscriptions list,
|
||||
# refresh content with the selected subreddit
|
||||
if page.selected_subreddit is not None:
|
||||
self.content = page.selected_subreddit
|
||||
self.nav = Navigator(self.content.get)
|
||||
# Open the newly created submission
|
||||
self.selected_page = self.open_submission_page(submission=submission)
|
||||
|
||||
@SubredditController.register(Command('SUBREDDIT_HIDE'))
|
||||
@logged_in
|
||||
@@ -314,7 +233,7 @@ class SubredditPage(Page):
|
||||
with self.term.loader('Hiding'):
|
||||
data['object'].hide()
|
||||
data['hidden'] = True
|
||||
|
||||
|
||||
def _draw_item(self, win, data, inverted):
|
||||
|
||||
n_rows, n_cols = win.getmaxyx()
|
||||
@@ -353,8 +272,7 @@ class SubredditPage(Page):
|
||||
self.term.add_space(win)
|
||||
|
||||
attr = self.term.attr('Created')
|
||||
self.term.add_line(win, '{created}{edited}'.format(**data),
|
||||
attr=attr)
|
||||
self.term.add_line(win, '{created}{edited}'.format(**data), attr=attr)
|
||||
|
||||
if data['comments'] is not None:
|
||||
attr = self.term.attr('Separator')
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from . import docs
|
||||
from .page import Page, PageController
|
||||
from .content import SubscriptionContent, SubredditContent
|
||||
from .content import SubscriptionContent
|
||||
from .page import Page, PageController, logged_in
|
||||
from .objects import Navigator, Command
|
||||
|
||||
|
||||
@@ -12,10 +12,11 @@ class SubscriptionController(PageController):
|
||||
|
||||
|
||||
class SubscriptionPage(Page):
|
||||
|
||||
BANNER = None
|
||||
FOOTER = docs.FOOTER_SUBSCRIPTION
|
||||
|
||||
name = 'subscription'
|
||||
|
||||
def __init__(self, reddit, term, config, oauth, content_type='subreddit'):
|
||||
super(SubscriptionPage, self).__init__(reddit, term, config, oauth)
|
||||
|
||||
@@ -24,13 +25,18 @@ class SubscriptionPage(Page):
|
||||
reddit, term.loader, content_type)
|
||||
self.nav = Navigator(self.content.get)
|
||||
self.content_type = content_type
|
||||
self.selected_subreddit = None
|
||||
|
||||
def handle_selected_page(self):
|
||||
"""
|
||||
Always close the current page when another page is selected.
|
||||
"""
|
||||
if self.selected_page:
|
||||
self.active = False
|
||||
|
||||
def refresh_content(self, order=None, name=None):
|
||||
"""
|
||||
Re-download all subscriptions and reset the page index
|
||||
"""
|
||||
|
||||
# reddit.get_my_subreddits() does not support sorting by order
|
||||
if order:
|
||||
self.term.flash()
|
||||
@@ -42,41 +48,19 @@ class SubscriptionPage(Page):
|
||||
if not self.term.loader.exception:
|
||||
self.nav = Navigator(self.content.get)
|
||||
|
||||
@SubscriptionController.register(Command('PROMPT'))
|
||||
def prompt_subreddit(self):
|
||||
"""
|
||||
Open a prompt to navigate to a different subreddit
|
||||
"""
|
||||
|
||||
name = self.term.prompt_input('Enter page: /')
|
||||
if name is not None:
|
||||
with self.term.loader('Loading page'):
|
||||
content = SubredditContent.from_name(
|
||||
self.reddit, name, self.term.loader)
|
||||
if not self.term.loader.exception:
|
||||
self.selected_subreddit = content
|
||||
self.active = False
|
||||
|
||||
@SubscriptionController.register(Command('SUBSCRIPTION_SELECT'))
|
||||
def select_subreddit(self):
|
||||
"""
|
||||
Store the selected subreddit and return to the subreddit page
|
||||
"""
|
||||
|
||||
name = self.get_selected_item()['name']
|
||||
with self.term.loader('Loading page'):
|
||||
content = SubredditContent.from_name(
|
||||
self.reddit, name, self.term.loader)
|
||||
if not self.term.loader.exception:
|
||||
self.selected_subreddit = content
|
||||
self.active = False
|
||||
self.selected_page = self.open_subreddit_page(name)
|
||||
|
||||
@SubscriptionController.register(Command('SUBSCRIPTION_EXIT'))
|
||||
def close_subscriptions(self):
|
||||
"""
|
||||
Close subscriptions and return to the subreddit page
|
||||
"""
|
||||
|
||||
self.active = False
|
||||
|
||||
def _draw_banner(self):
|
||||
|
||||
@@ -143,6 +143,9 @@ PROMPT = /
|
||||
SAVE = w
|
||||
COPY_PERMALINK = y
|
||||
COPY_URL = Y
|
||||
PRIVATE_MESSAGE = C
|
||||
SUBSCRIPTIONS = s
|
||||
MULTIREDDITS = S
|
||||
|
||||
; Submission page
|
||||
SUBMISSION_TOGGLE_COMMENT = 0x20
|
||||
@@ -159,11 +162,16 @@ SUBREDDIT_SEARCH = f
|
||||
SUBREDDIT_POST = c
|
||||
SUBREDDIT_OPEN = l, <KEY_RIGHT>
|
||||
SUBREDDIT_OPEN_IN_BROWSER = o, <LF>, <KEY_ENTER>
|
||||
SUBREDDIT_OPEN_SUBSCRIPTIONS = s
|
||||
SUBREDDIT_OPEN_MULTIREDDITS = S
|
||||
SUBREDDIT_FRONTPAGE = p
|
||||
SUBREDDIT_HIDE = 0x20
|
||||
|
||||
; Subscription page
|
||||
SUBSCRIPTION_SELECT = l, <LF>, <KEY_ENTER>, <KEY_RIGHT>
|
||||
SUBSCRIPTION_EXIT = h, s, S, <ESC>, <KEY_LEFT>
|
||||
|
||||
; Inbox page
|
||||
INBOX_VIEW_CONTEXT = l, <KEY_RIGHT>
|
||||
INBOX_OPEN_SUBMISSION = o, <LF>, <KEY_ENTER>
|
||||
INBOX_REPLY = c
|
||||
INBOX_MARK_READ = w
|
||||
INBOX_EXIT = h, <ESC>, <KEY_LEFT>
|
||||
|
||||
@@ -602,8 +602,8 @@ class Terminal(object):
|
||||
webbrowser.open_new_tab(url)
|
||||
finally:
|
||||
try:
|
||||
null.close()
|
||||
except AttributeError:
|
||||
os.close(null)
|
||||
except OSError:
|
||||
pass
|
||||
os.dup2(stdout, 1)
|
||||
os.dup2(stderr, 2)
|
||||
|
||||
Reference in New Issue
Block a user