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