Show More
@@ -29,16 +29,22 b" ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count'])" | |||||
29 | # An action for the carriage return character |
|
29 | # An action for the carriage return character | |
30 | CarriageReturnAction = namedtuple('CarriageReturnAction', ['action']) |
|
30 | CarriageReturnAction = namedtuple('CarriageReturnAction', ['action']) | |
31 |
|
31 | |||
|
32 | # An action for the \n character | |||
|
33 | NewLineAction = namedtuple('NewLineAction', ['action']) | |||
|
34 | ||||
32 | # An action for the beep character |
|
35 | # An action for the beep character | |
33 | BeepAction = namedtuple('BeepAction', ['action']) |
|
36 | BeepAction = namedtuple('BeepAction', ['action']) | |
34 |
|
37 | |||
|
38 | # An action for backspace | |||
|
39 | BackSpaceAction = namedtuple('BackSpaceAction', ['action']) | |||
|
40 | ||||
35 | # Regular expressions. |
|
41 | # Regular expressions. | |
36 | CSI_COMMANDS = 'ABCDEFGHJKSTfmnsu' |
|
42 | CSI_COMMANDS = 'ABCDEFGHJKSTfmnsu' | |
37 | CSI_SUBPATTERN = '\[(.*?)([%s])' % CSI_COMMANDS |
|
43 | CSI_SUBPATTERN = '\[(.*?)([%s])' % CSI_COMMANDS | |
38 | OSC_SUBPATTERN = '\](.*?)[\x07\x1b]' |
|
44 | OSC_SUBPATTERN = '\](.*?)[\x07\x1b]' | |
39 | ANSI_PATTERN = ('\x01?\x1b(%s|%s)\x02?' % \ |
|
45 | ANSI_PATTERN = ('\x01?\x1b(%s|%s)\x02?' % \ | |
40 | (CSI_SUBPATTERN, OSC_SUBPATTERN)) |
|
46 | (CSI_SUBPATTERN, OSC_SUBPATTERN)) | |
41 | ANSI_OR_SPECIAL_PATTERN = re.compile('(\b|\r(?!\n))|(?:%s)' % ANSI_PATTERN) |
|
47 | ANSI_OR_SPECIAL_PATTERN = re.compile('(\a|\b|\r(?!\n)|\r?\n)|(?:%s)' % ANSI_PATTERN) | |
42 | SPECIAL_PATTERN = re.compile('([\f])') |
|
48 | SPECIAL_PATTERN = re.compile('([\f])') | |
43 |
|
49 | |||
44 | #----------------------------------------------------------------------------- |
|
50 | #----------------------------------------------------------------------------- | |
@@ -83,24 +89,41 b' class AnsiCodeProcessor(object):' | |||||
83 | self.actions = [] |
|
89 | self.actions = [] | |
84 | start = 0 |
|
90 | start = 0 | |
85 |
|
91 | |||
|
92 | # strings ending with \r are assumed to be ending in \r\n since | |||
|
93 | # \n is appended to output strings automatically. Accounting | |||
|
94 | # for that, here. | |||
|
95 | last_char = '\n' if len(string) > 0 and string[-1] == '\n' else None | |||
|
96 | string = string[:-1] if last_char is not None else string | |||
|
97 | ||||
86 | for match in ANSI_OR_SPECIAL_PATTERN.finditer(string): |
|
98 | for match in ANSI_OR_SPECIAL_PATTERN.finditer(string): | |
87 | raw = string[start:match.start()] |
|
99 | raw = string[start:match.start()] | |
88 | substring = SPECIAL_PATTERN.sub(self._replace_special, raw) |
|
100 | substring = SPECIAL_PATTERN.sub(self._replace_special, raw) | |
89 | if substring or self.actions: |
|
101 | if substring or self.actions: | |
90 | yield substring |
|
102 | yield substring | |
|
103 | self.actions = [] | |||
91 | start = match.end() |
|
104 | start = match.end() | |
92 |
|
105 | |||
93 | self.actions = [] |
|
|||
94 | groups = filter(lambda x: x is not None, match.groups()) |
|
106 | groups = filter(lambda x: x is not None, match.groups()) | |
95 |
|
|
107 | g0 = groups[0] | |
96 | self.actions.append(CarriageReturnAction('carriage-return')) |
|
108 | if g0 == '\a': | |
97 | yield '' |
|
|||
98 | elif groups[0] == '\b': |
|
|||
99 | self.actions.append(BeepAction('beep')) |
|
109 | self.actions.append(BeepAction('beep')) | |
100 |
yield |
|
110 | yield None | |
|
111 | self.actions = [] | |||
|
112 | elif g0 == '\r': | |||
|
113 | self.actions.append(CarriageReturnAction('carriage-return')) | |||
|
114 | yield None | |||
|
115 | self.actions = [] | |||
|
116 | elif g0 == '\b': | |||
|
117 | self.actions.append(BackSpaceAction('backspace')) | |||
|
118 | yield None | |||
|
119 | self.actions = [] | |||
|
120 | elif g0 == '\n' or g0 == '\r\n': | |||
|
121 | self.actions.append(NewLineAction('newline')) | |||
|
122 | yield g0 | |||
|
123 | self.actions = [] | |||
101 | else: |
|
124 | else: | |
102 | params = [ param for param in groups[1].split(';') if param ] |
|
125 | params = [ param for param in groups[1].split(';') if param ] | |
103 |
if g |
|
126 | if g0.startswith('['): | |
104 | # Case 1: CSI code. |
|
127 | # Case 1: CSI code. | |
105 | try: |
|
128 | try: | |
106 | params = map(int, params) |
|
129 | params = map(int, params) | |
@@ -110,7 +133,7 b' class AnsiCodeProcessor(object):' | |||||
110 | else: |
|
133 | else: | |
111 | self.set_csi_code(groups[2], params) |
|
134 | self.set_csi_code(groups[2], params) | |
112 |
|
135 | |||
113 |
elif g |
|
136 | elif g0.startswith(']'): | |
114 | # Case 2: OSC code. |
|
137 | # Case 2: OSC code. | |
115 | self.set_osc_code(params) |
|
138 | self.set_osc_code(params) | |
116 |
|
139 | |||
@@ -119,6 +142,10 b' class AnsiCodeProcessor(object):' | |||||
119 | if substring or self.actions: |
|
142 | if substring or self.actions: | |
120 | yield substring |
|
143 | yield substring | |
121 |
|
144 | |||
|
145 | if last_char is not None: | |||
|
146 | self.actions.append(NewLineAction('newline')) | |||
|
147 | yield last_char | |||
|
148 | ||||
122 | def set_csi_code(self, command, params=[]): |
|
149 | def set_csi_code(self, command, params=[]): | |
123 | """ Set attributes based on CSI (Control Sequence Introducer) code. |
|
150 | """ Set attributes based on CSI (Control Sequence Introducer) code. | |
124 |
|
151 |
@@ -1553,8 +1553,31 b' class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):' | |||||
1553 | elif act.action == 'beep': |
|
1553 | elif act.action == 'beep': | |
1554 | QtGui.qApp.beep() |
|
1554 | QtGui.qApp.beep() | |
1555 |
|
1555 | |||
|
1556 | elif act.action == 'backspace': | |||
|
1557 | if not cursor.atBlockStart(): | |||
|
1558 | cursor.movePosition( | |||
|
1559 | cursor.PreviousCharacter, cursor.KeepAnchor) | |||
|
1560 | ||||
|
1561 | elif act.action == 'newline': | |||
|
1562 | cursor.movePosition(cursor.EndOfLine) | |||
|
1563 | ||||
1556 | format = self._ansi_processor.get_format() |
|
1564 | format = self._ansi_processor.get_format() | |
1557 | cursor.insertText(substring, format) |
|
1565 | ||
|
1566 | selection = cursor.selectedText() | |||
|
1567 | if len(selection) == 0: | |||
|
1568 | cursor.insertText(substring, format) | |||
|
1569 | elif substring is not None: | |||
|
1570 | # BS and CR are treated as a change in print | |||
|
1571 | # position, rather than a backwards character | |||
|
1572 | # deletion for output equivalence with (I)Python | |||
|
1573 | # terminal. | |||
|
1574 | if len(substring) >= len(selection): | |||
|
1575 | cursor.insertText(substring, format) | |||
|
1576 | else: | |||
|
1577 | old_text = selection[len(substring):] | |||
|
1578 | cursor.insertText(substring + old_text, format) | |||
|
1579 | cursor.movePosition(cursor.PreviousCharacter, | |||
|
1580 | cursor.KeepAnchor, len(old_text)) | |||
1558 | else: |
|
1581 | else: | |
1559 | cursor.insertText(text) |
|
1582 | cursor.insertText(text) | |
1560 | cursor.endEditBlock() |
|
1583 | cursor.endEditBlock() |
@@ -106,28 +106,65 b' class TestAnsiCodeProcessor(unittest.TestCase):' | |||||
106 | """ Are carriage return characters processed correctly? |
|
106 | """ Are carriage return characters processed correctly? | |
107 | """ |
|
107 | """ | |
108 | string = 'foo\rbar' # carriage return |
|
108 | string = 'foo\rbar' # carriage return | |
109 | self.assertEquals(list(self.processor.split_string(string)), ['foo', '', 'bar']) |
|
109 | splits = [] | |
110 | self.assertEquals(len(self.processor.actions), 1) |
|
110 | actions = [] | |
111 |
|
|
111 | for split in self.processor.split_string(string): | |
112 | self.assertEquals(action.action, 'carriage-return') |
|
112 | splits.append(split) | |
|
113 | actions.append([action.action for action in self.processor.actions]) | |||
|
114 | self.assertEquals(splits, ['foo', None, 'bar']) | |||
|
115 | self.assertEquals(actions, [[], ['carriage-return'], []]) | |||
113 |
|
116 | |||
114 | def test_carriage_return_newline(self): |
|
117 | def test_carriage_return_newline(self): | |
115 | """transform CRLF to LF""" |
|
118 | """transform CRLF to LF""" | |
116 | string = 'foo\rbar\r\ncat\r\n' # carriage return and newline |
|
119 | string = 'foo\rbar\r\ncat\r\n\n' # carriage return and newline | |
117 | # only one CR action should occur, and '\r\n' should transform to '\n' |
|
120 | # only one CR action should occur, and '\r\n' should transform to '\n' | |
118 | self.assertEquals(list(self.processor.split_string(string)), ['foo', '', 'bar\r\ncat\r\n']) |
|
121 | splits = [] | |
119 | self.assertEquals(len(self.processor.actions), 1) |
|
122 | actions = [] | |
120 |
|
|
123 | for split in self.processor.split_string(string): | |
121 | self.assertEquals(action.action, 'carriage-return') |
|
124 | splits.append(split) | |
|
125 | actions.append([action.action for action in self.processor.actions]) | |||
|
126 | self.assertEquals(splits, ['foo', None, 'bar', '\r\n', 'cat', '\r\n', '\n']) | |||
|
127 | self.assertEquals(actions, [[], ['carriage-return'], [], ['newline'], [], ['newline'], ['newline']]) | |||
122 |
|
128 | |||
123 | def test_beep(self): |
|
129 | def test_beep(self): | |
124 | """ Are beep characters processed correctly? |
|
130 | """ Are beep characters processed correctly? | |
125 | """ |
|
131 | """ | |
126 |
string = 'foo\ |
|
132 | string = 'foo\abar' # bell | |
127 | self.assertEquals(list(self.processor.split_string(string)), ['foo', '', 'bar']) |
|
133 | splits = [] | |
128 | self.assertEquals(len(self.processor.actions), 1) |
|
134 | actions = [] | |
129 |
|
|
135 | for split in self.processor.split_string(string): | |
130 | self.assertEquals(action.action, 'beep') |
|
136 | splits.append(split) | |
|
137 | actions.append([action.action for action in self.processor.actions]) | |||
|
138 | self.assertEquals(splits, ['foo', None, 'bar']) | |||
|
139 | self.assertEquals(actions, [[], ['beep'], []]) | |||
|
140 | ||||
|
141 | def test_backspace(self): | |||
|
142 | """ Are backspace characters processed correctly? | |||
|
143 | """ | |||
|
144 | string = 'foo\bbar' # backspace | |||
|
145 | splits = [] | |||
|
146 | actions = [] | |||
|
147 | for split in self.processor.split_string(string): | |||
|
148 | splits.append(split) | |||
|
149 | actions.append([action.action for action in self.processor.actions]) | |||
|
150 | self.assertEquals(splits, ['foo', None, 'bar']) | |||
|
151 | self.assertEquals(actions, [[], ['backspace'], []]) | |||
|
152 | ||||
|
153 | def test_combined(self): | |||
|
154 | """ Are CR and BS characters processed correctly in combination? | |||
|
155 | ||||
|
156 | BS is treated as a change in print position, rather than a | |||
|
157 | backwards character deletion. Therefore a BS at EOL is | |||
|
158 | effectively ignored. | |||
|
159 | """ | |||
|
160 | string = 'abc\rdef\b' # CR and backspace | |||
|
161 | splits = [] | |||
|
162 | actions = [] | |||
|
163 | for split in self.processor.split_string(string): | |||
|
164 | splits.append(split) | |||
|
165 | actions.append([action.action for action in self.processor.actions]) | |||
|
166 | self.assertEquals(splits, ['abc', None, 'def', None]) | |||
|
167 | self.assertEquals(actions, [[], ['carriage-return'], [], ['backspace']]) | |||
131 |
|
168 | |||
132 |
|
169 | |||
133 | if __name__ == '__main__': |
|
170 | if __name__ == '__main__': |
General Comments 0
You need to be logged in to leave comments.
Login now