Semi-major refactor, added upvotes/downvotes.
This commit is contained in:
@@ -10,13 +10,10 @@ from .errors import SubmissionURLError, SubredditNameError
|
|||||||
|
|
||||||
def split_text(big_text, width):
|
def split_text(big_text, width):
|
||||||
return [
|
return [
|
||||||
text
|
text for line in big_text.splitlines()
|
||||||
for line in big_text.splitlines()
|
# wrap returns an empty list when "line" is a newline. In order to
|
||||||
# wrap returns an empty list when "line" is a newline
|
# consider newlines we need a list containing an empty string.
|
||||||
# in order to consider newlines we need a list containing an
|
for text in (textwrap.wrap(line, width=width) or [''])]
|
||||||
# empty string
|
|
||||||
for text in (textwrap.wrap(line, width=width) or [''])
|
|
||||||
]
|
|
||||||
|
|
||||||
def strip_subreddit_url(permalink):
|
def strip_subreddit_url(permalink):
|
||||||
"""
|
"""
|
||||||
@@ -64,12 +61,10 @@ class BaseContent(object):
|
|||||||
def iterate(self, index, step, n_cols):
|
def iterate(self, index, step, n_cols):
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
||||||
# Hack to prevent displaying negative indicies if iterating in the
|
|
||||||
# negative direction.
|
|
||||||
if step < 0 and index < 0:
|
if step < 0 and index < 0:
|
||||||
|
# Hack to prevent displaying negative indicies if iterating in
|
||||||
|
# the negative direction.
|
||||||
break
|
break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield self.get(index, n_cols=n_cols)
|
yield self.get(index, n_cols=n_cols)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
@@ -109,6 +104,11 @@ class BaseContent(object):
|
|||||||
data['object'] = comment
|
data['object'] = comment
|
||||||
data['level'] = comment.nested_level
|
data['level'] = comment.nested_level
|
||||||
|
|
||||||
|
if getattr(comment.submission, 'author'):
|
||||||
|
sub_author = comment.submission.author.name
|
||||||
|
else:
|
||||||
|
sub_author = '[deleted]'
|
||||||
|
|
||||||
if isinstance(comment, praw.objects.MoreComments):
|
if isinstance(comment, praw.objects.MoreComments):
|
||||||
data['type'] = 'MoreComments'
|
data['type'] = 'MoreComments'
|
||||||
data['count'] = comment.count
|
data['count'] = comment.count
|
||||||
@@ -119,9 +119,9 @@ class BaseContent(object):
|
|||||||
data['created'] = humanize_timestamp(comment.created_utc)
|
data['created'] = humanize_timestamp(comment.created_utc)
|
||||||
data['score'] = '{} pts'.format(comment.score)
|
data['score'] = '{} pts'.format(comment.score)
|
||||||
data['author'] = (comment.author.name if getattr(comment, 'author') else '[deleted]')
|
data['author'] = (comment.author.name if getattr(comment, 'author') else '[deleted]')
|
||||||
sub_author = (comment.submission.author.name if getattr(comment.submission, 'author') else '[deleted]')
|
|
||||||
data['is_author'] = (data['author'] == sub_author)
|
data['is_author'] = (data['author'] == sub_author)
|
||||||
data['flair'] = (comment.author_flair_text if comment.author_flair_text else '')
|
data['flair'] = (comment.author_flair_text if comment.author_flair_text else '')
|
||||||
|
data['likes'] = comment.likes
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -148,6 +148,7 @@ class BaseContent(object):
|
|||||||
data['flair'] = (sub.link_flair_text if sub.link_flair_text else '')
|
data['flair'] = (sub.link_flair_text if sub.link_flair_text else '')
|
||||||
data['url_full'] = sub.url
|
data['url_full'] = sub.url
|
||||||
data['url'] = ('selfpost' if is_selfpost(sub.url) else sub.url)
|
data['url'] = ('selfpost' if is_selfpost(sub.url) else sub.url)
|
||||||
|
data['likes'] = sub.likes
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|||||||
10
rtv/main.py
10
rtv/main.py
@@ -5,9 +5,8 @@ import praw
|
|||||||
from requests.exceptions import ConnectionError, HTTPError
|
from requests.exceptions import ConnectionError, HTTPError
|
||||||
from praw.errors import InvalidUserPass
|
from praw.errors import InvalidUserPass
|
||||||
|
|
||||||
from . import utils
|
|
||||||
from .errors import SubmissionURLError, SubredditNameError
|
from .errors import SubmissionURLError, SubredditNameError
|
||||||
from .utils import curses_session, load_config, HELP
|
from .utils import Symbol, curses_session, load_config, HELP
|
||||||
from .subreddit import SubredditPage
|
from .subreddit import SubredditPage
|
||||||
from .submission import SubmissionPage
|
from .submission import SubmissionPage
|
||||||
|
|
||||||
@@ -28,7 +27,7 @@ terminal window.
|
|||||||
|
|
||||||
EPILOG = """
|
EPILOG = """
|
||||||
Controls
|
Controls
|
||||||
-----
|
--------
|
||||||
RTV currently supports browsing both subreddits and individual submissions.
|
RTV currently supports browsing both subreddits and individual submissions.
|
||||||
In each mode the controls are slightly different. In subreddit mode you can
|
In each mode the controls are slightly different. In subreddit mode you can
|
||||||
browse through the top submissions on either the front page or a specific
|
browse through the top submissions on either the front page or a specific
|
||||||
@@ -45,7 +44,7 @@ def main():
|
|||||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
parser.add_argument('-s', dest='subreddit', help='subreddit name')
|
parser.add_argument('-s', dest='subreddit', help='subreddit name')
|
||||||
parser.add_argument('-l', dest='link', help='full link to a submission')
|
parser.add_argument('-l', dest='link', help='full link to a submission')
|
||||||
parser.add_argument('--unicode', help='enable unicode (beta)',
|
parser.add_argument('--unicode', help='enable unicode (experimental)',
|
||||||
action='store_true')
|
action='store_true')
|
||||||
|
|
||||||
group = parser.add_argument_group(
|
group = parser.add_argument_group(
|
||||||
@@ -64,7 +63,7 @@ def main():
|
|||||||
if getattr(args, key) is None:
|
if getattr(args, key) is None:
|
||||||
setattr(args, key, val)
|
setattr(args, key, val)
|
||||||
|
|
||||||
utils.UNICODE = args.unicode
|
Symbol.UNICODE = args.unicode
|
||||||
|
|
||||||
if args.subreddit is None:
|
if args.subreddit is None:
|
||||||
args.subreddit = 'front'
|
args.subreddit = 'front'
|
||||||
@@ -75,6 +74,7 @@ def main():
|
|||||||
reddit.config.decode_html_entities = True
|
reddit.config.decode_html_entities = True
|
||||||
|
|
||||||
if args.username:
|
if args.username:
|
||||||
|
# PRAW will prompt for password if it is None
|
||||||
reddit.login(args.username, args.password)
|
reddit.login(args.username, args.password)
|
||||||
|
|
||||||
with curses_session() as stdscr:
|
with curses_session() as stdscr:
|
||||||
|
|||||||
47
rtv/page.py
47
rtv/page.py
@@ -1,6 +1,6 @@
|
|||||||
import curses
|
import curses
|
||||||
|
|
||||||
from .utils import Color, clean
|
from .utils import Color, Symbol
|
||||||
|
|
||||||
class Navigator(object):
|
class Navigator(object):
|
||||||
"""
|
"""
|
||||||
@@ -129,14 +129,41 @@ class BasePage(object):
|
|||||||
continue
|
continue
|
||||||
self.stdscr.nodelay(0)
|
self.stdscr.nodelay(0)
|
||||||
|
|
||||||
|
def upvote(self):
|
||||||
|
|
||||||
|
data = self.content.get(self.nav.absolute_index)
|
||||||
|
if 'likes' not in data:
|
||||||
|
curses.flash()
|
||||||
|
|
||||||
|
elif data['likes']:
|
||||||
|
data['object'].clear_vote()
|
||||||
|
data['likes'] = None
|
||||||
|
else:
|
||||||
|
data['object'].upvote()
|
||||||
|
data['likes'] = True
|
||||||
|
|
||||||
|
def downvote(self):
|
||||||
|
|
||||||
|
data = self.content.get(self.nav.absolute_index)
|
||||||
|
if 'likes' not in data:
|
||||||
|
curses.flash()
|
||||||
|
|
||||||
|
if data['likes'] is False:
|
||||||
|
data['object'].clear_vote()
|
||||||
|
data['likes'] = None
|
||||||
|
else:
|
||||||
|
data['object'].downvote()
|
||||||
|
data['likes'] = False
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
|
|
||||||
n_rows, n_cols = self.stdscr.getmaxyx()
|
n_rows, n_cols = self.stdscr.getmaxyx()
|
||||||
if n_rows < self.MIN_HEIGHT or n_cols < self.MIN_WIDTH:
|
if n_rows < self.MIN_HEIGHT or n_cols < self.MIN_WIDTH:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Note: 2 argument form of derwin breaks PDcurses on Windows 7!
|
||||||
self._header_window = self.stdscr.derwin(1, n_cols, 0, 0)
|
self._header_window = self.stdscr.derwin(1, n_cols, 0, 0)
|
||||||
self._content_window = self.stdscr.derwin(1, 0)
|
self._content_window = self.stdscr.derwin(n_rows-1, n_cols, 1, 0)
|
||||||
|
|
||||||
self.stdscr.erase()
|
self.stdscr.erase()
|
||||||
self._draw_header()
|
self._draw_header()
|
||||||
@@ -157,7 +184,7 @@ class BasePage(object):
|
|||||||
self._header_window.bkgd(' ', attr)
|
self._header_window.bkgd(' ', attr)
|
||||||
|
|
||||||
sub_name = self.content.name.replace('/r/front', 'Front Page ')
|
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, Symbol.clean(sub_name), n_cols-1)
|
||||||
|
|
||||||
if self.reddit.user is not None:
|
if self.reddit.user is not None:
|
||||||
username = self.reddit.user.name
|
username = self.reddit.user.name
|
||||||
@@ -165,7 +192,7 @@ class BasePage(object):
|
|||||||
# 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):
|
if (s_col - 1) >= len(sub_name):
|
||||||
n = (n_cols - s_col - 1)
|
n = (n_cols - s_col - 1)
|
||||||
self._header_window.addnstr(0, s_col, clean(username), n)
|
self._header_window.addnstr(0, s_col, Symbol.clean(username), n)
|
||||||
|
|
||||||
self._header_window.refresh()
|
self._header_window.refresh()
|
||||||
|
|
||||||
@@ -215,14 +242,11 @@ class BasePage(object):
|
|||||||
self.remove_cursor()
|
self.remove_cursor()
|
||||||
|
|
||||||
valid, redraw = self.nav.move(direction, len(self._subwindows))
|
valid, redraw = self.nav.move(direction, len(self._subwindows))
|
||||||
if not valid:
|
if not valid: curses.flash()
|
||||||
curses.flash()
|
|
||||||
|
|
||||||
# TODO: If we don't redraw, ACS_VLINE gets screwed up when changing the
|
|
||||||
# attr back to normal. There may be a way around this.
|
|
||||||
if True: #if redraw
|
|
||||||
self._draw_content()
|
|
||||||
|
|
||||||
|
# Note: ACS_VLINE doesn't like changing the attribute, so always redraw.
|
||||||
|
# if redraw: self._draw_content()
|
||||||
|
self._draw_content()
|
||||||
self.add_cursor()
|
self.add_cursor()
|
||||||
|
|
||||||
def _edit_cursor(self, attribute=None):
|
def _edit_cursor(self, attribute=None):
|
||||||
@@ -231,7 +255,6 @@ class BasePage(object):
|
|||||||
if self.nav.absolute_index < 0:
|
if self.nav.absolute_index < 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
# TODO: attach attr to data[attr] or something
|
|
||||||
window, attr = self._subwindows[self.nav.cursor_index]
|
window, attr = self._subwindows[self.nav.cursor_index]
|
||||||
if attr is not None:
|
if attr is not None:
|
||||||
attribute |= attr
|
attribute |= attr
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import curses
|
import curses
|
||||||
import sys
|
import sys
|
||||||
import webbrowser
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from .content import SubmissionContent
|
from .content import SubmissionContent
|
||||||
from .page import BasePage
|
from .page import BasePage
|
||||||
from .utils import LoadScreen, Color, ESCAPE, display_help, open_new_tab, clean
|
from .utils import LoadScreen, Color, Symbol, display_help, open_browser
|
||||||
|
|
||||||
class SubmissionPage(BasePage):
|
class SubmissionPage(BasePage):
|
||||||
|
|
||||||
@@ -42,6 +39,9 @@ class SubmissionPage(BasePage):
|
|||||||
self.toggle_comment()
|
self.toggle_comment()
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
|
elif cmd in (curses.KEY_LEFT, ord('h')):
|
||||||
|
break
|
||||||
|
|
||||||
elif cmd == ord('o'):
|
elif cmd == ord('o'):
|
||||||
self.open_link()
|
self.open_link()
|
||||||
self.draw()
|
self.draw()
|
||||||
@@ -54,17 +54,19 @@ class SubmissionPage(BasePage):
|
|||||||
display_help(self.stdscr)
|
display_help(self.stdscr)
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
|
elif cmd == ord('a'):
|
||||||
|
self.upvote()
|
||||||
|
self.draw()
|
||||||
|
|
||||||
|
elif cmd == ord('z'):
|
||||||
|
self.downvote()
|
||||||
|
self.draw()
|
||||||
|
|
||||||
elif cmd == ord('q'):
|
elif cmd == ord('q'):
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
elif cmd == curses.KEY_RESIZE:
|
elif cmd == curses.KEY_RESIZE:
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
elif cmd in (ESCAPE, curses.KEY_LEFT, ord('h')):
|
|
||||||
break
|
|
||||||
|
|
||||||
else:
|
|
||||||
curses.beep()
|
|
||||||
|
|
||||||
def toggle_comment(self):
|
def toggle_comment(self):
|
||||||
|
|
||||||
@@ -81,19 +83,16 @@ class SubmissionPage(BasePage):
|
|||||||
# Always open the page for the submission
|
# Always open the page for the submission
|
||||||
# May want to expand at some point to open comment permalinks
|
# May want to expand at some point to open comment permalinks
|
||||||
url = self.content.get(-1)['permalink']
|
url = self.content.get(-1)['permalink']
|
||||||
open_new_tab(url)
|
open_browser(url)
|
||||||
|
|
||||||
def draw_item(self, win, data, inverted=False):
|
def draw_item(self, win, data, inverted=False):
|
||||||
|
|
||||||
if data['type'] == 'MoreComments':
|
if data['type'] == 'MoreComments':
|
||||||
return self.draw_more_comments(win, data)
|
return self.draw_more_comments(win, data)
|
||||||
|
|
||||||
elif data['type'] == 'HiddenComment':
|
elif data['type'] == 'HiddenComment':
|
||||||
return self.draw_more_comments(win, data)
|
return self.draw_more_comments(win, data)
|
||||||
|
|
||||||
elif data['type'] == 'Comment':
|
elif data['type'] == 'Comment':
|
||||||
return self.draw_comment(win, data, inverted=inverted)
|
return self.draw_comment(win, data, inverted=inverted)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return self.draw_submission(win, data)
|
return self.draw_submission(win, data)
|
||||||
|
|
||||||
@@ -109,53 +108,66 @@ class SubmissionPage(BasePage):
|
|||||||
|
|
||||||
row = offset
|
row = offset
|
||||||
if row in valid_rows:
|
if row in valid_rows:
|
||||||
text = '{author}'.format(**data)
|
|
||||||
|
text = Symbol.clean('{author} '.format(**data))
|
||||||
attr = curses.A_BOLD
|
attr = curses.A_BOLD
|
||||||
attr |= (Color.BLUE if not data['is_author'] else Color.GREEN)
|
attr |= (Color.BLUE if not data['is_author'] else Color.GREEN)
|
||||||
win.addnstr(row, 1, clean(text), n_cols-1, attr)
|
win.addnstr(row, 1, text, n_cols-1, attr)
|
||||||
text = ' {flair}'.format(**data)
|
|
||||||
win.addnstr(clean(text), n_cols-win.getyx()[1], curses.A_BOLD | Color.YELLOW)
|
if data['flair']:
|
||||||
text = ' {score} {created}'.format(**data)
|
text = Symbol.clean('{flair} '.format(**data))
|
||||||
win.addnstr(clean(text), n_cols - win.getyx()[1])
|
attr = curses.A_BOLD | Color.YELLOW
|
||||||
|
win.addnstr(text, n_cols-win.getyx()[1], attr)
|
||||||
|
|
||||||
|
if data['likes'] is None:
|
||||||
|
text, attr = Symbol.BULLET, curses.A_BOLD
|
||||||
|
elif data['likes']:
|
||||||
|
text, attr = Symbol.UARROW, (curses.A_BOLD | Color.GREEN)
|
||||||
|
else:
|
||||||
|
text, attr = Symbol.DARROW, (curses.A_BOLD | Color.RED)
|
||||||
|
win.addnstr(text, n_cols-win.getyx()[1], attr)
|
||||||
|
|
||||||
|
text = Symbol.clean(' {score} {created}'.format(**data))
|
||||||
|
win.addnstr(text, n_cols-win.getyx()[1])
|
||||||
|
|
||||||
n_body = len(data['split_body'])
|
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:
|
if row in valid_rows:
|
||||||
win.addnstr(row, 1, clean(text), n_cols-1)
|
text = Symbol.clean(text)
|
||||||
|
win.addnstr(row, 1, text, n_cols-1)
|
||||||
|
|
||||||
# Vertical line, unfortunately vline() doesn't support custom color so
|
# Unfortunately vline() doesn't support custom color so we have to
|
||||||
# we have to build it one chr at a time.
|
# build it one segment at a time.
|
||||||
attr = Color.get_level(data['level'])
|
attr = Color.get_level(data['level'])
|
||||||
for y in range(n_rows):
|
for y in range(n_rows):
|
||||||
|
|
||||||
x = 0
|
x = 0
|
||||||
|
|
||||||
# Nobody pays attention to curses ;(
|
|
||||||
# http://bugs.python.org/issue21088
|
# http://bugs.python.org/issue21088
|
||||||
if (sys.version_info.major,
|
if (sys.version_info.major,
|
||||||
sys.version_info.minor,
|
sys.version_info.minor,
|
||||||
sys.version_info.micro) == (3, 4, 0):
|
sys.version_info.micro) == (3, 4, 0):
|
||||||
x, y = y, x
|
x, y = y, x
|
||||||
|
|
||||||
win.addch(y, x, curses.ACS_VLINE, attr)
|
win.addch(y, x, curses.ACS_VLINE, attr)
|
||||||
|
|
||||||
return attr | curses.ACS_VLINE
|
return (attr | curses.ACS_VLINE)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def draw_more_comments(win, data):
|
def draw_more_comments(win, data):
|
||||||
|
|
||||||
n_rows, n_cols = win.getmaxyx()
|
n_rows, n_cols = win.getmaxyx()
|
||||||
n_cols -= 1
|
n_cols -= 1
|
||||||
text = '{body}'.format(**data)
|
|
||||||
win.addnstr(0, 1, clean(text), n_cols-1)
|
|
||||||
text = ' [{count}]'.format(**data)
|
|
||||||
win.addnstr(clean(text), n_cols - win.getyx()[1], curses.A_BOLD)
|
|
||||||
|
|
||||||
|
text = Symbol.clean('{body}'.format(**data))
|
||||||
|
win.addnstr(0, 1, text, n_cols-1)
|
||||||
|
text = Symbol.clean(' [{count}]'.format(**data))
|
||||||
|
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.
|
||||||
attr = Color.get_level(data['level'])
|
attr = Color.get_level(data['level'])
|
||||||
for y in range(n_rows):
|
win.addch(0, 0, curses.ACS_VLINE, attr)
|
||||||
win.addch(y, 0, curses.ACS_VLINE, attr)
|
|
||||||
|
|
||||||
return attr | curses.ACS_VLINE
|
return (attr | curses.ACS_VLINE)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def draw_submission(win, data):
|
def draw_submission(win, data):
|
||||||
@@ -169,28 +181,31 @@ class SubmissionPage(BasePage):
|
|||||||
return
|
return
|
||||||
|
|
||||||
for row, text in enumerate(data['split_title'], start=1):
|
for row, text in enumerate(data['split_title'], start=1):
|
||||||
win.addnstr(row, 1, clean(text), n_cols, curses.A_BOLD)
|
text = Symbol.clean(text)
|
||||||
|
win.addnstr(row, 1, text, n_cols, curses.A_BOLD)
|
||||||
|
|
||||||
row = len(data['split_title']) + 1
|
row = len(data['split_title']) + 1
|
||||||
attr = curses.A_BOLD | Color.GREEN
|
attr = curses.A_BOLD | Color.GREEN
|
||||||
text = '{author}'.format(**data)
|
text = Symbol.clean('{author}'.format(**data))
|
||||||
win.addnstr(row, 1, clean(text), n_cols, attr)
|
win.addnstr(row, 1, text, n_cols, attr)
|
||||||
text = ' {flair}'.format(**data)
|
attr = curses.A_BOLD | Color.YELLOW
|
||||||
win.addnstr(clean(text), n_cols-win.getyx()[1], curses.A_BOLD | Color.YELLOW)
|
text = Symbol.clean(' {flair}'.format(**data))
|
||||||
text = ' {created} {subreddit}'.format(**data)
|
win.addnstr(text, n_cols-win.getyx()[1], attr)
|
||||||
win.addnstr(clean(text), n_cols - win.getyx()[1])
|
text = Symbol.clean(' {created} {subreddit}'.format(**data))
|
||||||
|
win.addnstr(text, n_cols-win.getyx()[1])
|
||||||
|
|
||||||
row = len(data['split_title']) + 2
|
row = len(data['split_title']) + 2
|
||||||
attr = curses.A_UNDERLINE | Color.BLUE
|
attr = curses.A_UNDERLINE | Color.BLUE
|
||||||
text = '{url}'.format(**data)
|
text = Symbol.clean('{url}'.format(**data))
|
||||||
win.addnstr(row, 1, clean(text), n_cols, attr)
|
win.addnstr(row, 1, text, n_cols, attr)
|
||||||
|
|
||||||
offset = len(data['split_title']) + 3
|
offset = len(data['split_title']) + 3
|
||||||
for row, text in enumerate(data['split_text'], start=offset):
|
for row, text in enumerate(data['split_text'], start=offset):
|
||||||
win.addnstr(row, 1, clean(text), n_cols)
|
text = Symbol.clean(text)
|
||||||
|
win.addnstr(row, 1, text, n_cols)
|
||||||
|
|
||||||
row = len(data['split_title']) + len(data['split_text']) + 3
|
row = len(data['split_title']) + len(data['split_text']) + 3
|
||||||
text = '{score} {comments}'.format(**data)
|
text = Symbol.clean('{score} {comments}'.format(**data))
|
||||||
win.addnstr(row, 1, clean(text), n_cols, curses.A_BOLD)
|
win.addnstr(row, 1, text, n_cols, curses.A_BOLD)
|
||||||
|
|
||||||
win.border()
|
win.border()
|
||||||
@@ -7,9 +7,8 @@ from .errors import SubredditNameError
|
|||||||
from .page import BasePage
|
from .page import BasePage
|
||||||
from .submission import SubmissionPage
|
from .submission import SubmissionPage
|
||||||
from .content import SubredditContent
|
from .content import SubredditContent
|
||||||
from .utils import (LoadScreen, Color, text_input, display_message,
|
from .utils import (LoadScreen, Symbol, Color, text_input, display_message,
|
||||||
display_help, open_new_tab, clean)
|
display_help, open_browser)
|
||||||
|
|
||||||
|
|
||||||
# Used to keep track of browsing history across the current session
|
# Used to keep track of browsing history across the current session
|
||||||
_opened_links = set()
|
_opened_links = set()
|
||||||
@@ -54,6 +53,14 @@ class SubredditPage(BasePage):
|
|||||||
display_help(self.stdscr)
|
display_help(self.stdscr)
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
|
elif cmd == ord('a'):
|
||||||
|
self.upvote()
|
||||||
|
self.draw()
|
||||||
|
|
||||||
|
elif cmd == ord('z'):
|
||||||
|
self.downvote()
|
||||||
|
self.draw()
|
||||||
|
|
||||||
elif cmd == ord('q'):
|
elif cmd == ord('q'):
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
@@ -64,9 +71,6 @@ class SubredditPage(BasePage):
|
|||||||
self.prompt_subreddit()
|
self.prompt_subreddit()
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
else:
|
|
||||||
curses.beep()
|
|
||||||
|
|
||||||
def refresh_content(self, name=None):
|
def refresh_content(self, name=None):
|
||||||
|
|
||||||
name = name or self.content.name
|
name = name or self.content.name
|
||||||
@@ -75,19 +79,23 @@ class SubredditPage(BasePage):
|
|||||||
self.content = SubredditContent.from_name(
|
self.content = SubredditContent.from_name(
|
||||||
self.reddit, name, self.loader)
|
self.reddit, name, self.loader)
|
||||||
|
|
||||||
except (SubredditNameError, HTTPError):
|
except SubredditNameError:
|
||||||
display_message(self.stdscr, ['Invalid Subreddit'])
|
display_message(self.stdscr, ['Invalid subreddit'])
|
||||||
|
|
||||||
|
except HTTPError:
|
||||||
|
display_message(self.stdscr, ['Could not reach subreddit'])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.nav.page_index, self.nav.cursor_index = 0, 0
|
self.nav.page_index, self.nav.cursor_index = 0, 0
|
||||||
self.nav.inverted = False
|
self.nav.inverted = False
|
||||||
|
|
||||||
def prompt_subreddit(self):
|
def prompt_subreddit(self):
|
||||||
|
"Open a prompt to type in a new subreddit"
|
||||||
|
|
||||||
attr = curses.A_BOLD | Color.CYAN
|
attr = curses.A_BOLD | Color.CYAN
|
||||||
prompt = 'Enter Subreddit: /r/'
|
prompt = 'Enter Subreddit: /r/'
|
||||||
n_rows, n_cols = self.stdscr.getmaxyx()
|
n_rows, n_cols = self.stdscr.getmaxyx()
|
||||||
self.stdscr.addstr(n_rows-1, 0, clean(prompt), attr)
|
self.stdscr.addstr(n_rows-1, 0, prompt, attr)
|
||||||
self.stdscr.refresh()
|
self.stdscr.refresh()
|
||||||
window = self.stdscr.derwin(1, n_cols-len(prompt),n_rows-1, len(prompt))
|
window = self.stdscr.derwin(1, n_cols-len(prompt),n_rows-1, len(prompt))
|
||||||
window.attrset(attr)
|
window.attrset(attr)
|
||||||
@@ -108,9 +116,10 @@ class SubredditPage(BasePage):
|
|||||||
_opened_links.add(data['url_full'])
|
_opened_links.add(data['url_full'])
|
||||||
|
|
||||||
def open_link(self):
|
def open_link(self):
|
||||||
|
"Open a link with the webbrowser"
|
||||||
|
|
||||||
url = self.content.get(self.nav.absolute_index)['url_full']
|
url = self.content.get(self.nav.absolute_index)['url_full']
|
||||||
open_new_tab(url)
|
open_browser(url)
|
||||||
|
|
||||||
global _opened_links
|
global _opened_links
|
||||||
_opened_links.add(url)
|
_opened_links.add(url)
|
||||||
@@ -128,27 +137,38 @@ class SubredditPage(BasePage):
|
|||||||
n_title = len(data['split_title'])
|
n_title = len(data['split_title'])
|
||||||
for row, text in enumerate(data['split_title'], start=offset):
|
for row, text in enumerate(data['split_title'], start=offset):
|
||||||
if row in valid_rows:
|
if row in valid_rows:
|
||||||
attr = curses.A_BOLD
|
text = Symbol.clean(text)
|
||||||
win.addstr(row, 1, clean(text), attr)
|
win.addnstr(row, 1, text, n_cols-1, curses.A_BOLD)
|
||||||
|
|
||||||
row = n_title + offset
|
row = n_title + offset
|
||||||
if row in valid_rows:
|
if row in valid_rows:
|
||||||
seen = (data['url_full'] in _opened_links)
|
seen = (data['url_full'] in _opened_links)
|
||||||
link_color = Color.MAGENTA if seen else Color.BLUE
|
link_color = Color.MAGENTA if seen else Color.BLUE
|
||||||
attr = curses.A_UNDERLINE | link_color
|
attr = curses.A_UNDERLINE | link_color
|
||||||
text = '{url}'.format(**data)
|
text = Symbol.clean('{url}'.format(**data))
|
||||||
win.addnstr(row, 1, clean(text), n_cols-1, attr)
|
win.addnstr(row, 1, text, n_cols-1, attr)
|
||||||
|
|
||||||
row = n_title + offset + 1
|
row = n_title + offset + 1
|
||||||
if row in valid_rows:
|
if row in valid_rows:
|
||||||
text = '{created} {comments} {score}'.format(**data)
|
text = Symbol.clean('{score} '.format(**data))
|
||||||
win.addnstr(row, 1, clean(text), n_cols-1)
|
win.addnstr(row, 1, text, n_cols-1)
|
||||||
|
|
||||||
|
if data['likes'] is None:
|
||||||
|
text, attr = Symbol.BULLET, curses.A_BOLD
|
||||||
|
elif data['likes']:
|
||||||
|
text, attr = Symbol.UARROW, curses.A_BOLD | Color.GREEN
|
||||||
|
else:
|
||||||
|
text, attr = Symbol.DARROW, curses.A_BOLD | Color.RED
|
||||||
|
win.addnstr(text, n_cols-win.getyx()[1], attr)
|
||||||
|
|
||||||
|
text = Symbol.clean(' {created} {comments}'.format(**data))
|
||||||
|
win.addnstr(text, n_cols-win.getyx()[1])
|
||||||
|
|
||||||
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 = Symbol.clean('{author}'.format(**data))
|
||||||
win.addnstr(row, 1, clean(text), n_cols-1, curses.A_BOLD)
|
win.addnstr(row, 1, text, n_cols-1, curses.A_BOLD)
|
||||||
text = ' {subreddit}'.format(**data)
|
text = Symbol.clean(' {subreddit}'.format(**data))
|
||||||
win.addnstr(clean(text), n_cols - win.getyx()[1], Color.YELLOW)
|
win.addnstr(text, n_cols-win.getyx()[1], Color.YELLOW)
|
||||||
text = ' {flair}'.format(**data)
|
text = Symbol.clean(' {flair}'.format(**data))
|
||||||
win.addnstr(clean(text), n_cols - win.getyx()[1], Color.RED)
|
win.addnstr(text, n_cols-win.getyx()[1], Color.RED)
|
||||||
|
|||||||
60
rtv/utils.py
60
rtv/utils.py
@@ -12,9 +12,6 @@ from six.moves import configparser
|
|||||||
|
|
||||||
from .errors import EscapePressed
|
from .errors import EscapePressed
|
||||||
|
|
||||||
FORCE_ASCII = True
|
|
||||||
ESCAPE = 27
|
|
||||||
|
|
||||||
HELP = """
|
HELP = """
|
||||||
Global Commands
|
Global Commands
|
||||||
`UP/DOWN` or `j/k` : Scroll to the prev/next item
|
`UP/DOWN` or `j/k` : Scroll to the prev/next item
|
||||||
@@ -32,30 +29,43 @@ Submission Mode
|
|||||||
`RIGHT` or `l` : Fold the selected comment, or load additional comments
|
`RIGHT` or `l` : Fold the selected comment, or load additional comments
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def clean(string):
|
class Symbol(object):
|
||||||
"""
|
|
||||||
Required reading!
|
|
||||||
http://nedbatchelder.com/text/unipain.html
|
|
||||||
|
|
||||||
Python 2 input string will be a unicode type (unicode code points). Curses
|
UNICODE = False
|
||||||
will accept that if all of the points are in the ascii range. However, if
|
|
||||||
any of the code points are not valid ascii curses will throw a
|
|
||||||
UnicodeEncodeError: 'ascii' codec can't encode character, ordinal not in
|
|
||||||
range(128). However, if we encode the unicode to a utf-8 byte string and
|
|
||||||
pass that to curses, curses will render correctly.
|
|
||||||
|
|
||||||
Python 3 input string will be a string type (unicode code points). Curses
|
ESCAPE = 27
|
||||||
will accept that in all cases. However, the n character count in addnstr
|
|
||||||
will get screwed up.
|
|
||||||
|
|
||||||
"""
|
# Curses does define constants for these (e.g. curses.ACS_BULLET)
|
||||||
if six.PY2:
|
# However, they rely on using the curses.addch() function, which has been
|
||||||
string = string.encode('utf-8', 'replace')
|
# found to be buggy and a PITA to work with. By defining them as unicode
|
||||||
else:
|
# points they can be added via the more reliable curses.addstr().
|
||||||
string = string.encode('utf-8', 'replace')
|
# http://bugs.python.org/issue21088
|
||||||
pass
|
UARROW = u'\u25b2'.encode('utf-8')
|
||||||
|
DARROW = u'\u25bc'.encode('utf-8')
|
||||||
|
BULLET = u'\u2022'.encode('utf-8')
|
||||||
|
|
||||||
return string
|
@classmethod
|
||||||
|
def clean(cls, string):
|
||||||
|
"""
|
||||||
|
Required reading!
|
||||||
|
http://nedbatchelder.com/text/unipain.html
|
||||||
|
|
||||||
|
Python 2 input string will be a unicode type (unicode code points). Curses
|
||||||
|
will accept that if all of the points are in the ascii range. However, if
|
||||||
|
any of the code points are not valid ascii curses will throw a
|
||||||
|
UnicodeEncodeError: 'ascii' codec can't encode character, ordinal not in
|
||||||
|
range(128). However, if we encode the unicode to a utf-8 byte string and
|
||||||
|
pass that to curses, curses will render correctly.
|
||||||
|
|
||||||
|
Python 3 input string will be a string type (unicode code points). Curses
|
||||||
|
will accept that in all cases. However, the n character count in addnstr
|
||||||
|
will get screwed up.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
encoding = 'utf-8' if cls.UNICODE else 'ascii'
|
||||||
|
string = string.encode(encoding, 'replace')
|
||||||
|
return string
|
||||||
|
|
||||||
class Color(object):
|
class Color(object):
|
||||||
|
|
||||||
@@ -123,7 +133,7 @@ def text_input(window):
|
|||||||
def validate(ch):
|
def validate(ch):
|
||||||
"Filters characters for special key sequences"
|
"Filters characters for special key sequences"
|
||||||
|
|
||||||
if ch == ESCAPE:
|
if ch == Symbol.ESCAPE:
|
||||||
raise EscapePressed
|
raise EscapePressed
|
||||||
|
|
||||||
# Fix backspace for iterm
|
# Fix backspace for iterm
|
||||||
@@ -245,7 +255,7 @@ class LoadScreen(object):
|
|||||||
window.refresh()
|
window.refresh()
|
||||||
time.sleep(interval)
|
time.sleep(interval)
|
||||||
|
|
||||||
def open_new_tab(url):
|
def open_browser(url):
|
||||||
"""
|
"""
|
||||||
Call webbrowser.open_new_tab(url) and redirect stdout/stderr to devnull.
|
Call webbrowser.open_new_tab(url) and redirect stdout/stderr to devnull.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user