1
# -*- test-case-name: twisted.conch.test.test_helper -*-
2
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
3
# See LICENSE for details.
5
"""Partial in-memory terminal emulator
7
API Stability: Unstable
9
@author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>}
14
from zope.interface import implements
16
from twisted.internet import defer, protocol, reactor
17
from twisted.python import log
19
from twisted.conch.insults import insults
23
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, N_COLORS = range(9)
25
class CharacterAttribute:
26
"""Represents the attributes of a single character.
28
Character set, intensity, underlinedness, blinkitude, video
29
reversal, as well as foreground and background colors made up a
30
character's attributes.
32
def __init__(self, charset=insults.G0,
33
bold=False, underline=False,
34
blink=False, reverseVideo=False,
35
foreground=WHITE, background=BLACK,
38
self.charset = charset
40
self.underline = underline
42
self.reverseVideo = reverseVideo
43
self.foreground = foreground
44
self.background = background
46
self._subtracting = _subtracting
48
def __eq__(self, other):
49
return vars(self) == vars(other)
51
def __ne__(self, other):
52
return not self.__eq__(other)
56
c.__dict__.update(vars(self))
59
def wantOne(self, **kw):
61
if getattr(self, k) != v:
63
attr._subtracting = not v
70
# Spit out a vt102 control sequence that will set up
71
# all the attributes set here. Except charset.
76
attrs.append(insults.BOLD)
78
attrs.append(insults.UNDERLINE)
80
attrs.append(insults.BLINK)
82
attrs.append(insults.REVERSE_VIDEO)
83
if self.foreground != WHITE:
84
attrs.append(FOREGROUND + self.foreground)
85
if self.background != BLACK:
86
attrs.append(BACKGROUND + self.background)
88
return '\x1b[' + ';'.join(map(str, attrs)) + 'm'
91
# XXX - need to support scroll regions and scroll history
92
class TerminalBuffer(protocol.Protocol):
93
implements(insults.ITerminalTransport)
95
for keyID in ('UP_ARROW', 'DOWN_ARROW', 'RIGHT_ARROW', 'LEFT_ARROW',
96
'HOME', 'INSERT', 'DELETE', 'END', 'PGUP', 'PGDN',
97
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9',
99
exec '%s = object()' % (keyID,)
110
def getCharacter(self, x, y):
111
return self.lines[y][x]
113
def connectionMade(self):
116
def write(self, bytes):
118
self.insertAtCursor(b)
120
def _currentCharacterAttributes(self):
121
return CharacterAttribute(self.activeCharset, **self.graphicRendition)
123
def insertAtCursor(self, b):
126
elif b == '\n' or self.x >= self.width:
129
if b in string.printable and b not in '\r\n':
130
ch = (b, self._currentCharacterAttributes())
131
if self.modes.get(insults.modes.IRM):
132
self.lines[self.y][self.x:self.x] = [ch]
133
self.lines[self.y].pop()
135
self.lines[self.y][self.x] = ch
138
def _emptyLine(self, width):
139
return [(self.void, self._currentCharacterAttributes()) for i in xrange(width)]
141
def _scrollDown(self):
143
if self.y >= self.height:
146
self.lines.append(self._emptyLine(self.width))
153
self.lines.insert(0, self._emptyLine(self.width))
155
def cursorUp(self, n=1):
156
self.y = max(0, self.y - n)
158
def cursorDown(self, n=1):
159
self.y = min(self.height - 1, self.y + n)
161
def cursorBackward(self, n=1):
162
self.x = max(0, self.x - n)
164
def cursorForward(self, n=1):
165
self.x = min(self.width, self.x + n)
167
def cursorPosition(self, column, line):
171
def cursorHome(self):
178
def reverseIndex(self):
182
self.insertAtCursor('\n')
184
def saveCursor(self):
185
self._savedCursor = (self.x, self.y)
187
def restoreCursor(self):
188
self.x, self.y = self._savedCursor
189
del self._savedCursor
191
def setModes(self, modes):
195
def resetModes(self, modes):
202
def applicationKeypadMode(self):
203
self.keypadMode = 'app'
205
def numericKeypadMode(self):
206
self.keypadMode = 'num'
208
def selectCharacterSet(self, charSet, which):
209
self.charsets[which] = charSet
212
self.activeCharset = insults.G0
215
self.activeCharset = insults.G1
217
def singleShift2(self):
218
oldActiveCharset = self.activeCharset
219
self.activeCharset = insults.G2
220
f = self.insertAtCursor
221
def insertAtCursor(b):
223
del self.insertAtCursor
224
self.activeCharset = oldActiveCharset
225
self.insertAtCursor = insertAtCursor
227
def singleShift3(self):
228
oldActiveCharset = self.activeCharset
229
self.activeCharset = insults.G3
230
f = self.insertAtCursor
231
def insertAtCursor(b):
233
del self.insertAtCursor
234
self.activeCharset = oldActiveCharset
235
self.insertAtCursor = insertAtCursor
237
def selectGraphicRendition(self, *attributes):
239
if a == insults.NORMAL:
240
self.graphicRendition = {
244
'reverseVideo': False,
247
elif a == insults.BOLD:
248
self.graphicRendition['bold'] = True
249
elif a == insults.UNDERLINE:
250
self.graphicRendition['underline'] = True
251
elif a == insults.BLINK:
252
self.graphicRendition['blink'] = True
253
elif a == insults.REVERSE_VIDEO:
254
self.graphicRendition['reverseVideo'] = True
259
log.msg("Unknown graphic rendition attribute: " + repr(a))
261
if FOREGROUND <= v <= FOREGROUND + N_COLORS:
262
self.graphicRendition['foreground'] = v - FOREGROUND
263
elif BACKGROUND <= v <= BACKGROUND + N_COLORS:
264
self.graphicRendition['background'] = v - BACKGROUND
266
log.msg("Unknown graphic rendition attribute: " + repr(a))
269
self.lines[self.y] = self._emptyLine(self.width)
271
def eraseToLineEnd(self):
272
width = self.width - self.x
273
self.lines[self.y][self.x:] = self._emptyLine(width)
275
def eraseToLineBeginning(self):
276
self.lines[self.y][:self.x + 1] = self._emptyLine(self.x + 1)
278
def eraseDisplay(self):
279
self.lines = [self._emptyLine(self.width) for i in xrange(self.height)]
281
def eraseToDisplayEnd(self):
282
self.eraseToLineEnd()
283
height = self.height - self.y - 1
284
self.lines[self.y + 1:] = [self._emptyLine(self.width) for i in range(height)]
286
def eraseToDisplayBeginning(self):
287
self.eraseToLineBeginning()
288
self.lines[:self.y] = [self._emptyLine(self.width) for i in range(self.y)]
290
def deleteCharacter(self, n=1):
291
del self.lines[self.y][self.x:self.x+n]
292
self.lines[self.y].extend(self._emptyLine(min(self.width - self.x, n)))
294
def insertLine(self, n=1):
295
self.lines[self.y:self.y] = [self._emptyLine(self.width) for i in range(n)]
296
del self.lines[self.height:]
298
def deleteLine(self, n=1):
299
del self.lines[self.y:self.y+n]
300
self.lines.extend([self._emptyLine(self.width) for i in range(n)])
302
def reportCursorPosition(self):
303
return (self.x, self.y)
306
self.home = insults.Vector(0, 0)
309
self.numericKeypad = 'app'
310
self.activeCharset = insults.G0
311
self.graphicRendition = {
315
'reverseVideo': False,
319
insults.G0: insults.CS_US,
320
insults.G1: insults.CS_US,
321
insults.G2: insults.CS_ALTERNATE,
322
insults.G3: insults.CS_ALTERNATE_SPECIAL}
325
def unhandledControlSequence(self, buf):
326
print 'Could not handle', repr(buf)
334
if ch is not self.void:
338
buf.append(self.fill)
339
lines.append(''.join(buf[:length]))
340
return '\n'.join(lines)
342
class ExpectationTimeout(Exception):
345
class ExpectableBuffer(TerminalBuffer):
348
def connectionMade(self):
349
TerminalBuffer.connectionMade(self)
352
def write(self, bytes):
353
TerminalBuffer.write(self, bytes)
354
self._checkExpected()
356
def cursorHome(self):
357
TerminalBuffer.cursorHome(self)
360
def _timeoutExpected(self, d):
361
d.errback(ExpectationTimeout())
362
self._checkExpected()
364
def _checkExpected(self):
365
s = str(self)[self._mark:]
366
while self._expecting:
367
expr, timer, deferred = self._expecting[0]
368
if timer and not timer.active():
369
del self._expecting[0]
371
for match in expr.finditer(s):
374
del self._expecting[0]
375
self._mark += match.end()
377
deferred.callback(match)
382
def expect(self, expression, timeout=None, scheduler=reactor):
386
timer = scheduler.callLater(timeout, self._timeoutExpected, d)
387
self._expecting.append((re.compile(expression), timer, d))
388
self._checkExpected()
391
__all__ = ['CharacterAttribute', 'TerminalBuffer', 'ExpectableBuffer']