Merge branch 'yskmt-login'

This commit is contained in:
Michael Lazar
2015-04-02 09:59:38 -07:00
12 changed files with 300 additions and 195 deletions

View File

@@ -59,7 +59,7 @@ RTV currently supports browsing both subreddits and individual submissions. In e
:``▲``/``▼`` or ``j``/``k``: Scroll to the prev/next item
:``a``/``z``: Upvote/downvote the selected item
:``o``: Open the selected item in the default web browser
:``ENTER`` or ``o``: Open the selected item in the default web browser
:``r``: Refresh the current page
:``?``: Show the help message
:``q``: Quit
@@ -70,6 +70,7 @@ In subreddit mode you can browse through the top submissions on either the front
:``►`` or ``l``: View comments for the selected submission
:``/``: Open a prompt to switch subreddits
:``f``: Open a prompt to search the current subreddit
The ``/`` prompt accepts subreddits in the following formats

View File

@@ -3,4 +3,4 @@ from .__version__ import __version__
__title__ = 'Reddit Terminal Viewer'
__author__ = 'Michael Lazar'
__license__ = 'The MIT License (MIT)'
__copyright__ = '(c) 2015 Michael Lazar'
__copyright__ = '(c) 2015 Michael Lazar'

View File

@@ -18,6 +18,7 @@ from .docs import *
__all__ = []
def load_config():
"""
Search for a configuration file at the location ~/.rtv and attempt to load
@@ -34,11 +35,12 @@ def load_config():
return defaults
def command_line():
parser = argparse.ArgumentParser(
prog='rtv', description=SUMMARY,
epilog=CONTROLS+HELP,
epilog=CONTROLS + HELP,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('-s', dest='subreddit', help='subreddit name')
@@ -56,15 +58,16 @@ def command_line():
return args
def main():
"Main entry point"
# logging.basicConfig(level=logging.DEBUG, filename='rtv.log')
locale.setlocale(locale.LC_ALL, '')
args = command_line()
local_config = load_config()
# Fill in empty arguments with config file values. Paramaters explicitly
# typed on the command line will take priority over config file params.
for key, val in local_config.items():
@@ -84,11 +87,11 @@ def main():
# PRAW will prompt for password if it is None
reddit.login(args.username, args.password)
with curses_session() as stdscr:
if args.link:
page = SubmissionPage(stdscr, reddit, url=args.link)
page.loop()
page = SubredditPage(stdscr, reddit, args.subreddit)
if args.link:
page = SubmissionPage(stdscr, reddit, url=args.link)
page.loop()
page = SubredditPage(stdscr, reddit, args.subreddit)
page.loop()
except praw.errors.InvalidUserPass:
print('Invalid password for username: {}'.format(args.username))
except requests.ConnectionError:

View File

@@ -2,4 +2,4 @@
Global configuration settings
"""
unicode = False
unicode = False

View File

