##// END OF EJS Templates
Added 256-color support to Qt console escape sequence processing.
epatters -
Show More
@@ -26,23 +26,30 b" MoveAction = namedtuple('MoveAction', ['action', 'dir', 'unit', 'count'])"
26 26 # An action for scroll requests (SU and ST) and form feeds.
27 27 ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count'])
28 28
29 # Regular expressions.
30 CSI_COMMANDS = 'ABCDEFGHJKSTfmnsu'
31 CSI_SUBPATTERN = '\[(.*?)([%s])' % CSI_COMMANDS
32 OSC_SUBPATTERN = '\](.*?)[\x07\x1b]'
33 ANSI_PATTERN = re.compile('\x01?\x1b(%s|%s)\x02?' % \
34 (CSI_SUBPATTERN, OSC_SUBPATTERN))
35 SPECIAL_PATTERN = re.compile('([\f])')
36
29 37 #-----------------------------------------------------------------------------
30 38 # Classes
31 39 #-----------------------------------------------------------------------------
32 40
33 41 class AnsiCodeProcessor(object):
34 42 """ Translates special ASCII characters and ANSI escape codes into readable
35 attributes.
43 attributes. It also supports a few non-standard, xterm-specific codes.
36 44 """
37 45
38 46 # Whether to increase intensity or set boldness for SGR code 1.
39 47 # (Different terminals handle this in different ways.)
40 bold_text_enabled = False
48 bold_text_enabled = False
41 49
42 # Protected class variables.
43 _ansi_commands = 'ABCDEFGHJKSTfmnsu'
44 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)([%s])\x02?' % _ansi_commands)
45 _special_pattern = re.compile('([\f])')
50 # We provide an empty default color map because subclasses will likely want
51 # to use a custom color format.
52 default_color_map = {}
46 53
47 54 #---------------------------------------------------------------------------
48 55 # AnsiCodeProcessor interface
@@ -50,6 +57,7 b' class AnsiCodeProcessor(object):'
50 57
51 58 def __init__(self):
52 59 self.actions = []
60 self.color_map = self.default_color_map.copy()
53 61 self.reset_sgr()
54 62
55 63 def reset_sgr(self):
@@ -68,27 +76,32 b' class AnsiCodeProcessor(object):'
68 76 self.actions = []
69 77 start = 0
70 78
71 for match in self._ansi_pattern.finditer(string):
79 for match in ANSI_PATTERN.finditer(string):
72 80 raw = string[start:match.start()]
73 substring = self._special_pattern.sub(self._replace_special, raw)
81 substring = SPECIAL_PATTERN.sub(self._replace_special, raw)
74 82 if substring or self.actions:
75 83 yield substring
76 84 start = match.end()
77 85
78 86 self.actions = []
79 try:
80 params = []
81 for param in match.group(1).split(';'):
82 if param:
83 params.append(int(param))
84 except ValueError:
85 # Silently discard badly formed escape codes.
86 pass
87 else:
88 self.set_csi_code(match.group(2), params)
87 groups = filter(lambda x: x is not None, match.groups())
88 params = [ param for param in groups[1].split(';') if param ]
89 if groups[0].startswith('['):
90 # Case 1: CSI code.
91 try:
92 params = map(int, params)
93 except ValueError:
94 # Silently discard badly formed codes.
95 pass
96 else:
97 self.set_csi_code(groups[2], params)
98
99 elif groups[0].startswith(']'):
100 # Case 2: OSC code.
101 self.set_osc_code(params)
89 102
90 103 raw = string[start:]
91 substring = self._special_pattern.sub(self._replace_special, raw)
104 substring = SPECIAL_PATTERN.sub(self._replace_special, raw)
92 105 if substring or self.actions:
93 106 yield substring
94 107
@@ -105,10 +118,9 b' class AnsiCodeProcessor(object):'
105 118 """
106 119 if command == 'm': # SGR - Select Graphic Rendition
107 120 if params:
108 for code in params:
109 self.set_sgr_code(code)
121 self.set_sgr_code(params)
110 122 else:
111 self.set_sgr_code(0)
123 self.set_sgr_code([0])
112 124
113 125 elif (command == 'J' or # ED - Erase Data
114 126 command == 'K'): # EL - Erase in Line
@@ -128,10 +140,44 b' class AnsiCodeProcessor(object):'
128 140 dir = 'up' if command == 'S' else 'down'
129 141 count = params[0] if params else 1
130 142 self.actions.append(ScrollAction('scroll', dir, 'line', count))
143
144 def set_osc_code(self, params):
145 """ Set attributes based on OSC (Operating System Command) parameters.
131 146
132 def set_sgr_code(self, code):
133 """ Set attributes based on SGR (Select Graphic Rendition) code.
147 Parameters
148 ----------
149 params : sequence of str
150 The parameters for the command.
134 151 """
152 try:
153 command = int(params.pop(0))
154 except (IndexError, ValueError):
155 return
156
157 if command == 4:
158 # xterm-specific: set color number to color spec.
159 try:
160 color = int(params.pop(0))
161 spec = params.pop(0)
162 self.color_map[color] = self._parse_xterm_color_spec(spec)
163 except (IndexError, ValueError):
164 pass
165
166 def set_sgr_code(self, params):
167 """ Set attributes based on SGR (Select Graphic Rendition) codes.
168
169 Parameters
170 ----------
171 params : sequence of ints
172 A list of SGR codes for one or more SGR commands. Usually this
173 sequence will have one element per command, although certain
174 xterm-specific commands requires multiple elements.
175 """
176 # Always consume the first parameter.
177 if not params:
178 return
179 code = params.pop(0)
180
135 181 if code == 0:
136 182 self.reset_sgr()
137 183 elif code == 1:
@@ -154,17 +200,38 b' class AnsiCodeProcessor(object):'
154 200 self.underline = False
155 201 elif code >= 30 and code <= 37:
156 202 self.foreground_color = code - 30
203 elif code == 38 and params and params.pop(0) == 5:
204 # xterm-specific: 256 color support.
205 if params:
206 self.foreground_color = params.pop(0)
157 207 elif code == 39:
158 208 self.foreground_color = None
159 209 elif code >= 40 and code <= 47:
160 210 self.background_color = code - 40
211 elif code == 48 and params and params.pop(0) == 5:
212 # xterm-specific: 256 color support.
213 if params:
214 self.background_color = params.pop(0)
161 215 elif code == 49:
162 216 self.background_color = None
163 217
218 # Recurse with unconsumed parameters.
219 self.set_sgr_code(params)
220
164 221 #---------------------------------------------------------------------------
165 222 # Protected interface
166 223 #---------------------------------------------------------------------------
167 224
225 def _parse_xterm_color_spec(self, spec):
226 if spec.startswith('rgb:'):
227 return tuple(map(lambda x: int(x, 16), spec[4:].split('/')))
228 elif spec.startswith('rgbi:'):
229 return tuple(map(lambda x: int(float(x) * 255),
230 spec[5:].split('/')))
231 elif spec == '?':
232 raise ValueError('Unsupported xterm color spec')
233 return spec
234
168 235 def _replace_special(self, match):
169 236 special = match.group(1)
170 237 if special == '\f':
@@ -176,20 +243,51 b' class QtAnsiCodeProcessor(AnsiCodeProcessor):'
176 243 """ Translates ANSI escape codes into QTextCharFormats.
177 244 """
178 245
179 # A map from color codes to RGB colors.
180 default_map = (# Normal, Bright/Light ANSI color code
181 ('black', 'grey'), # 0: black
182 ('darkred', 'red'), # 1: red
183 ('darkgreen', 'lime'), # 2: green
184 ('brown', 'yellow'), # 3: yellow
185 ('darkblue', 'deepskyblue'), # 4: blue
186 ('darkviolet', 'magenta'), # 5: magenta
187 ('steelblue', 'cyan'), # 6: cyan
188 ('grey', 'white')) # 7: white
246 # A map from ANSI color codes to SVG color names or RGB(A) tuples.
247 darkbg_color_map = {
248 0 : 'black', # black
249 1 : 'darkred', # red
250 2 : 'darkgreen', # green
251 3 : 'brown', # yellow
252 4 : 'darkblue', # blue
253 5 : 'darkviolet', # magenta
254 6 : 'steelblue', # cyan
255 7 : 'grey', # white
256 8 : 'grey', # black (bright)
257 9 : 'red', # red (bright)
258 10 : 'lime', # green (bright)
259 11 : 'yellow', # yellow (bright)
260 12 : 'deepskyblue', # blue (bright)
261 13 : 'magenta', # magenta (bright)
262 14 : 'cyan', # cyan (bright)
263 15 : 'white' } # white (bright)
264
265 # Set the default color map for super class.
266 default_color_map = darkbg_color_map.copy()
267
268 def get_color(self, color, intensity=0):
269 """ Returns a QColor for a given color code, or None if one cannot be
270 constructed.
271 """
272 if color is None:
273 return None
189 274
190 def __init__(self):
191 super(QtAnsiCodeProcessor, self).__init__()
192 self.color_map = self.default_map
275 # Adjust for intensity, if possible.
276 if color < 8 and intensity > 0:
277 color += 8
278
279 constructor = self.color_map.get(color, None)
280 if isinstance(constructor, basestring):
281 # If this is an X11 color name, we just hope there is a close SVG
282 # color name. We could use QColor's static method
283 # 'setAllowX11ColorNames()', but this is global and only available
284 # on X11. It seems cleaner to aim for uniformity of behavior.
285 return QtGui.QColor(constructor)
286
287 elif isinstance(constructor, (tuple, list)):
288 return QtGui.QColor(*constructor)
289
290 return None
193 291
194 292 def get_format(self):
195 293 """ Returns a QTextCharFormat that encodes the current style attributes.
@@ -197,14 +295,14 b' class QtAnsiCodeProcessor(AnsiCodeProcessor):'
197 295 format = QtGui.QTextCharFormat()
198 296
199 297 # Set foreground color
200 if self.foreground_color is not None:
201 color = self.color_map[self.foreground_color][self.intensity]
202 format.setForeground(QtGui.QColor(color))
298 qcolor = self.get_color(self.foreground_color, self.intensity)
299 if qcolor is not None:
300 format.setForeground(qcolor)
203 301
204 302 # Set background color
205 if self.background_color is not None:
206 color = self.color_map[self.background_color][self.intensity]
207 format.setBackground(QtGui.QColor(color))
303 qcolor = self.get_color(self.background_color, self.intensity)
304 if qcolor is not None:
305 format.setBackground(qcolor)
208 306
209 307 # Set font weight/style options
210 308 if self.bold:
@@ -220,14 +318,17 b' class QtAnsiCodeProcessor(AnsiCodeProcessor):'
220 318 """ Given a background color (a QColor), attempt to set a color map
221 319 that will be aesthetically pleasing.
222 320 """
223 if color.value() < 127:
224 # Colors appropriate for a terminal with a dark background.
225 self.color_map = self.default_map
321 # Set a new default color map.
322 self.default_color_map = self.darkbg_color_map.copy()
226 323
227 else:
324 if color.value() >= 127:
228 325 # Colors appropriate for a terminal with a light background. For
229 326 # now, only use non-bright colors...
230 self.color_map = [ (pair[0], pair[0]) for pair in self.default_map ]
327 for i in xrange(8):
328 self.default_color_map[i + 8] = self.default_color_map[i]
231 329
232 330 # ...and replace white with black.
233 self.color_map[7] = ('black', 'black')
331 self.default_color_map[7] = self.default_color_map[15] = 'black'
332
333 # Update the current color map with the new defaults.
334 self.color_map.update(self.default_color_map)
@@ -10,7 +10,9 b' class TestAnsiCodeProcessor(unittest.TestCase):'
10 10 def setUp(self):
11 11 self.processor = AnsiCodeProcessor()
12 12
13 def testClear(self):
13 def test_clear(self):
14 """ Do control sequences for clearing the console work?
15 """
14 16 string = '\x1b[2J\x1b[K'
15 17 i = -1
16 18 for i, substring in enumerate(self.processor.split_string(string)):
@@ -30,8 +32,10 b' class TestAnsiCodeProcessor(unittest.TestCase):'
30 32 self.fail('Too many substrings.')
31 33 self.assertEquals(i, 1, 'Too few substrings.')
32 34
33 def testColors(self):
34 string = "first\x1b[34mblue\x1b[0mlast"
35 def test_colors(self):
36 """ Do basic controls sequences for colors work?
37 """
38 string = 'first\x1b[34mblue\x1b[0mlast'
35 39 i = -1
36 40 for i, substring in enumerate(self.processor.split_string(string)):
37 41 if i == 0:
@@ -47,7 +51,24 b' class TestAnsiCodeProcessor(unittest.TestCase):'
47 51 self.fail('Too many substrings.')
48 52 self.assertEquals(i, 2, 'Too few substrings.')
49 53
50 def testScroll(self):
54 def test_colors_xterm(self):
55 """ Do xterm-specific control sequences for colors work?
56 """
57 string = '\x1b]4;20;rgb:ff/ff/ff\x1b' \
58 '\x1b]4;25;rgbi:1.0/1.0/1.0\x1b'
59 substrings = list(self.processor.split_string(string))
60 desired = { 20 : (255, 255, 255),
61 25 : (255, 255, 255) }
62 self.assertEquals(self.processor.color_map, desired)
63
64 string = '\x1b[38;5;20m\x1b[48;5;25m'
65 substrings = list(self.processor.split_string(string))
66 self.assertEquals(self.processor.foreground_color, 20)
67 self.assertEquals(self.processor.background_color, 25)
68
69 def test_scroll(self):
70 """ Do control sequences for scrolling the buffer work?
71 """
51 72 string = '\x1b[5S\x1b[T'
52 73 i = -1
53 74 for i, substring in enumerate(self.processor.split_string(string)):
@@ -69,7 +90,9 b' class TestAnsiCodeProcessor(unittest.TestCase):'
69 90 self.fail('Too many substrings.')
70 91 self.assertEquals(i, 1, 'Too few substrings.')
71 92
72 def testSpecials(self):
93 def test_specials(self):
94 """ Are special characters processed correctly?
95 """
73 96 string = '\f' # form feed
74 97 self.assertEquals(list(self.processor.split_string(string)), [''])
75 98 self.assertEquals(len(self.processor.actions), 1)
General Comments 0
You need to be logged in to leave comments. Login now