##// END OF EJS Templates
color: add debugcolor command (issue4094)...
Olle Lundberg -
r20455:a2c08adb default
parent child Browse files
Show More
@@ -1,557 +1,572 b''
1 1 # color.py color output for the status and qseries commands
2 2 #
3 3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''colorize output from some commands
9 9
10 10 This extension modifies the status and resolve commands to add color
11 11 to their output to reflect file status, the qseries command to add
12 12 color to reflect patch status (applied, unapplied, missing), and to
13 13 diff-related commands to highlight additions, removals, diff headers,
14 14 and trailing whitespace.
15 15
16 16 Other effects in addition to color, like bold and underlined text, are
17 17 also available. By default, the terminfo database is used to find the
18 18 terminal codes used to change color and effect. If terminfo is not
19 19 available, then effects are rendered with the ECMA-48 SGR control
20 20 function (aka ANSI escape codes).
21 21
22 22 Default effects may be overridden from your configuration file::
23 23
24 24 [color]
25 25 status.modified = blue bold underline red_background
26 26 status.added = green bold
27 27 status.removed = red bold blue_background
28 28 status.deleted = cyan bold underline
29 29 status.unknown = magenta bold underline
30 30 status.ignored = black bold
31 31
32 32 # 'none' turns off all effects
33 33 status.clean = none
34 34 status.copied = none
35 35
36 36 qseries.applied = blue bold underline
37 37 qseries.unapplied = black bold
38 38 qseries.missing = red bold
39 39
40 40 diff.diffline = bold
41 41 diff.extended = cyan bold
42 42 diff.file_a = red bold
43 43 diff.file_b = green bold
44 44 diff.hunk = magenta
45 45 diff.deleted = red
46 46 diff.inserted = green
47 47 diff.changed = white
48 48 diff.trailingwhitespace = bold red_background
49 49
50 50 resolve.unresolved = red bold
51 51 resolve.resolved = green bold
52 52
53 53 bookmarks.current = green
54 54
55 55 branches.active = none
56 56 branches.closed = black bold
57 57 branches.current = green
58 58 branches.inactive = none
59 59
60 60 tags.normal = green
61 61 tags.local = black bold
62 62
63 63 rebase.rebased = blue
64 64 rebase.remaining = red bold
65 65
66 66 shelve.age = cyan
67 67 shelve.newest = green bold
68 68 shelve.name = blue bold
69 69
70 70 histedit.remaining = red bold
71 71
72 72 The available effects in terminfo mode are 'blink', 'bold', 'dim',
73 73 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
74 74 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
75 75 'underline'. How each is rendered depends on the terminal emulator.
76 76 Some may not be available for a given terminal type, and will be
77 77 silently ignored.
78 78
79 79 Note that on some systems, terminfo mode may cause problems when using
80 80 color with the pager extension and less -R. less with the -R option
81 81 will only display ECMA-48 color codes, and terminfo mode may sometimes
82 82 emit codes that less doesn't understand. You can work around this by
83 83 either using ansi mode (or auto mode), or by using less -r (which will
84 84 pass through all terminal control codes, not just color control
85 85 codes).
86 86
87 87 Because there are only eight standard colors, this module allows you
88 88 to define color names for other color slots which might be available
89 89 for your terminal type, assuming terminfo mode. For instance::
90 90
91 91 color.brightblue = 12
92 92 color.pink = 207
93 93 color.orange = 202
94 94
95 95 to set 'brightblue' to color slot 12 (useful for 16 color terminals
96 96 that have brighter colors defined in the upper eight) and, 'pink' and
97 97 'orange' to colors in 256-color xterm's default color cube. These
98 98 defined colors may then be used as any of the pre-defined eight,
99 99 including appending '_background' to set the background to that color.
100 100
101 101 By default, the color extension will use ANSI mode (or win32 mode on
102 102 Windows) if it detects a terminal. To override auto mode (to enable
103 103 terminfo mode, for example), set the following configuration option::
104 104
105 105 [color]
106 106 mode = terminfo
107 107
108 108 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
109 109 disable color.
110 110 '''
111 111
112 112 import os
113 113
114 114 from mercurial import commands, dispatch, extensions, ui as uimod, util
115 115 from mercurial import templater, error
116 116 from mercurial.i18n import _
117 117
118 118 testedwith = 'internal'
119 119
120 120 # start and stop parameters for effects
121 121 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
122 122 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
123 123 'italic': 3, 'underline': 4, 'inverse': 7,
124 124 'black_background': 40, 'red_background': 41,
125 125 'green_background': 42, 'yellow_background': 43,
126 126 'blue_background': 44, 'purple_background': 45,
127 127 'cyan_background': 46, 'white_background': 47}
128 128
129 129 def _terminfosetup(ui, mode):
130 130 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
131 131
132 132 global _terminfo_params
133 133 # If we failed to load curses, we go ahead and return.
134 134 if not _terminfo_params:
135 135 return
136 136 # Otherwise, see what the config file says.
137 137 if mode not in ('auto', 'terminfo'):
138 138 return
139 139
140 140 _terminfo_params.update((key[6:], (False, int(val)))
141 141 for key, val in ui.configitems('color')
142 142 if key.startswith('color.'))
143 143
144 144 try:
145 145 curses.setupterm()
146 146 except curses.error, e:
147 147 _terminfo_params = {}
148 148 return
149 149
150 150 for key, (b, e) in _terminfo_params.items():
151 151 if not b:
152 152 continue
153 153 if not curses.tigetstr(e):
154 154 # Most terminals don't support dim, invis, etc, so don't be
155 155 # noisy and use ui.debug().
156 156 ui.debug("no terminfo entry for %s\n" % e)
157 157 del _terminfo_params[key]
158 158 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
159 159 # Only warn about missing terminfo entries if we explicitly asked for
160 160 # terminfo mode.
161 161 if mode == "terminfo":
162 162 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
163 163 "ECMA-48 color\n"))
164 164 _terminfo_params = {}
165 165
166 166 def _modesetup(ui, coloropt):
167 167 global _terminfo_params
168 168
169 169 auto = coloropt == 'auto'
170 170 always = not auto and util.parsebool(coloropt)
171 171 if not always and not auto:
172 172 return None
173 173
174 174 formatted = always or (os.environ.get('TERM') != 'dumb' and ui.formatted())
175 175
176 176 mode = ui.config('color', 'mode', 'auto')
177 177 realmode = mode
178 178 if mode == 'auto':
179 179 if os.name == 'nt' and 'TERM' not in os.environ:
180 180 # looks line a cmd.exe console, use win32 API or nothing
181 181 realmode = 'win32'
182 182 else:
183 183 realmode = 'ansi'
184 184
185 185 if realmode == 'win32':
186 186 _terminfo_params = {}
187 187 if not w32effects:
188 188 if mode == 'win32':
189 189 # only warn if color.mode is explicitly set to win32
190 190 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
191 191 return None
192 192 _effects.update(w32effects)
193 193 elif realmode == 'ansi':
194 194 _terminfo_params = {}
195 195 elif realmode == 'terminfo':
196 196 _terminfosetup(ui, mode)
197 197 if not _terminfo_params:
198 198 if mode == 'terminfo':
199 199 ## FIXME Shouldn't we return None in this case too?
200 200 # only warn if color.mode is explicitly set to win32
201 201 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
202 202 realmode = 'ansi'
203 203 else:
204 204 return None
205 205
206 206 if always or (auto and formatted):
207 207 return realmode
208 208 return None
209 209
210 210 try:
211 211 import curses
212 212 # Mapping from effect name to terminfo attribute name or color number.
213 213 # This will also force-load the curses module.
214 214 _terminfo_params = {'none': (True, 'sgr0'),
215 215 'standout': (True, 'smso'),
216 216 'underline': (True, 'smul'),
217 217 'reverse': (True, 'rev'),
218 218 'inverse': (True, 'rev'),
219 219 'blink': (True, 'blink'),
220 220 'dim': (True, 'dim'),
221 221 'bold': (True, 'bold'),
222 222 'invisible': (True, 'invis'),
223 223 'italic': (True, 'sitm'),
224 224 'black': (False, curses.COLOR_BLACK),
225 225 'red': (False, curses.COLOR_RED),
226 226 'green': (False, curses.COLOR_GREEN),
227 227 'yellow': (False, curses.COLOR_YELLOW),
228 228 'blue': (False, curses.COLOR_BLUE),
229 229 'magenta': (False, curses.COLOR_MAGENTA),
230 230 'cyan': (False, curses.COLOR_CYAN),
231 231 'white': (False, curses.COLOR_WHITE)}
232 232 except ImportError:
233 233 _terminfo_params = False
234 234
235 235 _styles = {'grep.match': 'red bold',
236 236 'grep.linenumber': 'green',
237 237 'grep.rev': 'green',
238 238 'grep.change': 'green',
239 239 'grep.sep': 'cyan',
240 240 'grep.filename': 'magenta',
241 241 'grep.user': 'magenta',
242 242 'grep.date': 'magenta',
243 243 'bookmarks.current': 'green',
244 244 'branches.active': 'none',
245 245 'branches.closed': 'black bold',
246 246 'branches.current': 'green',
247 247 'branches.inactive': 'none',
248 248 'diff.changed': 'white',
249 249 'diff.deleted': 'red',
250 250 'diff.diffline': 'bold',
251 251 'diff.extended': 'cyan bold',
252 252 'diff.file_a': 'red bold',
253 253 'diff.file_b': 'green bold',
254 254 'diff.hunk': 'magenta',
255 255 'diff.inserted': 'green',
256 256 'diff.trailingwhitespace': 'bold red_background',
257 257 'diffstat.deleted': 'red',
258 258 'diffstat.inserted': 'green',
259 259 'histedit.remaining': 'red bold',
260 260 'ui.prompt': 'yellow',
261 261 'log.changeset': 'yellow',
262 262 'rebase.rebased': 'blue',
263 263 'rebase.remaining': 'red bold',
264 264 'resolve.resolved': 'green bold',
265 265 'resolve.unresolved': 'red bold',
266 266 'shelve.age': 'cyan',
267 267 'shelve.newest': 'green bold',
268 268 'shelve.name': 'blue bold',
269 269 'status.added': 'green bold',
270 270 'status.clean': 'none',
271 271 'status.copied': 'none',
272 272 'status.deleted': 'cyan bold underline',
273 273 'status.ignored': 'black bold',
274 274 'status.modified': 'blue bold',
275 275 'status.removed': 'red bold',
276 276 'status.unknown': 'magenta bold underline',
277 277 'tags.normal': 'green',
278 278 'tags.local': 'black bold'}
279 279
280 280
281 281 def _effect_str(effect):
282 282 '''Helper function for render_effects().'''
283 283
284 284 bg = False
285 285 if effect.endswith('_background'):
286 286 bg = True
287 287 effect = effect[:-11]
288 288 attr, val = _terminfo_params[effect]
289 289 if attr:
290 290 return curses.tigetstr(val)
291 291 elif bg:
292 292 return curses.tparm(curses.tigetstr('setab'), val)
293 293 else:
294 294 return curses.tparm(curses.tigetstr('setaf'), val)
295 295
296 296 def render_effects(text, effects):
297 297 'Wrap text in commands to turn on each effect.'
298 298 if not text:
299 299 return text
300 300 if not _terminfo_params:
301 301 start = [str(_effects[e]) for e in ['none'] + effects.split()]
302 302 start = '\033[' + ';'.join(start) + 'm'
303 303 stop = '\033[' + str(_effects['none']) + 'm'
304 304 else:
305 305 start = ''.join(_effect_str(effect)
306 306 for effect in ['none'] + effects.split())
307 307 stop = _effect_str('none')
308 308 return ''.join([start, text, stop])
309 309
310 310 def extstyles():
311 311 for name, ext in extensions.extensions():
312 312 _styles.update(getattr(ext, 'colortable', {}))
313 313
314 314 def configstyles(ui):
315 315 for status, cfgeffects in ui.configitems('color'):
316 316 if '.' not in status or status.startswith('color.'):
317 317 continue
318 318 cfgeffects = ui.configlist('color', status)
319 319 if cfgeffects:
320 320 good = []
321 321 for e in cfgeffects:
322 322 if not _terminfo_params and e in _effects:
323 323 good.append(e)
324 324 elif e in _terminfo_params or e[:-11] in _terminfo_params:
325 325 good.append(e)
326 326 else:
327 327 ui.warn(_("ignoring unknown color/effect %r "
328 328 "(configured in color.%s)\n")
329 329 % (e, status))
330 330 _styles[status] = ' '.join(good)
331 331
332 332 class colorui(uimod.ui):
333 333 def popbuffer(self, labeled=False):
334 334 if self._colormode is None:
335 335 return super(colorui, self).popbuffer(labeled)
336 336
337 337 if labeled:
338 338 return ''.join(self.label(a, label) for a, label
339 339 in self._buffers.pop())
340 340 return ''.join(a for a, label in self._buffers.pop())
341 341
342 342 _colormode = 'ansi'
343 343 def write(self, *args, **opts):
344 344 if self._colormode is None:
345 345 return super(colorui, self).write(*args, **opts)
346 346
347 347 label = opts.get('label', '')
348 348 if self._buffers:
349 349 self._buffers[-1].extend([(str(a), label) for a in args])
350 350 elif self._colormode == 'win32':
351 351 for a in args:
352 352 win32print(a, super(colorui, self).write, **opts)
353 353 else:
354 354 return super(colorui, self).write(
355 355 *[self.label(str(a), label) for a in args], **opts)
356 356
357 357 def write_err(self, *args, **opts):
358 358 if self._colormode is None:
359 359 return super(colorui, self).write_err(*args, **opts)
360 360
361 361 label = opts.get('label', '')
362 362 if self._colormode == 'win32':
363 363 for a in args:
364 364 win32print(a, super(colorui, self).write_err, **opts)
365 365 else:
366 366 return super(colorui, self).write_err(
367 367 *[self.label(str(a), label) for a in args], **opts)
368 368
369 369 def label(self, msg, label):
370 370 if self._colormode is None:
371 371 return super(colorui, self).label(msg, label)
372 372
373 373 effects = []
374 374 for l in label.split():
375 375 s = _styles.get(l, '')
376 376 if s:
377 377 effects.append(s)
378 378 effects = ' '.join(effects)
379 379 if effects:
380 380 return '\n'.join([render_effects(s, effects)
381 381 for s in msg.split('\n')])
382 382 return msg
383 383
384 384 def templatelabel(context, mapping, args):
385 385 if len(args) != 2:
386 386 # i18n: "label" is a keyword
387 387 raise error.ParseError(_("label expects two arguments"))
388 388
389 389 thing = templater._evalifliteral(args[1], context, mapping)
390 390
391 391 # apparently, repo could be a string that is the favicon?
392 392 repo = mapping.get('repo', '')
393 393 if isinstance(repo, str):
394 394 return thing
395 395
396 396 label = templater.stringify(args[0][0](context, mapping, args[0][1]))
397 397 label = templater.runtemplate(context, mapping,
398 398 templater.compiletemplate(label, context))
399 399
400 400 thing = templater.stringify(thing)
401 401 label = templater.stringify(label)
402 402
403 403 return repo.ui.label(thing, label)
404 404
405 405 def uisetup(ui):
406 406 if ui.plain():
407 407 return
408 408 if not isinstance(ui, colorui):
409 409 colorui.__bases__ = (ui.__class__,)
410 410 ui.__class__ = colorui
411 411 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
412 412 mode = _modesetup(ui_, opts['color'])
413 413 colorui._colormode = mode
414 414 if mode:
415 415 extstyles()
416 416 configstyles(ui_)
417 417 return orig(ui_, opts, cmd, cmdfunc)
418 418 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
419 419 templater.funcs['label'] = templatelabel
420 420
421 421 def extsetup(ui):
422 422 commands.globalopts.append(
423 423 ('', 'color', 'auto',
424 424 # i18n: 'always', 'auto', and 'never' are keywords and should
425 425 # not be translated
426 426 _("when to colorize (boolean, always, auto, or never)"),
427 427 _('TYPE')))
428 428
429 def debugcolor(ui, repo, **opts):
430 global _styles
431 _styles = {}
432 for effect in _effects.keys():
433 _styles[effect] = effect
434 ui.write(('colormode: %s\n') % ui._colormode)
435 ui.write(_('available colors:\n'))
436 for label, colors in _styles.items():
437 ui.write(('%s\n') % colors, label=label)
438
429 439 if os.name != 'nt':
430 440 w32effects = None
431 441 else:
432 442 import re, ctypes
433 443
434 444 _kernel32 = ctypes.windll.kernel32
435 445
436 446 _WORD = ctypes.c_ushort
437 447
438 448 _INVALID_HANDLE_VALUE = -1
439 449
440 450 class _COORD(ctypes.Structure):
441 451 _fields_ = [('X', ctypes.c_short),
442 452 ('Y', ctypes.c_short)]
443 453
444 454 class _SMALL_RECT(ctypes.Structure):
445 455 _fields_ = [('Left', ctypes.c_short),
446 456 ('Top', ctypes.c_short),
447 457 ('Right', ctypes.c_short),
448 458 ('Bottom', ctypes.c_short)]
449 459
450 460 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
451 461 _fields_ = [('dwSize', _COORD),
452 462 ('dwCursorPosition', _COORD),
453 463 ('wAttributes', _WORD),
454 464 ('srWindow', _SMALL_RECT),
455 465 ('dwMaximumWindowSize', _COORD)]
456 466
457 467 _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
458 468 _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
459 469
460 470 _FOREGROUND_BLUE = 0x0001
461 471 _FOREGROUND_GREEN = 0x0002
462 472 _FOREGROUND_RED = 0x0004
463 473 _FOREGROUND_INTENSITY = 0x0008
464 474
465 475 _BACKGROUND_BLUE = 0x0010
466 476 _BACKGROUND_GREEN = 0x0020
467 477 _BACKGROUND_RED = 0x0040
468 478 _BACKGROUND_INTENSITY = 0x0080
469 479
470 480 _COMMON_LVB_REVERSE_VIDEO = 0x4000
471 481 _COMMON_LVB_UNDERSCORE = 0x8000
472 482
473 483 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
474 484 w32effects = {
475 485 'none': -1,
476 486 'black': 0,
477 487 'red': _FOREGROUND_RED,
478 488 'green': _FOREGROUND_GREEN,
479 489 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
480 490 'blue': _FOREGROUND_BLUE,
481 491 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
482 492 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
483 493 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
484 494 'bold': _FOREGROUND_INTENSITY,
485 495 'black_background': 0x100, # unused value > 0x0f
486 496 'red_background': _BACKGROUND_RED,
487 497 'green_background': _BACKGROUND_GREEN,
488 498 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
489 499 'blue_background': _BACKGROUND_BLUE,
490 500 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
491 501 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
492 502 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
493 503 _BACKGROUND_BLUE),
494 504 'bold_background': _BACKGROUND_INTENSITY,
495 505 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
496 506 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
497 507 }
498 508
499 509 passthrough = set([_FOREGROUND_INTENSITY,
500 510 _BACKGROUND_INTENSITY,
501 511 _COMMON_LVB_UNDERSCORE,
502 512 _COMMON_LVB_REVERSE_VIDEO])
503 513
504 514 stdout = _kernel32.GetStdHandle(
505 515 _STD_OUTPUT_HANDLE) # don't close the handle returned
506 516 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
507 517 w32effects = None
508 518 else:
509 519 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
510 520 if not _kernel32.GetConsoleScreenBufferInfo(
511 521 stdout, ctypes.byref(csbi)):
512 522 # stdout may not support GetConsoleScreenBufferInfo()
513 523 # when called from subprocess or redirected
514 524 w32effects = None
515 525 else:
516 526 origattr = csbi.wAttributes
517 527 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
518 528 re.MULTILINE | re.DOTALL)
519 529
520 530 def win32print(text, orig, **opts):
521 531 label = opts.get('label', '')
522 532 attr = origattr
523 533
524 534 def mapcolor(val, attr):
525 535 if val == -1:
526 536 return origattr
527 537 elif val in passthrough:
528 538 return attr | val
529 539 elif val > 0x0f:
530 540 return (val & 0x70) | (attr & 0x8f)
531 541 else:
532 542 return (val & 0x07) | (attr & 0xf8)
533 543
534 544 # determine console attributes based on labels
535 545 for l in label.split():
536 546 style = _styles.get(l, '')
537 547 for effect in style.split():
538 548 attr = mapcolor(w32effects[effect], attr)
539 549
540 550 # hack to ensure regexp finds data
541 551 if not text.startswith('\033['):
542 552 text = '\033[m' + text
543 553
544 554 # Look for ANSI-like codes embedded in text
545 555 m = re.match(ansire, text)
546 556
547 557 try:
548 558 while m:
549 559 for sattr in m.group(1).split(';'):
550 560 if sattr:
551 561 attr = mapcolor(int(sattr), attr)
552 562 _kernel32.SetConsoleTextAttribute(stdout, attr)
553 563 orig(m.group(2), **opts)
554 564 m = re.match(ansire, m.group(3))
555 565 finally:
556 566 # Explicitly reset original attributes
557 567 _kernel32.SetConsoleTextAttribute(stdout, origattr)
568
569 cmdtable = {
570 'debugcolor':
571 (debugcolor, [], ('hg debugcolor'))
572 }
General Comments 0
You need to be logged in to leave comments. Login now