##// END OF EJS Templates
color: add ui to effect rendering...
Pierre-Yves David -
r31112:7f056fdb default
parent child Browse files
Show More
@@ -1,470 +1,470 b''
1 1 # utility for color output for Mercurial commands
2 2 #
3 3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com> and other
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 from __future__ import absolute_import
9 9
10 10 from .i18n import _
11 11
12 12 from . import (
13 13 encoding,
14 14 pycompat,
15 15 util
16 16 )
17 17
18 18 try:
19 19 import curses
20 20 # Mapping from effect name to terminfo attribute name (or raw code) or
21 21 # color number. This will also force-load the curses module.
22 22 _terminfo_params = {
23 23 'none': (True, 'sgr0', ''),
24 24 'standout': (True, 'smso', ''),
25 25 'underline': (True, 'smul', ''),
26 26 'reverse': (True, 'rev', ''),
27 27 'inverse': (True, 'rev', ''),
28 28 'blink': (True, 'blink', ''),
29 29 'dim': (True, 'dim', ''),
30 30 'bold': (True, 'bold', ''),
31 31 'invisible': (True, 'invis', ''),
32 32 'italic': (True, 'sitm', ''),
33 33 'black': (False, curses.COLOR_BLACK, ''),
34 34 'red': (False, curses.COLOR_RED, ''),
35 35 'green': (False, curses.COLOR_GREEN, ''),
36 36 'yellow': (False, curses.COLOR_YELLOW, ''),
37 37 'blue': (False, curses.COLOR_BLUE, ''),
38 38 'magenta': (False, curses.COLOR_MAGENTA, ''),
39 39 'cyan': (False, curses.COLOR_CYAN, ''),
40 40 'white': (False, curses.COLOR_WHITE, ''),
41 41 }
42 42 except ImportError:
43 43 curses = None
44 44 _terminfo_params = {}
45 45
46 46 # allow the extensions to change the default
47 47 _enabledbydefault = False
48 48
49 49 # start and stop parameters for effects
50 50 _effects = {
51 51 'none': 0,
52 52 'black': 30,
53 53 'red': 31,
54 54 'green': 32,
55 55 'yellow': 33,
56 56 'blue': 34,
57 57 'magenta': 35,
58 58 'cyan': 36,
59 59 'white': 37,
60 60 'bold': 1,
61 61 'italic': 3,
62 62 'underline': 4,
63 63 'inverse': 7,
64 64 'dim': 2,
65 65 'black_background': 40,
66 66 'red_background': 41,
67 67 'green_background': 42,
68 68 'yellow_background': 43,
69 69 'blue_background': 44,
70 70 'purple_background': 45,
71 71 'cyan_background': 46,
72 72 'white_background': 47,
73 73 }
74 74
75 75 _styles = {
76 76 'grep.match': 'red bold',
77 77 'grep.linenumber': 'green',
78 78 'grep.rev': 'green',
79 79 'grep.change': 'green',
80 80 'grep.sep': 'cyan',
81 81 'grep.filename': 'magenta',
82 82 'grep.user': 'magenta',
83 83 'grep.date': 'magenta',
84 84 'bookmarks.active': 'green',
85 85 'branches.active': 'none',
86 86 'branches.closed': 'black bold',
87 87 'branches.current': 'green',
88 88 'branches.inactive': 'none',
89 89 'diff.changed': 'white',
90 90 'diff.deleted': 'red',
91 91 'diff.diffline': 'bold',
92 92 'diff.extended': 'cyan bold',
93 93 'diff.file_a': 'red bold',
94 94 'diff.file_b': 'green bold',
95 95 'diff.hunk': 'magenta',
96 96 'diff.inserted': 'green',
97 97 'diff.tab': '',
98 98 'diff.trailingwhitespace': 'bold red_background',
99 99 'changeset.public' : '',
100 100 'changeset.draft' : '',
101 101 'changeset.secret' : '',
102 102 'diffstat.deleted': 'red',
103 103 'diffstat.inserted': 'green',
104 104 'histedit.remaining': 'red bold',
105 105 'ui.prompt': 'yellow',
106 106 'log.changeset': 'yellow',
107 107 'patchbomb.finalsummary': '',
108 108 'patchbomb.from': 'magenta',
109 109 'patchbomb.to': 'cyan',
110 110 'patchbomb.subject': 'green',
111 111 'patchbomb.diffstats': '',
112 112 'rebase.rebased': 'blue',
113 113 'rebase.remaining': 'red bold',
114 114 'resolve.resolved': 'green bold',
115 115 'resolve.unresolved': 'red bold',
116 116 'shelve.age': 'cyan',
117 117 'shelve.newest': 'green bold',
118 118 'shelve.name': 'blue bold',
119 119 'status.added': 'green bold',
120 120 'status.clean': 'none',
121 121 'status.copied': 'none',
122 122 'status.deleted': 'cyan bold underline',
123 123 'status.ignored': 'black bold',
124 124 'status.modified': 'blue bold',
125 125 'status.removed': 'red bold',
126 126 'status.unknown': 'magenta bold underline',
127 127 'tags.normal': 'green',
128 128 'tags.local': 'black bold',
129 129 }
130 130
131 131 def loadcolortable(ui, extname, colortable):
132 132 _styles.update(colortable)
133 133
134 134 def _terminfosetup(ui, mode):
135 135 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
136 136
137 137 # If we failed to load curses, we go ahead and return.
138 138 if curses is None:
139 139 return
140 140 # Otherwise, see what the config file says.
141 141 if mode not in ('auto', 'terminfo'):
142 142 return
143 143
144 144 for key, val in ui.configitems('color'):
145 145 if key.startswith('color.'):
146 146 newval = (False, int(val), '')
147 147 _terminfo_params[key[6:]] = newval
148 148 elif key.startswith('terminfo.'):
149 149 newval = (True, '', val.replace('\\E', '\x1b'))
150 150 _terminfo_params[key[9:]] = newval
151 151 try:
152 152 curses.setupterm()
153 153 except curses.error as e:
154 154 _terminfo_params.clear()
155 155 return
156 156
157 157 for key, (b, e, c) in _terminfo_params.items():
158 158 if not b:
159 159 continue
160 160 if not c and not curses.tigetstr(e):
161 161 # Most terminals don't support dim, invis, etc, so don't be
162 162 # noisy and use ui.debug().
163 163 ui.debug("no terminfo entry for %s\n" % e)
164 164 del _terminfo_params[key]
165 165 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
166 166 # Only warn about missing terminfo entries if we explicitly asked for
167 167 # terminfo mode.
168 168 if mode == "terminfo":
169 169 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
170 170 "ECMA-48 color\n"))
171 171 _terminfo_params.clear()
172 172
173 173 def setup(ui):
174 174 """configure color on a ui
175 175
176 176 That function both set the colormode for the ui object and read
177 177 the configuration looking for custom colors and effect definitions."""
178 178 mode = _modesetup(ui)
179 179 ui._colormode = mode
180 180 if mode and mode != 'debug':
181 181 configstyles(ui)
182 182
183 183 def _modesetup(ui):
184 184 if ui.plain():
185 185 return None
186 186 default = 'never'
187 187 if _enabledbydefault:
188 188 default = 'auto'
189 189 # experimental config: ui.color
190 190 config = ui.config('ui', 'color', default)
191 191 if config == 'debug':
192 192 return 'debug'
193 193
194 194 auto = (config == 'auto')
195 195 always = not auto and util.parsebool(config)
196 196 if not always and not auto:
197 197 return None
198 198
199 199 formatted = (always or (encoding.environ.get('TERM') != 'dumb'
200 200 and ui.formatted()))
201 201
202 202 mode = ui.config('color', 'mode', 'auto')
203 203
204 204 # If pager is active, color.pagermode overrides color.mode.
205 205 if getattr(ui, 'pageractive', False):
206 206 mode = ui.config('color', 'pagermode', mode)
207 207
208 208 realmode = mode
209 209 if mode == 'auto':
210 210 if pycompat.osname == 'nt':
211 211 term = encoding.environ.get('TERM')
212 212 # TERM won't be defined in a vanilla cmd.exe environment.
213 213
214 214 # UNIX-like environments on Windows such as Cygwin and MSYS will
215 215 # set TERM. They appear to make a best effort attempt at setting it
216 216 # to something appropriate. However, not all environments with TERM
217 217 # defined support ANSI. Since "ansi" could result in terminal
218 218 # gibberish, we error on the side of selecting "win32". However, if
219 219 # w32effects is not defined, we almost certainly don't support
220 220 # "win32", so don't even try.
221 221 if (term and 'xterm' in term) or not w32effects:
222 222 realmode = 'ansi'
223 223 else:
224 224 realmode = 'win32'
225 225 else:
226 226 realmode = 'ansi'
227 227
228 228 def modewarn():
229 229 # only warn if color.mode was explicitly set and we're in
230 230 # a formatted terminal
231 231 if mode == realmode and ui.formatted():
232 232 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
233 233
234 234 if realmode == 'win32':
235 235 _terminfo_params.clear()
236 236 if not w32effects:
237 237 modewarn()
238 238 return None
239 239 _effects.update(w32effects)
240 240 elif realmode == 'ansi':
241 241 _terminfo_params.clear()
242 242 elif realmode == 'terminfo':
243 243 _terminfosetup(ui, mode)
244 244 if not _terminfo_params:
245 245 ## FIXME Shouldn't we return None in this case too?
246 246 modewarn()
247 247 realmode = 'ansi'
248 248 else:
249 249 return None
250 250
251 251 if always or (auto and formatted):
252 252 return realmode
253 253 return None
254 254
255 255 def configstyles(ui):
256 256 for status, cfgeffects in ui.configitems('color'):
257 257 if '.' not in status or status.startswith(('color.', 'terminfo.')):
258 258 continue
259 259 cfgeffects = ui.configlist('color', status)
260 260 if cfgeffects:
261 261 good = []
262 262 for e in cfgeffects:
263 if valideffect(e):
263 if valideffect(ui, e):
264 264 good.append(e)
265 265 else:
266 266 ui.warn(_("ignoring unknown color/effect %r "
267 267 "(configured in color.%s)\n")
268 268 % (e, status))
269 269 _styles[status] = ' '.join(good)
270 270
271 def valideffect(effect):
271 def valideffect(ui, effect):
272 272 'Determine if the effect is valid or not.'
273 273 return ((not _terminfo_params and effect in _effects)
274 274 or (effect in _terminfo_params
275 275 or effect[:-11] in _terminfo_params))
276 276
277 def _effect_str(effect):
277 def _effect_str(ui, effect):
278 278 '''Helper function for render_effects().'''
279 279
280 280 bg = False
281 281 if effect.endswith('_background'):
282 282 bg = True
283 283 effect = effect[:-11]
284 284 try:
285 285 attr, val, termcode = _terminfo_params[effect]
286 286 except KeyError:
287 287 return ''
288 288 if attr:
289 289 if termcode:
290 290 return termcode
291 291 else:
292 292 return curses.tigetstr(val)
293 293 elif bg:
294 294 return curses.tparm(curses.tigetstr('setab'), val)
295 295 else:
296 296 return curses.tparm(curses.tigetstr('setaf'), val)
297 297
298 def _render_effects(text, effects):
298 def _render_effects(ui, text, effects):
299 299 'Wrap text in commands to turn on each effect.'
300 300 if not text:
301 301 return text
302 302 if _terminfo_params:
303 start = ''.join(_effect_str(effect)
303 start = ''.join(_effect_str(ui, effect)
304 304 for effect in ['none'] + effects.split())
305 stop = _effect_str('none')
305 stop = _effect_str(ui, 'none')
306 306 else:
307 307 start = [str(_effects[e]) for e in ['none'] + effects.split()]
308 308 start = '\033[' + ';'.join(start) + 'm'
309 309 stop = '\033[' + str(_effects['none']) + 'm'
310 310 return ''.join([start, text, stop])
311 311
312 312 def colorlabel(ui, msg, label):
313 313 """add color control code according to the mode"""
314 314 if ui._colormode == 'debug':
315 315 if label and msg:
316 316 if msg[-1] == '\n':
317 317 msg = "[%s|%s]\n" % (label, msg[:-1])
318 318 else:
319 319 msg = "[%s|%s]" % (label, msg)
320 320 elif ui._colormode is not None:
321 321 effects = []
322 322 for l in label.split():
323 323 s = _styles.get(l, '')
324 324 if s:
325 325 effects.append(s)
326 elif valideffect(l):
326 elif valideffect(ui, l):
327 327 effects.append(l)
328 328 effects = ' '.join(effects)
329 329 if effects:
330 msg = '\n'.join([_render_effects(line, effects)
330 msg = '\n'.join([_render_effects(ui, line, effects)
331 331 for line in msg.split('\n')])
332 332 return msg
333 333
334 334 w32effects = None
335 335 if pycompat.osname == 'nt':
336 336 import ctypes
337 337 import re
338 338
339 339 _kernel32 = ctypes.windll.kernel32
340 340
341 341 _WORD = ctypes.c_ushort
342 342
343 343 _INVALID_HANDLE_VALUE = -1
344 344
345 345 class _COORD(ctypes.Structure):
346 346 _fields_ = [('X', ctypes.c_short),
347 347 ('Y', ctypes.c_short)]
348 348
349 349 class _SMALL_RECT(ctypes.Structure):
350 350 _fields_ = [('Left', ctypes.c_short),
351 351 ('Top', ctypes.c_short),
352 352 ('Right', ctypes.c_short),
353 353 ('Bottom', ctypes.c_short)]
354 354
355 355 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
356 356 _fields_ = [('dwSize', _COORD),
357 357 ('dwCursorPosition', _COORD),
358 358 ('wAttributes', _WORD),
359 359 ('srWindow', _SMALL_RECT),
360 360 ('dwMaximumWindowSize', _COORD)]
361 361
362 362 _STD_OUTPUT_HANDLE = 0xfffffff5 # (DWORD)-11
363 363 _STD_ERROR_HANDLE = 0xfffffff4 # (DWORD)-12
364 364
365 365 _FOREGROUND_BLUE = 0x0001
366 366 _FOREGROUND_GREEN = 0x0002
367 367 _FOREGROUND_RED = 0x0004
368 368 _FOREGROUND_INTENSITY = 0x0008
369 369
370 370 _BACKGROUND_BLUE = 0x0010
371 371 _BACKGROUND_GREEN = 0x0020
372 372 _BACKGROUND_RED = 0x0040
373 373 _BACKGROUND_INTENSITY = 0x0080
374 374
375 375 _COMMON_LVB_REVERSE_VIDEO = 0x4000
376 376 _COMMON_LVB_UNDERSCORE = 0x8000
377 377
378 378 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
379 379 w32effects = {
380 380 'none': -1,
381 381 'black': 0,
382 382 'red': _FOREGROUND_RED,
383 383 'green': _FOREGROUND_GREEN,
384 384 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
385 385 'blue': _FOREGROUND_BLUE,
386 386 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
387 387 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
388 388 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
389 389 'bold': _FOREGROUND_INTENSITY,
390 390 'black_background': 0x100, # unused value > 0x0f
391 391 'red_background': _BACKGROUND_RED,
392 392 'green_background': _BACKGROUND_GREEN,
393 393 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
394 394 'blue_background': _BACKGROUND_BLUE,
395 395 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
396 396 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
397 397 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
398 398 _BACKGROUND_BLUE),
399 399 'bold_background': _BACKGROUND_INTENSITY,
400 400 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
401 401 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
402 402 }
403 403
404 404 passthrough = set([_FOREGROUND_INTENSITY,
405 405 _BACKGROUND_INTENSITY,
406 406 _COMMON_LVB_UNDERSCORE,
407 407 _COMMON_LVB_REVERSE_VIDEO])
408 408
409 409 stdout = _kernel32.GetStdHandle(
410 410 _STD_OUTPUT_HANDLE) # don't close the handle returned
411 411 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
412 412 w32effects = None
413 413 else:
414 414 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
415 415 if not _kernel32.GetConsoleScreenBufferInfo(
416 416 stdout, ctypes.byref(csbi)):
417 417 # stdout may not support GetConsoleScreenBufferInfo()
418 418 # when called from subprocess or redirected
419 419 w32effects = None
420 420 else:
421 421 origattr = csbi.wAttributes
422 422 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
423 423 re.MULTILINE | re.DOTALL)
424 424
425 425 def win32print(writefunc, *msgs, **opts):
426 426 for text in msgs:
427 427 _win32print(text, writefunc, **opts)
428 428
429 429 def _win32print(text, writefunc, **opts):
430 430 label = opts.get('label', '')
431 431 attr = origattr
432 432
433 433 def mapcolor(val, attr):
434 434 if val == -1:
435 435 return origattr
436 436 elif val in passthrough:
437 437 return attr | val
438 438 elif val > 0x0f:
439 439 return (val & 0x70) | (attr & 0x8f)
440 440 else:
441 441 return (val & 0x07) | (attr & 0xf8)
442 442
443 443 # determine console attributes based on labels
444 444 for l in label.split():
445 445 style = _styles.get(l, '')
446 446 for effect in style.split():
447 447 try:
448 448 attr = mapcolor(w32effects[effect], attr)
449 449 except KeyError:
450 450 # w32effects could not have certain attributes so we skip
451 451 # them if not found
452 452 pass
453 453 # hack to ensure regexp finds data
454 454 if not text.startswith('\033['):
455 455 text = '\033[m' + text
456 456
457 457 # Look for ANSI-like codes embedded in text
458 458 m = re.match(ansire, text)
459 459
460 460 try:
461 461 while m:
462 462 for sattr in m.group(1).split(';'):
463 463 if sattr:
464 464 attr = mapcolor(int(sattr), attr)
465 465 _kernel32.SetConsoleTextAttribute(stdout, attr)
466 466 writefunc(m.group(2), **opts)
467 467 m = re.match(ansire, m.group(3))
468 468 finally:
469 469 # Explicitly reset original attributes
470 470 _kernel32.SetConsoleTextAttribute(stdout, origattr)
General Comments 0
You need to be logged in to leave comments. Login now