Mercurial > hg
changeset 10826:717c35d55fb3
color: colorize based on output labels instead of parsing output
By overriding ui.write(), ui.write_err(), ui.popbuffer(), and ui.label(),
the color extension can avoid parsing command output and simply colorize
output based on labels.
As before, the color extension provides a list of default colors for
core commands/labels. Other extensions can provide their own defaults by
specifying a colortable dict (similar to cmdtable).
In this process, --color is promoted to a global option and the deprecated
--no-color option is removed.
author | Brodie Rao <brodie@bitheap.org> |
---|---|
date | Fri, 02 Apr 2010 15:22:17 -0500 |
parents | 781689b9b6bb |
children | b66388f6adfa |
files | hgext/bookmarks.py hgext/color.py hgext/mq.py tests/test-bookmarks tests/test-bookmarks-current tests/test-bookmarks-current.out tests/test-bookmarks.out tests/test-churn tests/test-churn.out tests/test-eolfilename.out tests/test-grep tests/test-grep.out tests/test-log tests/test-log.out tests/test-mq-guards tests/test-mq-guards.out |
diffstat | 16 files changed, 183 insertions(+), 296 deletions(-) [+] |
line wrap: on
line diff
--- a/hgext/bookmarks.py Fri Apr 02 15:22:15 2010 -0500 +++ b/hgext/bookmarks.py Fri Apr 02 15:22:17 2010 -0500 @@ -339,3 +339,5 @@ ('m', 'rename', '', _('rename a given bookmark'))], _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')), } + +colortable = {'bookmarks.current': 'green'}
--- a/hgext/color.py Fri Apr 02 15:22:15 2010 -0500 +++ b/hgext/color.py Fri Apr 02 15:22:17 2010 -0500 @@ -65,310 +65,118 @@ import os, sys -from mercurial import cmdutil, commands, extensions +from mercurial import commands, dispatch, extensions from mercurial.i18n import _ +from mercurial.ui import ui as uicls # start and stop parameters for effects -_effect_params = {'none': 0, - 'black': 30, - 'red': 31, - 'green': 32, - 'yellow': 33, - 'blue': 34, - 'magenta': 35, - 'cyan': 36, - 'white': 37, - 'bold': 1, - 'italic': 3, - 'underline': 4, - 'inverse': 7, - 'black_background': 40, - 'red_background': 41, - 'green_background': 42, - 'yellow_background': 43, - 'blue_background': 44, - 'purple_background': 45, - 'cyan_background': 46, - 'white_background': 47} +_effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, + 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1, + 'italic': 3, 'underline': 4, 'inverse': 7, + 'black_background': 40, 'red_background': 41, + 'green_background': 42, 'yellow_background': 43, + 'blue_background': 44, 'purple_background': 45, + 'cyan_background': 46, 'white_background': 47} + +_styles = {'grep.match': 'red bold', + 'diff.changed': 'white', + 'diff.deleted': 'red', + 'diff.diffline': 'bold', + 'diff.extended': 'cyan bold', + 'diff.file_a': 'red bold', + 'diff.file_b': 'green bold', + 'diff.hunk': 'magenta', + 'diff.inserted': 'green', + 'diff.trailingwhitespace': 'bold red_background', + 'diffstat.deleted': 'red', + 'diffstat.inserted': 'green', + 'log.changeset': 'yellow', + 'resolve.resolved': 'green bold', + 'resolve.unresolved': 'red bold', + 'status.added': 'green bold', + 'status.clean': 'none', + 'status.copied': 'none', + 'status.deleted': 'cyan bold underline', + 'status.ignored': 'black bold', + 'status.modified': 'blue bold', + 'status.removed': 'red bold', + 'status.unknown': 'magenta bold underline'} + def render_effects(text, effects): 'Wrap text in commands to turn on each effect.' - start = [str(_effect_params[e]) for e in ['none'] + effects] + if not text: + return text + start = [str(_effects[e]) for e in ['none'] + effects.split()] start = '\033[' + ';'.join(start) + 'm' - stop = '\033[' + str(_effect_params['none']) + 'm' - return ''.join([start, text, stop]) - -def _colorstatuslike(abbreviations, effectdefs, orig, ui, repo, *pats, **opts): - '''run a status-like command with colorized output''' - delimiter = opts.get('print0') and '\0' or '\n' - - nostatus = opts.get('no_status') - opts['no_status'] = False - # run original command and capture its output - ui.pushbuffer() - retval = orig(ui, repo, *pats, **opts) - # filter out empty strings - lines_with_status = [line for line in ui.popbuffer().split(delimiter) if line] - - if nostatus: - lines = [l[2:] for l in lines_with_status] + stop = '\033[' + str(_effects['none']) + 'm' + if text[-1] == '\n': + return ''.join([start, text[:-1], stop, '\n']) else: - lines = lines_with_status - - # apply color to output and display it - for i in xrange(len(lines)): - try: - status = abbreviations[lines_with_status[i][0]] - except KeyError: - # Ignore lines with invalid codes, especially in the case of - # of unknown filenames containing newlines (issue2036). - pass - else: - effects = effectdefs[status] - if effects: - lines[i] = render_effects(lines[i], effects) - ui.write(lines[i] + delimiter) - return retval - - -_status_abbreviations = { 'M': 'modified', - 'A': 'added', - 'R': 'removed', - '!': 'deleted', - '?': 'unknown', - 'I': 'ignored', - 'C': 'clean', - ' ': 'copied', } - -_status_effects = { 'modified': ['blue', 'bold'], - 'added': ['green', 'bold'], - 'removed': ['red', 'bold'], - 'deleted': ['cyan', 'bold', 'underline'], - 'unknown': ['magenta', 'bold', 'underline'], - 'ignored': ['black', 'bold'], - 'clean': ['none'], - 'copied': ['none'], } - -def colorstatus(orig, ui, repo, *pats, **opts): - '''run the status command with colored output''' - return _colorstatuslike(_status_abbreviations, _status_effects, - orig, ui, repo, *pats, **opts) - - -_resolve_abbreviations = { 'U': 'unresolved', - 'R': 'resolved', } - -_resolve_effects = { 'unresolved': ['red', 'bold'], - 'resolved': ['green', 'bold'], } - -def colorresolve(orig, ui, repo, *pats, **opts): - '''run the resolve command with colored output''' - if not opts.get('list'): - # only colorize for resolve -l - return orig(ui, repo, *pats, **opts) - return _colorstatuslike(_resolve_abbreviations, _resolve_effects, - orig, ui, repo, *pats, **opts) - - -_bookmark_effects = { 'current': ['green'] } - -def colorbookmarks(orig, ui, repo, *pats, **opts): - def colorize(orig, s): - lines = s.split('\n') - for i, line in enumerate(lines): - if line.startswith(" *"): - lines[i] = render_effects(line, _bookmark_effects['current']) - orig('\n'.join(lines)) - oldwrite = extensions.wrapfunction(ui, 'write', colorize) - try: - orig(ui, repo, *pats, **opts) - finally: - ui.write = oldwrite - -def colorqseries(orig, ui, repo, *dummy, **opts): - '''run the qseries command with colored output''' - ui.pushbuffer() - retval = orig(ui, repo, **opts) - patchlines = ui.popbuffer().splitlines() - patchnames = repo.mq.series - - for patch, patchname in zip(patchlines, patchnames): - if opts['missing']: - effects = _patch_effects['missing'] - # Determine if patch is applied. - elif [applied for applied in repo.mq.applied - if patchname == applied.name]: - effects = _patch_effects['applied'] - else: - effects = _patch_effects['unapplied'] - - patch = patch.replace(patchname, render_effects(patchname, effects), 1) - ui.write(patch + '\n') - return retval - -_patch_effects = { 'applied': ['blue', 'bold', 'underline'], - 'missing': ['red', 'bold'], - 'unapplied': ['black', 'bold'], } -def colorwrap(orig, *args): - '''wrap ui.write for colored diff output''' - def _colorize(s): - lines = s.split('\n') - for i, line in enumerate(lines): - stripline = line - if line and line[0] in '+-': - # highlight trailing whitespace, but only in changed lines - stripline = line.rstrip() - for prefix, style in _diff_prefixes: - if stripline.startswith(prefix): - lines[i] = render_effects(stripline, _diff_effects[style]) - break - if line != stripline: - lines[i] += render_effects( - line[len(stripline):], _diff_effects['trailingwhitespace']) - return '\n'.join(lines) - orig(*[_colorize(s) for s in args]) + return ''.join([start, text, stop]) -def colorshowpatch(orig, self, node): - '''wrap cmdutil.changeset_printer.showpatch with colored output''' - oldwrite = extensions.wrapfunction(self.ui, 'write', colorwrap) - try: - orig(self, node) - finally: - self.ui.write = oldwrite - -def colordiffstat(orig, s): - lines = s.split('\n') - for i, line in enumerate(lines): - if line and line[-1] in '+-': - name, graph = line.rsplit(' ', 1) - graph = graph.replace('-', - render_effects('-', _diff_effects['deleted'])) - graph = graph.replace('+', - render_effects('+', _diff_effects['inserted'])) - lines[i] = ' '.join([name, graph]) - orig('\n'.join(lines)) - -def colordiff(orig, ui, repo, *pats, **opts): - '''run the diff command with colored output''' - if opts.get('stat'): - wrapper = colordiffstat - else: - wrapper = colorwrap - oldwrite = extensions.wrapfunction(ui, 'write', wrapper) - try: - orig(ui, repo, *pats, **opts) - finally: - ui.write = oldwrite - -def colorchurn(orig, ui, repo, *pats, **opts): - '''run the churn command with colored output''' - if not opts.get('diffstat'): - return orig(ui, repo, *pats, **opts) - oldwrite = extensions.wrapfunction(ui, 'write', colordiffstat) - try: - orig(ui, repo, *pats, **opts) - finally: - ui.write = oldwrite - -_diff_prefixes = [('diff', 'diffline'), - ('copy', 'extended'), - ('rename', 'extended'), - ('old', 'extended'), - ('new', 'extended'), - ('deleted', 'extended'), - ('---', 'file_a'), - ('+++', 'file_b'), - ('@', 'hunk'), - ('-', 'deleted'), - ('+', 'inserted')] - -_diff_effects = {'diffline': ['bold'], - 'extended': ['cyan', 'bold'], - 'file_a': ['red', 'bold'], - 'file_b': ['green', 'bold'], - 'hunk': ['magenta'], - 'deleted': ['red'], - 'inserted': ['green'], - 'changed': ['white'], - 'trailingwhitespace': ['bold', 'red_background']} +def extstyles(): + for name, ext in extensions.extensions(): + _styles.update(getattr(ext, 'colortable', {})) -def extsetup(ui): - '''Initialize the extension.''' - _setupcmd(ui, 'diff', commands.table, colordiff, _diff_effects) - _setupcmd(ui, 'incoming', commands.table, None, _diff_effects) - _setupcmd(ui, 'log', commands.table, None, _diff_effects) - _setupcmd(ui, 'outgoing', commands.table, None, _diff_effects) - _setupcmd(ui, 'tip', commands.table, None, _diff_effects) - _setupcmd(ui, 'status', commands.table, colorstatus, _status_effects) - _setupcmd(ui, 'resolve', commands.table, colorresolve, _resolve_effects) - - try: - mq = extensions.find('mq') - _setupcmd(ui, 'qdiff', mq.cmdtable, colordiff, _diff_effects) - _setupcmd(ui, 'qseries', mq.cmdtable, colorqseries, _patch_effects) - except KeyError: - mq = None - - try: - rec = extensions.find('record') - _setupcmd(ui, 'record', rec.cmdtable, colordiff, _diff_effects) - except KeyError: - rec = None - - if mq and rec: - _setupcmd(ui, 'qrecord', rec.cmdtable, colordiff, _diff_effects) - try: - churn = extensions.find('churn') - _setupcmd(ui, 'churn', churn.cmdtable, colorchurn, _diff_effects) - except KeyError: - churn = None - - try: - bookmarks = extensions.find('bookmarks') - _setupcmd(ui, 'bookmarks', bookmarks.cmdtable, colorbookmarks, - _bookmark_effects) - except KeyError: - # The bookmarks extension is not enabled - pass - -def _setupcmd(ui, cmd, table, func, effectsmap): - '''patch in command to command table and load effect map''' - def nocolor(orig, *args, **opts): - - if (opts['no_color'] or opts['color'] == 'never' or - (opts['color'] == 'auto' and (os.environ.get('TERM') == 'dumb' - or not sys.__stdout__.isatty()))): - del opts['no_color'] - del opts['color'] - return orig(*args, **opts) - - oldshowpatch = extensions.wrapfunction(cmdutil.changeset_printer, - 'showpatch', colorshowpatch) - del opts['no_color'] - del opts['color'] - try: - if func is not None: - return func(orig, *args, **opts) - return orig(*args, **opts) - finally: - cmdutil.changeset_printer.showpatch = oldshowpatch - - entry = extensions.wrapcommand(table, cmd, nocolor) - entry[1].extend([ - ('', 'color', 'auto', _("when to colorize (always, auto, or never)")), - ('', 'no-color', None, _("don't colorize output (DEPRECATED)")), - ]) - - for status in effectsmap: - configkey = cmd + '.' + status - effects = ui.configlist('color', configkey) - if effects: +def configstyles(ui): + for status, cfgeffects in ui.configitems('color'): + if '.' not in status: + continue + cfgeffects = ui.configlist('color', status) + if cfgeffects: good = [] - for e in effects: - if e in _effect_params: + for e in cfgeffects: + if e in _effects: good.append(e) else: ui.warn(_("ignoring unknown color/effect %r " "(configured in color.%s)\n") - % (e, configkey)) - effectsmap[status] = good + % (e, status)) + _styles[status] = ' '.join(good) + +_buffers = None +def style(msg, label): + effects = '' + for l in label.split(): + effects += _styles.get(l, '') + if effects: + return render_effects(msg, effects) + return msg + +def popbuffer(orig, labeled=False): + global _buffers + if labeled: + return ''.join(style(a, label) for a, label in _buffers.pop()) + return ''.join(a for a, label in _buffers.pop()) + +def write(orig, *args, **opts): + label = opts.get('label', '') + global _buffers + if _buffers: + _buffers[-1].extend([(str(a), label) for a in args]) + else: + return orig(*[style(str(a), label) for a in args], **opts) + +def write_err(orig, *args, **opts): + label = opts.get('label', '') + return orig(*[style(str(a), label) for a in args], **opts) + +def uisetup(ui): + def colorcmd(orig, ui_, opts, cmd, cmdfunc): + if (opts['color'] == 'always' or + (opts['color'] == 'auto' and (os.environ.get('TERM') != 'dumb' + and sys.__stdout__.isatty()))): + global _buffers + _buffers = ui_._buffers + extensions.wrapfunction(ui_, 'popbuffer', popbuffer) + extensions.wrapfunction(ui_, 'write', write) + extensions.wrapfunction(ui_, 'write_err', write_err) + ui_.label = style + extstyles() + configstyles(ui) + return orig(ui_, opts, cmd, cmdfunc) + extensions.wrapfunction(dispatch, '_runcommand', colorcmd) + +commands.globalopts.append(('', 'color', 'auto', + _("when to colorize (always, auto, or never)")))
--- a/hgext/mq.py Fri Apr 02 15:22:15 2010 -0500 +++ b/hgext/mq.py Fri Apr 02 15:22:17 2010 -0500 @@ -2810,3 +2810,11 @@ [('a', 'applied', None, _('finish all applied changesets'))], _('hg qfinish [-a] [REV]...')), } + +colortable = {'qguard.negative': 'red', + 'qguard.positive': 'yellow', + 'qguard.unguarded': 'green', + 'qseries.applied': 'blue bold underline', + 'qseries.guarded': 'black bold', + 'qseries.missing': 'red bold', + 'qseries.unapplied': 'black bold'}
--- a/tests/test-bookmarks Fri Apr 02 15:22:15 2010 -0500 +++ b/tests/test-bookmarks Fri Apr 02 15:22:17 2010 -0500 @@ -14,6 +14,9 @@ echo % list bookmarks hg bookmarks +echo % list bookmarks with color +hg --config extensions.color= bookmarks --color=always + echo a > a hg add a hg commit -m 0
--- a/tests/test-bookmarks-current Fri Apr 02 15:22:15 2010 -0500 +++ b/tests/test-bookmarks-current Fri Apr 02 15:22:17 2010 -0500 @@ -17,6 +17,9 @@ echo % list bookmarks hg bookmark +echo % list bookmarks with color +hg --config extensions.color= bookmark --color=always + echo % update to bookmark X hg update X
--- a/tests/test-bookmarks-current.out Fri Apr 02 15:22:15 2010 -0500 +++ b/tests/test-bookmarks-current.out Fri Apr 02 15:22:17 2010 -0500 @@ -3,6 +3,8 @@ % set bookmark X % list bookmarks * X -1:000000000000 +% list bookmarks with color +[0;32m * X -1:000000000000[0m % update to bookmark X 0 files updated, 0 files merged, 0 files removed, 0 files unresolved % list bookmarks
--- a/tests/test-bookmarks.out Fri Apr 02 15:22:15 2010 -0500 +++ b/tests/test-bookmarks.out Fri Apr 02 15:22:17 2010 -0500 @@ -3,6 +3,8 @@ % bookmark rev -1 % list bookmarks * X -1:000000000000 +% list bookmarks with color +[0;32m * X -1:000000000000[0m % bookmark X moved to rev 0 * X 0:f7b1eb17ad24 % look up bookmark
--- a/tests/test-churn Fri Apr 02 15:22:15 2010 -0500 +++ b/tests/test-churn Fri Apr 02 15:22:17 2010 -0500 @@ -52,6 +52,8 @@ hg rm d/g/f2.txt hg ci -Am "removed d/g/f2.txt" -u user1 -d 14:00 d/g/f2.txt hg churn --diffstat +echo % churn --diffstat with color +hg --config extensions.color= churn --diffstat --color=always echo % changeset number churn hg churn -c
--- a/tests/test-churn.out Fri Apr 02 15:22:15 2010 -0500 +++ b/tests/test-churn.out Fri Apr 02 15:22:17 2010 -0500 @@ -32,6 +32,10 @@ user1 +3/-1 +++++++++++++++++++++++++++++++++++++++++-------------- user3 +3/-0 +++++++++++++++++++++++++++++++++++++++++ user2 +2/-0 +++++++++++++++++++++++++++ +% churn --diffstat with color +user1 +3/-1 [0;32m+++++++++++++++++++++++++++++++++++++++++[0m[0;31m--------------[0m +user3 +3/-0 [0;32m+++++++++++++++++++++++++++++++++++++++++[0m +user2 +2/-0 [0;32m+++++++++++++++++++++++++++[0m % changeset number churn user1 4 *************************************************************** user3 3 ***********************************************
--- a/tests/test-eolfilename.out Fri Apr 02 15:22:15 2010 -0500 +++ b/tests/test-eolfilename.out Fri Apr 02 15:22:17 2010 -0500 @@ -14,7 +14,7 @@ o hell o % test issue2039 -[0;35;1;4m? foo[0m -bar -[0;35;1;4m? foo[0m -bar.baz +[0;35;1;4m? foo +bar[0m +[0;35;1;4m? foo +bar.baz[0m
--- a/tests/test-grep Fri Apr 02 15:22:15 2010 -0500 +++ b/tests/test-grep Fri Apr 02 15:22:17 2010 -0500 @@ -21,6 +21,8 @@ hg grep '**test**' echo % simple hg grep port port +echo % simple with color +hg --config extensions.color= grep --color=always port port echo % all hg grep --traceback --all -nu port port echo % other
--- a/tests/test-grep.out Fri Apr 02 15:22:15 2010 -0500 +++ b/tests/test-grep.out Fri Apr 02 15:22:17 2010 -0500 @@ -4,6 +4,10 @@ port:4:export port:4:vaportight port:4:import/export +% simple with color +port:4:ex[0;31;1mport[0m +port:4:va[0;31;1mport[0might +port:4:im[0;31;1mport[0m/export % all port:4:4:-:spam:import/export port:3:4:+:eggs:import/export
--- a/tests/test-log Fri Apr 02 15:22:15 2010 -0500 +++ b/tests/test-log Fri Apr 02 15:22:17 2010 -0500 @@ -118,6 +118,9 @@ echo '% log -d -1' hg log -d -1 +echo '% log -p -l2 --color=always' +hg --config extensions.color= log -p -l2 --color=always + cd .. hg init usertest
--- a/tests/test-log.out Fri Apr 02 15:22:15 2010 -0500 +++ b/tests/test-log.out Fri Apr 02 15:22:17 2010 -0500 @@ -279,6 +279,33 @@ summary: r1 % log -d -1 +% log -p -l2 --color=always +[0;33mchangeset: 6:2404bbcab562[0m +tag: tip +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: b1.1 + +[0;1mdiff -r 302e9dd6890d -r 2404bbcab562 b1[0m +[0;31;1m--- a/b1 Thu Jan 01 00:00:01 1970 +0000[0m +[0;32;1m+++ b/b1 Thu Jan 01 00:00:01 1970 +0000[0m +[0;35m@@ -1,1 +1,2 @@[0m + b1 +[0;32m+postm[0m + +[0;33mchangeset: 5:302e9dd6890d[0m +parent: 3:e62f78d544b4 +parent: 4:ddb82e70d1a1 +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: m12 + +[0;1mdiff -r e62f78d544b4 -r 302e9dd6890d b2[0m +[0;31;1m--- /dev/null Thu Jan 01 00:00:00 1970 +0000[0m +[0;32;1m+++ b/b2 Thu Jan 01 00:00:01 1970 +0000[0m +[0;35m@@ -0,0 +1,1 @@[0m +[0;32m+b2[0m + adding a adding b changeset: 0:29a4c94f1924
--- a/tests/test-mq-guards Fri Apr 02 15:22:15 2010 -0500 +++ b/tests/test-mq-guards Fri Apr 02 15:22:17 2010 -0500 @@ -99,6 +99,8 @@ hg qselect 1 2 3 echo % list patches and guards hg qguard -l +echo % list patches and guards with color +hg --config extensions.color= qguard -l --color=always echo % list series hg qseries -v echo % list guards @@ -125,6 +127,8 @@ echo % should show new.patch and b.patch as Guarded, c.patch as Applied echo % and d.patch as Unapplied hg qseries -v +echo % qseries again, but with color +hg --config extensions.color= qseries -v --color=always hg qguard d.patch +2 echo % new.patch, b.patch: Guarded. c.patch: Applied. d.patch: Guarded. @@ -159,3 +163,5 @@ echo the guards file was not ignored in the past hg qdelete -k b.patch hg qseries -m +echo % hg qseries -m with color +hg --config extensions.color= qseries -m --color=always
--- a/tests/test-mq-guards.out Fri Apr 02 15:22:15 2010 -0500 +++ b/tests/test-mq-guards.out Fri Apr 02 15:22:17 2010 -0500 @@ -84,6 +84,10 @@ a.patch: +1 +2 -3 b.patch: +2 c.patch: unguarded +% list patches and guards with color +a.patch: [0;33m+1[0m [0;33m+2[0m [0;31m-3[0m +b.patch: [0;33m+2[0m +c.patch: [0;32munguarded[0m % list series 0 G a.patch 1 U b.patch @@ -126,6 +130,11 @@ 1 G b.patch 2 A c.patch 3 U d.patch +% qseries again, but with color +[0;30;1m0 G new.patch[0m +[0;30;1m1 G b.patch[0m +[0;34;1;4m2 A c.patch[0m +[0;30;1m3 U d.patch[0m % new.patch, b.patch: Guarded. c.patch: Applied. d.patch: Guarded. 0 G new.patch 1 G b.patch @@ -206,3 +215,5 @@ % hg qseries -m: only b.patch should be shown the guards file was not ignored in the past b.patch +% hg qseries -m with color +[0;31;1mb.patch[0m