This commit is contained in:
Michael Lazar
2015-10-16 11:40:43 -07:00
10 changed files with 55 additions and 33 deletions

View File

@@ -4,7 +4,7 @@ RTV: Reddit Terminal Viewer
RTV is an application that allows you to view and interact with reddit from your terminal. It is compatible with *most* terminal emulators on Linux and OSX.
.. image:: http://i.imgur.com/W1hxqCt.png
.. image:: http://i.imgur.com/xpOEi1E.png
RTV is built in **python** using the **curses** library.
@@ -201,7 +201,7 @@ How do I run the code directly using python?
This project is structured to be run as a python *module*. This means that in order to resolve imports you need to launch using python's ``-m`` flag. This method works for all versions of python. Follow the example below, which assumes that you have cloned the repository into the directory **~/rtv_project**.
.. code-block:: bash
$ cd ~/rtv_project
$ python2 -m rtv
$ python3 -m rtv
@@ -225,7 +225,7 @@ License
Please see `LICENSE <https://github.com/michael-lazar/rtv/blob/master/LICENSE>`_.
.. |python| image:: https://img.shields.io/badge/python-2.7%2C%203.4-blue.svg?style=flat-square
.. |python| image:: https://img.shields.io/badge/python-2.7%2C%203.5-blue.svg?style=flat-square
:target: https://pypi.python.org/pypi/rtv/
:alt: Supported Python versions

View File

@@ -1,4 +1,3 @@
import os
import sys
import locale
import logging
@@ -6,7 +5,6 @@ import logging
import praw
import praw.errors
import tornado
import requests
from requests import exceptions
from . import config
@@ -28,6 +26,7 @@ _logger = logging.getLogger(__name__)
# ptrace_scope to 0 in /etc/sysctl.d/10-ptrace.conf.
# http://blog.mellenthin.de/archives/2010/10/18/gdb-attach-fails
def main():
"Main entry point"
@@ -77,7 +76,8 @@ def main():
subreddit = args.subreddit or 'front'
page = SubredditPage(stdscr, reddit, oauth, subreddit)
page.loop()
except (exceptions.RequestException, praw.errors.PRAWException, RTVError) as e:
except (exceptions.RequestException, praw.errors.PRAWException,
RTVError) as e:
_logger.exception(e)
print('{}: {}'.format(type(e).__name__, e))
except KeyboardInterrupt:

View File

@@ -21,8 +21,10 @@ oauth_client_id = 'E2oEtRQfdfAfNQ'
oauth_client_secret = 'praw_gapfill'
oauth_redirect_uri = 'http://127.0.0.1:65000/'
oauth_redirect_port = 65000
oauth_scope = ['edit', 'history', 'identity', 'mysubreddits', 'privatemessages',
'read', 'report', 'save', 'submit', 'subscribe', 'vote']
oauth_scope = ['edit', 'history', 'identity', 'mysubreddits',
'privatemessages', 'read', 'report', 'save', 'submit',
'subscribe', 'vote']
def build_parser():
parser = argparse.ArgumentParser(
@@ -51,6 +53,7 @@ def build_parser():
help='Remove any saved OAuth tokens before starting')
return parser
def load_config():
"""
Attempt to load settings from the local config file.
@@ -74,6 +77,7 @@ def load_config():
return config_dict
def load_refresh_token(filename=TOKEN):
if os.path.exists(filename):
with open(filename) as fp:
@@ -81,10 +85,12 @@ def load_refresh_token(filename=TOKEN):
else:
return None
def save_refresh_token(token, filename=TOKEN):
with open(filename, 'w+') as fp:
fp.write(token)
def clear_refresh_token(filename=TOKEN):
if os.path.exists(filename):
os.remove(filename)

View File

@@ -4,7 +4,8 @@ import praw
import requests
import re
from .exceptions import SubmissionError, SubredditError, SubscriptionError, AccountError
from .exceptions import (SubmissionError, SubredditError, SubscriptionError,
AccountError)
from .helpers import humanize_timestamp, wrap_text, strip_subreddit_url
__all__ = ['SubredditContent', 'SubmissionContent', 'SubscriptionContent']
@@ -112,7 +113,8 @@ class BaseContent(object):
"selfpost" or "x-post" or a link.
"""
reddit_link = re.compile("https?://(www\.)?(np\.)?redd(it\.com|\.it)/r/.*")
reddit_link = re.compile(
"https?://(www\.)?(np\.)?redd(it\.com|\.it)/r/.*")
author = getattr(sub, 'author', '[deleted]')
name = getattr(author, 'name', '[deleted]')
flair = getattr(sub, 'link_flair_text', '')

