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 | 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 |
|
|
79 | for match in ANSI_PATTERN.finditer(string): | |
|
72 | 80 | raw = string[start:match.start()] |
|
73 |
substring = |
|
|
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 = [] |
|
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. | |
|
79 | 91 | try: |
|
80 |
params = |
|
|
81 | for param in match.group(1).split(';'): | |
|
82 | if param: | |
|
83 | params.append(int(param)) | |
|
92 | params = map(int, params) | |
|
84 | 93 | except ValueError: |
|
85 |
# Silently discard badly formed |
|
|
94 | # Silently discard badly formed codes. | |
|
86 | 95 | pass |
|
87 | 96 | else: |
|
88 |
self.set_csi_code( |
|
|
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 = |
|
|
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 |
|
|
|
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 |
@@ -129,9 +141,43 b' class AnsiCodeProcessor(object):' | |||
|
129 | 141 | count = params[0] if params else 1 |
|
130 | 142 | self.actions.append(ScrollAction('scroll', dir, 'line', count)) |
|
131 | 143 | |
|
132 |
def set_ |
|
|
133 |
""" Set attributes based on |
|
|
144 | def set_osc_code(self, params): | |
|
145 | """ Set attributes based on OSC (Operating System Command) parameters. | |
|
146 | ||
|
147 | Parameters | |
|
148 | ---------- | |
|
149 | params : sequence of str | |
|
150 | The parameters for the command. | |
|
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. | |
|
134 | 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 |
|
|
180 | default_map = (# Normal, Bright/Light ANSI color code | |
|
181 |
|
|
|
182 |
|
|
|
183 |
|
|
|
184 |
|
|
|
185 |
|
|
|
186 |
|
|
|
187 |
|
|
|
188 |
|
|
|
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( |
|
|
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( |
|
|
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] = |
|
|
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 test |
|
|
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 test |
|
|
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 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 | 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 test |
|
|
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