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:
John Helmert
2019-07-05 15:11:47 -05:00
parent df1e439c57
commit c146b13aa5
4 changed files with 243 additions and 93 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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.