| OLD | NEW |
| (Empty) |
| 1 # -*- test-case-name: twisted.conch.test.test_recvline -*- | |
| 2 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
| 3 # See LICENSE for details. | |
| 4 | |
| 5 """ | |
| 6 Basic line editing support. | |
| 7 | |
| 8 @author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>} | |
| 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 transpo
rts" | |
| 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 b
oth | |
| 82 the transport passed to makeConnection and an instance of helper.TerminalBuf
fer. | |
| 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 | |
| 120 def connectionMade(self): | |
| 121 # A list containing the characters making up the current line | |
| 122 self.lineBuffer = [] | |
| 123 | |
| 124 # A zero-based (wtf else?) index into self.lineBuffer. | |
| 125 # Indicates the current cursor position. | |
| 126 self.lineBufferIndex = 0 | |
| 127 | |
| 128 t = self.terminal | |
| 129 # A map of keyIDs to bound instance methods. | |
| 130 self.keyHandlers = { | |
| 131 t.LEFT_ARROW: self.handle_LEFT, | |
| 132 t.RIGHT_ARROW: self.handle_RIGHT, | |
| 133 t.TAB: self.handle_TAB, | |
| 134 | |
| 135 # Both of these should not be necessary, but figuring out | |
| 136 # which is necessary is a huge hassle. | |
| 137 '\r': self.handle_RETURN, | |
| 138 '\n': self.handle_RETURN, | |
| 139 | |
| 140 t.BACKSPACE: self.handle_BACKSPACE, | |
| 141 t.DELETE: self.handle_DELETE, | |
| 142 t.INSERT: self.handle_INSERT, | |
| 143 t.HOME: self.handle_HOME, | |
| 144 t.END: self.handle_END} | |
| 145 | |
| 146 self.initializeScreen() | |
| 147 | |
| 148 def initializeScreen(self): | |
| 149 # Hmm, state sucks. Oh well. | |
| 150 # For now we will just take over the whole terminal. | |
| 151 self.terminal.reset() | |
| 152 self.terminal.write(self.ps[self.pn]) | |
| 153 # XXX Note: I would prefer to default to starting in insert | |
| 154 # mode, however this does not seem to actually work! I do not | |
| 155 # know why. This is probably of interest to implementors | |
| 156 # subclassing RecvLine. | |
| 157 | |
| 158 # XXX XXX Note: But the unit tests all expect the initial mode | |
| 159 # to be insert right now. Fuck, there needs to be a way to | |
| 160 # query the current mode or something. | |
| 161 # self.setTypeoverMode() | |
| 162 self.setInsertMode() | |
| 163 | |
| 164 def currentLineBuffer(self): | |
| 165 s = ''.join(self.lineBuffer) | |
| 166 return s[:self.lineBufferIndex], s[self.lineBufferIndex:] | |
| 167 | |
| 168 def setInsertMode(self): | |
| 169 self.mode = 'insert' | |
| 170 self.terminal.setModes([insults.modes.IRM]) | |
| 171 | |
| 172 def setTypeoverMode(self): | |
| 173 self.mode = 'typeover' | |
| 174 self.terminal.resetModes([insults.modes.IRM]) | |
| 175 | |
| 176 def drawInputLine(self): | |
| 177 """ | |
| 178 Write a line containing the current input prompt and the current line | |
| 179 buffer at the current cursor position. | |
| 180 """ | |
| 181 self.terminal.write(self.ps[self.pn] + ''.join(self.lineBuffer)) | |
| 182 | |
| 183 def terminalSize(self, width, height): | |
| 184 # XXX - Clear the previous input line, redraw it at the new | |
| 185 # cursor position | |
| 186 self.terminal.eraseDisplay() | |
| 187 self.terminal.cursorHome() | |
| 188 self.width = width | |
| 189 self.height = height | |
| 190 self.drawInputLine() | |
| 191 | |
| 192 def unhandledControlSequence(self, seq): | |
| 193 pass | |
| 194 | |
| 195 def keystrokeReceived(self, keyID, modifier): | |
| 196 m = self.keyHandlers.get(keyID) | |
| 197 if m is not None: | |
| 198 m() | |
| 199 elif keyID in string.printable: | |
| 200 self.characterReceived(keyID, False) | |
| 201 else: | |
| 202 log.msg("Received unhandled keyID: %r" % (keyID,)) | |
| 203 | |
| 204 def characterReceived(self, ch, moreCharactersComing): | |
| 205 if self.mode == 'insert': | |
| 206 self.lineBuffer.insert(self.lineBufferIndex, ch) | |
| 207 else: | |
| 208 self.lineBuffer[self.lineBufferIndex:self.lineBufferIndex+1] = [ch] | |
| 209 self.lineBufferIndex += 1 | |
| 210 self.terminal.write(ch) | |
| 211 | |
| 212 def handle_TAB(self): | |
| 213 n = self.TABSTOP - (len(self.lineBuffer) % self.TABSTOP) | |
| 214 self.terminal.cursorForward(n) | |
| 215 self.lineBufferIndex += n | |
| 216 self.lineBuffer.extend(' ' * n) | |
| 217 | |
| 218 def handle_LEFT(self): | |
| 219 if self.lineBufferIndex > 0: | |
| 220 self.lineBufferIndex -= 1 | |
| 221 self.terminal.cursorBackward() | |
| 222 | |
| 223 def handle_RIGHT(self): | |
| 224 if self.lineBufferIndex < len(self.lineBuffer): | |
| 225 self.lineBufferIndex += 1 | |
| 226 self.terminal.cursorForward() | |
| 227 | |
| 228 def handle_HOME(self): | |
| 229 if self.lineBufferIndex: | |
| 230 self.terminal.cursorBackward(self.lineBufferIndex) | |
| 231 self.lineBufferIndex = 0 | |
| 232 | |
| 233 def handle_END(self): | |
| 234 offset = len(self.lineBuffer) - self.lineBufferIndex | |
| 235 if offset: | |
| 236 self.terminal.cursorForward(offset) | |
| 237 self.lineBufferIndex = len(self.lineBuffer) | |
| 238 | |
| 239 def handle_BACKSPACE(self): | |
| 240 if self.lineBufferIndex > 0: | |
| 241 self.lineBufferIndex -= 1 | |
| 242 del self.lineBuffer[self.lineBufferIndex] | |
| 243 self.terminal.cursorBackward() | |
| 244 self.terminal.deleteCharacter() | |
| 245 | |
| 246 def handle_DELETE(self): | |
| 247 if self.lineBufferIndex < len(self.lineBuffer): | |
| 248 del self.lineBuffer[self.lineBufferIndex] | |
| 249 self.terminal.deleteCharacter() | |
| 250 | |
| 251 def handle_RETURN(self): | |
| 252 line = ''.join(self.lineBuffer) | |
| 253 self.lineBuffer = [] | |
| 254 self.lineBufferIndex = 0 | |
| 255 self.terminal.nextLine() | |
| 256 self.lineReceived(line) | |
| 257 | |
| 258 def handle_INSERT(self): | |
| 259 assert self.mode in ('typeover', 'insert') | |
| 260 if self.mode == 'typeover': | |
| 261 self.setInsertMode() | |
| 262 else: | |
| 263 self.setTypeoverMode() | |
| 264 | |
| 265 def lineReceived(self, line): | |
| 266 pass | |
| 267 | |
| 268 class HistoricRecvLine(RecvLine): | |
| 269 """L{TerminalProtocol} which adds both basic line-editing features and input
history. | |
| 270 | |
| 271 Everything supported by L{RecvLine} is also supported by this class. In add
ition, the | |
| 272 up and down arrows traverse the input history. Each received line is automa
tically | |
| 273 added to the end of the input history. | |
| 274 """ | |
| 275 def connectionMade(self): | |
| 276 RecvLine.connectionMade(self) | |
| 277 | |
| 278 self.historyLines = [] | |
| 279 self.historyPosition = 0 | |
| 280 | |
| 281 t = self.terminal | |
| 282 self.keyHandlers.update({t.UP_ARROW: self.handle_UP, | |
| 283 t.DOWN_ARROW: self.handle_DOWN}) | |
| 284 | |
| 285 def currentHistoryBuffer(self): | |
| 286 b = tuple(self.historyLines) | |
| 287 return b[:self.historyPosition], b[self.historyPosition:] | |
| 288 | |
| 289 def _deliverBuffer(self, buf): | |
| 290 if buf: | |
| 291 for ch in buf[:-1]: | |
| 292 self.characterReceived(ch, True) | |
| 293 self.characterReceived(buf[-1], False) | |
| 294 | |
| 295 def handle_UP(self): | |
| 296 if self.lineBuffer and self.historyPosition == len(self.historyLines): | |
| 297 self.historyLines.append(self.lineBuffer) | |
| 298 if self.historyPosition > 0: | |
| 299 self.handle_HOME() | |
| 300 self.terminal.eraseToLineEnd() | |
| 301 | |
| 302 self.historyPosition -= 1 | |
| 303 self.lineBuffer = [] | |
| 304 | |
| 305 self._deliverBuffer(self.historyLines[self.historyPosition]) | |
| 306 | |
| 307 def handle_DOWN(self): | |
| 308 if self.historyPosition < len(self.historyLines) - 1: | |
| 309 self.handle_HOME() | |
| 310 self.terminal.eraseToLineEnd() | |
| 311 | |
| 312 self.historyPosition += 1 | |
| 313 self.lineBuffer = [] | |
| 314 | |
| 315 self._deliverBuffer(self.historyLines[self.historyPosition]) | |
| 316 else: | |
| 317 self.handle_HOME() | |
| 318 self.terminal.eraseToLineEnd() | |
| 319 | |
| 320 self.historyPosition = len(self.historyLines) | |
| 321 self.lineBuffer = [] | |
| 322 self.lineBufferIndex = 0 | |
| 323 | |
| 324 def handle_RETURN(self): | |
| 325 if self.lineBuffer: | |
| 326 self.historyLines.append(''.join(self.lineBuffer)) | |
| 327 self.historyPosition = len(self.historyLines) | |
| 328 return RecvLine.handle_RETURN(self) | |
| OLD | NEW |