Implement user formatting of SubredditPages
Users can now specify a format string in their config file that determines the internal layout of subwindows of a SubredditPage. The config string is passed to SubredditPage._create_format(), and internal details can be found in its docstring. SubredditPage._draw_item_format() then draws the format produced by SubredditPage._create_format(). With this addition, simplication could be made with the compact format. Instead of its own dedicated draw function, it now uses _draw_item_format() with a format that produces the same result as _draw_item_compact(). With this, _draw_item_compact() will be removed as its functionality has been replaced. If the user specifies a look_and_feel and a subreddit_format in their config, the latter overrides the formatting of the former. Relevant to #3
This commit is contained in:
@@ -33,6 +33,9 @@ class Config(object):
|
|||||||
HISTORY = os.path.join(XDG_DATA_HOME, 'tuir', 'history.log')
|
HISTORY = os.path.join(XDG_DATA_HOME, 'tuir', 'history.log')
|
||||||
THEMES = os.path.join(XDG_CONFIG_HOME, 'tuir', 'themes')
|
THEMES = os.path.join(XDG_CONFIG_HOME, 'tuir', 'themes')
|
||||||
|
|
||||||
|
COMPACT_FORMAT = "%t\n" \
|
||||||
|
"<%i|%s%v|%cC> %r%e %a %S %F"
|
||||||
|
|
||||||
def __init__(self, history_file=HISTORY, token_file=TOKEN, **kwargs):
|
def __init__(self, history_file=HISTORY, token_file=TOKEN, **kwargs):
|
||||||
|
|
||||||
self.history_file = history_file
|
self.history_file = history_file
|
||||||
@@ -123,7 +126,7 @@ class Config(object):
|
|||||||
if filename is None:
|
if filename is None:
|
||||||
filename = Config.CONFIG
|
filename = Config.CONFIG
|
||||||
|
|
||||||
config = configparser.ConfigParser()
|
config = configparser.RawConfigParser()
|
||||||
if os.path.exists(filename):
|
if os.path.exists(filename):
|
||||||
with codecs.open(filename, encoding='utf-8') as fp:
|
with codecs.open(filename, encoding='utf-8') as fp:
|
||||||
config.readfp(fp)
|
config.readfp(fp)
|
||||||
|
|||||||
@@ -576,10 +576,10 @@ class SubredditContent(Content):
|
|||||||
self._submissions = submissions
|
self._submissions = submissions
|
||||||
self._submission_data = []
|
self._submission_data = []
|
||||||
|
|
||||||
if self.config['look_and_feel'] == 'compact':
|
if self.config['look_and_feel'] == 'default':
|
||||||
self.max_title_rows = 1
|
|
||||||
else:
|
|
||||||
self.max_title_rows = 4
|
self.max_title_rows = 4
|
||||||
|
else:
|
||||||
|
self.max_title_rows = 1
|
||||||
|
|
||||||
# Verify that content exists for the given submission generator.
|
# Verify that content exists for the given submission generator.
|
||||||
# This is necessary because PRAW loads submissions lazily, and
|
# This is necessary because PRAW loads submissions lazily, and
|
||||||
@@ -855,7 +855,7 @@ class SubredditContent(Content):
|
|||||||
data['index'] = len(self._submission_data) + 1
|
data['index'] = len(self._submission_data) + 1
|
||||||
|
|
||||||
# Add the post number to the beginning of the title if necessary
|
# Add the post number to the beginning of the title if necessary
|
||||||
if self.config['look_and_feel'] != 'compact':
|
if self.config['look_and_feel'] == 'default':
|
||||||
data['title'] = '{0}. {1}'.format(data['index'], data['title'])
|
data['title'] = '{0}. {1}'.format(data['index'], data['title'])
|
||||||
|
|
||||||
self._submission_data.append(data)
|
self._submission_data.append(data)
|
||||||
@@ -865,6 +865,8 @@ class SubredditContent(Content):
|
|||||||
|
|
||||||
if self.config['look_and_feel'] == 'compact':
|
if self.config['look_and_feel'] == 'compact':
|
||||||
data['n_rows'] = 2
|
data['n_rows'] = 2
|
||||||
|
elif self.config['subreddit_format']:
|
||||||
|
data['n_rows'] = self.config['subreddit_format'].count('\n') + 1
|
||||||
else:
|
else:
|
||||||
data['split_title'] = self.wrap_text(data['title'], width=n_cols)
|
data['split_title'] = self.wrap_text(data['title'], width=n_cols)
|
||||||
data['n_rows'] = len(data['split_title']) + 3
|
data['n_rows'] = len(data['split_title']) + 3
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from . import docs
|
from . import docs
|
||||||
@@ -32,6 +33,11 @@ class SubredditPage(Page):
|
|||||||
self.nav = Navigator(self.content.get)
|
self.nav = Navigator(self.content.get)
|
||||||
self.toggled_subreddit = None
|
self.toggled_subreddit = None
|
||||||
|
|
||||||
|
if self.config['subreddit_format']:
|
||||||
|
self.format = self._create_format(self.config['subreddit_format'])
|
||||||
|
elif self.config['look_and_feel'] == 'compact':
|
||||||
|
self.format = self._create_format(self.config.COMPACT_FORMAT)
|
||||||
|
|
||||||
def handle_selected_page(self):
|
def handle_selected_page(self):
|
||||||
"""
|
"""
|
||||||
Open all selected pages in subwindows except other subreddit pages.
|
Open all selected pages in subwindows except other subreddit pages.
|
||||||
@@ -235,6 +241,195 @@ class SubredditPage(Page):
|
|||||||
data['object'].hide()
|
data['object'].hide()
|
||||||
data['hidden'] = True
|
data['hidden'] = True
|
||||||
|
|
||||||
|
def _submission_attr(self, data):
|
||||||
|
if data['url_full'] in self.config.history:
|
||||||
|
return self.term.attr('SubmissionTitleSeen')
|
||||||
|
else:
|
||||||
|
return self.term.attr('SubmissionTitle')
|
||||||
|
|
||||||
|
def _url_attr(self, data):
|
||||||
|
if data['url_full'] in self.config.history:
|
||||||
|
return self.term.attr('LinkSeen')
|
||||||
|
else:
|
||||||
|
return self.term.attr('Link')
|
||||||
|
|
||||||
|
def _gold_str(self, data):
|
||||||
|
if data['gold'] > 1:
|
||||||
|
return self.term.gilded + 'x{} '.format(data['gold'])
|
||||||
|
elif data['gold'] == 1:
|
||||||
|
return self.term.gilded + ' '
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def _create_format(self, format_string):
|
||||||
|
"""
|
||||||
|
Returns a list of tuples of the format (datafield, attr, first) which
|
||||||
|
will be used by _draw_item to output information in the proper order.
|
||||||
|
It would be trivial to use the strings as simple format strings, but
|
||||||
|
more must be done in order to associate strings with their Attribute.
|
||||||
|
|
||||||
|
datafield = lambda that retrieves the proper field of the data
|
||||||
|
dict
|
||||||
|
|
||||||
|
attr = lambda that returns the proper attribute class associated with
|
||||||
|
datafield. Sometimes this isn't known until drawtime; see above
|
||||||
|
*_attr() functions for examples
|
||||||
|
|
||||||
|
first = boolean that signifies whether or not term.add_line should be
|
||||||
|
called with a col argument to start a line
|
||||||
|
"""
|
||||||
|
form = []
|
||||||
|
first = True
|
||||||
|
|
||||||
|
# Split the list between %., newlines, and certain separator characters
|
||||||
|
# to treat them separately
|
||||||
|
format_list = re.split(r'(%.|[\n<>|\\])', format_string, re.DOTALL)
|
||||||
|
|
||||||
|
for item in format_list:
|
||||||
|
# Use lambdas because the actual data to be used is only known at
|
||||||
|
# drawtime. This way the format list knows how to use the data,
|
||||||
|
# and can simply be used when the data is available
|
||||||
|
if item == "%i":
|
||||||
|
form.append((lambda data: data['index'],
|
||||||
|
lambda data: self._submission_attr(data), first))
|
||||||
|
elif item == "%t":
|
||||||
|
form.append((lambda data: data['title'],
|
||||||
|
lambda data: self._submission_attr(data), first))
|
||||||
|
elif item == "%s":
|
||||||
|
# Need to cut off the characters that aren't the score number
|
||||||
|
form.append((lambda data: data['score'][:-4],
|
||||||
|
lambda data: self.term.attr('Score'), first))
|
||||||
|
elif item == "%v":
|
||||||
|
# This isn't great, self.term.get_arrow gets called twice
|
||||||
|
form.append((lambda data: self.term.get_arrow(data['likes'])[0],
|
||||||
|
lambda data: self.term.get_arrow(data['likes'])[1], first))
|
||||||
|
elif item == "%c":
|
||||||
|
form.append((
|
||||||
|
lambda data: '{0}'.format(data['comments'][:-9])
|
||||||
|
if data['comments'] else None, # Don't try to subscript null items
|
||||||
|
lambda data: self.term.attr('CommentCount'),
|
||||||
|
first))
|
||||||
|
elif item == "%r":
|
||||||
|
form.append((lambda data: data['created'],
|
||||||
|
lambda data: self.term.attr('Created'), first))
|
||||||
|
elif item == "%R":
|
||||||
|
raise NotImplementedError("'%R' subreddit_format specifier not yet supported")
|
||||||
|
elif item == "%e":
|
||||||
|
form.append((lambda data: data['edited'],
|
||||||
|
lambda data: self.term.attr('Created'), first))
|
||||||
|
elif item == "%E":
|
||||||
|
raise NotImplementedError("'%E' subreddit_format specifier not yet supported")
|
||||||
|
elif item == "%a":
|
||||||
|
form.append((lambda data: data['author'],
|
||||||
|
lambda data: self.term.attr('SubmissionAuthor'), first))
|
||||||
|
elif item == "%S":
|
||||||
|
form.append((lambda data: "/r/" + data['subreddit'],
|
||||||
|
lambda data: self.term.attr('SubmissionSubreddit'),
|
||||||
|
first))
|
||||||
|
elif item == "%u":
|
||||||
|
raise NotImplementedError("'%u' subreddit_format specifier not yet supported")
|
||||||
|
elif item == "%U":
|
||||||
|
form.append((lambda data: data['url'],
|
||||||
|
lambda data: self._url_attr(data), first))
|
||||||
|
elif item == "%A":
|
||||||
|
form.append((lambda data: '[saved]' if data['saved'] else '',
|
||||||
|
lambda data: self.term.attr('Saved'), first))
|
||||||
|
elif item == "%h":
|
||||||
|
form.append((lambda data: '[hidden]' if data['hidden'] else '',
|
||||||
|
lambda data: self.term.attr('Hidden'), first))
|
||||||
|
elif item == "%T":
|
||||||
|
form.append((lambda data: '[stickied]' if data['stickied'] else '',
|
||||||
|
lambda data: self.term.attr('Stickied'), first))
|
||||||
|
elif item == "%g":
|
||||||
|
form.append((lambda data: self._gold_str(data),
|
||||||
|
lambda data: self.term.attr('Gold'), first))
|
||||||
|
elif item == "%n":
|
||||||
|
form.append((lambda data: 'NSFW' if data['nsfw'] else '',
|
||||||
|
lambda data: self.term.attr('NSFW'), first))
|
||||||
|
elif item == "%f":
|
||||||
|
form.append((lambda data: data['flair'] if data['flair'] else '',
|
||||||
|
lambda data: self.term.attr('SubmissionFlair'), first))
|
||||||
|
elif item == "%F":
|
||||||
|
form.append((lambda data: data['flair'] + ' ' if data['flair'] else '',
|
||||||
|
lambda data: self.term.attr('SubmissionFlair'), first))
|
||||||
|
|
||||||
|
form.append((lambda data: '[saved] ' if data['saved'] else '',
|
||||||
|
lambda data: self.term.attr('Saved'), first))
|
||||||
|
|
||||||
|
form.append((lambda data: '[hidden] ' if data['hidden'] else '',
|
||||||
|
lambda data: self.term.attr('Hidden'), first))
|
||||||
|
|
||||||
|
form.append((lambda data: '[stickied] ' if data['stickied'] else '',
|
||||||
|
lambda data: self.term.attr('Stickied'), first))
|
||||||
|
|
||||||
|
form.append((lambda data: self._gold_str(data),
|
||||||
|
lambda data: self.term.attr('Gold'), first))
|
||||||
|
|
||||||
|
form.append((lambda data: 'NSFW ' if data['nsfw'] else '',
|
||||||
|
lambda data: self.term.attr('NSFW'), first))
|
||||||
|
elif item == "\n":
|
||||||
|
form.append((item, None, first))
|
||||||
|
first = True
|
||||||
|
|
||||||
|
continue
|
||||||
|
else: # Write something else that isn't in the data dict
|
||||||
|
# Make certain "separator" characters use the Separator
|
||||||
|
# attribute
|
||||||
|
if item in r"<>|\\":
|
||||||
|
form.append((item,
|
||||||
|
lambda data: self.term.attr('Separator'),
|
||||||
|
first))
|
||||||
|
else:
|
||||||
|
form.append((item, None, first))
|
||||||
|
|
||||||
|
first = False
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
|
def _draw_item_format(self, win, data, valid_rows, offset):
|
||||||
|
last_attr = None
|
||||||
|
for get_data, get_attr, first in self.format:
|
||||||
|
# add_line wants strings, make sure we give it strings
|
||||||
|
if callable(get_data):
|
||||||
|
string = str(get_data(data))
|
||||||
|
else:
|
||||||
|
# Start writing to the next line if we hit a newline
|
||||||
|
if get_data == "\n":
|
||||||
|
offset += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Otherwise, proceed on the same line
|
||||||
|
string = str(get_data)
|
||||||
|
|
||||||
|
# We only want to print a maximum of one line of data
|
||||||
|
# TODO - support line wrapping
|
||||||
|
string = string.split('\n')[0]
|
||||||
|
|
||||||
|
# Don't try to write null strings to the screen. This happens in
|
||||||
|
# places like user pages, where data['comments'] is None
|
||||||
|
if string is None:
|
||||||
|
continue
|
||||||
|
elif string is ' ':
|
||||||
|
# Make sure spaces aren't treated like normal strings and print
|
||||||
|
# them to the window this way. This ensures they won't be drawn
|
||||||
|
# with an attribute.
|
||||||
|
self.term.add_space(win)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# To make sure we don't try to write a None as an attribute,
|
||||||
|
# we can use the one that was last used
|
||||||
|
if get_attr is None:
|
||||||
|
attr = last_attr
|
||||||
|
else:
|
||||||
|
attr = get_attr(data)
|
||||||
|
|
||||||
|
if first:
|
||||||
|
self.term.add_line(win, string, offset, 1, attr=attr)
|
||||||
|
else:
|
||||||
|
self.term.add_line(win, string, offset, attr=attr)
|
||||||
|
|
||||||
|
last_attr = attr
|
||||||
|
|
||||||
def _draw_item_default(self, win, data, n_rows, n_cols, valid_rows, offset):
|
def _draw_item_default(self, win, data, n_rows, n_cols, valid_rows, offset):
|
||||||
"""
|
"""
|
||||||
Draw items with default look and feel
|
Draw items with default look and feel
|
||||||
@@ -326,92 +521,6 @@ class SubredditPage(Page):
|
|||||||
self.term.add_space(win)
|
self.term.add_space(win)
|
||||||
self.term.add_line(win, '{flair}'.format(**data), attr=attr)
|
self.term.add_line(win, '{flair}'.format(**data), attr=attr)
|
||||||
|
|
||||||
def _draw_item_compact(self, win, data, n_rows, n_cols, valid_rows, offset):
|
|
||||||
"""
|
|
||||||
Draw items with compact look and feel
|
|
||||||
"""
|
|
||||||
|
|
||||||
if data['url_full'] in self.config.history:
|
|
||||||
sub_attr = self.term.attr('SubmissionTitleSeen')
|
|
||||||
else:
|
|
||||||
sub_attr = self.term.attr('SubmissionTitle')
|
|
||||||
|
|
||||||
if offset in valid_rows:
|
|
||||||
# On user pages, comments are displayed as the title.
|
|
||||||
# The raw data has newlines, so we display just the first line
|
|
||||||
title = data['title'].split('\n')[0]
|
|
||||||
self.term.add_line(win, '{0}'.format(title), offset, 1, attr=sub_attr)
|
|
||||||
|
|
||||||
offset += 1 # Next row
|
|
||||||
|
|
||||||
if offset in valid_rows:
|
|
||||||
sep_attr = self.term.attr('Separator')
|
|
||||||
|
|
||||||
self.term.add_line(win, '<', offset, 1, attr=sep_attr)
|
|
||||||
|
|
||||||
self.term.add_line(win, '{index}'.format(**data), offset, attr=sub_attr)
|
|
||||||
|
|
||||||
self.term.add_line(win, '|', offset, attr=sep_attr)
|
|
||||||
|
|
||||||
# Seems that praw doesn't give us raw numbers for score and comment
|
|
||||||
# count, so we have to cut off the extraneous characters
|
|
||||||
attr = self.term.attr('Score')
|
|
||||||
self.term.add_line(win, '{0}'.format(data['score'][:-4]), offset, attr=attr)
|
|
||||||
|
|
||||||
arrow, attr = self.term.get_arrow(data['likes'])
|
|
||||||
self.term.add_line(win, arrow, attr=attr)
|
|
||||||
|
|
||||||
if data['comments'] is not None:
|
|
||||||
self.term.add_line(win, '|', offset, attr=sep_attr)
|
|
||||||
attr = self.term.attr('CommentCount')
|
|
||||||
self.term.add_line(win, '{0}C'.format(data['comments'][:-9]), attr=attr)
|
|
||||||
|
|
||||||
self.term.add_line(win, '>', offset, attr=sep_attr)
|
|
||||||
self.term.add_space(win)
|
|
||||||
|
|
||||||
attr = self.term.attr('Created')
|
|
||||||
self.term.add_line(win, '{created}{edited}'.format(**data), attr=attr)
|
|
||||||
self.term.add_space(win)
|
|
||||||
|
|
||||||
attr = self.term.attr('SubmissionAuthor')
|
|
||||||
self.term.add_line(win, '{author}'.format(**data), offset, attr=attr)
|
|
||||||
self.term.add_space(win)
|
|
||||||
|
|
||||||
attr = self.term.attr('SubmissionSubreddit')
|
|
||||||
self.term.add_line(win, '/r/{subreddit}'.format(**data), attr=attr)
|
|
||||||
|
|
||||||
if data['saved']:
|
|
||||||
attr = self.term.attr('Saved')
|
|
||||||
self.term.add_space(win)
|
|
||||||
self.term.add_line(win, '[saved]', attr=attr)
|
|
||||||
|
|
||||||
if data['hidden']:
|
|
||||||
attr = self.term.attr('Hidden')
|
|
||||||
self.term.add_space(win)
|
|
||||||
self.term.add_line(win, '[hidden]', attr=attr)
|
|
||||||
|
|
||||||
if data['stickied']:
|
|
||||||
attr = self.term.attr('Stickied')
|
|
||||||
self.term.add_space(win)
|
|
||||||
self.term.add_line(win, '[stickied]', attr=attr)
|
|
||||||
|
|
||||||
if data['gold']:
|
|
||||||
attr = self.term.attr('Gold')
|
|
||||||
self.term.add_space(win)
|
|
||||||
count = 'x{}'.format(data['gold']) if data['gold'] > 1 else ''
|
|
||||||
text = self.term.gilded + count
|
|
||||||
self.term.add_line(win, text, attr=attr)
|
|
||||||
|
|
||||||
if data['nsfw']:
|
|
||||||
attr = self.term.attr('NSFW')
|
|
||||||
self.term.add_space(win)
|
|
||||||
self.term.add_line(win, 'NSFW', attr=attr)
|
|
||||||
|
|
||||||
if data['flair']:
|
|
||||||
attr = self.term.attr('SubmissionFlair')
|
|
||||||
self.term.add_space(win)
|
|
||||||
self.term.add_line(win, '{flair}'.format(**data), attr=attr)
|
|
||||||
|
|
||||||
def _draw_item(self, win, data, inverted):
|
def _draw_item(self, win, data, inverted):
|
||||||
n_rows, n_cols = win.getmaxyx()
|
n_rows, n_cols = win.getmaxyx()
|
||||||
n_cols -= 1 # Leave space for the cursor in the first column
|
n_cols -= 1 # Leave space for the cursor in the first column
|
||||||
@@ -420,8 +529,9 @@ class SubredditPage(Page):
|
|||||||
valid_rows = range(0, n_rows)
|
valid_rows = range(0, n_rows)
|
||||||
offset = 0 if not inverted else - (data['n_rows'] - n_rows)
|
offset = 0 if not inverted else - (data['n_rows'] - n_rows)
|
||||||
|
|
||||||
if self.config['look_and_feel'] == 'compact':
|
if self.config['look_and_feel'] == 'compact' or \
|
||||||
self._draw_item_compact(win, data, n_rows, n_cols, valid_rows, offset)
|
self.config['subreddit_format']:
|
||||||
|
self._draw_item_format(win, data, valid_rows, offset)
|
||||||
else:
|
else:
|
||||||
self._draw_item_default(win, data, n_rows, n_cols, valid_rows, offset)
|
self._draw_item_default(win, data, n_rows, n_cols, valid_rows, offset)
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,41 @@ hide_username = False
|
|||||||
; look_and_feel = default
|
; look_and_feel = default
|
||||||
; look_and_feel = compact
|
; look_and_feel = compact
|
||||||
|
|
||||||
|
; The subreddit_format option defines the format of submissions in a Subreddit
|
||||||
|
; page. If specified, this option will override whatever was set in
|
||||||
|
; look_and_feel. Multiple lines are supported, but lines after the first must
|
||||||
|
; be indented. Unfortunately, this means that this isn't capable of
|
||||||
|
; interpreting leading spaces.
|
||||||
|
;
|
||||||
|
; List of valid format specifiers and what they evaluate to:
|
||||||
|
;
|
||||||
|
; %i index
|
||||||
|
; %t title
|
||||||
|
; %s score
|
||||||
|
; %v vote status
|
||||||
|
; %c comment count
|
||||||
|
; %r relative creation time
|
||||||
|
; %R absolute creation time (NotImplemented)
|
||||||
|
; %e relative edit time
|
||||||
|
; %E absolute edit time (NotImplemented)
|
||||||
|
; %a author
|
||||||
|
; %S subreddit
|
||||||
|
; %u short url - 'self.reddit' or 'gfycat.com' (NotImplemented)
|
||||||
|
; %U full url
|
||||||
|
; %A saved
|
||||||
|
; %h hidden
|
||||||
|
; %T stickied
|
||||||
|
; %g gold
|
||||||
|
; %n nsfw
|
||||||
|
; %f post flair
|
||||||
|
; %F all flair - saved, hidden, stickied, gold, nsfw, post flair,
|
||||||
|
; separated by spaces
|
||||||
|
;
|
||||||
|
; For example, the compact look_and_feel is a format of:
|
||||||
|
; subreddit_format = %t
|
||||||
|
; <%i|%s%v|%cC> %r%e %a %S %F
|
||||||
|
;
|
||||||
|
|
||||||
; Color theme, use "tuir --list-themes" to view a list of valid options.
|
; Color theme, use "tuir --list-themes" to view a list of valid options.
|
||||||
; This can be an absolute filepath, or the name of a theme file that has
|
; This can be an absolute filepath, or the name of a theme file that has
|
||||||
; been installed into either the custom of default theme paths.
|
; been installed into either the custom of default theme paths.
|
||||||
|
|||||||
Reference in New Issue
Block a user