177 lines
5.7 KiB
Python
177 lines
5.7 KiB
Python
import sys
|
|
import os
|
|
import subprocess
|
|
from datetime import datetime
|
|
from tempfile import NamedTemporaryFile
|
|
|
|
# kitchen solves deficiencies in textwrap's handling of unicode characters
|
|
from kitchen.text.display import wrap, textual_width_chop
|
|
import six
|
|
|
|
from . import config
|
|
from .exceptions import ProgramError
|
|
|
|
__all__ = ['open_browser', 'clean', 'wrap_text', 'strip_textpad',
|
|
'strip_subreddit_url', 'humanize_timestamp', 'open_editor']
|
|
|
|
def open_editor(data=''):
|
|
"""
|
|
Open a temporary file using the system's default editor.
|
|
|
|
The data string will be written to the file before opening. This function
|
|
will block until the editor has closed. At that point the file will be
|
|
read and and lines starting with '#' will be stripped.
|
|
"""
|
|
|
|
with NamedTemporaryFile(prefix='rtv-', suffix='.txt', mode='w') as fp:
|
|
fp.write(data)
|
|
fp.flush()
|
|
editor = os.getenv('RTV_EDITOR') or os.getenv('EDITOR') or 'nano'
|
|
|
|
try:
|
|
subprocess.Popen([editor, fp.name]).wait()
|
|
except OSError as e:
|
|
raise ProgramError(editor)
|
|
|
|
# Open a second file object to read. This appears to be necessary in
|
|
# order to read the changes made by some editors (gedit). w+ mode does
|
|
# not work!
|
|
with open(fp.name) as fp2:
|
|
text = ''.join(line for line in fp2 if not line.startswith('#'))
|
|
text = text.rstrip()
|
|
|
|
return text
|
|
|
|
|
|
def open_browser(url):
|
|
"""
|
|
Call webbrowser.open_new_tab(url) and redirect stdout/stderr to devnull.
|
|
|
|
This is a workaround to stop firefox from spewing warning messages to the
|
|
console. See http://bugs.python.org/issue22277 for a better description
|
|
of the problem.
|
|
"""
|
|
command = "import webbrowser; webbrowser.open_new_tab('%s')" % url
|
|
args = [sys.executable, '-c', command]
|
|
with open(os.devnull, 'ab+', 0) as null:
|
|
subprocess.check_call(args, stdout=null, stderr=null)
|
|
|
|
|
|
def clean(string, n_cols=None):
|
|
"""
|
|
Required reading!
|
|
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
|
|
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.
|
|
|
|
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.
|
|
"""
|
|
|
|
if n_cols is not None and n_cols <= 0:
|
|
return ''
|
|
|
|
if not config.unicode:
|
|
if six.PY3 or isinstance(string, unicode):
|
|
string = string.encode('ascii', 'replace')
|
|
return string[:n_cols] if n_cols else string
|
|
else:
|
|
if n_cols:
|
|
string = textual_width_chop(string, n_cols)
|
|
if six.PY3 or isinstance(string, unicode):
|
|
string = string.encode('utf-8')
|
|
return string
|
|
|
|
|
|
def wrap_text(text, width):
|
|
"""
|
|
Wrap text paragraphs to the given character width while preserving newlines.
|
|
"""
|
|
out = []
|
|
for paragraph in text.splitlines():
|
|
# Wrap returns an empty list when paragraph is a newline. In order to
|
|
# preserve newlines we substitute a list containing an empty string.
|
|
lines = wrap(paragraph, width=width) or ['']
|
|
out.extend(lines)
|
|
return out
|
|
|
|
|
|
def strip_textpad(text):
|
|
"""
|
|
Attempt to intelligently strip excess whitespace from the output of a
|
|
curses textpad.
|
|
"""
|
|
|
|
if text is None:
|
|
return text
|
|
|
|
# Trivial case where the textbox is only one line long.
|
|
if '\n' not in text:
|
|
return text.rstrip()
|
|
|
|
# Allow one space at the end of the line. If there is more than one space,
|
|
# assume that a newline operation was intended by the user
|
|
stack, current_line = [], ''
|
|
for line in text.split('\n'):
|
|
if line.endswith(' '):
|
|
stack.append(current_line + line.rstrip())
|
|
current_line = ''
|
|
else:
|
|
current_line += line
|
|
stack.append(current_line)
|
|
|
|
# Prune empty lines at the bottom of the textbox.
|
|
for item in stack[::-1]:
|
|
if len(item) == 0:
|
|
stack.pop()
|
|
else:
|
|
break
|
|
|
|
out = '\n'.join(stack)
|
|
return out
|
|
|
|
|
|
def strip_subreddit_url(permalink):
|
|
"""
|
|
Strip a subreddit name from the subreddit's permalink.
|
|
|
|
This is used to avoid submission.subreddit.url making a seperate API call.
|
|
"""
|
|
|
|
subreddit = permalink.split('/')[4]
|
|
return '/r/{}'.format(subreddit)
|
|
|
|
|
|
def humanize_timestamp(utc_timestamp, verbose=False):
|
|
"""
|
|
Convert a utc timestamp into a human readable relative-time.
|
|
"""
|
|
|
|
timedelta = datetime.utcnow() - datetime.utcfromtimestamp(utc_timestamp)
|
|
|
|
seconds = int(timedelta.total_seconds())
|
|
if seconds < 60:
|
|
return 'moments ago' if verbose else '0min'
|
|
minutes = seconds // 60
|
|
if minutes < 60:
|
|
return ('%d minutes ago' % minutes) if verbose else ('%dmin' % minutes)
|
|
hours = minutes // 60
|
|
if hours < 24:
|
|
return ('%d hours ago' % hours) if verbose else ('%dhr' % hours)
|
|
days = hours // 24
|
|
if days < 30:
|
|
return ('%d days ago' % days) if verbose else ('%dday' % days)
|
|
months = days // 30.4
|
|
if months < 12:
|
|
return ('%d months ago' % months) if verbose else ('%dmonth' % months)
|
|
years = months // 12
|
|
return ('%d years ago' % years) if verbose else ('%dyr' % years)
|