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