1 # color.py color output for the status and qseries commands
3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
5 # This program is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation; either version 2 of the License, or (at your
8 # option) any later version.
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
13 # Public License for more details.
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 '''colorize output from some commands
21 This extension modifies the status command to add color to its output
22 to reflect file status, the qseries command to add color to reflect
23 patch status (applied, unapplied, missing), and to diff-related
24 commands to highlight additions, removals, diff headers, and trailing
27 Other effects in addition to color, like bold and underlined text, are
28 also available. Effects are rendered with the ECMA-48 SGR control
29 function (aka ANSI escape codes). This module also provides the
30 render_text function, which can be used to add effects to any text.
32 Default effects may be overridden from the .hgrc file::
35 status.modified = blue bold underline red_background
36 status.added = green bold
37 status.removed = red bold blue_background
38 status.deleted = cyan bold underline
39 status.unknown = magenta bold underline
40 status.ignored = black bold
42 # 'none' turns off all effects
46 qseries.applied = blue bold underline
47 qseries.unapplied = black bold
48 qseries.missing = red bold
51 diff.extended = cyan bold
52 diff.file_a = red bold
53 diff.file_b = green bold
58 diff.trailingwhitespace = bold red_background
64 from mercurial import cmdutil, commands, extensions, error
65 from mercurial.i18n import _
67 # start and stop parameters for effects
68 _effect_params = {'none': 0,
81 'black_background': 40,
83 'green_background': 42,
84 'yellow_background': 43,
85 'blue_background': 44,
86 'purple_background': 45,
87 'cyan_background': 46,
88 'white_background': 47}
90 def render_effects(text, effects):
91 'Wrap text in commands to turn on each effect.'
92 start = [str(_effect_params[e]) for e in ['none'] + effects]
93 start = '\033[' + ';'.join(start) + 'm'
94 stop = '\033[' + str(_effect_params['none']) + 'm'
95 return ''.join([start, text, stop])
97 def colorstatus(orig, ui, repo, *pats, **opts):
98 '''run the status command with colored output'''
100 delimiter = opts['print0'] and '\0' or '\n'
102 nostatus = opts.get('no_status')
103 opts['no_status'] = False
104 # run status and capture its output
106 retval = orig(ui, repo, *pats, **opts)
107 # filter out empty strings
108 lines_with_status = [ line for line in ui.popbuffer().split(delimiter) if line ]
111 lines = [l[2:] for l in lines_with_status]
113 lines = lines_with_status
115 # apply color to output and display it
116 for i in xrange(len(lines)):
117 status = _status_abbreviations[lines_with_status[i][0]]
118 effects = _status_effects[status]
120 lines[i] = render_effects(lines[i], effects)
121 ui.write(lines[i] + delimiter)
124 _status_abbreviations = { 'M': 'modified',
133 _status_effects = { 'modified': ['blue', 'bold'],
134 'added': ['green', 'bold'],
135 'removed': ['red', 'bold'],
136 'deleted': ['cyan', 'bold', 'underline'],
137 'unknown': ['magenta', 'bold', 'underline'],
138 'ignored': ['black', 'bold'],
140 'copied': ['none'], }
142 def colorqseries(orig, ui, repo, *dummy, **opts):
143 '''run the qseries command with colored output'''
145 retval = orig(ui, repo, **opts)
146 patchlines = ui.popbuffer().splitlines()
147 patchnames = repo.mq.series
149 for patch, patchname in itertools.izip(patchlines, patchnames):
151 effects = _patch_effects['missing']
152 # Determine if patch is applied.
153 elif [ applied for applied in repo.mq.applied
154 if patchname == applied.name ]:
155 effects = _patch_effects['applied']
157 effects = _patch_effects['unapplied']
159 patch = patch.replace(patchname, render_effects(patchname, effects), 1)
160 ui.write(patch + '\n')
163 _patch_effects = { 'applied': ['blue', 'bold', 'underline'],
164 'missing': ['red', 'bold'],
165 'unapplied': ['black', 'bold'], }
167 def colorwrap(orig, s):
168 '''wrap ui.write for colored diff output'''
169 lines = s.split('\n')
170 for i, line in enumerate(lines):
172 if line and line[0] in '+-':
173 # highlight trailing whitespace, but only in changed lines
174 stripline = line.rstrip()
175 for prefix, style in _diff_prefixes:
176 if stripline.startswith(prefix):
177 lines[i] = render_effects(stripline, _diff_effects[style])
179 if line != stripline:
180 lines[i] += render_effects(
181 line[len(stripline):], _diff_effects['trailingwhitespace'])
182 orig('\n'.join(lines))
184 def colorshowpatch(orig, self, node):
185 '''wrap cmdutil.changeset_printer.showpatch with colored output'''
186 oldwrite = extensions.wrapfunction(self.ui, 'write', colorwrap)
190 self.ui.write = oldwrite
192 def colordiff(orig, ui, repo, *pats, **opts):
193 '''run the diff command with colored output'''
194 oldwrite = extensions.wrapfunction(ui, 'write', colorwrap)
196 orig(ui, repo, *pats, **opts)
200 _diff_prefixes = [('diff', 'diffline'),
201 ('copy', 'extended'),
202 ('rename', 'extended'),
205 ('deleted', 'extended'),
212 _diff_effects = {'diffline': ['bold'],
213 'extended': ['cyan', 'bold'],
214 'file_a': ['red', 'bold'],
215 'file_b': ['green', 'bold'],
218 'inserted': ['green'],
219 'changed': ['white'],
220 'trailingwhitespace': ['bold', 'red_background']}
225 '''Initialize the extension.'''
228 _setupcmd(ui, 'diff', commands.table, colordiff, _diff_effects)
229 _setupcmd(ui, 'incoming', commands.table, None, _diff_effects)
230 _setupcmd(ui, 'log', commands.table, None, _diff_effects)
231 _setupcmd(ui, 'outgoing', commands.table, None, _diff_effects)
232 _setupcmd(ui, 'tip', commands.table, None, _diff_effects)
233 _setupcmd(ui, 'status', commands.table, colorstatus, _status_effects)
237 mq = extensions.find('mq')
239 # If we are loaded after mq, we must wrap commands.table
240 _setupcmd(_ui, 'qdiff', commands.table, colordiff, _diff_effects)
241 _setupcmd(_ui, 'qseries', commands.table, colorqseries, _patch_effects)
242 except error.UnknownCommand:
243 # Otherwise we wrap mq.cmdtable
244 _setupcmd(_ui, 'qdiff', mq.cmdtable, colordiff, _diff_effects)
245 _setupcmd(_ui, 'qseries', mq.cmdtable, colorqseries, _patch_effects)
247 # The mq extension is not enabled
250 def _setupcmd(ui, cmd, table, func, effectsmap):
251 '''patch in command to command table and load effect map'''
252 def nocolor(orig, *args, **opts):
254 if (opts['no_color'] or opts['color'] == 'never' or
255 (opts['color'] == 'auto' and (os.environ.get('TERM') == 'dumb'
256 or not sys.__stdout__.isatty()))):
257 return orig(*args, **opts)
259 oldshowpatch = extensions.wrapfunction(cmdutil.changeset_printer,
260 'showpatch', colorshowpatch)
263 return func(orig, *args, **opts)
264 return orig(*args, **opts)
266 cmdutil.changeset_printer.showpatch = oldshowpatch
268 entry = extensions.wrapcommand(table, cmd, nocolor)
270 ('', 'color', 'auto', _("when to colorize (always, auto, or never)")),
271 ('', 'no-color', None, _("don't colorize output")),
274 for status in effectsmap:
275 configkey = cmd + '.' + status
276 effects = ui.configlist('color', configkey)
280 if e in _effect_params:
283 ui.warn(_("ignoring unknown color/effect %r "
284 "(configured in color.%s)\n")
286 effectsmap[status] = good