From 21c436b6d54ed65d8eca6c92fc29b5009128a8a4 2011-12-07 07:20:41 From: Fernando Perez Date: 2011-12-07 07:20:41 Subject: [PATCH] Merge pull request #1089 from mdboom/qtconsole-carriage-return Support carriage return ('\r') and beep ('\b') characters in the qtconsole, providing text-mode 'scroll bars' and terminal bell in the console. It extends AnsiCodeProcessor to understand the '\r' character and move the cursor back to the start of the line. It also understands the '\b' character and calls QTApplication::beep(). Neither are strictly speaking ANSI code sequences, of course, but they seem related enough and was simple enough to do it this way. Closes #629. --- diff --git a/IPython/frontend/qt/console/ansi_code_processor.py b/IPython/frontend/qt/console/ansi_code_processor.py index eeee218..f027ccf 100644 --- a/IPython/frontend/qt/console/ansi_code_processor.py +++ b/IPython/frontend/qt/console/ansi_code_processor.py @@ -26,12 +26,19 @@ MoveAction = namedtuple('MoveAction', ['action', 'dir', 'unit', 'count']) # An action for scroll requests (SU and ST) and form feeds. ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count']) +# An action for the carriage return character +CarriageReturnAction = namedtuple('CarriageReturnAction', ['action']) + +# An action for the beep character +BeepAction = namedtuple('BeepAction', ['action']) + # Regular expressions. CSI_COMMANDS = 'ABCDEFGHJKSTfmnsu' CSI_SUBPATTERN = '\[(.*?)([%s])' % CSI_COMMANDS OSC_SUBPATTERN = '\](.*?)[\x07\x1b]' -ANSI_PATTERN = re.compile('\x01?\x1b(%s|%s)\x02?' % \ - (CSI_SUBPATTERN, OSC_SUBPATTERN)) +ANSI_PATTERN = ('\x01?\x1b(%s|%s)\x02?' % \ + (CSI_SUBPATTERN, OSC_SUBPATTERN)) +ANSI_OR_SPECIAL_PATTERN = re.compile('(\b|\r)|(?:%s)' % ANSI_PATTERN) SPECIAL_PATTERN = re.compile('([\f])') #----------------------------------------------------------------------------- @@ -76,7 +83,7 @@ class AnsiCodeProcessor(object): self.actions = [] start = 0 - for match in ANSI_PATTERN.finditer(string): + for match in ANSI_OR_SPECIAL_PATTERN.finditer(string): raw = string[start:match.start()] substring = SPECIAL_PATTERN.sub(self._replace_special, raw) if substring or self.actions: @@ -85,20 +92,27 @@ class AnsiCodeProcessor(object): self.actions = [] groups = filter(lambda x: x is not None, match.groups()) - params = [ param for param in groups[1].split(';') if param ] - if groups[0].startswith('['): - # Case 1: CSI code. - try: - params = map(int, params) - except ValueError: - # Silently discard badly formed codes. - pass - else: - self.set_csi_code(groups[2], params) - - elif groups[0].startswith(']'): - # Case 2: OSC code. - self.set_osc_code(params) + if groups[0] == '\r': + self.actions.append(CarriageReturnAction('carriage-return')) + yield '' + elif groups[0] == '\b': + self.actions.append(BeepAction('beep')) + yield '' + else: + params = [ param for param in groups[1].split(';') if param ] + if groups[0].startswith('['): + # Case 1: CSI code. + try: + params = map(int, params) + except ValueError: + # Silently discard badly formed codes. + pass + else: + self.set_csi_code(groups[2], params) + + elif groups[0].startswith(']'): + # Case 2: OSC code. + self.set_osc_code(params) raw = string[start:] substring = SPECIAL_PATTERN.sub(self._replace_special, raw) diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index 5ccbe9e..2f776e9 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -1513,6 +1513,13 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): cursor.joinPreviousEditBlock() cursor.deletePreviousChar() + elif act.action == 'carriage-return': + cursor.movePosition( + cursor.StartOfLine, cursor.KeepAnchor) + + elif act.action == 'beep': + QtGui.qApp.beep() + format = self._ansi_processor.get_format() cursor.insertText(substring, format) else: 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 f71334a..0f602b1 100644 --- a/IPython/frontend/qt/console/tests/test_ansi_code_processor.py +++ b/IPython/frontend/qt/console/tests/test_ansi_code_processor.py @@ -90,8 +90,8 @@ class TestAnsiCodeProcessor(unittest.TestCase): self.fail('Too many substrings.') self.assertEquals(i, 1, 'Too few substrings.') - def test_specials(self): - """ Are special characters processed correctly? + def test_formfeed(self): + """ Are formfeed characters processed correctly? """ string = '\f' # form feed self.assertEquals(list(self.processor.split_string(string)), ['']) @@ -102,6 +102,26 @@ class TestAnsiCodeProcessor(unittest.TestCase): self.assertEquals(action.unit, 'page') self.assertEquals(action.count, 1) + def test_carriage_return(self): + """ Are carriage return characters processed correctly? + """ + string = 'foo\rbar' # form feed + self.assertEquals(list(self.processor.split_string(string)), ['foo', '', 'bar']) + self.assertEquals(len(self.processor.actions), 1) + action = self.processor.actions[0] + self.assertEquals(action.action, 'carriage-return') + self.assertEquals(action.count, 1) + + def test_beep(self): + """ Are beep characters processed correctly? + """ + string = 'foo\bbar' # form feed + self.assertEquals(list(self.processor.split_string(string)), ['foo', '', 'bar']) + self.assertEquals(len(self.processor.actions), 1) + action = self.processor.actions[0] + self.assertEquals(action.action, 'beep') + self.assertEquals(action.count, 1) + if __name__ == '__main__': unittest.main()