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