##// END OF EJS Templates
diff colorization: finish highlighting trailing whitespace
Georg Brandl -
r7457:a70fb83c default
parent child Browse files
Show More
@@ -1,263 +1,271 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 '''add color output to status, qseries, and diff-related commands
19 '''add color output to status, qseries, and diff-related commands
20
20
21 This extension modifies the status command to add color to its output to
21 This extension modifies the status command to add color to its output to
22 reflect file status, the qseries command to add color to reflect patch status
22 reflect file status, the qseries command to add color to reflect patch status
23 (applied, unapplied, missing), and to diff-related commands to highlight
23 (applied, unapplied, missing), and to diff-related commands to highlight
24 additions, removals, diff headers, and trailing whitespace. Other effects in
24 additions, removals, diff headers, and trailing whitespace.
25 addition to color, like bold and underlined text, are also available.
25
26 Effects are rendered with the ECMA-48 SGR control function (aka ANSI escape
26 Other effects in addition to color, like bold and underlined text, are also
27 codes). This module also provides the render_text function, which can be
27 available. Effects are rendered with the ECMA-48 SGR control function (aka
28 used to add effects to any text.
28 ANSI escape codes). This module also provides the render_text function,
29 which can be used to add effects to any text.
29
30
30 To enable this extension, add this to your .hgrc file:
31 To enable this extension, add this to your .hgrc file:
31 [extensions]
32 [extensions]
32 color =
33 color =
33
34
34 Default effects my be overriden from the .hgrc file:
35 Default effects my be overriden from the .hgrc file:
35
36
36 [color]
37 [color]
37 status.modified = blue bold underline red_background
38 status.modified = blue bold underline red_background
38 status.added = green bold
39 status.added = green bold
39 status.removed = red bold blue_background
40 status.removed = red bold blue_background
40 status.deleted = cyan bold underline
41 status.deleted = cyan bold underline
41 status.unknown = magenta bold underline
42 status.unknown = magenta bold underline
42 status.ignored = black bold
43 status.ignored = black bold
43
44
44 # 'none' turns off all effects
45 # 'none' turns off all effects
45 status.clean = none
46 status.clean = none
46 status.copied = none
47 status.copied = none
47
48
48 qseries.applied = blue bold underline
49 qseries.applied = blue bold underline
49 qseries.unapplied = black bold
50 qseries.unapplied = black bold
50 qseries.missing = red bold
51 qseries.missing = red bold
51
52
52 diff.diffline = bold
53 diff.diffline = bold
53 diff.extended = cyan bold
54 diff.extended = cyan bold
54 diff.file_a = red bold
55 diff.file_a = red bold
55 diff.file_b = green bold
56 diff.file_b = green bold
56 diff.hunk = magenta
57 diff.hunk = magenta
57 diff.deleted = red
58 diff.deleted = red
58 diff.inserted = green
59 diff.inserted = green
59 diff.changed = white
60 diff.changed = white
60 diff.whitespace = bold red_background
61 diff.trailingwhitespace = bold red_background
61 '''
62 '''
62
63
63 import os, re, sys
64 import os, re, sys
64
65
65 from mercurial import cmdutil, commands, extensions
66 from mercurial import cmdutil, commands, extensions
66 from mercurial.i18n import _
67 from mercurial.i18n import _
67
68
68 # start and stop parameters for effects
69 # start and stop parameters for effects
69 _effect_params = { 'none': (0, 0),
70 _effect_params = { 'none': (0, 0),
70 'black': (30, 39),
71 'black': (30, 39),
71 'red': (31, 39),
72 'red': (31, 39),
72 'green': (32, 39),
73 'green': (32, 39),
73 'yellow': (33, 39),
74 'yellow': (33, 39),
74 'blue': (34, 39),
75 'blue': (34, 39),
75 'magenta': (35, 39),
76 'magenta': (35, 39),
76 'cyan': (36, 39),
77 'cyan': (36, 39),
77 'white': (37, 39),
78 'white': (37, 39),
78 'bold': (1, 22),
79 'bold': (1, 22),
79 'italic': (3, 23),
80 'italic': (3, 23),
80 'underline': (4, 24),
81 'underline': (4, 24),
81 'inverse': (7, 27),
82 'inverse': (7, 27),
82 'black_background': (40, 49),
83 'black_background': (40, 49),
83 'red_background': (41, 49),
84 'red_background': (41, 49),
84 'green_background': (42, 49),
85 'green_background': (42, 49),
85 'yellow_background': (43, 49),
86 'yellow_background': (43, 49),
86 'blue_background': (44, 49),
87 'blue_background': (44, 49),
87 'purple_background': (45, 49),
88 'purple_background': (45, 49),
88 'cyan_background': (46, 49),
89 'cyan_background': (46, 49),
89 'white_background': (47, 49), }
90 'white_background': (47, 49), }
90
91
91 def render_effects(text, *effects):
92 def render_effects(text, *effects):
92 'Wrap text in commands to turn on each effect.'
93 'Wrap text in commands to turn on each effect.'
93 start = [ str(_effect_params['none'][0]) ]
94 start = [ str(_effect_params['none'][0]) ]
94 stop = []
95 stop = []
95 for effect in effects:
96 for effect in effects:
96 start.append(str(_effect_params[effect][0]))
97 start.append(str(_effect_params[effect][0]))
97 stop.append(str(_effect_params[effect][1]))
98 stop.append(str(_effect_params[effect][1]))
98 stop.append(str(_effect_params['none'][1]))
99 stop.append(str(_effect_params['none'][1]))
99 start = '\033[' + ';'.join(start) + 'm'
100 start = '\033[' + ';'.join(start) + 'm'
100 stop = '\033[' + ';'.join(stop) + 'm'
101 stop = '\033[' + ';'.join(stop) + 'm'
101 return start + text + stop
102 return start + text + stop
102
103
103 def colorstatus(orig, ui, repo, *pats, **opts):
104 def colorstatus(orig, ui, repo, *pats, **opts):
104 '''run the status command with colored output'''
105 '''run the status command with colored output'''
105
106
106 delimiter = opts['print0'] and '\0' or '\n'
107 delimiter = opts['print0'] and '\0' or '\n'
107
108
108 nostatus = opts.get('no_status')
109 nostatus = opts.get('no_status')
109 opts['no_status'] = False
110 opts['no_status'] = False
110 # run status and capture its output
111 # run status and capture its output
111 ui.pushbuffer()
112 ui.pushbuffer()
112 retval = orig(ui, repo, *pats, **opts)
113 retval = orig(ui, repo, *pats, **opts)
113 # filter out empty strings
114 # filter out empty strings
114 lines_with_status = [ line for line in ui.popbuffer().split(delimiter) if line ]
115 lines_with_status = [ line for line in ui.popbuffer().split(delimiter) if line ]
115
116
116 if nostatus:
117 if nostatus:
117 lines = [l[2:] for l in lines_with_status]
118 lines = [l[2:] for l in lines_with_status]
118 else:
119 else:
119 lines = lines_with_status
120 lines = lines_with_status
120
121
121 # apply color to output and display it
122 # apply color to output and display it
122 for i in xrange(0, len(lines)):
123 for i in xrange(0, len(lines)):
123 status = _status_abbreviations[lines_with_status[i][0]]
124 status = _status_abbreviations[lines_with_status[i][0]]
124 effects = _status_effects[status]
125 effects = _status_effects[status]
125 if effects:
126 if effects:
126 lines[i] = render_effects(lines[i], *effects)
127 lines[i] = render_effects(lines[i], *effects)
127 ui.write(lines[i] + delimiter)
128 ui.write(lines[i] + delimiter)
128 return retval
129 return retval
129
130
130 _status_abbreviations = { 'M': 'modified',
131 _status_abbreviations = { 'M': 'modified',
131 'A': 'added',
132 'A': 'added',
132 'R': 'removed',
133 'R': 'removed',
133 '!': 'deleted',
134 '!': 'deleted',
134 '?': 'unknown',
135 '?': 'unknown',
135 'I': 'ignored',
136 'I': 'ignored',
136 'C': 'clean',
137 'C': 'clean',
137 ' ': 'copied', }
138 ' ': 'copied', }
138
139
139 _status_effects = { 'modified': ('blue', 'bold'),
140 _status_effects = { 'modified': ('blue', 'bold'),
140 'added': ('green', 'bold'),
141 'added': ('green', 'bold'),
141 'removed': ('red', 'bold'),
142 'removed': ('red', 'bold'),
142 'deleted': ('cyan', 'bold', 'underline'),
143 'deleted': ('cyan', 'bold', 'underline'),
143 'unknown': ('magenta', 'bold', 'underline'),
144 'unknown': ('magenta', 'bold', 'underline'),
144 'ignored': ('black', 'bold'),
145 'ignored': ('black', 'bold'),
145 'clean': ('none', ),
146 'clean': ('none', ),
146 'copied': ('none', ), }
147 'copied': ('none', ), }
147
148
148 def colorqseries(orig, ui, repo, *dummy, **opts):
149 def colorqseries(orig, ui, repo, *dummy, **opts):
149 '''run the qseries command with colored output'''
150 '''run the qseries command with colored output'''
150 ui.pushbuffer()
151 ui.pushbuffer()
151 retval = orig(ui, repo, **opts)
152 retval = orig(ui, repo, **opts)
152 patches = ui.popbuffer().splitlines()
153 patches = ui.popbuffer().splitlines()
153 for patch in patches:
154 for patch in patches:
154 patchname = patch
155 patchname = patch
155 if opts['summary']:
156 if opts['summary']:
156 patchname = patchname.split(': ')[0]
157 patchname = patchname.split(': ')[0]
157 if ui.verbose:
158 if ui.verbose:
158 patchname = patchname.split(' ', 2)[-1]
159 patchname = patchname.split(' ', 2)[-1]
159
160
160 if opts['missing']:
161 if opts['missing']:
161 effects = _patch_effects['missing']
162 effects = _patch_effects['missing']
162 # Determine if patch is applied.
163 # Determine if patch is applied.
163 elif [ applied for applied in repo.mq.applied
164 elif [ applied for applied in repo.mq.applied
164 if patchname == applied.name ]:
165 if patchname == applied.name ]:
165 effects = _patch_effects['applied']
166 effects = _patch_effects['applied']
166 else:
167 else:
167 effects = _patch_effects['unapplied']
168 effects = _patch_effects['unapplied']
168 ui.write(render_effects(patch, *effects) + '\n')
169 ui.write(render_effects(patch, *effects) + '\n')
169 return retval
170 return retval
170
171
171 _patch_effects = { 'applied': ('blue', 'bold', 'underline'),
172 _patch_effects = { 'applied': ('blue', 'bold', 'underline'),
172 'missing': ('red', 'bold'),
173 'missing': ('red', 'bold'),
173 'unapplied': ('black', 'bold'), }
174 'unapplied': ('black', 'bold'), }
174
175
175 def colorwrap(orig, s):
176 def colorwrap(orig, s):
176 '''wrap ui.write for colored diff output'''
177 '''wrap ui.write for colored diff output'''
177 lines = s.split('\n')
178 lines = s.split('\n')
178 for i, line in enumerate(lines):
179 for i, line in enumerate(lines):
180 stripline = line
181 if line and line[0] in '+-':
182 # highlight trailing whitespace, but only in changed lines
183 stripline = line.rstrip()
179 for prefix, style in _diff_prefixes:
184 for prefix, style in _diff_prefixes:
180 if line.startswith(prefix):
185 if stripline.startswith(prefix):
181 effects = _diff_effects[style]
186 lines[i] = render_effects(stripline, *_diff_effects[style])
182 lines[i] = render_effects(line, *_diff_effects[style])
183 break
187 break
188 if line != stripline:
189 lines[i] += render_effects(
190 line[len(stripline):], *_diff_effects['trailingwhitespace'])
184 orig('\n'.join(lines))
191 orig('\n'.join(lines))
185
192
186 def colorshowpatch(orig, self, node):
193 def colorshowpatch(orig, self, node):
187 '''wrap cmdutil.changeset_printer.showpatch with colored output'''
194 '''wrap cmdutil.changeset_printer.showpatch with colored output'''
188 oldwrite = extensions.wrapfunction(self.ui, 'write', colorwrap)
195 oldwrite = extensions.wrapfunction(self.ui, 'write', colorwrap)
189 try:
196 try:
190 orig(self, node)
197 orig(self, node)
191 finally:
198 finally:
192 self.ui.write = oldwrite
199 self.ui.write = oldwrite
193
200
194 def colordiff(orig, ui, repo, *pats, **opts):
201 def colordiff(orig, ui, repo, *pats, **opts):
195 '''run the diff command with colored output'''
202 '''run the diff command with colored output'''
196 oldwrite = extensions.wrapfunction(ui, 'write', colorwrap)
203 oldwrite = extensions.wrapfunction(ui, 'write', colorwrap)
197 try:
204 try:
198 orig(ui, repo, *pats, **opts)
205 orig(ui, repo, *pats, **opts)
199 finally:
206 finally:
200 ui.write = oldwrite
207 ui.write = oldwrite
201
208
202 _diff_prefixes = [('diff', 'diffline'),
209 _diff_prefixes = [('diff', 'diffline'),
203 ('copy', 'extended'),
210 ('copy', 'extended'),
204 ('rename', 'extended'),
211 ('rename', 'extended'),
205 ('new', 'extended'),
212 ('new', 'extended'),
206 ('deleted', 'extended'),
213 ('deleted', 'extended'),
207 ('---', 'file_a'),
214 ('---', 'file_a'),
208 ('+++', 'file_b'),
215 ('+++', 'file_b'),
209 ('@', 'hunk'),
216 ('@', 'hunk'),
210 ('-', 'deleted'),
217 ('-', 'deleted'),
211 ('+', 'inserted')]
218 ('+', 'inserted')]
212
219
213 _diff_effects = {'diffline': ('bold',),
220 _diff_effects = {'diffline': ('bold',),
214 'extended': ('cyan', 'bold'),
221 'extended': ('cyan', 'bold'),
215 'file_a': ('red', 'bold'),
222 'file_a': ('red', 'bold'),
216 'file_b': ('green', 'bold'),
223 'file_b': ('green', 'bold'),
217 'hunk': ('magenta',),
224 'hunk': ('magenta',),
218 'deleted': ('red',),
225 'deleted': ('red',),
219 'inserted': ('green',),
226 'inserted': ('green',),
220 'changed': ('white',)}
227 'changed': ('white',),
228 'trailingwhitespace': ('bold', 'red_background'),}
221
229
222 def uisetup(ui):
230 def uisetup(ui):
223 '''Initialize the extension.'''
231 '''Initialize the extension.'''
224 _setupcmd(ui, 'diff', commands.table, colordiff, _diff_effects)
232 _setupcmd(ui, 'diff', commands.table, colordiff, _diff_effects)
225 _setupcmd(ui, 'incoming', commands.table, None, _diff_effects)
233 _setupcmd(ui, 'incoming', commands.table, None, _diff_effects)
226 _setupcmd(ui, 'log', commands.table, None, _diff_effects)
234 _setupcmd(ui, 'log', commands.table, None, _diff_effects)
227 _setupcmd(ui, 'outgoing', commands.table, None, _diff_effects)
235 _setupcmd(ui, 'outgoing', commands.table, None, _diff_effects)
228 _setupcmd(ui, 'tip', commands.table, None, _diff_effects)
236 _setupcmd(ui, 'tip', commands.table, None, _diff_effects)
229 _setupcmd(ui, 'status', commands.table, colorstatus, _status_effects)
237 _setupcmd(ui, 'status', commands.table, colorstatus, _status_effects)
230 if ui.config('extensions', 'hgext.mq') is not None or \
238 if ui.config('extensions', 'hgext.mq') is not None or \
231 ui.config('extensions', 'mq') is not None:
239 ui.config('extensions', 'mq') is not None:
232 from hgext import mq
240 from hgext import mq
233 _setupcmd(ui, 'qdiff', mq.cmdtable, colordiff, _diff_effects)
241 _setupcmd(ui, 'qdiff', mq.cmdtable, colordiff, _diff_effects)
234 _setupcmd(ui, 'qseries', mq.cmdtable, colorqseries, _patch_effects)
242 _setupcmd(ui, 'qseries', mq.cmdtable, colorqseries, _patch_effects)
235
243
236 def _setupcmd(ui, cmd, table, func, effectsmap):
244 def _setupcmd(ui, cmd, table, func, effectsmap):
237 '''patch in command to command table and load effect map'''
245 '''patch in command to command table and load effect map'''
238 def nocolor(orig, *args, **opts):
246 def nocolor(orig, *args, **opts):
239
247
240 if (opts['no_color'] or opts['color'] == 'never' or
248 if (opts['no_color'] or opts['color'] == 'never' or
241 (opts['color'] == 'auto' and (os.environ.get('TERM') == 'dumb'
249 (opts['color'] == 'auto' and (os.environ.get('TERM') == 'dumb'
242 or not sys.__stdout__.isatty()))):
250 or not sys.__stdout__.isatty()))):
243 return orig(*args, **opts)
251 return orig(*args, **opts)
244
252
245 oldshowpatch = extensions.wrapfunction(cmdutil.changeset_printer,
253 oldshowpatch = extensions.wrapfunction(cmdutil.changeset_printer,
246 'showpatch', colorshowpatch)
254 'showpatch', colorshowpatch)
247 try:
255 try:
248 if func is not None:
256 if func is not None:
249 return func(orig, *args, **opts)
257 return func(orig, *args, **opts)
250 return orig(*args, **opts)
258 return orig(*args, **opts)
251 finally:
259 finally:
252 cmdutil.changeset_printer.showpatch = oldshowpatch
260 cmdutil.changeset_printer.showpatch = oldshowpatch
253
261
254 entry = extensions.wrapcommand(table, cmd, nocolor)
262 entry = extensions.wrapcommand(table, cmd, nocolor)
255 entry[1].extend([
263 entry[1].extend([
256 ('', 'color', 'auto', _("when to colorize (always, auto, or never)")),
264 ('', 'color', 'auto', _("when to colorize (always, auto, or never)")),
257 ('', 'no-color', None, _("don't colorize output")),
265 ('', 'no-color', None, _("don't colorize output")),
258 ])
266 ])
259
267
260 for status in effectsmap:
268 for status in effectsmap:
261 effects = ui.config('color', cmd + '.' + status)
269 effects = ui.config('color', cmd + '.' + status)
262 if effects:
270 if effects:
263 effectsmap[status] = re.split('\W+', effects)
271 effectsmap[status] = re.split('\W+', effects)
General Comments 0
You need to be logged in to leave comments. Login now