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)