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