comparison hgext/color.py @ 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 67f20625703f
children 14c7526fed89
comparison
equal deleted inserted replaced
13986:9c374cf76b7d 13987:e0f07847f8de
23 color to reflect patch status (applied, unapplied, missing), and to 23 color to reflect patch status (applied, unapplied, missing), and to
24 diff-related commands to highlight additions, removals, diff headers, 24 diff-related commands to highlight additions, removals, diff headers,
25 and trailing whitespace. 25 and trailing whitespace.
26 26
27 Other effects in addition to color, like bold and underlined text, are 27 Other effects in addition to color, like bold and underlined text, are
28 also available. Effects are rendered with the ECMA-48 SGR control 28 also available. By default, the terminfo database is used to find the
29 terminal codes used to change color and effect. If terminfo is not
30 available, then effects are rendered with the ECMA-48 SGR control
29 function (aka ANSI escape codes). 31 function (aka ANSI escape codes).
30 32
31 Default effects may be overridden from your configuration file:: 33 Default effects may be overridden from your configuration file::
32 34
33 [color] 35 [color]
64 branches.active = none 66 branches.active = none
65 branches.closed = black bold 67 branches.closed = black bold
66 branches.current = green 68 branches.current = green
67 branches.inactive = none 69 branches.inactive = none
68 70
69 The color extension will try to detect whether to use ANSI codes or 71 The available effects in terminfo mode are 'blink', 'bold', 'dim',
70 Win32 console APIs, unless it is made explicit:: 72 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
73 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
74 'underline'. How each is rendered depends on the terminal emulator.
75 Some may not be available for a given terminal type, and will be
76 silently ignored.
77
78 Because there are only eight standard colors, this module allows you
79 to define color names for other color slots which might be available
80 for your terminal type, assuming terminfo mode. For instance::
81
82 color.brightblue = 12
83 color.pink = 207
84 color.orange = 202
85
86 to set 'brightblue' to color slot 12 (useful for 16 color terminals
87 that have brighter colors defined in the upper eight) and, 'pink' and
88 'orange' to colors in 256-color xterm's default color cube. These
89 defined colors may then be used as any of the pre-defined eight,
90 including appending '_background' to set the background to that color.
91
92 The color extension will try to detect whether to use terminfo, ANSI
93 codes or Win32 console APIs, unless it is made explicit; e.g.::
71 94
72 [color] 95 [color]
73 mode = ansi 96 mode = ansi
74 97
75 Any value other than 'ansi', 'win32', or 'auto' will disable color. 98 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
99 disable color.
76 100
77 ''' 101 '''
78 102
79 import os 103 import os
80 104
87 'italic': 3, 'underline': 4, 'inverse': 7, 111 'italic': 3, 'underline': 4, 'inverse': 7,
88 'black_background': 40, 'red_background': 41, 112 'black_background': 40, 'red_background': 41,
89 'green_background': 42, 'yellow_background': 43, 113 'green_background': 42, 'yellow_background': 43,
90 'blue_background': 44, 'purple_background': 45, 114 'blue_background': 44, 'purple_background': 45,
91 'cyan_background': 46, 'white_background': 47} 115 'cyan_background': 46, 'white_background': 47}
116
117 def _terminfosetup(ui):
118 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
119
120 global _terminfo_params
121 # If we failed to load curses, we go ahead and return.
122 if not _terminfo_params:
123 return
124 # Otherwise, see what the config file says.
125 mode = ui.config('color', 'mode', 'auto')
126 if mode not in ('auto', 'terminfo'):
127 return
128
129 _terminfo_params.update(dict((
130 (key[6:], (False, int(val)))
131 for key, val in ui.configitems('color')
132 if key.startswith('color.')
133 )))
134
135 try:
136 curses.setupterm()
137 except curses.error, e:
138 _terminfo_params = {}
139 return
140
141 for key, (b, e) in _terminfo_params.items():
142 if not b:
143 continue
144 if not curses.tigetstr(e):
145 # Most terminals don't support dim, invis, etc, so don't be
146 # noisy and use ui.debug().
147 ui.debug("no terminfo entry for %s\n" % e)
148 del _terminfo_params[key]
149 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
150 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
151 "ECMA-48 color\n"))
152 _terminfo_params = {}
153
154 try:
155 import curses
156 # Mapping from effect name to terminfo attribute name or color number.
157 # This will also force-load the curses module.
158 _terminfo_params = {'none': (True, 'sgr0'),
159 'standout': (True, 'smso'),
160 'underline': (True, 'smul'),
161 'reverse': (True, 'rev'),
162 'inverse': (True, 'rev'),
163 'blink': (True, 'blink'),
164 'dim': (True, 'dim'),
165 'bold': (True, 'bold'),
166 'invisible': (True, 'invis'),
167 'italic': (True, 'sitm'),
168 'black': (False, curses.COLOR_BLACK),
169 'red': (False, curses.COLOR_RED),
170 'green': (False, curses.COLOR_GREEN),
171 'yellow': (False, curses.COLOR_YELLOW),
172 'blue': (False, curses.COLOR_BLUE),
173 'magenta': (False, curses.COLOR_MAGENTA),
174 'cyan': (False, curses.COLOR_CYAN),
175 'white': (False, curses.COLOR_WHITE)}
176 except ImportError:
177 _terminfo_params = False
92 178
93 _styles = {'grep.match': 'red bold', 179 _styles = {'grep.match': 'red bold',
94 'bookmarks.current': 'green', 180 'bookmarks.current': 'green',
95 'branches.active': 'none', 181 'branches.active': 'none',
96 'branches.closed': 'black bold', 182 'branches.closed': 'black bold',
119 'status.modified': 'blue bold', 205 'status.modified': 'blue bold',
120 'status.removed': 'red bold', 206 'status.removed': 'red bold',
121 'status.unknown': 'magenta bold underline'} 207 'status.unknown': 'magenta bold underline'}
122 208
123 209
210 def _effect_str(effect):
211 '''Helper function for render_effects().'''
212
213 bg = False
214 if effect.endswith('_background'):
215 bg = True
216 effect = effect[:-11]
217 attr, val = _terminfo_params[effect]
218 if attr:
219 return curses.tigetstr(val)
220 elif bg:
221 return curses.tparm(curses.tigetstr('setab'), val)
222 else:
223 return curses.tparm(curses.tigetstr('setaf'), val)
224
124 def render_effects(text, effects): 225 def render_effects(text, effects):
125 'Wrap text in commands to turn on each effect.' 226 'Wrap text in commands to turn on each effect.'
126 if not text: 227 if not text:
127 return text 228 return text
128 start = [str(_effects[e]) for e in ['none'] + effects.split()] 229 if not _terminfo_params:
129 start = '\033[' + ';'.join(start) + 'm' 230 start = [str(_effects[e]) for e in ['none'] + effects.split()]
130 stop = '\033[' + str(_effects['none']) + 'm' 231 start = '\033[' + ';'.join(start) + 'm'
232 stop = '\033[' + str(_effects['none']) + 'm'
233 else:
234 start = ''.join(_effect_str(effect)
235 for effect in ['none'] + effects.split())
236 stop = _effect_str('none')
131 return ''.join([start, text, stop]) 237 return ''.join([start, text, stop])
132 238
133 def extstyles(): 239 def extstyles():
134 for name, ext in extensions.extensions(): 240 for name, ext in extensions.extensions():
135 _styles.update(getattr(ext, 'colortable', {})) 241 _styles.update(getattr(ext, 'colortable', {}))
136 242
137 def configstyles(ui): 243 def configstyles(ui):
138 for status, cfgeffects in ui.configitems('color'): 244 for status, cfgeffects in ui.configitems('color'):
139 if '.' not in status: 245 if '.' not in status or status.startswith('color.'):
140 continue 246 continue
141 cfgeffects = ui.configlist('color', status) 247 cfgeffects = ui.configlist('color', status)
142 if cfgeffects: 248 if cfgeffects:
143 good = [] 249 good = []
144 for e in cfgeffects: 250 for e in cfgeffects:
145 if e in _effects: 251 if not _terminfo_params and e in _effects:
252 good.append(e)
253 elif e in _terminfo_params or e[:-11] in _terminfo_params:
146 good.append(e) 254 good.append(e)
147 else: 255 else:
148 ui.warn(_("ignoring unknown color/effect %r " 256 ui.warn(_("ignoring unknown color/effect %r "
149 "(configured in color.%s)\n") 257 "(configured in color.%s)\n")
150 % (e, status)) 258 % (e, status))
190 for s in msg.split('\n')]) 298 for s in msg.split('\n')])
191 return msg 299 return msg
192 300
193 301
194 def uisetup(ui): 302 def uisetup(ui):
303 global _terminfo_params
195 if ui.plain(): 304 if ui.plain():
196 return 305 return
197 mode = ui.config('color', 'mode', 'auto') 306 mode = ui.config('color', 'mode', 'auto')
198 if mode == 'auto': 307 if mode == 'auto':
199 if os.name == 'nt' and 'TERM' not in os.environ: 308 if os.name == 'nt' and 'TERM' not in os.environ:
200 # looks line a cmd.exe console, use win32 API or nothing 309 # looks line a cmd.exe console, use win32 API or nothing
201 mode = w32effects and 'win32' or 'none' 310 mode = w32effects and 'win32' or 'none'
202 else: 311 else:
203 mode = 'ansi' 312 _terminfosetup(ui)
313 if not _terminfo_params:
314 mode = 'ansi'
315 else:
316 mode = 'terminfo'
204 if mode == 'win32': 317 if mode == 'win32':
205 if w32effects is None: 318 if w32effects is None:
206 # only warn if color.mode is explicitly set to win32 319 # only warn if color.mode is explicitly set to win32
207 ui.warn(_('warning: failed to set color mode to %s\n') % mode) 320 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
208 return 321 return
209 _effects.update(w32effects) 322 _effects.update(w32effects)
210 elif mode != 'ansi': 323 elif mode == 'ansi':
324 _terminfo_params = {}
325 elif mode == 'terminfo':
326 _terminfosetup(ui)
327 elif mode not in ('ansi', 'terminfo'):
211 return 328 return
212 def colorcmd(orig, ui_, opts, cmd, cmdfunc): 329 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
213 coloropt = opts['color'] 330 coloropt = opts['color']
214 auto = coloropt == 'auto' 331 auto = coloropt == 'auto'
215 always = util.parsebool(coloropt) 332 always = util.parsebool(coloropt)