Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Support carriage return ('\r') and beep ('\b') characters in the qtconsole #1089

Merged
merged 3 commits into from

4 participants

@mdboom

This addresses the bug in #629.

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.

Here's a "poor man's scrollbar" function to help test this:

def scrollbar():
    import sys
    import time
    for i in range(80):
        print '\r' + ('=' * i),
        sys.stdout.flush() # required for regular console
        time.sleep(0.1)
    print '\b'
@epatters

Thanks for this! A few comments:

  1. Rather than changing the main regex and processing loop, just add '\r' and '\b' to SPECIAL_PATTERN = re.compile('([\f])'), then add a few lines here: https://github.com/ipython/ipython/blob/master/IPython/frontend/qt/console/ansi_code_processor.py#L235.

  2. Please unit test this functionality (see https://github.com/ipython/ipython/blob/master/IPython/frontend/qt/console/tests/test_ansi_code_processor.py). This is one of the few components of the Qt console that's easy to test, so let's not miss the low-hanging fruit.

Thanks again. I'm sure others will appreciate this.

@mdboom
  1. I don't think this creates the correct semantics. SPECIAL_PATTERN is not used for splitting the string, only finding characters in it. But we need to insert the carriage-return action at exactly the point in the string where it appears. I would expect:
In [1]: print 'foo\rbar'
bar

but by putting everything in SPECIAL_PATTERN, one gets:

In [1]: print 'foo\rbar'
foobar

Admittedly, for the \b character, I don't think it's as crucial.

  1. Very good point. I've added some tests that work against the current commits (using the '\r' and '\b' characters as split points).
@minrk
Owner

This seems good to go. @epatters - does the explanation satisfy you?

@epatters

Yes, thanks for the explanation @mdboom. This looks good to me.

@fperez
Owner

@epatters, many thanks for the review and feedback. This looks solid, merging now. Thanks @mdboom!!

@fperez fperez merged commit 21c436b into ipython:master
@minrk minrk referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
@minrk minrk referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
@minrk minrk referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
@minrk minrk referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
@jenshnielsen jenshnielsen referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
@jenshnielsen jenshnielsen referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
@minrk minrk referenced this pull request from a commit in minrk/ipython
@minrk minrk [qtconsole] carriage-return action matches CR only, not CRLF
CarriageReturn action introduced in #1089 clears a line in the qtconsole, which means that CRLF line endings would replace whole lines with '\n'.

This changes the regex to only match `\r` not followed by `\n` preventing the CR action from firing on CRLF.

Test included

closes #1111
814d5b9
@fperez fperez referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
@ellisonbg ellisonbg referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
@astrofrog astrofrog referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
@mattvonrocketstein mattvonrocketstein referenced this pull request from a commit in mattvonrocketstein/ipython
@minrk minrk [qtconsole] carriage-return action matches CR only, not CRLF
CarriageReturn action introduced in #1089 clears a line in the qtconsole, which means that CRLF line endings would replace whole lines with '\n'.

This changes the regex to only match `\r` not followed by `\n` preventing the CR action from firing on CRLF.

Test included

closes #1111
c061f16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
48 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)
View
7 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:
View
24 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()
Something went wrong with that request. Please try again.