Configurable colors and update improvement

Add a theme section in the configuration file to allow users to
set the colors used by different elements of the ui.

Improve the update mechanism so that incremental changes to the
configuration file can be incorporated.
main
Fabien Benureau 9 years ago
parent 789db93911
commit 3099d353f9

@ -27,6 +27,7 @@ def generate_colors(stream, color=True, bold=True, italic=True):
colors[u'bold'] = u''
colors[u'italic'] = u''
colors[u'end'] = u''
colors[u''] = u''
if (color or bold or italic) and _color_supported(stream):
bold_flag, italic_flag = '', ''
@ -36,6 +37,8 @@ def generate_colors(stream, color=True, bold=True, italic=True):
if italic:
colors['italic'] = u'\033[3m'
italic_flag = '3;'
if bold and italic:
colors['bolditalic'] = u'\033[1;3m'
for i, name in enumerate(COLOR_LIST):
if color:
@ -69,10 +72,17 @@ def dye_err(s, color='end'):
def _nodye(s, *args, **kwargs):
return s
def setup(color=False, bold=False, italic=False):
def setup(conf):
global COLORS_OUT, COLORS_ERR
COLORS_OUT = generate_colors(sys.stdout, color=color, bold=bold, italic=italic)
COLORS_ERR = generate_colors(sys.stderr, color=color, bold=bold, italic=italic)
COLORS_OUT = generate_colors(sys.stdout, color=conf['formating']['color'],
bold=conf['formating']['bold'],
italic=conf['formating']['italics'])
COLORS_ERR = generate_colors(sys.stderr, color=conf['formating']['color'],
bold=conf['formating']['bold'],
italic=conf['formating']['italics'])
for key, value in conf['theme'].items():
COLORS_OUT[key] = COLORS_OUT.get(value, '')
COLORS_ERR[key] = COLORS_ERR.get(value, '')
# undye
undye_re = re.compile('\x1b\[[;\d]*[A-Za-z]')
@ -80,10 +90,3 @@ undye_re = re.compile('\x1b\[[;\d]*[A-Za-z]')
def undye(s):
"""Purge string s of color"""
return undye_re.sub('', s)
# colors
ok = 'green'
error = 'red'
citekey = 'purple'
filepath = 'bold'
tag = 'cyan'

@ -37,10 +37,10 @@ def command(conf, args):
if args.move:
content.remove_file(document)
# else:
# if ui.input_yn('{} has been copied into pubs; should the original be removed?'.format(color.dye_out(document, 'bold'))):
# if ui.input_yn('{} has been copied into pubs; should the original be removed?'.format(color.dye_out(document, 'filepath'))):
# content.remove_file(document)
ui.message('{} attached to {}'.format(color.dye_out(document, 'bold'), color.dye_out(paper.citekey, color.citekey)))
ui.message('{} attached to {}'.format(color.dye_out(document, 'filepath'), color.dye_out(paper.citekey, 'citekey')))
except ValueError as v:
ui.error(v.message)

@ -80,7 +80,7 @@ def command(conf, args):
ui.error('could not load entry for citekey {}.'.format(k))
else:
rp.push_paper(p)
ui.message('{} imported'.format(color.dye_out(p.citekey, color.citekey)))
ui.message('{} imported'.format(color.dye_out(p.citekey, 'citekey')))
docfile = bibstruct.extract_docfile(p.bibdata)
if docfile is None:
ui.warning("no file for {}.".format(p.citekey))

@ -34,10 +34,10 @@ def command(conf, args):
if check_directory(pubsdir, fail=False) and len(os.listdir(pubsdir)) > 0:
ui.error('directory {} is not empty.'.format(
color.dye_err(pubsdir, color.filepath)))
color.dye_err(pubsdir, 'filepath')))
ui.exit()
ui.message('Initializing pubs in {}'.format(color.dye_out(pubsdir, color.filepath)))
ui.message('Initializing pubs in {}'.format(color.dye_out(pubsdir, 'filepath')))
conf['main']['pubsdir'] = pubsdir
conf['main']['docsdir'] = docsdir

@ -33,7 +33,7 @@ def command(conf, args):
if paper.docpath is None:
ui.error('No document associated with the entry {}.'.format(
color.dye_err(citekey, color.citekey)))
color.dye_err(citekey, 'citekey')))
ui.exit()
try:
@ -41,7 +41,7 @@ def command(conf, args):
cmd = with_command.split()
cmd.append(docpath)
subprocess.Popen(cmd)
ui.message('{} opened.'.format(color.dye(docpath, color.filepath)))
ui.message('{} opened.'.format(color.dye(docpath, 'filepath')))
except OSError:
ui.error("Command does not exist: %s." % with_command)
ui.exit(127)

