|
1 # |
|
2 # This is a simple console class, which tries to handle a VT100-workalike |
|
3 # terminal at the other end of a given socket connection. |
|
4 # |
|
5 # Limitations: |
|
6 # - currently the remote screen size is assumed to be 80x25 |
|
7 # - sockets are assumed to be blocking (since the platform in mind didn't |
|
8 # have nonblocking sockets) |
|
9 # |
|
10 # Portions Copyright (c) 2005 Nokia Corporation |
|
11 # |
|
12 # The code is derived from unix_console.py, which contained the |
|
13 # following copyright notice: |
|
14 # |
|
15 # Copyright 2000-2004 Michael Hudson mwh@python.net |
|
16 # |
|
17 # All Rights Reserved |
|
18 # |
|
19 # |
|
20 # Permission to use, copy, modify, and distribute this software and |
|
21 # its documentation for any purpose is hereby granted without fee, |
|
22 # provided that the above copyright notice appear in all copies and |
|
23 # that both that copyright notice and this permission notice appear in |
|
24 # supporting documentation. |
|
25 # |
|
26 # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO |
|
27 # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY |
|
28 # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, |
|
29 # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER |
|
30 # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF |
|
31 # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN |
|
32 # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
|
33 |
|
34 import sys |
|
35 from pyrepl.console import Console, Event |
|
36 from pyrepl import unix_eventqueue |
|
37 import dumbcurses as curses |
|
38 import socket |
|
39 |
|
40 def _my_getstr(cap, optional=0): |
|
41 r = curses.tigetstr(cap) |
|
42 if not optional and r is None: |
|
43 raise RuntimeError, \ |
|
44 "terminal doesn't have the required '%s' capability"%cap |
|
45 return r |
|
46 |
|
47 class SocketConsole(Console): |
|
48 MAX_READ=16 |
|
49 def __init__(self, socket, encoding=None): |
|
50 # if encoding is None: |
|
51 # encoding = sys.getdefaultencoding() |
|
52 |
|
53 # self.encoding = encoding |
|
54 #print >>sys.stderr,"Encoding "+encoding |
|
55 self.encoding='latin-1' |
|
56 |
|
57 self._socket=socket |
|
58 |
|
59 self.__buffer = [] |
|
60 |
|
61 self._bel = _my_getstr("bel") |
|
62 self._civis = _my_getstr("civis", optional=1) |
|
63 self._clear = _my_getstr("clear") |
|
64 self._cnorm = _my_getstr("cnorm", optional=1) |
|
65 self._cub = _my_getstr("cub", optional=1) |
|
66 self._cub1 = _my_getstr("cub1", 1) |
|
67 self._cud = _my_getstr("cud", 1) |
|
68 self._cud1 = _my_getstr("cud1", 1) |
|
69 self._cuf = _my_getstr("cuf", 1) |
|
70 self._cuf1 = _my_getstr("cuf1", 1) |
|
71 self._cup = _my_getstr("cup") |
|
72 self._cuu = _my_getstr("cuu", 1) |
|
73 self._cuu1 = _my_getstr("cuu1", 1) |
|
74 self._dch1 = _my_getstr("dch1", 1) |
|
75 self._dch = _my_getstr("dch", 1) |
|
76 self._el = _my_getstr("el") |
|
77 self._hpa = _my_getstr("hpa", 1) |
|
78 self._ich = _my_getstr("ich", 1) |
|
79 self._ich1 = _my_getstr("ich1", 1) |
|
80 self._ind = _my_getstr("ind", 1) |
|
81 self._pad = _my_getstr("pad", 1) |
|
82 self._ri = _my_getstr("ri", 1) |
|
83 self._rmkx = _my_getstr("rmkx", 1) |
|
84 self._smkx = _my_getstr("smkx", 1) |
|
85 |
|
86 ## work out how we're going to sling the cursor around |
|
87 if 0 and self._hpa: # hpa don't work in windows telnet :-( |
|
88 self.__move_x = self.__move_x_hpa |
|
89 elif self._cub and self._cuf: |
|
90 self.__move_x = self.__move_x_cub_cuf |
|
91 elif self._cub1 and self._cuf1: |
|
92 self.__move_x = self.__move_x_cub1_cuf1 |
|
93 else: |
|
94 raise RuntimeError, "insufficient terminal (horizontal)" |
|
95 |
|
96 if self._cuu and self._cud: |
|
97 self.__move_y = self.__move_y_cuu_cud |
|
98 elif self._cuu1 and self._cud1: |
|
99 self.__move_y = self.__move_y_cuu1_cud1 |
|
100 else: |
|
101 raise RuntimeError, "insufficient terminal (vertical)" |
|
102 |
|
103 if self._dch1: |
|
104 self.dch1 = self._dch1 |
|
105 elif self._dch: |
|
106 self.dch1 = curses.tparm(self._dch, 1) |
|
107 else: |
|
108 self.dch1 = None |
|
109 |
|
110 if self._ich1: |
|
111 self.ich1 = self._ich1 |
|
112 elif self._ich: |
|
113 self.ich1 = curses.tparm(self._ich, 1) |
|
114 else: |
|
115 self.ich1 = None |
|
116 |
|
117 self.__move = self.__move_short |
|
118 |
|
119 self.event_queue = unix_eventqueue.EventQueue() |
|
120 self.busy=False |
|
121 |
|
122 def _oswrite(self,str): |
|
123 try: |
|
124 self._socket.send(str) |
|
125 except socket.error: |
|
126 raise IOError("Socket error: %s %s"%(sys.exc_info()[0:2])) |
|
127 |
|
128 def _osread(self,n=1): |
|
129 try: |
|
130 out=self._socket.recv(n) |
|
131 except socket.error: |
|
132 raise EOFError("Socket error: %s %s"%(sys.exc_info()[0:2])) |
|
133 return out |
|
134 |
|
135 def write(self,str): |
|
136 self._oswrite(str.replace('\n','\n\r')) |
|
137 |
|
138 def flush(self): |
|
139 self.flushoutput() |
|
140 |
|
141 def read(self,n=1): |
|
142 return self._osread(n) |
|
143 |
|
144 def readline(self,n=None): |
|
145 line=[] |
|
146 while 1: |
|
147 ch=self.read(1) |
|
148 line.append(ch) |
|
149 self.write(ch) |
|
150 if ch == '\n': |
|
151 break |
|
152 if n and len(line)>=n: |
|
153 break |
|
154 return ''.join(line) |
|
155 |
|
156 # No readlines() because reading until EOF doesn't make sense |
|
157 # for the console. |
|
158 |
|
159 def isatty(self): |
|
160 return True |
|
161 |
|
162 def writelines(self,seq): |
|
163 for k in seq: |
|
164 self.write(k) |
|
165 |
|
166 def change_encoding(self, encoding): |
|
167 self.encoding = encoding |
|
168 |
|
169 def refresh(self, screen, (cx, cy)): |
|
170 # this function is still too long (over 90 lines) |
|
171 self.__maybe_write_code(self._civis) |
|
172 |
|
173 if not self.__gone_tall: |
|
174 while len(self.screen) < min(len(screen), self.height): |
|
175 self.__move(0, len(self.screen) - 1) |
|
176 self.__write("\n") |
|
177 self.__posxy = 0, len(self.screen) |
|
178 self.screen.append("") |
|
179 else: |
|
180 while len(self.screen) < len(screen): |
|
181 self.screen.append("") |
|
182 |
|
183 if len(screen) > self.height: |
|
184 self.__gone_tall = 1 |
|
185 self.__move = self.__move_tall |
|
186 |
|
187 px, py = self.__posxy |
|
188 old_offset = offset = self.__offset |
|
189 height = self.height |
|
190 |
|
191 # we make sure the cursor is on the screen, and that we're |
|
192 # using all of the screen if we can |
|
193 if cy < offset: |
|
194 offset = cy |
|
195 elif cy >= offset + height: |
|
196 offset = cy - height + 1 |
|
197 elif offset > 0 and len(screen) < offset + height: |
|
198 offset = max(len(screen) - height, 0) |
|
199 screen.append([]) |
|
200 |
|
201 oldscr = self.screen[old_offset:old_offset + height] |
|
202 newscr = screen[offset:offset + height] |
|
203 |
|
204 # use hardware scrolling if we have it. |
|
205 if old_offset > offset and self._ri: |
|
206 self.__write_code(self._cup, 0, 0) |
|
207 self.__posxy = 0, old_offset |
|
208 for i in range(old_offset - offset): |
|
209 self.__write_code(self._ri) |
|
210 oldscr.pop(-1) |
|
211 oldscr.insert(0, "") |
|
212 elif old_offset < offset and self._ind: |
|
213 self.__write_code(self._cup, self.height - 1, 0) |
|
214 self.__posxy = 0, old_offset + self.height - 1 |
|
215 for i in range(offset - old_offset): |
|
216 self.__write_code(self._ind) |
|
217 oldscr.pop(0) |
|
218 oldscr.append("") |
|
219 |
|
220 self.__offset = offset |
|
221 |
|
222 for y, oldline, newline, in zip(range(offset, offset + height), |
|
223 oldscr, |
|
224 newscr): |
|
225 if oldline != newline: |
|
226 self.write_changed_line(y, oldline, newline, px) |
|
227 |
|
228 y = len(newscr) |
|
229 while y < len(oldscr): |
|
230 self.__move(0, y) |
|
231 self.__posxy = 0, y |
|
232 self.__write_code(self._el) |
|
233 y += 1 |
|
234 |
|
235 self.__maybe_write_code(self._cnorm) |
|
236 |
|
237 #self.flushoutput() |
|
238 self.screen = screen |
|
239 self.move_cursor(cx, cy) # this does self.flushoutput() |
|
240 |
|
241 def write_changed_line(self, y, oldline, newline, px): |
|
242 # this is frustrating; there's no reason to test (say) |
|
243 # self.dch1 inside the loop -- but alternative ways of |
|
244 # structuring this function are equally painful (I'm trying to |
|
245 # avoid writing code generators these days...) |
|
246 x = 0 |
|
247 minlen = min(len(oldline), len(newline)) |
|
248 while x < minlen and oldline[x] == newline[x]: |
|
249 x += 1 |
|
250 if oldline[x:] == newline[x+1:] and self.ich1: |
|
251 if ( y == self.__posxy[1] and x > self.__posxy[0] |
|
252 and oldline[px:x] == newline[px+1:x+1] ): |
|
253 x = px |
|
254 self.__move(x, y) |
|
255 self.__write_code(self.ich1) |
|
256 self.__write(newline[x]) |
|
257 self.__posxy = x + 1, y |
|
258 elif x < minlen and oldline[x + 1:] == newline[x + 1:]: |
|
259 self.__move(x, y) |
|
260 self.__write(newline[x]) |
|
261 self.__posxy = x + 1, y |
|
262 elif (self.dch1 and self.ich1 and len(newline) == self.width |
|
263 and x < len(newline) - 2 |
|
264 and newline[x+1:-1] == oldline[x:-2]): |
|
265 self.__move(self.width - 2, y) |
|
266 self.__posxy = self.width - 2, y |
|
267 self.__write_code(self.dch1) |
|
268 self.__move(x, y) |
|
269 self.__write_code(self.ich1) |
|
270 self.__write(newline[x]) |
|
271 self.__posxy = x + 1, y |
|
272 else: |
|
273 self.__move(x, y) |
|
274 if len(oldline) > len(newline): |
|
275 self.__write_code(self._el) |
|
276 self.__write(newline[x:]) |
|
277 self.__posxy = len(newline), y |
|
278 #self.flushoutput() # removed for efficiency |
|
279 |
|
280 def __write(self, text): |
|
281 self.__buffer.append((text, 0)) |
|
282 |
|
283 def __write_code(self, fmt, *args): |
|
284 self.__buffer.append((curses.tparm(fmt, *args), 1)) |
|
285 |
|
286 def __maybe_write_code(self, fmt, *args): |
|
287 if fmt: |
|
288 self.__write_code(fmt, *args) |
|
289 |
|
290 def __move_y_cuu1_cud1(self, y): |
|
291 dy = y - self.__posxy[1] |
|
292 if dy > 0: |
|
293 self.__write_code(dy*self._cud1) |
|
294 elif dy < 0: |
|
295 self.__write_code((-dy)*self._cuu1) |
|
296 |
|
297 def __move_y_cuu_cud(self, y): |
|
298 dy = y - self.__posxy[1] |
|
299 if dy > 0: |
|
300 self.__write_code(self._cud, dy) |
|
301 elif dy < 0: |
|
302 self.__write_code(self._cuu, -dy) |
|
303 |
|
304 def __move_x_hpa(self, x): |
|
305 if x != self.__posxy[0]: |
|
306 self.__write_code(self._hpa, x) |
|
307 |
|
308 def __move_x_cub1_cuf1(self, x): |
|
309 dx = x - self.__posxy[0] |
|
310 if dx > 0: |
|
311 self.__write_code(self._cuf1*dx) |
|
312 elif dx < 0: |
|
313 self.__write_code(self._cub1*(-dx)) |
|
314 |
|
315 def __move_x_cub_cuf(self, x): |
|
316 dx = x - self.__posxy[0] |
|
317 if dx > 0: |
|
318 self.__write_code(self._cuf, dx) |
|
319 elif dx < 0: |
|
320 self.__write_code(self._cub, -dx) |
|
321 |
|
322 def __move_short(self, x, y): |
|
323 self.__move_x(x) |
|
324 self.__move_y(y) |
|
325 |
|
326 def __move_tall(self, x, y): |
|
327 assert 0 <= y - self.__offset < self.height, y - self.__offset |
|
328 self.__write_code(self._cup, y - self.__offset, x) |
|
329 |
|
330 def move_cursor(self, x, y): |
|
331 if y < self.__offset or y >= self.__offset + self.height: |
|
332 self.event_queue.insert(Event('scroll', None)) |
|
333 else: |
|
334 self.__move(x, y) |
|
335 self.__posxy = x, y |
|
336 if not self.isbusy(): |
|
337 self.flushoutput() |
|
338 |
|
339 def prepare(self): |
|
340 self.screen = [] |
|
341 self.height, self.width = self.getheightwidth() |
|
342 |
|
343 self.__buffer = [] |
|
344 |
|
345 self.__posxy = 0, 0 |
|
346 self.__gone_tall = 0 |
|
347 self.__move = self.__move_short |
|
348 self.__offset = 0 |
|
349 |
|
350 self.__maybe_write_code(self._rmkx) # Turn off application cursor mode. |
|
351 |
|
352 def restore(self): |
|
353 # We never put the cursor keys in application mode, so this |
|
354 # is redundant now: |
|
355 #self.__maybe_write_code(self._rmkx) |
|
356 self.flushoutput() |
|
357 |
|
358 def __sigwinch(self, signum, frame): |
|
359 self.height, self.width = self.getheightwidth() |
|
360 self.event_queue.insert(Event('resize', None)) |
|
361 |
|
362 def isbusy(self): |
|
363 return self.busy |
|
364 |
|
365 def get_event(self, block=1): |
|
366 while self.event_queue.empty(): |
|
367 chars = self._osread(self.MAX_READ) |
|
368 # try: |
|
369 # except EOFError: |
|
370 # raise |
|
371 # except: |
|
372 # print >>sys.stderr,"Exception!" |
|
373 # import traceback |
|
374 # traceback.print_exc() |
|
375 # raise |
|
376 for c in chars: |
|
377 self.event_queue.push(c) |
|
378 if not block: |
|
379 break |
|
380 self.busy=len(self.event_queue.events)>1 |
|
381 return self.event_queue.get() |
|
382 |
|
383 def wait(self): |
|
384 pass |
|
385 #self.pollob.poll() |
|
386 |
|
387 def set_cursor_vis(self, vis): |
|
388 if vis: |
|
389 self.__maybe_write_code(self._cnorm) |
|
390 else: |
|
391 self.__maybe_write_code(self._civis) |
|
392 |
|
393 def repaint_prep(self): |
|
394 if not self.__gone_tall: |
|
395 self.__posxy = 0, self.__posxy[1] |
|
396 self.__write("\r") |
|
397 ns = len(self.screen)*['\000'*self.width] |
|
398 self.screen = ns |
|
399 else: |
|
400 self.__posxy = 0, self.__offset |
|
401 self.__move(0, self.__offset) |
|
402 ns = self.height*['\000'*self.width] |
|
403 self.screen = ns |
|
404 |
|
405 def getheightwidth(self): |
|
406 return 25, 80 |
|
407 |
|
408 def forgetinput(self): |
|
409 pass |
|
410 |
|
411 def flushoutput(self): |
|
412 if not self.event_queue.empty(): |
|
413 return |
|
414 if len(self.__buffer)==0: |
|
415 return |
|
416 outbuf=[] |
|
417 for text, iscode in self.__buffer: |
|
418 if iscode: |
|
419 outbuf.append(text) |
|
420 # we don't use delays here, so we don't need tputs |
|
421 # processing. |
|
422 # outbuf.append(self.__tputs(text)) |
|
423 else: |
|
424 outbuf.append(text.encode(self.encoding)) |
|
425 |
|
426 self._oswrite(''.join(outbuf)) |
|
427 del self.__buffer[:] |
|
428 |
|
429 def finish(self): |
|
430 y = len(self.screen) - 1 |
|
431 while y >= 0 and not self.screen[y]: |
|
432 y -= 1 |
|
433 self.__move(0, min(y, self.height + self.__offset - 1)) |
|
434 self.__write("\n\r") |
|
435 self.flushoutput() |
|
436 |
|
437 def beep(self): |
|
438 self.__maybe_write_code(self._bel) |
|
439 self.flushoutput() |
|
440 |
|
441 def getpending(self): |
|
442 e = Event('key', '', '') |
|
443 |
|
444 while not self.event_queue.empty(): |
|
445 e2 = self.event_queue.get() |
|
446 e.data += e2.data |
|
447 e.raw += e2.raw |
|
448 |
|
449 amount = 1000 |
|
450 raw = unicode(self._osread(amount), self.encoding, 'replace') |
|
451 e.data += raw |
|
452 e.raw += raw |
|
453 return e |
|
454 |
|
455 def clear(self): |
|
456 self.__write_code(self._clear) |
|
457 self.__gone_tall = 1 |
|
458 self.__move = self.__move_tall |
|
459 self.__posxy = 0, 0 |
|
460 self.screen = [] |
|
461 |