Mercurial > hg
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)"))) |