From 3099d353f96706fc0912fa0eb96840b8d92bb2a5 Mon Sep 17 00:00:00 2001 From: Fabien Benureau Date: Mon, 7 Dec 2015 09:47:56 +0100 Subject: [PATCH] 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. --- pubs/color.py | 23 +++++++++------- pubs/commands/attach_cmd.py | 4 +-- pubs/commands/import_cmd.py | 2 +- pubs/commands/init_cmd.py | 4 +-- pubs/commands/open_cmd.py | 4 +-- pubs/commands/remove_cmd.py | 6 ++-- pubs/commands/tag_cmd.py | 5 ++-- pubs/config/spec.py | 33 ++++++++++++++++++---- pubs/pretty.py | 13 +++++---- pubs/uis.py | 10 ++----- pubs/update.py | 55 +++++++++++++++++++++++++++++++------ pubs/utils.py | 4 +-- tests/test_color.py | 2 +- tests/test_pretty.py | 5 ++-- 14 files changed, 115 insertions(+), 55 deletions(-) diff --git a/pubs/color.py b/pubs/color.py index 2319e6a..ef22452 100644 --- a/pubs/color.py +++ b/pubs/color.py @@ -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' diff --git a/pubs/commands/attach_cmd.py b/pubs/commands/attach_cmd.py index 41bcdea..99bd10b 100644 --- a/pubs/commands/attach_cmd.py +++ b/pubs/commands/attach_cmd.py @@ -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) diff --git a/pubs/commands/import_cmd.py b/pubs/commands/import_cmd.py index 30d200e..ceda2bd 100644 --- a/pubs/commands/import_cmd.py +++ b/pubs/commands/import_cmd.py @@ -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)) diff --git a/pubs/commands/init_cmd.py b/pubs/commands/init_cmd.py index 6329e0e..048e713 100644 --- a/pubs/commands/init_cmd.py +++ b/pubs/commands/init_cmd.py @@ -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 diff --git a/pubs/commands/open_cmd.py b/pubs/commands/open_cmd.py index fa4cec0..226d236 100644 --- a/pubs/commands/open_cmd.py +++ b/pubs/commands/open_cmd.py @@ -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) diff --git a/pubs/commands/remove_cmd.py b/pubs/commands/remove_cmd.py index 47e20d8..ec39f48 100644 --- a/pubs/commands/remove_cmd.py +++ b/pubs/commands/remove_cmd.py @@ -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]))) diff --git a/pubs/commands/tag_cmd.py b/pubs/commands/tag_cmd.py index fb8e9a2..29e1078 100644 --- a/pubs/commands/tag_cmd.py +++ b/pubs/commands/tag_cmd.py @@ -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: diff --git a/pubs/config/spec.py b/pubs/config/spec.py index 6f8be61..699a26d 100644 --- a/pubs/config/spec.py +++ b/pubs/config/spec.py @@ -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] diff --git a/pubs/pretty.py b/pubs/pretty.py index 307637a..0859b61 100644 --- a/pubs/pretty.py +++ b/pubs/pretty.py @@ -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) diff --git a/pubs/uis.py b/pubs/uis.py index 82e8744..ac566bf 100644 --- a/pubs/uis.py +++ b/pubs/uis.py @@ -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)]) diff --git a/pubs/update.py b/pubs/update.py index 7f90832..71c582c 100644 --- a/pubs/update.py +++ b/pubs/update.py @@ -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 diff --git a/pubs/utils.py b/pubs/utils.py index 8ca1a2a..ed06087 100644 --- a/pubs/utils.py +++ b/pubs/utils.py @@ -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: diff --git a/tests/test_color.py b/tests/test_color.py index 0590ecf..ee86c42 100644 --- a/tests/test_color.py +++ b/tests/test_color.py @@ -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__': diff --git a/tests/test_pretty.py b/tests/test_pretty.py index be0648d..3985361 100644 --- a/tests/test_pretty.py +++ b/tests/test_pretty.py @@ -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()