View File

@@ -24,17 +24,18 @@ def clean(string, n_cols=None):
http://nedbatchelder.com/text/unipain.html
Python 2 input string will be a unicode type (unicode code points). Curses
will accept unicode 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
will accept unicode 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). If we encode the unicode to a utf-8 byte string and pass that to
curses, it will render correctly.
range(128). If we encode the unicode to a utf-8 byte string and pass that
to curses, it 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 not be correct. If code points are passed to addnstr, curses will treat
each code point as one character and will not account for wide characters.
If utf-8 is passed in, addnstr will treat each 'byte' as a single character.
will not be correct. If code points are passed to addnstr, curses will
treat each code point as one character and will not account for wide
characters. If utf-8 is passed in, addnstr will treat each 'byte' as a
single character.
"""
if n_cols is not None and n_cols <= 0:
@@ -124,7 +125,8 @@ def check_browser_display():
# Use the convention defined here to parse $BROWSER
# https://docs.python.org/2/library/webbrowser.html
console_browsers = ['www-browser', 'links', 'links2', 'elinks', 'lynx', 'w3m']
console_browsers = ['www-browser', 'links', 'links2', 'elinks', 'lynx',
'w3m']
if "BROWSER" in os.environ:
user_browser = os.environ["BROWSER"].split(os.pathsep)[0]
if user_browser in console_browsers:
@@ -138,7 +140,8 @@ def check_browser_display():
def wrap_text(text, width):
"""
Wrap text paragraphs to the given character width while preserving newlines.
Wrap text paragraphs to the given character width while preserving
newlines.
"""
out = []
for paragraph in text.splitlines():

View File

@@ -9,7 +9,8 @@ def history_path():
Create the path to the history log
"""
HOME = os.path.expanduser('~')
XDG_CONFIG_HOME = os.getenv('XDG_CACHE_HOME', os.path.join(HOME, '.config'))
XDG_CONFIG_HOME = os.getenv('XDG_CACHE_HOME',
os.path.join(HOME, '.config'))
path = os.path.join(XDG_CONFIG_HOME, 'rtv')
if not os.path.exists(path):
os.makedirs(path)

View File

