comparison hgext/color.py @ 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 44b4a2a31623
children b66388f6adfa
comparison
equal deleted inserted replaced
10825:781689b9b6bb 10826:717c35d55fb3
63 bookmarks.current = green 63 bookmarks.current = green
64 ''' 64 '''
65 65
66 import os, sys 66 import os, sys
67 67
68 from mercurial import cmdutil, commands, extensions 68 from mercurial import commands, dispatch, extensions
69 from mercurial.i18n import _ 69 from mercurial.i18n import _
70 from mercurial.ui import ui as uicls
70 71
71 # start and stop parameters for effects 72 # start and stop parameters for effects
72 _effect_params = {'none': 0, 73 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
73 'black': 30, 74 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
74 'red': 31, 75 'italic': 3, 'underline': 4, 'inverse': 7,
75 'green': 32, 76 'black_background': 40, 'red_background': 41,
76 'yellow': 33, 77 'green_background': 42, 'yellow_background': 43,
77 'blue': 34, 78 'blue_background': 44, 'purple_background': 45,
78 'magenta': 35, 79 'cyan_background': 46, 'white_background': 47}
79 'cyan': 36, 80
80 'white': 37, 81 _styles = {'grep.match': 'red bold',
81 'bold': 1, 82 'diff.changed': 'white',
82 'italic': 3, 83 'diff.deleted': 'red',
83 'underline': 4, 84 'diff.diffline': 'bold',
84 'inverse': 7, 85 'diff.extended': 'cyan bold',
85 'black_background': 40, 86 'diff.file_a': 'red bold',
86 'red_background': 41, 87 'diff.file_b': 'green bold',
87 'green_background': 42, 88 'diff.hunk': 'magenta',
88 'yellow_background': 43, 89 'diff.inserted': 'green',
89 'blue_background': 44, 90 'diff.trailingwhitespace': 'bold red_background',
90 'purple_background': 45, 91 'diffstat.deleted': 'red',
91 'cyan_background': 46, 92 'diffstat.inserted': 'green',
92 'white_background': 47} 93 'log.changeset': 'yellow',
94 'resolve.resolved': 'green bold',
95 'resolve.unresolved': 'red bold',
96 'status.added': 'green bold',
97 'status.clean': 'none',
98 'status.copied': 'none',
99 'status.deleted': 'cyan bold underline',
100 'status.ignored': 'black bold',
101 'status.modified': 'blue bold',
102 'status.removed': 'red bold',
103 'status.unknown': 'magenta bold underline'}
104
93 105
94 def render_effects(text, effects): 106 def render_effects(text, effects):
95 'Wrap text in commands to turn on each effect.' 107 'Wrap text in commands to turn on each effect.'
96 start = [str(_effect_params[e]) for e in ['none'] + effects] 108 if not text:
109 return text
110 start = [str(_effects[e]) for e in ['none'] + effects.split()]
97 start = '\033[' + ';'.join(start) + 'm' 111 start = '\033[' + ';'.join(start) + 'm'
98 stop = '\033[' + str(_effect_params['none']) + 'm' 112 stop = '\033[' + str(_effects['none']) + 'm'
99 return ''.join([start, text, stop]) 113 if text[-1] == '\n':
114 return ''.join([start, text[:-1], stop, '\n'])
115 else:
116 return ''.join([start, text, stop])
100 117
101 def _colorstatuslike(abbreviations, effectdefs, orig, ui, repo, *pats, **opts): 118 def extstyles():
102 '''run a status-like command with colorized output''' 119 for name, ext in extensions.extensions():
103 delimiter = opts.get('print0') and '\0' or '\n' 120 _styles.update(getattr(ext, 'colortable', {}))
104 121
105 nostatus = opts.get('no_status') 122 def configstyles(ui):
106 opts['no_status'] = False 123 for status, cfgeffects in ui.configitems('color'):
107 # run original command and capture its output 124 if '.' not in status:
108 ui.pushbuffer() 125 continue
109 retval = orig(ui, repo, *pats, **opts) 126 cfgeffects = ui.configlist('color', status)
110 # filter out empty strings 127 if cfgeffects:
111 lines_with_status = [line for line in ui.popbuffer().split(delimiter) if line]
112
113 if nostatus:
114 lines = [l[2:] for l in lines_with_status]
115 else:
116 lines = lines_with_status
117
118 # apply color to output and display it
119 for i in xrange(len(lines)):
120 try:
121 status = abbreviations[lines_with_status[i][0]]
122 except KeyError:
123 # Ignore lines with invalid codes, especially in the case of
124 # of unknown filenames containing newlines (issue2036).
125 pass
126 else:
127 effects = effectdefs[status]
128 if effects:
129 lines[i] = render_effects(lines[i], effects)
130 ui.write(lines[i] + delimiter)
131 return retval
132
133
134 _status_abbreviations = { 'M': 'modified',
135 'A': 'added',
136 'R': 'removed',
137 '!': 'deleted',
138 '?': 'unknown',
139 'I': 'ignored',
140 'C': 'clean',
141 ' ': 'copied', }
142
143 _status_effects = { 'modified': ['blue', 'bold'],
144 'added': ['green', 'bold'],
145 'removed': ['red', 'bold'],
146 'deleted': ['cyan', 'bold', 'underline'],
147 'unknown': ['magenta', 'bold', 'underline'],
148 'ignored': ['black', 'bold'],
149 'clean': ['none'],
150 'copied': ['none'], }
151
152 def colorstatus(orig, ui, repo, *pats, **opts):
153 '''run the status command with colored output'''
154 return _colorstatuslike(_status_abbreviations, _status_effects,
155 orig, ui, repo, *pats, **opts)
156
157
158 _resolve_abbreviations = { 'U': 'unresolved',
159 'R': 'resolved', }
160
161 _resolve_effects = { 'unresolved': ['red', 'bold'],
162 'resolved': ['green', 'bold'], }
163
164 def colorresolve(orig, ui, repo, *pats, **opts):
165 '''run the resolve command with colored output'''
166 if not opts.get('list'):
167 # only colorize for resolve -l
168 return orig(ui, repo, *pats, **opts)
169 return _colorstatuslike(_resolve_abbreviations, _resolve_effects,
170 orig, ui, repo, *pats, **opts)
171
172
173 _bookmark_effects = { 'current': ['green'] }
174
175 def colorbookmarks(orig, ui, repo, *pats, **opts):
176 def colorize(orig, s):
177 lines = s.split('\n')
178 for i, line in enumerate(lines):
179 if line.startswith(" *"):
180 lines[i] = render_effects(line, _bookmark_effects['current'])
181 orig('\n'.join(lines))
182 oldwrite = extensions.wrapfunction(ui, 'write', colorize)
183 try:
184 orig(ui, repo, *pats, **opts)
185 finally:
186 ui.write = oldwrite
187
188 def colorqseries(orig, ui, repo, *dummy, **opts):
189 '''run the qseries command with colored output'''
190 ui.pushbuffer()
191 retval = orig(ui, repo, **opts)
192 patchlines = ui.popbuffer().splitlines()
193 patchnames = repo.mq.series
194
195 for patch, patchname in zip(patchlines, patchnames):
196 if opts['missing']:
197 effects = _patch_effects['missing']
198 # Determine if patch is applied.
199 elif [applied for applied in repo.mq.applied
200 if patchname == applied.name]:
201 effects = _patch_effects['applied']
202 else:
203 effects = _patch_effects['unapplied']
204
205 patch = patch.replace(patchname, render_effects(patchname, effects), 1)
206 ui.write(patch + '\n')
207 return retval
208
209 _patch_effects = { 'applied': ['blue', 'bold', 'underline'],
210 'missing': ['red', 'bold'],
211 'unapplied': ['black', 'bold'], }
212 def colorwrap(orig, *args):
213 '''wrap ui.write for colored diff output'''
214 def _colorize(s):
215 lines = s.split('\n')
216 for i, line in enumerate(lines):
217 stripline = line
218 if line and line[0] in '+-':
219 # highlight trailing whitespace, but only in changed lines
220 stripline = line.rstrip()
221 for prefix, style in _diff_prefixes:
222 if stripline.startswith(prefix):
223 lines[i] = render_effects(stripline, _diff_effects[style])
224 break
225 if line != stripline:
226 lines[i] += render_effects(
227 line[len(stripline):], _diff_effects['trailingwhitespace'])
228 return '\n'.join(lines)
229 orig(*[_colorize(s) for s in args])
230
231 def colorshowpatch(orig, self, node):
232 '''wrap cmdutil.changeset_printer.showpatch with colored output'''
233 oldwrite = extensions.wrapfunction(self.ui, 'write', colorwrap)
234 try:
235 orig(self, node)
236 finally:
237 self.ui.write = oldwrite
238
239 def colordiffstat(orig, s):
240 lines = s.split('\n')
241 for i, line in enumerate(lines):
242 if line and line[-1] in '+-':
243 name, graph = line.rsplit(' ', 1)
244 graph = graph.replace('-',
245 render_effects('-', _diff_effects['deleted']))
246 graph = graph.replace('+',
247 render_effects('+', _diff_effects['inserted']))
248 lines[i] = ' '.join([name, graph])
249 orig('\n'.join(lines))
250
251 def colordiff(orig, ui, repo, *pats, **opts):
252 '''run the diff command with colored output'''
253 if opts.get('stat'):
254 wrapper = colordiffstat
255 else:
256 wrapper = colorwrap
257 oldwrite = extensions.wrapfunction(ui, 'write', wrapper)
258 try:
259 orig(ui, repo, *pats, **opts)
260 finally:
261 ui.write = oldwrite
262
263 def colorchurn(orig, ui, repo, *pats, **opts):
264 '''run the churn command with colored output'''
265 if not opts.get('diffstat'):
266 return orig(ui, repo, *pats, **opts)
267 oldwrite = extensions.wrapfunction(ui, 'write', colordiffstat)
268 try:
269 orig(ui, repo, *pats, **opts)
270 finally:
271 ui.write = oldwrite
272
273 _diff_prefixes = [('diff', 'diffline'),
274 ('copy', 'extended'),
275 ('rename', 'extended'),
276 ('old', 'extended'),
277 ('new', 'extended'),
278 ('deleted', 'extended'),
279 ('---', 'file_a'),
280 ('+++', 'file_b'),
281 ('@', 'hunk'),
282 ('-', 'deleted'),
283 ('+', 'inserted')]
284
285 _diff_effects = {'diffline': ['bold'],
286 'extended': ['cyan', 'bold'],
287 'file_a': ['red', 'bold'],
288 'file_b': ['green', 'bold'],
289 'hunk': ['magenta'],
290 'deleted': ['red'],
291 'inserted': ['green'],
292 'changed': ['white'],
293 'trailingwhitespace': ['bold', 'red_background']}
294
295 def extsetup(ui):
296 '''Initialize the extension.'''
297 _setupcmd(ui, 'diff', commands.table, colordiff, _diff_effects)
298 _setupcmd(ui, 'incoming', commands.table, None, _diff_effects)
299 _setupcmd(ui, 'log', commands.table, None, _diff_effects)
300 _setupcmd(ui, 'outgoing', commands.table, None, _diff_effects)
301 _setupcmd(ui, 'tip', commands.table, None, _diff_effects)
302 _setupcmd(ui, 'status', commands.table, colorstatus, _status_effects)
303 _setupcmd(ui, 'resolve', commands.table, colorresolve, _resolve_effects)
304
305 try:
306 mq = extensions.find('mq')
307 _setupcmd(ui, 'qdiff', mq.cmdtable, colordiff, _diff_effects)
308 _setupcmd(ui, 'qseries', mq.cmdtable, colorqseries, _patch_effects)
309 except KeyError:
310 mq = None
311
312 try:
313 rec = extensions.find('record')
314 _setupcmd(ui, 'record', rec.cmdtable, colordiff, _diff_effects)
315 except KeyError:
316 rec = None
317
318 if mq and rec:
319 _setupcmd(ui, 'qrecord', rec.cmdtable, colordiff, _diff_effects)
320 try:
321 churn = extensions.find('churn')
322 _setupcmd(ui, 'churn', churn.cmdtable, colorchurn, _diff_effects)
323 except KeyError:
324 churn = None
325
326 try:
327 bookmarks = extensions.find('bookmarks')
328 _setupcmd(ui, 'bookmarks', bookmarks.cmdtable, colorbookmarks,
329 _bookmark_effects)
330 except KeyError:
331 # The bookmarks extension is not enabled
332 pass
333
334 def _setupcmd(ui, cmd, table, func, effectsmap):
335 '''patch in command to command table and load effect map'''
336 def nocolor(orig, *args, **opts):
337
338 if (opts['no_color'] or opts['color'] == 'never' or
339 (opts['color'] == 'auto' and (os.environ.get('TERM') == 'dumb'
340 or not sys.__stdout__.isatty()))):
341 del opts['no_color']
342 del opts['color']
343 return orig(*args, **opts)
344
345 oldshowpatch = extensions.wrapfunction(cmdutil.changeset_printer,
346 'showpatch', colorshowpatch)
347 del opts['no_color']
348 del opts['color']
349 try:
350 if func is not None:
351 return func(orig, *args, **opts)
352 return orig(*args, **opts)
353 finally:
354 cmdutil.changeset_printer.showpatch = oldshowpatch
355
356 entry = extensions.wrapcommand(table, cmd, nocolor)
357 entry[1].extend([
358 ('', 'color', 'auto', _("when to colorize (always, auto, or never)")),
359 ('', 'no-color', None, _("don't colorize output (DEPRECATED)")),
360 ])
361
362 for status in effectsmap:
363 configkey = cmd + '.' + status
364 effects = ui.configlist('color', configkey)
365 if effects:
366 good = [] 128 good = []
367 for e in effects: 129 for e in cfgeffects:
368 if e in _effect_params: 130 if e in _effects:
369 good.append(e) 131 good.append(e)
370 else: 132 else:
371 ui.warn(_("ignoring unknown color/effect %r " 133 ui.warn(_("ignoring unknown color/effect %r "
372 "(configured in color.%s)\n") 134 "(configured in color.%s)\n")
373 % (e, configkey)) 135 % (e, status))
374 effectsmap[status] = good 136 _styles[status] = ' '.join(good)
137
138 _buffers = None
139 def style(msg, label):
140 effects = ''
141 for l in label.split():
142 effects += _styles.get(l, '')
143 if effects:
144 return render_effects(msg, effects)
145 return msg
146
147 def popbuffer(orig, labeled=False):
148 global _buffers
149 if labeled:
150 return ''.join(style(a, label) for a, label in _buffers.pop())
151 return ''.join(a for a, label in _buffers.pop())
152
153 def write(orig, *args, **opts):
154 label = opts.get('label', '')
155 global _buffers
156 if _buffers:
157 _buffers[-1].extend([(str(a), label) for a in args])
158 else:
159 return orig(*[style(str(a), label) for a in args], **opts)
160
161 def write_err(orig, *args, **opts):
162 label = opts.get('label', '')
163 return orig(*[style(str(a), label) for a in args], **opts)
164
165 def uisetup(ui):
166 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
167 if (opts['color'] == 'always' or
168 (opts['color'] == 'auto' and (os.environ.get('TERM') != 'dumb'
169 and sys.__stdout__.isatty()))):
170 global _buffers
171 _buffers = ui_._buffers
172 extensions.wrapfunction(ui_, 'popbuffer', popbuffer)
173 extensions.wrapfunction(ui_, 'write', write)
174 extensions.wrapfunction(ui_, 'write_err', write_err)
175 ui_.label = style
176 extstyles()
177 configstyles(ui)
178 return orig(ui_, opts, cmd, cmdfunc)
179 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
180
181 commands.globalopts.append(('', 'color', 'auto',
182 _("when to colorize (always, auto, or never)")))