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.
This commit is contained in:
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)
|
||||
|
10
pubs/uis.py
10
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)])
|
||||
|
@ -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…
x
Reference in New Issue
Block a user