@@ -8,6 +8,7 @@ from .helpers import humanize_timestamp, wrap_text, strip_subreddit_url
__all__ = ['SubredditContent', 'SubmissionContent']
class BaseContent(object):
def get(self, index, n_cols):
@@ -40,7 +41,8 @@ class BaseContent(object):
retval = []
while stack:
item = stack.pop(0)
if isinstance(item, praw.objects.MoreComments) and (item.count==0):
if isinstance(item, praw.objects.MoreComments) and (
item.count == 0):
continue
nested = getattr(item, 'replies', None)
if nested:
@@ -70,9 +72,12 @@ class BaseContent(object):
data['body'] = comment.body
data['created'] = humanize_timestamp(comment.created_utc)
data['score'] = '{} pts'.format(comment.score)
data['author'] = (comment.author.name if getattr(comment, 'author') else '[deleted]')
data['is_author'] = (data['author'] == getattr(comment.submission, 'author'))
data['flair'] = (comment.author_flair_text if comment.author_flair_text else '')
author = getattr(comment, 'author')
data['author'] = (author.name if author else '[deleted]')
sub_author = getattr(comment.submission.author, 'name')
data['is_author'] = (data['author'] == sub_author)
flair = comment.author_flair_text
data['flair'] = (flair if flair else '')
data['likes'] = comment.likes
return data
@@ -94,7 +99,8 @@ class BaseContent(object):
data['created'] = humanize_timestamp(sub.created_utc)
data['comments'] = '{} comments'.format(sub.num_comments)
data['score'] = '{} pts'.format(sub.score)
data['author'] = (sub.author.name if getattr(sub, 'author') else '[deleted]')
author = getattr(sub, 'author')
data['author'] = (author.name if author else '[deleted]')
data['permalink'] = sub.permalink
data['subreddit'] = strip_subreddit_url(sub.permalink)
data['flair'] = (sub.link_flair_text if sub.link_flair_text else '')
@@ -104,7 +110,9 @@ class BaseContent(object):
return data
class SubmissionContent(BaseContent):
"""
Grab a submission from PRAW and lazily store comments to an internal
list for repeat access.
@@ -155,9 +163,10 @@ class SubmissionContent(BaseContent):
elif index == -1:
data = self._submission_data
data['split_title'] = textwrap.wrap(data['title'], width=n_cols-2)
data['split_text'] = wrap_text(data['text'], width=n_cols-2)
data['n_rows'] = len(data['split_title'])+len(data['split_text'])+5
data['split_title'] = textwrap.wrap(data['title'],
width=n_cols -2)
data['split_text'] = wrap_text(data['text'], width=n_cols - 2)
data['n_rows'] = len(data['split_title']) + len(data['split_text']) + 5
data['offset'] = 0
else:
@@ -191,7 +200,7 @@ class SubmissionContent(BaseContent):
elif data['type'] == 'Comment':
cache = [data]
count = 1
for d in self.iterate(index+1, 1, n_cols):
for d in self.iterate(index + 1, 1, n_cols):
if d['level'] <= data['level']:
break
@@ -204,10 +213,10 @@ class SubmissionContent(BaseContent):
comment['count'] = count
comment['level'] = data['level']
comment['body'] = 'Hidden'.format(count)
self._comment_data[index:index+len(cache)] = [comment]
self._comment_data[index:index + len(cache)] = [comment]
elif data['type'] == 'HiddenComment':
self._comment_data[index:index+1] = data['cache']
self._comment_data[index:index + 1] = data['cache']
elif data['type'] == 'MoreComments':
with self._loader():
@@ -215,13 +224,14 @@ class SubmissionContent(BaseContent):
comments = self.flatten_comments(comments,
root_level=data['level'])
comment_data = [self.strip_praw_comment(c) for c in comments]
self._comment_data[index:index+1] = comment_data
self._comment_data[index:index + 1] = comment_data
else:
raise ValueError('% type not recognized' % data['type'])
class SubredditContent(BaseContent):
"""
Grabs a subreddit from PRAW and lazily stores submissions to an internal
list for repeat access.
@@ -235,7 +245,7 @@ class SubredditContent(BaseContent):
self._submission_data = []
@classmethod
def from_name(cls, reddit, name, loader, order='hot'):
def from_name(cls, reddit, name, loader, order='hot', search=None):
if name is None:
name = 'front'
@@ -252,9 +262,11 @@ class SubredditContent(BaseContent):
display_name = '/r/{}'.format(name)
else:
display_name = '/r/{}/{}'.format(name, order)
if name == 'front':
if order == 'hot':
if search:
submissions = reddit.search(search, None, order)
elif order == 'hot':
submissions = reddit.get_front_page(limit=None)
elif order == 'top':
submissions = reddit.get_top(limit=None)
@@ -266,10 +278,11 @@ class SubredditContent(BaseContent):
submissions = reddit.get_controversial(limit=None)
else:
raise SubredditError(display_name)
else:
subreddit = reddit.get_subreddit(name)
if order == 'hot':
if search:
submissions = reddit.search(search, name, order)
elif order == 'hot':
submissions = subreddit.get_hot(limit=None)
elif order == 'top':
submissions = subreddit.get_top(limit=None)

View File

