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 |
|
79 | for match in ANSI_PATTERN.finditer(string): | |
72 | raw = string[start:match.start()] |
|
80 | raw = string[start:match.start()] | |
73 |
substring = |
|
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 |
|
|
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 |
|
|
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 = |
|
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 |
|
|
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 |
|
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 |
|
|
248 | 0 : 'black', # black | |
182 |
|
|
249 | 1 : 'darkred', # red | |
183 |
|
|
250 | 2 : 'darkgreen', # green | |
184 |
|
|
251 | 3 : 'brown', # yellow | |
185 |
|
|
252 | 4 : 'darkblue', # blue | |
186 |
|
|
253 | 5 : 'darkviolet', # magenta | |
187 |
|
|
254 | 6 : 'steelblue', # cyan | |
188 |
|
|
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( |
|
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( |
|
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] = |
|
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 test |
|
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 test |
|
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 test |
|
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 test |
|
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