Add colored output to status and qseries commands
authorKevin Christen <kevin.christen@gmail.com>
Mon, 31 Dec 2007 09:15:39 -0600
changeset 5787 b7b22a2ade2e
parent 5786 c69ef6fdb092
child 5788 4107e823dc2c
Add colored output to status and qseries commands
hgext/color.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/color.py	Mon Dec 31 09:15:39 2007 -0600
@@ -0,0 +1,220 @@
+# color.py color output for the status and qseries commands
+#
+# Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
+# Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program.  If not, see <http://www.gnu.org/licenses/>.  This
+# software may be used and distributed according to the terms of the GNU
+# General Public License, incorporated herein by reference.
+
+'''add color output to the status and qseries commands
+
+This extension modifies the status command to add color to its output to
+reflect file status, and the qseries command to add color to reflect patch
+status (applied, unapplied, missing).  Other effects in addition to color,
+like bold and underlined text, are also available.  Effects are rendered
+with the ECMA-48 SGR control function (aka ANSI escape codes).  This module
+also provides the render_text function, which can be used to add effects to
+any text.
+
+To enable this extension, add this to your .hgrc file:
+[extensions]
+color =
+
+Default effects my be overriden from the .hgrc file:
+
+[color]
+status.modified = blue bold underline red_background
+status.added = green bold
+status.removed = red bold blue_background
+status.deleted = cyan bold underline
+status.unknown = magenta bold underline
+status.ignored = black bold
+
+ 'none' turns off all effects
+status.clean = none
+status.copied = none
+
+qseries.applied = blue bold underline
+qseries.unapplied = black bold
+qseries.missing = red bold
+'''
+
+import re, sys
+
+from mercurial import commands, cmdutil, ui
+from mercurial.i18n import _
+
+# start and stop parameters for effects
+_effect_params = { 'none': (0, 0),
+                   'black': (30, 39),
+                   'red': (31, 39),
+                   'green': (32, 39),
+                   'yellow': (33, 39),
+                   'blue': (34, 39),
+                   'magenta': (35, 39),
+                   'cyan': (36, 39),
+                   'white': (37, 39),
+                   'bold': (1, 22),
+                   'italic': (3, 23),
+                   'underline': (4, 24),
+                   'inverse': (7, 27),
+                   'black_background': (40, 49),
+                   'red_background': (41, 49),
+                   'green_background': (42, 49),
+                   'yellow_background': (43, 49),
+                   'blue_background': (44, 49),
+                   'purple_background': (45, 49),
+                   'cyan_background': (46, 49),
+                   'white_background': (47, 49), }
+
+def render_effects(text, *effects):
+    'Wrap text in commands to turn on each effect.'
+    start = []
+    stop = []
+    for effect in effects:
+        start.append(str(_effect_params[effect][0]))
+        stop.append(str(_effect_params[effect][1]))
+    start = '\033[' + ';'.join(start) + 'm'
+    stop = '\033[' + ';'.join(stop) + 'm'
+    return start + text + stop
+
+def colorstatus(statusfunc, ui, repo, *pats, **opts):
+    '''run the status command with colored output'''
+
+    delimiter = opts['print0'] and '\0' or '\n'
+
+    # run status and capture it's output
+    ui.pushbuffer()
+    retval = statusfunc(ui, repo, *pats, **opts)
+    # filter out empty strings
+    lines = [ line for line in ui.popbuffer().split(delimiter) if line ]
+
+    if opts['no_status']:
+        # if --no-status, run the command again without that option to get
+        # output with status abbreviations
+        opts['no_status'] = False
+        ui.pushbuffer()
+        statusfunc(ui, repo, *pats, **opts)
+        # filter out empty strings
+        lines_with_status = [ line for
+                              line in ui.popbuffer().split(delimiter) if line ]
+    else:
+        lines_with_status = lines
+
+    # apply color to output and display it
+    for i in xrange(0, len(lines)):
+        status = _status_abbreviations[lines_with_status[i][0]]
+        effects = _status_effects[status]
+        if effects:
+            lines[i] = render_effects(lines[i], *effects)
+        sys.stdout.write(lines[i] + delimiter)
+    return retval
+
+_status_abbreviations = { 'M': 'modified',
+                          'A': 'added',
+                          'R': 'removed',
+                          'D': '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 colorqseries(qseriesfunc, ui, repo, *dummy, **opts):
+    '''run the qseries command with colored output'''
+    ui.pushbuffer()
+    retval = qseriesfunc(ui, repo, **opts)
+    patches = ui.popbuffer().splitlines()
+    for patch in patches:
+        if opts['missing']:
+            effects = _patch_effects['missing']
+        # Determine if patch is applied.  Search for beginning of output
+        # line in the applied patch list, in case --summary has been used
+        # and output line isn't just the patch name.
+        elif [ applied for applied in repo.mq.applied
+               if patch.startswith(applied.name) ]:
+            effects = _patch_effects['applied']
+        else:
+            effects = _patch_effects['unapplied']
+        sys.stdout.write(render_effects(patch, *effects) + '\n')
+    return retval
+
+_patch_effects = { 'applied': ('blue', 'bold', 'underline'),
+                   'missing': ('red', 'bold'),
+                   'unapplied': ('black', 'bold'), }
+
+def uisetup(ui):
+    '''Initialize the extension.'''
+    nocoloropt = ('', 'no-color', None, _("don't colorize output"))
+    _decoratecmd(ui, 'status', commands.table, colorstatus, nocoloropt)
+    _configcmdeffects(ui, 'status', _status_effects);
+    if ui.config('extensions', 'hgext.mq', default=None) is not None:
+        from hgext import mq
+        _decoratecmd(ui, 'qseries', mq.cmdtable, colorqseries, nocoloropt)
+        _configcmdeffects(ui, 'qseries', _patch_effects);
+
+def _decoratecmd(ui, cmd, table, delegate, *delegateoptions):
+    '''Replace the function that implements cmd in table with a decorator.
+
+    The decorator that becomes the new implementation of cmd calls
+    delegate.  The delegate's first argument is the replaced function,
+    followed by the normal Mercurial command arguments (ui, repo, ...).  If
+    the delegate adds command options, supply them as delegateoptions.
+    '''
+    cmdkey, cmdentry = _cmdtableitem(ui, cmd, table)
+    decorator = lambda ui, repo, *args, **opts: \
+                    _colordecorator(delegate, cmdentry[0],
+                                    ui, repo, *args, **opts)
+    # make sure 'hg help cmd' still works
+    decorator.__doc__ = cmdentry[0].__doc__
+    decoratorentry = (decorator,) + cmdentry[1:]
+    for option in delegateoptions:
+        decoratorentry[1].append(option)
+    table[cmdkey] = decoratorentry
+
+def _cmdtableitem(ui, cmd, table):
+    '''Return key, value from table for cmd, or None if not found.'''
+    aliases, entry = cmdutil.findcmd(ui, cmd, table)
+    for candidatekey, candidateentry in table.iteritems():
+        if candidateentry is entry:
+            return candidatekey, entry
+
+def _colordecorator(colorfunc, nocolorfunc, ui, repo, *args, **opts):
+    '''Delegate to colorfunc or nocolorfunc, depending on conditions.
+
+    Delegate to colorfunc unless --no-color option is set or output is not
+    to a tty.
+    '''
+    if opts['no_color'] or not sys.stdout.isatty():
+        return nocolorfunc(ui, repo, *args, **opts)
+    return colorfunc(nocolorfunc, ui, repo, *args, **opts)
+
+def _configcmdeffects(ui, cmdname, effectsmap):
+    '''Override default effects for cmdname with those from .hgrc file.
+
+    Entries in the .hgrc file are in the [color] section, and look like
+    'cmdname'.'status' (for instance, 'status.modified = blue bold inverse').
+    '''
+    for status in effectsmap:
+        effects = ui.config('color', cmdname + '.' + status)
+        if effects:
+            effectsmap[status] = re.split('\W+', effects)