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