color.py
374 lines
| 13.2 KiB
| text/x-python
|
PythonLexer
/ hgext / color.py
|
r5787 | # color.py color output for the status and qseries commands | ||
# | ||||
# Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com> | ||||
# | ||||
|
r5792 | # This program is free software; you can redistribute it and/or modify it | ||
|
r5787 | # under the terms of the GNU General Public License as published by the | ||
|
r5792 | # Free Software Foundation; either version 2 of the License, or (at your | ||
|
r5787 | # option) any later version. | ||
# | ||||
# This program is distributed in the hope that it will be useful, but | ||||
# WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General | ||||
# Public License for more details. | ||||
# | ||||
# You should have received a copy of the GNU General Public License along | ||||
|
r5792 | # with this program; if not, write to the Free Software Foundation, Inc., | ||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||||
|
r5787 | |||
|
r8894 | '''colorize output from some commands | ||
|
r5787 | |||
|
r10223 | This extension modifies the status and resolve commands to add color to their | ||
output to reflect file status, the qseries command to add color to reflect | ||||
|
r7988 | patch status (applied, unapplied, missing), and to diff-related | ||
commands to highlight additions, removals, diff headers, and trailing | ||||
whitespace. | ||||
|
r7457 | |||
|
r7988 | Other effects in addition to color, like bold and underlined text, are | ||
also available. Effects are rendered with the ECMA-48 SGR control | ||||
function (aka ANSI escape codes). This module also provides the | ||||
render_text function, which can be used to add effects to any text. | ||||
|
r5787 | |||
|
r9206 | Default effects may be overridden from the .hgrc file:: | ||
|
r5787 | |||
|
r9206 | [color] | ||
status.modified = blue bold underline red_background | ||||
status.added = green bold | ||||
status.removed = red bold blue_background | ||||
status.deleted = cyan bold underline | ||||
status.unknown = magenta bold underline | ||||
status.ignored = black bold | ||||
|
r5787 | |||
|
r9206 | # 'none' turns off all effects | ||
status.clean = none | ||||
status.copied = none | ||||
|
r5787 | |||
|
r9206 | qseries.applied = blue bold underline | ||
qseries.unapplied = black bold | ||||
qseries.missing = red bold | ||||
|
r7456 | |||
|
r9206 | diff.diffline = bold | ||
diff.extended = cyan bold | ||||
diff.file_a = red bold | ||||
diff.file_b = green bold | ||||
diff.hunk = magenta | ||||
diff.deleted = red | ||||
diff.inserted = green | ||||
diff.changed = white | ||||
diff.trailingwhitespace = bold red_background | ||||
|
r10046 | |||
|
r10223 | resolve.unresolved = red bold | ||
resolve.resolved = green bold | ||||
|
r10046 | bookmarks.current = green | ||
|
r5787 | ''' | ||
|
r8623 | import os, sys | ||
|
r5787 | |||
|
r10463 | from mercurial import cmdutil, commands, extensions | ||
|
r5787 | from mercurial.i18n import _ | ||
# start and stop parameters for effects | ||||
|
r7459 | _effect_params = {'none': 0, | ||
'black': 30, | ||||
'red': 31, | ||||
'green': 32, | ||||
'yellow': 33, | ||||
'blue': 34, | ||||
'magenta': 35, | ||||
'cyan': 36, | ||||
'white': 37, | ||||
'bold': 1, | ||||
'italic': 3, | ||||
'underline': 4, | ||||
'inverse': 7, | ||||
'black_background': 40, | ||||
'red_background': 41, | ||||
'green_background': 42, | ||||
'yellow_background': 43, | ||||
'blue_background': 44, | ||||
'purple_background': 45, | ||||
'cyan_background': 46, | ||||
'white_background': 47} | ||||
|
r5787 | |||
|
r8622 | def render_effects(text, effects): | ||
|
r5787 | 'Wrap text in commands to turn on each effect.' | ||
|
r8622 | start = [str(_effect_params[e]) for e in ['none'] + effects] | ||
|
r5787 | start = '\033[' + ';'.join(start) + 'm' | ||
|
r7459 | stop = '\033[' + str(_effect_params['none']) + 'm' | ||
return ''.join([start, text, stop]) | ||||
|
r5787 | |||
|
r10223 | def _colorstatuslike(abbreviations, effectdefs, orig, ui, repo, *pats, **opts): | ||
'''run a status-like command with colorized output''' | ||||
delimiter = opts.get('print0') and '\0' or '\n' | ||||
|
r5787 | |||
|
r7419 | nostatus = opts.get('no_status') | ||
opts['no_status'] = False | ||||
|
r10223 | # run original command and capture its output | ||
|
r5787 | ui.pushbuffer() | ||
|
r7216 | retval = orig(ui, repo, *pats, **opts) | ||
|
r5787 | # filter out empty strings | ||
|
r10222 | lines_with_status = [line for line in ui.popbuffer().split(delimiter) if line] | ||
|
r5787 | |||
|
r7419 | if nostatus: | ||
lines = [l[2:] for l in lines_with_status] | ||||
|
r5787 | else: | ||
|
r7419 | lines = lines_with_status | ||
|
r5787 | |||
# apply color to output and display it | ||||
|
r8624 | for i in xrange(len(lines)): | ||
|
r10475 | try: | ||
status = abbreviations[lines_with_status[i][0]] | ||||
except KeyError: | ||||
# Ignore lines with invalid codes, especially in the case of | ||||
# of unknown filenames containing newlines (issue2036). | ||||
pass | ||||
else: | ||||
effects = effectdefs[status] | ||||
if effects: | ||||
lines[i] = render_effects(lines[i], effects) | ||||
|
r7455 | ui.write(lines[i] + delimiter) | ||
|
r5787 | return retval | ||
|
r10223 | |||
|
r5787 | _status_abbreviations = { 'M': 'modified', | ||
'A': 'added', | ||||
'R': 'removed', | ||||
|
r5796 | '!': 'deleted', | ||
|
r5787 | '?': 'unknown', | ||
'I': 'ignored', | ||||
'C': 'clean', | ||||
' ': 'copied', } | ||||
|
r8622 | _status_effects = { 'modified': ['blue', 'bold'], | ||
'added': ['green', 'bold'], | ||||
'removed': ['red', 'bold'], | ||||
'deleted': ['cyan', 'bold', 'underline'], | ||||
'unknown': ['magenta', 'bold', 'underline'], | ||||
'ignored': ['black', 'bold'], | ||||
'clean': ['none'], | ||||
'copied': ['none'], } | ||||
|
r5787 | |||
|
r10223 | def colorstatus(orig, ui, repo, *pats, **opts): | ||
'''run the status command with colored output''' | ||||
return _colorstatuslike(_status_abbreviations, _status_effects, | ||||
orig, ui, repo, *pats, **opts) | ||||
_resolve_abbreviations = { 'U': 'unresolved', | ||||
'R': 'resolved', } | ||||
_resolve_effects = { 'unresolved': ['red', 'bold'], | ||||
'resolved': ['green', 'bold'], } | ||||
def colorresolve(orig, ui, repo, *pats, **opts): | ||||
'''run the resolve command with colored output''' | ||||
if not opts.get('list'): | ||||
# only colorize for resolve -l | ||||
return orig(ui, repo, *pats, **opts) | ||||
return _colorstatuslike(_resolve_abbreviations, _resolve_effects, | ||||
orig, ui, repo, *pats, **opts) | ||||
|
r10046 | _bookmark_effects = { 'current': ['green'] } | ||
def colorbookmarks(orig, ui, repo, *pats, **opts): | ||||
def colorize(orig, s): | ||||
lines = s.split('\n') | ||||
for i, line in enumerate(lines): | ||||
if line.startswith(" *"): | ||||
lines[i] = render_effects(line, _bookmark_effects['current']) | ||||
orig('\n'.join(lines)) | ||||
oldwrite = extensions.wrapfunction(ui, 'write', colorize) | ||||
try: | ||||
orig(ui, repo, *pats, **opts) | ||||
finally: | ||||
ui.write = oldwrite | ||||
|
r7216 | def colorqseries(orig, ui, repo, *dummy, **opts): | ||
|
r5787 | '''run the qseries command with colored output''' | ||
ui.pushbuffer() | ||||
|
r7216 | retval = orig(ui, repo, **opts) | ||
|
r9311 | patchlines = ui.popbuffer().splitlines() | ||
patchnames = repo.mq.series | ||||
|
r6855 | |||
|
r9374 | for patch, patchname in zip(patchlines, patchnames): | ||
|
r5787 | if opts['missing']: | ||
effects = _patch_effects['missing'] | ||||
|
r6855 | # Determine if patch is applied. | ||
|
r10282 | elif [applied for applied in repo.mq.applied | ||
if patchname == applied.name]: | ||||
|
r5787 | effects = _patch_effects['applied'] | ||
else: | ||||
effects = _patch_effects['unapplied'] | ||||
|
r9017 | |||
patch = patch.replace(patchname, render_effects(patchname, effects), 1) | ||||
ui.write(patch + '\n') | ||||
|
r5787 | return retval | ||
|
r8622 | _patch_effects = { 'applied': ['blue', 'bold', 'underline'], | ||
|
r9520 | 'missing': ['red', 'bold'], | ||
'unapplied': ['black', 'bold'], } | ||||
|
r9551 | def colorwrap(orig, *args): | ||
|
r7456 | '''wrap ui.write for colored diff output''' | ||
|
r9551 | def _colorize(s): | ||
lines = s.split('\n') | ||||
for i, line in enumerate(lines): | ||||
stripline = line | ||||
if line and line[0] in '+-': | ||||
# highlight trailing whitespace, but only in changed lines | ||||
stripline = line.rstrip() | ||||
for prefix, style in _diff_prefixes: | ||||
if stripline.startswith(prefix): | ||||
lines[i] = render_effects(stripline, _diff_effects[style]) | ||||
break | ||||
if line != stripline: | ||||
lines[i] += render_effects( | ||||
line[len(stripline):], _diff_effects['trailingwhitespace']) | ||||
return '\n'.join(lines) | ||||
orig(*[_colorize(s) for s in args]) | ||||
|
r7456 | |||
def colorshowpatch(orig, self, node): | ||||
'''wrap cmdutil.changeset_printer.showpatch with colored output''' | ||||
oldwrite = extensions.wrapfunction(self.ui, 'write', colorwrap) | ||||
try: | ||||
orig(self, node) | ||||
finally: | ||||
self.ui.write = oldwrite | ||||
|
r9641 | def colordiffstat(orig, s): | ||
lines = s.split('\n') | ||||
for i, line in enumerate(lines): | ||||
if line and line[-1] in '+-': | ||||
name, graph = line.rsplit(' ', 1) | ||||
graph = graph.replace('-', | ||||
render_effects('-', _diff_effects['deleted'])) | ||||
graph = graph.replace('+', | ||||
render_effects('+', _diff_effects['inserted'])) | ||||
lines[i] = ' '.join([name, graph]) | ||||
orig('\n'.join(lines)) | ||||
|
r7456 | def colordiff(orig, ui, repo, *pats, **opts): | ||
'''run the diff command with colored output''' | ||||
|
r9641 | if opts.get('stat'): | ||
wrapper = colordiffstat | ||||
else: | ||||
wrapper = colorwrap | ||||
oldwrite = extensions.wrapfunction(ui, 'write', wrapper) | ||||
|
r7456 | try: | ||
orig(ui, repo, *pats, **opts) | ||||
finally: | ||||
ui.write = oldwrite | ||||
|
r10004 | def colorchurn(orig, ui, repo, *pats, **opts): | ||
'''run the churn command with colored output''' | ||||
if not opts.get('diffstat'): | ||||
return orig(ui, repo, *pats, **opts) | ||||
oldwrite = extensions.wrapfunction(ui, 'write', colordiffstat) | ||||
try: | ||||
orig(ui, repo, *pats, **opts) | ||||
finally: | ||||
ui.write = oldwrite | ||||
|
r7456 | _diff_prefixes = [('diff', 'diffline'), | ||
('copy', 'extended'), | ||||
('rename', 'extended'), | ||||
|
r7539 | ('old', 'extended'), | ||
|
r7456 | ('new', 'extended'), | ||
('deleted', 'extended'), | ||||
('---', 'file_a'), | ||||
('+++', 'file_b'), | ||||
('@', 'hunk'), | ||||
('-', 'deleted'), | ||||
('+', 'inserted')] | ||||
|
r8630 | _diff_effects = {'diffline': ['bold'], | ||
|
r8622 | 'extended': ['cyan', 'bold'], | ||
'file_a': ['red', 'bold'], | ||||
'file_b': ['green', 'bold'], | ||||
|
r8630 | 'hunk': ['magenta'], | ||
'deleted': ['red'], | ||||
'inserted': ['green'], | ||||
'changed': ['white'], | ||||
'trailingwhitespace': ['bold', 'red_background']} | ||||
|
r7456 | |||
|
r9711 | def extsetup(ui): | ||
|
r5787 | '''Initialize the extension.''' | ||
|
r7456 | _setupcmd(ui, 'diff', commands.table, colordiff, _diff_effects) | ||
_setupcmd(ui, 'incoming', commands.table, None, _diff_effects) | ||||
_setupcmd(ui, 'log', commands.table, None, _diff_effects) | ||||
_setupcmd(ui, 'outgoing', commands.table, None, _diff_effects) | ||||
_setupcmd(ui, 'tip', commands.table, None, _diff_effects) | ||||
|
r7216 | _setupcmd(ui, 'status', commands.table, colorstatus, _status_effects) | ||
|
r10223 | _setupcmd(ui, 'resolve', commands.table, colorresolve, _resolve_effects) | ||
|
r8963 | |||
|
r8278 | try: | ||
mq = extensions.find('mq') | ||||
|
r9412 | _setupcmd(ui, 'qdiff', mq.cmdtable, colordiff, _diff_effects) | ||
_setupcmd(ui, 'qseries', mq.cmdtable, colorqseries, _patch_effects) | ||||
|
r8278 | except KeyError: | ||
|
r9711 | mq = None | ||
|
r5787 | |||
|
r9550 | try: | ||
rec = extensions.find('record') | ||||
_setupcmd(ui, 'record', rec.cmdtable, colordiff, _diff_effects) | ||||
except KeyError: | ||||
|
r9711 | rec = None | ||
if mq and rec: | ||||
_setupcmd(ui, 'qrecord', rec.cmdtable, colordiff, _diff_effects) | ||||
|
r10004 | try: | ||
churn = extensions.find('churn') | ||||
_setupcmd(ui, 'churn', churn.cmdtable, colorchurn, _diff_effects) | ||||
except KeyError: | ||||
churn = None | ||||
|
r9550 | |||
|
r10046 | try: | ||
bookmarks = extensions.find('bookmarks') | ||||
_setupcmd(ui, 'bookmarks', bookmarks.cmdtable, colorbookmarks, | ||||
_bookmark_effects) | ||||
except KeyError: | ||||
# The bookmarks extension is not enabled | ||||
pass | ||||
|
r7216 | def _setupcmd(ui, cmd, table, func, effectsmap): | ||
'''patch in command to command table and load effect map''' | ||||
|
r7455 | def nocolor(orig, *args, **opts): | ||
if (opts['no_color'] or opts['color'] == 'never' or | ||||
(opts['color'] == 'auto' and (os.environ.get('TERM') == 'dumb' | ||||
or not sys.__stdout__.isatty()))): | ||||
|
r10045 | del opts['no_color'] | ||
del opts['color'] | ||||
|
r7455 | return orig(*args, **opts) | ||
|
r7456 | oldshowpatch = extensions.wrapfunction(cmdutil.changeset_printer, | ||
'showpatch', colorshowpatch) | ||||
|
r10045 | del opts['no_color'] | ||
del opts['color'] | ||||
|
r7456 | try: | ||
if func is not None: | ||||
return func(orig, *args, **opts) | ||||
return orig(*args, **opts) | ||||
finally: | ||||
cmdutil.changeset_printer.showpatch = oldshowpatch | ||||
|
r5787 | |||
|
r7216 | entry = extensions.wrapcommand(table, cmd, nocolor) | ||
|
r7455 | entry[1].extend([ | ||
('', 'color', 'auto', _("when to colorize (always, auto, or never)")), | ||||
|
r9478 | ('', 'no-color', None, _("don't colorize output (DEPRECATED)")), | ||
|
r7455 | ]) | ||
|
r5787 | |||
for status in effectsmap: | ||||
|
r8945 | configkey = cmd + '.' + status | ||
effects = ui.configlist('color', configkey) | ||||
|
r5787 | if effects: | ||
|
r8945 | good = [] | ||
for e in effects: | ||||
if e in _effect_params: | ||||
good.append(e) | ||||
else: | ||||
ui.warn(_("ignoring unknown color/effect %r " | ||||
"(configured in color.%s)\n") | ||||
% (e, configkey)) | ||||
effectsmap[status] = good | ||||