##// END OF EJS Templates
color: add support for Windows consoles...
Steve Borho -
r10870:a4944b43 default
parent child Browse files
Show More
@@ -1,182 +1,276 b''
1 # color.py color output for the status and qseries commands
1 # color.py color output for the status and qseries commands
2 #
2 #
3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
4 #
4 #
5 # This program is free software; you can redistribute it and/or modify it
5 # This program is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation; either version 2 of the License, or (at your
7 # Free Software Foundation; either version 2 of the License, or (at your
8 # option) any later version.
8 # option) any later version.
9 #
9 #
10 # This program is distributed in the hope that it will be useful, but
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
13 # Public License for more details.
13 # Public License for more details.
14 #
14 #
15 # You should have received a copy of the GNU General Public License along
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
18
19 '''colorize output from some commands
19 '''colorize output from some commands
20
20
21 This extension modifies the status and resolve commands to add color to their
21 This extension modifies the status and resolve commands to add color to their
22 output to reflect file status, the qseries command to add color to reflect
22 output to reflect file status, the qseries command to add color to reflect
23 patch status (applied, unapplied, missing), and to diff-related
23 patch status (applied, unapplied, missing), and to diff-related
24 commands to highlight additions, removals, diff headers, and trailing
24 commands to highlight additions, removals, diff headers, and trailing
25 whitespace.
25 whitespace.
26
26
27 Other effects in addition to color, like bold and underlined text, are
27 Other effects in addition to color, like bold and underlined text, are
28 also available. Effects are rendered with the ECMA-48 SGR control
28 also available. Effects are rendered with the ECMA-48 SGR control
29 function (aka ANSI escape codes). This module also provides the
29 function (aka ANSI escape codes). This module also provides the
30 render_text function, which can be used to add effects to any text.
30 render_text function, which can be used to add effects to any text.
31
31
32 Default effects may be overridden from the .hgrc file::
32 Default effects may be overridden from the .hgrc file::
33
33
34 [color]
34 [color]
35 status.modified = blue bold underline red_background
35 status.modified = blue bold underline red_background
36 status.added = green bold
36 status.added = green bold
37 status.removed = red bold blue_background
37 status.removed = red bold blue_background
38 status.deleted = cyan bold underline
38 status.deleted = cyan bold underline
39 status.unknown = magenta bold underline
39 status.unknown = magenta bold underline
40 status.ignored = black bold
40 status.ignored = black bold
41
41
42 # 'none' turns off all effects
42 # 'none' turns off all effects
43 status.clean = none
43 status.clean = none
44 status.copied = none
44 status.copied = none
45
45
46 qseries.applied = blue bold underline
46 qseries.applied = blue bold underline
47 qseries.unapplied = black bold
47 qseries.unapplied = black bold
48 qseries.missing = red bold
48 qseries.missing = red bold
49
49
50 diff.diffline = bold
50 diff.diffline = bold
51 diff.extended = cyan bold
51 diff.extended = cyan bold
52 diff.file_a = red bold
52 diff.file_a = red bold
53 diff.file_b = green bold
53 diff.file_b = green bold
54 diff.hunk = magenta
54 diff.hunk = magenta
55 diff.deleted = red
55 diff.deleted = red
56 diff.inserted = green
56 diff.inserted = green
57 diff.changed = white
57 diff.changed = white
58 diff.trailingwhitespace = bold red_background
58 diff.trailingwhitespace = bold red_background
59
59
60 resolve.unresolved = red bold
60 resolve.unresolved = red bold
61 resolve.resolved = green bold
61 resolve.resolved = green bold
62
62
63 bookmarks.current = green
63 bookmarks.current = green
64
65 The color extension will try to detect whether to use ANSI codes or
66 Win32 console APIs, unless it is made explicit::
67
68 [color]
69 mode = ansi
70
71 Any value other than 'ansi', 'win32', or 'auto' will disable color.
72
64 '''
73 '''
65
74
66 import os, sys
75 import os, sys
67
76
68 from mercurial import commands, dispatch, extensions
77 from mercurial import commands, dispatch, extensions
69 from mercurial.i18n import _
78 from mercurial.i18n import _
70
79
71 # start and stop parameters for effects
80 # start and stop parameters for effects
72 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
81 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
73 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
82 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
74 'italic': 3, 'underline': 4, 'inverse': 7,
83 'italic': 3, 'underline': 4, 'inverse': 7,
75 'black_background': 40, 'red_background': 41,
84 'black_background': 40, 'red_background': 41,
76 'green_background': 42, 'yellow_background': 43,
85 'green_background': 42, 'yellow_background': 43,
77 'blue_background': 44, 'purple_background': 45,
86 'blue_background': 44, 'purple_background': 45,
78 'cyan_background': 46, 'white_background': 47}
87 'cyan_background': 46, 'white_background': 47}
79
88
80 _styles = {'grep.match': 'red bold',
89 _styles = {'grep.match': 'red bold',
81 'diff.changed': 'white',
90 'diff.changed': 'white',
82 'diff.deleted': 'red',
91 'diff.deleted': 'red',
83 'diff.diffline': 'bold',
92 'diff.diffline': 'bold',
84 'diff.extended': 'cyan bold',
93 'diff.extended': 'cyan bold',
85 'diff.file_a': 'red bold',
94 'diff.file_a': 'red bold',
86 'diff.file_b': 'green bold',
95 'diff.file_b': 'green bold',
87 'diff.hunk': 'magenta',
96 'diff.hunk': 'magenta',
88 'diff.inserted': 'green',
97 'diff.inserted': 'green',
89 'diff.trailingwhitespace': 'bold red_background',
98 'diff.trailingwhitespace': 'bold red_background',
90 'diffstat.deleted': 'red',
99 'diffstat.deleted': 'red',
91 'diffstat.inserted': 'green',
100 'diffstat.inserted': 'green',
92 'log.changeset': 'yellow',
101 'log.changeset': 'yellow',
93 'resolve.resolved': 'green bold',
102 'resolve.resolved': 'green bold',
94 'resolve.unresolved': 'red bold',
103 'resolve.unresolved': 'red bold',
95 'status.added': 'green bold',
104 'status.added': 'green bold',
96 'status.clean': 'none',
105 'status.clean': 'none',
97 'status.copied': 'none',
106 'status.copied': 'none',
98 'status.deleted': 'cyan bold underline',
107 'status.deleted': 'cyan bold underline',
99 'status.ignored': 'black bold',
108 'status.ignored': 'black bold',
100 'status.modified': 'blue bold',
109 'status.modified': 'blue bold',
101 'status.removed': 'red bold',
110 'status.removed': 'red bold',
102 'status.unknown': 'magenta bold underline'}
111 'status.unknown': 'magenta bold underline'}
103
112
104
113
105 def render_effects(text, effects):
114 def render_effects(text, effects):
106 'Wrap text in commands to turn on each effect.'
115 'Wrap text in commands to turn on each effect.'
107 if not text:
116 if not text:
108 return text
117 return text
109 start = [str(_effects[e]) for e in ['none'] + effects.split()]
118 start = [str(_effects[e]) for e in ['none'] + effects.split()]
110 start = '\033[' + ';'.join(start) + 'm'
119 start = '\033[' + ';'.join(start) + 'm'
111 stop = '\033[' + str(_effects['none']) + 'm'
120 stop = '\033[' + str(_effects['none']) + 'm'
112 return ''.join([start, text, stop])
121 return ''.join([start, text, stop])
113
122
114 def extstyles():
123 def extstyles():
115 for name, ext in extensions.extensions():
124 for name, ext in extensions.extensions():
116 _styles.update(getattr(ext, 'colortable', {}))
125 _styles.update(getattr(ext, 'colortable', {}))
117
126
118 def configstyles(ui):
127 def configstyles(ui):
119 for status, cfgeffects in ui.configitems('color'):
128 for status, cfgeffects in ui.configitems('color'):
120 if '.' not in status:
129 if '.' not in status:
121 continue
130 continue
122 cfgeffects = ui.configlist('color', status)
131 cfgeffects = ui.configlist('color', status)
123 if cfgeffects:
132 if cfgeffects:
124 good = []
133 good = []
125 for e in cfgeffects:
134 for e in cfgeffects:
126 if e in _effects:
135 if e in _effects:
127 good.append(e)
136 good.append(e)
128 else:
137 else:
129 ui.warn(_("ignoring unknown color/effect %r "
138 ui.warn(_("ignoring unknown color/effect %r "
130 "(configured in color.%s)\n")
139 "(configured in color.%s)\n")
131 % (e, status))
140 % (e, status))
132 _styles[status] = ' '.join(good)
141 _styles[status] = ' '.join(good)
133
142
134 _buffers = None
143 _buffers = None
135 def style(msg, label):
144 def style(msg, label):
136 effects = []
145 effects = []
137 for l in label.split():
146 for l in label.split():
138 s = _styles.get(l, '')
147 s = _styles.get(l, '')
139 if s:
148 if s:
140 effects.append(s)
149 effects.append(s)
141 effects = ''.join(effects)
150 effects = ''.join(effects)
142 if effects:
151 if effects:
143 return '\n'.join([render_effects(s, effects)
152 return '\n'.join([render_effects(s, effects)
144 for s in msg.split('\n')])
153 for s in msg.split('\n')])
145 return msg
154 return msg
146
155
147 def popbuffer(orig, labeled=False):
156 def popbuffer(orig, labeled=False):
148 global _buffers
157 global _buffers
149 if labeled:
158 if labeled:
150 return ''.join(style(a, label) for a, label in _buffers.pop())
159 return ''.join(style(a, label) for a, label in _buffers.pop())
151 return ''.join(a for a, label in _buffers.pop())
160 return ''.join(a for a, label in _buffers.pop())
152
161
162 mode = 'ansi'
153 def write(orig, *args, **opts):
163 def write(orig, *args, **opts):
154 label = opts.get('label', '')
164 label = opts.get('label', '')
155 global _buffers
165 global _buffers
156 if _buffers:
166 if _buffers:
157 _buffers[-1].extend([(str(a), label) for a in args])
167 _buffers[-1].extend([(str(a), label) for a in args])
168 elif mode == 'win32':
169 for a in args:
170 win32print(a, orig, **opts)
158 else:
171 else:
159 return orig(*[style(str(a), label) for a in args], **opts)
172 return orig(*[style(str(a), label) for a in args], **opts)
160
173
161 def write_err(orig, *args, **opts):
174 def write_err(orig, *args, **opts):
162 label = opts.get('label', '')
175 label = opts.get('label', '')
163 return orig(*[style(str(a), label) for a in args], **opts)
176 if mode == 'win32':
177 for a in args:
178 win32print(a, orig, **opts)
179 else:
180 return orig(*[style(str(a), label) for a in args], **opts)
164
181
165 def uisetup(ui):
182 def uisetup(ui):
183 global mode
184 mode = ui.config('color', 'mode', 'auto')
185 if mode == 'auto':
186 if os.name == 'nt' and 'TERM' not in os.environ:
187 # looks line a cmd.exe console, use win32 API or nothing
188 mode = w32effects and 'win32' or 'none'
189 else:
190 mode = 'ansi'
191 if mode == 'win32':
192 if w32effects is None:
193 # only warn if color.mode is explicitly set to win32
194 ui.warn(_('win32console not found, please install pywin32\n'))
195 return
196 _effects.update(w32effects)
197 elif mode != 'ansi':
198 return
166 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
199 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
167 if (opts['color'] == 'always' or
200 if (opts['color'] == 'always' or
168 (opts['color'] == 'auto' and (os.environ.get('TERM') != 'dumb'
201 (opts['color'] == 'auto' and (os.environ.get('TERM') != 'dumb'
169 and sys.__stdout__.isatty()))):
202 and sys.__stdout__.isatty()))):
170 global _buffers
203 global _buffers
171 _buffers = ui_._buffers
204 _buffers = ui_._buffers
172 extensions.wrapfunction(ui_, 'popbuffer', popbuffer)
205 extensions.wrapfunction(ui_, 'popbuffer', popbuffer)
173 extensions.wrapfunction(ui_, 'write', write)
206 extensions.wrapfunction(ui_, 'write', write)
174 extensions.wrapfunction(ui_, 'write_err', write_err)
207 extensions.wrapfunction(ui_, 'write_err', write_err)
175 ui_.label = style
208 ui_.label = style
176 extstyles()
209 extstyles()
177 configstyles(ui)
210 configstyles(ui)
178 return orig(ui_, opts, cmd, cmdfunc)
211 return orig(ui_, opts, cmd, cmdfunc)
179 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
212 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
180
213
181 commands.globalopts.append(('', 'color', 'auto',
214 commands.globalopts.append(('', 'color', 'auto',
182 _("when to colorize (always, auto, or never)")))
215 _("when to colorize (always, auto, or never)")))
216
217 try:
218 import re
219 from win32console import *
220
221 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
222 w32effects = {
223 'none': 0,
224 'black': 0,
225 'red': FOREGROUND_RED,
226 'green': FOREGROUND_GREEN,
227 'yellow': FOREGROUND_RED | FOREGROUND_GREEN,
228 'blue': FOREGROUND_BLUE,
229 'magenta': FOREGROUND_BLUE | FOREGROUND_RED,
230 'cyan': FOREGROUND_BLUE | FOREGROUND_GREEN,
231 'white': FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE,
232 'bold': FOREGROUND_INTENSITY,
233 'black_background': 0,
234 'red_background': BACKGROUND_RED,
235 'green_background': BACKGROUND_GREEN,
236 'blue_background': BACKGROUND_BLUE,
237 'cyan_background': BACKGROUND_BLUE | BACKGROUND_GREEN,
238 'bold_background': FOREGROUND_INTENSITY,
239 'underline': COMMON_LVB_UNDERSCORE, # double-byte charsets only
240 'inverse': COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
241 }
242
243 stdout = GetStdHandle(STD_OUTPUT_HANDLE)
244 origattr = stdout.GetConsoleScreenBufferInfo()['Attributes']
245 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)', re.MULTILINE | re.DOTALL)
246
247 def win32print(text, orig, **opts):
248 label = opts.get('label', '')
249 attr = 0
250
251 # determine console attributes based on labels
252 for l in label.split():
253 style = _styles.get(l, '')
254 for effect in style.split():
255 attr |= w32effects[effect]
256
257 # hack to ensure regexp finds data
258 if not text.startswith('\033['):
259 text = '\033[m' + text
260
261 # Look for ANSI-like codes embedded in text
262 m = re.match(ansire, text)
263 while m:
264 for sattr in m.group(1).split(';'):
265 if sattr:
266 val = int(sattr)
267 attr = val and attr|val or 0
268 stdout.SetConsoleTextAttribute(attr or origattr)
269 orig(m.group(2), **opts)
270 m = re.match(ansire, m.group(3))
271
272 # Explicity reset original attributes
273 stdout.SetConsoleTextAttribute(origattr)
274
275 except ImportError:
276 w32effects = None
General Comments 0
You need to be logged in to leave comments. Login now