[PATCH 1 of 1] color: add support for terminfo-based attributes and color

Danek Duvall duvall at comfychair.org
Sun Mar 13 19:31:41 CDT 2011


# HG changeset patch
# User Danek Duvall <duvall at comfychair.org>
# Date 1300062521 25200
# Node ID 9b439834741aeb3ffea4a65a25315ea7f69ce6f5
# Parent  1bb2a56a9d73b386378564381807fdf8df38ea3f
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
@@ -19,15 +19,17 @@
 '''colorize output from some commands
 
 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
-patch status (applied, unapplied, missing), and to diff-related
-commands to highlight additions, removals, diff headers, and trailing
-whitespace.
+output to reflect file status, the qseries command to add color to reflect patch
+status (applied, unapplied, missing), and to diff-related commands to highlight
+additions, removals, diff headers, 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
-function (aka ANSI escape codes). This module also provides the
-render_text function, which can be used to add effects to any text.
+Other effects in addition to color, like bold and underlined text, are 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).
+
+This module also provides the render_text function, which can be used to add
+effects to any text.
 
 Default effects may be overridden from your configuration file::
 
@@ -67,13 +69,33 @@ 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.
 
 '''
 
@@ -91,6 +113,35 @@ _effects = {'none': 0, 'black': 30, 'red
             'blue_background': 44, 'purple_background': 45,
             'cyan_background': 46, 'white_background': 47}
 
+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)}
+
+    # If True, use terminfo-based color; if False use ECMA-48 codes directly.
+    # Disable if we were unable to load the curses module.
+    _terminfo = True
+except ImportError:
+    _terminfo = False
+
 _styles = {'grep.match': 'red bold',
            'bookmarks.current': 'green',
            'branches.active': 'none',
@@ -121,13 +172,35 @@ _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:
+        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 +209,16 @@ 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 and e in _effects:
+                    good.append(e)
+                elif _terminfo and (e in _terminfo_params or
+                  e[:-11] in _terminfo_params):
                     good.append(e)
                 else:
                     ui.warn(_("ignoring unknown color/effect %r "
@@ -200,14 +276,18 @@ 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 _terminfo is False:
+                mode = 'ansi'
+            else:
+                mode = 'terminfo'
     if mode == 'win32':
         if w32effects is None:
             # only warn if color.mode is explicitly set to win32
             ui.warn(_('win32console not found, please install pywin32\n'))
             return
         _effects.update(w32effects)
-    elif mode != 'ansi':
+    elif mode not in ('ansi', 'terminfo'):
         return
     def colorcmd(orig, ui_, opts, cmd, cmdfunc):
         coloropt = opts['color']
@@ -232,6 +312,39 @@ def extsetup(ui):
          _("when to colorize (boolean, always, auto, or never)"),
          _('TYPE')))
 
+def _terminfosetup(ui):
+    '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
+
+    global _terminfo
+    # If we failed to load curses, we go ahead and return.
+    if _terminfo is False:
+        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.')
+    )))
+
+    _terminfo = True
+    curses.setupterm()
+    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 = False
+
 try:
     import re, pywintypes, win32console as win32c
 




More information about the Mercurial-devel mailing list