Initial import to Tizen
[profile/ivi/python-twisted.git] / twisted / conch / recvline.py
1 # -*- test-case-name: twisted.conch.test.test_recvline -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
4
5 """
6 Basic line editing support.
7
8 @author: Jp Calderone
9 """
10
11 import string
12
13 from zope.interface import implements
14
15 from twisted.conch.insults import insults, helper
16
17 from twisted.python import log, reflect
18
19 _counters = {}
20 class 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
46 class 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 """\
72 def %s(self, *a, **kw):
73     for tpt in self.transports:
74         result = tpt.%s(*a, **kw)
75     return result
76 """ % (method, method)
77
78 class 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
98 class 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
269 class 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)