##// END OF EJS Templates
* Moved AnsiCodeProcessor to separate file, refactored its API, and added unit tests....
epatters -
Show More
@@ -0,0 +1,131 b''
1 # Standard library imports
2 import re
3
4 # System library imports
5 from PyQt4 import QtCore, QtGui
6
7
8 class AnsiCodeProcessor(object):
9 """ Translates ANSI escape codes into readable attributes.
10 """
11
12 # Protected class variables.
13 _ansi_commands = 'ABCDEFGHJKSTfmnsu'
14 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)([%s])\x02?' % _ansi_commands)
15
16 def __init__(self):
17 self.reset()
18
19 def reset(self):
20 """ Reset attributs to their default values.
21 """
22 self.intensity = 0
23 self.italic = False
24 self.bold = False
25 self.underline = False
26 self.foreground_color = None
27 self.background_color = None
28
29 def split_string(self, string):
30 """ Yields substrings for which the same escape code applies.
31 """
32 start = 0
33
34 for match in self._ansi_pattern.finditer(string):
35 substring = string[start:match.start()]
36 if substring:
37 yield substring
38 start = match.end()
39
40 params = map(int, match.group(1).split(';'))
41 self.set_csi_code(match.group(2), params)
42
43 substring = string[start:]
44 if substring:
45 yield substring
46
47 def set_csi_code(self, command, params=[]):
48 """ Set attributes based on CSI (Control Sequence Introducer) code.
49
50 Parameters
51 ----------
52 command : str
53 The code identifier, i.e. the final character in the sequence.
54
55 params : sequence of integers, optional
56 The parameter codes for the command.
57 """
58 if command == 'm': # SGR - Select Graphic Rendition
59 for code in params:
60 self.set_sgr_code(code)
61
62 def set_sgr_code(self, code):
63 """ Set attributes based on SGR (Select Graphic Rendition) code.
64 """
65 if code == 0:
66 self.reset()
67 elif code == 1:
68 self.intensity = 1
69 self.bold = True
70 elif code == 2:
71 self.intensity = 0
72 elif code == 3:
73 self.italic = True
74 elif code == 4:
75 self.underline = True
76 elif code == 22:
77 self.intensity = 0
78 self.bold = False
79 elif code == 23:
80 self.italic = False
81 elif code == 24:
82 self.underline = False
83 elif code >= 30 and code <= 37:
84 self.foreground_color = code - 30
85 elif code == 39:
86 self.foreground_color = None
87 elif code >= 40 and code <= 47:
88 self.background_color = code - 40
89 elif code == 49:
90 self.background_color = None
91
92
93 class QtAnsiCodeProcessor(AnsiCodeProcessor):
94 """ Translates ANSI escape codes into QTextCharFormats.
95 """
96
97 # A map from color codes to RGB colors.
98 ansi_colors = ( # Normal, Bright/Light
99 ('#000000', '#7f7f7f'), # 0: black
100 ('#cd0000', '#ff0000'), # 1: red
101 ('#00cd00', '#00ff00'), # 2: green
102 ('#cdcd00', '#ffff00'), # 3: yellow
103 ('#0000ee', '#0000ff'), # 4: blue
104 ('#cd00cd', '#ff00ff'), # 5: magenta
105 ('#00cdcd', '#00ffff'), # 6: cyan
106 ('#e5e5e5', '#ffffff')) # 7: white
107
108 def get_format(self):
109 """ Returns a QTextCharFormat that encodes the current style attributes.
110 """
111 format = QtGui.QTextCharFormat()
112
113 # Set foreground color
114 if self.foreground_color is not None:
115 color = self.ansi_colors[self.foreground_color][self.intensity]
116 format.setForeground(QtGui.QColor(color))
117
118 # Set background color
119 if self.background_color is not None:
120 color = self.ansi_colors[self.background_color][self.intensity]
121 format.setBackground(QtGui.QColor(color))
122
123 # Set font weight/style options
124 if self.bold:
125 format.setFontWeight(QtGui.QFont.Bold)
126 else:
127 format.setFontWeight(QtGui.QFont.Normal)
128 format.setFontItalic(self.italic)
129 format.setFontUnderline(self.underline)
130
131 return format
@@ -0,0 +1,32 b''
1 # Standard library imports
2 import unittest
3
4 # Local imports
5 from IPython.frontend.qt.console.ansi_code_processor import AnsiCodeProcessor
6
7
8 class TestAnsiCodeProcessor(unittest.TestCase):
9
10 def setUp(self):
11 self.processor = AnsiCodeProcessor()
12
13 def testColors(self):
14 string = "first\x1b[34mblue\x1b[0mlast"
15 i = -1
16 for i, substring in enumerate(self.processor.split_string(string)):
17 if i == 0:
18 self.assertEquals(substring, 'first')
19 self.assertEquals(self.processor.foreground_color, None)
20 elif i == 1:
21 self.assertEquals(substring, 'blue')
22 self.assertEquals(self.processor.foreground_color, 4)
23 elif i == 2:
24 self.assertEquals(substring, 'last')
25 self.assertEquals(self.processor.foreground_color, None)
26 else:
27 self.fail("Too many substrings.")
28 self.assertEquals(i, 2, "Too few substrings.")
29
30
31 if __name__ == '__main__':
32 unittest.main()
@@ -1,99 +1,14 b''
1 1 # Standard library imports
2 import re
3 2 import sys
4 3
5 4 # System library imports
6 5 from PyQt4 import QtCore, QtGui
7 6
8 7 # Local imports
8 from ansi_code_processor import QtAnsiCodeProcessor
9 9 from completion_widget import CompletionWidget
10 10
11 11
12 class AnsiCodeProcessor(object):
13 """ Translates ANSI escape codes into readable attributes.
14 """
15
16 def __init__(self):
17 self.ansi_colors = ( # Normal, Bright/Light
18 ('#000000', '#7f7f7f'), # 0: black
19 ('#cd0000', '#ff0000'), # 1: red
20 ('#00cd00', '#00ff00'), # 2: green
21 ('#cdcd00', '#ffff00'), # 3: yellow
22 ('#0000ee', '#0000ff'), # 4: blue
23 ('#cd00cd', '#ff00ff'), # 5: magenta
24 ('#00cdcd', '#00ffff'), # 6: cyan
25 ('#e5e5e5', '#ffffff')) # 7: white
26 self.reset()
27
28 def set_code(self, code):
29 """ Set attributes based on code.
30 """
31 if code == 0:
32 self.reset()
33 elif code == 1:
34 self.intensity = 1
35 self.bold = True
36 elif code == 3:
37 self.italic = True
38 elif code == 4:
39 self.underline = True
40 elif code == 22:
41 self.intensity = 0
42 self.bold = False
43 elif code == 23:
44 self.italic = False
45 elif code == 24:
46 self.underline = False
47 elif code >= 30 and code <= 37:
48 self.foreground_color = code - 30
49 elif code == 39:
50 self.foreground_color = None
51 elif code >= 40 and code <= 47:
52 self.background_color = code - 40
53 elif code == 49:
54 self.background_color = None
55
56 def reset(self):
57 """ Reset attributs to their default values.
58 """
59 self.intensity = 0
60 self.italic = False
61 self.bold = False
62 self.underline = False
63 self.foreground_color = None
64 self.background_color = None
65
66
67 class QtAnsiCodeProcessor(AnsiCodeProcessor):
68 """ Translates ANSI escape codes into QTextCharFormats.
69 """
70
71 def get_format(self):
72 """ Returns a QTextCharFormat that encodes the current style attributes.
73 """
74 format = QtGui.QTextCharFormat()
75
76 # Set foreground color
77 if self.foreground_color is not None:
78 color = self.ansi_colors[self.foreground_color][self.intensity]
79 format.setForeground(QtGui.QColor(color))
80
81 # Set background color
82 if self.background_color is not None:
83 color = self.ansi_colors[self.background_color][self.intensity]
84 format.setBackground(QtGui.QColor(color))
85
86 # Set font weight/style options
87 if self.bold:
88 format.setFontWeight(QtGui.QFont.Bold)
89 else:
90 format.setFontWeight(QtGui.QFont.Normal)
91 format.setFontItalic(self.italic)
92 format.setFontUnderline(self.underline)
93
94 return format
95
96
97 12 class ConsoleWidget(QtGui.QPlainTextEdit):
98 13 """ Base class for console-type widgets. This class is mainly concerned with
99 14 dealing with the prompt, keeping the cursor inside the editing line, and
@@ -114,8 +29,10 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
114 29 # priority (when it has focus) over, e.g., window-level menu shortcuts.
115 30 override_shortcuts = False
116 31
32 # The number of spaces to show for a tab character.
33 tab_width = 4
34
117 35 # Protected class variables.
118 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?')
119 36 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
120 37 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
121 38 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
@@ -375,15 +292,9 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
375 292 """
376 293 cursor = self._get_end_cursor()
377 294 if self.ansi_codes:
378 format = QtGui.QTextCharFormat()
379 previous_end = 0
380 for match in self._ansi_pattern.finditer(text):
381 cursor.insertText(text[previous_end:match.start()], format)
382 previous_end = match.end()
383 for code in match.group(1).split(';'):
384 self._ansi_processor.set_code(int(code))
295 for substring in self._ansi_processor.split_string(text):
385 296 format = self._ansi_processor.get_format()
386 cursor.insertText(text[previous_end:], format)
297 cursor.insertText(substring, format)
387 298 else:
388 299 cursor.insertText(text)
389 300
@@ -531,6 +442,9 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
531 442 def _set_font(self, font):
532 443 """ Sets the base font for the ConsoleWidget to the specified QFont.
533 444 """
445 font_metrics = QtGui.QFontMetrics(font)
446 self.setTabStopWidth(self.tab_width * font_metrics.width(' '))
447
534 448 self._completion_widget.setFont(font)
535 449 self.document().setDefaultFont(font)
536 450
@@ -118,7 +118,7 b' class FrontendWidget(HistoryConsoleWidget):'
118 118 prompt created. When triggered by an Enter/Return key press,
119 119 'interactive' is True; otherwise, it is False.
120 120 """
121 complete = self._input_splitter.push(source)
121 complete = self._input_splitter.push(source.replace('\t', ' '))
122 122 if interactive:
123 123 complete = not self._input_splitter.push_accepts_more()
124 124 return complete
@@ -138,7 +138,8 b' class FrontendWidget(HistoryConsoleWidget):'
138 138 # Auto-indent if this is a continuation prompt.
139 139 if self._get_prompt_cursor().blockNumber() != \
140 140 self._get_end_cursor().blockNumber():
141 self.appendPlainText(' ' * self._input_splitter.indent_spaces)
141 spaces = self._input_splitter.indent_spaces
142 self.appendPlainText('\t' * (spaces / 4) + ' ' * (spaces % 4))
142 143
143 144 def _prompt_finished_hook(self):
144 145 """ Called immediately after a prompt is finished, i.e. when some input
@@ -153,9 +154,7 b' class FrontendWidget(HistoryConsoleWidget):'
153 154 """
154 155 self._keep_cursor_in_buffer()
155 156 cursor = self.textCursor()
156 if not self._complete():
157 cursor.insertText(' ')
158 return False
157 return not self._complete()
159 158
160 159 #---------------------------------------------------------------------------
161 160 # 'FrontendWidget' interface
@@ -341,7 +340,7 b' class FrontendWidget(HistoryConsoleWidget):'
341 340 self.appendPlainText(omsg['content']['data'])
342 341 self.moveCursor(QtGui.QTextCursor.End)
343 342
344 def _handle_execute_reply(self, rep):
343 def _handle_execute_reply(self, reply):
345 344 if self._hidden:
346 345 return
347 346
@@ -349,16 +348,20 b' class FrontendWidget(HistoryConsoleWidget):'
349 348 # before writing a new prompt.
350 349 self.kernel_manager.sub_channel.flush()
351 350
352 content = rep['content']
353 status = content['status']
351 status = reply['content']['status']
354 352 if status == 'error':
355 self.appendPlainText(content['traceback'][-1])
353 self._handle_execute_error(reply)
356 354 elif status == 'aborted':
357 355 text = "ERROR: ABORTED\n"
358 356 self.appendPlainText(text)
359 357 self._hidden = True
360 358 self._show_interpreter_prompt()
361 self.executed.emit(rep)
359 self.executed.emit(reply)
360
361 def _handle_execute_error(self, reply):
362 content = reply['content']
363 traceback = ''.join(content['traceback'])
364 self.appendPlainText(traceback)
362 365
363 366 def _handle_complete_reply(self, rep):
364 367 cursor = self.textCursor()
@@ -12,6 +12,7 b' class IPythonWidget(FrontendWidget):'
12 12
13 13 # The default stylesheet for prompts, colors, etc.
14 14 default_stylesheet = """
15 .error { color: red; }
15 16 .in-prompt { color: navy; }
16 17 .in-prompt-number { font-weight: bold; }
17 18 .out-prompt { color: darkred; }
@@ -90,6 +91,21 b' class IPythonWidget(FrontendWidget):'
90 91
91 92 #------ Signal handlers ----------------------------------------------------
92 93
94 def _handle_execute_error(self, reply):
95 """ Reimplemented for IPython-style traceback formatting.
96 """
97 content = reply['content']
98 traceback_lines = content['traceback'][:]
99 traceback = ''.join(traceback_lines)
100 traceback = traceback.replace(' ', '&nbsp;')
101 traceback = traceback.replace('\n', '<br/>')
102
103 ename = content['ename']
104 ename_styled = '<span class="error">%s</span>' % ename
105 traceback = traceback.replace(ename, ename_styled)
106
107 self.appendHtml(traceback)
108
93 109 def _handle_pyout(self, omsg):
94 110 """ Reimplemented for IPython-style "display hook".
95 111 """
@@ -268,7 +268,7 b' class Kernel(object):'
268 268 exc_content = {
269 269 u'status' : u'error',
270 270 u'traceback' : tb,
271 u'etype' : unicode(etype),
271 u'ename' : unicode(etype.__name__),
272 272 u'evalue' : unicode(evalue)
273 273 }
274 274 exc_msg = self.session.msg(u'pyerr', exc_content, parent)
General Comments 0
You need to be logged in to leave comments. Login now