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