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