Show More
@@ -1,34 +1,38 b'' | |||||
|
1 | """ Utilities for processing ANSI escape codes and special ASCII characters. | |||
|
2 | """ | |||
|
3 | #----------------------------------------------------------------------------- | |||
|
4 | # Imports | |||
|
5 | #----------------------------------------------------------------------------- | |||
|
6 | ||||
1 | # Standard library imports |
|
7 | # Standard library imports | |
|
8 | from collections import namedtuple | |||
2 | import re |
|
9 | import re | |
3 |
|
10 | |||
4 | # System library imports |
|
11 | # System library imports | |
5 | from PyQt4 import QtCore, QtGui |
|
12 | from PyQt4 import QtCore, QtGui | |
6 |
|
13 | |||
|
14 | #----------------------------------------------------------------------------- | |||
|
15 | # Constants and datatypes | |||
|
16 | #----------------------------------------------------------------------------- | |||
7 |
|
17 | |||
8 | class AnsiAction(object): |
|
18 | # An action for erase requests (ED and EL commands). | |
9 | """ Represents an action requested by an ANSI escape sequence. |
|
19 | EraseAction = namedtuple('EraseAction', ['action', 'area', 'erase_to']) | |
10 | """ |
|
|||
11 | def __init__(self, kind): |
|
|||
12 | self.kind = kind |
|
|||
13 |
|
20 | |||
14 | class MoveAction(AnsiAction): |
|
21 | # An action for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, CHA, CUP, | |
15 | """ An AnsiAction for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, |
|
22 | # and HVP commands). | |
16 | CHA, and CUP commands). |
|
23 | # FIXME: Not implemented in AnsiCodeProcessor. | |
17 | """ |
|
24 | MoveAction = namedtuple('MoveAction', ['action', 'dir', 'unit', 'count']) | |
18 | def __init__(self): |
|
|||
19 | raise NotImplementedError |
|
|||
20 |
|
25 | |||
21 | class EraseAction(AnsiAction): |
|
26 | # An action for scroll requests (SU and ST) and form feeds. | |
22 | """ An AnsiAction for erase requests (ED and EL commands). |
|
27 | ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count']) | |
23 | """ |
|
|||
24 | def __init__(self, area, erase_to): |
|
|||
25 | super(EraseAction, self).__init__('erase') |
|
|||
26 | self.area = area |
|
|||
27 | self.erase_to = erase_to |
|
|||
28 |
|
28 | |||
|
29 | #----------------------------------------------------------------------------- | |||
|
30 | # Classes | |||
|
31 | #----------------------------------------------------------------------------- | |||
29 |
|
32 | |||
30 | class AnsiCodeProcessor(object): |
|
33 | class AnsiCodeProcessor(object): | |
31 |
""" Translates ANSI escape codes into readable |
|
34 | """ Translates special ASCII characters and ANSI escape codes into readable | |
|
35 | attributes. | |||
32 | """ |
|
36 | """ | |
33 |
|
37 | |||
34 | # Whether to increase intensity or set boldness for SGR code 1. |
|
38 | # Whether to increase intensity or set boldness for SGR code 1. | |
@@ -38,6 +42,11 b' class AnsiCodeProcessor(object):' | |||||
38 | # Protected class variables. |
|
42 | # Protected class variables. | |
39 | _ansi_commands = 'ABCDEFGHJKSTfmnsu' |
|
43 | _ansi_commands = 'ABCDEFGHJKSTfmnsu' | |
40 | _ansi_pattern = re.compile('\x01?\x1b\[(.*?)([%s])\x02?' % _ansi_commands) |
|
44 | _ansi_pattern = re.compile('\x01?\x1b\[(.*?)([%s])\x02?' % _ansi_commands) | |
|
45 | _special_pattern = re.compile('([\f])') | |||
|
46 | ||||
|
47 | #--------------------------------------------------------------------------- | |||
|
48 | # AnsiCodeProcessor interface | |||
|
49 | #--------------------------------------------------------------------------- | |||
41 |
|
50 | |||
42 | def __init__(self): |
|
51 | def __init__(self): | |
43 | self.actions = [] |
|
52 | self.actions = [] | |
@@ -60,7 +69,8 b' class AnsiCodeProcessor(object):' | |||||
60 | start = 0 |
|
69 | start = 0 | |
61 |
|
70 | |||
62 | for match in self._ansi_pattern.finditer(string): |
|
71 | for match in self._ansi_pattern.finditer(string): | |
63 |
|
|
72 | raw = string[start:match.start()] | |
|
73 | substring = self._special_pattern.sub(self._replace_special, raw) | |||
64 | if substring or self.actions: |
|
74 | if substring or self.actions: | |
65 | yield substring |
|
75 | yield substring | |
66 | start = match.end() |
|
76 | start = match.end() | |
@@ -77,7 +87,8 b' class AnsiCodeProcessor(object):' | |||||
77 | else: |
|
87 | else: | |
78 | self.set_csi_code(match.group(2), params) |
|
88 | self.set_csi_code(match.group(2), params) | |
79 |
|
89 | |||
80 |
|
|
90 | raw = string[start:] | |
|
91 | substring = self._special_pattern.sub(self._replace_special, raw) | |||
81 | if substring or self.actions: |
|
92 | if substring or self.actions: | |
82 | yield substring |
|
93 | yield substring | |
83 |
|
94 | |||
@@ -107,7 +118,13 b' class AnsiCodeProcessor(object):' | |||||
107 | erase_to = 'start' |
|
118 | erase_to = 'start' | |
108 | elif code == 2: |
|
119 | elif code == 2: | |
109 | erase_to = 'all' |
|
120 | erase_to = 'all' | |
110 | self.actions.append(EraseAction(area, erase_to)) |
|
121 | self.actions.append(EraseAction('erase', area, erase_to)) | |
|
122 | ||||
|
123 | elif (command == 'S' or # SU - Scroll Up | |||
|
124 | command == 'T'): # SD - Scroll Down | |||
|
125 | dir = 'up' if command == 'S' else 'down' | |||
|
126 | count = params[0] if params else 1 | |||
|
127 | self.actions.append(ScrollAction('scroll', dir, 'line', count)) | |||
111 |
|
128 | |||
112 | def set_sgr_code(self, code): |
|
129 | def set_sgr_code(self, code): | |
113 | """ Set attributes based on SGR (Select Graphic Rendition) code. |
|
130 | """ Set attributes based on SGR (Select Graphic Rendition) code. | |
@@ -140,6 +157,16 b' class AnsiCodeProcessor(object):' | |||||
140 | self.background_color = code - 40 |
|
157 | self.background_color = code - 40 | |
141 | elif code == 49: |
|
158 | elif code == 49: | |
142 | self.background_color = None |
|
159 | self.background_color = None | |
|
160 | ||||
|
161 | #--------------------------------------------------------------------------- | |||
|
162 | # Protected interface | |||
|
163 | #--------------------------------------------------------------------------- | |||
|
164 | ||||
|
165 | def _replace_special(self, match): | |||
|
166 | special = match.group(1) | |||
|
167 | if special == '\f': | |||
|
168 | self.actions.append(ScrollAction('scroll', 'down', 'page', 1)) | |||
|
169 | return '' | |||
143 |
|
170 | |||
144 |
|
171 | |||
145 | class QtAnsiCodeProcessor(AnsiCodeProcessor): |
|
172 | class QtAnsiCodeProcessor(AnsiCodeProcessor): |
@@ -1257,8 +1257,9 b' class ConsoleWidget(Configurable, QtGui.QWidget):' | |||||
1257 | cursor.beginEditBlock() |
|
1257 | cursor.beginEditBlock() | |
1258 | if self.ansi_codes: |
|
1258 | if self.ansi_codes: | |
1259 | for substring in self._ansi_processor.split_string(text): |
|
1259 | for substring in self._ansi_processor.split_string(text): | |
1260 |
for act |
|
1260 | for act in self._ansi_processor.actions: | |
1261 |
if action |
|
1261 | if ((act.action == 'erase' and act.area == 'screen') or | |
|
1262 | (act.action == 'scroll' and act.unit == 'page')): | |||
1262 | cursor.select(QtGui.QTextCursor.Document) |
|
1263 | cursor.select(QtGui.QTextCursor.Document) | |
1263 | cursor.removeSelectedText() |
|
1264 | cursor.removeSelectedText() | |
1264 | format = self._ansi_processor.get_format() |
|
1265 | format = self._ansi_processor.get_format() |
@@ -17,13 +17,13 b' class TestAnsiCodeProcessor(unittest.TestCase):' | |||||
17 | if i == 0: |
|
17 | if i == 0: | |
18 | self.assertEquals(len(self.processor.actions), 1) |
|
18 | self.assertEquals(len(self.processor.actions), 1) | |
19 | action = self.processor.actions[0] |
|
19 | action = self.processor.actions[0] | |
20 |
self.assertEquals(action. |
|
20 | self.assertEquals(action.action, 'erase') | |
21 | self.assertEquals(action.area, 'screen') |
|
21 | self.assertEquals(action.area, 'screen') | |
22 | self.assertEquals(action.erase_to, 'all') |
|
22 | self.assertEquals(action.erase_to, 'all') | |
23 | elif i == 1: |
|
23 | elif i == 1: | |
24 | self.assertEquals(len(self.processor.actions), 1) |
|
24 | self.assertEquals(len(self.processor.actions), 1) | |
25 | action = self.processor.actions[0] |
|
25 | action = self.processor.actions[0] | |
26 |
self.assertEquals(action. |
|
26 | self.assertEquals(action.action, 'erase') | |
27 | self.assertEquals(action.area, 'line') |
|
27 | self.assertEquals(action.area, 'line') | |
28 | self.assertEquals(action.erase_to, 'end') |
|
28 | self.assertEquals(action.erase_to, 'end') | |
29 | else: |
|
29 | else: | |
@@ -47,6 +47,38 b' class TestAnsiCodeProcessor(unittest.TestCase):' | |||||
47 | self.fail('Too many substrings.') |
|
47 | self.fail('Too many substrings.') | |
48 | self.assertEquals(i, 2, 'Too few substrings.') |
|
48 | self.assertEquals(i, 2, 'Too few substrings.') | |
49 |
|
49 | |||
|
50 | def testScroll(self): | |||
|
51 | string = '\x1b[5S\x1b[T' | |||
|
52 | i = -1 | |||
|
53 | for i, substring in enumerate(self.processor.split_string(string)): | |||
|
54 | if i == 0: | |||
|
55 | self.assertEquals(len(self.processor.actions), 1) | |||
|
56 | action = self.processor.actions[0] | |||
|
57 | self.assertEquals(action.action, 'scroll') | |||
|
58 | self.assertEquals(action.dir, 'up') | |||
|
59 | self.assertEquals(action.unit, 'line') | |||
|
60 | self.assertEquals(action.count, 5) | |||
|
61 | elif i == 1: | |||
|
62 | self.assertEquals(len(self.processor.actions), 1) | |||
|
63 | action = self.processor.actions[0] | |||
|
64 | self.assertEquals(action.action, 'scroll') | |||
|
65 | self.assertEquals(action.dir, 'down') | |||
|
66 | self.assertEquals(action.unit, 'line') | |||
|
67 | self.assertEquals(action.count, 1) | |||
|
68 | else: | |||
|
69 | self.fail('Too many substrings.') | |||
|
70 | self.assertEquals(i, 1, 'Too few substrings.') | |||
|
71 | ||||
|
72 | def testSpecials(self): | |||
|
73 | string = '\f' # form feed | |||
|
74 | self.assertEquals(list(self.processor.split_string(string)), ['']) | |||
|
75 | self.assertEquals(len(self.processor.actions), 1) | |||
|
76 | action = self.processor.actions[0] | |||
|
77 | self.assertEquals(action.action, 'scroll') | |||
|
78 | self.assertEquals(action.dir, 'down') | |||
|
79 | self.assertEquals(action.unit, 'page') | |||
|
80 | self.assertEquals(action.count, 1) | |||
|
81 | ||||
50 |
|
82 | |||
51 | if __name__ == '__main__': |
|
83 | if __name__ == '__main__': | |
52 | unittest.main() |
|
84 | unittest.main() |
General Comments 0
You need to be logged in to leave comments.
Login now