##// END OF EJS Templates
color: fallback and test label as an effect...
Sean Farley -
r20993:a8db48e9 default
parent child Browse files
Show More
@@ -1,577 +1,579
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 valideffect(effect):
315 315 'Determine if the effect is valid or not.'
316 316 good = False
317 317 if not _terminfo_params and effect in _effects:
318 318 good = True
319 319 elif effect in _terminfo_params or effect[:-11] in _terminfo_params:
320 320 good = True
321 321 return good
322 322
323 323 def configstyles(ui):
324 324 for status, cfgeffects in ui.configitems('color'):
325 325 if '.' not in status or status.startswith('color.'):
326 326 continue
327 327 cfgeffects = ui.configlist('color', status)
328 328 if cfgeffects:
329 329 good = []
330 330 for e in cfgeffects:
331 331 if valideffect(e):
332 332 good.append(e)
333 333 else:
334 334 ui.warn(_("ignoring unknown color/effect %r "
335 335 "(configured in color.%s)\n")
336 336 % (e, status))
337 337 _styles[status] = ' '.join(good)
338 338
339 339 class colorui(uimod.ui):
340 340 def popbuffer(self, labeled=False):
341 341 if self._colormode is None:
342 342 return super(colorui, self).popbuffer(labeled)
343 343
344 344 if labeled:
345 345 return ''.join(self.label(a, label) for a, label
346 346 in self._buffers.pop())
347 347 return ''.join(a for a, label in self._buffers.pop())
348 348
349 349 _colormode = 'ansi'
350 350 def write(self, *args, **opts):
351 351 if self._colormode is None:
352 352 return super(colorui, self).write(*args, **opts)
353 353
354 354 label = opts.get('label', '')
355 355 if self._buffers:
356 356 self._buffers[-1].extend([(str(a), label) for a in args])
357 357 elif self._colormode == 'win32':
358 358 for a in args:
359 359 win32print(a, super(colorui, self).write, **opts)
360 360 else:
361 361 return super(colorui, self).write(
362 362 *[self.label(str(a), label) for a in args], **opts)
363 363
364 364 def write_err(self, *args, **opts):
365 365 if self._colormode is None:
366 366 return super(colorui, self).write_err(*args, **opts)
367 367
368 368 label = opts.get('label', '')
369 369 if self._colormode == 'win32':
370 370 for a in args:
371 371 win32print(a, super(colorui, self).write_err, **opts)
372 372 else:
373 373 return super(colorui, self).write_err(
374 374 *[self.label(str(a), label) for a in args], **opts)
375 375
376 376 def label(self, msg, label):
377 377 if self._colormode is None:
378 378 return super(colorui, self).label(msg, label)
379 379
380 380 effects = []
381 381 for l in label.split():
382 382 s = _styles.get(l, '')
383 383 if s:
384 384 effects.append(s)
385 elif valideffect(l):
386 effects.append(l)
385 387 effects = ' '.join(effects)
386 388 if effects:
387 389 return '\n'.join([render_effects(s, effects)
388 390 for s in msg.split('\n')])
389 391 return msg
390 392
391 393 def templatelabel(context, mapping, args):
392 394 if len(args) != 2:
393 395 # i18n: "label" is a keyword
394 396 raise error.ParseError(_("label expects two arguments"))
395 397
396 398 thing = templater._evalifliteral(args[1], context, mapping)
397 399
398 400 # apparently, repo could be a string that is the favicon?
399 401 repo = mapping.get('repo', '')
400 402 if isinstance(repo, str):
401 403 return thing
402 404
403 405 label = templater._evalifliteral(args[0], context, mapping)
404 406
405 407 thing = templater.stringify(thing)
406 408 label = templater.stringify(label)
407 409
408 410 return repo.ui.label(thing, label)
409 411
410 412 def uisetup(ui):
411 413 if ui.plain():
412 414 return
413 415 if not isinstance(ui, colorui):
414 416 colorui.__bases__ = (ui.__class__,)
415 417 ui.__class__ = colorui
416 418 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
417 419 mode = _modesetup(ui_, opts['color'])
418 420 colorui._colormode = mode
419 421 if mode:
420 422 extstyles()
421 423 configstyles(ui_)
422 424 return orig(ui_, opts, cmd, cmdfunc)
423 425 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
424 426 templater.funcs['label'] = templatelabel
425 427
426 428 def extsetup(ui):
427 429 commands.globalopts.append(
428 430 ('', 'color', 'auto',
429 431 # i18n: 'always', 'auto', and 'never' are keywords and should
430 432 # not be translated
431 433 _("when to colorize (boolean, always, auto, or never)"),
432 434 _('TYPE')))
433 435
434 436 def debugcolor(ui, repo, **opts):
435 437 global _styles
436 438 _styles = {}
437 439 for effect in _effects.keys():
438 440 _styles[effect] = effect
439 441 ui.write(('colormode: %s\n') % ui._colormode)
440 442 ui.write(_('available colors:\n'))
441 443 for label, colors in _styles.items():
442 444 ui.write(('%s\n') % colors, label=label)
443 445
444 446 if os.name != 'nt':
445 447 w32effects = None
446 448 else:
447 449 import re, ctypes
448 450
449 451 _kernel32 = ctypes.windll.kernel32
450 452
451 453 _WORD = ctypes.c_ushort
452 454
453 455 _INVALID_HANDLE_VALUE = -1
454 456
455 457 class _COORD(ctypes.Structure):
456 458 _fields_ = [('X', ctypes.c_short),
457 459 ('Y', ctypes.c_short)]
458 460
459 461 class _SMALL_RECT(ctypes.Structure):
460 462 _fields_ = [('Left', ctypes.c_short),
461 463 ('Top', ctypes.c_short),
462 464 ('Right', ctypes.c_short),
463 465 ('Bottom', ctypes.c_short)]
464 466
465 467 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
466 468 _fields_ = [('dwSize', _COORD),
467 469 ('dwCursorPosition', _COORD),
468 470 ('wAttributes', _WORD),
469 471 ('srWindow', _SMALL_RECT),
470 472 ('dwMaximumWindowSize', _COORD)]
471 473
472 474 _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
473 475 _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
474 476
475 477 _FOREGROUND_BLUE = 0x0001
476 478 _FOREGROUND_GREEN = 0x0002
477 479 _FOREGROUND_RED = 0x0004
478 480 _FOREGROUND_INTENSITY = 0x0008
479 481
480 482 _BACKGROUND_BLUE = 0x0010
481 483 _BACKGROUND_GREEN = 0x0020
482 484 _BACKGROUND_RED = 0x0040
483 485 _BACKGROUND_INTENSITY = 0x0080
484 486
485 487 _COMMON_LVB_REVERSE_VIDEO = 0x4000
486 488 _COMMON_LVB_UNDERSCORE = 0x8000
487 489
488 490 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
489 491 w32effects = {
490 492 'none': -1,
491 493 'black': 0,
492 494 'red': _FOREGROUND_RED,
493 495 'green': _FOREGROUND_GREEN,
494 496 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
495 497 'blue': _FOREGROUND_BLUE,
496 498 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
497 499 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
498 500 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
499 501 'bold': _FOREGROUND_INTENSITY,
500 502 'black_background': 0x100, # unused value > 0x0f
501 503 'red_background': _BACKGROUND_RED,
502 504 'green_background': _BACKGROUND_GREEN,
503 505 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
504 506 'blue_background': _BACKGROUND_BLUE,
505 507 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
506 508 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
507 509 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
508 510 _BACKGROUND_BLUE),
509 511 'bold_background': _BACKGROUND_INTENSITY,
510 512 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
511 513 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
512 514 }
513 515
514 516 passthrough = set([_FOREGROUND_INTENSITY,
515 517 _BACKGROUND_INTENSITY,
516 518 _COMMON_LVB_UNDERSCORE,
517 519 _COMMON_LVB_REVERSE_VIDEO])
518 520
519 521 stdout = _kernel32.GetStdHandle(
520 522 _STD_OUTPUT_HANDLE) # don't close the handle returned
521 523 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
522 524 w32effects = None
523 525 else:
524 526 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
525 527 if not _kernel32.GetConsoleScreenBufferInfo(
526 528 stdout, ctypes.byref(csbi)):
527 529 # stdout may not support GetConsoleScreenBufferInfo()
528 530 # when called from subprocess or redirected
529 531 w32effects = None
530 532 else:
531 533 origattr = csbi.wAttributes
532 534 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
533 535 re.MULTILINE | re.DOTALL)
534 536
535 537 def win32print(text, orig, **opts):
536 538 label = opts.get('label', '')
537 539 attr = origattr
538 540
539 541 def mapcolor(val, attr):
540 542 if val == -1:
541 543 return origattr
542 544 elif val in passthrough:
543 545 return attr | val
544 546 elif val > 0x0f:
545 547 return (val & 0x70) | (attr & 0x8f)
546 548 else:
547 549 return (val & 0x07) | (attr & 0xf8)
548 550
549 551 # determine console attributes based on labels
550 552 for l in label.split():
551 553 style = _styles.get(l, '')
552 554 for effect in style.split():
553 555 attr = mapcolor(w32effects[effect], attr)
554 556
555 557 # hack to ensure regexp finds data
556 558 if not text.startswith('\033['):
557 559 text = '\033[m' + text
558 560
559 561 # Look for ANSI-like codes embedded in text
560 562 m = re.match(ansire, text)
561 563
562 564 try:
563 565 while m:
564 566 for sattr in m.group(1).split(';'):
565 567 if sattr:
566 568 attr = mapcolor(int(sattr), attr)
567 569 _kernel32.SetConsoleTextAttribute(stdout, attr)
568 570 orig(m.group(2), **opts)
569 571 m = re.match(ansire, m.group(3))
570 572 finally:
571 573 # Explicitly reset original attributes
572 574 _kernel32.SetConsoleTextAttribute(stdout, origattr)
573 575
574 576 cmdtable = {
575 577 'debugcolor':
576 578 (debugcolor, [], ('hg debugcolor'))
577 579 }
General Comments 0
You need to be logged in to leave comments. Login now