diff --git a/IPython/frontend/qt/console/ansi_code_processor.py b/IPython/frontend/qt/console/ansi_code_processor.py index 6365d1c..2c02e1d 100644 --- a/IPython/frontend/qt/console/ansi_code_processor.py +++ b/IPython/frontend/qt/console/ansi_code_processor.py @@ -5,6 +5,28 @@ import re from PyQt4 import QtCore, QtGui +class AnsiAction(object): + """ Represents an action requested by an ANSI escape sequence. + """ + def __init__(self, kind): + self.kind = kind + +class MoveAction(AnsiAction): + """ An AnsiAction for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, + CHA, and CUP commands). + """ + def __init__(self): + raise NotImplementedError + +class EraseAction(AnsiAction): + """ An AnsiAction for erase requests (ED and EL commands). + """ + def __init__(self, area, erase_to): + super(EraseAction, self).__init__('erase') + self.area = area + self.erase_to = erase_to + + class AnsiCodeProcessor(object): """ Translates ANSI escape codes into readable attributes. """ @@ -14,10 +36,11 @@ class AnsiCodeProcessor(object): _ansi_pattern = re.compile('\x01?\x1b\[(.*?)([%s])\x02?' % _ansi_commands) def __init__(self): - self.reset() + self.actions = [] + self.reset_sgr() - def reset(self): - """ Reset attributs to their default values. + def reset_sgr(self): + """ Reset graphics attributs to their default values. """ self.intensity = 0 self.italic = False @@ -29,19 +52,29 @@ class AnsiCodeProcessor(object): def split_string(self, string): """ Yields substrings for which the same escape code applies. """ + self.actions = [] start = 0 for match in self._ansi_pattern.finditer(string): substring = string[start:match.start()] - if substring: + if substring or self.actions: yield substring start = match.end() - params = map(int, match.group(1).split(';')) - self.set_csi_code(match.group(2), params) + self.actions = [] + try: + params = [] + for param in match.group(1).split(';'): + if param: + params.append(int(param)) + except ValueError: + # Silently discard badly formed escape codes. + pass + else: + self.set_csi_code(match.group(2), params) substring = string[start:] - if substring: + if substring or self.actions: yield substring def set_csi_code(self, command, params=[]): @@ -55,15 +88,28 @@ class AnsiCodeProcessor(object): params : sequence of integers, optional The parameter codes for the command. """ - if command == 'm': # SGR - Select Graphic Rendition + if command == 'm': # SGR - Select Graphic Rendition for code in params: self.set_sgr_code(code) + + elif (command == 'J' or # ED - Erase Data + command == 'K'): # EL - Erase in Line + code = params[0] if params else 0 + if 0 <= code <= 2: + area = 'screen' if command == 'J' else 'line' + if code == 0: + erase_to = 'end' + elif code == 1: + erase_to = 'start' + elif code == 2: + erase_to = 'all' + self.actions.append(EraseAction(area, erase_to)) def set_sgr_code(self, code): """ Set attributes based on SGR (Select Graphic Rendition) code. """ if code == 0: - self.reset() + self.reset_sgr() elif code == 1: self.intensity = 1 self.bold = True diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index d2f88a8..439b7ca 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -895,6 +895,10 @@ class ConsoleWidget(QtGui.QWidget): cursor.beginEditBlock() if self.ansi_codes: for substring in self._ansi_processor.split_string(text): + for action in self._ansi_processor.actions: + if action.kind == 'erase' and action.area == 'screen': + cursor.select(QtGui.QTextCursor.Document) + cursor.removeSelectedText() format = self._ansi_processor.get_format() cursor.insertText(substring, format) else: diff --git a/IPython/frontend/qt/console/demo.py b/IPython/frontend/qt/console/demo.py index 3d2ae7d..5461ccd 100644 --- a/IPython/frontend/qt/console/demo.py +++ b/IPython/frontend/qt/console/demo.py @@ -54,7 +54,7 @@ def main(): from ipython_widget import IPythonWidget widget = IPythonWidget() widget.kernel_manager = kernel_manager - widget.setWindowTitle('Python') + widget.setWindowTitle('Python' if namespace.pure else 'IPython') widget.show() app.exec_() diff --git a/IPython/frontend/qt/console/tests/test_ansi_code_processor.py b/IPython/frontend/qt/console/tests/test_ansi_code_processor.py index cad76dc..fabf9f0 100644 --- a/IPython/frontend/qt/console/tests/test_ansi_code_processor.py +++ b/IPython/frontend/qt/console/tests/test_ansi_code_processor.py @@ -10,6 +10,26 @@ class TestAnsiCodeProcessor(unittest.TestCase): def setUp(self): self.processor = AnsiCodeProcessor() + def testClear(self): + string = '\x1b[2J\x1b[K' + i = -1 + for i, substring in enumerate(self.processor.split_string(string)): + if i == 0: + self.assertEquals(len(self.processor.actions), 1) + action = self.processor.actions[0] + self.assertEquals(action.kind, 'erase') + self.assertEquals(action.area, 'screen') + self.assertEquals(action.erase_to, 'all') + elif i == 1: + self.assertEquals(len(self.processor.actions), 1) + action = self.processor.actions[0] + self.assertEquals(action.kind, 'erase') + self.assertEquals(action.area, 'line') + self.assertEquals(action.erase_to, 'end') + else: + self.fail('Too many substrings.') + self.assertEquals(i, 1, 'Too few substrings.') + def testColors(self): string = "first\x1b[34mblue\x1b[0mlast" i = -1 @@ -24,8 +44,8 @@ class TestAnsiCodeProcessor(unittest.TestCase): self.assertEquals(substring, 'last') self.assertEquals(self.processor.foreground_color, None) else: - self.fail("Too many substrings.") - self.assertEquals(i, 2, "Too few substrings.") + self.fail('Too many substrings.') + self.assertEquals(i, 2, 'Too few substrings.') if __name__ == '__main__':