color.py
471 lines
| 15.3 KiB
| text/x-python
|
PythonLexer
/ mercurial / color.py
Pierre-Yves David
|
r30652 | # utility for color output for Mercurial commands | ||
# | ||||
# Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com> and other | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
from __future__ import absolute_import | ||||
Pierre-Yves David
|
r30971 | from .i18n import _ | ||
Pierre-Yves David
|
r31101 | from . import ( | ||
encoding, | ||||
pycompat, | ||||
util | ||||
) | ||||
Pierre-Yves David
|
r31067 | |||
Pierre-Yves David
|
r30968 | try: | ||
import curses | ||||
# Mapping from effect name to terminfo attribute name (or raw code) or | ||||
# color number. This will also force-load the curses module. | ||||
Pierre-Yves David
|
r31113 | _baseterminfoparams = { | ||
Pierre-Yves David
|
r31109 | 'none': (True, 'sgr0', ''), | ||
'standout': (True, 'smso', ''), | ||||
'underline': (True, 'smul', ''), | ||||
'reverse': (True, 'rev', ''), | ||||
'inverse': (True, 'rev', ''), | ||||
'blink': (True, 'blink', ''), | ||||
'dim': (True, 'dim', ''), | ||||
'bold': (True, 'bold', ''), | ||||
'invisible': (True, 'invis', ''), | ||||
'italic': (True, 'sitm', ''), | ||||
'black': (False, curses.COLOR_BLACK, ''), | ||||
'red': (False, curses.COLOR_RED, ''), | ||||
'green': (False, curses.COLOR_GREEN, ''), | ||||
'yellow': (False, curses.COLOR_YELLOW, ''), | ||||
'blue': (False, curses.COLOR_BLUE, ''), | ||||
'magenta': (False, curses.COLOR_MAGENTA, ''), | ||||
'cyan': (False, curses.COLOR_CYAN, ''), | ||||
'white': (False, curses.COLOR_WHITE, ''), | ||||
} | ||||
Pierre-Yves David
|
r30968 | except ImportError: | ||
curses = None | ||||
Pierre-Yves David
|
r31113 | _baseterminfoparams = {} | ||
Pierre-Yves David
|
r30968 | |||
Pierre-Yves David
|
r31110 | # allow the extensions to change the default | ||
_enabledbydefault = False | ||||
Pierre-Yves David
|
r30967 | # start and stop parameters for effects | ||
Pierre-Yves David
|
r31109 | _effects = { | ||
'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, | ||||
'dim': 2, | ||||
'black_background': 40, | ||||
'red_background': 41, | ||||
'green_background': 42, | ||||
'yellow_background': 43, | ||||
'blue_background': 44, | ||||
'purple_background': 45, | ||||
'cyan_background': 46, | ||||
'white_background': 47, | ||||
} | ||||
Pierre-Yves David
|
r30967 | |||
Pierre-Yves David
|
r31109 | _styles = { | ||
'grep.match': 'red bold', | ||||
'grep.linenumber': 'green', | ||||
'grep.rev': 'green', | ||||
'grep.change': 'green', | ||||
'grep.sep': 'cyan', | ||||
'grep.filename': 'magenta', | ||||
'grep.user': 'magenta', | ||||
'grep.date': 'magenta', | ||||
'bookmarks.active': 'green', | ||||
'branches.active': 'none', | ||||
'branches.closed': 'black bold', | ||||
'branches.current': 'green', | ||||
'branches.inactive': 'none', | ||||
'diff.changed': 'white', | ||||
'diff.deleted': 'red', | ||||
'diff.diffline': 'bold', | ||||
'diff.extended': 'cyan bold', | ||||
'diff.file_a': 'red bold', | ||||
'diff.file_b': 'green bold', | ||||
'diff.hunk': 'magenta', | ||||
'diff.inserted': 'green', | ||||
'diff.tab': '', | ||||
'diff.trailingwhitespace': 'bold red_background', | ||||
'changeset.public' : '', | ||||
'changeset.draft' : '', | ||||
'changeset.secret' : '', | ||||
'diffstat.deleted': 'red', | ||||
'diffstat.inserted': 'green', | ||||
'histedit.remaining': 'red bold', | ||||
'ui.prompt': 'yellow', | ||||
'log.changeset': 'yellow', | ||||
'patchbomb.finalsummary': '', | ||||
'patchbomb.from': 'magenta', | ||||
'patchbomb.to': 'cyan', | ||||
'patchbomb.subject': 'green', | ||||
'patchbomb.diffstats': '', | ||||
'rebase.rebased': 'blue', | ||||
'rebase.remaining': 'red bold', | ||||
'resolve.resolved': 'green bold', | ||||
'resolve.unresolved': 'red bold', | ||||
'shelve.age': 'cyan', | ||||
'shelve.newest': 'green bold', | ||||
'shelve.name': 'blue bold', | ||||
'status.added': 'green bold', | ||||
'status.clean': 'none', | ||||
'status.copied': 'none', | ||||
'status.deleted': 'cyan bold underline', | ||||
'status.ignored': 'black bold', | ||||
'status.modified': 'blue bold', | ||||
'status.removed': 'red bold', | ||||
'status.unknown': 'magenta bold underline', | ||||
'tags.normal': 'green', | ||||
'tags.local': 'black bold', | ||||
} | ||||
Pierre-Yves David
|
r30653 | |||
def loadcolortable(ui, extname, colortable): | ||||
_styles.update(colortable) | ||||
Pierre-Yves David
|
r30969 | |||
Pierre-Yves David
|
r31100 | def _terminfosetup(ui, mode): | ||
'''Initialize terminfo data and the terminal if we're in terminfo mode.''' | ||||
# If we failed to load curses, we go ahead and return. | ||||
if curses is None: | ||||
return | ||||
# Otherwise, see what the config file says. | ||||
if mode not in ('auto', 'terminfo'): | ||||
return | ||||
Pierre-Yves David
|
r31113 | ui._terminfoparams.update(_baseterminfoparams) | ||
Pierre-Yves David
|
r31100 | |||
for key, val in ui.configitems('color'): | ||||
if key.startswith('color.'): | ||||
newval = (False, int(val), '') | ||||
Pierre-Yves David
|
r31113 | ui._terminfoparams[key[6:]] = newval | ||
Pierre-Yves David
|
r31100 | elif key.startswith('terminfo.'): | ||
newval = (True, '', val.replace('\\E', '\x1b')) | ||||
Pierre-Yves David
|
r31113 | ui._terminfoparams[key[9:]] = newval | ||
Pierre-Yves David
|
r31100 | try: | ||
curses.setupterm() | ||||
except curses.error as e: | ||||
Pierre-Yves David
|
r31113 | ui._terminfoparams.clear() | ||
Pierre-Yves David
|
r31100 | return | ||
Pierre-Yves David
|
r31113 | for key, (b, e, c) in ui._terminfoparams.items(): | ||
Pierre-Yves David
|
r31100 | if not b: | ||
continue | ||||
if not c and not curses.tigetstr(e): | ||||
# Most terminals don't support dim, invis, etc, so don't be | ||||
# noisy and use ui.debug(). | ||||
ui.debug("no terminfo entry for %s\n" % e) | ||||
Pierre-Yves David
|
r31113 | del ui._terminfoparams[key] | ||
Pierre-Yves David
|
r31100 | if not curses.tigetstr('setaf') or not curses.tigetstr('setab'): | ||
# Only warn about missing terminfo entries if we explicitly asked for | ||||
# terminfo mode. | ||||
if mode == "terminfo": | ||||
ui.warn(_("no terminfo entry for setab/setaf: reverting to " | ||||
"ECMA-48 color\n")) | ||||
Pierre-Yves David
|
r31113 | ui._terminfoparams.clear() | ||
Pierre-Yves David
|
r31100 | |||
Pierre-Yves David
|
r31110 | def setup(ui): | ||
Pierre-Yves David
|
r31105 | """configure color on a ui | ||
Pierre-Yves David
|
r31110 | That function both set the colormode for the ui object and read | ||
Pierre-Yves David
|
r31105 | the configuration looking for custom colors and effect definitions.""" | ||
Pierre-Yves David
|
r31110 | mode = _modesetup(ui) | ||
Pierre-Yves David
|
r31106 | ui._colormode = mode | ||
Pierre-Yves David
|
r31105 | if mode and mode != 'debug': | ||
configstyles(ui) | ||||
Pierre-Yves David
|
r31110 | def _modesetup(ui): | ||
Pierre-Yves David
|
r31103 | if ui.plain(): | ||
return None | ||||
Pierre-Yves David
|
r31110 | default = 'never' | ||
if _enabledbydefault: | ||||
default = 'auto' | ||||
# experimental config: ui.color | ||||
config = ui.config('ui', 'color', default) | ||||
if config == 'debug': | ||||
Pierre-Yves David
|
r31101 | return 'debug' | ||
Pierre-Yves David
|
r31110 | auto = (config == 'auto') | ||
always = not auto and util.parsebool(config) | ||||
Pierre-Yves David
|
r31101 | if not always and not auto: | ||
return None | ||||
formatted = (always or (encoding.environ.get('TERM') != 'dumb' | ||||
and ui.formatted())) | ||||
mode = ui.config('color', 'mode', 'auto') | ||||
# If pager is active, color.pagermode overrides color.mode. | ||||
if getattr(ui, 'pageractive', False): | ||||
mode = ui.config('color', 'pagermode', mode) | ||||
realmode = mode | ||||
if mode == 'auto': | ||||
if pycompat.osname == 'nt': | ||||
term = encoding.environ.get('TERM') | ||||
# TERM won't be defined in a vanilla cmd.exe environment. | ||||
# UNIX-like environments on Windows such as Cygwin and MSYS will | ||||
# set TERM. They appear to make a best effort attempt at setting it | ||||
# to something appropriate. However, not all environments with TERM | ||||
# defined support ANSI. Since "ansi" could result in terminal | ||||
# gibberish, we error on the side of selecting "win32". However, if | ||||
# w32effects is not defined, we almost certainly don't support | ||||
# "win32", so don't even try. | ||||
if (term and 'xterm' in term) or not w32effects: | ||||
realmode = 'ansi' | ||||
else: | ||||
realmode = 'win32' | ||||
else: | ||||
realmode = 'ansi' | ||||
def modewarn(): | ||||
# only warn if color.mode was explicitly set and we're in | ||||
# a formatted terminal | ||||
if mode == realmode and ui.formatted(): | ||||
ui.warn(_('warning: failed to set color mode to %s\n') % mode) | ||||
if realmode == 'win32': | ||||
Pierre-Yves David
|
r31113 | ui._terminfoparams.clear() | ||
Pierre-Yves David
|
r31101 | if not w32effects: | ||
modewarn() | ||||
return None | ||||
_effects.update(w32effects) | ||||
elif realmode == 'ansi': | ||||
Pierre-Yves David
|
r31113 | ui._terminfoparams.clear() | ||
Pierre-Yves David
|
r31101 | elif realmode == 'terminfo': | ||
_terminfosetup(ui, mode) | ||||
Pierre-Yves David
|
r31113 | if not ui._terminfoparams: | ||
Pierre-Yves David
|
r31101 | ## FIXME Shouldn't we return None in this case too? | ||
modewarn() | ||||
realmode = 'ansi' | ||||
else: | ||||
return None | ||||
if always or (auto and formatted): | ||||
return realmode | ||||
return None | ||||
Pierre-Yves David
|
r30971 | def configstyles(ui): | ||
for status, cfgeffects in ui.configitems('color'): | ||||
if '.' not in status or status.startswith(('color.', 'terminfo.')): | ||||
continue | ||||
cfgeffects = ui.configlist('color', status) | ||||
if cfgeffects: | ||||
good = [] | ||||
for e in cfgeffects: | ||||
Pierre-Yves David
|
r31112 | if valideffect(ui, e): | ||
Pierre-Yves David
|
r30971 | good.append(e) | ||
else: | ||||
ui.warn(_("ignoring unknown color/effect %r " | ||||
"(configured in color.%s)\n") | ||||
% (e, status)) | ||||
_styles[status] = ' '.join(good) | ||||
Pierre-Yves David
|
r31112 | def valideffect(ui, effect): | ||
Pierre-Yves David
|
r30969 | 'Determine if the effect is valid or not.' | ||
Pierre-Yves David
|
r31113 | return ((not ui._terminfoparams and effect in _effects) | ||
or (effect in ui._terminfoparams | ||||
or effect[:-11] in ui._terminfoparams)) | ||||
Pierre-Yves David
|
r30972 | |||
Pierre-Yves David
|
r31112 | def _effect_str(ui, effect): | ||
Pierre-Yves David
|
r30972 | '''Helper function for render_effects().''' | ||
bg = False | ||||
if effect.endswith('_background'): | ||||
bg = True | ||||
effect = effect[:-11] | ||||
try: | ||||
Pierre-Yves David
|
r31113 | attr, val, termcode = ui._terminfoparams[effect] | ||
Pierre-Yves David
|
r30972 | except KeyError: | ||
return '' | ||||
if attr: | ||||
if termcode: | ||||
return termcode | ||||
else: | ||||
return curses.tigetstr(val) | ||||
elif bg: | ||||
return curses.tparm(curses.tigetstr('setab'), val) | ||||
else: | ||||
return curses.tparm(curses.tigetstr('setaf'), val) | ||||
Pierre-Yves David
|
r30973 | |||
Pierre-Yves David
|
r31112 | def _render_effects(ui, text, effects): | ||
Pierre-Yves David
|
r30973 | 'Wrap text in commands to turn on each effect.' | ||
if not text: | ||||
return text | ||||
Pierre-Yves David
|
r31113 | if ui._terminfoparams: | ||
Pierre-Yves David
|
r31112 | start = ''.join(_effect_str(ui, effect) | ||
Pierre-Yves David
|
r31071 | for effect in ['none'] + effects.split()) | ||
Pierre-Yves David
|
r31112 | stop = _effect_str(ui, 'none') | ||
Pierre-Yves David
|
r31071 | else: | ||
Pierre-Yves David
|
r30973 | start = [str(_effects[e]) for e in ['none'] + effects.split()] | ||
start = '\033[' + ';'.join(start) + 'm' | ||||
stop = '\033[' + str(_effects['none']) + 'm' | ||||
return ''.join([start, text, stop]) | ||||
Pierre-Yves David
|
r31067 | |||
Pierre-Yves David
|
r31086 | def colorlabel(ui, msg, label): | ||
"""add color control code according to the mode""" | ||||
if ui._colormode == 'debug': | ||||
if label and msg: | ||||
if msg[-1] == '\n': | ||||
msg = "[%s|%s]\n" % (label, msg[:-1]) | ||||
else: | ||||
msg = "[%s|%s]" % (label, msg) | ||||
elif ui._colormode is not None: | ||||
effects = [] | ||||
for l in label.split(): | ||||
s = _styles.get(l, '') | ||||
if s: | ||||
effects.append(s) | ||||
Pierre-Yves David
|
r31112 | elif valideffect(ui, l): | ||
Pierre-Yves David
|
r31086 | effects.append(l) | ||
effects = ' '.join(effects) | ||||
if effects: | ||||
Pierre-Yves David
|
r31112 | msg = '\n'.join([_render_effects(ui, line, effects) | ||
Pierre-Yves David
|
r31086 | for line in msg.split('\n')]) | ||
return msg | ||||
Pierre-Yves David
|
r31067 | w32effects = None | ||
if pycompat.osname == 'nt': | ||||
import ctypes | ||||
import re | ||||
_kernel32 = ctypes.windll.kernel32 | ||||
_WORD = ctypes.c_ushort | ||||
_INVALID_HANDLE_VALUE = -1 | ||||
class _COORD(ctypes.Structure): | ||||
_fields_ = [('X', ctypes.c_short), | ||||
('Y', ctypes.c_short)] | ||||
class _SMALL_RECT(ctypes.Structure): | ||||
_fields_ = [('Left', ctypes.c_short), | ||||
('Top', ctypes.c_short), | ||||
('Right', ctypes.c_short), | ||||
('Bottom', ctypes.c_short)] | ||||
class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): | ||||
_fields_ = [('dwSize', _COORD), | ||||
('dwCursorPosition', _COORD), | ||||
('wAttributes', _WORD), | ||||
('srWindow', _SMALL_RECT), | ||||
('dwMaximumWindowSize', _COORD)] | ||||
_STD_OUTPUT_HANDLE = 0xfffffff5 # (DWORD)-11 | ||||
_STD_ERROR_HANDLE = 0xfffffff4 # (DWORD)-12 | ||||
_FOREGROUND_BLUE = 0x0001 | ||||
_FOREGROUND_GREEN = 0x0002 | ||||
_FOREGROUND_RED = 0x0004 | ||||
_FOREGROUND_INTENSITY = 0x0008 | ||||
_BACKGROUND_BLUE = 0x0010 | ||||
_BACKGROUND_GREEN = 0x0020 | ||||
_BACKGROUND_RED = 0x0040 | ||||
_BACKGROUND_INTENSITY = 0x0080 | ||||
_COMMON_LVB_REVERSE_VIDEO = 0x4000 | ||||
_COMMON_LVB_UNDERSCORE = 0x8000 | ||||
# http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx | ||||
w32effects = { | ||||
'none': -1, | ||||
'black': 0, | ||||
'red': _FOREGROUND_RED, | ||||
'green': _FOREGROUND_GREEN, | ||||
'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN, | ||||
'blue': _FOREGROUND_BLUE, | ||||
'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED, | ||||
'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN, | ||||
'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE, | ||||
'bold': _FOREGROUND_INTENSITY, | ||||
'black_background': 0x100, # unused value > 0x0f | ||||
'red_background': _BACKGROUND_RED, | ||||
'green_background': _BACKGROUND_GREEN, | ||||
'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN, | ||||
'blue_background': _BACKGROUND_BLUE, | ||||
'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED, | ||||
'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN, | ||||
'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN | | ||||
_BACKGROUND_BLUE), | ||||
'bold_background': _BACKGROUND_INTENSITY, | ||||
'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only | ||||
'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only | ||||
} | ||||
passthrough = set([_FOREGROUND_INTENSITY, | ||||
_BACKGROUND_INTENSITY, | ||||
_COMMON_LVB_UNDERSCORE, | ||||
_COMMON_LVB_REVERSE_VIDEO]) | ||||
stdout = _kernel32.GetStdHandle( | ||||
_STD_OUTPUT_HANDLE) # don't close the handle returned | ||||
if stdout is None or stdout == _INVALID_HANDLE_VALUE: | ||||
w32effects = None | ||||
else: | ||||
csbi = _CONSOLE_SCREEN_BUFFER_INFO() | ||||
if not _kernel32.GetConsoleScreenBufferInfo( | ||||
stdout, ctypes.byref(csbi)): | ||||
# stdout may not support GetConsoleScreenBufferInfo() | ||||
# when called from subprocess or redirected | ||||
w32effects = None | ||||
else: | ||||
origattr = csbi.wAttributes | ||||
ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)', | ||||
re.MULTILINE | re.DOTALL) | ||||
Pierre-Yves David
|
r31089 | def win32print(writefunc, *msgs, **opts): | ||
for text in msgs: | ||||
_win32print(text, writefunc, **opts) | ||||
def _win32print(text, writefunc, **opts): | ||||
Pierre-Yves David
|
r31067 | label = opts.get('label', '') | ||
attr = origattr | ||||
def mapcolor(val, attr): | ||||
if val == -1: | ||||
return origattr | ||||
elif val in passthrough: | ||||
return attr | val | ||||
elif val > 0x0f: | ||||
return (val & 0x70) | (attr & 0x8f) | ||||
else: | ||||
return (val & 0x07) | (attr & 0xf8) | ||||
# determine console attributes based on labels | ||||
for l in label.split(): | ||||
style = _styles.get(l, '') | ||||
for effect in style.split(): | ||||
try: | ||||
attr = mapcolor(w32effects[effect], attr) | ||||
except KeyError: | ||||
# w32effects could not have certain attributes so we skip | ||||
# them if not found | ||||
pass | ||||
# hack to ensure regexp finds data | ||||
if not text.startswith('\033['): | ||||
text = '\033[m' + text | ||||
# Look for ANSI-like codes embedded in text | ||||
m = re.match(ansire, text) | ||||
try: | ||||
while m: | ||||
for sattr in m.group(1).split(';'): | ||||
if sattr: | ||||
attr = mapcolor(int(sattr), attr) | ||||
_kernel32.SetConsoleTextAttribute(stdout, attr) | ||||
Pierre-Yves David
|
r31088 | writefunc(m.group(2), **opts) | ||
Pierre-Yves David
|
r31067 | m = re.match(ansire, m.group(3)) | ||
finally: | ||||
# Explicitly reset original attributes | ||||
_kernel32.SetConsoleTextAttribute(stdout, origattr) | ||||