[PATCH 1 of 2] ui: add ui.write() label API

Brodie Rao dackze at gmail.com
Fri Mar 26 14:14:16 CDT 2010


# HG changeset patch
# User Brodie Rao <me+hg at dackz.net>
# Date 1266550756 18000
# Node ID c5eaa1e8207881ad48aaeaf98fcd642bdb6c76b3
# Parent  bd36e5c0ccb110c11bf0bda72ea77171d6844f18
ui: add ui.write() label API

This adds output labeling support with the following methods:

- ui.write(..., label='topic.name topic2.name2 ...')
- ui.write_err(.., label=...)
- ui.popbuffer(labeled=False)
- ui.style(msg, label)

By adding an API to label output directly, the color extension can forgo
parsing command output and instead override the above methods to insert
ANSI color codes.

GUI tools can also override the above methods and use the labels to do
GUI-specific styling.

In addition to the color extension, the following commands have been
updated to use the new API:

- bookmarks
- churn
- diff
- grep
- log (and other commands using the changeset printer)
- record
- qdiff
- qguard
- qseries

diff --git a/hgext/bookmarks.py b/hgext/bookmarks.py
--- a/hgext/bookmarks.py
+++ b/hgext/bookmarks.py
@@ -152,15 +152,22 @@ def bookmark(ui, repo, mark=None, rev=No
             for bmark, n in marks.iteritems():
                 if ui.configbool('bookmarks', 'track.current'):
                     current = repo._bookmarkcurrent
-                    prefix = (bmark == current and n == cur) and '*' or ' '
+                    if bmark == current and n == cur:
+                        prefix, label = '*', 'bookmarks.current'
+                    else:
+                        prefix, label = ' ', ''
                 else:
-                    prefix = (n == cur) and '*' or ' '
+                    if n == cur:
+                        prefix, label = '*', 'bookmarks.current'
+                    else:
+                        prefix, label = ' ', ''
 
                 if ui.quiet:
-                    ui.write("%s\n" % bmark)
+                    ui.write("%s\n" % bmark, label=label)
                 else:
                     ui.write(" %s %-25s %d:%s\n" % (
-                        prefix, bmark, repo.changelog.rev(n), hexfn(n)))
+                        prefix, bmark, repo.changelog.rev(n), hexfn(n)),
+                        label=label)
         return
 
 def _revstostrip(changelog, node):
@@ -332,3 +339,5 @@ cmdtable = {
           ('m', 'rename', '', _('rename a given bookmark'))],
          _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
 }
+
+colortable = {'bookmarks.current': 'green'}
diff --git a/hgext/churn.py b/hgext/churn.py
--- a/hgext/churn.py
+++ b/hgext/churn.py
@@ -153,8 +153,10 @@ def churn(ui, repo, *pats, **opts):
         def format(name, (added, removed)):
             return "%s %15s %s%s\n" % (pad(name, maxname),
                                        '+%d/-%d' % (added, removed),
-                                       '+' * charnum(added),
-                                       '-' * charnum(removed))
+                                       ui.label('+' * charnum(added),
+                                                'diffstat.inserted'),
+                                       ui.label('-' * charnum(removed),
+                                                'diffstat.deleted'))
     else:
         width -= 6
         def format(name, count):
diff --git a/hgext/color.py b/hgext/color.py
--- a/hgext/color.py
+++ b/hgext/color.py
@@ -65,310 +65,119 @@ Default effects may be overridden from t
 
 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])
+    stop = '\033[' + str(_effects['none']) + 'm'
+    if text[-1] == '\n':
+        return ''.join([start, text[:-1], stop, '\n'])
+    else:
+        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'
+def extstyles():
+    for name, ext in extensions.extensions():
+        _styles.update(getattr(ext, 'colortable', {}))
 
-    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]
-    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])
-
-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 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):
+    if labeled:
+        global _buffers
+        return ''.join(style(a, label) for a, label in _buffers.pop())
+    else:
+        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)")))
diff --git a/hgext/mq.py b/hgext/mq.py
--- a/hgext/mq.py
+++ b/hgext/mq.py
@@ -481,15 +481,22 @@ class queue(object):
             opts['unified'] = '0'
 
         m = cmdutil.match(repo, files, opts)
