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