@ -21,12 +21,12 @@ def command(conf, args):
if force is None:
are_you_sure = (("Are you sure you want to delete paper(s) [{}]"
" (this will also delete associated documents)?")
.format(', '.join([color.dye_out(c, color.citekey) for c in args.citekeys])))
.format(', '.join([color.dye_out(c, 'citekey') for c in args.citekeys])))
sure = ui.input_yn(question=are_you_sure, default='n')
if force or sure:
for c in args.citekeys:
rp.remove_paper(c)
ui.message('The paper(s) [{}] were removed'.format(', '.join([color.dye_out(c, color.citekey) for c in args.citekeys])))
ui.message('The paper(s) [{}] were removed'.format(', '.join([color.dye_out(c, 'citekey') for c in args.citekeys])))
# FIXME: print should check that removal proceeded well.
else:
ui.message('The paper(s) [{}] were *not* removed'.format(', '.join([color.dye_out(c, color.citekey) for c in args.citekeys])))
ui.message('The paper(s) [{}] were *not* removed'.format(', '.join([color.dye_out(c, 'citekey') for c in args.citekeys])))

@ -83,13 +83,12 @@ def command(conf, args):
rp = Repository(conf)
if citekeyOrTag is None:
ui.message(color.dye_out(' '.join(sorted(rp.get_tags())), color.tag))
ui.message(color.dye_out(' '.join(sorted(rp.get_tags())), 'tag'))
else:
if rp.databroker.exists(citekeyOrTag):
p = rp.pull_paper(citekeyOrTag)
if tags == []:
ui.message(color.dye_out(' '.join(sorted(p.tags)),
color.tag))
ui.message(color.dye_out(' '.join(sorted(p.tags)), 'tag'))
else:
add_tags, remove_tags = _tag_groups(_parse_tags(tags))
for tag in add_tags:

@ -13,10 +13,6 @@ docsdir = string(default="docsdir://")
# linked when adding a publication.
doc_add = option('copy', 'move', 'link', default='move')
# if True, pubs will ask confirmation before copying/moving/linking the
# document file.
doc_add_ask = boolean(default=True)
# the command to use when opening document files
open_cmd = string(default=None)
@ -41,7 +37,34 @@ color = boolean(default=True)
[theme]
# Here you can define the color theme used by pubs, if enabled in the
# 'formating' section. Predefined theme are available at:
# https://github.com/pubs/pubs/blob/master/theme.md
# Available colors are: 'black', 'red', 'green', 'yellow', 'blue', 'purple',
# 'cyan', and 'grey'. Bold colors are available by prefixing 'b' in front of
# the color name ('bblack', 'bred', etc.), italic colors by prefixing 'i',
# and bold italic by prefixing 'bi'. Finally, 'bold', 'italic' and
# 'bolditalic' can be used to apply formatting without changing the color.
# For no color, use an empty string ''
# messages
ok = string(default='green')
warning = string(default='yellow')
error = string(default='red')
# ui elements
filepath = string(default='bold')
citekey = string(default='purple')
tag = string(default='cyan')
# bibliographic fields
author = string(default='bold')
title = string(default='')
publisher = string(default='italic')
year = string(default='bold')
volume = string(default='bold')
pages = string(default='')
[plugins]

@ -33,10 +33,11 @@ def bib_oneliner(bibdata):
journal = ' ' + bibdata.get('booktitle', '')
return u'{authors} \"{title}\"{journal}{year}'.format(
authors=color.dye_out(authors, 'bold'),
title=bibdata.get('title', ''),
journal=color.dye_out(journal, 'italic'),
year=' ({})'.format(bibdata['year']) if 'year' in bibdata else '',
authors=color.dye_out(authors, 'author'),
title=color.dye_out(bibdata.get('title', ''), 'title'),
journal=color.dye_out(journal, 'publisher'),
year=' ({})'.format(color.dye_out(bibdata['year'], 'year'))
if 'year' in bibdata else ''
)
@ -55,7 +56,7 @@ def paper_oneliner(p, citekey_only=False):
else:
bibdesc = bib_oneliner(p.bibdata)
tags = '' if len(p.tags) == 0 else '| {}'.format(
','.join(color.dye_out(t, color.tag) for t in sorted(p.tags)))
','.join(color.dye_out(t, 'tag') for t in sorted(p.tags)))
return u'[{citekey}] {descr} {tags}'.format(
citekey=color.dye_out(p.citekey, 'purple'),
citekey=color.dye_out(p.citekey, 'citekey'),
descr=bibdesc, tags=tags)

@ -44,9 +44,7 @@ class PrintUI(object):
:param conf: if None, conservative default values are used.
Useful to instanciate the UI before parsing the config file.
"""
color.setup(color=conf['formating']['color'],
bold=conf['formating']['bold'],
italic=conf['formating']['italics'])
color.setup(conf)
self.encoding = _get_encoding(conf)
self._stdout = codecs.getwriter(self.encoding)(_get_raw_stdout(),
errors='replace')
@ -59,11 +57,11 @@ class PrintUI(object):
def warning(self, message, **kwargs):
kwargs['file'] = self._stderr
print('{}: {}'.format(color.dye_err('warning', 'yellow'), message), **kwargs)
print('{}: {}'.format(color.dye_err('warning', 'warning'), message), **kwargs)
def error(self, message, **kwargs):
kwargs['file'] = self._stderr
print('{}: {}'.format(color.dye_err('error', 'red'), message), **kwargs)
print('{}: {}'.format(color.dye_err('error', 'error'), message), **kwargs)
def exit(self, error_code=1):
sys.exit(error_code)
@ -97,14 +95,12 @@ class InputUI(PrintUI):
:returns: int
the index of the chosen option
"""
char_color = 'bold'
option_chars = [s[0] for s in options]
displayed_chars = [c.upper() if i == default else c
for i, c in enumerate(option_chars)]
if len(set(option_chars)) != len(option_chars): # duplicate chars, char choices are deactivated. #FIXME: should only deactivate ambiguous chars
option_chars = []
char_color = color.end
option_str = '/'.join(["{}{}".format(color.dye_out(c, 'bold'), s[1:])
for c, s in zip(displayed_chars, options)])

