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'bold'] = u''
colors[u'italic'] = u'' colors[u'italic'] = u''
colors[u'end'] = u'' colors[u'end'] = u''
colors[u''] = u''
if (color or bold or italic) and _color_supported(stream): if (color or bold or italic) and _color_supported(stream):
bold_flag, italic_flag = '', '' bold_flag, italic_flag = '', ''
@ -36,6 +37,8 @@ def generate_colors(stream, color=True, bold=True, italic=True):
if italic: if italic:
colors['italic'] = u'\033[3m' colors['italic'] = u'\033[3m'
italic_flag = '3;' italic_flag = '3;'
if bold and italic:
colors['bolditalic'] = u'\033[1;3m'
for i, name in enumerate(COLOR_LIST): for i, name in enumerate(COLOR_LIST):
if color: if color:
@ -69,10 +72,17 @@ def dye_err(s, color='end'):
def _nodye(s, *args, **kwargs): def _nodye(s, *args, **kwargs):
return s return s
def setup(color=False, bold=False, italic=False): def setup(conf):
global COLORS_OUT, COLORS_ERR global COLORS_OUT, COLORS_ERR
COLORS_OUT = generate_colors(sys.stdout, color=color, bold=bold, italic=italic) COLORS_OUT = generate_colors(sys.stdout, color=conf['formating']['color'],
COLORS_ERR = generate_colors(sys.stderr, color=color, bold=bold, italic=italic) 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
undye_re = re.compile('\x1b\[[;\d]*[A-Za-z]') 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): def undye(s):
"""Purge string s of color""" """Purge string s of color"""
return undye_re.sub('', s) 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: if args.move:
content.remove_file(document) content.remove_file(document)
# else: # 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) # 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: except ValueError as v:
ui.error(v.message) ui.error(v.message)

@ -80,7 +80,7 @@ def command(conf, args):
ui.error('could not load entry for citekey {}.'.format(k)) ui.error('could not load entry for citekey {}.'.format(k))
else: else:
rp.push_paper(p) 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) docfile = bibstruct.extract_docfile(p.bibdata)
if docfile is None: if docfile is None:
ui.warning("no file for {}.".format(p.citekey)) 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: if check_directory(pubsdir, fail=False) and len(os.listdir(pubsdir)) > 0:
ui.error('directory {} is not empty.'.format( ui.error('directory {} is not empty.'.format(
color.dye_err(pubsdir, color.filepath))) color.dye_err(pubsdir, 'filepath')))
ui.exit() 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']['pubsdir'] = pubsdir
conf['main']['docsdir'] = docsdir conf['main']['docsdir'] = docsdir

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