@@ -23,6 +23,7 @@ UARROW = u'\u25b2'.encode('utf-8')
DARROW = u'\u25bc'.encode('utf-8')
BULLET = u'\u2022'.encode('utf-8')
def show_notification(stdscr, message):
"""
Overlay a message box on the center of the screen and wait for user input.
@@ -51,19 +52,24 @@ def show_notification(stdscr, message):
for index, line in enumerate(message, start=1):
window.addstr(index, 1, line)
window.refresh()
stdscr.getch()
ch = stdscr.getch()
window.clear()
window = None
stdscr.refresh()
return ch
def show_help(stdscr):
"""
Overlay a message box with the help screen.
"""
show_notification(stdscr, HELP.split("\n"))
class LoadScreen(object):
"""
Display a loading dialog while waiting for a blocking action to complete.
@@ -128,10 +134,10 @@ class LoadScreen(object):
n_rows, n_cols = self._stdscr.getmaxyx()
s_row = (n_rows - 3) // 2
s_col = (n_cols - message_len - 1) // 2
window = self._stdscr.derwin(3, message_len+2, s_row, s_col)
window = self._stdscr.derwin(3, message_len + 2, s_row, s_col)
while True:
for i in range(len(trail)+1):
for i in range(len(trail) + 1):
if not self._is_running:
window.clear()
@@ -145,7 +151,9 @@ class LoadScreen(object):
window.refresh()
time.sleep(interval)
class Color(object):
"""
Color attributes for curses.
"""
@@ -158,7 +166,7 @@ class Color(object):
'MAGENTA': (curses.COLOR_MAGENTA, -1),
'CYAN': (curses.COLOR_CYAN, -1),
'WHITE': (curses.COLOR_WHITE, -1),
}
}
@classmethod
def init(cls):
@@ -182,6 +190,7 @@ class Color(object):
levels = [cls.MAGENTA, cls.CYAN, cls.GREEN, cls.YELLOW]
return levels[level % len(levels)]
def text_input(window, allow_resize=True):
"""
Transform a window into a text box that will accept user input and loop
@@ -192,7 +201,7 @@ def text_input(window, allow_resize=True):
"""
window.clear()
# Set cursor mode to 1 because 2 doesn't display on some terminals
curses.curs_set(1)
@@ -223,6 +232,7 @@ def text_input(window, allow_resize=True):
curses.curs_set(0)
return strip_textpad(out)
@contextmanager
def curses_session():
"""

View File

@@ -2,9 +2,7 @@ from .__version__ import __version__
__all__ = ['AGENT', 'SUMMARY', 'AUTH', 'CONTROLS', 'HELP']
AGENT = """
desktop:https://github.com/michael-lazar/rtv:{} (by /u/civilization_phaze_3)
""".format(__version__)
AGENT = "desktop:https://github.com/michael-lazar/rtv:{} (by /u/civilization_phaze_3)".format(__version__)
SUMMARY = """
Reddit Terminal Viewer is a lightweight browser for www.reddit.com built into a
@@ -32,17 +30,19 @@ Global Commands
`a/z` : Upvote/downvote the selected item
`r` : Refresh the current page
`q` : Quit the program
`o` : Open the selected item in the default web browser
`ENTER` or `o` : Open the selected item in the default web browser
`u` : Log in
`?` : Show this help message
Subreddit Mode
`RIGHT` or `l` : View comments for the selected submission
`/` : Open a prompt to switch subreddits
`f` : Open a prompt to search the current subreddit
Submission Mode
`LEFT` or `h` : Return to subreddit mode
`RIGHT` or `l` : Fold the selected comment, or load additional comments
`c` : Comment/reply on the selected item
`c` : Comment/reply on the selected item
"""
COMMENT_FILE = """

View File

@@ -1,17 +1,23 @@
class SubmissionError(Exception):
"Submission could not be loaded"
"""Submission could not be loaded"""
def __init__(self, url):
self.url = url
class SubredditError(Exception):
"Subreddit could not be reached"
"""Subreddit could not be reached"""
def __init__(self, name):
self.name = name
class ProgramError(Exception):
"Problem executing an external program"
"""Problem executing an external program"""
def __init__(self, name):
self.name = name
class EscapeInterrupt(Exception):
"Signal that the ESC key has been pressed"
"""Signal that the ESC key has been pressed"""

View File

