changeset 13987:e0f07847f8de

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.
author Danek Duvall <duvall@comfychair.org>
date Thu, 21 Apr 2011 13:47:45 -0700
parents 9c374cf76b7d
children 994ad067ac6e
files hgext/color.py tests/test-branches.t tests/test-diff-color.t tests/test-eolfilename.t tests/test-mq-guards.t tests/test-mq.t tests/test-status-color.t
diffstat 7 files changed, 150 insertions(+), 14 deletions(-) [+]
line wrap: on
line diff
--- a/hgext/color.py	Thu Apr 21 21:16:54 2011 +0200
+++ b/hgext/color.py	Thu Apr 21 13:47:45 2011 -0700
@@ -25,7 +25,9 @@
 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 @@
   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 @@
             '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',
@@ -121,13 +207,33 @@
            '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():
@@ -136,13 +242,15 @@
 
 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 "
@@ -192,6 +300,7 @@
 
 
 def uisetup(ui):
+    global _terminfo_params
     if ui.plain():
         return
     mode = ui.config('color', 'mode', 'auto')
@@ -200,14 +309,22 @@
             # 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']
--- a/tests/test-branches.t	Thu Apr 21 21:16:54 2011 +0200
+++ b/tests/test-branches.t	Thu Apr 21 13:47:45 2011 -0700
@@ -350,6 +350,8 @@
 
   $ 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
--- a/tests/test-diff-color.t	Thu Apr 21 21:16:54 2011 +0200
+++ b/tests/test-diff-color.t	Thu Apr 21 13:47:45 2011 -0700
@@ -1,5 +1,7 @@
 Setup
 
+  $ echo "[color]" >> $HGRCPATH
+  $ echo "mode = ansi" >> $HGRCPATH
   $ echo "[extensions]" >> $HGRCPATH
   $ echo "color=" >> $HGRCPATH
   $ hg init repo
--- a/tests/test-eolfilename.t	Thu Apr 21 21:16:54 2011 +0200
+++ b/tests/test-eolfilename.t	Thu Apr 21 13:47:45 2011 -0700
@@ -57,6 +57,8 @@
   $ 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"
--- a/tests/test-mq-guards.t	Thu Apr 21 21:16:54 2011 +0200
+++ b/tests/test-mq-guards.t	Thu Apr 21 13:47:45 2011 -0700
@@ -309,7 +309,7 @@
 
 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 @@
 
 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)
--- a/tests/test-mq.t	Thu Apr 21 21:16:54 2011 +0200
+++ b/tests/test-mq.t	Thu Apr 21 13:47:45 2011 -0700
@@ -177,7 +177,7 @@
 
 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)
--- a/tests/test-status-color.t	Thu Apr 21 21:16:54 2011 +0200
+++ b/tests/test-status-color.t	Thu Apr 21 13:47:45 2011 -0700
@@ -163,6 +163,19 @@
   \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