root/trunk/twisted/conch/recvline.py

Revision 32505, 11.0 KB (checked in by exarkun, 9 months ago)

Merge recvline-fnkeys-5246

Author: djfroofy
Reviewer: exarkun
Fixes: #5246

Handle non-string keystrokes in twisted.conch.recvline.RecvLine.keystrokeReceived so that
they do not cause a exception to be raised when they are received. This makes things like
F1-F12 and PGUP and PGDN not lead to a RecvLine being disconnected.

Line 
1# -*- test-case-name: twisted.conch.test.test_recvline -*-
2# Copyright (c) Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5"""
6Basic line editing support.
7
8@author: Jp Calderone
9"""
10
11import string
12
13from zope.interface import implements
14
15from twisted.conch.insults import insults, helper
16
17from twisted.python import log, reflect
18
19_counters = {}
20class Logging(object):
21    """Wrapper which logs attribute lookups.
22
23    This was useful in debugging something, I guess.  I forget what.
24    It can probably be deleted or moved somewhere more appropriate.
25    Nothing special going on here, really.
26    """
27    def __init__(self, original):
28        self.original = original
29        key = reflect.qual(original.__class__)
30        count = _counters.get(key, 0)
31        _counters[key] = count + 1
32        self._logFile = file(key + '-' + str(count), 'w')
33
34    def __str__(self):
35        return str(super(Logging, self).__getattribute__('original'))
36
37    def __repr__(self):
38        return repr(super(Logging, self).__getattribute__('original'))
39
40    def __getattribute__(self, name):
41        original = super(Logging, self).__getattribute__('original')
42        logFile = super(Logging, self).__getattribute__('_logFile')
43        logFile.write(name + '\n')
44        return getattr(original, name)
45
46class TransportSequence(object):
47    """An L{ITerminalTransport} implementation which forwards calls to
48    one or more other L{ITerminalTransport}s.
49
50    This is a cheap way for servers to keep track of the state they
51    expect the client to see, since all terminal manipulations can be
52    send to the real client and to a terminal emulator that lives in
53    the server process.
54    """
55    implements(insults.ITerminalTransport)
56
57    for keyID in ('UP_ARROW', 'DOWN_ARROW', 'RIGHT_ARROW', 'LEFT_ARROW',
58                  'HOME', 'INSERT', 'DELETE', 'END', 'PGUP', 'PGDN',
59                  'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9',
60                  'F10', 'F11', 'F12'):
61        exec '%s = object()' % (keyID,)
62
63    TAB = '\t'
64    BACKSPACE = '\x7f'
65
66    def __init__(self, *transports):
67        assert transports, "Cannot construct a TransportSequence with no transports"
68        self.transports = transports
69
70    for method in insults.ITerminalTransport:
71        exec """\
72def %s(self, *a, **kw):
73    for tpt in self.transports:
74        result = tpt.%s(*a, **kw)
75    return result
76""" % (method, method)
77
78class LocalTerminalBufferMixin(object):
79    """A mixin for RecvLine subclasses which records the state of the terminal.
80
81    This is accomplished by performing all L{ITerminalTransport} operations on both
82    the transport passed to makeConnection and an instance of helper.TerminalBuffer.
83
84    @ivar terminalCopy: A L{helper.TerminalBuffer} instance which efforts
85    will be made to keep up to date with the actual terminal
86    associated with this protocol instance.
87    """
88
89    def makeConnection(self, transport):
90        self.terminalCopy = helper.TerminalBuffer()
91        self.terminalCopy.connectionMade()
92        return super(LocalTerminalBufferMixin, self).makeConnection(
93            TransportSequence(transport, self.terminalCopy))
94
95    def __str__(self):
96        return str(self.terminalCopy)
97
98class RecvLine(insults.TerminalProtocol):
99    """L{TerminalProtocol} which adds line editing features.
100
101    Clients will be prompted for lines of input with all the usual
102    features: character echoing, left and right arrow support for
103    moving the cursor to different areas of the line buffer, backspace
104    and delete for removing characters, and insert for toggling
105    between typeover and insert mode.  Tabs will be expanded to enough
106    spaces to move the cursor to the next tabstop (every four
107    characters by default).  Enter causes the line buffer to be
108    cleared and the line to be passed to the lineReceived() method
109    which, by default, does nothing.  Subclasses are responsible for
110    redrawing the input prompt (this will probably change).
111    """
112    width = 80
113    height = 24
114
115    TABSTOP = 4
116
117    ps = ('>>> ', '... ')
118    pn = 0
119    _printableChars = set(string.printable)
120
121    def connectionMade(self):
122        # A list containing the characters making up the current line
123        self.lineBuffer = []
124
125        # A zero-based (wtf else?) index into self.lineBuffer.
126        # Indicates the current cursor position.
127        self.lineBufferIndex = 0
128
129        t = self.terminal
130        # A map of keyIDs to bound instance methods.
131        self.keyHandlers = {
132            t.LEFT_ARROW: self.handle_LEFT,
133            t.RIGHT_ARROW: self.handle_RIGHT,
134            t.TAB: self.handle_TAB,
135
136            # Both of these should not be necessary, but figuring out
137            # which is necessary is a huge hassle.
138            '\r': self.handle_RETURN,
139            '\n': self.handle_RETURN,
140
141            t.BACKSPACE: self.handle_BACKSPACE,
142            t.DELETE: self.handle_DELETE,
143            t.INSERT: self.handle_INSERT,
144            t.HOME: self.handle_HOME,
145            t.END: self.handle_END}
146
147        self.initializeScreen()
148
149    def initializeScreen(self):
150        # Hmm, state sucks.  Oh well.
151        # For now we will just take over the whole terminal.
152        self.terminal.reset()
153        self.terminal.write(self.ps[self.pn])
154        # XXX Note: I would prefer to default to starting in insert
155        # mode, however this does not seem to actually work!  I do not
156        # know why.  This is probably of interest to implementors
157        # subclassing RecvLine.
158
159        # XXX XXX Note: But the unit tests all expect the initial mode
160        # to be insert right now.  Fuck, there needs to be a way to
161        # query the current mode or something.
162        # self.setTypeoverMode()
163        self.setInsertMode()
164
165    def currentLineBuffer(self):
166        s = ''.join(self.lineBuffer)
167        return s[:self.lineBufferIndex], s[self.lineBufferIndex:]
168
169    def setInsertMode(self):
170        self.mode = 'insert'
171        self.terminal.setModes([insults.modes.IRM])
172
173    def setTypeoverMode(self):
174        self.mode = 'typeover'
175        self.terminal.resetModes([insults.modes.IRM])
176
177    def drawInputLine(self):
178        """
179        Write a line containing the current input prompt and the current line
180        buffer at the current cursor position.
181        """
182        self.terminal.write(self.ps[self.pn] + ''.join(self.lineBuffer))
183
184    def terminalSize(self, width, height):
185        # XXX - Clear the previous input line, redraw it at the new
186        # cursor position
187        self.terminal.eraseDisplay()
188        self.terminal.cursorHome()
189        self.width = width
190        self.height = height
191        self.drawInputLine()
192
193    def unhandledControlSequence(self, seq):
194        pass
195
196    def keystrokeReceived(self, keyID, modifier):
197        m = self.keyHandlers.get(keyID)
198        if m is not None:
199            m()
200        elif keyID in self._printableChars:
201            self.characterReceived(keyID, False)
202        else:
203            log.msg("Received unhandled keyID: %r" % (keyID,))
204
205    def characterReceived(self, ch, moreCharactersComing):
206        if self.mode == 'insert':
207            self.lineBuffer.insert(self.lineBufferIndex, ch)
208        else:
209            self.lineBuffer[self.lineBufferIndex:self.lineBufferIndex+1] = [ch]
210        self.lineBufferIndex += 1
211        self.terminal.write(ch)
212
213    def handle_TAB(self):
214        n = self.TABSTOP - (len(self.lineBuffer) % self.TABSTOP)
215        self.terminal.cursorForward(n)
216        self.lineBufferIndex += n
217        self.lineBuffer.extend(' ' * n)
218
219    def handle_LEFT(self):
220        if self.lineBufferIndex > 0:
221            self.lineBufferIndex -= 1
222            self.terminal.cursorBackward()
223
224    def handle_RIGHT(self):
225        if self.lineBufferIndex < len(self.lineBuffer):
226            self.lineBufferIndex += 1
227            self.terminal.cursorForward()
228
229    def handle_HOME(self):
230        if self.lineBufferIndex:
231            self.terminal.cursorBackward(self.lineBufferIndex)
232            self.lineBufferIndex = 0
233
234    def handle_END(self):
235        offset = len(self.lineBuffer) - self.lineBufferIndex
236        if offset:
237            self.terminal.cursorForward(offset)
238            self.lineBufferIndex = len(self.lineBuffer)
239
240    def handle_BACKSPACE(self):
241        if self.lineBufferIndex > 0:
242            self.lineBufferIndex -= 1
243            del self.lineBuffer[self.lineBufferIndex]
244            self.terminal.cursorBackward()
245            self.terminal.deleteCharacter()
246
247    def handle_DELETE(self):
248        if self.lineBufferIndex < len(self.lineBuffer):
249            del self.lineBuffer[self.lineBufferIndex]
250            self.terminal.deleteCharacter()
251
252    def handle_RETURN(self):
253        line = ''.join(self.lineBuffer)
254        self.lineBuffer = []
255        self.lineBufferIndex = 0
256        self.terminal.nextLine()
257        self.lineReceived(line)
258
259    def handle_INSERT(self):
260        assert self.mode in ('typeover', 'insert')
261        if self.mode == 'typeover':
262            self.setInsertMode()
263        else:
264            self.setTypeoverMode()
265
266    def lineReceived(self, line):
267        pass
268
269class HistoricRecvLine(RecvLine):
270    """L{TerminalProtocol} which adds both basic line-editing features and input history.
271
272    Everything supported by L{RecvLine} is also supported by this class.  In addition, the
273    up and down arrows traverse the input history.  Each received line is automatically
274    added to the end of the input history.
275    """
276    def connectionMade(self):
277        RecvLine.connectionMade(self)
278
279        self.historyLines = []
280        self.historyPosition = 0
281
282        t = self.terminal
283        self.keyHandlers.update({t.UP_ARROW: self.handle_UP,
284                                 t.DOWN_ARROW: self.handle_DOWN})
285
286    def currentHistoryBuffer(self):
287        b = tuple(self.historyLines)
288        return b[:self.historyPosition], b[self.historyPosition:]
289
290    def _deliverBuffer(self, buf):
291        if buf:
292            for ch in buf[:-1]:
293                self.characterReceived(ch, True)
294            self.characterReceived(buf[-1], False)
295
296    def handle_UP(self):
297        if self.lineBuffer and self.historyPosition == len(self.historyLines):
298            self.historyLines.append(self.lineBuffer)
299        if self.historyPosition > 0:
300            self.handle_HOME()
301            self.terminal.eraseToLineEnd()
302
303            self.historyPosition -= 1
304            self.lineBuffer = []
305
306            self._deliverBuffer(self.historyLines[self.historyPosition])
307
308    def handle_DOWN(self):
309        if self.historyPosition < len(self.historyLines) - 1:
310            self.handle_HOME()
311            self.terminal.eraseToLineEnd()
312
313            self.historyPosition += 1
314            self.lineBuffer = []
315
316            self._deliverBuffer(self.historyLines[self.historyPosition])
317        else:
318            self.handle_HOME()
319            self.terminal.eraseToLineEnd()
320
321            self.historyPosition = len(self.historyLines)
322            self.lineBuffer = []
323            self.lineBufferIndex = 0
324
325    def handle_RETURN(self):
326        if self.lineBuffer:
327            self.historyLines.append(''.join(self.lineBuffer))
328        self.historyPosition = len(self.historyLines)
329        return RecvLine.handle_RETURN(self)
Note: See TracBrowser for help on using the browser.