changes by Barry, e.g. font lock & email addresses
[python/dscho.git] / Demo / cwilib / vt100.py
blob9edd4810b955f789a0694825ecbd9139df3106f5
1 # 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...
6 from array import array
7 import regex
8 import string
10 # Tunable parameters
11 DEBUGLEVEL = 1
13 # Symbolic constants
14 ESC = '\033'
17 # VT100 emulation class
19 class VT100:
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()
35 def msg(self, msg, *args):
36 if self.debuglevel > 0:
37 print 'VT100:', msg%args
39 def set_debuglevel(self, debuglevel):
40 self.debuglevel = debuglevel
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 = []
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
59 def fill_bottom(self):
60 while len(self.lines) < self.height:
61 self.lines.append(self.blankline[:])
62 self.attrs.append(self.blankattr[:])
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[:])
69 def clear_all(self):
70 self.lines = []
71 self.attrs = []
72 self.fill_bottom()
74 def clear_below(self):
75 del self.lines[self.y:]
76 del self.attrs[self.y:]
77 self.fill_bottom()
79 def clear_above(self):
80 del self.lines[:self.y]
81 del self.attrs[:self.y]
82 self.fill_top()
84 def send(self, buffer):
85 self.msg('send: unfinished=%s, buffer=%s',
86 `self.unfinished`, `buffer`)
87 self.unfinished = self.unfinished + buffer
88 i = 0
89 n = len(self.unfinished)
90 while i < n:
91 c = self.unfinished[i]
92 i = i+1
93 if c != ESC:
94 self.add_char(c)
95 continue
96 if i >= n:
97 i = i-1
98 break
99 c = self.unfinished[i]
100 i = i+1
101 if c == 'c':
102 self.reset()
103 continue
104 if c <> '[':
105 self.msg('unrecognized: ESC %s', `c`)
106 continue
107 argstr = ''
108 while i < n:
109 c = self.unfinished[i]
110 i = i+1
111 if c not in '0123456789;':
112 break
113 argstr = argstr + c
114 else:
115 i = i - len(argstr) - 2
116 break
117 ## self.msg('found ESC [ %s %s' % (`argstr`, `c`))
118 args = string.splitfields(argstr, ';')
119 for j in range(len(args)):
120 s = args[j]
121 while s[:1] == '0': s = s[1:]
122 if s: args[j] = eval(s)
123 else: args[j] = 0
124 p1 = p2 = 0
125 if args: p1 = args[0]
126 if args[1:]: p2 = args[1]
127 if c in '@ABCDH':
128 if not p1: p1 = 1
129 if c in 'H':
130 if not p2: p2 = 1
131 if c == '@':
132 for j in range(p1):
133 self.add_char(' ')
134 elif c == 'A':
135 self.move_by(0, -p1)
136 elif c == 'B':
137 self.move_by(0, p1)
138 elif c == 'C':
139 self.move_by(p1, 0)
140 elif c == 'D':
141 self.move_by(-p1, 0)
142 elif c == 'H':
143 self.move_to(p2-1, p1-1)
144 elif c == 'J':
145 if p1 == 0: self.clear_above()
146 elif p1 == 1: self.clear_below()
147 elif p1 == 2: self.clear_all()
148 else: self.msg('weird ESC [ %d J', p1)
149 elif c == 'K':
150 if p1 == 0: self.erase_right()
151 elif p1 == 1: self.erase_left()
152 elif p1 == 2: self.erase_line()
153 else: self.msg('weird ESC [ %d K', p1)
154 elif c == 'm':
155 if p1 == 0:
156 self.curattrs = []
157 else:
158 if p1 not in self.curattrs:
159 self.curattrs.append(p1)
160 self.curattrs.sort()
161 else:
162 self.msg('unrecognized: ESC [ %s', `argstr+c`)
163 self.unfinished = self.unfinished[i:]
165 def add_char(self, c):
166 if c == '\r':
167 self.move_to(0, self.y)
168 return
169 if c in '\n\f\v':
170 self.move_to(self.x, self.y + 1)
171 if self.y >= self.height:
172 self.scroll_up(1)
173 self.move_to(self.x, self.height - 1)
174 return
175 if c == '\b':
176 self.move_by(-1, 0)
177 return
178 if c == '\a':
179 self.msg('BELL')
180 return
181 if c == '\t':
182 self.move_to((self.x+8)/8*8, self.y)
183 return
184 if c == '\0':
185 return
186 if c < ' ' or c > '~':
187 self.msg('ignored control char: %s', `c`)
188 return
189 if self.x >= self.width:
190 self.move_to(0, self.y + 1)
191 if self.y >= self.height:
192 self.scroll_up(1)
193 self.move_to(self.x, self.height - 1)
194 self.lines[self.y][self.x] = c
195 if self.curattrs:
196 self.attrs[self.y][self.x] = max(self.curattrs)
197 else:
198 self.attrs[self.y][self.x] = 0
199 self.move_by(1, 0)
201 def move_to(self, x, y):
202 self.x = min(max(0, x), self.width)
203 self.y = min(max(0, y), self.height)
205 def move_by(self, dx, dy):
206 self.move_to(self.x + dx, self.y + dy)
208 def scroll_up(self, nlines):
209 del self.lines[:max(0, nlines)]
210 del self.attrs[:max(0, nlines)]
211 self.fill_bottom()
213 def scroll_down(self, nlines):
214 del self.lines[-max(0, nlines):]
215 del self.attrs[-max(0, nlines):]
216 self.fill_top()
218 def erase_left(self):
219 x = min(self.width-1, x)
220 y = min(self.height-1, y)
221 self.lines[y][:x] = self.blankline[:x]
222 self.attrs[y][:x] = self.blankattr[:x]
224 def erase_right(self):
225 x = min(self.width-1, x)
226 y = min(self.height-1, y)
227 self.lines[y][x:] = self.blankline[x:]
228 self.attrs[y][x:] = self.blankattr[x:]
230 def erase_line(self):
231 self.lines[y][:] = self.blankline
232 self.attrs[y][:] = self.blankattr
234 # The following routines help automating the recognition of
235 # standard screens. A standard screen is characterized by
236 # a number of fields. A field is part of a line,
237 # characterized by a (lineno, begin, end) tuple;
238 # e.g. the first 10 characters of the second line are
239 # specified by the tuple (1, 0, 10). Fields can be:
240 # - regex: desired contents given by a regular expression,
241 # - extract: can be extracted,
242 # - cursor: screen is only valid if cursor in field,
243 # - copy: identical to another screen (position is ignored).
244 # A screen is defined as a dictionary full of fields. Screens
245 # also have names and are placed in a dictionary.
247 def reset_recognizer(self):
248 self.screens = {}
250 def define_screen(self, screenname, fields):
251 fieldscopy = {}
252 # Check if the fields make sense
253 for fieldname in fields.keys():
254 field = fields[fieldname]
255 ftype, lineno, begin, end, extra = field
256 if ftype in ('match', 'search'):
257 extra = regex.compile(extra)
258 elif ftype == 'extract':
259 extra = None
260 elif ftype == 'cursor':
261 extra = None
262 elif ftype == 'copy':
263 if not self.screens.has_key(extra):
264 raise ValueError, 'bad copy ref'
265 else:
266 raise ValueError, 'bad ftype: %s' % `ftype`
267 fieldscopy[fieldname] = (
268 ftype, lineno, begin, end, extra)
269 self.screens[screenname] = fieldscopy
271 def which_screens(self):
272 self.busy = []
273 self.okay = []
274 self.fail = []
275 for name in self.screens.keys():
276 ok = self.match_screen(name)
277 return self.okay[:]
279 def match_screen(self, name):
280 if name in self.busy: raise RuntimeError, 'recursive match'
281 if name in self.okay: return 1
282 if name in self.fail: return 0
283 self.busy.append(name)
284 fields = self.screens[name]
285 ok = 0
286 for key in fields.keys():
287 field = fields[key]
288 ftype, lineno, begin, end, extra = field
289 if ftype == 'copy':
290 if not self.match_screen(extra): break
291 elif ftype == 'search':
292 text = self.lines[lineno][begin:end].tostring()
293 if extra.search(text) < 0:
294 break
295 elif ftype == 'match':
296 text = self.lines[lineno][begin:end].tostring()
297 if extra.match(text) < 0:
298 break
299 elif ftype == 'cursor':
300 if self.x != lineno or not \
301 begin <= self.y < end:
302 break
303 else:
304 ok = 1
305 if ok:
306 self.okay.append(name)
307 else:
308 self.fail.append(name)
309 self.busy.remove(name)
310 return ok
312 def extract_field(self, screenname, fieldname):
313 ftype, lineno, begin, end, extra = \
314 self.screens[screenname][fieldname]
315 return stripright(self.lines[lineno][begin:end].tostring())
317 def extract_rect(self, left, top, right, bottom):
318 lines = []
319 for i in range(top, bottom):
320 lines.append(stripright(self.lines[i][left:right])
321 .tostring())
322 return lines
325 def stripright(line):
326 i = len(line)
327 while i > 0 and line[i-1] in string.whitespace: i = i-1
328 return line[:i]