@ -21,12 +21,12 @@ def command(conf, args):
if force is None: if force is None:
are_you_sure = (("Are you sure you want to delete paper(s) [{}]" are_you_sure = (("Are you sure you want to delete paper(s) [{}]"
" (this will also delete associated documents)?") " (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') sure = ui.input_yn(question=are_you_sure, default='n')
if force or sure: if force or sure:
for c in args.citekeys: for c in args.citekeys:
rp.remove_paper(c) 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. # FIXME: print should check that removal proceeded well.
else: 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) rp = Repository(conf)
if citekeyOrTag is None: 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: else:
if rp.databroker.exists(citekeyOrTag): if rp.databroker.exists(citekeyOrTag):
p = rp.pull_paper(citekeyOrTag) p = rp.pull_paper(citekeyOrTag)
if tags == []: if tags == []:
ui.message(color.dye_out(' '.join(sorted(p.tags)), ui.message(color.dye_out(' '.join(sorted(p.tags)), 'tag'))
color.tag))
else: else:
add_tags, remove_tags = _tag_groups(_parse_tags(tags)) add_tags, remove_tags = _tag_groups(_parse_tags(tags))
for tag in add_tags: for tag in add_tags:

@ -13,10 +13,6 @@ docsdir = string(default="docsdir://")
# linked when adding a publication. # linked when adding a publication.
doc_add = option('copy', 'move', 'link', default='move') 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 # the command to use when opening document files
open_cmd = string(default=None) open_cmd = string(default=None)
@ -41,7 +37,34 @@ color = boolean(default=True)
[theme] [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] [plugins]

@ -33,10 +33,11 @@ def bib_oneliner(bibdata):
journal = ' ' + bibdata.get('booktitle', '') journal = ' ' + bibdata.get('booktitle', '')
return u'{authors} \"{title}\"{journal}{year}'.format( return u'{authors} \"{title}\"{journal}{year}'.format(
authors=color.dye_out(authors, 'bold'), authors=color.dye_out(authors, 'author'),
title=bibdata.get('title', ''), title=color.dye_out(bibdata.get('title', ''), 'title'),
journal=color.dye_out(journal, 'italic'), journal=color.dye_out(journal, 'publisher'),
year=' ({})'.format(bibdata['year']) if 'year' in bibdata else '', 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: else:
bibdesc = bib_oneliner(p.bibdata) bibdesc = bib_oneliner(p.bibdata)
tags = '' if len(p.tags) == 0 else '| {}'.format( 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( return u'[{citekey}] {descr} {tags}'.format(
citekey=color.dye_out(p.citekey, 'purple'), citekey=color.dye_out(p.citekey, 'citekey'),
descr=bibdesc, tags=tags) descr=bibdesc, tags=tags)

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

@ -1,9 +1,13 @@
import shutil
import StringIO
from . import config from . import config
from . import uis from . import uis
from . import color
from .__init__ import __version__ 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.""" """Runs an update if necessary, and return True in that case."""
code_version = __version__.split('.') code_version = __version__.split('.')
@ -24,18 +28,18 @@ def update_check(conf):
'newest version.') 'newest version.')
sys.exit() sys.exit()
elif repo_version < code_version: elif repo_version <= code_version:
return update(conf, code_version, repo_version) return update(conf, code_version, repo_version, path=path)
return False 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.""" """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 if repo_version == ['0', '5', '0']: # we need to update
default_conf = config.load_default_conf() 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']: for key in ['pubsdir', 'docsdir', 'edit_cmd', 'open_cmd']:
default_conf['main'][key] = conf['pubs'][key] default_conf['main'][key] = conf['pubs'][key]
@ -46,13 +50,15 @@ def update(conf, code_version, repo_version):
else: else:
default_conf['main']['add_doc'] = 'link' default_conf['main']['add_doc'] = 'link'
backup_path = config.get_confpath() + '.old' backup_path = path + '.old'
config.save_conf(conf, path=backup_path) shutil.move(path, backup_path)
config.save_conf(default_conf) config.save_conf(default_conf)
uis.init_ui(default_conf)
ui = uis.get_ui()
ui.warning( ui.warning(
'Your configuration file has been updated. ' '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 ' 'Some, but not all, of your settings has been transferred '
'to the new file.\n' 'to the new file.\n'
'You can inspect and modify your configuration ' 'You can inspect and modify your configuration '
@ -60,4 +66,35 @@ def update(conf, code_version, repo_version):
) )
return True 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 return False

@ -9,13 +9,13 @@ def resolve_citekey(repo, citekey, ui=None, exit_on_fail=True):
citekeys = repo.citekeys_from_prefix(citekey) citekeys = repo.citekeys_from_prefix(citekey)
if len(citekeys) == 0: if len(citekeys) == 0:
if ui is not None: 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: if exit_on_fail:
ui.exit() ui.exit()
elif len(citekeys) == 1: elif len(citekeys) == 1:
if citekeys[0] != citekey: if citekeys[0] != citekey:
if ui is not None: 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] citekey = citekeys[0]
elif citekey not in citekeys: elif citekey not in citekeys:
if ui is not None: if ui is not None:

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

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

Loading…
Cancel
Save