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.
|
Press `?` to open the help screen.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
HELP = """
|
HELP = """\
|
||||||
|
====================================
|
||||||
Reddit Terminal Viewer
|
Reddit Terminal Viewer
|
||||||
|
|
||||||
https://github.com/michael-lazar/rtv
|
https://github.com/michael-lazar/rtv
|
||||||
====================================
|
====================================
|
||||||
|
|
||||||
[Basic Commands]
|
[Commands]
|
||||||
j/k or ▲/▼ : Move the cursor up/down
|
j : Move the cursor up
|
||||||
m/n or PgUp/PgDn : Jump to the previous/next page
|
k : Move the cursor down
|
||||||
gg/G : Jump to the top/bottom of the page
|
m : Move up one page
|
||||||
1-5 : Toggle post order
|
n : Move down one page
|
||||||
r or F5 : Refresh page content
|
gg : Jump to the first post
|
||||||
u : Log in or switch accounts
|
G : Jump to the last post
|
||||||
/ : Open a prompt to switch subreddits
|
1 : Sort by hot
|
||||||
? : Show the help screen
|
2 : Sort by top
|
||||||
q/Q : Quit/Force quit
|
3 : Sort by rising
|
||||||
|
4 : Sort by new
|
||||||
[Authenticated Commands]
|
5 : Sort by controversial
|
||||||
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
|
|
||||||
p : Return to the front page
|
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]
|
[Prompt]
|
||||||
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]
|
|
||||||
The `/` prompt accepts subreddits in the following formats
|
The `/` prompt accepts subreddits in the following formats
|
||||||
|
|
||||||
- python
|
- python
|
||||||
@@ -72,6 +74,22 @@ https://github.com/michael-lazar/rtv
|
|||||||
- /domain/python.org (search by domain)
|
- /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 = """
|
COMMENT_FILE = """
|
||||||
# Please enter a comment. Lines starting with '#' will be ignored,
|
# Please enter a comment. Lines starting with '#' will be ignored,
|
||||||
# and an empty message aborts the comment.
|
# 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):
|
class Page(object):
|
||||||
|
|
||||||
|
FOOTER = None
|
||||||
|
|
||||||
def __init__(self, reddit, term, config, oauth):
|
def __init__(self, reddit, term, config, oauth):
|
||||||
|
|
||||||
self.reddit = reddit
|
self.reddit = reddit
|
||||||
@@ -299,6 +301,7 @@ class Page(object):
|
|||||||
self._draw_header()
|
self._draw_header()
|
||||||
self._draw_banner()
|
self._draw_banner()
|
||||||
self._draw_content()
|
self._draw_content()
|
||||||
|
self._draw_footer()
|
||||||
self._add_cursor()
|
self._add_cursor()
|
||||||
self.term.stdscr.touchwin()
|
self.term.stdscr.touchwin()
|
||||||
self.term.stdscr.refresh()
|
self.term.stdscr.refresh()
|
||||||
@@ -306,6 +309,7 @@ class Page(object):
|
|||||||
def _draw_header(self):
|
def _draw_header(self):
|
||||||
|
|
||||||
n_rows, n_cols = self.term.stdscr.getmaxyx()
|
n_rows, n_cols = self.term.stdscr.getmaxyx()
|
||||||
|
|
||||||
# Note: 2 argument form of derwin breaks PDcurses on Windows 7!
|
# Note: 2 argument form of derwin breaks PDcurses on Windows 7!
|
||||||
window = self.term.stdscr.derwin(1, n_cols, self._row, 0)
|
window = self.term.stdscr.derwin(1, n_cols, self._row, 0)
|
||||||
window.erase()
|
window.erase()
|
||||||
@@ -351,7 +355,7 @@ class Page(object):
|
|||||||
ch, attr = str(' '), curses.A_BOLD | Color.YELLOW
|
ch, attr = str(' '), curses.A_BOLD | Color.YELLOW
|
||||||
window.bkgd(ch, attr)
|
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)
|
distance = (n_cols - sum(len(t) for t in items) - 1) / (len(items) - 1)
|
||||||
spacing = max(1, int(distance)) * ' '
|
spacing = max(1, int(distance)) * ' '
|
||||||
text = spacing.join(items)
|
text = spacing.join(items)
|
||||||
@@ -370,7 +374,7 @@ class Page(object):
|
|||||||
|
|
||||||
n_rows, n_cols = self.term.stdscr.getmaxyx()
|
n_rows, n_cols = self.term.stdscr.getmaxyx()
|
||||||
window = self.term.stdscr.derwin(
|
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()
|
window.erase()
|
||||||
win_n_rows, win_n_cols = window.getmaxyx()
|
win_n_rows, win_n_cols = window.getmaxyx()
|
||||||
|
|
||||||
@@ -383,7 +387,7 @@ class Page(object):
|
|||||||
# and draw upwards.
|
# and draw upwards.
|
||||||
cancel_inverted = True
|
cancel_inverted = True
|
||||||
current_row = (win_n_rows - 1) if inverted else 0
|
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
|
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):
|
for data in self.content.iterate(page_index, step, win_n_cols - 2):
|
||||||
subwin_n_rows = min(available_rows, data['n_rows'])
|
subwin_n_rows = min(available_rows, data['n_rows'])
|
||||||
@@ -396,7 +400,7 @@ class Page(object):
|
|||||||
subwin_inverted = True
|
subwin_inverted = True
|
||||||
top_item_height = None
|
top_item_height = None
|
||||||
subwin_n_cols = win_n_cols - data['offset']
|
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(
|
subwindow = window.derwin(
|
||||||
subwin_n_rows, subwin_n_cols, start, data['offset'])
|
subwin_n_rows, subwin_n_cols, start, data['offset'])
|
||||||
attr = self._draw_item(subwindow, data, subwin_inverted)
|
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
|
# if the content will fill up the page, given that it is dependent
|
||||||
# on the size of the terminal.
|
# on the size of the terminal.
|
||||||
self.nav.flip((len(self._subwindows) - 1))
|
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):
|
def _add_cursor(self):
|
||||||
self._edit_cursor(curses.A_REVERSE)
|
self._edit_cursor(curses.A_REVERSE)
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ class SubmissionController(PageController):
|
|||||||
|
|
||||||
class SubmissionPage(Page):
|
class SubmissionPage(Page):
|
||||||
|
|
||||||
|
FOOTER = docs.FOOTER_SUBMISSION
|
||||||
|
|
||||||
def __init__(self, reddit, term, config, oauth, url=None, submission=None):
|
def __init__(self, reddit, term, config, oauth, url=None, submission=None):
|
||||||
super(SubmissionPage, self).__init__(reddit, term, config, oauth)
|
super(SubmissionPage, self).__init__(reddit, term, config, oauth)
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ class SubredditController(PageController):
|
|||||||
|
|
||||||
class SubredditPage(Page):
|
class SubredditPage(Page):
|
||||||
|
|
||||||
|
FOOTER = docs.FOOTER_SUBREDDIT
|
||||||
|
|
||||||
def __init__(self, reddit, term, config, oauth, name):
|
def __init__(self, reddit, term, config, oauth, name):
|
||||||
"""
|
"""
|
||||||
Params:
|
Params:
|
||||||
@@ -265,7 +267,7 @@ class SubredditPage(Page):
|
|||||||
row = n_title + offset + 2
|
row = n_title + offset + 2
|
||||||
if row in valid_rows:
|
if row in valid_rows:
|
||||||
text = '{author}'.format(**data)
|
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)
|
text = ' /r/{subreddit}'.format(**data)
|
||||||
self.term.add_line(win, text, attr=Color.YELLOW)
|
self.term.add_line(win, text, attr=Color.YELLOW)
|
||||||
if data['flair']:
|
if data['flair']:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import curses
|
import curses
|
||||||
|
|
||||||
|
from . import docs
|
||||||
from .page import Page, PageController
|
from .page import Page, PageController
|
||||||
from .content import SubscriptionContent, SubredditContent
|
from .content import SubscriptionContent, SubredditContent
|
||||||
from .objects import Color, Navigator, Command
|
from .objects import Color, Navigator, Command
|
||||||
@@ -14,6 +15,8 @@ class SubscriptionController(PageController):
|
|||||||
|
|
||||||
class SubscriptionPage(Page):
|
class SubscriptionPage(Page):
|
||||||
|
|
||||||
|
FOOTER = docs.FOOTER_SUBSCRIPTION
|
||||||
|
|
||||||
def __init__(self, reddit, term, config, oauth, content_type='subreddit'):
|
def __init__(self, reddit, term, config, oauth, content_type='subreddit'):
|
||||||
super(SubscriptionPage, self).__init__(reddit, term, config, oauth)
|
super(SubscriptionPage, self).__init__(reddit, term, config, oauth)
|
||||||
|
|
||||||
|
|||||||
@@ -664,18 +664,20 @@ class Terminal(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
n_rows, n_cols = self.stdscr.getmaxyx()
|
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)
|
prompt = self.clean(prompt, n_cols-1)
|
||||||
|
|
||||||
# Create a new window to draw the text at the bottom of the screen,
|
# Create a new window to draw the text at the bottom of the screen,
|
||||||
# so we can erase it when we're done.
|
# so we can erase it when we're done.
|
||||||
prompt_win = curses.newwin(1, len(prompt)+1, n_rows-1, 0)
|
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()
|
prompt_win.refresh()
|
||||||
|
|
||||||
# Create a separate window for text input
|
# Create a separate window for text input
|
||||||
input_win = curses.newwin(1, n_cols-len(prompt), n_rows-1, len(prompt))
|
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:
|
if key:
|
||||||
curses.curs_set(1)
|
curses.curs_set(1)
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ def test_submission_page_construct(reddit, terminal, config, oauth):
|
|||||||
'[5]controversial').encode('utf-8')
|
'[5]controversial').encode('utf-8')
|
||||||
window.addstr.assert_any_call(0, 0, menu)
|
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
|
||||||
submission_data = page.content.get(-1)
|
submission_data = page.content.get(-1)
|
||||||
text = submission_data['title'].encode('utf-8')
|
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')
|
text = '(Not enough space to display)'.encode('ascii')
|
||||||
window = terminal.stdscr.subwin
|
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):
|
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]
|
window.getch.side_effect = [ord('h'), ord('i'), terminal.RETURN]
|
||||||
assert isinstance(terminal.prompt_input('hi'), six.text_type)
|
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'))
|
||||||
stdscr.subwin.addstr.assert_called_with(0, 0, 'hi'.encode('ascii'), attr)
|
|
||||||
assert window.nlines == 1
|
assert window.nlines == 1
|
||||||
assert window.ncols == 78
|
assert window.ncols == 78
|
||||||
|
|
||||||
@@ -254,27 +253,26 @@ def test_prompt_input(terminal, stdscr, use_ascii):
|
|||||||
def test_prompt_y_or_n(terminal, stdscr):
|
def test_prompt_y_or_n(terminal, stdscr):
|
||||||
|
|
||||||
stdscr.getch.side_effect = [ord('y'), ord('N'), terminal.ESCAPE, ord('a')]
|
stdscr.getch.side_effect = [ord('y'), ord('N'), terminal.ESCAPE, ord('a')]
|
||||||
attr = Color.CYAN | curses.A_BOLD
|
|
||||||
text = 'hi'.encode('ascii')
|
text = 'hi'.encode('ascii')
|
||||||
|
|
||||||
# Press 'y'
|
# Press 'y'
|
||||||
assert terminal.prompt_y_or_n('hi')
|
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
|
assert not curses.flash.called
|
||||||
|
|
||||||
# Press 'N'
|
# Press 'N'
|
||||||
assert not terminal.prompt_y_or_n('hi')
|
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
|
assert not curses.flash.called
|
||||||
|
|
||||||
# Press Esc
|
# Press Esc
|
||||||
assert not terminal.prompt_y_or_n('hi')
|
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
|
assert not curses.flash.called
|
||||||
|
|
||||||
# Press an invalid key
|
# Press an invalid key
|
||||||
assert not terminal.prompt_y_or_n('hi')
|
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
|
assert curses.flash.called
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user