]> git.lizzy.rs Git - plan9front.git/blob - sys/lib/python/hgext/color.py
hgwebfs: write headers individually, so they are not limited by webfs iounit (thanks...
[plan9front.git] / sys / lib / python / hgext / color.py
1 # color.py color output for the status and qseries commands
2 #
3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
4 #
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
7 # Free Software Foundation; either version 2 of the License, or (at your
8 # option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
13 # Public License for more details.
14 #
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.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
19 '''colorize output from some commands
20
21 This extension modifies the status command to add color to its output
22 to reflect file status, the qseries command to add color to reflect
23 patch status (applied, unapplied, missing), and to diff-related
24 commands to highlight additions, removals, diff headers, and trailing
25 whitespace.
26
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
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.
31
32 Default effects may be overridden from the .hgrc file::
33
34   [color]
35   status.modified = blue bold underline red_background
36   status.added = green bold
37   status.removed = red bold blue_background
38   status.deleted = cyan bold underline
39   status.unknown = magenta bold underline
40   status.ignored = black bold
41
42   # 'none' turns off all effects
43   status.clean = none
44   status.copied = none
45
46   qseries.applied = blue bold underline
47   qseries.unapplied = black bold
48   qseries.missing = red bold
49
50   diff.diffline = bold
51   diff.extended = cyan bold
52   diff.file_a = red bold
53   diff.file_b = green bold
54   diff.hunk = magenta
55   diff.deleted = red
56   diff.inserted = green
57   diff.changed = white
58   diff.trailingwhitespace = bold red_background
59 '''
60
61 import os, sys
62 import itertools
63
64 from mercurial import cmdutil, commands, extensions, error
65 from mercurial.i18n import _
66
67 # start and stop parameters for effects
68 _effect_params = {'none': 0,
69                   'black': 30,
70                   'red': 31,
71                   'green': 32,
72                   'yellow': 33,
73                   'blue': 34,
74                   'magenta': 35,
75                   'cyan': 36,
76                   'white': 37,
77                   'bold': 1,
78                   'italic': 3,
79                   'underline': 4,
80                   'inverse': 7,
81                   'black_background': 40,
82                   'red_background': 41,
83                   'green_background': 42,
84                   'yellow_background': 43,
85                   'blue_background': 44,
86                   'purple_background': 45,
87                   'cyan_background': 46,
88                   'white_background': 47}
89
90 def render_effects(text, effects):
91     'Wrap text in commands to turn on each effect.'
92     start = [str(_effect_params[e]) for e in ['none'] + effects]
93     start = '\033[' + ';'.join(start) + 'm'
94     stop = '\033[' + str(_effect_params['none']) + 'm'
95     return ''.join([start, text, stop])
96
97 def colorstatus(orig, ui, repo, *pats, **opts):
98     '''run the status command with colored output'''
99
100     delimiter = opts['print0'] and '\0' or '\n'
101
102     nostatus = opts.get('no_status')
103     opts['no_status'] = False
104     # run status and capture its output
105     ui.pushbuffer()
106     retval = orig(ui, repo, *pats, **opts)
107     # filter out empty strings
108     lines_with_status = [ line for line in ui.popbuffer().split(delimiter) if line ]
109
110     if nostatus:
111         lines = [l[2:] for l in lines_with_status]
112     else:
113         lines = lines_with_status
114
115     # apply color to output and display it
116     for i in xrange(len(lines)):
117         status = _status_abbreviations[lines_with_status[i][0]]
118         effects = _status_effects[status]
119         if effects:
120             lines[i] = render_effects(lines[i], effects)
121         ui.write(lines[i] + delimiter)
122     return retval
123
124 _status_abbreviations = { 'M': 'modified',
125                           'A': 'added',
126                           'R': 'removed',
127                           '!': 'deleted',
128                           '?': 'unknown',
129                           'I': 'ignored',
130                           'C': 'clean',
131                           ' ': 'copied', }
132
133 _status_effects = { 'modified': ['blue', 'bold'],
134                     'added': ['green', 'bold'],
135                     'removed': ['red', 'bold'],
136                     'deleted': ['cyan', 'bold', 'underline'],
137                     'unknown': ['magenta', 'bold', 'underline'],
138                     'ignored': ['black', 'bold'],
139                     'clean': ['none'],
140                     'copied': ['none'], }
141
142 def colorqseries(orig, ui, repo, *dummy, **opts):
143     '''run the qseries command with colored output'''
144     ui.pushbuffer()
145     retval = orig(ui, repo, **opts)
146     patchlines = ui.popbuffer().splitlines()
147     patchnames = repo.mq.series
148
149     for patch, patchname in itertools.izip(patchlines, patchnames):
150         if opts['missing']:
151             effects = _patch_effects['missing']
152         # Determine if patch is applied.
153         elif [ applied for applied in repo.mq.applied
154                if patchname == applied.name ]:
155             effects = _patch_effects['applied']
156         else:
157             effects = _patch_effects['unapplied']
158
159         patch = patch.replace(patchname, render_effects(patchname, effects), 1)
160         ui.write(patch + '\n')
161     return retval
162
163 _patch_effects = { 'applied': ['blue', 'bold', 'underline'],
164                    'missing': ['red', 'bold'],
165                    'unapplied': ['black', 'bold'], }
166
167 def colorwrap(orig, s):
168     '''wrap ui.write for colored diff output'''
169     lines = s.split('\n')
170     for i, line in enumerate(lines):
171         stripline = line
172         if line and line[0] in '+-':
173             # highlight trailing whitespace, but only in changed lines
174             stripline = line.rstrip()
175         for prefix, style in _diff_prefixes:
176             if stripline.startswith(prefix):
177                 lines[i] = render_effects(stripline, _diff_effects[style])
178                 break
179         if line != stripline:
180             lines[i] += render_effects(
181                 line[len(stripline):], _diff_effects['trailingwhitespace'])
182     orig('\n'.join(lines))
183
184 def colorshowpatch(orig, self, node):
185     '''wrap cmdutil.changeset_printer.showpatch with colored output'''
186     oldwrite = extensions.wrapfunction(self.ui, 'write', colorwrap)
187     try:
188         orig(self, node)
189     finally:
190         self.ui.write = oldwrite
191
192 def colordiff(orig, ui, repo, *pats, **opts):
193     '''run the diff command with colored output'''
194     oldwrite = extensions.wrapfunction(ui, 'write', colorwrap)
195     try:
196         orig(ui, repo, *pats, **opts)
197     finally:
198         ui.write = oldwrite
199
200 _diff_prefixes = [('diff', 'diffline'),
201                   ('copy', 'extended'),
202                   ('rename', 'extended'),
203                   ('old', 'extended'),
204                   ('new', 'extended'),
205                   ('deleted', 'extended'),
206                   ('---', 'file_a'),
207                   ('+++', 'file_b'),
208                   ('@', 'hunk'),
209                   ('-', 'deleted'),
210                   ('+', 'inserted')]
211
212 _diff_effects = {'diffline': ['bold'],
213                  'extended': ['cyan', 'bold'],
214                  'file_a': ['red', 'bold'],
215                  'file_b': ['green', 'bold'],
216                  'hunk': ['magenta'],
217                  'deleted': ['red'],
218                  'inserted': ['green'],
219                  'changed': ['white'],
220                  'trailingwhitespace': ['bold', 'red_background']}
221
222 _ui = None
223
224 def uisetup(ui):
225     '''Initialize the extension.'''
226     global _ui
227     _ui = ui
228     _setupcmd(ui, 'diff', commands.table, colordiff, _diff_effects)
229     _setupcmd(ui, 'incoming', commands.table, None, _diff_effects)
230     _setupcmd(ui, 'log', commands.table, None, _diff_effects)
231     _setupcmd(ui, 'outgoing', commands.table, None, _diff_effects)
232     _setupcmd(ui, 'tip', commands.table, None, _diff_effects)
233     _setupcmd(ui, 'status', commands.table, colorstatus, _status_effects)
234
235 def extsetup():
236     try:
237         mq = extensions.find('mq')
238         try:
239             # If we are loaded after mq, we must wrap commands.table
240             _setupcmd(_ui, 'qdiff', commands.table, colordiff, _diff_effects)
241             _setupcmd(_ui, 'qseries', commands.table, colorqseries, _patch_effects)
242         except error.UnknownCommand:
243             # Otherwise we wrap mq.cmdtable
244             _setupcmd(_ui, 'qdiff', mq.cmdtable, colordiff, _diff_effects)
245             _setupcmd(_ui, 'qseries', mq.cmdtable, colorqseries, _patch_effects)
246     except KeyError:
247         # The mq extension is not enabled
248         pass
249
250 def _setupcmd(ui, cmd, table, func, effectsmap):
251     '''patch in command to command table and load effect map'''
252     def nocolor(orig, *args, **opts):
253
254         if (opts['no_color'] or opts['color'] == 'never' or
255             (opts['color'] == 'auto' and (os.environ.get('TERM') == 'dumb'
256                                           or not sys.__stdout__.isatty()))):
257             return orig(*args, **opts)
258
259         oldshowpatch = extensions.wrapfunction(cmdutil.changeset_printer,
260                                                'showpatch', colorshowpatch)
261         try:
262             if func is not None:
263                 return func(orig, *args, **opts)
264             return orig(*args, **opts)
265         finally:
266             cmdutil.changeset_printer.showpatch = oldshowpatch
267
268     entry = extensions.wrapcommand(table, cmd, nocolor)
269     entry[1].extend([
270         ('', 'color', 'auto', _("when to colorize (always, auto, or never)")),
271         ('', 'no-color', None, _("don't colorize output")),
272     ])
273
274     for status in effectsmap:
275         configkey = cmd + '.' + status
276         effects = ui.configlist('color', configkey)
277         if effects:
278             good = []
279             for e in effects:
280                 if e in _effect_params:
281                     good.append(e)
282                 else:
283                     ui.warn(_("ignoring unknown color/effect %r "
284                               "(configured in color.%s)\n")
285                             % (e, configkey))
286             effectsmap[status] = good