1# -*- test-case-name: twisted.conch.test.test_insults -*- 2# Copyright (c) Twisted Matrix Laboratories. 3# See LICENSE for details. 4 5""" 6VT102 and VT220 terminal manipulation. 7 8@author: Jp Calderone 9""" 10 11from zope.interface import implements, Interface 12 13from twisted.internet import protocol, defer, interfaces as iinternet 14 15class 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 50class TerminalProtocol(object): 51 implements(ITerminalProtocol) 52 53 def makeConnection(self, terminal): 54 # assert ITerminalTransport.providedBy(transport), "TerminalProtocol.makeConnection 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 74class 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 scrolling if necessary. 109 """ 110 111 def saveCursor(): 112 """Save the cursor position, character attribute, character set, and origin mode selection. 113 """ 114 115 def restoreCursor(): 116 """Restore the previously saved cursor position, character attribute, character set, and origin mode selection. 117 118 If no cursor state was previously saved, move the cursor to the home position. 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 BOLD. 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, double-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 cursor 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 margin 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 the 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 270CSI = '\x1b' 271CST = {'~': 'tilde'} 272 273class 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 300class 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 320CS_US = 'CS_US' 321CS_UK = 'CS_UK' 322CS_DRAWING = 'CS_DRAWING' 323CS_ALTERNATE = 'CS_ALTERNATE' 324CS_ALTERNATE_SPECIAL = 'CS_ALTERNATE_SPECIAL' 325 326# Groupings (or something?? These are like variables that can be bound to character sets) 327G0 = 'G0' 328G1 = 'G1' 329 330# G2 and G3 cannot be changed, but they can be shifted to. 331G2 = 'G2' 332G3 = 'G3' 333 334# Character attributes 335 336NORMAL = 0 337BOLD = 1 338UNDERLINE = 4 339BLINK = 5 340REVERSE_VIDEO = 7 341 342class Vector: 343 def __init__(self, x, y): 344 self.x = x 345 self.y = y 346 347def 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 359class _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 370FUNCTION_KEYS = [ 371 _const(_name) for _name in _KEY_NAMES] 372 373class 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(protocolFactory), "ServerProtocol.__init__ must be passed an ITerminalProtocol implementor" 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, **self.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 778for name, const in zip(_KEY_NAMES, FUNCTION_KEYS): 779 setattr(ServerProtocol, name, const) 780 781 782 783class 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(terminalFactory), "ClientProtocol.__init__ must be passed an ITerminalTransport implementor" 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.terminalKwArgs) 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 """ 852 Parse the given data from a terminal server, dispatching to event 853 handlers defined by C{self.terminal}. 854 """ 855 toWrite = [] 856 for b in bytes: 857 if self.state == 'data': 858 if b == '\x1b': 859 if toWrite: 860 self.terminal.write(''.join(toWrite)) 861 del toWrite[:] 862 self.state = 'escaped' 863 elif b == '\x14': 864 if toWrite: 865 self.terminal.write(''.join(toWrite)) 866 del toWrite[:] 867 self.terminal.shiftOut() 868 elif b == '\x15': 869 if toWrite: 870 self.terminal.write(''.join(toWrite)) 871 del toWrite[:] 872 self.terminal.shiftIn() 873 elif b == '\x08': 874 if toWrite: 875 self.terminal.write(''.join(toWrite)) 876 del toWrite[:] 877 self.terminal.cursorBackward() 878 else: 879 toWrite.append(b) 880 elif self.state == 'escaped': 881 fName = self._shorts.get(b) 882 if fName is not None: 883 self.state = 'data' 884 getattr(self.terminal, fName)() 885 else: 886 state = self._longs.get(b) 887 if state is not None: 888 self.state = state 889 else: 890 self.terminal.unhandledControlSequence('\x1b' + b) 891 self.state = 'data' 892 elif self.state == 'bracket-escape': 893 if self._escBuf is None: 894 self._escBuf = [] 895 if b.isalpha() or b == '~': 896 self._handleControlSequence(''.join(self._escBuf), b) 897 del self._escBuf 898 self.state = 'data' 899 else: 900 self._escBuf.append(b) 901 elif self.state == 'select-g0': 902 self.terminal.selectCharacterSet(self._charsets.get(b, b), G0) 903 self.state = 'data' 904 elif self.state == 'select-g1': 905 self.terminal.selectCharacterSet(self._charsets.get(b, b), G1) 906 self.state = 'data' 907 elif self.state == 'select-height-width': 908 self._handleHeightWidth(b) 909 self.state = 'data' 910 else: 911 raise ValueError("Illegal state") 912 if toWrite: 913 self.terminal.write(''.join(toWrite)) 914 915 916 def _handleControlSequence(self, buf, terminal): 917 f = getattr(self.controlSequenceParser, CST.get(terminal, terminal), None) 918 if f is None: 919 self.terminal.unhandledControlSequence('\x1b[' + buf + terminal) 920 else: 921 f(self, self.terminal, buf) 922 923 class ControlSequenceParser: 924 def _makeSimple(ch, fName): 925 n = 'cursor' + fName 926 def simple(self, proto, handler, buf): 927 if not buf: 928 getattr(handler, n)(1) 929 else: 930 try: 931 m = int(buf) 932 except ValueError: 933 handler.unhandledControlSequence('\x1b[' + buf + ch) 934 else: 935 getattr(handler, n)(m) 936 return simple 937 for (ch, fName) in (('A', 'Up'), 938 ('B', 'Down'), 939 ('C', 'Forward'), 940 ('D', 'Backward')): 941 exec ch + " = _makeSimple(ch, fName)" 942 del _makeSimple 943 944 def h(self, proto, handler, buf): 945 # XXX - Handle '?' to introduce ANSI-Compatible private modes. 946 try: 947 modes = map(int, buf.split(';')) 948 except ValueError: 949 handler.unhandledControlSequence('\x1b[' + buf + 'h') 950 else: 951 handler.setModes(modes) 952 953 def l(self, proto, handler, buf): 954 # XXX - Handle '?' to introduce ANSI-Compatible private modes. 955 try: 956 modes = map(int, buf.split(';')) 957 except ValueError: 958 handler.unhandledControlSequence('\x1b[' + buf + 'l') 959 else: 960 handler.resetModes(modes) 961 962 def r(self, proto, handler, buf): 963 parts = buf.split(';') 964 if len(parts) == 1: 965 handler.setScrollRegion(None, None) 966 elif len(parts) == 2: 967 try: 968 if parts[0]: 969 pt = int(parts[0]) 970 else: 971 pt = None 972 if parts[1]: 973 pb = int(parts[1]) 974 else: 975 pb = None 976 except ValueError: 977 handler.unhandledControlSequence('\x1b[' + buf + 'r') 978 else: 979 handler.setScrollRegion(pt, pb) 980 else: 981 handler.unhandledControlSequence('\x1b[' + buf + 'r') 982 983 def K(self, proto, handler, buf): 984 if not buf: 985 handler.eraseToLineEnd() 986 elif buf == '1': 987 handler.eraseToLineBeginning() 988 elif buf == '2': 989 handler.eraseLine() 990 else: 991 handler.unhandledControlSequence('\x1b[' + buf + 'K') 992 993 def H(self, proto, handler, buf): 994 handler.cursorHome() 995 996 def J(self, proto, handler, buf): 997 if not buf: 998 handler.eraseToDisplayEnd() 999 elif buf == '1': 1000 handler.eraseToDisplayBeginning() 1001 elif buf == '2': 1002 handler.eraseDisplay() 1003 else: 1004 handler.unhandledControlSequence('\x1b[' + buf + 'J') 1005 1006 def P(self, proto, handler, buf): 1007 if not buf: 1008 handler.deleteCharacter(1) 1009 else: 1010 try: 1011 n = int(buf) 1012 except ValueError: 1013 handler.unhandledControlSequence('\x1b[' + buf + 'P') 1014 else: 1015 handler.deleteCharacter(n) 1016 1017 def L(self, proto, handler, buf): 1018 if not buf: 1019 handler.insertLine(1) 1020 else: 1021 try: 1022 n = int(buf) 1023 except ValueError: 1024 handler.unhandledControlSequence('\x1b[' + buf + 'L') 1025 else: 1026 handler.insertLine(n) 1027 1028 def M(self, proto, handler, buf): 1029 if not buf: 1030 handler.deleteLine(1) 1031 else: 1032 try: 1033 n = int(buf) 1034 except ValueError: 1035 handler.unhandledControlSequence('\x1b[' + buf + 'M') 1036 else: 1037 handler.deleteLine(n) 1038 1039 def n(self, proto, handler, buf): 1040 if buf == '6': 1041 x, y = handler.reportCursorPosition() 1042 proto.transport.write('\x1b[%d;%dR' % (x + 1, y + 1)) 1043 else: 1044 handler.unhandledControlSequence('\x1b[' + buf + 'n') 1045 1046 def m(self, proto, handler, buf): 1047 if not buf: 1048 handler.selectGraphicRendition(NORMAL) 1049 else: 1050 attrs = [] 1051 for a in buf.split(';'): 1052 try: 1053 a = int(a) 1054 except ValueError: 1055 pass 1056 attrs.append(a) 1057 handler.selectGraphicRendition(*attrs) 1058 1059 controlSequenceParser = ControlSequenceParser() 1060 1061 def _handleHeightWidth(self, b): 1062 if b == '3': 1063 self.terminal.doubleHeightLine(True) 1064 elif b == '4': 1065 self.terminal.doubleHeightLine(False) 1066 elif b == '5': 1067 self.terminal.singleWidthLine() 1068 elif b == '6': 1069 self.terminal.doubleWidthLine() 1070 else: 1071 self.terminal.unhandledControlSequence('\x1b#' + b) 1072 1073 1074__all__ = [ 1075 # Interfaces 1076 'ITerminalProtocol', 'ITerminalTransport', 1077 1078 # Symbolic constants 1079 'modes', 'privateModes', 'FUNCTION_KEYS', 1080 1081 'CS_US', 'CS_UK', 'CS_DRAWING', 'CS_ALTERNATE', 'CS_ALTERNATE_SPECIAL', 1082 'G0', 'G1', 'G2', 'G3', 1083 1084 'UNDERLINE', 'REVERSE_VIDEO', 'BLINK', 'BOLD', 'NORMAL', 1085 1086 # Protocol classes 1087 'ServerProtocol', 'ClientProtocol'] 1088