206 lines
5.9 KiB
Python
206 lines
5.9 KiB
Python
import os
|
|
import curses
|
|
import time
|
|
import threading
|
|
from curses import textpad
|
|
from datetime import datetime, timedelta
|
|
from contextlib import contextmanager
|
|
|
|
class EscapePressed(Exception):
|
|
pass
|
|
|
|
|
|
class LoadScreen(object):
|
|
|
|
def __init__(
|
|
self,
|
|
stdscr,
|
|
message='Downloading',
|
|
trail='...',
|
|
delay=0.5,
|
|
interval=0.4):
|
|
|
|
self._stdscr = stdscr
|
|
self.message = message
|
|
self.delay = delay
|
|
self.interval=interval
|
|
self.trail = trail
|
|
|
|
message_len = len(self.message) + len(self.trail)
|
|
n_rows, n_cols = stdscr.getmaxyx()
|
|
s_row = (n_rows - 2) / 2
|
|
s_col = (n_cols - message_len - 1) / 2
|
|
self.window = stdscr.derwin(3, message_len+2, s_row, s_col)
|
|
|
|
self._animator = threading.Thread(target=self.animate)
|
|
self._animator.daemon = True
|
|
self._is_running = None
|
|
|
|
def __enter__(self):
|
|
|
|
self._is_running = True
|
|
self._animator.start()
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
|
|
self._is_running = False
|
|
self._animator.join()
|
|
|
|
del self.window
|
|
self._stdscr.refresh()
|
|
|
|
def animate(self):
|
|
|
|
# Delay before popping up the animation to avoid flashing
|
|
# the screen if the load time is effectively zero
|
|
start = time.time()
|
|
while (time.time() - start) < self.delay:
|
|
if not self._is_running:
|
|
return
|
|
|
|
while True:
|
|
for i in xrange(len(self.trail)+1):
|
|
|
|
if not self._is_running:
|
|
return
|
|
|
|
self.window.erase()
|
|
self.window.border()
|
|
self.window.addstr(1, 1, self.message + self.trail[:i])
|
|
self.window.refresh()
|
|
time.sleep(self.interval)
|
|
|
|
|
|
def clean(unicode_string):
|
|
"""
|
|
Convert unicode string into ascii-safe characters.
|
|
"""
|
|
|
|
return unicode_string.encode('ascii', 'replace').replace('\\', '')
|
|
|
|
|
|
def strip_subreddit_url(permalink):
|
|
"""
|
|
Grab the subreddit from the permalink because submission.subreddit.url
|
|
makes a seperate call to the API.
|
|
"""
|
|
|
|
subreddit = clean(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)
|
|
|
|
|
|
def validate(ch):
|
|
"Filters characters for special key sequences"
|
|
if ch == 27:
|
|
raise EscapePressed
|
|
return ch
|
|
|
|
def text_input(window):
|
|
"""
|
|
Transform a window into a text box that will accept user input and loop
|
|
until an escape sequence is entered.
|
|
|
|
If enter is pressed, return the input text as a string.
|
|
If escape is pressed, return None.
|
|
"""
|
|
|
|
window.clear()
|
|
curses.curs_set(2)
|
|
textbox = textpad.Textbox(window, insert_mode=True)
|
|
|
|
# Wrapping in an exception block so that we can distinguish when the user
|
|
# hits the return character from when the user tries to back out of the
|
|
# input.
|
|
try:
|
|
out = textbox.edit(validate=validate)
|
|
out = out.strip()
|
|
except EscapePressed:
|
|
out = None
|
|
|
|
curses.curs_set(0)
|
|
return out
|
|
|
|
@contextmanager
|
|
def curses_session():
|
|
|
|
try:
|
|
# Curses must wait for some time after the Escape key is pressed to see
|
|
# check if it is the beginning of an escape sequence indicating a
|
|
# special key. The default wait time is 1 second, which means that
|
|
# getch() will not return the escape key (ord(27)), until a full second
|
|
# after it has been pressed. Turn this down to 25 ms, which is close to
|
|
# what VIM uses.
|
|
# http://stackoverflow.com/questions/27372068
|
|
os.environ['ESCDELAY'] = '25'
|
|
|
|
# Initialize curses
|
|
stdscr = curses.initscr()
|
|
|
|
# Turn off echoing of keys, and enter cbreak mode,
|
|
# where no buffering is performed on keyboard input
|
|
curses.noecho()
|
|
curses.cbreak()
|
|
|
|
# In keypad mode, escape sequences for special keys
|
|
# (like the cursor keys) will be interpreted and
|
|
# a special value like curses.KEY_LEFT will be returned
|
|
stdscr.keypad(1)
|
|
|
|
# Start color, too. Harmless if the terminal doesn't have
|
|
# color; user can test with has_color() later on. The try/catch
|
|
# works around a minor bit of over-conscientiousness in the curses
|
|
# module -- the error return from C start_color() is ignorable.
|
|
try:
|
|
curses.start_color()
|
|
|
|
# Assign the terminal's default (background) color to code -1
|
|
curses.use_default_colors()
|
|
except:
|
|
pass
|
|
|
|
# Hide blinking cursor
|
|
curses.curs_set(0)
|
|
|
|
# Initialize color pairs - colored text on the default background
|
|
curses.init_pair(1, curses.COLOR_RED, -1)
|
|
curses.init_pair(2, curses.COLOR_GREEN, -1)
|
|
curses.init_pair(3, curses.COLOR_YELLOW, -1)
|
|
curses.init_pair(4, curses.COLOR_BLUE, -1)
|
|
curses.init_pair(5, curses.COLOR_MAGENTA, -1)
|
|
curses.init_pair(6, curses.COLOR_CYAN, -1)
|
|
|
|
yield stdscr
|
|
|
|
finally:
|
|
|
|
if stdscr is not None:
|
|
stdscr.keypad(0)
|
|
curses.echo()
|
|
curses.nocbreak()
|
|
curses.endwin() |