| 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 |