OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.conch.test.test_insults -*- | |
2 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
3 # See LICENSE for details. | |
4 | |
5 """ | |
6 VT102 and VT220 terminal manipulation. | |
7 | |
8 @author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>} | |
9 """ | |
10 | |
11 from zope.interface import implements, Interface | |
12 | |
13 from twisted.internet import protocol, defer, interfaces as iinternet | |
14 | |
15 class ITerminalProtocol(Interface): | |
16 def makeConnection(transport): | |
17 """Called with an L{ITerminalTransport} when a connection is established
. | |
18 """ | |
19 | |
20 def keystrokeReceived(keyID, modifier): | |
21 """A keystroke was received. | |
22 | |
23 Each keystroke corresponds to one invocation of this method. | |
24 keyID is a string identifier for that key. Printable characters | |
25 are represented by themselves. Control keys, such as arrows and | |
26 function keys, are represented with symbolic constants on | |
27 L{ServerProtocol}. | |
28 """ | |
29 | |
30 def terminalSize(width, height): | |
31 """Called to indicate the size of the terminal. | |
32 | |
33 A terminal of 80x24 should be assumed if this method is not | |
34 called. This method might not be called for real terminals. | |
35 """ | |
36 | |
37 def unhandledControlSequence(seq): | |
38 """Called when an unsupported control sequence is received. | |
39 | |
40 @type seq: C{str} | |
41 @param seq: The whole control sequence which could not be interpreted. | |
42 """ | |
43 | |
44 def connectionLost(reason): | |
45 """Called when the connection has been lost. | |
46 | |
47 reason is a Failure describing why. | |
48 """ | |
49 | |
50 class TerminalProtocol(object): | |
51 implements(ITerminalProtocol) | |
52 | |
53 def makeConnection(self, terminal): | |
54 # assert ITerminalTransport.providedBy(transport), "TerminalProtocol.mak
eConnection must be passed an ITerminalTransport implementor" | |
55 self.terminal = terminal | |
56 self.connectionMade() | |
57 | |
58 def connectionMade(self): | |
59 """Called after a connection has been established. | |
60 """ | |
61 | |
62 def keystrokeReceived(self, keyID, modifier): | |
63 pass | |
64 | |
65 def terminalSize(self, width, height): | |
66 pass | |
67 | |
68 def unhandledControlSequence(self, seq): | |
69 pass | |
70 | |
71 def connectionLost(self, reason): | |
72 pass | |
73 | |
74 class ITerminalTransport(iinternet.ITransport): | |
75 def cursorUp(n=1): | |
76 """Move the cursor up n lines. | |
77 """ | |
78 | |
79 def cursorDown(n=1): | |
80 """Move the cursor down n lines. | |
81 """ | |
82 | |
83 def cursorForward(n=1): | |
84 """Move the cursor right n columns. | |
85 """ | |
86 | |
87 def cursorBackward(n=1): | |
88 """Move the cursor left n columns. | |
89 """ | |
90 | |
91 def cursorPosition(column, line): | |
92 """Move the cursor to the given line and column. | |
93 """ | |
94 | |
95 def cursorHome(): | |
96 """Move the cursor home. | |
97 """ | |
98 | |
99 def index(): | |
100 """Move the cursor down one line, performing scrolling if necessary. | |
101 """ | |
102 | |
103 def reverseIndex(): | |
104 """Move the cursor up one line, performing scrolling if necessary. | |
105 """ | |
106 | |
107 def nextLine(): | |
108 """Move the cursor to the first position on the next line, performing sc
rolling if necessary. | |
109 """ | |
110 | |
111 def saveCursor(): | |
112 """Save the cursor position, character attribute, character set, and ori
gin mode selection. | |
113 """ | |
114 | |
115 def restoreCursor(): | |
116 """Restore the previously saved cursor position, character attribute, ch
aracter set, and origin mode selection. | |
117 | |
118 If no cursor state was previously saved, move the cursor to the home pos
ition. | |
119 """ | |
120 | |
121 def setModes(modes): | |
122 """Set the given modes on the terminal. | |
123 """ | |
124 | |
125 def resetModes(mode): | |
126 """Reset the given modes on the terminal. | |
127 """ | |
128 | |
129 | |
130 def setPrivateModes(modes): | |
131 """ | |
132 Set the given DEC private modes on the terminal. | |
133 """ | |
134 | |
135 | |
136 def resetPrivateModes(modes): | |
137 """ | |
138 Reset the given DEC private modes on the terminal. | |
139 """ | |
140 | |
141 | |
142 def applicationKeypadMode(): | |
143 """Cause keypad to generate control functions. | |
144 | |
145 Cursor key mode selects the type of characters generated by cursor keys. | |
146 """ | |
147 | |
148 def numericKeypadMode(): | |
149 """Cause keypad to generate normal characters. | |
150 """ | |
151 | |
152 def selectCharacterSet(charSet, which): | |
153 """Select a character set. | |
154 | |
155 charSet should be one of CS_US, CS_UK, CS_DRAWING, CS_ALTERNATE, or | |
156 CS_ALTERNATE_SPECIAL. | |
157 | |
158 which should be one of G0 or G1. | |
159 """ | |
160 | |
161 def shiftIn(): | |
162 """Activate the G0 character set. | |
163 """ | |
164 | |
165 def shiftOut(): | |
166 """Activate the G1 character set. | |
167 """ | |
168 | |
169 def singleShift2(): | |
170 """Shift to the G2 character set for a single character. | |
171 """ | |
172 | |
173 def singleShift3(): | |
174 """Shift to the G3 character set for a single character. | |
175 """ | |
176 | |
177 def selectGraphicRendition(*attributes): | |
178 """Enabled one or more character attributes. | |
179 | |
180 Arguments should be one or more of UNDERLINE, REVERSE_VIDEO, BLINK, or B
OLD. | |
181 NORMAL may also be specified to disable all character attributes. | |
182 """ | |
183 | |
184 def horizontalTabulationSet(): | |
185 """Set a tab stop at the current cursor position. | |
186 """ | |
187 | |
188 def tabulationClear(): | |
189 """Clear the tab stop at the current cursor position. | |
190 """ | |
191 | |
192 def tabulationClearAll(): | |
193 """Clear all tab stops. | |
194 """ | |
195 | |
196 def doubleHeightLine(top=True): | |
197 """Make the current line the top or bottom half of a double-height, doub
le-width line. | |
198 | |
199 If top is True, the current line is the top half. Otherwise, it is the
bottom half. | |
200 """ | |
201 | |
202 def singleWidthLine(): | |
203 """Make the current line a single-width, single-height line. | |
204 """ | |
205 | |
206 def doubleWidthLine(): | |
207 """Make the current line a double-width line. | |
208 """ | |
209 | |
210 def eraseToLineEnd(): | |
211 """Erase from the cursor to the end of line, including cursor position. | |
212 """ | |
213 | |
214 def eraseToLineBeginning(): | |
215 """Erase from the cursor to the beginning of the line, including the cur
sor position. | |
216 """ | |
217 | |
218 def eraseLine(): | |
219 """Erase the entire cursor line. | |
220 """ | |
221 | |
222 def eraseToDisplayEnd(): | |
223 """Erase from the cursor to the end of the display, including the cursor
position. | |
224 """ | |
225 | |
226 def eraseToDisplayBeginning(): | |
227 """Erase from the cursor to the beginning of the display, including the
cursor position. | |
228 """ | |
229 | |
230 def eraseDisplay(): | |
231 """Erase the entire display. | |
232 """ | |
233 | |
234 def deleteCharacter(n=1): | |
235 """Delete n characters starting at the cursor position. | |
236 | |
237 Characters to the right of deleted characters are shifted to the left. | |
238 """ | |
239 | |
240 def insertLine(n=1): | |
241 """Insert n lines at the cursor position. | |
242 | |
243 Lines below the cursor are shifted down. Lines moved past the bottom ma
rgin are lost. | |
244 This command is ignored when the cursor is outside the scroll region. | |
245 """ | |
246 | |
247 def deleteLine(n=1): | |
248 """Delete n lines starting at the cursor position. | |
249 | |
250 Lines below the cursor are shifted up. This command is ignored when the
cursor is outside | |
251 the scroll region. | |
252 """ | |
253 | |
254 def reportCursorPosition(): | |
255 """Return a Deferred that fires with a two-tuple of (x, y) indicating th
e cursor position. | |
256 """ | |
257 | |
258 def reset(): | |
259 """Reset the terminal to its initial state. | |
260 """ | |
261 | |
262 def unhandledControlSequence(seq): | |
263 """Called when an unsupported control sequence is received. | |
264 | |
265 @type seq: C{str} | |
266 @param seq: The whole control sequence which could not be interpreted. | |
267 """ | |
268 | |
269 | |
270 CSI = '\x1b' | |
271 CST = {'~': 'tilde'} | |
272 | |
273 class modes: | |
274 """ECMA 48 standardized modes | |
275 """ | |
276 | |
277 # BREAKS YOPUR KEYBOARD MOFO | |
278 KEYBOARD_ACTION = KAM = 2 | |
279 | |
280 # When set, enables character insertion. New display characters | |
281 # move old display characters to the right. Characters moved past | |
282 # the right margin are lost. | |
283 | |
284 # When reset, enables replacement mode (disables character | |
285 # insertion). New display characters replace old display | |
286 # characters at cursor position. The old character is erased. | |
287 INSERTION_REPLACEMENT = IRM = 4 | |
288 | |
289 # Set causes a received linefeed, form feed, or vertical tab to | |
290 # move cursor to first column of next line. RETURN transmits both | |
291 # a carriage return and linefeed. This selection is also called | |
292 # new line option. | |
293 | |
294 # Reset causes a received linefeed, form feed, or vertical tab to | |
295 # move cursor to next line in current column. RETURN transmits a | |
296 # carriage return. | |
297 LINEFEED_NEWLINE = LNM = 20 | |
298 | |
299 | |
300 class privateModes: | |
301 """ANSI-Compatible Private Modes | |
302 """ | |
303 ERROR = 0 | |
304 CURSOR_KEY = 1 | |
305 ANSI_VT52 = 2 | |
306 COLUMN = 3 | |
307 SCROLL = 4 | |
308 SCREEN = 5 | |
309 ORIGIN = 6 | |
310 AUTO_WRAP = 7 | |
311 AUTO_REPEAT = 8 | |
312 PRINTER_FORM_FEED = 18 | |
313 PRINTER_EXTENT = 19 | |
314 | |
315 # Toggle cursor visibility (reset hides it) | |
316 CURSOR_MODE = 25 | |
317 | |
318 | |
319 # Character sets | |
320 CS_US = 'CS_US' | |
321 CS_UK = 'CS_UK' | |
322 CS_DRAWING = 'CS_DRAWING' | |
323 CS_ALTERNATE = 'CS_ALTERNATE' | |
324 CS_ALTERNATE_SPECIAL = 'CS_ALTERNATE_SPECIAL' | |
325 | |
326 # Groupings (or something?? These are like variables that can be bound to charac
ter sets) | |
327 G0 = 'G0' | |
328 G1 = 'G1' | |
329 | |
330 # G2 and G3 cannot be changed, but they can be shifted to. | |
331 G2 = 'G2' | |
332 G3 = 'G3' | |
333 | |
334 # Character attributes | |
335 | |
336 NORMAL = 0 | |
337 BOLD = 1 | |
338 UNDERLINE = 4 | |
339 BLINK = 5 | |
340 REVERSE_VIDEO = 7 | |
341 | |
342 class Vector: | |
343 def __init__(self, x, y): | |
344 self.x = x | |
345 self.y = y | |
346 | |
347 def log(s): | |
348 file('log', 'a').write(str(s) + '\n') | |
349 | |
350 # XXX TODO - These attributes are really part of the | |
351 # ITerminalTransport interface, I think. | |
352 _KEY_NAMES = ('UP_ARROW', 'DOWN_ARROW', 'RIGHT_ARROW', 'LEFT_ARROW', | |
353 'HOME', 'INSERT', 'DELETE', 'END', 'PGUP', 'PGDN', 'NUMPAD_MIDDLE'
, | |
354 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', | |
355 'F10', 'F11', 'F12', | |
356 | |
357 'ALT', 'SHIFT', 'CONTROL') | |
358 | |
359 class _const(object): | |
360 """ | |
361 @ivar name: A string naming this constant | |
362 """ | |
363 def __init__(self, name): | |
364 self.name = name | |
365 | |
366 def __repr__(self): | |
367 return '[' + self.name + ']' | |
368 | |
369 | |
370 FUNCTION_KEYS = [ | |
371 _const(_name) for _name in _KEY_NAMES] | |
372 | |
373 class ServerProtocol(protocol.Protocol): | |
374 implements(ITerminalTransport) | |
375 | |
376 protocolFactory = None | |
377 terminalProtocol = None | |
378 | |
379 TAB = '\t' | |
380 BACKSPACE = '\x7f' | |
381 ## | |
382 | |
383 lastWrite = '' | |
384 | |
385 state = 'data' | |
386 | |
387 termSize = Vector(80, 24) | |
388 cursorPos = Vector(0, 0) | |
389 scrollRegion = None | |
390 | |
391 # Factory who instantiated me | |
392 factory = None | |
393 | |
394 def __init__(self, protocolFactory=None, *a, **kw): | |
395 """ | |
396 @param protocolFactory: A callable which will be invoked with | |
397 *a, **kw and should return an ITerminalProtocol implementor. | |
398 This will be invoked when a connection to this ServerProtocol | |
399 is established. | |
400 | |
401 @param a: Any positional arguments to pass to protocolFactory. | |
402 @param kw: Any keyword arguments to pass to protocolFactory. | |
403 """ | |
404 # assert protocolFactory is None or ITerminalProtocol.implementedBy(prot
ocolFactory), "ServerProtocol.__init__ must be passed an ITerminalProtocol imple
mentor" | |
405 if protocolFactory is not None: | |
406 self.protocolFactory = protocolFactory | |
407 self.protocolArgs = a | |
408 self.protocolKwArgs = kw | |
409 | |
410 self._cursorReports = [] | |
411 | |
412 def connectionMade(self): | |
413 if self.protocolFactory is not None: | |
414 self.terminalProtocol = self.protocolFactory(*self.protocolArgs, **s
elf.protocolKwArgs) | |
415 | |
416 try: | |
417 factory = self.factory | |
418 except AttributeError: | |
419 pass | |
420 else: | |
421 self.terminalProtocol.factory = factory | |
422 | |
423 self.terminalProtocol.makeConnection(self) | |
424 | |
425 def dataReceived(self, data): | |
426 for ch in data: | |
427 if self.state == 'data': | |
428 if ch == '\x1b': | |
429 self.state = 'escaped' | |
430 else: | |
431 self.terminalProtocol.keystrokeReceived(ch, None) | |
432 elif self.state == 'escaped': | |
433 if ch == '[': | |
434 self.state = 'bracket-escaped' | |
435 self.escBuf = [] | |
436 elif ch == 'O': | |
437 self.state = 'low-function-escaped' | |
438 else: | |
439 self.state = 'data' | |
440 self._handleShortControlSequence(ch) | |
441 elif self.state == 'bracket-escaped': | |
442 if ch == 'O': | |
443 self.state = 'low-function-escaped' | |
444 elif ch.isalpha() or ch == '~': | |
445 self._handleControlSequence(''.join(self.escBuf) + ch) | |
446 del self.escBuf | |
447 self.state = 'data' | |
448 else: | |
449 self.escBuf.append(ch) | |
450 elif self.state == 'low-function-escaped': | |
451 self._handleLowFunctionControlSequence(ch) | |
452 self.state = 'data' | |
453 else: | |
454 raise ValueError("Illegal state") | |
455 | |
456 def _handleShortControlSequence(self, ch): | |
457 self.terminalProtocol.keystrokeReceived(ch, self.ALT) | |
458 | |
459 def _handleControlSequence(self, buf): | |
460 buf = '\x1b[' + buf | |
461 f = getattr(self.controlSequenceParser, CST.get(buf[-1], buf[-1]), None) | |
462 if f is None: | |
463 self.unhandledControlSequence(buf) | |
464 else: | |
465 f(self, self.terminalProtocol, buf[:-1]) | |
466 | |
467 def unhandledControlSequence(self, buf): | |
468 self.terminalProtocol.unhandledControlSequence(buf) | |
469 | |
470 def _handleLowFunctionControlSequence(self, ch): | |
471 map = {'P': self.F1, 'Q': self.F2, 'R': self.F3, 'S': self.F4} | |
472 keyID = map.get(ch) | |
473 if keyID is not None: | |
474 self.terminalProtocol.keystrokeReceived(keyID, None) | |
475 else: | |
476 self.terminalProtocol.unhandledControlSequence('\x1b[O' + ch) | |
477 | |
478 class ControlSequenceParser: | |
479 def A(self, proto, handler, buf): | |
480 if buf == '\x1b[': | |
481 handler.keystrokeReceived(proto.UP_ARROW, None) | |
482 else: | |
483 handler.unhandledControlSequence(buf + 'A') | |
484 | |
485 def B(self, proto, handler, buf): | |
486 if buf == '\x1b[': | |
487 handler.keystrokeReceived(proto.DOWN_ARROW, None) | |
488 else: | |
489 handler.unhandledControlSequence(buf + 'B') | |
490 | |
491 def C(self, proto, handler, buf): | |
492 if buf == '\x1b[': | |
493 handler.keystrokeReceived(proto.RIGHT_ARROW, None) | |
494 else: | |
495 handler.unhandledControlSequence(buf + 'C') | |
496 | |
497 def D(self, proto, handler, buf): | |
498 if buf == '\x1b[': | |
499 handler.keystrokeReceived(proto.LEFT_ARROW, None) | |
500 else: | |
501 handler.unhandledControlSequence(buf + 'D') | |
502 | |
503 def E(self, proto, handler, buf): | |
504 if buf == '\x1b[': | |
505 handler.keystrokeReceived(proto.NUMPAD_MIDDLE, None) | |
506 else: | |
507 handler.unhandledControlSequence(buf + 'E') | |
508 | |
509 def F(self, proto, handler, buf): | |
510 if buf == '\x1b[': | |
511 handler.keystrokeReceived(proto.END, None) | |
512 else: | |
513 handler.unhandledControlSequence(buf + 'F') | |
514 | |
515 def H(self, proto, handler, buf): | |
516 if buf == '\x1b[': | |
517 handler.keystrokeReceived(proto.HOME, None) | |
518 else: | |
519 handler.unhandledControlSequence(buf + 'H') | |
520 | |
521 def R(self, proto, handler, buf): | |
522 if not proto._cursorReports: | |
523 handler.unhandledControlSequence(buf + 'R') | |
524 elif buf.startswith('\x1b['): | |
525 report = buf[2:] | |
526 parts = report.split(';') | |
527 if len(parts) != 2: | |
528 handler.unhandledControlSequence(buf + 'R') | |
529 else: | |
530 Pl, Pc = parts | |
531 try: | |
532 Pl, Pc = int(Pl), int(Pc) | |
533 except ValueError: | |
534 handler.unhandledControlSequence(buf + 'R') | |
535 else: | |
536 d = proto._cursorReports.pop(0) | |
537 d.callback((Pc - 1, Pl - 1)) | |
538 else: | |
539 handler.unhandledControlSequence(buf + 'R') | |
540 | |
541 def Z(self, proto, handler, buf): | |
542 if buf == '\x1b[': | |
543 handler.keystrokeReceived(proto.TAB, proto.SHIFT) | |
544 else: | |
545 handler.unhandledControlSequence(buf + 'Z') | |
546 | |
547 def tilde(self, proto, handler, buf): | |
548 map = {1: proto.HOME, 2: proto.INSERT, 3: proto.DELETE, | |
549 4: proto.END, 5: proto.PGUP, 6: proto.PGDN, | |
550 | |
551 15: proto.F5, 17: proto.F6, 18: proto.F7, | |
552 19: proto.F8, 20: proto.F9, 21: proto.F10, | |
553 23: proto.F11, 24: proto.F12} | |
554 | |
555 if buf.startswith('\x1b['): | |
556 ch = buf[2:] | |
557 try: | |
558 v = int(ch) | |
559 except ValueError: | |
560 handler.unhandledControlSequence(buf + '~') | |
561 else: | |
562 symbolic = map.get(v) | |
563 if symbolic is not None: | |
564 handler.keystrokeReceived(map[v], None) | |
565 else: | |
566 handler.unhandledControlSequence(buf + '~') | |
567 else: | |
568 handler.unhandledControlSequence(buf + '~') | |
569 | |
570 controlSequenceParser = ControlSequenceParser() | |
571 | |
572 # ITerminalTransport | |
573 def cursorUp(self, n=1): | |
574 assert n >= 1 | |
575 self.cursorPos.y = max(self.cursorPos.y - n, 0) | |
576 self.write('\x1b[%dA' % (n,)) | |
577 | |
578 def cursorDown(self, n=1): | |
579 assert n >= 1 | |
580 self.cursorPos.y = min(self.cursorPos.y + n, self.termSize.y - 1) | |
581 self.write('\x1b[%dB' % (n,)) | |
582 | |
583 def cursorForward(self, n=1): | |
584 assert n >= 1 | |
585 self.cursorPos.x = min(self.cursorPos.x + n, self.termSize.x - 1) | |
586 self.write('\x1b[%dC' % (n,)) | |
587 | |
588 def cursorBackward(self, n=1): | |
589 assert n >= 1 | |
590 self.cursorPos.x = max(self.cursorPos.x - n, 0) | |
591 self.write('\x1b[%dD' % (n,)) | |
592 | |
593 def cursorPosition(self, column, line): | |
594 self.write('\x1b[%d;%dH' % (line + 1, column + 1)) | |
595 | |
596 def cursorHome(self): | |
597 self.cursorPos.x = self.cursorPos.y = 0 | |
598 self.write('\x1b[H') | |
599 | |
600 def index(self): | |
601 self.cursorPos.y = min(self.cursorPos.y + 1, self.termSize.y - 1) | |
602 self.write('\x1bD') | |
603 | |
604 def reverseIndex(self): | |
605 self.cursorPos.y = max(self.cursorPos.y - 1, 0) | |
606 self.write('\x1bM') | |
607 | |
608 def nextLine(self): | |
609 self.cursorPos.x = 0 | |
610 self.cursorPos.y = min(self.cursorPos.y + 1, self.termSize.y - 1) | |
611 self.write('\n') | |
612 | |
613 def saveCursor(self): | |
614 self._savedCursorPos = Vector(self.cursorPos.x, self.cursorPos.y) | |
615 self.write('\x1b7') | |
616 | |
617 def restoreCursor(self): | |
618 self.cursorPos = self._savedCursorPos | |
619 del self._savedCursorPos | |
620 self.write('\x1b8') | |
621 | |
622 def setModes(self, modes): | |
623 # XXX Support ANSI-Compatible private modes | |
624 self.write('\x1b[%sh' % (';'.join(map(str, modes)),)) | |
625 | |
626 def setPrivateModes(self, modes): | |
627 self.write('\x1b[?%sh' % (';'.join(map(str, modes)),)) | |
628 | |
629 def resetModes(self, modes): | |
630 # XXX Support ANSI-Compatible private modes | |
631 self.write('\x1b[%sl' % (';'.join(map(str, modes)),)) | |
632 | |
633 def resetPrivateModes(self, modes): | |
634 self.write('\x1b[?%sl' % (';'.join(map(str, modes)),)) | |
635 | |
636 def applicationKeypadMode(self): | |
637 self.write('\x1b=') | |
638 | |
639 def numericKeypadMode(self): | |
640 self.write('\x1b>') | |
641 | |
642 def selectCharacterSet(self, charSet, which): | |
643 # XXX Rewrite these as dict lookups | |
644 if which == G0: | |
645 which = '(' | |
646 elif which == G1: | |
647 which = ')' | |
648 else: | |
649 raise ValueError("`which' argument to selectCharacterSet must be G0
or G1") | |
650 if charSet == CS_UK: | |
651 charSet = 'A' | |
652 elif charSet == CS_US: | |
653 charSet = 'B' | |
654 elif charSet == CS_DRAWING: | |
655 charSet = '0' | |
656 elif charSet == CS_ALTERNATE: | |
657 charSet = '1' | |
658 elif charSet == CS_ALTERNATE_SPECIAL: | |
659 charSet = '2' | |
660 else: | |
661 raise ValueError("Invalid `charSet' argument to selectCharacterSet") | |
662 self.write('\x1b' + which + charSet) | |
663 | |
664 def shiftIn(self): | |
665 self.write('\x15') | |
666 | |
667 def shiftOut(self): | |
668 self.write('\x14') | |
669 | |
670 def singleShift2(self): | |
671 self.write('\x1bN') | |
672 | |
673 def singleShift3(self): | |
674 self.write('\x1bO') | |
675 | |
676 def selectGraphicRendition(self, *attributes): | |
677 attrs = [] | |
678 for a in attributes: | |
679 attrs.append(a) | |
680 self.write('\x1b[%sm' % (';'.join(attrs),)) | |
681 | |
682 def horizontalTabulationSet(self): | |
683 self.write('\x1bH') | |
684 | |
685 def tabulationClear(self): | |
686 self.write('\x1b[q') | |
687 | |
688 def tabulationClearAll(self): | |
689 self.write('\x1b[3q') | |
690 | |
691 def doubleHeightLine(self, top=True): | |
692 if top: | |
693 self.write('\x1b#3') | |
694 else: | |
695 self.write('\x1b#4') | |
696 | |
697 def singleWidthLine(self): | |
698 self.write('\x1b#5') | |
699 | |
700 def doubleWidthLine(self): | |
701 self.write('\x1b#6') | |
702 | |
703 def eraseToLineEnd(self): | |
704 self.write('\x1b[K') | |
705 | |
706 def eraseToLineBeginning(self): | |
707 self.write('\x1b[1K') | |
708 | |
709 def eraseLine(self): | |
710 self.write('\x1b[2K') | |
711 | |
712 def eraseToDisplayEnd(self): | |
713 self.write('\x1b[J') | |
714 | |
715 def eraseToDisplayBeginning(self): | |
716 self.write('\x1b[1J') | |
717 | |
718 def eraseDisplay(self): | |
719 self.write('\x1b[2J') | |
720 | |
721 def deleteCharacter(self, n=1): | |
722 self.write('\x1b[%dP' % (n,)) | |
723 | |
724 def insertLine(self, n=1): | |
725 self.write('\x1b[%dL' % (n,)) | |
726 | |
727 def deleteLine(self, n=1): | |
728 self.write('\x1b[%dM' % (n,)) | |
729 | |
730 def setScrollRegion(self, first=None, last=None): | |
731 if first is not None: | |
732 first = '%d' % (first,) | |
733 else: | |
734 first = '' | |
735 if last is not None: | |
736 last = '%d' % (last,) | |
737 else: | |
738 last = '' | |
739 self.write('\x1b[%s;%sr' % (first, last)) | |
740 | |
741 def resetScrollRegion(self): | |
742 self.setScrollRegion() | |
743 | |
744 def reportCursorPosition(self): | |
745 d = defer.Deferred() | |
746 self._cursorReports.append(d) | |
747 self.write('\x1b[6n') | |
748 return d | |
749 | |
750 def reset(self): | |
751 self.cursorPos.x = self.cursorPos.y = 0 | |
752 try: | |
753 del self._savedCursorPos | |
754 except AttributeError: | |
755 pass | |
756 self.write('\x1bc') | |
757 | |
758 # ITransport | |
759 def write(self, bytes): | |
760 if bytes: | |
761 self.lastWrite = bytes | |
762 self.transport.write('\r\n'.join(bytes.split('\n'))) | |
763 | |
764 def writeSequence(self, bytes): | |
765 self.write(''.join(bytes)) | |
766 | |
767 def loseConnection(self): | |
768 self.reset() | |
769 self.transport.loseConnection() | |
770 | |
771 def connectionLost(self, reason): | |
772 if self.terminalProtocol is not None: | |
773 try: | |
774 self.terminalProtocol.connectionLost(reason) | |
775 finally: | |
776 self.terminalProtocol = None | |
777 # Add symbolic names for function keys | |
778 for name, const in zip(_KEY_NAMES, FUNCTION_KEYS): | |
779 setattr(ServerProtocol, name, const) | |
780 | |
781 | |
782 | |
783 class ClientProtocol(protocol.Protocol): | |
784 | |
785 terminalFactory = None | |
786 terminal = None | |
787 | |
788 state = 'data' | |
789 | |
790 _escBuf = None | |
791 | |
792 _shorts = { | |
793 'D': 'index', | |
794 'M': 'reverseIndex', | |
795 'E': 'nextLine', | |
796 '7': 'saveCursor', | |
797 '8': 'restoreCursor', | |
798 '=': 'applicationKeypadMode', | |
799 '>': 'numericKeypadMode', | |
800 'N': 'singleShift2', | |
801 'O': 'singleShift3', | |
802 'H': 'horizontalTabulationSet', | |
803 'c': 'reset'} | |
804 | |
805 _longs = { | |
806 '[': 'bracket-escape', | |
807 '(': 'select-g0', | |
808 ')': 'select-g1', | |
809 '#': 'select-height-width'} | |
810 | |
811 _charsets = { | |
812 'A': CS_UK, | |
813 'B': CS_US, | |
814 '0': CS_DRAWING, | |
815 '1': CS_ALTERNATE, | |
816 '2': CS_ALTERNATE_SPECIAL} | |
817 | |
818 # Factory who instantiated me | |
819 factory = None | |
820 | |
821 def __init__(self, terminalFactory=None, *a, **kw): | |
822 """ | |
823 @param terminalFactory: A callable which will be invoked with | |
824 *a, **kw and should return an ITerminalTransport provider. | |
825 This will be invoked when this ClientProtocol establishes a | |
826 connection. | |
827 | |
828 @param a: Any positional arguments to pass to terminalFactory. | |
829 @param kw: Any keyword arguments to pass to terminalFactory. | |
830 """ | |
831 # assert terminalFactory is None or ITerminalTransport.implementedBy(ter
minalFactory), "ClientProtocol.__init__ must be passed an ITerminalTransport imp
lementor" | |
832 if terminalFactory is not None: | |
833 self.terminalFactory = terminalFactory | |
834 self.terminalArgs = a | |
835 self.terminalKwArgs = kw | |
836 | |
837 def connectionMade(self): | |
838 if self.terminalFactory is not None: | |
839 self.terminal = self.terminalFactory(*self.terminalArgs, **self.term
inalKwArgs) | |
840 self.terminal.factory = self.factory | |
841 self.terminal.makeConnection(self) | |
842 | |
843 def connectionLost(self, reason): | |
844 if self.terminal is not None: | |
845 try: | |
846 self.terminal.connectionLost(reason) | |
847 finally: | |
848 del self.terminal | |
849 | |
850 def dataReceived(self, bytes): | |
851 for b in bytes: | |
852 if self.state == 'data': | |
853 if b == '\x1b': | |
854 self.state = 'escaped' | |
855 elif b == '\x14': | |
856 self.terminal.shiftOut() | |
857 elif b == '\x15': | |
858 self.terminal.shiftIn() | |
859 elif b == '\x08': | |
860 self.terminal.cursorBackward() | |
861 else: | |
862 self.terminal.write(b) | |
863 elif self.state == 'escaped': | |
864 fName = self._shorts.get(b) | |
865 if fName is not None: | |
866 self.state = 'data' | |
867 getattr(self.terminal, fName)() | |
868 else: | |
869 state = self._longs.get(b) | |
870 if state is not None: | |
871 self.state = state | |
872 else: | |
873 self.terminal.unhandledControlSequence('\x1b' + b) | |
874 self.state = 'data' | |
875 elif self.state == 'bracket-escape': | |
876 if self._escBuf is None: | |
877 self._escBuf = [] | |
878 if b.isalpha() or b == '~': | |
879 self._handleControlSequence(''.join(self._escBuf), b) | |
880 del self._escBuf | |
881 self.state = 'data' | |
882 else: | |
883 self._escBuf.append(b) | |
884 elif self.state == 'select-g0': | |
885 self.terminal.selectCharacterSet(self._charsets.get(b, b), G0) | |
886 self.state = 'data' | |
887 elif self.state == 'select-g1': | |
888 self.terminal.selectCharacterSet(self._charsets.get(b, b), G1) | |
889 self.state = 'data' | |
890 elif self.state == 'select-height-width': | |
891 self._handleHeightWidth(b) | |
892 self.state = 'data' | |
893 else: | |
894 raise ValueError("Illegal state") | |
895 | |
896 def _handleControlSequence(self, buf, terminal): | |
897 f = getattr(self.controlSequenceParser, CST.get(terminal, terminal), Non
e) | |
898 if f is None: | |
899 self.terminal.unhandledControlSequence('\x1b[' + buf + terminal) | |
900 else: | |
901 f(self, self.terminal, buf) | |
902 | |
903 class ControlSequenceParser: | |
904 def _makeSimple(ch, fName): | |
905 n = 'cursor' + fName | |
906 def simple(self, proto, handler, buf): | |
907 if not buf: | |
908 getattr(handler, n)(1) | |
909 else: | |
910 try: | |
911 m = int(buf) | |
912 except ValueError: | |
913 handler.unhandledControlSequence('\x1b[' + buf + ch) | |
914 else: | |
915 getattr(handler, n)(m) | |
916 return simple | |
917 for (ch, fName) in (('A', 'Up'), | |
918 ('B', 'Down'), | |
919 ('C', 'Forward'), | |
920 ('D', 'Backward')): | |
921 exec ch + " = _makeSimple(ch, fName)" | |
922 del _makeSimple | |
923 | |
924 def h(self, proto, handler, buf): | |
925 # XXX - Handle '?' to introduce ANSI-Compatible private modes. | |
926 try: | |
927 modes = map(int, buf.split(';')) | |
928 except ValueError: | |
929 handler.unhandledControlSequence('\x1b[' + buf + 'h') | |
930 else: | |
931 handler.setModes(modes) | |
932 | |
933 def l(self, proto, handler, buf): | |
934 # XXX - Handle '?' to introduce ANSI-Compatible private modes. | |
935 try: | |
936 modes = map(int, buf.split(';')) | |
937 except ValueError: | |
938 handler.unhandledControlSequence('\x1b[' + buf + 'l') | |
939 else: | |
940 handler.resetModes(modes) | |
941 | |
942 def r(self, proto, handler, buf): | |
943 parts = buf.split(';') | |
944 if len(parts) == 1: | |
945 handler.setScrollRegion(None, None) | |
946 elif len(parts) == 2: | |
947 try: | |
948 if parts[0]: | |
949 pt = int(parts[0]) | |
950 else: | |
951 pt = None | |
952 if parts[1]: | |
953 pb = int(parts[1]) | |
954 else: | |
955 pb = None | |
956 except ValueError: | |
957 handler.unhandledControlSequence('\x1b[' + buf + 'r') | |
958 else: | |
959 handler.setScrollRegion(pt, pb) | |
960 else: | |
961 handler.unhandledControlSequence('\x1b[' + buf + 'r') | |
962 | |
963 def K(self, proto, handler, buf): | |
964 if not buf: | |
965 handler.eraseToLineEnd() | |
966 elif buf == '1': | |
967 handler.eraseToLineBeginning() | |
968 elif buf == '2': | |
969 handler.eraseLine() | |
970 else: | |
971 handler.unhandledControlSequence('\x1b[' + buf + 'K') | |
972 | |
973 def H(self, proto, handler, buf): | |
974 handler.cursorHome() | |
975 | |
976 def J(self, proto, handler, buf): | |
977 if not buf: | |
978 handler.eraseToDisplayEnd() | |
979 elif buf == '1': | |
980 handler.eraseToDisplayBeginning() | |
981 elif buf == '2': | |
982 handler.eraseDisplay() | |
983 else: | |
984 handler.unhandledControlSequence('\x1b[' + buf + 'J') | |
985 | |
986 def P(self, proto, handler, buf): | |
987 if not buf: | |
988 handler.deleteCharacter(1) | |
989 else: | |
990 try: | |
991 n = int(buf) | |
992 except ValueError: | |
993 handler.unhandledControlSequence('\x1b[' + buf + 'P') | |
994 else: | |
995 handler.deleteCharacter(n) | |
996 | |
997 def L(self, proto, handler, buf): | |
998 if not buf: | |
999 handler.insertLine(1) | |
1000 else: | |
1001 try: | |
1002 n = int(buf) | |
1003 except ValueError: | |
1004 handler.unhandledControlSequence('\x1b[' + buf + 'L') | |
1005 else: | |
1006 handler.insertLine(n) | |
1007 | |
1008 def M(self, proto, handler, buf): | |
1009 if not buf: | |
1010 handler.deleteLine(1) | |
1011 else: | |
1012 try: | |
1013 n = int(buf) | |
1014 except ValueError: | |
1015 handler.unhandledControlSequence('\x1b[' + buf + 'M') | |
1016 else: | |
1017 handler.deleteLine(n) | |
1018 | |
1019 def n(self, proto, handler, buf): | |
1020 if buf == '6': | |
1021 x, y = handler.reportCursorPosition() | |
1022 proto.transport.write('\x1b[%d;%dR' % (x + 1, y + 1)) | |
1023 else: | |
1024 handler.unhandledControlSequence('\x1b[' + buf + 'n') | |
1025 | |
1026 def m(self, proto, handler, buf): | |
1027 if not buf: | |
1028 handler.selectGraphicRendition(NORMAL) | |
1029 else: | |
1030 attrs = [] | |
1031 for a in buf.split(';'): | |
1032 try: | |
1033 a = int(a) | |
1034 except ValueError: | |
1035 pass | |
1036 attrs.append(a) | |
1037 handler.selectGraphicRendition(*attrs) | |
1038 | |
1039 controlSequenceParser = ControlSequenceParser() | |
1040 | |
1041 def _handleHeightWidth(self, b): | |
1042 if b == '3': | |
1043 self.terminal.doubleHeightLine(True) | |
1044 elif b == '4': | |
1045 self.terminal.doubleHeightLine(False) | |
1046 elif b == '5': | |
1047 self.terminal.singleWidthLine() | |
1048 elif b == '6': | |
1049 self.terminal.doubleWidthLine() | |
1050 else: | |
1051 self.terminal.unhandledControlSequence('\x1b#' + b) | |
1052 | |
1053 | |
1054 __all__ = [ | |
1055 # Interfaces | |
1056 'ITerminalProtocol', 'ITerminalTransport', | |
1057 | |
1058 # Symbolic constants | |
1059 'modes', 'privateModes', 'FUNCTION_KEYS', | |
1060 | |
1061 'CS_US', 'CS_UK', 'CS_DRAWING', 'CS_ALTERNATE', 'CS_ALTERNATE_SPECIAL', | |
1062 'G0', 'G1', 'G2', 'G3', | |
1063 | |
1064 'UNDERLINE', 'REVERSE_VIDEO', 'BLINK', 'BOLD', 'NORMAL', | |
1065 | |
1066 # Protocol classes | |
1067 'ServerProtocol', 'ClientProtocol'] | |
OLD | NEW |