##// END OF EJS Templates
color: code simplification
Patrick Mezard -
r13998:14c7526f default
parent child Browse files
Show More
@@ -1,480 +1,478 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
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 _terminfo_params.update(dict((
130 (key[6:], (False, int(val)))
129 _terminfo_params.update((key[6:], (False, int(val)))
131 130 for key, val in ui.configitems('color')
132 if key.startswith('color.')
133 )))
131 if key.startswith('color.'))
134 132
135 133 try:
136 134 curses.setupterm()
137 135 except curses.error, e:
138 136 _terminfo_params = {}
139 137 return
140 138
141 139 for key, (b, e) in _terminfo_params.items():
142 140 if not b:
143 141 continue
144 142 if not curses.tigetstr(e):
145 143 # Most terminals don't support dim, invis, etc, so don't be
146 144 # noisy and use ui.debug().
147 145 ui.debug("no terminfo entry for %s\n" % e)
148 146 del _terminfo_params[key]
149 147 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
150 148 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
151 149 "ECMA-48 color\n"))
152 150 _terminfo_params = {}
153 151
154 152 try:
155 153 import curses
156 154 # Mapping from effect name to terminfo attribute name or color number.
157 155 # This will also force-load the curses module.
158 156 _terminfo_params = {'none': (True, 'sgr0'),
159 157 'standout': (True, 'smso'),
160 158 'underline': (True, 'smul'),
161 159 'reverse': (True, 'rev'),
162 160 'inverse': (True, 'rev'),
163 161 'blink': (True, 'blink'),
164 162 'dim': (True, 'dim'),
165 163 'bold': (True, 'bold'),
166 164 'invisible': (True, 'invis'),
167 165 'italic': (True, 'sitm'),
168 166 'black': (False, curses.COLOR_BLACK),
169 167 'red': (False, curses.COLOR_RED),
170 168 'green': (False, curses.COLOR_GREEN),
171 169 'yellow': (False, curses.COLOR_YELLOW),
172 170 'blue': (False, curses.COLOR_BLUE),
173 171 'magenta': (False, curses.COLOR_MAGENTA),
174 172 'cyan': (False, curses.COLOR_CYAN),
175 173 'white': (False, curses.COLOR_WHITE)}
176 174 except ImportError:
177 175 _terminfo_params = False
178 176
179 177 _styles = {'grep.match': 'red bold',
180 178 'bookmarks.current': 'green',
181 179 'branches.active': 'none',
182 180 'branches.closed': 'black bold',
183 181 'branches.current': 'green',
184 182 'branches.inactive': 'none',
185 183 'diff.changed': 'white',
186 184 'diff.deleted': 'red',
187 185 'diff.diffline': 'bold',
188 186 'diff.extended': 'cyan bold',
189 187 'diff.file_a': 'red bold',
190 188 'diff.file_b': 'green bold',
191 189 'diff.hunk': 'magenta',
192 190 'diff.inserted': 'green',
193 191 'diff.trailingwhitespace': 'bold red_background',
194 192 'diffstat.deleted': 'red',
195 193 'diffstat.inserted': 'green',
196 194 'ui.prompt': 'yellow',
197 195 'log.changeset': 'yellow',
198 196 'resolve.resolved': 'green bold',
199 197 'resolve.unresolved': 'red bold',
200 198 'status.added': 'green bold',
201 199 'status.clean': 'none',
202 200 'status.copied': 'none',
203 201 'status.deleted': 'cyan bold underline',
204 202 'status.ignored': 'black bold',
205 203 'status.modified': 'blue bold',
206 204 'status.removed': 'red bold',
207 205 'status.unknown': 'magenta bold underline'}
208 206
209 207
210 208 def _effect_str(effect):
211 209 '''Helper function for render_effects().'''
212 210
213 211 bg = False
214 212 if effect.endswith('_background'):
215 213 bg = True
216 214 effect = effect[:-11]
217 215 attr, val = _terminfo_params[effect]
218 216 if attr:
219 217 return curses.tigetstr(val)
220 218 elif bg:
221 219 return curses.tparm(curses.tigetstr('setab'), val)
222 220 else:
223 221 return curses.tparm(curses.tigetstr('setaf'), val)
224 222
225 223 def render_effects(text, effects):
226 224 'Wrap text in commands to turn on each effect.'
227 225 if not text:
228 226 return text
229 227 if not _terminfo_params:
230 228 start = [str(_effects[e]) for e in ['none'] + effects.split()]
231 229 start = '\033[' + ';'.join(start) + 'm'
232 230 stop = '\033[' + str(_effects['none']) + 'm'
233 231 else:
234 232 start = ''.join(_effect_str(effect)
235 233 for effect in ['none'] + effects.split())
236 234 stop = _effect_str('none')
237 235 return ''.join([start, text, stop])
238 236
239 237 def extstyles():
240 238 for name, ext in extensions.extensions():
241 239 _styles.update(getattr(ext, 'colortable', {}))
242 240
243 241 def configstyles(ui):
244 242 for status, cfgeffects in ui.configitems('color'):
245 243 if '.' not in status or status.startswith('color.'):
246 244 continue
247 245 cfgeffects = ui.configlist('color', status)
248 246 if cfgeffects:
249 247 good = []
250 248 for e in cfgeffects:
251 249 if not _terminfo_params and e in _effects:
252 250 good.append(e)
253 251 elif e in _terminfo_params or e[:-11] in _terminfo_params:
254 252 good.append(e)
255 253 else:
256 254 ui.warn(_("ignoring unknown color/effect %r "
257 255 "(configured in color.%s)\n")
258 256 % (e, status))
259 257 _styles[status] = ' '.join(good)
260 258
261 259 class colorui(uimod.ui):
262 260 def popbuffer(self, labeled=False):
263 261 if labeled:
264 262 return ''.join(self.label(a, label) for a, label
265 263 in self._buffers.pop())
266 264 return ''.join(a for a, label in self._buffers.pop())
267 265
268 266 _colormode = 'ansi'
269 267 def write(self, *args, **opts):
270 268 label = opts.get('label', '')
271 269 if self._buffers:
272 270 self._buffers[-1].extend([(str(a), label) for a in args])
273 271 elif self._colormode == 'win32':
274 272 for a in args:
275 273 win32print(a, super(colorui, self).write, **opts)
276 274 else:
277 275 return super(colorui, self).write(
278 276 *[self.label(str(a), label) for a in args], **opts)
279 277
280 278 def write_err(self, *args, **opts):
281 279 label = opts.get('label', '')
282 280 if self._colormode == 'win32':
283 281 for a in args:
284 282 win32print(a, super(colorui, self).write_err, **opts)
285 283 else:
286 284 return super(colorui, self).write_err(
287 285 *[self.label(str(a), label) for a in args], **opts)
288 286
289 287 def label(self, msg, label):
290 288 effects = []
291 289 for l in label.split():
292 290 s = _styles.get(l, '')
293 291 if s:
294 292 effects.append(s)
295 293 effects = ''.join(effects)
296 294 if effects:
297 295 return '\n'.join([render_effects(s, effects)
298 296 for s in msg.split('\n')])
299 297 return msg
300 298
301 299
302 300 def uisetup(ui):
303 301 global _terminfo_params
304 302 if ui.plain():
305 303 return
306 304 mode = ui.config('color', 'mode', 'auto')
307 305 if mode == 'auto':
308 306 if os.name == 'nt' and 'TERM' not in os.environ:
309 307 # looks line a cmd.exe console, use win32 API or nothing
310 308 mode = w32effects and 'win32' or 'none'
311 309 else:
312 310 _terminfosetup(ui)
313 311 if not _terminfo_params:
314 312 mode = 'ansi'
315 313 else:
316 314 mode = 'terminfo'
317 315 if mode == 'win32':
318 316 if w32effects is None:
319 317 # only warn if color.mode is explicitly set to win32
320 318 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
321 319 return
322 320 _effects.update(w32effects)
323 321 elif mode == 'ansi':
324 322 _terminfo_params = {}
325 323 elif mode == 'terminfo':
326 324 _terminfosetup(ui)
327 elif mode not in ('ansi', 'terminfo'):
325 else:
328 326 return
329 327 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
330 328 coloropt = opts['color']
331 329 auto = coloropt == 'auto'
332 330 always = util.parsebool(coloropt)
333 331 if (always or
334 332 (always is None and
335 333 (auto and (os.environ.get('TERM') != 'dumb' and ui_.formatted())))):
336 334 colorui._colormode = mode
337 335 colorui.__bases__ = (ui_.__class__,)
338 336 ui_.__class__ = colorui
339 337 extstyles()
340 338 configstyles(ui_)
341 339 return orig(ui_, opts, cmd, cmdfunc)
342 340 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
343 341
344 342 def extsetup(ui):
345 343 commands.globalopts.append(
346 344 ('', 'color', 'auto',
347 345 # i18n: 'always', 'auto', and 'never' are keywords and should
348 346 # not be translated
349 347 _("when to colorize (boolean, always, auto, or never)"),
350 348 _('TYPE')))
351 349
352 350 if os.name != 'nt':
353 351 w32effects = None
354 352 else:
355 353 import re, ctypes
356 354
357 355 _kernel32 = ctypes.windll.kernel32
358 356
359 357 _WORD = ctypes.c_ushort
360 358
361 359 _INVALID_HANDLE_VALUE = -1
362 360
363 361 class _COORD(ctypes.Structure):
364 362 _fields_ = [('X', ctypes.c_short),
365 363 ('Y', ctypes.c_short)]
366 364
367 365 class _SMALL_RECT(ctypes.Structure):
368 366 _fields_ = [('Left', ctypes.c_short),
369 367 ('Top', ctypes.c_short),
370 368 ('Right', ctypes.c_short),
371 369 ('Bottom', ctypes.c_short)]
372 370
373 371 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
374 372 _fields_ = [('dwSize', _COORD),
375 373 ('dwCursorPosition', _COORD),
376 374 ('wAttributes', _WORD),
377 375 ('srWindow', _SMALL_RECT),
378 376 ('dwMaximumWindowSize', _COORD)]
379 377
380 378 _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
381 379 _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
382 380
383 381 _FOREGROUND_BLUE = 0x0001
384 382 _FOREGROUND_GREEN = 0x0002
385 383 _FOREGROUND_RED = 0x0004
386 384 _FOREGROUND_INTENSITY = 0x0008
387 385
388 386 _BACKGROUND_BLUE = 0x0010
389 387 _BACKGROUND_GREEN = 0x0020
390 388 _BACKGROUND_RED = 0x0040
391 389 _BACKGROUND_INTENSITY = 0x0080
392 390
393 391 _COMMON_LVB_REVERSE_VIDEO = 0x4000
394 392 _COMMON_LVB_UNDERSCORE = 0x8000
395 393
396 394 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
397 395 w32effects = {
398 396 'none': -1,
399 397 'black': 0,
400 398 'red': _FOREGROUND_RED,
401 399 'green': _FOREGROUND_GREEN,
402 400 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
403 401 'blue': _FOREGROUND_BLUE,
404 402 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
405 403 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
406 404 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
407 405 'bold': _FOREGROUND_INTENSITY,
408 406 'black_background': 0x100, # unused value > 0x0f
409 407 'red_background': _BACKGROUND_RED,
410 408 'green_background': _BACKGROUND_GREEN,
411 409 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
412 410 'blue_background': _BACKGROUND_BLUE,
413 411 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
414 412 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
415 413 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
416 414 _BACKGROUND_BLUE),
417 415 'bold_background': _BACKGROUND_INTENSITY,
418 416 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
419 417 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
420 418 }
421 419
422 420 passthrough = set([_FOREGROUND_INTENSITY,
423 421 _BACKGROUND_INTENSITY,
424 422 _COMMON_LVB_UNDERSCORE,
425 423 _COMMON_LVB_REVERSE_VIDEO])
426 424
427 425 stdout = _kernel32.GetStdHandle(
428 426 _STD_OUTPUT_HANDLE) # don't close the handle returned
429 427 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
430 428 w32effects = None
431 429 else:
432 430 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
433 431 if not _kernel32.GetConsoleScreenBufferInfo(
434 432 stdout, ctypes.byref(csbi)):
435 433 # stdout may not support GetConsoleScreenBufferInfo()
436 434 # when called from subprocess or redirected
437 435 w32effects = None
438 436 else:
439 437 origattr = csbi.wAttributes
440 438 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
441 439 re.MULTILINE | re.DOTALL)
442 440
443 441 def win32print(text, orig, **opts):
444 442 label = opts.get('label', '')
445 443 attr = origattr
446 444
447 445 def mapcolor(val, attr):
448 446 if val == -1:
449 447 return origattr
450 448 elif val in passthrough:
451 449 return attr | val
452 450 elif val > 0x0f:
453 451 return (val & 0x70) | (attr & 0x8f)
454 452 else:
455 453 return (val & 0x07) | (attr & 0xf8)
456 454
457 455 # determine console attributes based on labels
458 456 for l in label.split():
459 457 style = _styles.get(l, '')
460 458 for effect in style.split():
461 459 attr = mapcolor(w32effects[effect], attr)
462 460
463 461 # hack to ensure regexp finds data
464 462 if not text.startswith('\033['):
465 463 text = '\033[m' + text
466 464
467 465 # Look for ANSI-like codes embedded in text
468 466 m = re.match(ansire, text)
469 467
470 468 try:
471 469 while m:
472 470 for sattr in m.group(1).split(';'):
473 471 if sattr:
474 472 attr = mapcolor(int(sattr), attr)
475 473 _kernel32.SetConsoleTextAttribute(stdout, attr)
476 474 orig(m.group(2), **opts)
477 475 m = re.match(ansire, m.group(3))
478 476 finally:
479 477 # Explicity reset original attributes
480 478 _kernel32.SetConsoleTextAttribute(stdout, origattr)
General Comments 0
You need to be logged in to leave comments. Login now