-        chunks = patch.diff(repo, node1, node2, m, changes, diffopts)
-        write = fp is None and repo.ui.write or fp.write
+        if fp is None:
+            write = repo.ui.write
+        else:
+            def write(s, **kw):
+                fp.write(s)
         if stat:
             width = self.ui.interactive() and util.termwidth() or 80
-            write(patch.diffstat(util.iterlines(chunks), width=width,
-                                 git=diffopts.git))
+            chunks = patch.diff(repo, node1, node2, m, changes, diffopts)
+            for chunk, label in patch.diffstatui(util.iterlines(chunks),
+                                                 width=width,
+                                                 git=diffopts.git):
+                write(chunk, label=label)
         else:
-            for chunk in chunks:
-                write(chunk)
+            for chunk, label in patch.diffui(repo, node1, node2, m, changes,
+                                              diffopts):
+                write(chunk, label=label)
 
     def mergeone(self, repo, mergeq, head, patch, rev, diffopts):
         # first try just applying the patch
@@ -1409,7 +1416,7 @@ class queue(object):
 
     def qseries(self, repo, missing=None, start=0, length=None, status=None,
                 summary=False):
-        def displayname(pfx, patchname):
+        def displayname(pfx, patchname, state):
             if summary:
                 ph = patchheader(self.join(patchname), self.plainmode)
                 msg = ph.message and ph.message[0] or ''
@@ -1422,7 +1429,7 @@ class queue(object):
                 msg = "%s%s: %s" % (pfx, patchname, msg)
             else:
                 msg = pfx + patchname
-            self.ui.write(msg + '\n')
+            self.ui.write(msg + '\n', label='qseries.' + state)
 
         applied = set([p.name for p in self.applied])
         if length is None:
@@ -1433,17 +1440,17 @@ class queue(object):
             for i in xrange(start, start + length):
                 patch = self.series[i]
                 if patch in applied:
-                    stat = 'A'
+                    char, state = 'A', 'applied'
                 elif self.pushable(i)[0]:
-                    stat = 'U'
+                    char, state = 'U', 'unapplied'
                 else:
-                    stat = 'G'
+                    char, state = 'G', 'guarded'
                 pfx = ''
                 if self.ui.verbose:
-                    pfx = '%*d %s ' % (idxwidth, i, stat)
-                elif status and status != stat:
+                    pfx = '%*d %s ' % (idxwidth, i, char)
+                elif status and status != char:
                     continue
-                displayname(pfx, patch)
+                displayname(pfx, patch, state)
         else:
             msng_list = []
             for root, dirs, files in os.walk(self.path):
@@ -1457,7 +1464,7 @@ class queue(object):
                         msng_list.append(fl)
             for x in sorted(msng_list):
                 pfx = self.ui.verbose and ('D ') or ''
-                displayname(pfx, x)
+                displayname(pfx, x, 'missing')
 
     def issaveline(self, l):
         if l.name == '.hg.patches.save.line':