@@ -11,6 +11,7 @@ from .exceptions import ProgramError
__all__ = ['open_browser', 'clean', 'wrap_text', 'strip_textpad',
'strip_subreddit_url', 'humanize_timestamp', 'open_editor']
def open_editor(data=''):
"""
Open a temporary file using the system's default editor.
@@ -39,6 +40,7 @@ def open_editor(data=''):
return text
def open_browser(url):
"""
Call webbrowser.open_new_tab(url) and redirect stdout/stderr to devnull.
@@ -52,6 +54,7 @@ def open_browser(url):
with open(os.devnull, 'ab+', 0) as null:
subprocess.check_call(args, stdout=null, stderr=null)
def clean(string):
"""
Required reading!
@@ -75,6 +78,7 @@ def clean(string):
string = string.encode(encoding, 'replace')
return string
def wrap_text(text, width):
"""
Wrap text paragraphs to the given character width while preserving newlines.
@@ -87,6 +91,7 @@ def wrap_text(text, width):
out.extend(lines)
return out
def strip_textpad(text):
"""
Attempt to intelligently strip excess whitespace from the output of a
@@ -121,6 +126,7 @@ def strip_textpad(text):
out = '\n'.join(stack)
return out
def strip_subreddit_url(permalink):
"""
Strip a subreddit name from the subreddit's permalink.
@@ -131,6 +137,7 @@ def strip_subreddit_url(permalink):
subreddit = permalink.split('/')[4]
return '/r/{}'.format(subreddit)
def humanize_timestamp(utc_timestamp, verbose=False):
"""
Convert a utc timestamp into a human readable relative-time.
@@ -154,4 +161,4 @@ def humanize_timestamp(utc_timestamp, verbose=False):
if months < 12:
return ('%d months ago' % months) if verbose else ('%dmonth' % months)
years = months // 12
return ('%d years ago' % years) if verbose else ('%dyr' % years)
return ('%d years ago' % years) if verbose else ('%dyr' % years)

View File

@@ -1,13 +1,18 @@
import curses
import six
import sys
import praw.errors
from .helpers import clean
from .curses_helpers import Color, show_notification
from .curses_helpers import Color, show_notification, show_help, text_input
from .docs import AGENT
__all__ = ['Navigator']
class Navigator(object):
"""
Handles math behind cursor movement and screen paging.
"""
@@ -43,7 +48,7 @@ class Navigator(object):
valid, redraw = True, False
forward = ((direction*self.step) > 0)
forward = ((direction * self.step) > 0)
if forward:
if self.page_index < 0:
@@ -71,11 +76,12 @@ class Navigator(object):
else:
self.page_index -= self.step
if self._is_valid(self.absolute_index):
# We have reached the beginning of the page - move the index
# We have reached the beginning of the page - move the
# index
redraw = True
else:
self.page_index += self.step
valid = False # Revert
valid = False # Revert
return valid, redraw
@@ -96,7 +102,58 @@ class Navigator(object):
return True
class BaseController(object):
"""
Event handler for triggering functions with curses keypresses.
Register a keystroke to a class method using the @egister decorator.
#>>> @Controller.register('a', 'A')
#>>> def func(self, *args)
Register a default behavior by using `None`.
#>>> @Controller.register(None)
#>>> def default_func(self, *args)
Bind the controller to a class instance and trigger a key. Additional
arguments will be passed to the function.
#>>> controller = Controller(self)
#>>> controller.trigger('a', *args)
"""
character_map = {None: (lambda *args, **kwargs: None)}
def __init__(self, instance):
self.instance = instance
def trigger(self, char, *args, **kwargs):
if isinstance(char, six.string_types) and len(char) == 1:
char = ord(char)
func = self.character_map.get(char)
if func is None:
func = BaseController.character_map.get(char)
if func is None:
func = self.character_map.get(None)
if func is None:
func = BaseController.character_map.get(None)
return func(self.instance, *args, **kwargs)
@classmethod
def register(cls, *chars):
def wrap(f):
for char in chars:
if isinstance(char, six.string_types) and len(char) == 1:
cls.character_map[ord(char)] = f
else:
cls.character_map[char] = f
return f
return wrap
class BasePage(object):
"""
Base terminal viewer incorperates a cursor to navigate content
"""
@@ -115,11 +172,23 @@ class BasePage(object):
self._content_window = None
self._subwindows = None
@BaseController.register('q')
def exit(self):
sys.exit()
@BaseController.register('?')
def help(self):
show_help(self.stdscr)
@BaseController.register(curses.KEY_UP, 'k')
def move_cursor_up(self):
self._move_cursor(-1)
self.clear_input_queue()
@BaseController.register(curses.KEY_DOWN, 'j')
def move_cursor_down(self):
self._move_cursor(1)
self.clear_input_queue()
def clear_input_queue(self):
"Clear excessive input caused by the scroll wheel or holding down a key"
@@ -128,8 +197,8 @@ class BasePage(object):
continue
self.stdscr.nodelay(0)
@BaseController.register('a')
def upvote(self):
data = self.content.get(self.nav.absolute_index)
try:
if 'likes' not in data:
@@ -143,8 +212,8 @@ class BasePage(object):
except praw.errors.LoginOrScopeRequired:
show_notification(self.stdscr, ['Login to vote'])
@BaseController.register('z')
def downvote(self):
data = self.content.get(self.nav.absolute_index)
try:
if 'likes' not in data:
@@ -158,6 +227,61 @@ class BasePage(object):
except praw.errors.LoginOrScopeRequired:
show_notification(self.stdscr, ['Login to vote'])
@BaseController.register('u')
def login(self):
"""
Prompt to log into the user's account. Log out if the user is already
logged in.
"""
if self.reddit.is_logged_in():
self.logout()
return
username = self.prompt_input('Enter username:')
password = self.prompt_input('Enter password:', hide=True)
if not username or not password:
curses.flash()
return
try:
self.reddit.login(username, password)
except praw.errors.InvalidUserPass:
show_notification(self.stdscr, ['Invalid user/pass'])
else:
show_notification(self.stdscr, ['Logged in'])
def logout(self):
"""
Prompt to log out of the user's account.
"""
ch = self.prompt_input("Log out? (y/n):")
if ch == 'y':
self.reddit.clear_authentication()
show_notification(self.stdscr, ['Logged out'])
elif ch != 'n':
curses.flash()
def prompt_input(self, prompt, hide=False):
"""Prompt the user for input"""
attr = curses.A_BOLD | Color.CYAN
n_rows, n_cols = self.stdscr.getmaxyx()
if hide:
prompt += ' ' * (n_cols - len(prompt) - 1)
self.stdscr.addstr(n_rows-1, 0, prompt, attr)
out = self.stdscr.getstr(n_rows-1, 1)
else:
self.stdscr.addstr(n_rows - 1, 0, prompt, attr)
self.stdscr.refresh()
window = self.stdscr.derwin(1, n_cols - len(prompt),
n_rows - 1, len(prompt))
window.attrset(attr)
out = text_input(window)
return out
def draw(self):
n_rows, n_cols = self.stdscr.getmaxyx()
@@ -166,7 +290,7 @@ class BasePage(object):
# Note: 2 argument form of derwin breaks PDcurses on Windows 7!
self._header_window = self.stdscr.derwin(1, n_cols, 0, 0)
self._content_window = self.stdscr.derwin(n_rows-1, n_cols, 1, 0)
self._content_window = self.stdscr.derwin(n_rows - 1, n_cols, 1, 0)
self.stdscr.erase()
self._draw_header()
@@ -186,12 +310,13 @@ class BasePage(object):
self._header_window.bkgd(' ', attr)
sub_name = self.content.name.replace('/r/front', 'Front Page ')
self._header_window.addnstr(0, 0, clean(sub_name), n_cols-1)
self._header_window.addnstr(0, 0, clean(sub_name), n_cols - 1)
if self.reddit.user is not None:
username = self.reddit.user.name
s_col = (n_cols - len(username) - 1)
# Only print the username if it fits in the empty space on the right
# Only print the username if it fits in the empty space on the
# right
if (s_col - 1) >= len(sub_name):
n = (n_cols - s_col - 1)
self._header_window.addnstr(0, s_col, clean(username), n)
@@ -215,7 +340,7 @@ class BasePage(object):
# and draw upwards.
current_row = (n_rows - 1) if inverted else 0
available_rows = (n_rows - 1) if inverted else n_rows
for data in self.content.iterate(page_index, step, n_cols-2):
for data in self.content.iterate(page_index, step, n_cols - 2):
window_rows = min(available_rows, data['n_rows'])
window_cols = n_cols - data['offset']
start = current_row - window_rows if inverted else current_row
@@ -250,7 +375,8 @@ class BasePage(object):
self._remove_cursor()
valid, redraw = self.nav.move(direction, len(self._subwindows))
if not valid: curses.flash()
if not valid:
curses.flash()
# Note: ACS_VLINE doesn't like changing the attribute, so always redraw.
# if redraw: self._draw_content()

View File

@@ -5,20 +5,25 @@ import time
import praw.errors
from .content import SubmissionContent
from .page import BasePage, Navigator
from .page import BasePage, Navigator, BaseController
from .helpers import clean, open_browser, open_editor
from .curses_helpers import (BULLET, UARROW, DARROW, Color, LoadScreen,
show_help, show_notification, text_input)
show_notification, text_input)
from .docs import COMMENT_FILE
__all__ = ['SubmissionPage']
__all__ = ['SubmissionController', 'SubmissionPage']
class SubmissionController(BaseController):
character_map = {}
class SubmissionPage(BasePage):
def __init__(self, stdscr, reddit, url=None, submission=None):
self.controller = SubmissionController(self)
self.loader = LoadScreen(stdscr)
if url is not None:
content = SubmissionContent.from_url(reddit, url, self.loader)
elif submission is not None:
@@ -30,59 +35,14 @@ class SubmissionPage(BasePage):
page_index=-1)
def loop(self):
self.draw()
while True:
self.active = True
while self.active:
self.draw()
cmd = self.stdscr.getch()
self.controller.trigger(cmd)
if cmd in (curses.KEY_UP, ord('k')):
self.move_cursor_up()
self.clear_input_queue()
elif cmd in (curses.KEY_DOWN, ord('j')):
self.move_cursor_down()
self.clear_input_queue()
elif cmd in (curses.KEY_RIGHT, curses.KEY_ENTER, ord('l')):
self.toggle_comment()
self.draw()
elif cmd in (curses.KEY_LEFT, ord('h')):
break
elif cmd == ord('o'):
self.open_link()
self.draw()
elif cmd in (curses.KEY_F5, ord('r')):
self.refresh_content()
self.draw()
elif cmd == ord('c'):
self.add_comment()
self.draw()
elif cmd == ord('?'):
show_help(self.stdscr)
self.draw()
elif cmd == ord('a'):
self.upvote()
self.draw()
elif cmd == ord('z'):
self.downvote()
self.draw()
elif cmd == ord('q'):
sys.exit()
elif cmd == curses.KEY_RESIZE:
self.draw()
@SubmissionController.register(curses.KEY_RIGHT, 'l')
def toggle_comment(self):
current_index = self.nav.absolute_index
self.content.toggle(current_index)
if self.nav.inverted:
@@ -91,19 +51,27 @@ class SubmissionPage(BasePage):
# cursor index to go out of bounds.
self.nav.page_index, self.nav.cursor_index = current_index, 0
def refresh_content(self):
@SubmissionController.register(curses.KEY_LEFT, 'h')
def exit_submission(self):
self.active = False
@SubmissionController.register(curses.KEY_F5, 'r')
def refresh_content(self):
url = self.content.name
self.content = SubmissionContent.from_url(self.reddit, url, self.loader)
self.content = SubmissionContent.from_url(
self.reddit,
url,
self.loader)
self.nav = Navigator(self.content.get, page_index=-1)
@SubmissionController.register(curses.KEY_ENTER, 10, 'o')
def open_link(self):
# Always open the page for the submission
# May want to expand at some point to open comment permalinks
url = self.content.get(-1)['permalink']
open_browser(url)
@SubmissionController.register('c')
def add_comment(self):
"""
Add a comment on the submission if a header is selected.
@@ -174,12 +142,12 @@ class SubmissionPage(BasePage):
text = clean('{author} '.format(**data))
attr = curses.A_BOLD
attr |= (Color.BLUE if not data['is_author'] else Color.GREEN)
win.addnstr(row, 1, text, n_cols-1, attr)
win.addnstr(row, 1, text, n_cols - 1, attr)
if data['flair']:
text = clean('{flair} '.format(**data))
attr = curses.A_BOLD | Color.YELLOW
win.addnstr(text, n_cols-win.getyx()[1], attr)
win.addnstr(text, n_cols - win.getyx()[1], attr)
if data['likes'] is None:
text, attr = BULLET, curses.A_BOLD
@@ -187,16 +155,16 @@ class SubmissionPage(BasePage):
text, attr = UARROW, (curses.A_BOLD | Color.GREEN)
else:
text, attr = DARROW, (curses.A_BOLD | Color.RED)
win.addnstr(text, n_cols-win.getyx()[1], attr)
win.addnstr(text, n_cols - win.getyx()[1], attr)
text = clean(' {score} {created}'.format(**data))
win.addnstr(text, n_cols-win.getyx()[1])
win.addnstr(text, n_cols - win.getyx()[1])
n_body = len(data['split_body'])
for row, text in enumerate(data['split_body'], start=offset+1):
for row, text in enumerate(data['split_body'], start=offset + 1):
if row in valid_rows:
text = clean(text)
win.addnstr(row, 1, text, n_cols-1)
win.addnstr(row, 1, text, n_cols - 1)
# Unfortunately vline() doesn't support custom color so we have to
# build it one segment at a time.
@@ -205,8 +173,8 @@ class SubmissionPage(BasePage):
x = 0
# http://bugs.python.org/issue21088
if (sys.version_info.major,
sys.version_info.minor,
sys.version_info.micro) == (3, 4, 0):
sys.version_info.minor,
sys.version_info.micro) == (3, 4, 0):
x, y = y, x
win.addch(y, x, curses.ACS_VLINE, attr)
@@ -220,9 +188,9 @@ class SubmissionPage(BasePage):
n_cols -= 1
text = clean('{body}'.format(**data))
win.addnstr(0, 1, text, n_cols-1)
win.addnstr(0, 1, text, n_cols - 1)
text = clean(' [{count}]'.format(**data))
win.addnstr(text, n_cols-win.getyx()[1], curses.A_BOLD)
win.addnstr(text, n_cols - win.getyx()[1], curses.A_BOLD)
# Unfortunately vline() doesn't support custom color so we have to
# build it one segment at a time.
@@ -235,7 +203,7 @@ class SubmissionPage(BasePage):
def draw_submission(win, data):
n_rows, n_cols = win.getmaxyx()
n_cols -= 3 # one for each side of the border + one for offset
n_cols -= 3 # one for each side of the border + one for offset
# Don't print at all if there is not enough room to fit the whole sub
if data['n_rows'] > n_rows:
@@ -252,9 +220,9 @@ class SubmissionPage(BasePage):
win.addnstr(row, 1, text, n_cols, attr)
attr = curses.A_BOLD | Color.YELLOW
text = clean(' {flair}'.format(**data))
win.addnstr(text, n_cols-win.getyx()[1], attr)
win.addnstr(text, n_cols - win.getyx()[1], attr)
text = clean(' {created} {subreddit}'.format(**data))
win.addnstr(text, n_cols-win.getyx()[1])
win.addnstr(text, n_cols - win.getyx()[1])
row = len(data['split_title']) + 2
attr = curses.A_UNDERLINE | Color.BLUE
@@ -270,4 +238,4 @@ class SubmissionPage(BasePage):
text = clean('{score} {comments}'.format(**data))
win.addnstr(row, 1, text, n_cols, curses.A_BOLD)
win.border()
win.border()