@ -1,9 +1,13 @@
import shutil
import StringIO
from . import config
from . import uis
from . import color
from .__init__ import __version__
def update_check(conf):
def update_check(conf, path=None):
"""Runs an update if necessary, and return True in that case."""
code_version = __version__.split('.')
@ -24,18 +28,18 @@ def update_check(conf):
'newest version.')
sys.exit()
elif repo_version < code_version:
return update(conf, code_version, repo_version)
elif repo_version <= code_version:
return update(conf, code_version, repo_version, path=path)
return False
def update(conf, code_version, repo_version):
def update(conf, code_version, repo_version, path=None):
"""Runs an update if necessary, and return True in that case."""
if path is None:
path = config.get_confpath()
if repo_version == ['0', '5', '0']: # we need to update
default_conf = config.load_default_conf()
uis.init_ui(config.load_default_conf())
ui = uis.get_ui()
for key in ['pubsdir', 'docsdir', 'edit_cmd', 'open_cmd']:
default_conf['main'][key] = conf['pubs'][key]
@ -46,13 +50,15 @@ def update(conf, code_version, repo_version):
else:
default_conf['main']['add_doc'] = 'link'
backup_path = config.get_confpath() + '.old'
config.save_conf(conf, path=backup_path)
backup_path = path + '.old'
shutil.move(path, backup_path)
config.save_conf(default_conf)
uis.init_ui(default_conf)
ui = uis.get_ui()
ui.warning(
'Your configuration file has been updated. '
'The old file has been moved to `{}`. '.format(backup_path) +
'Your old configuration has been moved to `{}`. '.format(color.dye_out(backup_path, 'filepath')) +
'Some, but not all, of your settings has been transferred '
'to the new file.\n'
'You can inspect and modify your configuration '
@ -60,4 +66,35 @@ def update(conf, code_version, repo_version):
)
return True
# continuous update while configuration is stabilizing
if repo_version == code_version == ['0', '6', '0']:
default_conf = config.load_default_conf()
for section_name, section in conf.items():
for key, value in section.items():
try:
default_conf[section_name][key]
default_conf[section_name][key] = value
except KeyError:
pass
# comparing potential changes
with open(path, 'r') as f:
old_conf_text = f.read()
new_conf_text = StringIO.StringIO()
default_conf.write(outfile=new_conf_text)
if new_conf_text.getvalue() != old_conf_text:
backup_path = path + '.old'
shutil.move(path, backup_path)
config.save_conf(default_conf)
uis.init_ui(default_conf)
ui = uis.get_ui()
ui.warning('Your configuration file has been updated.\n'
'Your old configuration has been moved to `{}`.'.format(color.dye_out(backup_path, 'filepath')))
return True
return False

@ -9,13 +9,13 @@ def resolve_citekey(repo, citekey, ui=None, exit_on_fail=True):
citekeys = repo.citekeys_from_prefix(citekey)
if len(citekeys) == 0:
if ui is not None:
ui.error("no citekey named or beginning with '{}'".format(color.dye_out(citekey, color.citekey)))
ui.error("no citekey named or beginning with '{}'".format(color.dye_out(citekey, 'citekey')))
if exit_on_fail:
ui.exit()
elif len(citekeys) == 1:
if citekeys[0] != citekey:
if ui is not None:
ui.warning("provided citekey '{}' has been autocompleted into '{}'".format(color.dye_out(citekey, color.citekey), color.dye_out(citekeys[0], color.citekey)))
ui.warning("provided citekey '{}' has been autocompleted into '{}'".format(color.dye_out(citekey, 'citekey'), color.dye_out(citekeys[0], 'citekey')))
citekey = citekeys[0]
elif citekey not in citekeys:
if ui is not None:

@ -5,7 +5,7 @@ from pubs import color
def perf_color():
s = str(list(range(1000)))
for _ in range(5000000):
color.dye(s, color.red)
color.dye_out(s, 'red')
if __name__ == '__main__':

@ -5,7 +5,7 @@ import os
import dotdot
import fake_env
from pubs import endecoder, pretty, color
from pubs import endecoder, pretty, color, config
from str_fixtures import bibtex_raw0
@ -13,7 +13,8 @@ from str_fixtures import bibtex_raw0
class TestPretty(unittest.TestCase):
def setUp(self):
color.setup()
conf = config.load_default_conf()
color.setup(conf)
def test_oneliner(self):
decoder = endecoder.EnDecoder()

Loading…
Cancel
Save