##// END OF EJS Templates
color: be more conservative about setting ANSI mode on Windows (BC)...
Gregory Szorc -
r24028:a78888d9 default
parent child Browse files
Show More
@@ -1,653 +1,667 b''
1 1 # color.py color output for Mercurial commands
2 2 #
3 3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
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 '''colorize output from some commands
9 9
10 10 The color extension colorizes output from several Mercurial commands.
11 11 For example, the diff command shows additions in green and deletions
12 12 in red, while the status command shows modified files in magenta. Many
13 13 other commands have analogous colors. It is possible to customize
14 14 these colors.
15 15
16 16 Effects
17 17 -------
18 18
19 19 Other effects in addition to color, like bold and underlined text, are
20 20 also available. By default, the terminfo database is used to find the
21 21 terminal codes used to change color and effect. If terminfo is not
22 22 available, then effects are rendered with the ECMA-48 SGR control
23 23 function (aka ANSI escape codes).
24 24
25 25 The available effects in terminfo mode are 'blink', 'bold', 'dim',
26 26 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
27 27 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
28 28 'underline'. How each is rendered depends on the terminal emulator.
29 29 Some may not be available for a given terminal type, and will be
30 30 silently ignored.
31 31
32 32 Labels
33 33 ------
34 34
35 35 Text receives color effects depending on the labels that it has. Many
36 36 default Mercurial commands emit labelled text. You can also define
37 37 your own labels in templates using the label function, see :hg:`help
38 38 templates`. A single portion of text may have more than one label. In
39 39 that case, effects given to the last label will override any other
40 40 effects. This includes the special "none" effect, which nullifies
41 41 other effects.
42 42
43 43 Labels are normally invisible. In order to see these labels and their
44 44 position in the text, use the global --color=debug option. The same
45 45 anchor text may be associated to multiple labels, e.g.
46 46
47 47 [log.changeset changeset.secret|changeset: 22611:6f0a53c8f587]
48 48
49 49 The following are the default effects for some default labels. Default
50 50 effects may be overridden from your configuration file::
51 51
52 52 [color]
53 53 status.modified = blue bold underline red_background
54 54 status.added = green bold
55 55 status.removed = red bold blue_background
56 56 status.deleted = cyan bold underline
57 57 status.unknown = magenta bold underline
58 58 status.ignored = black bold
59 59
60 60 # 'none' turns off all effects
61 61 status.clean = none
62 62 status.copied = none
63 63
64 64 qseries.applied = blue bold underline
65 65 qseries.unapplied = black bold
66 66 qseries.missing = red bold
67 67
68 68 diff.diffline = bold
69 69 diff.extended = cyan bold
70 70 diff.file_a = red bold
71 71 diff.file_b = green bold
72 72 diff.hunk = magenta
73 73 diff.deleted = red
74 74 diff.inserted = green
75 75 diff.changed = white
76 76 diff.tab =
77 77 diff.trailingwhitespace = bold red_background
78 78
79 79 # Blank so it inherits the style of the surrounding label
80 80 changeset.public =
81 81 changeset.draft =
82 82 changeset.secret =
83 83
84 84 resolve.unresolved = red bold
85 85 resolve.resolved = green bold
86 86
87 87 bookmarks.current = green
88 88
89 89 branches.active = none
90 90 branches.closed = black bold
91 91 branches.current = green
92 92 branches.inactive = none
93 93
94 94 tags.normal = green
95 95 tags.local = black bold
96 96
97 97 rebase.rebased = blue
98 98 rebase.remaining = red bold
99 99
100 100 shelve.age = cyan
101 101 shelve.newest = green bold
102 102 shelve.name = blue bold
103 103
104 104 histedit.remaining = red bold
105 105
106 106 Custom colors
107 107 -------------
108 108
109 109 Because there are only eight standard colors, this module allows you
110 110 to define color names for other color slots which might be available
111 111 for your terminal type, assuming terminfo mode. For instance::
112 112
113 113 color.brightblue = 12
114 114 color.pink = 207
115 115 color.orange = 202
116 116
117 117 to set 'brightblue' to color slot 12 (useful for 16 color terminals
118 118 that have brighter colors defined in the upper eight) and, 'pink' and
119 119 'orange' to colors in 256-color xterm's default color cube. These
120 120 defined colors may then be used as any of the pre-defined eight,
121 121 including appending '_background' to set the background to that color.
122 122
123 123 Modes
124 124 -----
125 125
126 126 By default, the color extension will use ANSI mode (or win32 mode on
127 127 Windows) if it detects a terminal. To override auto mode (to enable
128 128 terminfo mode, for example), set the following configuration option::
129 129
130 130 [color]
131 131 mode = terminfo
132 132
133 133 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
134 134 disable color.
135 135
136 136 Note that on some systems, terminfo mode may cause problems when using
137 137 color with the pager extension and less -R. less with the -R option
138 138 will only display ECMA-48 color codes, and terminfo mode may sometimes
139 139 emit codes that less doesn't understand. You can work around this by
140 140 either using ansi mode (or auto mode), or by using less -r (which will
141 141 pass through all terminal control codes, not just color control
142 142 codes).
143 143 '''
144 144
145 145 import os
146 146
147 147 from mercurial import cmdutil, commands, dispatch, extensions, subrepo, util
148 148 from mercurial import ui as uimod
149 149 from mercurial import templater, error
150 150 from mercurial.i18n import _
151 151
152 152 cmdtable = {}
153 153 command = cmdutil.command(cmdtable)
154 154 testedwith = 'internal'
155 155
156 156 # start and stop parameters for effects
157 157 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
158 158 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
159 159 'italic': 3, 'underline': 4, 'inverse': 7, 'dim': 2,
160 160 'black_background': 40, 'red_background': 41,
161 161 'green_background': 42, 'yellow_background': 43,
162 162 'blue_background': 44, 'purple_background': 45,
163 163 'cyan_background': 46, 'white_background': 47}
164 164
165 165 def _terminfosetup(ui, mode):
166 166 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
167 167
168 168 global _terminfo_params
169 169 # If we failed to load curses, we go ahead and return.
170 170 if not _terminfo_params:
171 171 return
172 172 # Otherwise, see what the config file says.
173 173 if mode not in ('auto', 'terminfo'):
174 174 return
175 175
176 176 _terminfo_params.update((key[6:], (False, int(val)))
177 177 for key, val in ui.configitems('color')
178 178 if key.startswith('color.'))
179 179
180 180 try:
181 181 curses.setupterm()
182 182 except curses.error, e:
183 183 _terminfo_params = {}
184 184 return
185 185
186 186 for key, (b, e) in _terminfo_params.items():
187 187 if not b:
188 188 continue
189 189 if not curses.tigetstr(e):
190 190 # Most terminals don't support dim, invis, etc, so don't be
191 191 # noisy and use ui.debug().
192 192 ui.debug("no terminfo entry for %s\n" % e)
193 193 del _terminfo_params[key]
194 194 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
195 195 # Only warn about missing terminfo entries if we explicitly asked for
196 196 # terminfo mode.
197 197 if mode == "terminfo":
198 198 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
199 199 "ECMA-48 color\n"))
200 200 _terminfo_params = {}
201 201
202 202 def _modesetup(ui, coloropt):
203 203 global _terminfo_params
204 204
205 205 if coloropt == 'debug':
206 206 return 'debug'
207 207
208 208 auto = (coloropt == 'auto')
209 209 always = not auto and util.parsebool(coloropt)
210 210 if not always and not auto:
211 211 return None
212 212
213 213 formatted = always or (os.environ.get('TERM') != 'dumb' and ui.formatted())
214 214
215 215 mode = ui.config('color', 'mode', 'auto')
216 216 realmode = mode
217 217 if mode == 'auto':
218 if os.name == 'nt' and 'TERM' not in os.environ:
219 # looks line a cmd.exe console, use win32 API or nothing
220 realmode = 'win32'
218 if os.name == 'nt':
219 term = os.environ.get('TERM')
220 # TERM won't be defined in a vanilla cmd.exe environment.
221 if not term:
222 realmode = 'win32'
223
224 # UNIX-like environments on Windows such as Cygwin and MSYS will
225 # set TERM. They appear to make a best effort attempt at setting it
226 # to something appropriate. However, not all environments with TERM
227 # defined support ANSI. Since "ansi" could result in terminal
228 # gibberish, we error on the side of selecting "win32". However, if
229 # w32effects is not defined, we almost certainly don't support
230 # "win32", so don't even try.
231 if 'xterm' in term or not w32effects:
232 realmode = 'ansi'
233 else:
234 realmode = 'win32'
221 235 else:
222 236 realmode = 'ansi'
223 237
224 238 if realmode == 'win32':
225 239 _terminfo_params = {}
226 240 if not w32effects:
227 241 if mode == 'win32':
228 242 # only warn if color.mode is explicitly set to win32
229 243 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
230 244 return None
231 245 _effects.update(w32effects)
232 246 elif realmode == 'ansi':
233 247 _terminfo_params = {}
234 248 elif realmode == 'terminfo':
235 249 _terminfosetup(ui, mode)
236 250 if not _terminfo_params:
237 251 if mode == 'terminfo':
238 252 ## FIXME Shouldn't we return None in this case too?
239 253 # only warn if color.mode is explicitly set to win32
240 254 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
241 255 realmode = 'ansi'
242 256 else:
243 257 return None
244 258
245 259 if always or (auto and formatted):
246 260 return realmode
247 261 return None
248 262
249 263 try:
250 264 import curses
251 265 # Mapping from effect name to terminfo attribute name or color number.
252 266 # This will also force-load the curses module.
253 267 _terminfo_params = {'none': (True, 'sgr0'),
254 268 'standout': (True, 'smso'),
255 269 'underline': (True, 'smul'),
256 270 'reverse': (True, 'rev'),
257 271 'inverse': (True, 'rev'),
258 272 'blink': (True, 'blink'),
259 273 'dim': (True, 'dim'),
260 274 'bold': (True, 'bold'),
261 275 'invisible': (True, 'invis'),
262 276 'italic': (True, 'sitm'),
263 277 'black': (False, curses.COLOR_BLACK),
264 278 'red': (False, curses.COLOR_RED),
265 279 'green': (False, curses.COLOR_GREEN),
266 280 'yellow': (False, curses.COLOR_YELLOW),
267 281 'blue': (False, curses.COLOR_BLUE),
268 282 'magenta': (False, curses.COLOR_MAGENTA),
269 283 'cyan': (False, curses.COLOR_CYAN),
270 284 'white': (False, curses.COLOR_WHITE)}
271 285 except ImportError:
272 286 _terminfo_params = {}
273 287
274 288 _styles = {'grep.match': 'red bold',
275 289 'grep.linenumber': 'green',
276 290 'grep.rev': 'green',
277 291 'grep.change': 'green',
278 292 'grep.sep': 'cyan',
279 293 'grep.filename': 'magenta',
280 294 'grep.user': 'magenta',
281 295 'grep.date': 'magenta',
282 296 'bookmarks.current': 'green',
283 297 'branches.active': 'none',
284 298 'branches.closed': 'black bold',
285 299 'branches.current': 'green',
286 300 'branches.inactive': 'none',
287 301 'diff.changed': 'white',
288 302 'diff.deleted': 'red',
289 303 'diff.diffline': 'bold',
290 304 'diff.extended': 'cyan bold',
291 305 'diff.file_a': 'red bold',
292 306 'diff.file_b': 'green bold',
293 307 'diff.hunk': 'magenta',
294 308 'diff.inserted': 'green',
295 309 'diff.tab': '',
296 310 'diff.trailingwhitespace': 'bold red_background',
297 311 'changeset.public' : '',
298 312 'changeset.draft' : '',
299 313 'changeset.secret' : '',
300 314 'diffstat.deleted': 'red',
301 315 'diffstat.inserted': 'green',
302 316 'histedit.remaining': 'red bold',
303 317 'ui.prompt': 'yellow',
304 318 'log.changeset': 'yellow',
305 319 'patchbomb.finalsummary': '',
306 320 'patchbomb.from': 'magenta',
307 321 'patchbomb.to': 'cyan',
308 322 'patchbomb.subject': 'green',
309 323 'patchbomb.diffstats': '',
310 324 'rebase.rebased': 'blue',
311 325 'rebase.remaining': 'red bold',
312 326 'resolve.resolved': 'green bold',
313 327 'resolve.unresolved': 'red bold',
314 328 'shelve.age': 'cyan',
315 329 'shelve.newest': 'green bold',
316 330 'shelve.name': 'blue bold',
317 331 'status.added': 'green bold',
318 332 'status.clean': 'none',
319 333 'status.copied': 'none',
320 334 'status.deleted': 'cyan bold underline',
321 335 'status.ignored': 'black bold',
322 336 'status.modified': 'blue bold',
323 337 'status.removed': 'red bold',
324 338 'status.unknown': 'magenta bold underline',
325 339 'tags.normal': 'green',
326 340 'tags.local': 'black bold'}
327 341
328 342
329 343 def _effect_str(effect):
330 344 '''Helper function for render_effects().'''
331 345
332 346 bg = False
333 347 if effect.endswith('_background'):
334 348 bg = True
335 349 effect = effect[:-11]
336 350 attr, val = _terminfo_params[effect]
337 351 if attr:
338 352 return curses.tigetstr(val)
339 353 elif bg:
340 354 return curses.tparm(curses.tigetstr('setab'), val)
341 355 else:
342 356 return curses.tparm(curses.tigetstr('setaf'), val)
343 357
344 358 def render_effects(text, effects):
345 359 'Wrap text in commands to turn on each effect.'
346 360 if not text:
347 361 return text
348 362 if not _terminfo_params:
349 363 start = [str(_effects[e]) for e in ['none'] + effects.split()]
350 364 start = '\033[' + ';'.join(start) + 'm'
351 365 stop = '\033[' + str(_effects['none']) + 'm'
352 366 else:
353 367 start = ''.join(_effect_str(effect)
354 368 for effect in ['none'] + effects.split())
355 369 stop = _effect_str('none')
356 370 return ''.join([start, text, stop])
357 371
358 372 def extstyles():
359 373 for name, ext in extensions.extensions():
360 374 _styles.update(getattr(ext, 'colortable', {}))
361 375
362 376 def valideffect(effect):
363 377 'Determine if the effect is valid or not.'
364 378 good = False
365 379 if not _terminfo_params and effect in _effects:
366 380 good = True
367 381 elif effect in _terminfo_params or effect[:-11] in _terminfo_params:
368 382 good = True
369 383 return good
370 384
371 385 def configstyles(ui):
372 386 for status, cfgeffects in ui.configitems('color'):
373 387 if '.' not in status or status.startswith('color.'):
374 388 continue
375 389 cfgeffects = ui.configlist('color', status)
376 390 if cfgeffects:
377 391 good = []
378 392 for e in cfgeffects:
379 393 if valideffect(e):
380 394 good.append(e)
381 395 else:
382 396 ui.warn(_("ignoring unknown color/effect %r "
383 397 "(configured in color.%s)\n")
384 398 % (e, status))
385 399 _styles[status] = ' '.join(good)
386 400
387 401 class colorui(uimod.ui):
388 402 def popbuffer(self, labeled=False):
389 403 if self._colormode is None:
390 404 return super(colorui, self).popbuffer(labeled)
391 405
392 406 self._bufferstates.pop()
393 407 if labeled:
394 408 return ''.join(self.label(a, label) for a, label
395 409 in self._buffers.pop())
396 410 return ''.join(a for a, label in self._buffers.pop())
397 411
398 412 _colormode = 'ansi'
399 413 def write(self, *args, **opts):
400 414 if self._colormode is None:
401 415 return super(colorui, self).write(*args, **opts)
402 416
403 417 label = opts.get('label', '')
404 418 if self._buffers:
405 419 self._buffers[-1].extend([(str(a), label) for a in args])
406 420 elif self._colormode == 'win32':
407 421 for a in args:
408 422 win32print(a, super(colorui, self).write, **opts)
409 423 else:
410 424 return super(colorui, self).write(
411 425 *[self.label(str(a), label) for a in args], **opts)
412 426
413 427 def write_err(self, *args, **opts):
414 428 if self._colormode is None:
415 429 return super(colorui, self).write_err(*args, **opts)
416 430
417 431 label = opts.get('label', '')
418 432 if self._bufferstates and self._bufferstates[-1]:
419 433 return self.write(*args, **opts)
420 434 if self._colormode == 'win32':
421 435 for a in args:
422 436 win32print(a, super(colorui, self).write_err, **opts)
423 437 else:
424 438 return super(colorui, self).write_err(
425 439 *[self.label(str(a), label) for a in args], **opts)
426 440
427 441 def showlabel(self, msg, label):
428 442 if label and msg:
429 443 if msg[-1] == '\n':
430 444 return "[%s|%s]\n" % (label, msg[:-1])
431 445 else:
432 446 return "[%s|%s]" % (label, msg)
433 447 else:
434 448 return msg
435 449
436 450 def label(self, msg, label):
437 451 if self._colormode is None:
438 452 return super(colorui, self).label(msg, label)
439 453
440 454 if self._colormode == 'debug':
441 455 return self.showlabel(msg, label)
442 456
443 457 effects = []
444 458 for l in label.split():
445 459 s = _styles.get(l, '')
446 460 if s:
447 461 effects.append(s)
448 462 elif valideffect(l):
449 463 effects.append(l)
450 464 effects = ' '.join(effects)
451 465 if effects:
452 466 return '\n'.join([render_effects(s, effects)
453 467 for s in msg.split('\n')])
454 468 return msg
455 469
456 470 def templatelabel(context, mapping, args):
457 471 if len(args) != 2:
458 472 # i18n: "label" is a keyword
459 473 raise error.ParseError(_("label expects two arguments"))
460 474
461 475 # add known effects to the mapping so symbols like 'red', 'bold',
462 476 # etc. don't need to be quoted
463 477 mapping.update(dict([(k, k) for k in _effects]))
464 478
465 479 thing = templater._evalifliteral(args[1], context, mapping)
466 480
467 481 # apparently, repo could be a string that is the favicon?
468 482 repo = mapping.get('repo', '')
469 483 if isinstance(repo, str):
470 484 return thing
471 485
472 486 label = templater._evalifliteral(args[0], context, mapping)
473 487
474 488 thing = templater.stringify(thing)
475 489 label = templater.stringify(label)
476 490
477 491 return repo.ui.label(thing, label)
478 492
479 493 def uisetup(ui):
480 494 if ui.plain():
481 495 return
482 496 if not isinstance(ui, colorui):
483 497 colorui.__bases__ = (ui.__class__,)
484 498 ui.__class__ = colorui
485 499 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
486 500 mode = _modesetup(ui_, opts['color'])
487 501 colorui._colormode = mode
488 502 if mode and mode != 'debug':
489 503 extstyles()
490 504 configstyles(ui_)
491 505 return orig(ui_, opts, cmd, cmdfunc)
492 506 def colorgit(orig, gitsub, commands, env=None, stream=False, cwd=None):
493 507 if gitsub.ui._colormode and len(commands) and commands[0] == "diff":
494 508 # insert the argument in the front,
495 509 # the end of git diff arguments is used for paths
496 510 commands.insert(1, '--color')
497 511 return orig(gitsub, commands, env, stream, cwd)
498 512 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
499 513 extensions.wrapfunction(subrepo.gitsubrepo, '_gitnodir', colorgit)
500 514 templater.funcs['label'] = templatelabel
501 515
502 516 def extsetup(ui):
503 517 commands.globalopts.append(
504 518 ('', 'color', 'auto',
505 519 # i18n: 'always', 'auto', 'never', and 'debug' are keywords
506 520 # and should not be translated
507 521 _("when to colorize (boolean, always, auto, never, or debug)"),
508 522 _('TYPE')))
509 523
510 524 @command('debugcolor', [], 'hg debugcolor')
511 525 def debugcolor(ui, repo, **opts):
512 526 global _styles
513 527 _styles = {}
514 528 for effect in _effects.keys():
515 529 _styles[effect] = effect
516 530 ui.write(('color mode: %s\n') % ui._colormode)
517 531 ui.write(_('available colors:\n'))
518 532 for label, colors in _styles.items():
519 533 ui.write(('%s\n') % colors, label=label)
520 534
521 535 if os.name != 'nt':
522 536 w32effects = None
523 537 else:
524 538 import re, ctypes
525 539
526 540 _kernel32 = ctypes.windll.kernel32
527 541
528 542 _WORD = ctypes.c_ushort
529 543
530 544 _INVALID_HANDLE_VALUE = -1
531 545
532 546 class _COORD(ctypes.Structure):
533 547 _fields_ = [('X', ctypes.c_short),
534 548 ('Y', ctypes.c_short)]
535 549
536 550 class _SMALL_RECT(ctypes.Structure):
537 551 _fields_ = [('Left', ctypes.c_short),
538 552 ('Top', ctypes.c_short),
539 553 ('Right', ctypes.c_short),
540 554 ('Bottom', ctypes.c_short)]
541 555
542 556 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
543 557 _fields_ = [('dwSize', _COORD),
544 558 ('dwCursorPosition', _COORD),
545 559 ('wAttributes', _WORD),
546 560 ('srWindow', _SMALL_RECT),
547 561 ('dwMaximumWindowSize', _COORD)]
548 562
549 563 _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
550 564 _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
551 565
552 566 _FOREGROUND_BLUE = 0x0001
553 567 _FOREGROUND_GREEN = 0x0002
554 568 _FOREGROUND_RED = 0x0004
555 569 _FOREGROUND_INTENSITY = 0x0008
556 570
557 571 _BACKGROUND_BLUE = 0x0010
558 572 _BACKGROUND_GREEN = 0x0020
559 573 _BACKGROUND_RED = 0x0040
560 574 _BACKGROUND_INTENSITY = 0x0080
561 575
562 576 _COMMON_LVB_REVERSE_VIDEO = 0x4000
563 577 _COMMON_LVB_UNDERSCORE = 0x8000
564 578
565 579 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
566 580 w32effects = {
567 581 'none': -1,
568 582 'black': 0,
569 583 'red': _FOREGROUND_RED,
570 584 'green': _FOREGROUND_GREEN,
571 585 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
572 586 'blue': _FOREGROUND_BLUE,
573 587 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
574 588 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
575 589 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
576 590 'bold': _FOREGROUND_INTENSITY,
577 591 'black_background': 0x100, # unused value > 0x0f
578 592 'red_background': _BACKGROUND_RED,
579 593 'green_background': _BACKGROUND_GREEN,
580 594 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
581 595 'blue_background': _BACKGROUND_BLUE,
582 596 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
583 597 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
584 598 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
585 599 _BACKGROUND_BLUE),
586 600 'bold_background': _BACKGROUND_INTENSITY,
587 601 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
588 602 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
589 603 }
590 604
591 605 passthrough = set([_FOREGROUND_INTENSITY,
592 606 _BACKGROUND_INTENSITY,
593 607 _COMMON_LVB_UNDERSCORE,
594 608 _COMMON_LVB_REVERSE_VIDEO])
595 609
596 610 stdout = _kernel32.GetStdHandle(
597 611 _STD_OUTPUT_HANDLE) # don't close the handle returned
598 612 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
599 613 w32effects = None
600 614 else:
601 615 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
602 616 if not _kernel32.GetConsoleScreenBufferInfo(
603 617 stdout, ctypes.byref(csbi)):
604 618 # stdout may not support GetConsoleScreenBufferInfo()
605 619 # when called from subprocess or redirected
606 620 w32effects = None
607 621 else:
608 622 origattr = csbi.wAttributes
609 623 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
610 624 re.MULTILINE | re.DOTALL)
611 625
612 626 def win32print(text, orig, **opts):
613 627 label = opts.get('label', '')
614 628 attr = origattr
615 629
616 630 def mapcolor(val, attr):
617 631 if val == -1:
618 632 return origattr
619 633 elif val in passthrough:
620 634 return attr | val
621 635 elif val > 0x0f:
622 636 return (val & 0x70) | (attr & 0x8f)
623 637 else:
624 638 return (val & 0x07) | (attr & 0xf8)
625 639
626 640 # determine console attributes based on labels
627 641 for l in label.split():
628 642 style = _styles.get(l, '')
629 643 for effect in style.split():
630 644 try:
631 645 attr = mapcolor(w32effects[effect], attr)
632 646 except KeyError:
633 647 # w32effects could not have certain attributes so we skip
634 648 # them if not found
635 649 pass
636 650 # hack to ensure regexp finds data
637 651 if not text.startswith('\033['):
638 652 text = '\033[m' + text
639 653
640 654 # Look for ANSI-like codes embedded in text
641 655 m = re.match(ansire, text)
642 656
643 657 try:
644 658 while m:
645 659 for sattr in m.group(1).split(';'):
646 660 if sattr:
647 661 attr = mapcolor(int(sattr), attr)
648 662 _kernel32.SetConsoleTextAttribute(stdout, attr)
649 663 orig(m.group(2), **opts)
650 664 m = re.match(ansire, m.group(3))
651 665 finally:
652 666 # Explicitly reset original attributes
653 667 _kernel32.SetConsoleTextAttribute(stdout, origattr)
General Comments 0
You need to be logged in to leave comments. Login now