##// END OF EJS Templates
Added support for scroll sequences and form feeds to AnsiCodeProcessor....
epatters -
Show More
@@ -1,34 +1,38
1 """ Utilities for processing ANSI escape codes and special ASCII characters.
2 """
3 #-----------------------------------------------------------------------------
4 # Imports
5 #-----------------------------------------------------------------------------
6
1 7 # Standard library imports
8 from collections import namedtuple
2 9 import re
3 10
4 11 # System library imports
5 12 from PyQt4 import QtCore, QtGui
6 13
14 #-----------------------------------------------------------------------------
15 # Constants and datatypes
16 #-----------------------------------------------------------------------------
7 17
8 class AnsiAction(object):
9 """ Represents an action requested by an ANSI escape sequence.
10 """
11 def __init__(self, kind):
12 self.kind = kind
18 # An action for erase requests (ED and EL commands).
19 EraseAction = namedtuple('EraseAction', ['action', 'area', 'erase_to'])
13 20
14 class MoveAction(AnsiAction):
15 """ An AnsiAction for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL,
16 CHA, and CUP commands).
17 """
18 def __init__(self):
19 raise NotImplementedError
21 # An action for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, CHA, CUP,
22 # and HVP commands).
23 # FIXME: Not implemented in AnsiCodeProcessor.
24 MoveAction = namedtuple('MoveAction', ['action', 'dir', 'unit', 'count'])
20 25
21 class EraseAction(AnsiAction):
22 """ An AnsiAction for erase requests (ED and EL commands).
23 """
24 def __init__(self, area, erase_to):
25 super(EraseAction, self).__init__('erase')
26 self.area = area
27 self.erase_to = erase_to
26 # An action for scroll requests (SU and ST) and form feeds.
27 ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count'])
28 28
29 #-----------------------------------------------------------------------------
30 # Classes
31 #-----------------------------------------------------------------------------
29 32
30 33 class AnsiCodeProcessor(object):
31 """ Translates ANSI escape codes into readable attributes.
34 """ Translates special ASCII characters and ANSI escape codes into readable
35 attributes.
32 36 """
33 37
34 38 # Whether to increase intensity or set boldness for SGR code 1.
@@ -38,6 +42,11 class AnsiCodeProcessor(object):
38 42 # Protected class variables.
39 43 _ansi_commands = 'ABCDEFGHJKSTfmnsu'
40 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 51 def __init__(self):
43 52 self.actions = []
@@ -60,7 +69,8 class AnsiCodeProcessor(object):
60 69 start = 0
61 70
62 71 for match in self._ansi_pattern.finditer(string):
63 substring = string[start:match.start()]
72 raw = string[start:match.start()]
73 substring = self._special_pattern.sub(self._replace_special, raw)
64 74 if substring or self.actions:
65 75 yield substring
66 76 start = match.end()
@@ -77,7 +87,8 class AnsiCodeProcessor(object):
77 87 else:
78 88 self.set_csi_code(match.group(2), params)
79 89
80 substring = string[start:]
90 raw = string[start:]
91 substring = self._special_pattern.sub(self._replace_special, raw)
81 92 if substring or self.actions:
82 93 yield substring
83 94
@@ -107,7 +118,13 class AnsiCodeProcessor(object):
107 118 erase_to = 'start'
108 119 elif code == 2:
109 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 129 def set_sgr_code(self, code):
113 130 """ Set attributes based on SGR (Select Graphic Rendition) code.
@@ -140,6 +157,16 class AnsiCodeProcessor(object):
140 157 self.background_color = code - 40
141 158 elif code == 49:
142 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 172 class QtAnsiCodeProcessor(AnsiCodeProcessor):
@@ -1257,8 +1257,9 class ConsoleWidget(Configurable, QtGui.QWidget):
1257 1257 cursor.beginEditBlock()
1258 1258 if self.ansi_codes:
1259 1259 for substring in self._ansi_processor.split_string(text):
1260 for action in self._ansi_processor.actions:
1261 if action.kind == 'erase' and action.area == 'screen':
1260 for act in self._ansi_processor.actions:
1261 if ((act.action == 'erase' and act.area == 'screen') or
1262 (act.action == 'scroll' and act.unit == 'page')):
1262 1263 cursor.select(QtGui.QTextCursor.Document)
1263 1264 cursor.removeSelectedText()
1264 1265 format = self._ansi_processor.get_format()
@@ -17,13 +17,13 class TestAnsiCodeProcessor(unittest.TestCase):
17 17 if i == 0:
18 18 self.assertEquals(len(self.processor.actions), 1)
19 19 action = self.processor.actions[0]
20 self.assertEquals(action.kind, 'erase')
20 self.assertEquals(action.action, 'erase')
21 21 self.assertEquals(action.area, 'screen')
22 22 self.assertEquals(action.erase_to, 'all')
23 23 elif i == 1:
24 24 self.assertEquals(len(self.processor.actions), 1)
25 25 action = self.processor.actions[0]
26 self.assertEquals(action.kind, 'erase')
26 self.assertEquals(action.action, 'erase')
27 27 self.assertEquals(action.area, 'line')
28 28 self.assertEquals(action.erase_to, 'end')
29 29 else:
@@ -47,6 +47,38 class TestAnsiCodeProcessor(unittest.TestCase):
47 47 self.fail('Too many substrings.')
48 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 83 if __name__ == '__main__':
52 84 unittest.main()
General Comments 0
You need to be logged in to leave comments. Login now