##// 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 # Standard library imports
1 # Standard library imports
2 import re
3 import sys
2 import sys
4
3
5 # System library imports
4 # System library imports
6 from PyQt4 import QtCore, QtGui
5 from PyQt4 import QtCore, QtGui
7
6
8 # Local imports
7 # Local imports
8 from ansi_code_processor import QtAnsiCodeProcessor
9 from completion_widget import CompletionWidget
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 class ConsoleWidget(QtGui.QPlainTextEdit):
12 class ConsoleWidget(QtGui.QPlainTextEdit):
98 """ Base class for console-type widgets. This class is mainly concerned with
13 """ Base class for console-type widgets. This class is mainly concerned with
99 dealing with the prompt, keeping the cursor inside the editing line, and
14 dealing with the prompt, keeping the cursor inside the editing line, and
@@ -114,8 +29,10 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
114 # priority (when it has focus) over, e.g., window-level menu shortcuts.
29 # priority (when it has focus) over, e.g., window-level menu shortcuts.
115 override_shortcuts = False
30 override_shortcuts = False
116
31
32 # The number of spaces to show for a tab character.
33 tab_width = 4
34
117 # Protected class variables.
35 # Protected class variables.
118 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?')
119 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
36 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
120 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
37 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
121 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
38 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
@@ -375,15 +292,9 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
375 """
292 """
376 cursor = self._get_end_cursor()
293 cursor = self._get_end_cursor()
377 if self.ansi_codes:
294 if self.ansi_codes:
378 format = QtGui.QTextCharFormat()
295 for substring in self._ansi_processor.split_string(text):
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))
385 format = self._ansi_processor.get_format()
296 format = self._ansi_processor.get_format()
386 cursor.insertText(text[previous_end:], format)
297 cursor.insertText(substring, format)
387 else:
298 else:
388 cursor.insertText(text)
299 cursor.insertText(text)
389
300
@@ -531,6 +442,9 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
531 def _set_font(self, font):
442 def _set_font(self, font):
532 """ Sets the base font for the ConsoleWidget to the specified QFont.
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 self._completion_widget.setFont(font)
448 self._completion_widget.setFont(font)
535 self.document().setDefaultFont(font)
449 self.document().setDefaultFont(font)
536
450
@@ -118,7 +118,7 b' class FrontendWidget(HistoryConsoleWidget):'
118 prompt created. When triggered by an Enter/Return key press,
118 prompt created. When triggered by an Enter/Return key press,
119 'interactive' is True; otherwise, it is False.
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 if interactive:
122 if interactive:
123 complete = not self._input_splitter.push_accepts_more()
123 complete = not self._input_splitter.push_accepts_more()
124 return complete
124 return complete
@@ -138,7 +138,8 b' class FrontendWidget(HistoryConsoleWidget):'
138 # Auto-indent if this is a continuation prompt.
138 # Auto-indent if this is a continuation prompt.
139 if self._get_prompt_cursor().blockNumber() != \
139 if self._get_prompt_cursor().blockNumber() != \
140 self._get_end_cursor().blockNumber():
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 def _prompt_finished_hook(self):
144 def _prompt_finished_hook(self):
144 """ Called immediately after a prompt is finished, i.e. when some input
145 """ Called immediately after a prompt is finished, i.e. when some input
@@ -153,9 +154,7 b' class FrontendWidget(HistoryConsoleWidget):'
153 """
154 """
154 self._keep_cursor_in_buffer()
155 self._keep_cursor_in_buffer()
155 cursor = self.textCursor()
156 cursor = self.textCursor()
156 if not self._complete():
157 return not self._complete()
157 cursor.insertText(' ')
158 return False
159
158
160 #---------------------------------------------------------------------------
159 #---------------------------------------------------------------------------
161 # 'FrontendWidget' interface
160 # 'FrontendWidget' interface
@@ -341,7 +340,7 b' class FrontendWidget(HistoryConsoleWidget):'
341 self.appendPlainText(omsg['content']['data'])
340 self.appendPlainText(omsg['content']['data'])
342 self.moveCursor(QtGui.QTextCursor.End)
341 self.moveCursor(QtGui.QTextCursor.End)
343
342
344 def _handle_execute_reply(self, rep):
343 def _handle_execute_reply(self, reply):
345 if self._hidden:
344 if self._hidden:
346 return
345 return
347
346
@@ -349,16 +348,20 b' class FrontendWidget(HistoryConsoleWidget):'
349 # before writing a new prompt.
348 # before writing a new prompt.
350 self.kernel_manager.sub_channel.flush()
349 self.kernel_manager.sub_channel.flush()
351
350
352 content = rep['content']
351 status = reply['content']['status']
353 status = content['status']
354 if status == 'error':
352 if status == 'error':
355 self.appendPlainText(content['traceback'][-1])
353 self._handle_execute_error(reply)
356 elif status == 'aborted':
354 elif status == 'aborted':
357 text = "ERROR: ABORTED\n"
355 text = "ERROR: ABORTED\n"
358 self.appendPlainText(text)
356 self.appendPlainText(text)
359 self._hidden = True
357 self._hidden = True
360 self._show_interpreter_prompt()
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 def _handle_complete_reply(self, rep):
366 def _handle_complete_reply(self, rep):
364 cursor = self.textCursor()
367 cursor = self.textCursor()
@@ -12,6 +12,7 b' class IPythonWidget(FrontendWidget):'
12
12
13 # The default stylesheet for prompts, colors, etc.
13 # The default stylesheet for prompts, colors, etc.
14 default_stylesheet = """
14 default_stylesheet = """
15 .error { color: red; }
15 .in-prompt { color: navy; }
16 .in-prompt { color: navy; }
16 .in-prompt-number { font-weight: bold; }
17 .in-prompt-number { font-weight: bold; }
17 .out-prompt { color: darkred; }
18 .out-prompt { color: darkred; }
@@ -90,6 +91,21 b' class IPythonWidget(FrontendWidget):'
90
91
91 #------ Signal handlers ----------------------------------------------------
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 def _handle_pyout(self, omsg):
109 def _handle_pyout(self, omsg):
94 """ Reimplemented for IPython-style "display hook".
110 """ Reimplemented for IPython-style "display hook".
95 """
111 """
@@ -268,7 +268,7 b' class Kernel(object):'
268 exc_content = {
268 exc_content = {
269 u'status' : u'error',
269 u'status' : u'error',
270 u'traceback' : tb,
270 u'traceback' : tb,
271 u'etype' : unicode(etype),
271 u'ename' : unicode(etype.__name__),
272 u'evalue' : unicode(evalue)
272 u'evalue' : unicode(evalue)
273 }
273 }
274 exc_msg = self.session.msg(u'pyerr', exc_content, parent)
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