Moved files around, tweaked content generators. Things are in a broken state atm.
This commit is contained in:
266
rtv/content.py
266
rtv/content.py
@@ -1,9 +1,79 @@
|
|||||||
import textwrap
|
import textwrap
|
||||||
|
from datetime import datetime
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
import praw
|
import praw
|
||||||
|
|
||||||
from utils import clean, strip_subreddit_url, humanize_timestamp
|
def clean(unicode_string):
|
||||||
|
"""
|
||||||
|
Convert unicode string into ascii-safe characters.
|
||||||
|
"""
|
||||||
|
|
||||||
class ContainerBase(object):
|
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)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def default_loader(self):
|
||||||
|
yield
|
||||||
|
|
||||||
|
class BaseContent(object):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def flatten_comments(comments, root_level=0):
|
||||||
|
"""
|
||||||
|
Flatten a PRAW comment tree while preserving the nested level of each
|
||||||
|
comment via the `nested_level` attribute.
|
||||||
|
"""
|
||||||
|
|
||||||
|
stack = comments[:]
|
||||||
|
for item in stack:
|
||||||
|
item.nested_level = root_level
|
||||||
|
|
||||||
|
retval = []
|
||||||
|
while stack:
|
||||||
|
item = stack.pop(0)
|
||||||
|
nested = getattr(item, 'replies', None)
|
||||||
|
if nested:
|
||||||
|
for n in nested:
|
||||||
|
n.nested_level = item.nested_level + 1
|
||||||
|
stack[0:0] = nested
|
||||||
|
retval.append(item)
|
||||||
|
return retval
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def strip_praw_comment(comment):
|
def strip_praw_comment(comment):
|
||||||
@@ -55,97 +125,43 @@ class ContainerBase(object):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class SubredditContainer(ContainerBase):
|
|
||||||
|
class SubmissionContent(BaseContent):
|
||||||
"""
|
"""
|
||||||
Grabs a subreddit from PRAW and lazily stores submissions to an internal
|
Grab a submission from PRAW and lazily store comments to an internal
|
||||||
list for repeat access.
|
list for repeat access.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, reddit_session, subreddit='front'):
|
def __init__(
|
||||||
"""
|
self,
|
||||||
params:
|
submission,
|
||||||
session (praw.Reddit): Active reddit connection
|
loader=default_loader(),
|
||||||
subreddit (str): Subreddit to connect to, defaults to front page.
|
indent_size=2,
|
||||||
"""
|
max_indent_level=4):
|
||||||
self.r = reddit_session
|
|
||||||
self.r.config.decode_html_entities = True
|
|
||||||
|
|
||||||
self.subreddit = None
|
|
||||||
self.display_name = None
|
|
||||||
self._submissions = None
|
|
||||||
self._submission_data = None
|
|
||||||
|
|
||||||
self.reset(subreddit=subreddit)
|
|
||||||
|
|
||||||
def get(self, index, n_cols=70):
|
|
||||||
"""
|
|
||||||
Grab the `i`th submission, with the title field formatted to fit inside
|
|
||||||
of a window of width `n`
|
|
||||||
"""
|
|
||||||
|
|
||||||
if index < 0:
|
|
||||||
raise IndexError
|
|
||||||
|
|
||||||
while index >= len(self._submission_data):
|
|
||||||
|
|
||||||
try:
|
|
||||||
submission = self._submissions.next()
|
|
||||||
except StopIteration:
|
|
||||||
raise IndexError
|
|
||||||
else:
|
|
||||||
data = self.strip_praw_submission(submission)
|
|
||||||
self._submission_data.append(data)
|
|
||||||
|
|
||||||
# Modifies the original dict, faster than copying
|
|
||||||
data = self._submission_data[index]
|
|
||||||
data['split_title'] = textwrap.wrap(data['title'], width=n_cols)
|
|
||||||
data['n_rows'] = len(data['split_title']) + 3
|
|
||||||
data['offset'] = 0
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def iterate(self, index, step, n_cols):
|
|
||||||
|
|
||||||
while True:
|
|
||||||
yield self.get(index, n_cols)
|
|
||||||
index += step
|
|
||||||
|
|
||||||
def reset(self, subreddit=None):
|
|
||||||
"""
|
|
||||||
Clear the internal list and fetch a new submission generator. Switch
|
|
||||||
to the specified subreddit if one is given.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Fall back to the internal value if nothing is passed in.
|
|
||||||
self.subreddit = subreddit or self.subreddit
|
|
||||||
self._submission_data = []
|
|
||||||
|
|
||||||
if self.subreddit == 'front':
|
|
||||||
self._submissions = self.r.get_front_page(limit=None)
|
|
||||||
self.display_name = 'Front Page'
|
|
||||||
else:
|
|
||||||
sub = self.r.get_subreddit(self.subreddit)
|
|
||||||
self._submissions = sub.get_hot()
|
|
||||||
self.display_name = '/r/' + self.subreddit
|
|
||||||
|
|
||||||
|
|
||||||
class SubmissionContainer(ContainerBase):
|
|
||||||
"""
|
|
||||||
Grabs a submission from PRAW and lazily store comments to an internal
|
|
||||||
list for repeat access and to allow expanding and hiding comments.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, submission, indent_size=2, max_indent_level=4):
|
|
||||||
|
|
||||||
self.submission = submission
|
|
||||||
self.indent_size = indent_size
|
self.indent_size = indent_size
|
||||||
self.max_indent_level = max_indent_level
|
self.max_indent_level = max_indent_level
|
||||||
|
self._loader = loader
|
||||||
|
|
||||||
self.display_name = None
|
self._submission_data = self.strip_praw_submission(submission)
|
||||||
self._submission_data = None
|
self.name = self._submission_data['permalink']
|
||||||
self._comment_data = None
|
with loader:
|
||||||
|
comments = self.flatten_comments(submission.comments)
|
||||||
|
self._comment_data = [self.strip_praw_comment(c) for c in comments]
|
||||||
|
|
||||||
self.reset()
|
@classmethod
|
||||||
|
def from_url(
|
||||||
|
cls,
|
||||||
|
r,
|
||||||
|
url,
|
||||||
|
loader=default_loader(),
|
||||||
|
indent_size=2,
|
||||||
|
max_indent_level=4):
|
||||||
|
|
||||||
|
with loader:
|
||||||
|
submission = r.get_submission(url)
|
||||||
|
|
||||||
|
return cls(submission, loader, indent_size, max_indent_level)
|
||||||
|
|
||||||
def get(self, index, n_cols=70):
|
def get(self, index, n_cols=70):
|
||||||
"""
|
"""
|
||||||
@@ -211,7 +227,8 @@ class SubmissionContainer(ContainerBase):
|
|||||||
|
|
||||||
elif data['type'] == 'MoreComments':
|
elif data['type'] == 'MoreComments':
|
||||||
|
|
||||||
comments = data['object'].comments()
|
with self._loader:
|
||||||
|
comments = data['object'].comments()
|
||||||
comments = self.flatten_comments(comments, root_level=data['level'])
|
comments = self.flatten_comments(comments, root_level=data['level'])
|
||||||
comment_data = [self.strip_praw_comment(c) for c in comments]
|
comment_data = [self.strip_praw_comment(c) for c in comments]
|
||||||
self._comment_data[index:index+1] = comment_data
|
self._comment_data[index:index+1] = comment_data
|
||||||
@@ -225,36 +242,63 @@ class SubmissionContainer(ContainerBase):
|
|||||||
yield self.get(index, n_cols=n_cols)
|
yield self.get(index, n_cols=n_cols)
|
||||||
index += step
|
index += step
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
|
class SubredditContent(BaseContent):
|
||||||
|
"""
|
||||||
|
Grabs a subreddit from PRAW and lazily stores submissions to an internal
|
||||||
|
list for repeat access.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name, submission_generator, loader=default_loader()):
|
||||||
|
|
||||||
|
self.name = name
|
||||||
|
self._loader = loader
|
||||||
|
self._submissions = submission_generator
|
||||||
|
self._submission_data = []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_name(cls, r, name, loader=default_loader()):
|
||||||
|
|
||||||
|
if name == 'front':
|
||||||
|
return cls('Front Page', r.get_front_page(limit=None), loader)
|
||||||
|
|
||||||
|
if name == 'all':
|
||||||
|
sub = r.get_subreddit(name)
|
||||||
|
else:
|
||||||
|
sub = r.get_subreddit(name, fetch=True)
|
||||||
|
|
||||||
|
return cls('/r/'+sub.display_name, sub.get_hot(limit=None), loader)
|
||||||
|
|
||||||
|
def get(self, index, n_cols=70):
|
||||||
"""
|
"""
|
||||||
Fetch changes to the submission from PRAW and clear the internal list.
|
Grab the `i`th submission, with the title field formatted to fit inside
|
||||||
|
of a window of width `n`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.submission.refresh()
|
if index < 0:
|
||||||
self._submission_data = self.strip_praw_submission(self.submission)
|
raise IndexError
|
||||||
|
|
||||||
self.display_name = self._submission_data['permalink']
|
while index >= len(self._submission_data):
|
||||||
comments = self.flatten_comments(self.submission.comments)
|
|
||||||
self._comment_data = [self.strip_praw_comment(c) for c in comments]
|
|
||||||
|
|
||||||
@staticmethod
|
try:
|
||||||
def flatten_comments(comments, root_level=0):
|
with self._loader:
|
||||||
"""
|
submission = self._submissions.next()
|
||||||
Flatten a PRAW comment tree while preserving the nested level of each
|
except StopIteration:
|
||||||
comment via the `nested_level` attribute.
|
raise IndexError
|
||||||
"""
|
else:
|
||||||
|
data = self.strip_praw_submission(submission)
|
||||||
|
self._submission_data.append(data)
|
||||||
|
|
||||||
stack = comments[:]
|
# Modifies the original dict, faster than copying
|
||||||
for item in stack:
|
data = self._submission_data[index]
|
||||||
item.nested_level = root_level
|
data['split_title'] = textwrap.wrap(data['title'], width=n_cols)
|
||||||
|
data['n_rows'] = len(data['split_title']) + 3
|
||||||
|
data['offset'] = 0
|
||||||
|
|
||||||
retval = []
|
return data
|
||||||
while stack:
|
|
||||||
item = stack.pop(0)
|
def iterate(self, index, step, n_cols):
|
||||||
nested = getattr(item, 'replies', None)
|
|
||||||
if nested:
|
while True:
|
||||||
for n in nested:
|
yield self.get(index, n_cols)
|
||||||
n.nested_level = item.nested_level + 1
|
index += step
|
||||||
stack[0:0] = nested
|
|
||||||
retval.append(item)
|
|
||||||
return retval
|
|
||||||
15
rtv/main.py
15
rtv/main.py
@@ -1,8 +1,8 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import praw
|
import praw
|
||||||
from utils import curses_session
|
from utils import curses_session, LoadScreen
|
||||||
from content import SubredditContainer
|
from content import SubredditContent
|
||||||
from subreddit_viewer import SubredditViewer
|
from subreddit import SubredditPage
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Reddit Terminal Viewer')
|
parser = argparse.ArgumentParser(description='Reddit Terminal Viewer')
|
||||||
parser.add_argument('-s', dest='subreddit', default='front', help='subreddit name')
|
parser.add_argument('-s', dest='subreddit', default='front', help='subreddit name')
|
||||||
@@ -14,14 +14,17 @@ group.add_argument('-p', dest='password', help='reddit password')
|
|||||||
def main(args):
|
def main(args):
|
||||||
|
|
||||||
r = praw.Reddit(user_agent='reddit terminal viewer v0.0')
|
r = praw.Reddit(user_agent='reddit terminal viewer v0.0')
|
||||||
|
r.config.decode_html_entities = True
|
||||||
|
|
||||||
if args.username and args.password:
|
if args.username and args.password:
|
||||||
r.login(args.username, args.password)
|
r.login(args.username, args.password)
|
||||||
|
|
||||||
with curses_session() as stdscr:
|
with curses_session() as stdscr:
|
||||||
|
|
||||||
content = SubredditContainer(r, subreddit=args.subreddit)
|
loader = LoadScreen(stdscr)
|
||||||
viewer = SubredditViewer(stdscr, content)
|
content = SubredditContent(r, subreddit=args.subreddit, loader=loader)
|
||||||
viewer.loop()
|
page = SubredditPage(stdscr, content)
|
||||||
|
page.loop()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class Navigator(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class BaseViewer(object):
|
class BasePage(object):
|
||||||
"""
|
"""
|
||||||
Base terminal viewer incorperates a cursor to navigate content
|
Base terminal viewer incorperates a cursor to navigate content
|
||||||
"""
|
"""
|
||||||
@@ -2,16 +2,15 @@ import praw
|
|||||||
import curses
|
import curses
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from content import SubmissionContainer, SubredditContainer
|
from page import BasePage
|
||||||
from utils import curses_session, LoadScreen
|
from utils import curses_session
|
||||||
from viewer import BaseViewer
|
|
||||||
|
|
||||||
class SubmissionViewer(BaseViewer):
|
class SubmissionPage(BasePage):
|
||||||
|
|
||||||
def __init__(self, stdscr, content):
|
def __init__(self, stdscr, content):
|
||||||
|
|
||||||
page_index, cursor_index = -1, 1
|
page_index, cursor_index = -1, 1
|
||||||
super(SubmissionViewer, self).__init__(
|
super(SubmissionPage, self).__init__(
|
||||||
stdscr, content,
|
stdscr, content,
|
||||||
page_index=page_index, cursor_index=cursor_index)
|
page_index=page_index, cursor_index=cursor_index)
|
||||||
|
|
||||||
@@ -58,9 +57,7 @@ class SubmissionViewer(BaseViewer):
|
|||||||
|
|
||||||
def refresh_content(self):
|
def refresh_content(self):
|
||||||
|
|
||||||
self.add_loading()
|
self.content.reset()
|
||||||
with LoadScreen(self.stdscr):
|
|
||||||
self.content.reset()
|
|
||||||
self.stdscr.clear()
|
self.stdscr.clear()
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
@@ -3,12 +3,12 @@ import textwrap
|
|||||||
import curses
|
import curses
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from content import SubredditContainer, SubmissionContainer
|
from page import BasePage
|
||||||
from submission_viewer import SubmissionViewer
|
from submission import SubmissionPage
|
||||||
from viewer import BaseViewer
|
from content import SubmissionContent
|
||||||
from utils import curses_session, text_input, LoadScreen
|
from utils import curses_session, text_input
|
||||||
|
|
||||||
class SubredditViewer(BaseViewer):
|
class SubredditPage(BasePage):
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
|
|
||||||
@@ -51,8 +51,7 @@ class SubredditViewer(BaseViewer):
|
|||||||
|
|
||||||
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
|
||||||
with LoadScreen(self.stdscr):
|
self.content.reset(subreddit=subreddit)
|
||||||
self.content.reset(subreddit=subreddit)
|
|
||||||
self.stdscr.clear()
|
self.stdscr.clear()
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
@@ -73,11 +72,10 @@ class SubredditViewer(BaseViewer):
|
|||||||
def open_submission(self):
|
def open_submission(self):
|
||||||
"Select the current submission to view posts"
|
"Select the current submission to view posts"
|
||||||
|
|
||||||
with LoadScreen(self.stdscr):
|
submission = self.content.get(self.nav.absolute_index)['object']
|
||||||
submission = self.content.get(self.nav.absolute_index)['object']
|
content = SubmissionContent(submission, loader=self.content.loader)
|
||||||
content = SubmissionContainer(submission)
|
page = SubmissionPage(self.stdscr, content)
|
||||||
viewer = SubmissionViewer(self.stdscr, content)
|
page.loop()
|
||||||
viewer.loop()
|
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
|
|
||||||
143
rtv/utils.py
143
rtv/utils.py
@@ -3,12 +3,42 @@ import curses
|
|||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
from curses import textpad
|
from curses import textpad
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
class EscapePressed(Exception):
|
class EscapePressed(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def validate(ch):
|
||||||
|
"Filters characters for special key sequences"
|
||||||
|
if ch == 27:
|
||||||
|
raise EscapePressed
|
||||||
|
return ch
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
class LoadScreen(object):
|
class LoadScreen(object):
|
||||||
|
|
||||||
@@ -26,18 +56,14 @@ class LoadScreen(object):
|
|||||||
self.interval=interval
|
self.interval=interval
|
||||||
self.trail = trail
|
self.trail = trail
|
||||||
|
|
||||||
message_len = len(self.message) + len(self.trail)
|
self._animator = None
|
||||||
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
|
self._is_running = None
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
|
|
||||||
|
self._animator = threading.Thread(target=self.animate)
|
||||||
|
self._animator.daemon = True
|
||||||
|
|
||||||
self._is_running = True
|
self._is_running = True
|
||||||
self._animator.start()
|
self._animator.start()
|
||||||
|
|
||||||
@@ -46,106 +72,37 @@ class LoadScreen(object):
|
|||||||
self._is_running = False
|
self._is_running = False
|
||||||
self._animator.join()
|
self._animator.join()
|
||||||
|
|
||||||
del self.window
|
|
||||||
self._stdscr.refresh()
|
|
||||||
|
|
||||||
def animate(self):
|
def animate(self):
|
||||||
|
|
||||||
# Delay before popping up the animation to avoid flashing
|
# Delay before starting animation to avoid wasting resources if the
|
||||||
# the screen if the load time is effectively zero
|
# wait time is very short
|
||||||
start = time.time()
|
start = time.time()
|
||||||
while (time.time() - start) < self.delay:
|
while (time.time() - start) < self.delay:
|
||||||
if not self._is_running:
|
if not self._is_running:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
message_len = len(self.message) + len(self.trail)
|
||||||
|
n_rows, n_cols = self._stdscr.getmaxyx()
|
||||||
|
s_row = (n_rows - 2) / 2
|
||||||
|
s_col = (n_cols - message_len - 1) / 2
|
||||||
|
window = self._stdscr.derwin(3, message_len+2, s_row, s_col)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
for i in xrange(len(self.trail)+1):
|
for i in xrange(len(self.trail)+1):
|
||||||
|
|
||||||
if not self._is_running:
|
if not self._is_running:
|
||||||
|
# TODO: figure out why this isn't removing the screen
|
||||||
|
del window
|
||||||
|
self._stdscr.refresh()
|
||||||
return
|
return
|
||||||
|
|
||||||
self.window.erase()
|
window.erase()
|
||||||
self.window.border()
|
window.border()
|
||||||
self.window.addstr(1, 1, self.message + self.trail[:i])
|
window.addstr(1, 1, self.message + self.trail[:i])
|
||||||
self.window.refresh()
|
window.refresh()
|
||||||
time.sleep(self.interval)
|
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
|
@contextmanager
|
||||||
def curses_session():
|
def curses_session():
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user