##// END OF EJS Templates
color: sync text attributes and buffered text output on Windows (issue5508)...
Matt Harbison -
r31499:31d2ddfd default
parent child Browse files
Show More
@@ -1,471 +1,473
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 _baseterminfoparams = {
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 _baseterminfoparams = {}
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 _defaultstyles = {
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 _defaultstyles.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 ui._terminfoparams.update(_baseterminfoparams)
144 144
145 145 for key, val in ui.configitems('color'):
146 146 if key.startswith('color.'):
147 147 newval = (False, int(val), '')
148 148 ui._terminfoparams[key[6:]] = newval
149 149 elif key.startswith('terminfo.'):
150 150 newval = (True, '', val.replace('\\E', '\x1b'))
151 151 ui._terminfoparams[key[9:]] = newval
152 152 try:
153 153 curses.setupterm()
154 154 except curses.error as e:
155 155 ui._terminfoparams.clear()
156 156 return
157 157
158 158 for key, (b, e, c) in ui._terminfoparams.items():
159 159 if not b:
160 160 continue
161 161 if not c and not curses.tigetstr(e):
162 162 # Most terminals don't support dim, invis, etc, so don't be
163 163 # noisy and use ui.debug().
164 164 ui.debug("no terminfo entry for %s\n" % e)
165 165 del ui._terminfoparams[key]
166 166 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
167 167 # Only warn about missing terminfo entries if we explicitly asked for
168 168 # terminfo mode.
169 169 if mode == "terminfo":
170 170 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
171 171 "ECMA-48 color\n"))
172 172 ui._terminfoparams.clear()
173 173
174 174 def setup(ui):
175 175 """configure color on a ui
176 176
177 177 That function both set the colormode for the ui object and read
178 178 the configuration looking for custom colors and effect definitions."""
179 179 mode = _modesetup(ui)
180 180 ui._colormode = mode
181 181 if mode and mode != 'debug':
182 182 configstyles(ui)
183 183
184 184 def _modesetup(ui):
185 185 if ui.plain():
186 186 return None
187 187 default = 'never'
188 188 if _enabledbydefault:
189 189 default = 'auto'
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 ui._terminfoparams.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 ui._terminfoparams.clear()
242 242 elif realmode == 'terminfo':
243 243 _terminfosetup(ui, mode)
244 244 if not ui._terminfoparams:
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 ui._styles.update(_defaultstyles)
257 257 for status, cfgeffects in ui.configitems('color'):
258 258 if '.' not in status or status.startswith(('color.', 'terminfo.')):
259 259 continue
260 260 cfgeffects = ui.configlist('color', status)
261 261 if cfgeffects:
262 262 good = []
263 263 for e in cfgeffects:
264 264 if valideffect(ui, e):
265 265 good.append(e)
266 266 else:
267 267 ui.warn(_("ignoring unknown color/effect %r "
268 268 "(configured in color.%s)\n")
269 269 % (e, status))
270 270 ui._styles[status] = ' '.join(good)
271 271
272 272 def valideffect(ui, effect):
273 273 'Determine if the effect is valid or not.'
274 274 return ((not ui._terminfoparams and effect in _effects)
275 275 or (effect in ui._terminfoparams
276 276 or effect[:-11] in ui._terminfoparams))
277 277
278 278 def _effect_str(ui, effect):
279 279 '''Helper function for render_effects().'''
280 280
281 281 bg = False
282 282 if effect.endswith('_background'):
283 283 bg = True
284 284 effect = effect[:-11]
285 285 try:
286 286 attr, val, termcode = ui._terminfoparams[effect]
287 287 except KeyError:
288 288 return ''
289 289 if attr:
290 290 if termcode:
291 291 return termcode
292 292 else:
293 293 return curses.tigetstr(val)
294 294 elif bg:
295 295 return curses.tparm(curses.tigetstr('setab'), val)
296 296 else:
297 297 return curses.tparm(curses.tigetstr('setaf'), val)
298 298
299 299 def _render_effects(ui, text, effects):
300 300 'Wrap text in commands to turn on each effect.'
301 301 if not text:
302 302 return text
303 303 if ui._terminfoparams:
304 304 start = ''.join(_effect_str(ui, effect)
305 305 for effect in ['none'] + effects.split())
306 306 stop = _effect_str(ui, 'none')
307 307 else:
308 308 start = [str(_effects[e]) for e in ['none'] + effects.split()]
309 309 start = '\033[' + ';'.join(start) + 'm'
310 310 stop = '\033[' + str(_effects['none']) + 'm'
311 311 return ''.join([start, text, stop])
312 312
313 313 def colorlabel(ui, msg, label):
314 314 """add color control code according to the mode"""
315 315 if ui._colormode == 'debug':
316 316 if label and msg:
317 317 if msg[-1] == '\n':
318 318 msg = "[%s|%s]\n" % (label, msg[:-1])
319 319 else:
320 320 msg = "[%s|%s]" % (label, msg)
321 321 elif ui._colormode is not None:
322 322 effects = []
323 323 for l in label.split():
324 324 s = ui._styles.get(l, '')
325 325 if s:
326 326 effects.append(s)
327 327 elif valideffect(ui, l):
328 328 effects.append(l)
329 329 effects = ' '.join(effects)
330 330 if effects:
331 331 msg = '\n'.join([_render_effects(ui, line, effects)
332 332 for line in msg.split('\n')])
333 333 return msg
334 334
335 335 w32effects = None
336 336 if pycompat.osname == 'nt':
337 337 import ctypes
338 338 import re
339 339
340 340 _kernel32 = ctypes.windll.kernel32
341 341
342 342 _WORD = ctypes.c_ushort
343 343
344 344 _INVALID_HANDLE_VALUE = -1
345 345
346 346 class _COORD(ctypes.Structure):
347 347 _fields_ = [('X', ctypes.c_short),
348 348 ('Y', ctypes.c_short)]
349 349
350 350 class _SMALL_RECT(ctypes.Structure):
351 351 _fields_ = [('Left', ctypes.c_short),
352 352 ('Top', ctypes.c_short),
353 353 ('Right', ctypes.c_short),
354 354 ('Bottom', ctypes.c_short)]
355 355
356 356 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
357 357 _fields_ = [('dwSize', _COORD),
358 358 ('dwCursorPosition', _COORD),
359 359 ('wAttributes', _WORD),
360 360 ('srWindow', _SMALL_RECT),
361 361 ('dwMaximumWindowSize', _COORD)]
362 362
363 363 _STD_OUTPUT_HANDLE = 0xfffffff5 # (DWORD)-11
364 364 _STD_ERROR_HANDLE = 0xfffffff4 # (DWORD)-12
365 365
366 366 _FOREGROUND_BLUE = 0x0001
367 367 _FOREGROUND_GREEN = 0x0002
368 368 _FOREGROUND_RED = 0x0004
369 369 _FOREGROUND_INTENSITY = 0x0008
370 370
371 371 _BACKGROUND_BLUE = 0x0010
372 372 _BACKGROUND_GREEN = 0x0020
373 373 _BACKGROUND_RED = 0x0040
374 374 _BACKGROUND_INTENSITY = 0x0080
375 375
376 376 _COMMON_LVB_REVERSE_VIDEO = 0x4000
377 377 _COMMON_LVB_UNDERSCORE = 0x8000
378 378
379 379 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
380 380 w32effects = {
381 381 'none': -1,
382 382 'black': 0,
383 383 'red': _FOREGROUND_RED,
384 384 'green': _FOREGROUND_GREEN,
385 385 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
386 386 'blue': _FOREGROUND_BLUE,
387 387 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
388 388 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
389 389 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
390 390 'bold': _FOREGROUND_INTENSITY,
391 391 'black_background': 0x100, # unused value > 0x0f
392 392 'red_background': _BACKGROUND_RED,
393 393 'green_background': _BACKGROUND_GREEN,
394 394 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
395 395 'blue_background': _BACKGROUND_BLUE,
396 396 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
397 397 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
398 398 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
399 399 _BACKGROUND_BLUE),
400 400 'bold_background': _BACKGROUND_INTENSITY,
401 401 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
402 402 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
403 403 }
404 404
405 405 passthrough = set([_FOREGROUND_INTENSITY,
406 406 _BACKGROUND_INTENSITY,
407 407 _COMMON_LVB_UNDERSCORE,
408 408 _COMMON_LVB_REVERSE_VIDEO])
409 409
410 410 stdout = _kernel32.GetStdHandle(
411 411 _STD_OUTPUT_HANDLE) # don't close the handle returned
412 412 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
413 413 w32effects = None
414 414 else:
415 415 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
416 416 if not _kernel32.GetConsoleScreenBufferInfo(
417 417 stdout, ctypes.byref(csbi)):
418 418 # stdout may not support GetConsoleScreenBufferInfo()
419 419 # when called from subprocess or redirected
420 420 w32effects = None
421 421 else:
422 422 origattr = csbi.wAttributes
423 423 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
424 424 re.MULTILINE | re.DOTALL)
425 425
426 426 def win32print(ui, writefunc, *msgs, **opts):
427 427 for text in msgs:
428 428 _win32print(ui, text, writefunc, **opts)
429 429
430 430 def _win32print(ui, text, writefunc, **opts):
431 431 label = opts.get('label', '')
432 432 attr = origattr
433 433
434 434 def mapcolor(val, attr):
435 435 if val == -1:
436 436 return origattr
437 437 elif val in passthrough:
438 438 return attr | val
439 439 elif val > 0x0f:
440 440 return (val & 0x70) | (attr & 0x8f)
441 441 else:
442 442 return (val & 0x07) | (attr & 0xf8)
443 443
444 444 # determine console attributes based on labels
445 445 for l in label.split():
446 446 style = ui._styles.get(l, '')
447 447 for effect in style.split():
448 448 try:
449 449 attr = mapcolor(w32effects[effect], attr)
450 450 except KeyError:
451 451 # w32effects could not have certain attributes so we skip
452 452 # them if not found
453 453 pass
454 454 # hack to ensure regexp finds data
455 455 if not text.startswith('\033['):
456 456 text = '\033[m' + text
457 457
458 458 # Look for ANSI-like codes embedded in text
459 459 m = re.match(ansire, text)
460 460
461 461 try:
462 462 while m:
463 463 for sattr in m.group(1).split(';'):
464 464 if sattr:
465 465 attr = mapcolor(int(sattr), attr)
466 ui.flush()
466 467 _kernel32.SetConsoleTextAttribute(stdout, attr)
467 468 writefunc(m.group(2), **opts)
468 469 m = re.match(ansire, m.group(3))
469 470 finally:
470 471 # Explicitly reset original attributes
472 ui.flush()
471 473 _kernel32.SetConsoleTextAttribute(stdout, origattr)
General Comments 0
You need to be logged in to leave comments. Login now