Repository: ipython/ipython

Issue:
\r and \b in qtconsole don't behave as expected
In a normal terminal, the "\r" and "\b" control characters should move to the beginning of the line and back one character, respectively.  

However, in qtconsole, writing '\b' to stdout (or "print '\b'") seems to do nothing at all, and '\r' behaves like '\n'.  Is there any way to get around this and return them to the expected behavior?  

This is useful for things like download notifiers that shouldn't print a new line to update the percentage downloaded or something like that - I'm not seeing the terminal just fill up with garbage instead of a dynamically updating download notification.



Discussion:
I'm afraid you may have re-adjust your expectations somewhat: the qtconsole is not a "normal terminal". In particular, it does not currently support any ANSI sequences for (non-forward) cursor movement, and this includes the '\r' and '\b' characters.

I have tried to emulate a real terminal when practical, but robust support for arbitrary cursor movement is a pretty serious effort. I may find some time to work on this, but don't hold your breath.

That said, you are welcome to dig into the code. Feel free to ask for guidance if you are so inclined.

Fair enough - if I find time to dig into this, I'll re-post here.  Thanks for the info!

I marked it as low priority/enhancement because as Evan said, the Qt console behaves _like_ a console when feasible, but is not an ANSI terminal emulator.  Having said that, it would be very cool to have proper support of non-forward cursor movement, so  @eteq, if you manage to have a  go at this, we'd be delighted to incorporate the improvements.

I believe there are terminal emulator libs already in python. I had tried one before , do not remember its name. It have support for all escape characters.

The point is that by necessity, the qt console is _not_ a terminal emulator: it's a widget that feels in many ways like a console, but it has inline graphics, rich html support, and many features that simply break the assumptions of a pure terminal emulator.  A terminal emulator is a very constrained device that matches the assumptions of old-style vt-100 type environments, with a character model for addressing the 2-d space with an equispaced grid of rows and columns.  Our qt console is a real GUI app with pixel-level addressing, proportional fonts, graphics, etc.  That gives it much of its power, but also imposes some limitations in that we can't simply blindly reuse terminal emulator code that wants to address an 80x25  grid of fixed-width characters.


Resolving patch:
diff --git a/IPython/frontend/qt/console/ansi_code_processor.py b/IPython/frontend/qt/console/ansi_code_processor.py
--- a/IPython/frontend/qt/console/ansi_code_processor.py
+++ b/IPython/frontend/qt/console/ansi_code_processor.py
@@ -26,12 +26,19 @@
 # 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 @@ def split_string(self, string):
         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 @@ def split_string(self, string):
 
             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
--- a/IPython/frontend/qt/console/console_widget.py
+++ b/IPython/frontend/qt/console/console_widget.py
@@ -1513,6 +1513,13 @@ def _insert_plain_text(self, cursor, text):
                         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:


Test patch:
diff --git a/IPython/frontend/qt/console/tests/test_ansi_code_processor.py b/IPython/frontend/qt/console/tests/test_ansi_code_processor.py
--- 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 @@ def test_scroll(self):
                 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 @@ def test_specials(self):
         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()