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