blob: e80238965081ebb42d82aeb2c258a919f2d7686e [file] [log] [blame]
Guido van Rossum74b3f8a1993-10-28 09:53:13 +00001# VT100 terminal emulator.
2# This is incomplete and slow, but will do for now...
3# It shouldn't be difficult to extend it to be a more-or-less complete
4# VT100 emulator. And little bit of profiling could go a long way...
5
6from array import array
7import regex
8import string
9
10# Tunable parameters
11DEBUGLEVEL = 1
12
13# Symbolic constants
14ESC = '\033'
15
16
17# VT100 emulation class
18
19class VT100:
20
21 def __init__(self):
22 self.debuglevel = DEBUGLEVEL
23 # Unchangeable parameters (for now)
24 self.width = 80
25 self.height = 24
26 self.blankline = array('c', ' '*self.width)
27 self.blankattr = array('b', '\0'*self.width)
28 # Set mutable display state
29 self.reset()
30 # Set parser state
31 self.unfinished = ''
32 # Set screen recognition state
33 self.reset_recognizer()
34
35 def msg(self, msg, *args):
36 if self.debuglevel > 0:
37 print 'VT100:', msg%args
38
39 def set_debuglevel(self, debuglevel):
40 self.debuglevel = debuglevel
41
42 def reset(self):
43 self.lines = []
44 self.attrs = []
45 self.fill_bottom()
46 self.x = 0
47 self.y = 0
48 self.curattrs = []
49
50 def show(self):
51 lineno = 0
52 for line in self.lines:
53 lineno = lineno + 1
54 i = len(line)
55 while i > 0 and line[i-1] == ' ': i = i-1
56 print line[:i]
57 print 'CURSOR:', self.x, self.y
58
59 def fill_bottom(self):
60 while len(self.lines) < self.height:
61 self.lines.append(self.blankline[:])
62 self.attrs.append(self.blankattr[:])
63
64 def fill_top(self):
65 while len(self.lines) < self.height:
66 self.lines.insert(0, self.blankline[:])
67 self.attrs.insert(0, self.blankattr[:])
68
69 def clear_all(self):
70 self.lines = []
71 self.attrs = []
72 self.fill_bottom()
73
74 def clear_below(self):
75 del self.lines[self.y:]
76 del self.attrs[self.y:]
77 self.fill_bottom()
78
79 def clear_above(self):
80 del self.lines[:self.y]
81 del self.attrs[:self.y]
82 self.fill_top()
83
84 def send(self, buffer):
85 self.unfinished = self.unfinished + buffer
86 i = 0
87 n = len(self.unfinished)
88 while i < n:
89 c = self.unfinished[i]
90 i = i+1
91 if c != ESC:
92 self.add_char(c)
93 continue
94 if i >= n:
95 i = i-1
96 break
97 c = self.unfinished[i]
98 i = i+1
99 if c == 'c':
100 self.reset()
101 continue
102 if c <> '[':
103 self.msg('unrecognized: ESC %s', `c`)
104 continue
105 argstr = ''
106 while i < n:
107 c = self.unfinished[i]
108 i = i+1
109 if c not in '0123456789;':
110 break
111 argstr = argstr + c
112 else:
113 i = i - len(argstr)
114 break
115## self.msg('found ESC [ %s %s' % (`argstr`, `c`))
116 args = string.splitfields(argstr, ';')
117 for j in range(len(args)):
118 s = args[j]
119 while s[:1] == '0': s = s[1:]
120 if s: args[j] = eval(s)
121 else: args[j] = 0
122 p1 = p2 = 0
123 if args: p1 = args[0]
124 if args[1:]: p2 = args[1]
125 if c in '@ABCDH':
126 if not p1: p1 = 1
127 if c in 'H':
128 if not p2: p2 = 1
129 if c == '@':
130 for j in range(p1):
131 self.add_char(' ')
132 elif c == 'A':
133 self.move_by(0, -p1)
134 elif c == 'B':
135 self.move_by(0, p1)
136 elif c == 'C':
137 self.move_by(p1, 0)
138 elif c == 'D':
139 self.move_by(-p1, 0)
140 elif c == 'H':
141 self.move_to(p2-1, p1-1)
142 elif c == 'J':
143 if p1 == 0: self.clear_above()
144 elif p1 == 1: self.clear_below()
145 elif p1 == 2: self.clear_all()
146 else: self.msg('weird ESC [ %d J', p1)
147 elif c == 'K':
148 if p1 == 0: self.erase_right()
149 elif p1 == 1: self.erase_left()
150 elif p1 == 2: self.erase_line()
151 else: self.msg('weird ESC [ %d K', p1)
152 elif c == 'm':
153 if p1 == 0:
154 self.curattrs = []
155 else:
156 if p1 not in self.curattrs:
157 self.curattrs.append(p1)
158 self.curattrs.sort()
159 else:
160 self.msg('unrecognized: ESC [ %s', `argstr+c`)
161 self.unfinished = self.unfinished[i:]
162
163 def add_char(self, c):
164 if c == '\r':
165 self.move_to(0, self.y)
166 return
167 if c in '\n\f\v':
168 self.move_to(self.x, self.y + 1)
169 if self.y >= self.height:
170 self.scroll_up(1)
171 self.move_to(self.x, self.height - 1)
172 return
173 if c == '\b':
174 self.move_by(-1, 0)
175 return
176 if c == '\a':
177 self.msg('BELL')
178 return
179 if c == '\t':
180 self.move_to((self.x+8)/8*8, self.y)
181 return
182 if c == '\0':
183 return
184 if c < ' ' or c > '~':
185 self.msg('ignored control char: %s', `c`)
186 return
187 if self.x >= self.width:
188 self.move_to(0, self.y + 1)
189 if self.y >= self.height:
190 self.scroll_up(1)
191 self.move_to(self.x, self.height - 1)
192 self.lines[self.y][self.x] = c
193 if self.curattrs:
194 self.attrs[self.y][self.x] = max(self.curattrs)
195 else:
196 self.attrs[self.y][self.x] = 0
197 self.move_by(1, 0)
198
199 def move_to(self, x, y):
200 self.x = min(max(0, x), self.width)
201 self.y = min(max(0, y), self.height)
202
203 def move_by(self, dx, dy):
204 self.move_to(self.x + dx, self.y + dy)
205
206 def scroll_up(self, nlines):
207 del self.lines[:max(0, nlines)]
208 del self.attrs[:max(0, nlines)]
209 self.fill_bottom()
210
211 def scroll_down(self, nlines):
212 del self.lines[-max(0, nlines):]
213 del self.attrs[-max(0, nlines):]
214 self.fill_top()
215
216 def erase_left(self):
217 x = min(self.width-1, x)
218 y = min(self.height-1, y)
219 self.lines[y][:x] = self.blankline[:x]
220 self.attrs[y][:x] = self.blankattr[:x]
221
222 def erase_right(self):
223 x = min(self.width-1, x)
224 y = min(self.height-1, y)
225 self.lines[y][x:] = self.blankline[x:]
226 self.attrs[y][x:] = self.blankattr[x:]
227
228 def erase_line(self):
229 self.lines[y][:] = self.blankline
230 self.attrs[y][:] = self.blankattr
231
232 # The following routines help automating the recognition of
233 # standard screens. A standard screen is characterized by
234 # a number of fields. A field is part of a line,
235 # characterized by a (lineno, begin, end) tuple;
236 # e.g. the first 10 characters of the second line are
237 # specified by the tuple (1, 0, 10). Fields can be:
238 # - regex: desired contents given by a regular expression,
239 # - extract: can be extracted,
240 # - cursor: screen is only valid if cursor in field,
241 # - copy: identical to another screen (position is ignored).
242 # A screen is defined as a dictionary full of fields. Screens
243 # also have names and are placed in a dictionary.
244
245 def reset_recognizer(self):
246 self.screens = {}
247
248 def define_screen(self, screenname, fields):
249 fieldscopy = {}
250 # Check if the fields make sense
251 for fieldname in fields.keys():
252 field = fields[fieldname]
253 ftype, lineno, begin, end, extra = field
254 if ftype in ('match', 'search'):
255 extra = regex.compile(extra)
256 elif ftype == 'extract':
257 extra = None
258 elif ftype == 'cursor':
259 extra = None
260 elif ftype == 'copy':
261 if not self.screens.has_key(extra):
262 raise ValueError, 'bad copy ref'
263 else:
264 raise ValueError, 'bad ftype: %s' % `ftype`
265 fieldscopy[fieldname] = (
266 ftype, lineno, begin, end, extra)
267 self.screens[screenname] = fieldscopy
268
269 def which_screens(self):
270 self.busy = []
271 self.okay = []
272 self.fail = []
273 for name in self.screens.keys():
274 ok = self.match_screen(name)
275 return self.okay[:]
276
277 def match_screen(self, name):
278 if name in self.busy: raise RuntimeError, 'recursive match'
279 if name in self.okay: return 1
280 if name in self.fail: return 0
281 self.busy.append(name)
282 fields = self.screens[name]
283 ok = 0
284 for key in fields.keys():
285 field = fields[key]
286 ftype, lineno, begin, end, extra = field
287 if ftype == 'copy':
288 if not self.match_screen(extra): break
289 elif ftype == 'search':
290 text = self.lines[lineno][begin:end].tostring()
291 if extra.search(text) < 0:
292 break
293 elif ftype == 'match':
294 text = self.lines[lineno][begin:end].tostring()
295 if extra.match(text) < 0:
296 break
297 elif ftype == 'cursor':
298 if self.x != lineno or not \
299 begin <= self.y < end:
300 break
301 else:
302 ok = 1
303 if ok:
304 self.okay.append(name)
305 else:
306 self.fail.append(name)
307 self.busy.remove(name)
308 return ok
309
310 def extract_field(self, screenname, fieldname):
311 ftype, lineno, begin, end, extra = \
312 self.screens[screenname][fieldname]
313 return stripright(self.lines[lineno][begin:end].tostring())
314
315 def extract_rect(self, left, top, right, bottom):
316 lines = []
317 for i in range(top, bottom):
318 lines.append(stripright(self.lines[i][left:right])
319 .tostring())
320 return lines
321
322
323def stripright(line):
324 i = len(line)
325 while i > 0 and line[i-1] in string.whitespace: i = i-1
326 return line[:i]