@@ -2135,7 +2142,17 @@ def guard(ui, repo, *args, **opts):
     '''
     def status(idx):
         guards = q.series_guards[idx] or ['unguarded']
-        ui.write('%s: %s\n' % (q.series[idx], ' '.join(guards)))
+        ui.write('%s: ' % ui.label(q.series[idx], 'qguard.patch'))
+        for i, guard in enumerate(guards):
+            if guard.startswith('+'):
+                ui.write(guard, label='qguard.positive')
+            elif guard.startswith('-'):
+                ui.write(guard, label='qguard.negative')
+            else:
+                ui.write(guard, label='qguard.unguarded')
+            if i != len(guards) - 1:
+                ui.write(' ')
+        ui.write('\n')
     q = repo.mq
     patch = None
     args = list(args)
@@ -2793,3 +2810,11 @@ cmdtable = {
          [('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'}
diff --git a/hgext/progress.py b/hgext/progress.py
--- a/hgext/progress.py
+++ b/hgext/progress.py
@@ -163,10 +163,10 @@ class progbar(object):
                 self.show(topic, pos, item, unit, total)
         return orig(topic, pos, item=item, unit=unit, total=total)
 
-    def write(self, orig, *args):
+    def write(self, orig, *args, **opts):
         if self.printed:
             self.clear()
-        return orig(*args)
+        return orig(*args, **opts)
 
 sharedprog = None
 
diff --git a/hgext/record.py b/hgext/record.py
--- a/hgext/record.py
+++ b/hgext/record.py
@@ -520,7 +520,18 @@ def dorecord(ui, repo, commitfunc, *pats
                 os.rmdir(backupdir)
             except OSError:
                 pass
-    return cmdutil.commit(ui, repo, recordfunc, pats, opts)
+
+    # wrap ui.write so diff output can be labeled/colorized
+    def wrapwrite(orig, *args, **kw):
+        label = kw.pop('label', '')
+        for chunk, l in patch.difflabel(lambda: args):
+            orig(chunk, label=label + l)
+    oldwrite = ui.write
+    extensions.wrapfunction(ui, 'write', wrapwrite)
+    try:
+        return cmdutil.commit(ui, repo, recordfunc, pats, opts)
+    finally:
+        ui.write = oldwrite
 
 cmdtable = {
     "record":
diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -735,7 +735,7 @@ class changeset_printer(object):
         if self.buffered:
             self.ui.pushbuffer()
             self._show(ctx, copies, props)
-            self.hunk[ctx.rev()] = self.ui.popbuffer()
+            self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
         else:
             self._show(ctx, copies, props)
 
@@ -745,7 +745,8 @@ class changeset_printer(object):
         rev = ctx.rev()
 
         if self.ui.quiet:
-            self.ui.write("%d:%s\n" % (rev, short(changenode)))
+            self.ui.write("%d:%s\n" % (rev, short(changenode)),
+                          label='log.node')
             return
 
         log = self.repo.changelog
@@ -756,52 +757,66 @@ class changeset_printer(object):
         parents = [(p, hexfunc(log.node(p)))
                    for p in self._meaningful_parentrevs(log, rev)]
 
-        self.ui.write(_("changeset:   %d:%s\n") % (rev, hexfunc(changenode)))
+        self.ui.write(_("changeset:   %d:%s\n") % (rev, hexfunc(changenode)),
+                      label='log.changeset')
 
         branch = ctx.branch()
         # don't show the default branch name
         if branch != 'default':
             branch = encoding.tolocal(branch)
-            self.ui.write(_("branch:      %s\n") % branch)
+            self.ui.write(_("branch:      %s\n") % branch,
+                          label='log.branch')
         for tag in self.repo.nodetags(changenode):
-            self.ui.write(_("tag:         %s\n") % tag)
+            self.ui.write(_("tag:         %s\n") % tag,
+                          label='log.tag')
         for parent in parents:
-            self.ui.write(_("parent:      %d:%s\n") % parent)
+            self.ui.write(_("parent:      %d:%s\n") % parent,
+                          label='log.parent')
 
         if self.ui.debugflag:
             mnode = ctx.manifestnode()
             self.ui.write(_("manifest:    %d:%s\n") %
-                          (self.repo.manifest.rev(mnode), hex(mnode)))
-        self.ui.write(_("user:        %s\n") % ctx.user())
-        self.ui.write(_("date:        %s\n") % date)
+                          (self.repo.manifest.rev(mnode), hex(mnode)),
+                          label='ui.debug log.manifest')
+        self.ui.write(_("user:        %s\n") % ctx.user(),
+                      label='log.user')
+        self.ui.write(_("date:        %s\n") % date,
+                      label='log.date')
 
         if self.ui.debugflag:
             files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
             for key, value in zip([_("files:"), _("files+:"), _("files-:")],
                                   files):
                 if value:
-                    self.ui.write("%-12s %s\n" % (key, " ".join(value)))
+                    self.ui.write("%-12s %s\n" % (key, " ".join(value)),
+                                  label='ui.debug log.files')
         elif ctx.files() and self.ui.verbose:
-            self.ui.write(_("files:       %s\n") % " ".join(ctx.files()))
+            self.ui.write(_("files:       %s\n") % " ".join(ctx.files()),
+                          label='ui.note log.files')
         if copies and self.ui.verbose:
             copies = ['%s (%s)' % c for c in copies]
-            self.ui.write(_("copies:      %s\n") % ' '.join(copies))
+            self.ui.write(_("copies:      %s\n") % ' '.join(copies),
+                          label='ui.note log.copies')
 
         extra = ctx.extra()
         if extra and self.ui.debugflag:
             for key, value in sorted(extra.items()):
                 self.ui.write(_("extra:       %s=%s\n")
-                              % (key, value.encode('string_escape')))
+                              % (key, value.encode('string_escape')),
+                              label='ui.debug log.extra')
 
         description = ctx.description().strip()
         if description:
             if self.ui.verbose:
-                self.ui.write(_("description:\n"))
-                self.ui.write(description)
+                self.ui.write(_("description:\n"),
+                              label='ui.note log.description')
+                self.ui.write(description,
+                              label='ui.note log.description')
                 self.ui.write("\n\n")
             else:
                 self.ui.write(_("summary:     %s\n") %
-                              description.splitlines()[0])
+                              description.splitlines()[0],
+                              label='log.summary')
         self.ui.write("\n")
 
         self.showpatch(changenode)
@@ -809,10 +824,10 @@ class changeset_printer(object):
     def showpatch(self, node):
         if self.patch:
             prev = self.repo.changelog.parents(node)[0]
-            chunks = patch.diff(self.repo, prev, node, match=self.patch,
-                                opts=patch.diffopts(self.ui, self.diffopts))
-            for chunk in chunks:
-                self.ui.write(chunk)
+            chunks = patch.diffui(self.repo, prev, node, match=self.patch,
+                                  opts=patch.diffopts(self.ui, self.diffopts))
+            for chunk, label in chunks:
+                self.ui.write(chunk, label=label)
             self.ui.write("\n")
 
     def _meaningful_parentrevs(self, log, rev):
diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -1174,14 +1174,16 @@ def diff(ui, repo, *pats, **opts):
     diffopts = patch.diffopts(ui, opts)
 
     m = cmdutil.match(repo, pats, opts)
-    it = patch.diff(repo, node1, node2, match=m, opts=diffopts)
     if stat:
+        it = patch.diff(repo, node1, node2, match=m, opts=diffopts)
         width = ui.interactive() and util.termwidth() or 80
-        ui.write(patch.diffstat(util.iterlines(it), width=width,
-                                git=diffopts.git))
+        for chunk, label in patch.diffstatui(util.iterlines(it), width=width,
+                                             git=diffopts.git):
+            ui.write(chunk, label=label)
     else:
-        for chunk in it:
-            ui.write(chunk)
+        it = patch.diffui(repo, node1, node2, match=m, opts=diffopts)
+        for chunk, label in it:
+            ui.write(chunk, label=label)
 
 def export(ui, repo, *changesets, **opts):
     """dump the header and diffs for one or more changesets
@@ -1353,6 +1355,7 @@ def grep(ui, repo, pattern, *pats, **opt
             iter = [('', l) for l in states]
         for change, l in iter:
             cols = [fn, str(rev)]
+            before, match, after = None, None, None
             if opts.get('line_number'):
                 cols.append(str(l.linenum))
             if opts.get('all'):
@@ -1367,8 +1370,15 @@ def grep(ui, repo, pattern, *pats, **opt
                     continue
                 filerevmatches[c] = 1
             else:
-                cols.append(l.line)
-            ui.write(sep.join(cols), eol)
+                before = l.line[:l.colstart]
+                match = l.line[l.colstart:l.colend]
+                after = l.line[l.colend:]
+            ui.write(sep.join(cols))
+            if before is not None:
+                ui.write(sep + before)
+                ui.write(match, label='grep.match')
+                ui.write(after)
+            ui.write(eol)
             found = True
         return found
 
@@ -2605,7 +2615,9 @@ def resolve(ui, repo, *pats, **opts):
                 if nostatus:
                     ui.write("%s\n" % f)
                 else:
-                    ui.write("%s %s\n" % (ms[f].upper(), f))
+                    ui.write("%s %s\n" % (ms[f].upper(), f),
+                             label='resolve.' +
+                             {'u': 'unresolved', 'r': 'resolved'}[ms[f]])
             elif mark:
                 ms.mark(f, "r")
             elif unmark:
@@ -3044,9 +3056,11 @@ def status(ui, repo, *pats, **opts):
                 format = "%%s%s" % end
 
             for f in files:
-                ui.write(format % repo.pathto(f, cwd))
+                ui.write(format % repo.pathto(f, cwd),
+                         label='status.' + state)
                 if f in copy:
-                    ui.write('  %s%s' % (repo.pathto(copy[f], cwd), end))
+                    ui.write('  %s%s' % (repo.pathto(copy[f], cwd), end),
+                             label='status.copied')
 
 def summary(ui, repo, **opts):
     """summarize working directory state
diff --git a/mercurial/patch.py b/mercurial/patch.py
--- a/mercurial/patch.py
+++ b/mercurial/patch.py
@@ -1466,6 +1466,43 @@ def diff(repo, node1=None, node2=None, m
     else:
         return difffn(opts, None)
 
+def difflabel(func, *args, **kw):
+    '''yields 2-tuples of (output, label) based on the output of func()'''
+    prefixes = [('diff', 'diff.diffline'),
+                ('copy', 'diff.extended'),
+                ('rename', 'diff.extended'),
+                ('old', 'diff.extended'),
+                ('new', 'diff.extended'),
+                ('deleted', 'diff.extended'),
+                ('---', 'diff.file_a'),
+                ('+++', 'diff.file_b'),
+                ('@@', 'diff.hunk'),
+                ('-', 'diff.deleted'),
+                ('+', 'diff.inserted')]
+
+    for chunk in func(*args, **kw):
+        lines = chunk.split('\n')
+        for i, line in enumerate(lines):
+            if i != 0:
+                yield ('\n', '')
+            stripline = line
+            if line and line[0] in '+-':
+                # highlight trailing whitespace, but only in changed lines
+                stripline = line.rstrip()
+            for prefix, label in prefixes:
+                if stripline.startswith(prefix):
+                    yield (stripline, label)
+                    break
+            else:
+                yield (line, '')
+            if line != stripline:
+                yield (line[len(stripline):], 'diff.trailingwhitespace')
+
+def diffui(*args, **kw):
+    '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
+    return difflabel(diff, *args, **kw)
+
+
 def _addmodehdr(header, omode, nmode):
     if omode != nmode:
         header.append('old mode %s\n' % omode)
@@ -1636,3 +1673,22 @@ def diffstat(lines, width=80, git=False)
                       % (len(stats), totaladds, totalremoves))
 
     return ''.join(output)
+
+def diffstatui(*args, **kw):
+    '''like diffstat(), but yields 2-tuples of (output, label) for
+    ui.write()
+    '''
+
+    for line in diffstat(*args, **kw).splitlines():
+        if line and line[-1] in '+-':
+            name, graph = line.rsplit(' ', 1)
+            yield (name + ' ', '')
+            m = re.search(r'\++', graph)
+            if m:
+                yield (m.group(0), 'diffstat.inserted')
+            m = re.search(r'-+', graph)
+            if m:
+                yield (m.group(0), 'diffstat.deleted')
+        else:
+            yield (line, '')
+        yield ('\n', '')
diff --git a/mercurial/ui.py b/mercurial/ui.py
--- a/mercurial/ui.py
+++ b/mercurial/ui.py
@@ -14,7 +14,7 @@ _booleans = {'1': True, 'yes': True, 'tr
 
 class ui(object):
     def __init__(self, src=None):
-        self._buffers = []
+        self.buffers = []
         self.quiet = self.verbose = self.debugflag = self.tracebackflag = False
         self._reportuntrusted = True
         self._ocfg = config.config() # overlay
@@ -237,19 +237,40 @@ class ui(object):
         return path or loc
 
     def pushbuffer(self):
-        self._buffers.append([])
+        self.buffers.append([])
 
-    def popbuffer(self):
-        return "".join(self._buffers.pop())
+    def popbuffer(self, labeled=False):
+        '''Pop last buffer and return buffered output
 
