86
rtv/docs.py
86
rtv/docs.py
@@ -16,47 +16,49 @@ Move the cursor using either the arrow keys or *Vim* style movement.
|
||||
Press `?` to open the help screen.
|
||||
"""
|
||||
|
||||
HELP = """
|
||||
HELP = """\
|
||||
====================================
|
||||
Reddit Terminal Viewer
|
||||
|
||||
https://github.com/michael-lazar/rtv
|
||||
====================================
|
||||
|
||||
[Basic Commands]
|
||||
j/k or ▲/▼ : Move the cursor up/down
|
||||
m/n or PgUp/PgDn : Jump to the previous/next page
|
||||
gg/G : Jump to the top/bottom of the page
|
||||
1-5 : Toggle post order
|
||||
r or F5 : Refresh page content
|
||||
u : Log in or switch accounts
|
||||
/ : Open a prompt to switch subreddits
|
||||
? : Show the help screen
|
||||
q/Q : Quit/Force quit
|
||||
|
||||
[Authenticated Commands]
|
||||
a/z : Upvote/downvote
|
||||
c : Compose a new post or comment
|
||||
e : Edit an existing post or comment
|
||||
d : Delete an existing post or comment
|
||||
i : Display new messages prompt
|
||||
s : View a list of subscribed subreddits
|
||||
S : View a list of subscribed multireddits
|
||||
w : Save a submission
|
||||
|
||||
[Subreddit Commands]
|
||||
l or ► : Enter the selected submission
|
||||
o or ENTER : Open the submission link with your web browser
|
||||
f : Open a prompt to search the current subreddit
|
||||
[Commands]
|
||||
j : Move the cursor up
|
||||
k : Move the cursor down
|
||||
m : Move up one page
|
||||
n : Move down one page
|
||||
gg : Jump to the first post
|
||||
G : Jump to the last post
|
||||
1 : Sort by hot
|
||||
2 : Sort by top
|
||||
3 : Sort by rising
|
||||
4 : Sort by new
|
||||
5 : Sort by controversial
|
||||
p : Return to the front page
|
||||
r : Refresh page
|
||||
u : Login or logout
|
||||
/ : Open the subreddit prompt
|
||||
f : Open the search prompt
|
||||
? : Show the help screen
|
||||
q : Quit
|
||||
Q : Force quit
|
||||
a : Upvote
|
||||
z : Downvote
|
||||
c : Compose a new submission/comment
|
||||
e : Edit a submission/comment
|
||||
d : Delete a submission/comment
|
||||
i : Display new messages
|
||||
s : Show subscribed subreddits
|
||||
S : Show subscribed multireddits
|
||||
w : Save a submission/comment
|
||||
l : View comments, or open comment in pager
|
||||
h : Return to subreddit
|
||||
o : Open the submission or comment url
|
||||
SPACE : Fold or expand the selected comment tree
|
||||
b : Display urls with urlview
|
||||
|
||||
[Submission Commands]
|
||||
h or ◄ : Return to the subreddit
|
||||
l or ► : Open the selected comment in a new window
|
||||
o or ENTER : Open the comment permalink with your web browser
|
||||
SPACE : Fold the selected comment, or load additional comments
|
||||
b : Display URLs with urlview
|
||||
|
||||
[Navigating]
|
||||
[Prompt]
|
||||
The `/` prompt accepts subreddits in the following formats
|
||||
|
||||
- python
|
||||
@@ -72,6 +74,22 @@ https://github.com/michael-lazar/rtv
|
||||
- /domain/python.org (search by domain)
|
||||
"""
|
||||
|
||||
BANNER = """
|
||||
[1]hot [2]top [3]rising [4]new [5]controversial
|
||||
"""
|
||||
|
||||
FOOTER_SUBREDDIT = """
|
||||
[?]Help [q]Quit [l]Comments [/]Prompt [u]Login [o]Open [c]Post [a/z]Vote
|
||||
"""
|
||||
|
||||
FOOTER_SUBMISSION = """
|
||||
[?]Help [q]Quit [h]Return [space]Fold/Expand [o]Open [c]Comment [a/z]Vote
|
||||
"""
|
||||
|
||||
FOOTER_SUBSCRIPTION = """
|
||||
[?]Help [q]Quit [h]Return [l]Select
|
||||
"""
|
||||
|
||||
COMMENT_FILE = """
|
||||
# Please enter a comment. Lines starting with '#' will be ignored,
|
||||
# and an empty message aborts the comment.
|
||||
|
||||
28
rtv/page.py
28
rtv/page.py
@@ -34,6 +34,8 @@ class PageController(Controller):
|
||||
|
||||
class Page(object):
|
||||
|
||||
FOOTER = None
|
||||
|
||||
def __init__(self, reddit, term, config, oauth):
|
||||
|
||||
self.reddit = reddit
|
||||
@@ -299,6 +301,7 @@ class Page(object):
|
||||
self._draw_header()
|
||||
self._draw_banner()
|
||||
self._draw_content()
|
||||
self._draw_footer()
|
||||
self._add_cursor()
|
||||
self.term.stdscr.touchwin()
|
||||
self.term.stdscr.refresh()
|
||||
@@ -306,6 +309,7 @@ class Page(object):
|
||||
def _draw_header(self):
|
||||
|
||||
n_rows, n_cols = self.term.stdscr.getmaxyx()
|
||||
|
||||
# Note: 2 argument form of derwin breaks PDcurses on Windows 7!
|
||||
window = self.term.stdscr.derwin(1, n_cols, self._row, 0)
|
||||
window.erase()
|
||||
@@ -351,7 +355,7 @@ class Page(object):
|
||||
ch, attr = str(' '), curses.A_BOLD | Color.YELLOW
|
||||
window.bkgd(ch, attr)
|
||||
|
||||
items = ['[1]hot', '[2]top', '[3]rising', '[4]new', '[5]controversial']
|
||||
items = docs.BANNER.strip().split(' ')
|
||||
distance = (n_cols - sum(len(t) for t in items) - 1) / (len(items) - 1)
|
||||
spacing = max(1, int(distance)) * ' '
|
||||
text = spacing.join(items)
|
||||
@@ -370,7 +374,7 @@ class Page(object):
|
||||
|
||||
n_rows, n_cols = self.term.stdscr.getmaxyx()
|
||||
window = self.term.stdscr.derwin(
|
||||
n_rows - self._row, n_cols, self._row, 0)
|
||||
n_rows - self._row - 1, n_cols, self._row, 0)
|
||||
window.erase()
|
||||
win_n_rows, win_n_cols = window.getmaxyx()
|
||||
|
||||
@@ -383,7 +387,7 @@ class Page(object):
|
||||
# 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
|
||||
available_rows = 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'])
|
||||
@@ -396,7 +400,7 @@ class Page(object):
|
||||
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
|
||||
start = current_row - subwin_n_rows + 1 if inverted else current_row
|
||||
subwindow = window.derwin(
|
||||
subwin_n_rows, subwin_n_cols, start, data['offset'])
|
||||
attr = self._draw_item(subwindow, data, subwin_inverted)
|
||||
@@ -420,9 +424,21 @@ class Page(object):
|
||||
# if the content will fill up the page, given that it is dependent
|
||||
# on the size of the terminal.
|
||||
self.nav.flip((len(self._subwindows) - 1))
|
||||
self._draw_content()
|
||||
return self._draw_content()
|
||||
|
||||
self._row = n_rows
|
||||
self._row += win_n_rows
|
||||
|
||||
def _draw_footer(self):
|
||||
|
||||
n_rows, n_cols = self.term.stdscr.getmaxyx()
|
||||
window = self.term.stdscr.derwin(1, n_cols, self._row, 0)
|
||||
window.erase()
|
||||
ch, attr = str(' '), curses.A_REVERSE | curses.A_BOLD | Color.CYAN
|
||||
window.bkgd(ch, attr)
|
||||
|
||||
text = self.FOOTER.strip()
|
||||
self.term.add_line(window, text, 0, 0)
|
||||
self._row += 1
|
||||
|
||||
def _add_cursor(self):
|
||||
self._edit_cursor(curses.A_REVERSE)
|
||||
|
||||
@@ -17,6 +17,8 @@ class SubmissionController(PageController):
|
||||
|
||||
class SubmissionPage(Page):
|
||||
|
||||
FOOTER = docs.FOOTER_SUBMISSION
|
||||
|
||||
def __init__(self, reddit, term, config, oauth, url=None, submission=None):
|
||||
super(SubmissionPage, self).__init__(reddit, term, config, oauth)
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ class SubredditController(PageController):
|
||||
|
||||
class SubredditPage(Page):
|
||||
|
||||
FOOTER = docs.FOOTER_SUBREDDIT
|
||||
|
||||
def __init__(self, reddit, term, config, oauth, name):
|
||||
"""
|
||||
Params:
|
||||
@@ -265,7 +267,7 @@ class SubredditPage(Page):
|
||||
row = n_title + offset + 2
|
||||
if row in valid_rows:
|
||||
text = '{author}'.format(**data)
|
||||
self.term.add_line(win, text, row, 1, curses.A_BOLD)
|
||||
self.term.add_line(win, text, row, 1, Color.GREEN)
|
||||
text = ' /r/{subreddit}'.format(**data)
|
||||
self.term.add_line(win, text, attr=Color.YELLOW)
|
||||
if data['flair']:
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
||||
|
||||
import curses
|
||||
|
||||
from . import docs
|
||||
from .page import Page, PageController
|
||||
from .content import SubscriptionContent, SubredditContent
|
||||
from .objects import Color, Navigator, Command
|
||||
@@ -14,6 +15,8 @@ class SubscriptionController(PageController):
|
||||
|
||||
class SubscriptionPage(Page):
|
||||
|
||||
FOOTER = docs.FOOTER_SUBSCRIPTION
|
||||
|
||||
def __init__(self, reddit, term, config, oauth, content_type='subreddit'):
|
||||
super(SubscriptionPage, self).__init__(reddit, term, config, oauth)
|
||||
|
||||
|
||||
@@ -664,18 +664,20 @@ class Terminal(object):
|
||||
"""
|
||||
|
||||
n_rows, n_cols = self.stdscr.getmaxyx()
|
||||
attr = curses.A_BOLD | Color.CYAN
|
||||
ch, attr = str(' '), curses.A_BOLD | curses.A_REVERSE | Color.CYAN
|
||||
prompt = self.clean(prompt, n_cols-1)
|
||||
|
||||
# Create a new window to draw the text at the bottom of the screen,
|
||||
# so we can erase it when we're done.
|
||||
prompt_win = curses.newwin(1, len(prompt)+1, n_rows-1, 0)
|
||||
self.add_line(prompt_win, prompt, attr=attr)
|
||||
prompt_win.bkgd(ch, attr)
|
||||
self.add_line(prompt_win, prompt)
|
||||
prompt_win.refresh()
|
||||
|
||||
# Create a separate window for text input
|
||||
input_win = curses.newwin(1, n_cols-len(prompt), n_rows-1, len(prompt))
|
||||
input_win.attrset(attr)
|
||||
input_win.bkgd(ch, attr)
|
||||
input_win.refresh()
|
||||
|
||||
if key:
|
||||
curses.curs_set(1)
|
||||
|
||||
@@ -51,6 +51,11 @@ def test_submission_page_construct(reddit, terminal, config, oauth):
|
||||
'[5]controversial').encode('utf-8')
|
||||
window.addstr.assert_any_call(0, 0, menu)
|
||||
|
||||
# Footer
|
||||
text = ('[?]Help [q]Quit [h]Return [space]Fold/Expand [o]Open [c]Comment '
|
||||
'[a/z]Vote'.encode('utf-8'))
|
||||
window.addstr.assert_any_call(0, 0, text)
|
||||
|
||||
# Submission
|
||||
submission_data = page.content.get(-1)
|
||||
text = submission_data['title'].encode('utf-8')
|
||||
@@ -181,7 +186,7 @@ def test_submission_comment_not_enough_space(submission_page, terminal):
|
||||
|
||||
text = '(Not enough space to display)'.encode('ascii')
|
||||
window = terminal.stdscr.subwin
|
||||
window.subwin.addstr.assert_any_call(7, 1, text)
|
||||
window.subwin.addstr.assert_any_call(6, 1, text)
|
||||
|
||||
|
||||
def test_submission_vote(submission_page, refresh_token):
|
||||
|
||||
@@ -236,8 +236,7 @@ def test_prompt_input(terminal, stdscr, use_ascii):
|
||||
window.getch.side_effect = [ord('h'), ord('i'), terminal.RETURN]
|
||||
assert isinstance(terminal.prompt_input('hi'), six.text_type)
|
||||
|
||||
attr = Color.CYAN | curses.A_BOLD
|
||||
stdscr.subwin.addstr.assert_called_with(0, 0, 'hi'.encode('ascii'), attr)
|
||||
stdscr.subwin.addstr.assert_called_with(0, 0, 'hi'.encode('ascii'))
|
||||
assert window.nlines == 1
|
||||
assert window.ncols == 78
|
||||
|
||||
@@ -254,27 +253,26 @@ def test_prompt_input(terminal, stdscr, use_ascii):
|
||||
def test_prompt_y_or_n(terminal, stdscr):
|
||||
|
||||
stdscr.getch.side_effect = [ord('y'), ord('N'), terminal.ESCAPE, ord('a')]
|
||||
attr = Color.CYAN | curses.A_BOLD
|
||||
text = 'hi'.encode('ascii')
|
||||
|
||||
# Press 'y'
|
||||
assert terminal.prompt_y_or_n('hi')
|
||||
stdscr.subwin.addstr.assert_called_with(0, 0, text, attr)
|
||||
stdscr.subwin.addstr.assert_called_with(0, 0, text)
|
||||
assert not curses.flash.called
|
||||
|
||||
# Press 'N'
|
||||
assert not terminal.prompt_y_or_n('hi')
|
||||
stdscr.subwin.addstr.assert_called_with(0, 0, text, attr)
|
||||
stdscr.subwin.addstr.assert_called_with(0, 0, text)
|
||||
assert not curses.flash.called
|
||||
|
||||
# Press Esc
|
||||
assert not terminal.prompt_y_or_n('hi')
|
||||
stdscr.subwin.addstr.assert_called_with(0, 0, text, attr)
|
||||
stdscr.subwin.addstr.assert_called_with(0, 0, text)
|
||||
assert not curses.flash.called
|
||||
|
||||
# Press an invalid key
|
||||
assert not terminal.prompt_y_or_n('hi')
|
||||
stdscr.subwin.addstr.assert_called_with(0, 0, text, attr)
|
||||
stdscr.subwin.addstr.assert_called_with(0, 0, text)
|
||||
assert curses.flash.called
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user