##// END OF EJS Templates
color: bring back colors with pager...
Yuya Nishihara -
r11207:1d714c15 default
parent child Browse files
Show More
@@ -1,287 +1,291
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
64
65 The color extension will try to detect whether to use ANSI codes or
65 The color extension will try to detect whether to use ANSI codes or
66 Win32 console APIs, unless it is made explicit::
66 Win32 console APIs, unless it is made explicit::
67
67
68 [color]
68 [color]
69 mode = ansi
69 mode = ansi
70
70
71 Any value other than 'ansi', 'win32', or 'auto' will disable color.
71 Any value other than 'ansi', 'win32', or 'auto' will disable color.
72
72
73 '''
73 '''
74
74
75 import os, sys
75 import os, sys
76
76
77 from mercurial import commands, dispatch, extensions
77 from mercurial import commands, dispatch, extensions
78 from mercurial.i18n import _
78 from mercurial.i18n import _
79
79
80 # start and stop parameters for effects
80 # start and stop parameters for effects
81 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
81 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
82 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
82 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
83 'italic': 3, 'underline': 4, 'inverse': 7,
83 'italic': 3, 'underline': 4, 'inverse': 7,
84 'black_background': 40, 'red_background': 41,
84 'black_background': 40, 'red_background': 41,
85 'green_background': 42, 'yellow_background': 43,
85 'green_background': 42, 'yellow_background': 43,
86 'blue_background': 44, 'purple_background': 45,
86 'blue_background': 44, 'purple_background': 45,
87 'cyan_background': 46, 'white_background': 47}
87 'cyan_background': 46, 'white_background': 47}
88
88
89 _styles = {'grep.match': 'red bold',
89 _styles = {'grep.match': 'red bold',
90 'diff.changed': 'white',
90 'diff.changed': 'white',
91 'diff.deleted': 'red',
91 'diff.deleted': 'red',
92 'diff.diffline': 'bold',
92 'diff.diffline': 'bold',
93 'diff.extended': 'cyan bold',
93 'diff.extended': 'cyan bold',
94 'diff.file_a': 'red bold',
94 'diff.file_a': 'red bold',
95 'diff.file_b': 'green bold',
95 'diff.file_b': 'green bold',
96 'diff.hunk': 'magenta',
96 'diff.hunk': 'magenta',
97 'diff.inserted': 'green',
97 'diff.inserted': 'green',
98 'diff.trailingwhitespace': 'bold red_background',
98 'diff.trailingwhitespace': 'bold red_background',
99 'diffstat.deleted': 'red',
99 'diffstat.deleted': 'red',
100 'diffstat.inserted': 'green',
100 'diffstat.inserted': 'green',
101 'log.changeset': 'yellow',
101 'log.changeset': 'yellow',
102 'resolve.resolved': 'green bold',
102 'resolve.resolved': 'green bold',
103 'resolve.unresolved': 'red bold',
103 'resolve.unresolved': 'red bold',
104 'status.added': 'green bold',
104 'status.added': 'green bold',
105 'status.clean': 'none',
105 'status.clean': 'none',
106 'status.copied': 'none',
106 'status.copied': 'none',
107 'status.deleted': 'cyan bold underline',
107 'status.deleted': 'cyan bold underline',
108 'status.ignored': 'black bold',
108 'status.ignored': 'black bold',
109 'status.modified': 'blue bold',
109 'status.modified': 'blue bold',
110 'status.removed': 'red bold',
110 'status.removed': 'red bold',
111 'status.unknown': 'magenta bold underline'}
111 'status.unknown': 'magenta bold underline'}
112
112
113
113
114 def render_effects(text, effects):
114 def render_effects(text, effects):
115 'Wrap text in commands to turn on each effect.'
115 'Wrap text in commands to turn on each effect.'
116 if not text:
116 if not text:
117 return text
117 return text
118 start = [str(_effects[e]) for e in ['none'] + effects.split()]
118 start = [str(_effects[e]) for e in ['none'] + effects.split()]
119 start = '\033[' + ';'.join(start) + 'm'
119 start = '\033[' + ';'.join(start) + 'm'
120 stop = '\033[' + str(_effects['none']) + 'm'
120 stop = '\033[' + str(_effects['none']) + 'm'
121 return ''.join([start, text, stop])
121 return ''.join([start, text, stop])
122
122
123 def extstyles():
123 def extstyles():
124 for name, ext in extensions.extensions():
124 for name, ext in extensions.extensions():
125 _styles.update(getattr(ext, 'colortable', {}))
125 _styles.update(getattr(ext, 'colortable', {}))
126
126
127 def configstyles(ui):
127 def configstyles(ui):
128 for status, cfgeffects in ui.configitems('color'):
128 for status, cfgeffects in ui.configitems('color'):
129 if '.' not in status:
129 if '.' not in status:
130 continue
130 continue
131 cfgeffects = ui.configlist('color', status)
131 cfgeffects = ui.configlist('color', status)
132 if cfgeffects:
132 if cfgeffects:
133 good = []
133 good = []
134 for e in cfgeffects:
134 for e in cfgeffects:
135 if e in _effects:
135 if e in _effects:
136 good.append(e)
136 good.append(e)
137 else:
137 else:
138 ui.warn(_("ignoring unknown color/effect %r "
138 ui.warn(_("ignoring unknown color/effect %r "
139 "(configured in color.%s)\n")
139 "(configured in color.%s)\n")
140 % (e, status))
140 % (e, status))
141 _styles[status] = ' '.join(good)
141 _styles[status] = ' '.join(good)
142
142
143 _buffers = None
143 _buffers = None
144 def style(msg, label):
144 def style(msg, label):
145 effects = []
145 effects = []
146 for l in label.split():
146 for l in label.split():
147 s = _styles.get(l, '')
147 s = _styles.get(l, '')
148 if s:
148 if s:
149 effects.append(s)
149 effects.append(s)
150 effects = ''.join(effects)
150 effects = ''.join(effects)
151 if effects:
151 if effects:
152 return '\n'.join([render_effects(s, effects)
152 return '\n'.join([render_effects(s, effects)
153 for s in msg.split('\n')])
153 for s in msg.split('\n')])
154 return msg
154 return msg
155
155
156 def popbuffer(orig, labeled=False):
156 def popbuffer(orig, labeled=False):
157 global _buffers
157 global _buffers
158 if labeled:
158 if labeled:
159 return ''.join(style(a, label) for a, label in _buffers.pop())
159 return ''.join(style(a, label) for a, label in _buffers.pop())
160 return ''.join(a for a, label in _buffers.pop())
160 return ''.join(a for a, label in _buffers.pop())
161
161
162 mode = 'ansi'
162 mode = 'ansi'
163 def write(orig, *args, **opts):
163 def write(orig, *args, **opts):
164 label = opts.get('label', '')
164 label = opts.get('label', '')
165 global _buffers
165 global _buffers
166 if _buffers:
166 if _buffers:
167 _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':
168 elif mode == 'win32':
169 for a in args:
169 for a in args:
170 win32print(a, orig, **opts)
170 win32print(a, orig, **opts)
171 else:
171 else:
172 return orig(*[style(str(a), label) for a in args], **opts)
172 return orig(*[style(str(a), label) for a in args], **opts)
173
173
174 def write_err(orig, *args, **opts):
174 def write_err(orig, *args, **opts):
175 label = opts.get('label', '')
175 label = opts.get('label', '')
176 if mode == 'win32':
176 if mode == 'win32':
177 for a in args:
177 for a in args:
178 win32print(a, orig, **opts)
178 win32print(a, orig, **opts)
179 else:
179 else:
180 return orig(*[style(str(a), label) for a in args], **opts)
180 return orig(*[style(str(a), label) for a in args], **opts)
181
181
182 def uisetup(ui):
182 def uisetup(ui):
183 if ui.plain():
183 if ui.plain():
184 return
184 return
185 global mode
185 global mode
186 mode = ui.config('color', 'mode', 'auto')
186 mode = ui.config('color', 'mode', 'auto')
187 if mode == 'auto':
187 if mode == 'auto':
188 if os.name == 'nt' and 'TERM' not in os.environ:
188 if os.name == 'nt' and 'TERM' not in os.environ:
189 # looks line a cmd.exe console, use win32 API or nothing
189 # looks line a cmd.exe console, use win32 API or nothing
190 mode = w32effects and 'win32' or 'none'
190 mode = w32effects and 'win32' or 'none'
191 else:
191 else:
192 mode = 'ansi'
192 mode = 'ansi'
193 if mode == 'win32':
193 if mode == 'win32':
194 if w32effects is None:
194 if w32effects is None:
195 # only warn if color.mode is explicitly set to win32
195 # only warn if color.mode is explicitly set to win32
196 ui.warn(_('win32console not found, please install pywin32\n'))
196 ui.warn(_('win32console not found, please install pywin32\n'))
197 return
197 return
198 _effects.update(w32effects)
198 _effects.update(w32effects)
199 elif mode != 'ansi':
199 elif mode != 'ansi':
200 return
200 return
201
202 # check isatty() before anything else changes it (like pager)
203 isatty = sys.__stdout__.isatty()
204
201 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
205 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
202 if (opts['color'] == 'always' or
206 if (opts['color'] == 'always' or
203 (opts['color'] == 'auto' and (os.environ.get('TERM') != 'dumb'
207 (opts['color'] == 'auto' and (os.environ.get('TERM') != 'dumb'
204 and sys.__stdout__.isatty()))):
208 and isatty))):
205 global _buffers
209 global _buffers
206 _buffers = ui_._buffers
210 _buffers = ui_._buffers
207 extensions.wrapfunction(ui_, 'popbuffer', popbuffer)
211 extensions.wrapfunction(ui_, 'popbuffer', popbuffer)
208 extensions.wrapfunction(ui_, 'write', write)
212 extensions.wrapfunction(ui_, 'write', write)
209 extensions.wrapfunction(ui_, 'write_err', write_err)
213 extensions.wrapfunction(ui_, 'write_err', write_err)
210 ui_.label = style
214 ui_.label = style
211 extstyles()
215 extstyles()
212 configstyles(ui)
216 configstyles(ui)
213 return orig(ui_, opts, cmd, cmdfunc)
217 return orig(ui_, opts, cmd, cmdfunc)
214 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
218 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
215
219
216 commands.globalopts.append(('', 'color', 'auto',
220 commands.globalopts.append(('', 'color', 'auto',
217 _("when to colorize (always, auto, or never)")))
221 _("when to colorize (always, auto, or never)")))
218
222
219 try:
223 try:
220 import re, pywintypes
224 import re, pywintypes
221 from win32console import *
225 from win32console import *
222
226
223 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
227 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
224 w32effects = {
228 w32effects = {
225 'none': 0,
229 'none': 0,
226 'black': 0,
230 'black': 0,
227 'red': FOREGROUND_RED,
231 'red': FOREGROUND_RED,
228 'green': FOREGROUND_GREEN,
232 'green': FOREGROUND_GREEN,
229 'yellow': FOREGROUND_RED | FOREGROUND_GREEN,
233 'yellow': FOREGROUND_RED | FOREGROUND_GREEN,
230 'blue': FOREGROUND_BLUE,
234 'blue': FOREGROUND_BLUE,
231 'magenta': FOREGROUND_BLUE | FOREGROUND_RED,
235 'magenta': FOREGROUND_BLUE | FOREGROUND_RED,
232 'cyan': FOREGROUND_BLUE | FOREGROUND_GREEN,
236 'cyan': FOREGROUND_BLUE | FOREGROUND_GREEN,
233 'white': FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE,
237 'white': FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE,
234 'bold': FOREGROUND_INTENSITY,
238 'bold': FOREGROUND_INTENSITY,
235 'black_background': 0,
239 'black_background': 0,
236 'red_background': BACKGROUND_RED,
240 'red_background': BACKGROUND_RED,
237 'green_background': BACKGROUND_GREEN,
241 'green_background': BACKGROUND_GREEN,
238 'yellow_background': BACKGROUND_RED | BACKGROUND_GREEN,
242 'yellow_background': BACKGROUND_RED | BACKGROUND_GREEN,
239 'blue_background': BACKGROUND_BLUE,
243 'blue_background': BACKGROUND_BLUE,
240 'purple_background': BACKGROUND_BLUE | BACKGROUND_RED,
244 'purple_background': BACKGROUND_BLUE | BACKGROUND_RED,
241 'cyan_background': BACKGROUND_BLUE | BACKGROUND_GREEN,
245 'cyan_background': BACKGROUND_BLUE | BACKGROUND_GREEN,
242 'white_background': BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE,
246 'white_background': BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE,
243 'bold_background': BACKGROUND_INTENSITY,
247 'bold_background': BACKGROUND_INTENSITY,
244 'underline': COMMON_LVB_UNDERSCORE, # double-byte charsets only
248 'underline': COMMON_LVB_UNDERSCORE, # double-byte charsets only
245 'inverse': COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
249 'inverse': COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
246 }
250 }
247
251
248 stdout = GetStdHandle(STD_OUTPUT_HANDLE)
252 stdout = GetStdHandle(STD_OUTPUT_HANDLE)
249 try:
253 try:
250 origattr = stdout.GetConsoleScreenBufferInfo()['Attributes']
254 origattr = stdout.GetConsoleScreenBufferInfo()['Attributes']
251 except pywintypes.error:
255 except pywintypes.error:
252 # stdout may be defined but not support
256 # stdout may be defined but not support
253 # GetConsoleScreenBufferInfo(), when called from subprocess or
257 # GetConsoleScreenBufferInfo(), when called from subprocess or
254 # redirected.
258 # redirected.
255 raise ImportError()
259 raise ImportError()
256 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)', re.MULTILINE | re.DOTALL)
260 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)', re.MULTILINE | re.DOTALL)
257
261
258 def win32print(text, orig, **opts):
262 def win32print(text, orig, **opts):
259 label = opts.get('label', '')
263 label = opts.get('label', '')
260 attr = 0
264 attr = 0
261
265
262 # determine console attributes based on labels
266 # determine console attributes based on labels
263 for l in label.split():
267 for l in label.split():
264 style = _styles.get(l, '')
268 style = _styles.get(l, '')
265 for effect in style.split():
269 for effect in style.split():
266 attr |= w32effects[effect]
270 attr |= w32effects[effect]
267
271
268 # hack to ensure regexp finds data
272 # hack to ensure regexp finds data
269 if not text.startswith('\033['):
273 if not text.startswith('\033['):
270 text = '\033[m' + text
274 text = '\033[m' + text
271
275
272 # Look for ANSI-like codes embedded in text
276 # Look for ANSI-like codes embedded in text
273 m = re.match(ansire, text)
277 m = re.match(ansire, text)
274 while m:
278 while m:
275 for sattr in m.group(1).split(';'):
279 for sattr in m.group(1).split(';'):
276 if sattr:
280 if sattr:
277 val = int(sattr)
281 val = int(sattr)
278 attr = val and attr|val or 0
282 attr = val and attr|val or 0
279 stdout.SetConsoleTextAttribute(attr or origattr)
283 stdout.SetConsoleTextAttribute(attr or origattr)
280 orig(m.group(2), **opts)
284 orig(m.group(2), **opts)
281 m = re.match(ansire, m.group(3))
285 m = re.match(ansire, m.group(3))
282
286
283 # Explicity reset original attributes
287 # Explicity reset original attributes
284 stdout.SetConsoleTextAttribute(origattr)
288 stdout.SetConsoleTextAttribute(origattr)
285
289
286 except ImportError:
290 except ImportError:
287 w32effects = None
291 w32effects = None
General Comments 0
You need to be logged in to leave comments. Login now