##// 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 # An action for scroll requests (SU and ST) and form feeds.
26 # An action for scroll requests (SU and ST) and form feeds.
27 ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count'])
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 # Classes
38 # Classes
31 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
32
40
33 class AnsiCodeProcessor(object):
41 class AnsiCodeProcessor(object):
34 """ Translates special ASCII characters and ANSI escape codes into readable
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 # Whether to increase intensity or set boldness for SGR code 1.
46 # Whether to increase intensity or set boldness for SGR code 1.
39 # (Different terminals handle this in different ways.)
47 # (Different terminals handle this in different ways.)
40 bold_text_enabled = False
48 bold_text_enabled = False
41
49
42 # Protected class variables.
50 # We provide an empty default color map because subclasses will likely want
43 _ansi_commands = 'ABCDEFGHJKSTfmnsu'
51 # to use a custom color format.
44 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)([%s])\x02?' % _ansi_commands)
52 default_color_map = {}
45 _special_pattern = re.compile('([\f])')
46
53
47 #---------------------------------------------------------------------------
54 #---------------------------------------------------------------------------
48 # AnsiCodeProcessor interface
55 # AnsiCodeProcessor interface
@@ -50,6 +57,7 b' class AnsiCodeProcessor(object):'
50
57
51 def __init__(self):
58 def __init__(self):
52 self.actions = []
59 self.actions = []
60 self.color_map = self.default_color_map.copy()
53 self.reset_sgr()
61 self.reset_sgr()
54
62
55 def reset_sgr(self):
63 def reset_sgr(self):
@@ -68,27 +76,32 b' class AnsiCodeProcessor(object):'
68 self.actions = []
76 self.actions = []
69 start = 0
77 start = 0
70
78
71 for match in self._ansi_pattern.finditer(string):
79 for match in ANSI_PATTERN.finditer(string):
72 raw = string[start:match.start()]
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 if substring or self.actions:
82 if substring or self.actions:
75 yield substring
83 yield substring
76 start = match.end()
84 start = match.end()
77
85
78 self.actions = []
86 self.actions = []
79 try:
87 groups = filter(lambda x: x is not None, match.groups())
80 params = []
88 params = [ param for param in groups[1].split(';') if param ]
81 for param in match.group(1).split(';'):
89 if groups[0].startswith('['):
82 if param:
90 # Case 1: CSI code.
83 params.append(int(param))
91 try:
84 except ValueError:
92 params = map(int, params)
85 # Silently discard badly formed escape codes.
93 except ValueError:
86 pass
94 # Silently discard badly formed codes.
87 else:
95 pass
88 self.set_csi_code(match.group(2), params)
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 raw = string[start:]
103 raw = string[start:]
91 substring = self._special_pattern.sub(self._replace_special, raw)
104 substring = SPECIAL_PATTERN.sub(self._replace_special, raw)
92 if substring or self.actions:
105 if substring or self.actions:
93 yield substring
106 yield substring
94
107
@@ -105,10 +118,9 b' class AnsiCodeProcessor(object):'
105 """
118 """
106 if command == 'm': # SGR - Select Graphic Rendition
119 if command == 'm': # SGR - Select Graphic Rendition
107 if params:
120 if params:
108 for code in params:
121 self.set_sgr_code(params)
109 self.set_sgr_code(code)
110 else:
122 else:
111 self.set_sgr_code(0)
123 self.set_sgr_code([0])
112
124
113 elif (command == 'J' or # ED - Erase Data
125 elif (command == 'J' or # ED - Erase Data
114 command == 'K'): # EL - Erase in Line
126 command == 'K'): # EL - Erase in Line
@@ -128,10 +140,44 b' class AnsiCodeProcessor(object):'
128 dir = 'up' if command == 'S' else 'down'
140 dir = 'up' if command == 'S' else 'down'
129 count = params[0] if params else 1
141 count = params[0] if params else 1
130 self.actions.append(ScrollAction('scroll', dir, 'line', count))
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):
147 Parameters
133 """ Set attributes based on SGR (Select Graphic Rendition) code.
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 if code == 0:
181 if code == 0:
136 self.reset_sgr()
182 self.reset_sgr()
137 elif code == 1:
183 elif code == 1:
@@ -154,17 +200,38 b' class AnsiCodeProcessor(object):'
154 self.underline = False
200 self.underline = False
155 elif code >= 30 and code <= 37:
201 elif code >= 30 and code <= 37:
156 self.foreground_color = code - 30
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 elif code == 39:
207 elif code == 39:
158 self.foreground_color = None
208 self.foreground_color = None
159 elif code >= 40 and code <= 47:
209 elif code >= 40 and code <= 47:
160 self.background_color = code - 40
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 elif code == 49:
215 elif code == 49:
162 self.background_color = None
216 self.background_color = None
163
217
218 # Recurse with unconsumed parameters.
219 self.set_sgr_code(params)
220
164 #---------------------------------------------------------------------------
221 #---------------------------------------------------------------------------
165 # Protected interface
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 def _replace_special(self, match):
235 def _replace_special(self, match):
169 special = match.group(1)
236 special = match.group(1)
170 if special == '\f':
237 if special == '\f':
@@ -176,20 +243,51 b' class QtAnsiCodeProcessor(AnsiCodeProcessor):'
176 """ Translates ANSI escape codes into QTextCharFormats.
243 """ Translates ANSI escape codes into QTextCharFormats.
177 """
244 """
178
245
179 # A map from color codes to RGB colors.
246 # A map from ANSI color codes to SVG color names or RGB(A) tuples.
180 default_map = (# Normal, Bright/Light ANSI color code
247 darkbg_color_map = {
181 ('black', 'grey'), # 0: black
248 0 : 'black', # black
182 ('darkred', 'red'), # 1: red
249 1 : 'darkred', # red
183 ('darkgreen', 'lime'), # 2: green
250 2 : 'darkgreen', # green
184 ('brown', 'yellow'), # 3: yellow
251 3 : 'brown', # yellow
185 ('darkblue', 'deepskyblue'), # 4: blue
252 4 : 'darkblue', # blue
186 ('darkviolet', 'magenta'), # 5: magenta
253 5 : 'darkviolet', # magenta
187 ('steelblue', 'cyan'), # 6: cyan
254 6 : 'steelblue', # cyan
188 ('grey', 'white')) # 7: white
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):
275 # Adjust for intensity, if possible.
191 super(QtAnsiCodeProcessor, self).__init__()
276 if color < 8 and intensity > 0:
192 self.color_map = self.default_map
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 def get_format(self):
292 def get_format(self):
195 """ Returns a QTextCharFormat that encodes the current style attributes.
293 """ Returns a QTextCharFormat that encodes the current style attributes.
@@ -197,14 +295,14 b' class QtAnsiCodeProcessor(AnsiCodeProcessor):'
197 format = QtGui.QTextCharFormat()
295 format = QtGui.QTextCharFormat()
198
296
199 # Set foreground color
297 # Set foreground color
200 if self.foreground_color is not None:
298 qcolor = self.get_color(self.foreground_color, self.intensity)
201 color = self.color_map[self.foreground_color][self.intensity]
299 if qcolor is not None:
202 format.setForeground(QtGui.QColor(color))
300 format.setForeground(qcolor)
203
301
204 # Set background color
302 # Set background color
205 if self.background_color is not None:
303 qcolor = self.get_color(self.background_color, self.intensity)
206 color = self.color_map[self.background_color][self.intensity]
304 if qcolor is not None:
207 format.setBackground(QtGui.QColor(color))
305 format.setBackground(qcolor)
208
306
209 # Set font weight/style options
307 # Set font weight/style options
210 if self.bold:
308 if self.bold:
@@ -220,14 +318,17 b' class QtAnsiCodeProcessor(AnsiCodeProcessor):'
220 """ Given a background color (a QColor), attempt to set a color map
318 """ Given a background color (a QColor), attempt to set a color map
221 that will be aesthetically pleasing.
319 that will be aesthetically pleasing.
222 """
320 """
223 if color.value() < 127:
321 # Set a new default color map.
224 # Colors appropriate for a terminal with a dark background.
322 self.default_color_map = self.darkbg_color_map.copy()
225 self.color_map = self.default_map
226
323
227 else:
324 if color.value() >= 127:
228 # Colors appropriate for a terminal with a light background. For
325 # Colors appropriate for a terminal with a light background. For
229 # now, only use non-bright colors...
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 # ...and replace white with black.
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 def setUp(self):
10 def setUp(self):
11 self.processor = AnsiCodeProcessor()
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 string = '\x1b[2J\x1b[K'
16 string = '\x1b[2J\x1b[K'
15 i = -1
17 i = -1
16 for i, substring in enumerate(self.processor.split_string(string)):
18 for i, substring in enumerate(self.processor.split_string(string)):
@@ -30,8 +32,10 b' class TestAnsiCodeProcessor(unittest.TestCase):'
30 self.fail('Too many substrings.')
32 self.fail('Too many substrings.')
31 self.assertEquals(i, 1, 'Too few substrings.')
33 self.assertEquals(i, 1, 'Too few substrings.')
32
34
33 def testColors(self):
35 def test_colors(self):
34 string = "first\x1b[34mblue\x1b[0mlast"
36 """ Do basic controls sequences for colors work?
37 """
38 string = 'first\x1b[34mblue\x1b[0mlast'
35 i = -1
39 i = -1
36 for i, substring in enumerate(self.processor.split_string(string)):
40 for i, substring in enumerate(self.processor.split_string(string)):
37 if i == 0:
41 if i == 0:
@@ -47,7 +51,24 b' class TestAnsiCodeProcessor(unittest.TestCase):'
47 self.fail('Too many substrings.')
51 self.fail('Too many substrings.')
48 self.assertEquals(i, 2, 'Too few substrings.')
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 string = '\x1b[5S\x1b[T'
72 string = '\x1b[5S\x1b[T'
52 i = -1
73 i = -1
53 for i, substring in enumerate(self.processor.split_string(string)):
74 for i, substring in enumerate(self.processor.split_string(string)):
@@ -69,7 +90,9 b' class TestAnsiCodeProcessor(unittest.TestCase):'
69 self.fail('Too many substrings.')
90 self.fail('Too many substrings.')
70 self.assertEquals(i, 1, 'Too few substrings.')
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 string = '\f' # form feed
96 string = '\f' # form feed
74 self.assertEquals(list(self.processor.split_string(string)), [''])
97 self.assertEquals(list(self.processor.split_string(string)), [''])
75 self.assertEquals(len(self.processor.actions), 1)
98 self.assertEquals(len(self.processor.actions), 1)
General Comments 0
You need to be logged in to leave comments. Login now