[PATCH] color: add support for terminfo-based attributes and color
Danek Duvall
duvall at comfychair.org
Thu Apr 21 16:31:53 CDT 2011
# HG changeset patch
# User Danek Duvall <duvall at comfychair.org>
# Date 1303418865 25200
# Node ID 96422b9a2becd524b252ae85c0f4034584786a2b
# Parent d943efb9701f088313de17f6c32cad22928b96c7
color: add support for terminfo-based attributes and color
Using terminfo instead of hard-coding ECMA-48 control sequences provides a
greater assurance that the terminal codes are correct for the current
terminal type; not everything supports the ANSI escape codes.
It also allows us to use a wider range of colors when a terminal emulator
supports it (such as 16- or 256-color xterm), and a few more non-color
attributes, such as the ever-popular blink.
diff --git a/hgext/color.py b/hgext/color.py
--- a/hgext/color.py
+++ b/hgext/color.py
@@ -25,7 +25,9 @@ diff-related commands to highlight addit
and trailing whitespace.
Other effects in addition to color, like bold and underlined text, are
-also available. Effects are rendered with the ECMA-48 SGR control
+also available. By default, the terminfo database is used to find the
+terminal codes used to change color and effect. If terminfo is not
+available, then effects are rendered with the ECMA-48 SGR control
function (aka ANSI escape codes).
Default effects may be overridden from your configuration file::
@@ -66,13 +68,35 @@ Default effects may be overridden from y
branches.current = green
branches.inactive = none
-The color extension will try to detect whether to use ANSI codes or
-Win32 console APIs, unless it is made explicit::
+The available effects in terminfo mode are 'blink', 'bold', 'dim',
+'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
+ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
+'underline'. How each is rendered depends on the terminal emulator.
+Some may not be available for a given terminal type, and will be
+silently ignored.
+
+Because there are only eight standard colors, this module allows you
+to define color names for other color slots which might be available
+for your terminal type, assuming terminfo mode. For instance::
+
+ color.brightblue = 12
+ color.pink = 207
+ color.orange = 202
+
+to set 'brightblue' to color slot 12 (useful for 16 color terminals
+that have brighter colors defined in the upper eight) and, 'pink' and
+'orange' to colors in 256-color xterm's default color cube. These
+defined colors may then be used as any of the pre-defined eight,
+including appending '_background' to set the background to that color.
+
+The color extension will try to detect whether to use terminfo, ANSI
+codes or Win32 console APIs, unless it is made explicit; e.g.::
[color]
mode = ansi
-Any value other than 'ansi', 'win32', or 'auto' will disable color.
+Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
+disable color.
'''
@@ -90,6 +114,68 @@ _effects = {'none': 0, 'black': 30, 'red
'blue_background': 44, 'purple_background': 45,
'cyan_background': 46, 'white_background': 47}
+def _terminfosetup(ui):
+ '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
+
+ global _terminfo_params
+ # If we failed to load curses, we go ahead and return.
+ if not _terminfo_params:
+ return
+ # Otherwise, see what the config file says.
+ mode = ui.config('color', 'mode', 'auto')
+ if mode not in ('auto', 'terminfo'):
+ return
+
+ _terminfo_params.update(dict((
+ (key[6:], (False, int(val)))
+ for key, val in ui.configitems('color')
+ if key.startswith('color.')
+ )))
+
+ try:
+ curses.setupterm()
+ except curses.error, e:
+ _terminfo_params = {}
+ return
+
+ for key, (b, e) in _terminfo_params.items():
+ if not b:
+ continue
+ if 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)
+ del _terminfo_params[key]
+ if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
+ ui.warn(_("no terminfo entry for setab/setaf: reverting to "
+ "ECMA-48 color\n"))
+ _terminfo_params = {}
+
+try:
+ import curses
+ # Mapping from effect name to terminfo attribute name or color number.
+ # This will also force-load the curses module.
+ _terminfo_params = {'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)}
+except ImportError:
+ _terminfo_params = False
+
_styles = {'grep.match': 'red bold',
'bookmarks.current': 'green',
'branches.active': 'none',
@@ -120,13 +206,33 @@ _styles = {'grep.match': 'red bold',
'status.unknown': 'magenta bold underline'}
+def _effect_str(effect):
+ '''Helper function for render_effects().'''
+
+ bg = False
+ if effect.endswith('_background'):
+ bg = True
+ effect = effect[:-11]
+ attr, val = _terminfo_params[effect]
+ if attr:
+ return curses.tigetstr(val)
+ elif bg:
+ return curses.tparm(curses.tigetstr('setab'), val)
+ else:
+ return curses.tparm(curses.tigetstr('setaf'), val)
+
def render_effects(text, effects):
'Wrap text in commands to turn on each effect.'
if not text:
return text
- start = [str(_effects[e]) for e in ['none'] + effects.split()]
- start = '\033[' + ';'.join(start) + 'm'
- stop = '\033[' + str(_effects['none']) + 'm'
+ if not _terminfo_params:
+ start = [str(_effects[e]) for e in ['none'] + effects.split()]
+ start = '\033[' + ';'.join(start) + 'm'
+ stop = '\033[' + str(_effects['none']) + 'm'
+ else:
+ start = ''.join(_effect_str(effect)
+ for effect in ['none'] + effects.split())
+ stop = _effect_str('none')
return ''.join([start, text, stop])
def extstyles():
@@ -135,13 +241,15 @@ def extstyles():
def configstyles(ui):
for status, cfgeffects in ui.configitems('color'):
- if '.' not in status:
+ if '.' not in status or status.startswith('color.'):
continue
cfgeffects = ui.configlist('color', status)
if cfgeffects:
good = []
for e in cfgeffects:
- if e in _effects:
+ if not _terminfo_params and e in _effects:
+ good.append(e)
+ elif e in _terminfo_params or e[:-11] in _terminfo_params:
good.append(e)
else:
ui.warn(_("ignoring unknown color/effect %r "
@@ -191,6 +299,7 @@ class colorui(uimod.ui):
def uisetup(ui):
+ global _terminfo_params
if ui.plain():
return
mode = ui.config('color', 'mode', 'auto')
@@ -199,14 +308,22 @@ def uisetup(ui):
# looks line a cmd.exe console, use win32 API or nothing
mode = w32effects and 'win32' or 'none'
else:
- mode = 'ansi'
+ _terminfosetup(ui)
+ if not _terminfo_params:
+ mode = 'ansi'
+ else:
+ mode = 'terminfo'
if mode == 'win32':
if w32effects is None:
# only warn if color.mode is explicitly set to win32
ui.warn(_('warning: failed to set color mode to %s\n') % mode)
return
_effects.update(w32effects)
- elif mode != 'ansi':
+ elif mode == 'ansi':
+ _terminfo_params = {}
+ elif mode == 'terminfo':
+ _terminfosetup(ui)
+ elif mode not in ('ansi', 'terminfo'):
return
def colorcmd(orig, ui_, opts, cmd, cmdfunc):
coloropt = opts['color']
diff --git a/tests/test-branches.t b/tests/test-branches.t
--- a/tests/test-branches.t
+++ b/tests/test-branches.t
@@ -350,6 +350,8 @@ default branch colors:
$ echo "[extensions]" >> $HGRCPATH
$ echo "color =" >> $HGRCPATH
+ $ echo "[color]" >> $HGRCPATH
+ $ echo "mode = ansi" >> $HGRCPATH
$ hg up -C c
3 files updated, 0 files merged, 2 files removed, 0 files unresolved
diff --git a/tests/test-diff-color.t b/tests/test-diff-color.t
--- a/tests/test-diff-color.t
+++ b/tests/test-diff-color.t
@@ -1,5 +1,7 @@
Setup
+ $ echo "[color]" >> $HGRCPATH
+ $ echo "mode = ansi" >> $HGRCPATH
$ echo "[extensions]" >> $HGRCPATH
$ echo "color=" >> $HGRCPATH
$ hg init repo
diff --git a/tests/test-eolfilename.t b/tests/test-eolfilename.t
--- a/tests/test-eolfilename.t
+++ b/tests/test-eolfilename.t
@@ -46,6 +46,8 @@ test issue2039
$ cd bar
$ echo "[extensions]" >> $HGRCPATH
$ echo "color=" >> $HGRCPATH
+ $ echo "[color]" >> $HGRCPATH
+ $ echo "mode = ansi" >> $HGRCPATH
$ A=`printf 'foo\nbar'`
$ B=`printf 'foo\nbar.baz'`
$ touch "$A"
diff --git a/tests/test-mq-guards.t b/tests/test-mq-guards.t
--- a/tests/test-mq-guards.t
+++ b/tests/test-mq-guards.t
@@ -309,7 +309,7 @@ and d.patch as Unapplied
qseries again, but with color
- $ hg --config extensions.color= qseries -v --color=always
+ $ hg --config extensions.color= --config color.mode=ansi qseries -v --color=always
0 G \x1b[0;30;1mnew.patch\x1b[0m (esc)
1 G \x1b[0;30;1mb.patch\x1b[0m (esc)
2 A \x1b[0;34;1;4mc.patch\x1b[0m (esc)
@@ -432,5 +432,5 @@ the guards file was not ignored in the p
hg qseries -m with color
- $ hg --config extensions.color= qseries -m --color=always
+ $ hg --config extensions.color= --config color.mode=ansi qseries -m --color=always
\x1b[0;31;1mb.patch\x1b[0m (esc)
diff --git a/tests/test-mq.t b/tests/test-mq.t
--- a/tests/test-mq.t
+++ b/tests/test-mq.t
@@ -177,7 +177,7 @@ add an untracked file
status --mq with color (issue2096)
- $ hg status --mq --config extensions.color= --color=always
+ $ hg status --mq --config extensions.color= --config color.mode=ansi --color=always
\x1b[0;32;1mA .hgignore\x1b[0m (esc)
\x1b[0;32;1mA A\x1b[0m (esc)
\x1b[0;32;1mA B\x1b[0m (esc)
diff --git a/tests/test-status-color.t b/tests/test-status-color.t
--- a/tests/test-status-color.t
+++ b/tests/test-status-color.t
@@ -163,6 +163,19 @@ hg status -A:
\x1b[0;0mC .hgignore\x1b[0m (esc)
\x1b[0;0mC modified\x1b[0m (esc)
+hg status -A (with terminfo color):
+
+ $ TERM=xterm hg status --config color.mode=terminfo --color=always -A
+ \x1b(B\x1b[m\x1b[32m\x1b[1mA added\x1b(B\x1b[m (esc)
+ \x1b(B\x1b[m\x1b[32m\x1b[1mA copied\x1b(B\x1b[m (esc)
+ \x1b(B\x1b[m\x1b(B\x1b[m modified\x1b(B\x1b[m (esc)
+ \x1b(B\x1b[m\x1b[31m\x1b[1mR removed\x1b(B\x1b[m (esc)
+ \x1b(B\x1b[m\x1b[36m\x1b[1m\x1b[4m! deleted\x1b(B\x1b[m (esc)
+ \x1b(B\x1b[m\x1b[35m\x1b[1m\x1b[4m? unknown\x1b(B\x1b[m (esc)
+ \x1b(B\x1b[m\x1b[30m\x1b[1mI ignored\x1b(B\x1b[m (esc)
+ \x1b(B\x1b[m\x1b(B\x1b[mC .hgignore\x1b(B\x1b[m (esc)
+ \x1b(B\x1b[m\x1b(B\x1b[mC modified\x1b(B\x1b[m (esc)
+
$ echo "^ignoreddir$" > .hgignore
$ mkdir ignoreddir
More information about the Mercurial-devel
mailing list