diff --git a/IPython/frontend/qt/console/ansi_code_processor.py b/IPython/frontend/qt/console/ansi_code_processor.py new file mode 100644 index 0000000..6365d1c --- /dev/null +++ b/IPython/frontend/qt/console/ansi_code_processor.py @@ -0,0 +1,131 @@ +# Standard library imports +import re + +# System library imports +from PyQt4 import QtCore, QtGui + + +class AnsiCodeProcessor(object): + """ Translates ANSI escape codes into readable attributes. + """ + + # Protected class variables. + _ansi_commands = 'ABCDEFGHJKSTfmnsu' + _ansi_pattern = re.compile('\x01?\x1b\[(.*?)([%s])\x02?' % _ansi_commands) + + def __init__(self): + self.reset() + + def reset(self): + """ Reset attributs to their default values. + """ + self.intensity = 0 + self.italic = False + self.bold = False + self.underline = False + self.foreground_color = None + self.background_color = None + + def split_string(self, string): + """ Yields substrings for which the same escape code applies. + """ + start = 0 + + for match in self._ansi_pattern.finditer(string): + substring = string[start:match.start()] + if substring: + yield substring + start = match.end() + + params = map(int, match.group(1).split(';')) + self.set_csi_code(match.group(2), params) + + substring = string[start:] + if substring: + yield substring + + def set_csi_code(self, command, params=[]): + """ Set attributes based on CSI (Control Sequence Introducer) code. + + Parameters + ---------- + command : str + The code identifier, i.e. the final character in the sequence. + + params : sequence of integers, optional + The parameter codes for the command. + """ + if command == 'm': # SGR - Select Graphic Rendition + for code in params: + self.set_sgr_code(code) + + def set_sgr_code(self, code): + """ Set attributes based on SGR (Select Graphic Rendition) code. + """ + if code == 0: + self.reset() + elif code == 1: + self.intensity = 1 + self.bold = True + elif code == 2: + self.intensity = 0 + elif code == 3: + self.italic = True + elif code == 4: + self.underline = True + elif code == 22: + self.intensity = 0 + self.bold = False + elif code == 23: + self.italic = False + elif code == 24: + self.underline = False + elif code >= 30 and code <= 37: + self.foreground_color = code - 30 + elif code == 39: + self.foreground_color = None + elif code >= 40 and code <= 47: + self.background_color = code - 40 + elif code == 49: + self.background_color = None + + +class QtAnsiCodeProcessor(AnsiCodeProcessor): + """ Translates ANSI escape codes into QTextCharFormats. + """ + + # A map from color codes to RGB colors. + ansi_colors = ( # Normal, Bright/Light + ('#000000', '#7f7f7f'), # 0: black + ('#cd0000', '#ff0000'), # 1: red + ('#00cd00', '#00ff00'), # 2: green + ('#cdcd00', '#ffff00'), # 3: yellow + ('#0000ee', '#0000ff'), # 4: blue + ('#cd00cd', '#ff00ff'), # 5: magenta + ('#00cdcd', '#00ffff'), # 6: cyan + ('#e5e5e5', '#ffffff')) # 7: white + + def get_format(self): + """ Returns a QTextCharFormat that encodes the current style attributes. + """ + format = QtGui.QTextCharFormat() + + # Set foreground color + if self.foreground_color is not None: + color = self.ansi_colors[self.foreground_color][self.intensity] + format.setForeground(QtGui.QColor(color)) + + # Set background color + if self.background_color is not None: + color = self.ansi_colors[self.background_color][self.intensity] + format.setBackground(QtGui.QColor(color)) + + # Set font weight/style options + if self.bold: + format.setFontWeight(QtGui.QFont.Bold) + else: + format.setFontWeight(QtGui.QFont.Normal) + format.setFontItalic(self.italic) + format.setFontUnderline(self.underline) + + return format diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index 907902e..f5044a6 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -1,99 +1,14 @@ # Standard library imports -import re import sys # System library imports from PyQt4 import QtCore, QtGui # Local imports +from ansi_code_processor import QtAnsiCodeProcessor from completion_widget import CompletionWidget -class AnsiCodeProcessor(object): - """ Translates ANSI escape codes into readable attributes. - """ - - def __init__(self): - self.ansi_colors = ( # Normal, Bright/Light - ('#000000', '#7f7f7f'), # 0: black - ('#cd0000', '#ff0000'), # 1: red - ('#00cd00', '#00ff00'), # 2: green - ('#cdcd00', '#ffff00'), # 3: yellow - ('#0000ee', '#0000ff'), # 4: blue - ('#cd00cd', '#ff00ff'), # 5: magenta - ('#00cdcd', '#00ffff'), # 6: cyan - ('#e5e5e5', '#ffffff')) # 7: white - self.reset() - - def set_code(self, code): - """ Set attributes based on code. - """ - if code == 0: - self.reset() - elif code == 1: - self.intensity = 1 - self.bold = True - elif code == 3: - self.italic = True - elif code == 4: - self.underline = True - elif code == 22: - self.intensity = 0 - self.bold = False - elif code == 23: - self.italic = False - elif code == 24: - self.underline = False - elif code >= 30 and code <= 37: - self.foreground_color = code - 30 - elif code == 39: - self.foreground_color = None - elif code >= 40 and code <= 47: - self.background_color = code - 40 - elif code == 49: - self.background_color = None - - def reset(self): - """ Reset attributs to their default values. - """ - self.intensity = 0 - self.italic = False - self.bold = False - self.underline = False - self.foreground_color = None - self.background_color = None - - -class QtAnsiCodeProcessor(AnsiCodeProcessor): - """ Translates ANSI escape codes into QTextCharFormats. - """ - - def get_format(self): - """ Returns a QTextCharFormat that encodes the current style attributes. - """ - format = QtGui.QTextCharFormat() - - # Set foreground color - if self.foreground_color is not None: - color = self.ansi_colors[self.foreground_color][self.intensity] - format.setForeground(QtGui.QColor(color)) - - # Set background color - if self.background_color is not None: - color = self.ansi_colors[self.background_color][self.intensity] - format.setBackground(QtGui.QColor(color)) - - # Set font weight/style options - if self.bold: - format.setFontWeight(QtGui.QFont.Bold) - else: - format.setFontWeight(QtGui.QFont.Normal) - format.setFontItalic(self.italic) - format.setFontUnderline(self.underline) - - return format - - class ConsoleWidget(QtGui.QPlainTextEdit): """ Base class for console-type widgets. This class is mainly concerned with dealing with the prompt, keeping the cursor inside the editing line, and @@ -114,8 +29,10 @@ class ConsoleWidget(QtGui.QPlainTextEdit): # priority (when it has focus) over, e.g., window-level menu shortcuts. override_shortcuts = False + # The number of spaces to show for a tab character. + tab_width = 4 + # Protected class variables. - _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?') _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left, QtCore.Qt.Key_F : QtCore.Qt.Key_Right, QtCore.Qt.Key_A : QtCore.Qt.Key_Home, @@ -375,15 +292,9 @@ class ConsoleWidget(QtGui.QPlainTextEdit): """ cursor = self._get_end_cursor() if self.ansi_codes: - format = QtGui.QTextCharFormat() - previous_end = 0 - for match in self._ansi_pattern.finditer(text): - cursor.insertText(text[previous_end:match.start()], format) - previous_end = match.end() - for code in match.group(1).split(';'): - self._ansi_processor.set_code(int(code)) + for substring in self._ansi_processor.split_string(text): format = self._ansi_processor.get_format() - cursor.insertText(text[previous_end:], format) + cursor.insertText(substring, format) else: cursor.insertText(text) @@ -531,6 +442,9 @@ class ConsoleWidget(QtGui.QPlainTextEdit): def _set_font(self, font): """ Sets the base font for the ConsoleWidget to the specified QFont. """ + font_metrics = QtGui.QFontMetrics(font) + self.setTabStopWidth(self.tab_width * font_metrics.width(' ')) + self._completion_widget.setFont(font) self.document().setDefaultFont(font) diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py index 7890f07..9736e2e 100644 --- a/IPython/frontend/qt/console/frontend_widget.py +++ b/IPython/frontend/qt/console/frontend_widget.py @@ -118,7 +118,7 @@ class FrontendWidget(HistoryConsoleWidget): prompt created. When triggered by an Enter/Return key press, 'interactive' is True; otherwise, it is False. """ - complete = self._input_splitter.push(source) + complete = self._input_splitter.push(source.replace('\t', ' ')) if interactive: complete = not self._input_splitter.push_accepts_more() return complete @@ -138,7 +138,8 @@ class FrontendWidget(HistoryConsoleWidget): # Auto-indent if this is a continuation prompt. if self._get_prompt_cursor().blockNumber() != \ self._get_end_cursor().blockNumber(): - self.appendPlainText(' ' * self._input_splitter.indent_spaces) + spaces = self._input_splitter.indent_spaces + self.appendPlainText('\t' * (spaces / 4) + ' ' * (spaces % 4)) def _prompt_finished_hook(self): """ Called immediately after a prompt is finished, i.e. when some input @@ -153,9 +154,7 @@ class FrontendWidget(HistoryConsoleWidget): """ self._keep_cursor_in_buffer() cursor = self.textCursor() - if not self._complete(): - cursor.insertText(' ') - return False + return not self._complete() #--------------------------------------------------------------------------- # 'FrontendWidget' interface @@ -341,7 +340,7 @@ class FrontendWidget(HistoryConsoleWidget): self.appendPlainText(omsg['content']['data']) self.moveCursor(QtGui.QTextCursor.End) - def _handle_execute_reply(self, rep): + def _handle_execute_reply(self, reply): if self._hidden: return @@ -349,16 +348,20 @@ class FrontendWidget(HistoryConsoleWidget): # before writing a new prompt. self.kernel_manager.sub_channel.flush() - content = rep['content'] - status = content['status'] + status = reply['content']['status'] if status == 'error': - self.appendPlainText(content['traceback'][-1]) + self._handle_execute_error(reply) elif status == 'aborted': text = "ERROR: ABORTED\n" self.appendPlainText(text) self._hidden = True self._show_interpreter_prompt() - self.executed.emit(rep) + self.executed.emit(reply) + + def _handle_execute_error(self, reply): + content = reply['content'] + traceback = ''.join(content['traceback']) + self.appendPlainText(traceback) def _handle_complete_reply(self, rep): cursor = self.textCursor() diff --git a/IPython/frontend/qt/console/ipython_widget.py b/IPython/frontend/qt/console/ipython_widget.py index 1b950b2..ec34d66 100644 --- a/IPython/frontend/qt/console/ipython_widget.py +++ b/IPython/frontend/qt/console/ipython_widget.py @@ -12,6 +12,7 @@ class IPythonWidget(FrontendWidget): # The default stylesheet for prompts, colors, etc. default_stylesheet = """ + .error { color: red; } .in-prompt { color: navy; } .in-prompt-number { font-weight: bold; } .out-prompt { color: darkred; } @@ -90,6 +91,21 @@ class IPythonWidget(FrontendWidget): #------ Signal handlers ---------------------------------------------------- + def _handle_execute_error(self, reply): + """ Reimplemented for IPython-style traceback formatting. + """ + content = reply['content'] + traceback_lines = content['traceback'][:] + traceback = ''.join(traceback_lines) + traceback = traceback.replace(' ', ' ') + traceback = traceback.replace('\n', '
') + + ename = content['ename'] + ename_styled = '%s' % ename + traceback = traceback.replace(ename, ename_styled) + + self.appendHtml(traceback) + def _handle_pyout(self, omsg): """ Reimplemented for IPython-style "display hook". """ diff --git a/IPython/frontend/qt/console/tests/test_ansi_code_processor.py b/IPython/frontend/qt/console/tests/test_ansi_code_processor.py new file mode 100644 index 0000000..cad76dc --- /dev/null +++ b/IPython/frontend/qt/console/tests/test_ansi_code_processor.py @@ -0,0 +1,32 @@ +# Standard library imports +import unittest + +# Local imports +from IPython.frontend.qt.console.ansi_code_processor import AnsiCodeProcessor + + +class TestAnsiCodeProcessor(unittest.TestCase): + + def setUp(self): + self.processor = AnsiCodeProcessor() + + def testColors(self): + string = "first\x1b[34mblue\x1b[0mlast" + i = -1 + for i, substring in enumerate(self.processor.split_string(string)): + if i == 0: + self.assertEquals(substring, 'first') + self.assertEquals(self.processor.foreground_color, None) + elif i == 1: + self.assertEquals(substring, 'blue') + self.assertEquals(self.processor.foreground_color, 4) + elif i == 2: + self.assertEquals(substring, 'last') + self.assertEquals(self.processor.foreground_color, None) + else: + self.fail("Too many substrings.") + self.assertEquals(i, 2, "Too few substrings.") + + +if __name__ == '__main__': + unittest.main() diff --git a/IPython/zmq/kernel.py b/IPython/zmq/kernel.py index 4fb4f49..2688816 100755 --- a/IPython/zmq/kernel.py +++ b/IPython/zmq/kernel.py @@ -268,7 +268,7 @@ class Kernel(object): exc_content = { u'status' : u'error', u'traceback' : tb, - u'etype' : unicode(etype), + u'ename' : unicode(etype.__name__), u'evalue' : unicode(evalue) } exc_msg = self.session.msg(u'pyerr', exc_content, parent)