Skip to content

Commit

Permalink
Merge pull request ipython#1089 from mdboom/qtconsole-carriage-return
Browse files Browse the repository at this point in the history
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 ipython#629.
  • Loading branch information
fperez committed Dec 7, 2011
2 parents 964bfe4 + 6a95b5d commit 21c436b
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 19 deletions.
48 changes: 31 additions & 17 deletions IPython/frontend/qt/console/ansi_code_processor.py
Expand Up @@ -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])')

#-----------------------------------------------------------------------------
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions IPython/frontend/qt/console/console_widget.py
Expand Up @@ -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:
Expand Down
24 changes: 22 additions & 2 deletions IPython/frontend/qt/console/tests/test_ansi_code_processor.py
Expand Up @@ -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)), [''])
Expand All @@ -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()

0 comments on commit 21c436b

Please sign in to comment.