View File

@@ -1,81 +1,43 @@
import curses
import sys
import requests
from .exceptions import SubredditError
from .page import BasePage, Navigator
from .page import BasePage, Navigator, BaseController
from .submission import SubmissionPage
from .content import SubredditContent
from .helpers import clean, open_browser
from .curses_helpers import (BULLET, UARROW, DARROW, Color, LoadScreen,
text_input, show_notification, show_help)
from .curses_helpers import (BULLET, UARROW, DARROW, Color, LoadScreen,
show_notification)
__all__ = ['opened_links', 'SubredditPage']
__all__ = ['opened_links', 'SubredditController', 'SubredditPage']
# Used to keep track of browsing history across the current session
opened_links = set()
class SubredditController(BaseController):
character_map = {}
class SubredditPage(BasePage):
def __init__(self, stdscr, reddit, name):
self.controller = SubredditController(self)
self.loader = LoadScreen(stdscr)
content = SubredditContent.from_name(reddit, name, self.loader)
super(SubredditPage, self).__init__(stdscr, reddit, content)
def loop(self):
self.draw()
while True:
self.draw()
cmd = self.stdscr.getch()
self.controller.trigger(cmd)
if cmd in (curses.KEY_UP, ord('k')):
self.move_cursor_up()
self.clear_input_queue()
elif cmd in (curses.KEY_DOWN, ord('j')):
self.move_cursor_down()
self.clear_input_queue()
elif cmd in (curses.KEY_RIGHT, curses.KEY_ENTER, ord('l')):
self.open_submission()
self.draw()
elif cmd == ord('o'):
self.open_link()
self.draw()
elif cmd in (curses.KEY_F5, ord('r')):
self.refresh_content()
self.draw()
elif cmd == ord('?'):
show_help(self.stdscr)
self.draw()
elif cmd == ord('a'):
self.upvote()
self.draw()
elif cmd == ord('z'):
self.downvote()
self.draw()
elif cmd == ord('q'):
sys.exit()
elif cmd == curses.KEY_RESIZE:
self.draw()
elif cmd == ord('/'):
self.prompt_subreddit()
self.draw()
@SubredditController.register(curses.KEY_F5, 'r')
def refresh_content(self, name=None):
name = name or self.content.name
try:
@@ -88,21 +50,29 @@ class SubredditPage(BasePage):
else:
self.nav = Navigator(self.content.get)
@SubredditController.register('f')
def search_subreddit(self, name=None):
"""Open a prompt to search the subreddit"""
name = name or self.content.name
prompt = 'Search this Subreddit: '
search = self.prompt_input(prompt)
if search is not None:
try:
self.nav.cursor_index = 0
self.content = SubredditContent.from_name(self.reddit, name,
self.loader, search=search)
except IndexError: # if there are no submissions
show_notification(self.stdscr, ['No results found'])
@SubredditController.register('/')
def prompt_subreddit(self):
"Open a prompt to type in a new subreddit"
attr = curses.A_BOLD | Color.CYAN
"""Open a prompt to type in a new subreddit"""
prompt = 'Enter Subreddit: /r/'
n_rows, n_cols = self.stdscr.getmaxyx()
self.stdscr.addstr(n_rows-1, 0, prompt, attr)
self.stdscr.refresh()
window = self.stdscr.derwin(1, n_cols-len(prompt),n_rows-1, len(prompt))
window.attrset(attr)
out = text_input(window)
if out is not None:
self.refresh_content(name=out)
name = self.prompt_input(prompt)
if name is not None:
self.refresh_content(name=name)
@SubredditController.register(curses.KEY_RIGHT, 'l')
def open_submission(self):
"Select the current submission to view posts"
@@ -114,6 +84,7 @@ class SubredditPage(BasePage):
global opened_links
opened_links.add(data['url_full'])
@SubredditController.register(curses.KEY_ENTER, 10, 'o')
def open_link(self):
"Open a link with the webbrowser"
@@ -137,7 +108,7 @@ class SubredditPage(BasePage):
for row, text in enumerate(data['split_title'], start=offset):
if row in valid_rows:
text = clean(text)
win.addnstr(row, 1, text, n_cols-1, curses.A_BOLD)
win.addnstr(row, 1, text, n_cols - 1, curses.A_BOLD)
row = n_title + offset
if row in valid_rows:
@@ -145,12 +116,12 @@ class SubredditPage(BasePage):
link_color = Color.MAGENTA if seen else Color.BLUE
attr = curses.A_UNDERLINE | link_color
text = clean('{url}'.format(**data))
win.addnstr(row, 1, text, n_cols-1, attr)
win.addnstr(row, 1, text, n_cols - 1, attr)
row = n_title + offset + 1
if row in valid_rows:
text = clean('{score} '.format(**data))
win.addnstr(row, 1, text, n_cols-1)
win.addnstr(row, 1, text, n_cols - 1)
if data['likes'] is None:
text, attr = BULLET, curses.A_BOLD
@@ -158,16 +129,16 @@ class SubredditPage(BasePage):
text, attr = UARROW, curses.A_BOLD | Color.GREEN
else:
text, attr = DARROW, curses.A_BOLD | Color.RED
win.addnstr(text, n_cols-win.getyx()[1], attr)
win.addnstr(text, n_cols - win.getyx()[1], attr)
text = clean(' {created} {comments}'.format(**data))
win.addnstr(text, n_cols-win.getyx()[1])
win.addnstr(text, n_cols - win.getyx()[1])
row = n_title + offset + 2
if row in valid_rows:
text = clean('{author}'.format(**data))
win.addnstr(row, 1, text, n_cols-1, curses.A_BOLD)
win.addnstr(row, 1, text, n_cols - 1, curses.A_BOLD)
text = clean(' {subreddit}'.format(**data))
win.addnstr(text, n_cols-win.getyx()[1], Color.YELLOW)
win.addnstr(text, n_cols - win.getyx()[1], Color.YELLOW)
text = clean(' {flair}'.format(**data))
win.addnstr(text, n_cols-win.getyx()[1], Color.RED)
win.addnstr(text, n_cols - win.getyx()[1], Color.RED)