Implemented unicode sandwich.
This commit is contained in:
@@ -3,25 +3,10 @@ from datetime import datetime
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
import praw
|
import praw
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from .errors import SubmissionURLError, SubredditNameError
|
from .errors import SubmissionURLError, SubredditNameError
|
||||||
|
|
||||||
FORCE_ASCII = True
|
|
||||||
|
|
||||||
def clean(string):
|
|
||||||
|
|
||||||
encoding = 'ascii' if FORCE_ASCII else 'utf-8'
|
|
||||||
|
|
||||||
if six.PY2:
|
|
||||||
out = string.encode(encoding, 'replace')
|
|
||||||
else:
|
|
||||||
out = string.encode().decode(encoding, 'replace')
|
|
||||||
|
|
||||||
out = out.replace('\\', '')
|
|
||||||
return out
|
|
||||||
|
|
||||||
def split_text(big_text, width):
|
def split_text(big_text, width):
|
||||||
return [
|
return [
|
||||||
text
|
text
|
||||||
@@ -32,14 +17,13 @@ def split_text(big_text, width):
|
|||||||
for text in (textwrap.wrap(line, width=width) or [''])
|
for text in (textwrap.wrap(line, width=width) or [''])
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def strip_subreddit_url(permalink):
|
def strip_subreddit_url(permalink):
|
||||||
"""
|
"""
|
||||||
Grab the subreddit from the permalink because submission.subreddit.url
|
Grab the subreddit from the permalink because submission.subreddit.url
|
||||||
makes a seperate call to the API.
|
makes a seperate call to the API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
subreddit = clean(permalink).split('/')[4]
|
subreddit = permalink.split('/')[4]
|
||||||
return '/r/{}'.format(subreddit)
|
return '/r/{}'.format(subreddit)
|
||||||
|
|
||||||
|
|
||||||
@@ -131,13 +115,12 @@ class BaseContent(object):
|
|||||||
data['body'] = 'More comments'.format(comment.count)
|
data['body'] = 'More comments'.format(comment.count)
|
||||||
else:
|
else:
|
||||||
data['type'] = 'Comment'
|
data['type'] = 'Comment'
|
||||||
data['body'] = clean(comment.body)
|
data['body'] = comment.body
|
||||||
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'] = (clean(comment.author.name) if
|
data['author'] = (comment.author.name if
|
||||||
getattr(comment, 'author') else '[deleted]')
|
getattr(comment, 'author') else '[deleted]')
|
||||||
|
sub_author = (comment.submission.author.name if
|
||||||
sub_author = (clean(comment.submission.author.name) if
|
|
||||||
getattr(comment.submission, 'author') else '[deleted]')
|
getattr(comment.submission, 'author') else '[deleted]')
|
||||||
data['is_author'] = (data['author'] == sub_author)
|
data['is_author'] = (data['author'] == sub_author)
|
||||||
|
|
||||||
@@ -155,17 +138,17 @@ class BaseContent(object):
|
|||||||
data = {}
|
data = {}
|
||||||
data['object'] = sub
|
data['object'] = sub
|
||||||
data['type'] = 'Submission'
|
data['type'] = 'Submission'
|
||||||
data['title'] = clean(sub.title)
|
data['title'] = sub.title
|
||||||
data['text'] = clean(sub.selftext)
|
data['text'] = sub.selftext
|
||||||
data['created'] = humanize_timestamp(sub.created_utc)
|
data['created'] = humanize_timestamp(sub.created_utc)
|
||||||
data['comments'] = '{} comments'.format(sub.num_comments)
|
data['comments'] = '{} comments'.format(sub.num_comments)
|
||||||
data['score'] = '{} pts'.format(sub.score)
|
data['score'] = '{} pts'.format(sub.score)
|
||||||
data['author'] = (clean(sub.author.name) if getattr(sub, 'author')
|
data['author'] = (sub.author.name if getattr(sub, 'author')
|
||||||
else '[deleted]')
|
else '[deleted]')
|
||||||
data['permalink'] = clean(sub.permalink)
|
data['permalink'] = sub.permalink
|
||||||
data['subreddit'] = strip_subreddit_url(sub.permalink)
|
data['subreddit'] = strip_subreddit_url(sub.permalink)
|
||||||
data['url_full'] = clean(sub.url)
|
data['url_full'] = sub.url
|
||||||
data['url'] = ('selfpost' if is_selfpost(sub.url) else clean(sub.url))
|
data['url'] = ('selfpost' if is_selfpost(sub.url) else sub.url)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -359,11 +342,11 @@ class SubredditContent(BaseContent):
|
|||||||
# there is is no other way to check things like multireddits that
|
# there is is no other way to check things like multireddits that
|
||||||
# don't have a real corresponding subreddit object.
|
# don't have a real corresponding subreddit object.
|
||||||
content = cls(display_name, submissions, loader)
|
content = cls(display_name, submissions, loader)
|
||||||
try:
|
#try:
|
||||||
content.get(0)
|
content.get(0)
|
||||||
except:
|
#except:
|
||||||
# TODO: Trap specific errors
|
# # TODO: Trap specific errors
|
||||||
raise SubredditNameError(display_name)
|
# raise SubredditNameError(display_name)
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ 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 content
|
from . import utils
|
||||||
from .errors import SubmissionURLError, SubredditNameError
|
from .errors import SubmissionURLError, SubredditNameError
|
||||||
from .utils import curses_session, load_config, HELP
|
from .utils import curses_session, load_config, HELP
|
||||||
from .subreddit import SubredditPage
|
from .subreddit import SubredditPage
|
||||||
@@ -64,7 +64,7 @@ def main():
|
|||||||
if getattr(args, key) is None:
|
if getattr(args, key) is None:
|
||||||
setattr(args, key, val)
|
setattr(args, key, val)
|
||||||
|
|
||||||
content.FORCE_ASCII = args.force_ascii
|
utils.FORCE_ASCII = args.force_ascii
|
||||||
|
|
||||||
if args.subreddit is None:
|
if args.subreddit is None:
|
||||||
args.subreddit = 'front'
|
args.subreddit = 'front'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import curses
|
import curses
|
||||||
|
|
||||||
from .utils import Color
|
from .utils import Color, clean
|
||||||
|
|
||||||
class Navigator(object):
|
class Navigator(object):
|
||||||
"""
|
"""
|
||||||
@@ -157,14 +157,15 @@ 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, sub_name, n_cols-1)
|
self._header_window.addnstr(0, 0, 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
|
||||||
s_col = (n_cols - len(username) - 1)
|
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):
|
if (s_col - 1) >= len(sub_name):
|
||||||
self._header_window.addnstr(0, s_col, username, (n_cols-s_col-1))
|
n = (n_cols - s_col - 1)
|
||||||
|
self._header_window.addnstr(0, s_col, clean(username), n)
|
||||||
|
|
||||||
self._header_window.refresh()
|
self._header_window.refresh()
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ 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
|
from .utils import LoadScreen, Color, ESCAPE, display_help, open_new_tab, clean
|
||||||
|
|
||||||
class SubmissionPage(BasePage):
|
class SubmissionPage(BasePage):
|
||||||
|
|
||||||
@@ -99,7 +99,8 @@ class SubmissionPage(BasePage):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def draw_comment(win, data, inverted=False):
|
def draw_comment(win, data, inverted=False):
|
||||||
|
import logging
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
n_rows, n_cols = win.getmaxyx()
|
n_rows, n_cols = win.getmaxyx()
|
||||||
n_cols -= 1
|
n_cols -= 1
|
||||||
|
|
||||||
@@ -112,14 +113,14 @@ class SubmissionPage(BasePage):
|
|||||||
text = '{author}'.format(**data)
|
text = '{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, text, n_cols-1, attr)
|
win.addnstr(row, 1, clean(text), n_cols-1, attr)
|
||||||
text = ' {score} {created}'.format(**data)
|
text = ' {score} {created}'.format(**data)
|
||||||
win.addnstr(text, n_cols - win.getyx()[1])
|
win.addnstr(clean(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, text, n_cols-1)
|
win.addnstr(row, 1, clean(text), n_cols-50)
|
||||||
|
|
||||||
# Vertical line, unfortunately vline() doesn't support custom color so
|
# Vertical line, unfortunately vline() doesn't support custom color so
|
||||||
# we have to build it one chr at a time.
|
# we have to build it one chr at a time.
|
||||||
@@ -145,9 +146,9 @@ class SubmissionPage(BasePage):
|
|||||||
n_rows, n_cols = win.getmaxyx()
|
n_rows, n_cols = win.getmaxyx()
|
||||||
n_cols -= 1
|
n_cols -= 1
|
||||||
text = '{body}'.format(**data)
|
text = '{body}'.format(**data)
|
||||||
win.addnstr(0, 1, text, n_cols-1)
|
win.addnstr(0, 1, clean(text), n_cols-1)
|
||||||
text = ' [{count}]'.format(**data)
|
text = ' [{count}]'.format(**data)
|
||||||
win.addnstr(text, n_cols - win.getyx()[1], curses.A_BOLD)
|
win.addnstr(clean(text), n_cols - win.getyx()[1], curses.A_BOLD)
|
||||||
|
|
||||||
attr = Color.get_level(data['level'])
|
attr = Color.get_level(data['level'])
|
||||||
for y in range(n_rows):
|
for y in range(n_rows):
|
||||||
@@ -167,26 +168,26 @@ 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, text, n_cols, curses.A_BOLD)
|
win.addnstr(row, 1, clean(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 = '{author}'.format(**data)
|
||||||
win.addnstr(row, 1, text, n_cols, attr)
|
win.addnstr(row, 1, clean(text), n_cols, attr)
|
||||||
text = ' {created} {subreddit}'.format(**data)
|
text = ' {created} {subreddit}'.format(**data)
|
||||||
win.addnstr(text, n_cols - win.getyx()[1])
|
win.addnstr(clean(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 = '{url}'.format(**data)
|
||||||
win.addnstr(row, 1, text, n_cols, attr)
|
win.addnstr(row, 1, clean(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, text, n_cols)
|
win.addnstr(row, 1, clean(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 = '{score} {comments}'.format(**data)
|
||||||
win.addnstr(row, 1, text, n_cols, curses.A_BOLD)
|
win.addnstr(row, 1, clean(text), n_cols, curses.A_BOLD)
|
||||||
|
|
||||||
win.border()
|
win.border()
|
||||||
@@ -8,7 +8,7 @@ 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, Color, text_input, display_message,
|
||||||
display_help, open_new_tab)
|
display_help, open_new_tab, clean)
|
||||||
|
|
||||||
|
|
||||||
# Used to keep track of browsing history across the current session
|
# Used to keep track of browsing history across the current session
|
||||||
@@ -87,7 +87,7 @@ class SubredditPage(BasePage):
|
|||||||
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, prompt, attr)
|
self.stdscr.addstr(n_rows-1, 0, clean(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)
|
||||||
@@ -129,7 +129,7 @@ class SubredditPage(BasePage):
|
|||||||
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
|
attr = curses.A_BOLD
|
||||||
win.addstr(row, 1, text, attr)
|
win.addstr(row, 1, clean(text), attr)
|
||||||
|
|
||||||
row = n_title + offset
|
row = n_title + offset
|
||||||
if row in valid_rows:
|
if row in valid_rows:
|
||||||
@@ -137,16 +137,16 @@ class SubredditPage(BasePage):
|
|||||||
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 = '{url}'.format(**data)
|
||||||
win.addnstr(row, 1, text, n_cols-1, attr)
|
win.addnstr(row, 1, clean(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 = '{created} {comments} {score}'.format(**data)
|
||||||
win.addnstr(row, 1, text, n_cols-1)
|
win.addnstr(row, 1, clean(text), n_cols-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 = '{author}'.format(**data)
|
||||||
win.addnstr(row, 1, text, n_cols-1, curses.A_BOLD)
|
win.addnstr(row, 1, clean(text), n_cols-1, curses.A_BOLD)
|
||||||
text = ' {subreddit}'.format(**data)
|
text = ' {subreddit}'.format(**data)
|
||||||
win.addnstr(text, n_cols - win.getyx()[1], Color.YELLOW)
|
win.addnstr(clean(text), n_cols - win.getyx()[1], Color.YELLOW)
|
||||||
|
|||||||
27
rtv/utils.py
27
rtv/utils.py
@@ -7,10 +7,12 @@ import threading
|
|||||||
from curses import textpad, ascii
|
from curses import textpad, ascii
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
import six
|
||||||
from six.moves import configparser
|
from six.moves import configparser
|
||||||
|
|
||||||
from .errors import EscapePressed
|
from .errors import EscapePressed
|
||||||
|
|
||||||
|
FORCE_ASCII = True
|
||||||
ESCAPE = 27
|
ESCAPE = 27
|
||||||
|
|
||||||
HELP = """
|
HELP = """
|
||||||
@@ -30,6 +32,31 @@ 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):
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if six.PY2:
|
||||||
|
string = string.encode('utf-8', 'replace')
|
||||||
|
else:
|
||||||
|
string = string.encode('utf-8', 'replace')
|
||||||
|
pass
|
||||||
|
|
||||||
|
return string
|
||||||
|
|
||||||
class Color(object):
|
class Color(object):
|
||||||
|
|
||||||
COLORS = {
|
COLORS = {
|
||||||
|
|||||||
Reference in New Issue
Block a user