@@ -7,7 +7,7 @@ from tornado import gen, ioloop, web, httpserver
from concurrent.futures import ThreadPoolExecutor
from . import config
from .curses_helpers import show_notification, prompt_input
from .curses_helpers import show_notification
from .helpers import check_browser_display, open_browser
__all__ = ['OAuthTool']
@@ -18,6 +18,7 @@ oauth_error = None
template_path = os.path.join(os.path.dirname(__file__), 'templates')
class AuthHandler(web.RequestHandler):
def get(self):
@@ -27,12 +28,14 @@ class AuthHandler(web.RequestHandler):
oauth_code = self.get_argument('code', default='placeholder')
oauth_error = self.get_argument('error', default='placeholder')
self.render('index.html', state=oauth_state, code=oauth_code, error=oauth_error)
self.render('index.html', state=oauth_state, code=oauth_code,
error=oauth_error)
# Stop IOLoop if using a background browser such as firefox
if check_browser_display():
ioloop.IOLoop.current().stop()
class OAuthTool(object):
def __init__(self, reddit, stdscr=None, loader=None):
@@ -46,7 +49,8 @@ class OAuthTool(object):
# Initialize Tornado webapp
routes = [('/', AuthHandler)]
self.callback_app = web.Application(routes, template_path=template_path)
self.callback_app = web.Application(routes,
template_path=template_path)
self.reddit.set_oauth_app_info(config.oauth_client_id,
config.oauth_client_secret,

View File

@@ -117,7 +117,7 @@ class Navigator(object):
self.page_index += (self.step * (n_windows-1))
self.inverted = not self.inverted
self.cursor_index \
= (n_windows-(direction<0)) - self.cursor_index
= (n_windows-(direction < 0)) - self.cursor_index
valid = False
adj = 0
@@ -565,7 +565,8 @@ class BasePage(object):
if not valid:
curses.flash()
# Note: ACS_VLINE doesn't like changing the attribute, so always redraw.
# Note: ACS_VLINE doesn't like changing the attribute,
# so always redraw.
self._draw_content()
self._add_cursor()
@@ -575,7 +576,8 @@ class BasePage(object):
if not valid:
curses.flash()
# Note: ACS_VLINE doesn't like changing the attribute, so always redraw.
# Note: ACS_VLINE doesn't like changing the attribute,
# so always redraw.
self._draw_content()
self._add_cursor()

View File

@@ -10,7 +10,7 @@ from .page import BasePage, Navigator, BaseController
from .submission import SubmissionPage
from .subscription import SubscriptionPage
from .content import SubredditContent
from .helpers import open_browser, open_editor, strip_subreddit_url
from .helpers import open_browser, open_editor
from .docs import SUBMISSION_FILE
from .history import load_history, save_history
from .curses_helpers import (Color, LoadScreen, add_line, get_arrow, get_gold,
@@ -105,7 +105,8 @@ class SubredditPage(BasePage):
"Select the current submission to view posts"
data = self.content.get(self.nav.absolute_index)
page = SubmissionPage(self.stdscr, self.reddit, self.oauth, url=data['permalink'])
page = SubmissionPage(self.stdscr, self.reddit, self.oauth,
url=data['permalink'])
page.loop()
if data['url_type'] == 'selfpost':
global history
@@ -120,7 +121,8 @@ class SubredditPage(BasePage):
global history
history.add(url)
if data['url_type'] in ['x-post', 'selfpost']:
page = SubmissionPage(self.stdscr, self.reddit, self.oauth, url=url)
page = SubmissionPage(self.stdscr, self.reddit, self.oauth,
url=url)
page.loop()
else:
open_browser(url)
@@ -162,7 +164,8 @@ class SubredditPage(BasePage):
time.sleep(2.0)
# Open the newly created post
s.catch = False
page = SubmissionPage(self.stdscr, self.reddit, self.oauth, submission=post)
page = SubmissionPage(self.stdscr, self.reddit, self.oauth,
submission=post)
page.loop()
self.refresh_content()

View File

@@ -1,6 +1,4 @@
import curses
import sys
import time
import logging
from .content import SubscriptionContent
@@ -10,9 +8,11 @@ from .curses_helpers import (Color, LoadScreen, add_line)
__all__ = ['SubscriptionController', 'SubscriptionPage']
_logger = logging.getLogger(__name__)
class SubscriptionController(BaseController):
character_map = {}
class SubscriptionPage(BasePage):
def __init__(self, stdscr, reddit, oauth):
@@ -44,7 +44,8 @@ class SubscriptionPage(BasePage):
def store_selected_subreddit(self):
"Store the selected subreddit and return to the subreddit page"
self.selected_subreddit_data = self.content.get(self.nav.absolute_index)
self.selected_subreddit_data = self.content.get(
self.nav.absolute_index)
self.active = False
@SubscriptionController.register(curses.KEY_LEFT, 'h', 's')