Mercurial > hg
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