-    def write(self, *args):
-        if self._buffers:
-            self._buffers[-1].extend([str(a) for a in args])
+        If labeled is True, any labels associated with buffered
+        output will be handled. By default, this has no effect
+        on the output returned, but extensions and GUI tools may
+        handle this argument and returned styled output. If output
+        is being buffered so it can be captured and parsed or
+        processed, labeled should not be set to True.
+        '''
+        return "".join(self.buffers.pop())
+
+    def write(self, *args, **opts):
+        '''write args to output
+
+        By default, this method simply writes to the buffer or stdout,
+        but extensions or GUI tools may override this method,
+        write_err(), and popbuffer() to style output from various
+        parts of hg.
+
+        An optional keyword argument, "label", can be passed in.
+        This should be a string containing label names separated by
+        space. Label names take the form of "topic.type". For example,
+        ui.debug() issues a label of "ui.debug".
+        '''
+        if self.buffers:
+            self.buffers[-1].extend([str(a) for a in args])
         else:
             for a in args:
                 sys.stdout.write(str(a))
 
-    def write_err(self, *args):
+    def write_err(self, *args, **opts):
         try:
             if not getattr(sys.stdout, 'closed', False):
                 sys.stdout.flush()
@@ -335,17 +356,21 @@ class ui(object):
             return getpass.getpass(prompt or _('password: '))
         except EOFError:
             raise util.Abort(_('response expected'))
-    def status(self, *msg):
+    def status(self, *msg, **opts):
         if not self.quiet:
-            self.write(*msg)
-    def warn(self, *msg):
-        self.write_err(*msg)
-    def note(self, *msg):
+            opts['label'] = opts.get('label', '') + ' ui.status'
+            self.write(*msg, **opts)
+    def warn(self, *msg, **opts):
+        opts['label'] = opts.get('label', '') + ' ui.warning'
+        self.write_err(*msg, **opts)
+    def note(self, *msg, **opts):
         if self.verbose:
-            self.write(*msg)
-    def debug(self, *msg):
+            opts['label'] = opts.get('label', '') + ' ui.note'
+            self.write(*msg, **opts)
+    def debug(self, *msg, **opts):
         if self.debugflag:
-            self.write(*msg)
+            opts['label'] = opts.get('label', '') + ' ui.debug'
+            self.write(*msg, **opts)
     def edit(self, text, user):
         (fd, name) = tempfile.mkstemp(prefix="hg-editor-", suffix=".txt",
                                       text=True)
@@ -417,3 +442,12 @@ class ui(object):
                      % (topic, item, pos, total, unit, pct))
         else:
             self.debug('%s:%s %s%s\n' % (topic, item, pos, unit))
+
+    def label(self, msg, label):
+        '''style msg based on supplied label
+
+        Like ui.write(), this just returns msg unchanged, but extensions
+        and GUI tools can override it to allow styling output without
+        writing it.
+        '''
+        return msg
diff --git a/tests/test-bookmarks b/tests/test-bookmarks
--- a/tests/test-bookmarks
+++ b/tests/test-bookmarks
@@ -14,6 +14,9 @@ hg bookmark X
 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
diff --git a/tests/test-bookmarks-current b/tests/test-bookmarks-current
--- a/tests/test-bookmarks-current
+++ b/tests/test-bookmarks-current
@@ -17,6 +17,9 @@ hg bookmark X
 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
 
diff --git a/tests/test-bookmarks-current.out b/tests/test-bookmarks-current.out
--- a/tests/test-bookmarks-current.out
+++ b/tests/test-bookmarks-current.out
@@ -3,6 +3,8 @@ no bookmarks set
 % set bookmark X
 % list bookmarks
  * X                         -1:000000000000
+% list bookmarks with color
+ * X                         -1:000000000000
 % update to bookmark X
 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
 % list bookmarks
diff --git a/tests/test-bookmarks.out b/tests/test-bookmarks.out
--- a/tests/test-bookmarks.out
+++ b/tests/test-bookmarks.out
@@ -3,6 +3,8 @@ no bookmarks set
 % bookmark rev -1
 % list bookmarks
  * X                         -1:000000000000
+% list bookmarks with color
+ * X                         -1:000000000000
 % bookmark X moved to rev 0
  * X                         0:f7b1eb17ad24
 % look up bookmark
diff --git a/tests/test-churn b/tests/test-churn
--- a/tests/test-churn
+++ b/tests/test-churn
@@ -52,6 +52,8 @@ echo % churn with separated added/remove
 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
diff --git a/tests/test-churn.out b/tests/test-churn.out
--- a/tests/test-churn.out
+++ b/tests/test-churn.out
@@ -32,6 +32,10 @@ 13      1 *****************
 user1           +3/-1 +++++++++++++++++++++++++++++++++++++++++--------------
 user3           +3/-0 +++++++++++++++++++++++++++++++++++++++++
 user2           +2/-0 +++++++++++++++++++++++++++
+% churn --diffstat with color
+user1           +3/-1 +++++++++++++++++++++++++++++++++++++++++--------------
+user3           +3/-0 +++++++++++++++++++++++++++++++++++++++++
+user2           +2/-0 +++++++++++++++++++++++++++
 % changeset number churn
 user1      4 ***************************************************************
 user3      3 ***********************************************
diff --git a/tests/test-eolfilename.out b/tests/test-eolfilename.out
--- a/tests/test-eolfilename.out
+++ b/tests/test-eolfilename.out
@@ -14,7 +14,7 @@ f  hell
 o  hell
 o
 % test issue2039
-? foo
-bar
-? foo
-bar.baz
+? foo
+bar
+? foo
+bar.baz
diff --git a/tests/test-grep b/tests/test-grep
--- a/tests/test-grep
+++ b/tests/test-grep
@@ -21,6 +21,8 @@ echo % pattern error
 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
diff --git a/tests/test-grep.out b/tests/test-grep.out
--- a/tests/test-grep.out
+++ b/tests/test-grep.out
@@ -4,6 +4,10 @@ grep: invalid match pattern: nothing to 
 port:4:export
 port:4:vaportight
 port:4:import/export
+% simple with color
+port:4:export
+port:4:vaportight
+port:4:import/export
 % all
 port:4:4:-:spam:import/export
 port:3:4:+:eggs:import/export
diff --git a/tests/test-log b/tests/test-log
--- a/tests/test-log
+++ b/tests/test-log
@@ -114,6 +114,9 @@ hg log -k r1
 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
diff --git a/tests/test-log.out b/tests/test-log.out
--- a/tests/test-log.out
+++ b/tests/test-log.out
@@ -258,6 +258,33 @@ date:        Thu Jan 01 00:00:01 1970 +0
 summary:     r1
 
 % log -d -1
+% log -p -l2 --color=always
+changeset:   6:2404bbcab562
+tag:         tip
+user:        test
+date:        Thu Jan 01 00:00:01 1970 +0000
+summary:     b1.1
+
+diff -r 302e9dd6890d -r 2404bbcab562 b1
+--- a/b1	Thu Jan 01 00:00:01 1970 +0000
++++ b/b1	Thu Jan 01 00:00:01 1970 +0000
+@@ -1,1 +1,2 @@
+ b1
++postm
+
+changeset:   5:302e9dd6890d
+parent:      3:e62f78d544b4
+parent:      4:ddb82e70d1a1
+user:        test
+date:        Thu Jan 01 00:00:01 1970 +0000
+summary:     m12
+
+diff -r e62f78d544b4 -r 302e9dd6890d b2
+--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
++++ b/b2	Thu Jan 01 00:00:01 1970 +0000
+@@ -0,0 +1,1 @@
++b2
+
 adding a
 adding b
 changeset:   0:29a4c94f1924
diff --git a/tests/test-mq-guards b/tests/test-mq-guards
--- a/tests/test-mq-guards
+++ b/tests/test-mq-guards
@@ -99,6 +99,8 @@ hg qguard -- a.patch +1 +2 -3
 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 @@ hg qpop
 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 % hg qseries -m: only b.patch shoul
 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
diff --git a/tests/test-mq-guards.out b/tests/test-mq-guards.out
--- a/tests/test-mq-guards.out
+++ b/tests/test-mq-guards.out
@@ -84,6 +84,10 @@ number of unguarded, unapplied patches h
 a.patch: +1 +2 -3
 b.patch: +2
 c.patch: unguarded
+% list patches and guards with color
+a.patch: +1 +2 -3
+b.patch: +2
+c.patch: unguarded
 % list series
 0 G a.patch
 1 U b.patch
@@ -126,6 +130,11 @@ 0 G new.patch
 1 G b.patch
 2 A c.patch
 3 U d.patch
+% qseries again, but with color
+0 G new.patch
+1 G b.patch
+2 A c.patch
+3 U d.patch
 % new.patch, b.patch: Guarded. c.patch: Applied. d.patch: Guarded.
 0 G new.patch
 1 G b.patch
@@ -206,3 +215,5 @@ c.patch
 % 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
+b.patch


More information about the Mercurial-devel mailing list