aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/Twisted/py3/twisted/conch/insults
diff options
context:
space:
mode:
authorshmel1k <shmel1k@ydb.tech>2023-11-26 18:16:14 +0300
committershmel1k <shmel1k@ydb.tech>2023-11-26 18:43:30 +0300
commitb8cf9e88f4c5c64d9406af533d8948deb050d695 (patch)
tree218eb61fb3c3b96ec08b4d8cdfef383104a87d63 /contrib/python/Twisted/py3/twisted/conch/insults
parent523f645a83a0ec97a0332dbc3863bb354c92a328 (diff)
downloadydb-b8cf9e88f4c5c64d9406af533d8948deb050d695.tar.gz
add kikimr_configure
Diffstat (limited to 'contrib/python/Twisted/py3/twisted/conch/insults')
-rw-r--r--contrib/python/Twisted/py3/twisted/conch/insults/__init__.py4
-rw-r--r--contrib/python/Twisted/py3/twisted/conch/insults/helper.py556
-rw-r--r--contrib/python/Twisted/py3/twisted/conch/insults/insults.py1223
-rw-r--r--contrib/python/Twisted/py3/twisted/conch/insults/text.py176
-rw-r--r--contrib/python/Twisted/py3/twisted/conch/insults/window.py928
5 files changed, 2887 insertions, 0 deletions
diff --git a/contrib/python/Twisted/py3/twisted/conch/insults/__init__.py b/contrib/python/Twisted/py3/twisted/conch/insults/__init__.py
new file mode 100644
index 0000000000..3d83876698
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/conch/insults/__init__.py
@@ -0,0 +1,4 @@
+"""
+Insults: a replacement for Curses/S-Lang.
+
+Very basic at the moment."""
diff --git a/contrib/python/Twisted/py3/twisted/conch/insults/helper.py b/contrib/python/Twisted/py3/twisted/conch/insults/helper.py
new file mode 100644
index 0000000000..92eca1d89e
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/conch/insults/helper.py
@@ -0,0 +1,556 @@
+# -*- test-case-name: twisted.conch.test.test_helper -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Partial in-memory terminal emulator
+
+@author: Jp Calderone
+"""
+
+
+import re
+import string
+
+from zope.interface import implementer
+
+from incremental import Version
+
+from twisted.conch.insults import insults
+from twisted.internet import defer, protocol, reactor
+from twisted.logger import Logger
+from twisted.python import _textattributes
+from twisted.python.compat import iterbytes
+from twisted.python.deprecate import deprecated, deprecatedModuleAttribute
+
+FOREGROUND = 30
+BACKGROUND = 40
+BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, N_COLORS = range(9)
+
+
+class _FormattingState(_textattributes._FormattingStateMixin):
+ """
+ Represents the formatting state/attributes of a single character.
+
+ Character set, intensity, underlinedness, blinkitude, video
+ reversal, as well as foreground and background colors made up a
+ character's attributes.
+ """
+
+ compareAttributes = (
+ "charset",
+ "bold",
+ "underline",
+ "blink",
+ "reverseVideo",
+ "foreground",
+ "background",
+ "_subtracting",
+ )
+
+ def __init__(
+ self,
+ charset=insults.G0,
+ bold=False,
+ underline=False,
+ blink=False,
+ reverseVideo=False,
+ foreground=WHITE,
+ background=BLACK,
+ _subtracting=False,
+ ):
+ self.charset = charset
+ self.bold = bold
+ self.underline = underline
+ self.blink = blink
+ self.reverseVideo = reverseVideo
+ self.foreground = foreground
+ self.background = background
+ self._subtracting = _subtracting
+
+ @deprecated(Version("Twisted", 13, 1, 0))
+ def wantOne(self, **kw):
+ """
+ Add a character attribute to a copy of this formatting state.
+
+ @param kw: An optional attribute name and value can be provided with
+ a keyword argument.
+
+ @return: A formatting state instance with the new attribute.
+
+ @see: L{DefaultFormattingState._withAttribute}.
+ """
+ k, v = kw.popitem()
+ return self._withAttribute(k, v)
+
+ def toVT102(self):
+ # Spit out a vt102 control sequence that will set up
+ # all the attributes set here. Except charset.
+ attrs = []
+ if self._subtracting:
+ attrs.append(0)
+ if self.bold:
+ attrs.append(insults.BOLD)
+ if self.underline:
+ attrs.append(insults.UNDERLINE)
+ if self.blink:
+ attrs.append(insults.BLINK)
+ if self.reverseVideo:
+ attrs.append(insults.REVERSE_VIDEO)
+ if self.foreground != WHITE:
+ attrs.append(FOREGROUND + self.foreground)
+ if self.background != BLACK:
+ attrs.append(BACKGROUND + self.background)
+ if attrs:
+ return "\x1b[" + ";".join(map(str, attrs)) + "m"
+ return ""
+
+
+CharacterAttribute = _FormattingState
+
+deprecatedModuleAttribute(
+ Version("Twisted", 13, 1, 0),
+ "Use twisted.conch.insults.text.assembleFormattedText instead.",
+ "twisted.conch.insults.helper",
+ "CharacterAttribute",
+)
+
+
+# XXX - need to support scroll regions and scroll history
+@implementer(insults.ITerminalTransport)
+class TerminalBuffer(protocol.Protocol):
+ """
+ An in-memory terminal emulator.
+ """
+
+ for keyID in (
+ b"UP_ARROW",
+ b"DOWN_ARROW",
+ b"RIGHT_ARROW",
+ b"LEFT_ARROW",
+ b"HOME",
+ b"INSERT",
+ b"DELETE",
+ b"END",
+ b"PGUP",
+ b"PGDN",
+ b"F1",
+ b"F2",
+ b"F3",
+ b"F4",
+ b"F5",
+ b"F6",
+ b"F7",
+ b"F8",
+ b"F9",
+ b"F10",
+ b"F11",
+ b"F12",
+ ):
+ execBytes = keyID + b" = object()"
+ execStr = execBytes.decode("ascii")
+ exec(execStr)
+
+ TAB = b"\t"
+ BACKSPACE = b"\x7f"
+
+ width = 80
+ height = 24
+
+ fill = b" "
+ void = object()
+ _log = Logger()
+
+ def getCharacter(self, x, y):
+ return self.lines[y][x]
+
+ def connectionMade(self):
+ self.reset()
+
+ def write(self, data):
+ """
+ Add the given printable bytes to the terminal.
+
+ Line feeds in L{bytes} will be replaced with carriage return / line
+ feed pairs.
+ """
+ for b in iterbytes(data.replace(b"\n", b"\r\n")):
+ self.insertAtCursor(b)
+
+ def _currentFormattingState(self):
+ return _FormattingState(self.activeCharset, **self.graphicRendition)
+
+ def insertAtCursor(self, b):
+ """
+ Add one byte to the terminal at the cursor and make consequent state
+ updates.
+
+ If b is a carriage return, move the cursor to the beginning of the
+ current row.
+
+ If b is a line feed, move the cursor to the next row or scroll down if
+ the cursor is already in the last row.
+
+ Otherwise, if b is printable, put it at the cursor position (inserting
+ or overwriting as dictated by the current mode) and move the cursor.
+ """
+ if b == b"\r":
+ self.x = 0
+ elif b == b"\n":
+ self._scrollDown()
+ elif b in string.printable.encode("ascii"):
+ if self.x >= self.width:
+ self.nextLine()
+ ch = (b, self._currentFormattingState())
+ if self.modes.get(insults.modes.IRM):
+ self.lines[self.y][self.x : self.x] = [ch]
+ self.lines[self.y].pop()
+ else:
+ self.lines[self.y][self.x] = ch
+ self.x += 1
+
+ def _emptyLine(self, width):
+ return [(self.void, self._currentFormattingState()) for i in range(width)]
+
+ def _scrollDown(self):
+ self.y += 1
+ if self.y >= self.height:
+ self.y -= 1
+ del self.lines[0]
+ self.lines.append(self._emptyLine(self.width))
+
+ def _scrollUp(self):
+ self.y -= 1
+ if self.y < 0:
+ self.y = 0
+ del self.lines[-1]
+ self.lines.insert(0, self._emptyLine(self.width))
+
+ def cursorUp(self, n=1):
+ self.y = max(0, self.y - n)
+
+ def cursorDown(self, n=1):
+ self.y = min(self.height - 1, self.y + n)
+
+ def cursorBackward(self, n=1):
+ self.x = max(0, self.x - n)
+
+ def cursorForward(self, n=1):
+ self.x = min(self.width, self.x + n)
+
+ def cursorPosition(self, column, line):
+ self.x = column
+ self.y = line
+
+ def cursorHome(self):
+ self.x = self.home.x
+ self.y = self.home.y
+
+ def index(self):
+ self._scrollDown()
+
+ def reverseIndex(self):
+ self._scrollUp()
+
+ def nextLine(self):
+ """
+ Update the cursor position attributes and scroll down if appropriate.
+ """
+ self.x = 0
+ self._scrollDown()
+
+ def saveCursor(self):
+ self._savedCursor = (self.x, self.y)
+
+ def restoreCursor(self):
+ self.x, self.y = self._savedCursor
+ del self._savedCursor
+
+ def setModes(self, modes):
+ for m in modes:
+ self.modes[m] = True
+
+ def resetModes(self, modes):
+ for m in modes:
+ try:
+ del self.modes[m]
+ except KeyError:
+ pass
+
+ def setPrivateModes(self, modes):
+ """
+ Enable the given modes.
+
+ Track which modes have been enabled so that the implementations of
+ other L{insults.ITerminalTransport} methods can be properly implemented
+ to respect these settings.
+
+ @see: L{resetPrivateModes}
+ @see: L{insults.ITerminalTransport.setPrivateModes}
+ """
+ for m in modes:
+ self.privateModes[m] = True
+
+ def resetPrivateModes(self, modes):
+ """
+ Disable the given modes.
+
+ @see: L{setPrivateModes}
+ @see: L{insults.ITerminalTransport.resetPrivateModes}
+ """
+ for m in modes:
+ try:
+ del self.privateModes[m]
+ except KeyError:
+ pass
+
+ def applicationKeypadMode(self):
+ self.keypadMode = "app"
+
+ def numericKeypadMode(self):
+ self.keypadMode = "num"
+
+ def selectCharacterSet(self, charSet, which):
+ self.charsets[which] = charSet
+
+ def shiftIn(self):
+ self.activeCharset = insults.G0
+
+ def shiftOut(self):
+ self.activeCharset = insults.G1
+
+ def singleShift2(self):
+ oldActiveCharset = self.activeCharset
+ self.activeCharset = insults.G2
+ f = self.insertAtCursor
+
+ def insertAtCursor(b):
+ f(b)
+ del self.insertAtCursor
+ self.activeCharset = oldActiveCharset
+
+ self.insertAtCursor = insertAtCursor
+
+ def singleShift3(self):
+ oldActiveCharset = self.activeCharset
+ self.activeCharset = insults.G3
+ f = self.insertAtCursor
+
+ def insertAtCursor(b):
+ f(b)
+ del self.insertAtCursor
+ self.activeCharset = oldActiveCharset
+
+ self.insertAtCursor = insertAtCursor
+
+ def selectGraphicRendition(self, *attributes):
+ for a in attributes:
+ if a == insults.NORMAL:
+ self.graphicRendition = {
+ "bold": False,
+ "underline": False,
+ "blink": False,
+ "reverseVideo": False,
+ "foreground": WHITE,
+ "background": BLACK,
+ }
+ elif a == insults.BOLD:
+ self.graphicRendition["bold"] = True
+ elif a == insults.UNDERLINE:
+ self.graphicRendition["underline"] = True
+ elif a == insults.BLINK:
+ self.graphicRendition["blink"] = True
+ elif a == insults.REVERSE_VIDEO:
+ self.graphicRendition["reverseVideo"] = True
+ else:
+ try:
+ v = int(a)
+ except ValueError:
+ self._log.error(
+ "Unknown graphic rendition attribute: {attr!r}", attr=a
+ )
+ else:
+ if FOREGROUND <= v <= FOREGROUND + N_COLORS:
+ self.graphicRendition["foreground"] = v - FOREGROUND
+ elif BACKGROUND <= v <= BACKGROUND + N_COLORS:
+ self.graphicRendition["background"] = v - BACKGROUND
+ else:
+ self._log.error(
+ "Unknown graphic rendition attribute: {attr!r}", attr=a
+ )
+
+ def eraseLine(self):
+ self.lines[self.y] = self._emptyLine(self.width)
+
+ def eraseToLineEnd(self):
+ width = self.width - self.x
+ self.lines[self.y][self.x :] = self._emptyLine(width)
+
+ def eraseToLineBeginning(self):
+ self.lines[self.y][: self.x + 1] = self._emptyLine(self.x + 1)
+
+ def eraseDisplay(self):
+ self.lines = [self._emptyLine(self.width) for i in range(self.height)]
+
+ def eraseToDisplayEnd(self):
+ self.eraseToLineEnd()
+ height = self.height - self.y - 1
+ self.lines[self.y + 1 :] = [self._emptyLine(self.width) for i in range(height)]
+
+ def eraseToDisplayBeginning(self):
+ self.eraseToLineBeginning()
+ self.lines[: self.y] = [self._emptyLine(self.width) for i in range(self.y)]
+
+ def deleteCharacter(self, n=1):
+ del self.lines[self.y][self.x : self.x + n]
+ self.lines[self.y].extend(self._emptyLine(min(self.width - self.x, n)))
+
+ def insertLine(self, n=1):
+ self.lines[self.y : self.y] = [self._emptyLine(self.width) for i in range(n)]
+ del self.lines[self.height :]
+
+ def deleteLine(self, n=1):
+ del self.lines[self.y : self.y + n]
+ self.lines.extend([self._emptyLine(self.width) for i in range(n)])
+
+ def reportCursorPosition(self):
+ return (self.x, self.y)
+
+ def reset(self):
+ self.home = insults.Vector(0, 0)
+ self.x = self.y = 0
+ self.modes = {}
+ self.privateModes = {}
+ self.setPrivateModes(
+ [insults.privateModes.AUTO_WRAP, insults.privateModes.CURSOR_MODE]
+ )
+ self.numericKeypad = "app"
+ self.activeCharset = insults.G0
+ self.graphicRendition = {
+ "bold": False,
+ "underline": False,
+ "blink": False,
+ "reverseVideo": False,
+ "foreground": WHITE,
+ "background": BLACK,
+ }
+ self.charsets = {
+ insults.G0: insults.CS_US,
+ insults.G1: insults.CS_US,
+ insults.G2: insults.CS_ALTERNATE,
+ insults.G3: insults.CS_ALTERNATE_SPECIAL,
+ }
+ self.eraseDisplay()
+
+ def unhandledControlSequence(self, buf):
+ print("Could not handle", repr(buf))
+
+ def __bytes__(self):
+ lines = []
+ for L in self.lines:
+ buf = []
+ length = 0
+ for ch, attr in L:
+ if ch is not self.void:
+ buf.append(ch)
+ length = len(buf)
+ else:
+ buf.append(self.fill)
+ lines.append(b"".join(buf[:length]))
+ return b"\n".join(lines)
+
+ def getHost(self):
+ # ITransport.getHost
+ raise NotImplementedError("Unimplemented: TerminalBuffer.getHost")
+
+ def getPeer(self):
+ # ITransport.getPeer
+ raise NotImplementedError("Unimplemented: TerminalBuffer.getPeer")
+
+ def loseConnection(self):
+ # ITransport.loseConnection
+ raise NotImplementedError("Unimplemented: TerminalBuffer.loseConnection")
+
+ def writeSequence(self, data):
+ # ITransport.writeSequence
+ raise NotImplementedError("Unimplemented: TerminalBuffer.writeSequence")
+
+ def horizontalTabulationSet(self):
+ # ITerminalTransport.horizontalTabulationSet
+ raise NotImplementedError(
+ "Unimplemented: TerminalBuffer.horizontalTabulationSet"
+ )
+
+ def tabulationClear(self):
+ # TerminalTransport.tabulationClear
+ raise NotImplementedError("Unimplemented: TerminalBuffer.tabulationClear")
+
+ def tabulationClearAll(self):
+ # TerminalTransport.tabulationClearAll
+ raise NotImplementedError("Unimplemented: TerminalBuffer.tabulationClearAll")
+
+ def doubleHeightLine(self, top=True):
+ # ITerminalTransport.doubleHeightLine
+ raise NotImplementedError("Unimplemented: TerminalBuffer.doubleHeightLine")
+
+ def singleWidthLine(self):
+ # ITerminalTransport.singleWidthLine
+ raise NotImplementedError("Unimplemented: TerminalBuffer.singleWidthLine")
+
+ def doubleWidthLine(self):
+ # ITerminalTransport.doubleWidthLine
+ raise NotImplementedError("Unimplemented: TerminalBuffer.doubleWidthLine")
+
+
+class ExpectationTimeout(Exception):
+ pass
+
+
+class ExpectableBuffer(TerminalBuffer):
+ _mark = 0
+
+ def connectionMade(self):
+ TerminalBuffer.connectionMade(self)
+ self._expecting = []
+
+ def write(self, data):
+ TerminalBuffer.write(self, data)
+ self._checkExpected()
+
+ def cursorHome(self):
+ TerminalBuffer.cursorHome(self)
+ self._mark = 0
+
+ def _timeoutExpected(self, d):
+ d.errback(ExpectationTimeout())
+ self._checkExpected()
+
+ def _checkExpected(self):
+ s = self.__bytes__()[self._mark :]
+ while self._expecting:
+ expr, timer, deferred = self._expecting[0]
+ if timer and not timer.active():
+ del self._expecting[0]
+ continue
+ for match in expr.finditer(s):
+ if timer:
+ timer.cancel()
+ del self._expecting[0]
+ self._mark += match.end()
+ s = s[match.end() :]
+ deferred.callback(match)
+ break
+ else:
+ return
+
+ def expect(self, expression, timeout=None, scheduler=reactor):
+ d = defer.Deferred()
+ timer = None
+ if timeout:
+ timer = scheduler.callLater(timeout, self._timeoutExpected, d)
+ self._expecting.append((re.compile(expression), timer, d))
+ self._checkExpected()
+ return d
+
+
+__all__ = ["CharacterAttribute", "TerminalBuffer", "ExpectableBuffer"]
diff --git a/contrib/python/Twisted/py3/twisted/conch/insults/insults.py b/contrib/python/Twisted/py3/twisted/conch/insults/insults.py
new file mode 100644
index 0000000000..4640aab368
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/conch/insults/insults.py
@@ -0,0 +1,1223 @@
+# -*- test-case-name: twisted.conch.test.test_insults -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+VT102 and VT220 terminal manipulation.
+
+@author: Jp Calderone
+"""
+
+from zope.interface import Interface, implementer
+
+from twisted.internet import defer, interfaces as iinternet, protocol
+from twisted.python.compat import iterbytes, networkString
+
+
+class ITerminalProtocol(Interface):
+ def makeConnection(transport):
+ """
+ Called with an L{ITerminalTransport} when a connection is established.
+ """
+
+ def keystrokeReceived(keyID, modifier):
+ """
+ A keystroke was received.
+
+ Each keystroke corresponds to one invocation of this method.
+ keyID is a string identifier for that key. Printable characters
+ are represented by themselves. Control keys, such as arrows and
+ function keys, are represented with symbolic constants on
+ L{ServerProtocol}.
+ """
+
+ def terminalSize(width, height):
+ """
+ Called to indicate the size of the terminal.
+
+ A terminal of 80x24 should be assumed if this method is not
+ called. This method might not be called for real terminals.
+ """
+
+ def unhandledControlSequence(seq):
+ """
+ Called when an unsupported control sequence is received.
+
+ @type seq: L{str}
+ @param seq: The whole control sequence which could not be interpreted.
+ """
+
+ def connectionLost(reason):
+ """
+ Called when the connection has been lost.
+
+ reason is a Failure describing why.
+ """
+
+
+@implementer(ITerminalProtocol)
+class TerminalProtocol:
+ def makeConnection(self, terminal):
+ # assert ITerminalTransport.providedBy(transport), "TerminalProtocol.makeConnection must be passed an ITerminalTransport implementor"
+ self.terminal = terminal
+ self.connectionMade()
+
+ def connectionMade(self):
+ """
+ Called after a connection has been established.
+ """
+
+ def keystrokeReceived(self, keyID, modifier):
+ pass
+
+ def terminalSize(self, width, height):
+ pass
+
+ def unhandledControlSequence(self, seq):
+ pass
+
+ def connectionLost(self, reason):
+ pass
+
+
+class ITerminalTransport(iinternet.ITransport):
+ def cursorUp(n=1):
+ """
+ Move the cursor up n lines.
+ """
+
+ def cursorDown(n=1):
+ """
+ Move the cursor down n lines.
+ """
+
+ def cursorForward(n=1):
+ """
+ Move the cursor right n columns.
+ """
+
+ def cursorBackward(n=1):
+ """
+ Move the cursor left n columns.
+ """
+
+ def cursorPosition(column, line):
+ """
+ Move the cursor to the given line and column.
+ """
+
+ def cursorHome():
+ """
+ Move the cursor home.
+ """
+
+ def index():
+ """
+ Move the cursor down one line, performing scrolling if necessary.
+ """
+
+ def reverseIndex():
+ """
+ Move the cursor up one line, performing scrolling if necessary.
+ """
+
+ def nextLine():
+ """
+ Move the cursor to the first position on the next line, performing scrolling if necessary.
+ """
+
+ def saveCursor():
+ """
+ Save the cursor position, character attribute, character set, and origin mode selection.
+ """
+
+ def restoreCursor():
+ """
+ Restore the previously saved cursor position, character attribute, character set, and origin mode selection.
+
+ If no cursor state was previously saved, move the cursor to the home position.
+ """
+
+ def setModes(modes):
+ """
+ Set the given modes on the terminal.
+ """
+
+ def resetModes(mode):
+ """
+ Reset the given modes on the terminal.
+ """
+
+ def setPrivateModes(modes):
+ """
+ Set the given DEC private modes on the terminal.
+ """
+
+ def resetPrivateModes(modes):
+ """
+ Reset the given DEC private modes on the terminal.
+ """
+
+ def applicationKeypadMode():
+ """
+ Cause keypad to generate control functions.
+
+ Cursor key mode selects the type of characters generated by cursor keys.
+ """
+
+ def numericKeypadMode():
+ """
+ Cause keypad to generate normal characters.
+ """
+
+ def selectCharacterSet(charSet, which):
+ """
+ Select a character set.
+
+ charSet should be one of CS_US, CS_UK, CS_DRAWING, CS_ALTERNATE, or
+ CS_ALTERNATE_SPECIAL.
+
+ which should be one of G0 or G1.
+ """
+
+ def shiftIn():
+ """
+ Activate the G0 character set.
+ """
+
+ def shiftOut():
+ """
+ Activate the G1 character set.
+ """
+
+ def singleShift2():
+ """
+ Shift to the G2 character set for a single character.
+ """
+
+ def singleShift3():
+ """
+ Shift to the G3 character set for a single character.
+ """
+
+ def selectGraphicRendition(*attributes):
+ """
+ Enabled one or more character attributes.
+
+ Arguments should be one or more of UNDERLINE, REVERSE_VIDEO, BLINK, or BOLD.
+ NORMAL may also be specified to disable all character attributes.
+ """
+
+ def horizontalTabulationSet():
+ """
+ Set a tab stop at the current cursor position.
+ """
+
+ def tabulationClear():
+ """
+ Clear the tab stop at the current cursor position.
+ """
+
+ def tabulationClearAll():
+ """
+ Clear all tab stops.
+ """
+
+ def doubleHeightLine(top=True):
+ """
+ Make the current line the top or bottom half of a double-height, double-width line.
+
+ If top is True, the current line is the top half. Otherwise, it is the bottom half.
+ """
+
+ def singleWidthLine():
+ """
+ Make the current line a single-width, single-height line.
+ """
+
+ def doubleWidthLine():
+ """
+ Make the current line a double-width line.
+ """
+
+ def eraseToLineEnd():
+ """
+ Erase from the cursor to the end of line, including cursor position.
+ """
+
+ def eraseToLineBeginning():
+ """
+ Erase from the cursor to the beginning of the line, including the cursor position.
+ """
+
+ def eraseLine():
+ """
+ Erase the entire cursor line.
+ """
+
+ def eraseToDisplayEnd():
+ """
+ Erase from the cursor to the end of the display, including the cursor position.
+ """
+
+ def eraseToDisplayBeginning():
+ """
+ Erase from the cursor to the beginning of the display, including the cursor position.
+ """
+
+ def eraseDisplay():
+ """
+ Erase the entire display.
+ """
+
+ def deleteCharacter(n=1):
+ """
+ Delete n characters starting at the cursor position.
+
+ Characters to the right of deleted characters are shifted to the left.
+ """
+
+ def insertLine(n=1):
+ """
+ Insert n lines at the cursor position.
+
+ Lines below the cursor are shifted down. Lines moved past the bottom margin are lost.
+ This command is ignored when the cursor is outside the scroll region.
+ """
+
+ def deleteLine(n=1):
+ """
+ Delete n lines starting at the cursor position.
+
+ Lines below the cursor are shifted up. This command is ignored when the cursor is outside
+ the scroll region.
+ """
+
+ def reportCursorPosition():
+ """
+ Return a Deferred that fires with a two-tuple of (x, y) indicating the cursor position.
+ """
+
+ def reset():
+ """
+ Reset the terminal to its initial state.
+ """
+
+ def unhandledControlSequence(seq):
+ """
+ Called when an unsupported control sequence is received.
+
+ @type seq: L{str}
+ @param seq: The whole control sequence which could not be interpreted.
+ """
+
+
+CSI = b"\x1b"
+CST = {b"~": b"tilde"}
+
+
+class modes:
+ """
+ ECMA 48 standardized modes
+ """
+
+ # BREAKS YOPUR KEYBOARD MOFO
+ KEYBOARD_ACTION = KAM = 2
+
+ # When set, enables character insertion. New display characters
+ # move old display characters to the right. Characters moved past
+ # the right margin are lost.
+
+ # When reset, enables replacement mode (disables character
+ # insertion). New display characters replace old display
+ # characters at cursor position. The old character is erased.
+ INSERTION_REPLACEMENT = IRM = 4
+
+ # Set causes a received linefeed, form feed, or vertical tab to
+ # move cursor to first column of next line. RETURN transmits both
+ # a carriage return and linefeed. This selection is also called
+ # new line option.
+
+ # Reset causes a received linefeed, form feed, or vertical tab to
+ # move cursor to next line in current column. RETURN transmits a
+ # carriage return.
+ LINEFEED_NEWLINE = LNM = 20
+
+
+class privateModes:
+ """
+ ANSI-Compatible Private Modes
+ """
+
+ ERROR = 0
+ CURSOR_KEY = 1
+ ANSI_VT52 = 2
+ COLUMN = 3
+ SCROLL = 4
+ SCREEN = 5
+ ORIGIN = 6
+ AUTO_WRAP = 7
+ AUTO_REPEAT = 8
+ PRINTER_FORM_FEED = 18
+ PRINTER_EXTENT = 19
+
+ # Toggle cursor visibility (reset hides it)
+ CURSOR_MODE = 25
+
+
+# Character sets
+CS_US = b"CS_US"
+CS_UK = b"CS_UK"
+CS_DRAWING = b"CS_DRAWING"
+CS_ALTERNATE = b"CS_ALTERNATE"
+CS_ALTERNATE_SPECIAL = b"CS_ALTERNATE_SPECIAL"
+
+# Groupings (or something?? These are like variables that can be bound to character sets)
+G0 = b"G0"
+G1 = b"G1"
+
+# G2 and G3 cannot be changed, but they can be shifted to.
+G2 = b"G2"
+G3 = b"G3"
+
+# Character attributes
+
+NORMAL = 0
+BOLD = 1
+UNDERLINE = 4
+BLINK = 5
+REVERSE_VIDEO = 7
+
+
+class Vector:
+ def __init__(self, x, y):
+ self.x = x
+ self.y = y
+
+
+def log(s):
+ with open("log", "a") as f:
+ f.write(str(s) + "\n")
+
+
+# XXX TODO - These attributes are really part of the
+# ITerminalTransport interface, I think.
+_KEY_NAMES = (
+ "UP_ARROW",
+ "DOWN_ARROW",
+ "RIGHT_ARROW",
+ "LEFT_ARROW",
+ "HOME",
+ "INSERT",
+ "DELETE",
+ "END",
+ "PGUP",
+ "PGDN",
+ "NUMPAD_MIDDLE",
+ "F1",
+ "F2",
+ "F3",
+ "F4",
+ "F5",
+ "F6",
+ "F7",
+ "F8",
+ "F9",
+ "F10",
+ "F11",
+ "F12",
+ "ALT",
+ "SHIFT",
+ "CONTROL",
+)
+
+
+class _const:
+ """
+ @ivar name: A string naming this constant
+ """
+
+ def __init__(self, name: str) -> None:
+ self.name = name
+
+ def __repr__(self) -> str:
+ return "[" + self.name + "]"
+
+ def __bytes__(self) -> bytes:
+ return ("[" + self.name + "]").encode("ascii")
+
+
+FUNCTION_KEYS = [_const(_name).__bytes__() for _name in _KEY_NAMES]
+
+
+@implementer(ITerminalTransport)
+class ServerProtocol(protocol.Protocol):
+ protocolFactory = None
+ terminalProtocol = None
+
+ TAB = b"\t"
+ BACKSPACE = b"\x7f"
+ ##
+
+ lastWrite = b""
+
+ state = b"data"
+
+ termSize = Vector(80, 24)
+ cursorPos = Vector(0, 0)
+ scrollRegion = None
+
+ # Factory who instantiated me
+ factory = None
+
+ def __init__(self, protocolFactory=None, *a, **kw):
+ """
+ @param protocolFactory: A callable which will be invoked with
+ *a, **kw and should return an ITerminalProtocol implementor.
+ This will be invoked when a connection to this ServerProtocol
+ is established.
+
+ @param a: Any positional arguments to pass to protocolFactory.
+ @param kw: Any keyword arguments to pass to protocolFactory.
+ """
+ # assert protocolFactory is None or ITerminalProtocol.implementedBy(protocolFactory), "ServerProtocol.__init__ must be passed an ITerminalProtocol implementor"
+ if protocolFactory is not None:
+ self.protocolFactory = protocolFactory
+ self.protocolArgs = a
+ self.protocolKwArgs = kw
+
+ self._cursorReports = []
+
+ def getHost(self):
+ # ITransport.getHost
+ raise NotImplementedError("Unimplemented: ServerProtocol.getHost")
+
+ def getPeer(self):
+ # ITransport.getPeer
+ raise NotImplementedError("Unimplemented: ServerProtocol.getPeer")
+
+ def connectionMade(self):
+ if self.protocolFactory is not None:
+ self.terminalProtocol = self.protocolFactory(
+ *self.protocolArgs, **self.protocolKwArgs
+ )
+
+ try:
+ factory = self.factory
+ except AttributeError:
+ pass
+ else:
+ self.terminalProtocol.factory = factory
+
+ self.terminalProtocol.makeConnection(self)
+
+ def dataReceived(self, data):
+ for ch in iterbytes(data):
+ if self.state == b"data":
+ if ch == b"\x1b":
+ self.state = b"escaped"
+ else:
+ self.terminalProtocol.keystrokeReceived(ch, None)
+ elif self.state == b"escaped":
+ if ch == b"[":
+ self.state = b"bracket-escaped"
+ self.escBuf = []
+ elif ch == b"O":
+ self.state = b"low-function-escaped"
+ else:
+ self.state = b"data"
+ self._handleShortControlSequence(ch)
+ elif self.state == b"bracket-escaped":
+ if ch == b"O":
+ self.state = b"low-function-escaped"
+ elif ch.isalpha() or ch == b"~":
+ self._handleControlSequence(b"".join(self.escBuf) + ch)
+ del self.escBuf
+ self.state = b"data"
+ else:
+ self.escBuf.append(ch)
+ elif self.state == b"low-function-escaped":
+ self._handleLowFunctionControlSequence(ch)
+ self.state = b"data"
+ else:
+ raise ValueError("Illegal state")
+
+ def _handleShortControlSequence(self, ch):
+ self.terminalProtocol.keystrokeReceived(ch, self.ALT)
+
+ def _handleControlSequence(self, buf):
+ buf = b"\x1b[" + buf
+ f = getattr(
+ self.controlSequenceParser,
+ CST.get(buf[-1:], buf[-1:]).decode("ascii"),
+ None,
+ )
+ if f is None:
+ self.unhandledControlSequence(buf)
+ else:
+ f(self, self.terminalProtocol, buf[:-1])
+
+ def unhandledControlSequence(self, buf):
+ self.terminalProtocol.unhandledControlSequence(buf)
+
+ def _handleLowFunctionControlSequence(self, ch):
+ functionKeys = {b"P": self.F1, b"Q": self.F2, b"R": self.F3, b"S": self.F4}
+ keyID = functionKeys.get(ch)
+ if keyID is not None:
+ self.terminalProtocol.keystrokeReceived(keyID, None)
+ else:
+ self.terminalProtocol.unhandledControlSequence(b"\x1b[O" + ch)
+
+ class ControlSequenceParser:
+ def A(self, proto, handler, buf):
+ if buf == b"\x1b[":
+ handler.keystrokeReceived(proto.UP_ARROW, None)
+ else:
+ handler.unhandledControlSequence(buf + b"A")
+
+ def B(self, proto, handler, buf):
+ if buf == b"\x1b[":
+ handler.keystrokeReceived(proto.DOWN_ARROW, None)
+ else:
+ handler.unhandledControlSequence(buf + b"B")
+
+ def C(self, proto, handler, buf):
+ if buf == b"\x1b[":
+ handler.keystrokeReceived(proto.RIGHT_ARROW, None)
+ else:
+ handler.unhandledControlSequence(buf + b"C")
+
+ def D(self, proto, handler, buf):
+ if buf == b"\x1b[":
+ handler.keystrokeReceived(proto.LEFT_ARROW, None)
+ else:
+ handler.unhandledControlSequence(buf + b"D")
+
+ def E(self, proto, handler, buf):
+ if buf == b"\x1b[":
+ handler.keystrokeReceived(proto.NUMPAD_MIDDLE, None)
+ else:
+ handler.unhandledControlSequence(buf + b"E")
+
+ def F(self, proto, handler, buf):
+ if buf == b"\x1b[":
+ handler.keystrokeReceived(proto.END, None)
+ else:
+ handler.unhandledControlSequence(buf + b"F")
+
+ def H(self, proto, handler, buf):
+ if buf == b"\x1b[":
+ handler.keystrokeReceived(proto.HOME, None)
+ else:
+ handler.unhandledControlSequence(buf + b"H")
+
+ def R(self, proto, handler, buf):
+ if not proto._cursorReports:
+ handler.unhandledControlSequence(buf + b"R")
+ elif buf.startswith(b"\x1b["):
+ report = buf[2:]
+ parts = report.split(b";")
+ if len(parts) != 2:
+ handler.unhandledControlSequence(buf + b"R")
+ else:
+ Pl, Pc = parts
+ try:
+ Pl, Pc = int(Pl), int(Pc)
+ except ValueError:
+ handler.unhandledControlSequence(buf + b"R")
+ else:
+ d = proto._cursorReports.pop(0)
+ d.callback((Pc - 1, Pl - 1))
+ else:
+ handler.unhandledControlSequence(buf + b"R")
+
+ def Z(self, proto, handler, buf):
+ if buf == b"\x1b[":
+ handler.keystrokeReceived(proto.TAB, proto.SHIFT)
+ else:
+ handler.unhandledControlSequence(buf + b"Z")
+
+ def tilde(self, proto, handler, buf):
+ map = {
+ 1: proto.HOME,
+ 2: proto.INSERT,
+ 3: proto.DELETE,
+ 4: proto.END,
+ 5: proto.PGUP,
+ 6: proto.PGDN,
+ 15: proto.F5,
+ 17: proto.F6,
+ 18: proto.F7,
+ 19: proto.F8,
+ 20: proto.F9,
+ 21: proto.F10,
+ 23: proto.F11,
+ 24: proto.F12,
+ }
+
+ if buf.startswith(b"\x1b["):
+ ch = buf[2:]
+ try:
+ v = int(ch)
+ except ValueError:
+ handler.unhandledControlSequence(buf + b"~")
+ else:
+ symbolic = map.get(v)
+ if symbolic is not None:
+ handler.keystrokeReceived(map[v], None)
+ else:
+ handler.unhandledControlSequence(buf + b"~")
+ else:
+ handler.unhandledControlSequence(buf + b"~")
+
+ controlSequenceParser = ControlSequenceParser()
+
+ # ITerminalTransport
+ def cursorUp(self, n=1):
+ assert n >= 1
+ self.cursorPos.y = max(self.cursorPos.y - n, 0)
+ self.write(b"\x1b[%dA" % (n,))
+
+ def cursorDown(self, n=1):
+ assert n >= 1
+ self.cursorPos.y = min(self.cursorPos.y + n, self.termSize.y - 1)
+ self.write(b"\x1b[%dB" % (n,))
+
+ def cursorForward(self, n=1):
+ assert n >= 1
+ self.cursorPos.x = min(self.cursorPos.x + n, self.termSize.x - 1)
+ self.write(b"\x1b[%dC" % (n,))
+
+ def cursorBackward(self, n=1):
+ assert n >= 1
+ self.cursorPos.x = max(self.cursorPos.x - n, 0)
+ self.write(b"\x1b[%dD" % (n,))
+
+ def cursorPosition(self, column, line):
+ self.write(b"\x1b[%d;%dH" % (line + 1, column + 1))
+
+ def cursorHome(self):
+ self.cursorPos.x = self.cursorPos.y = 0
+ self.write(b"\x1b[H")
+
+ def index(self):
+ # ECMA48 5th Edition removes this
+ self.cursorPos.y = min(self.cursorPos.y + 1, self.termSize.y - 1)
+ self.write(b"\x1bD")
+
+ def reverseIndex(self):
+ self.cursorPos.y = max(self.cursorPos.y - 1, 0)
+ self.write(b"\x1bM")
+
+ def nextLine(self):
+ self.cursorPos.x = 0
+ self.cursorPos.y = min(self.cursorPos.y + 1, self.termSize.y - 1)
+ self.write(b"\n")
+
+ def saveCursor(self):
+ self._savedCursorPos = Vector(self.cursorPos.x, self.cursorPos.y)
+ self.write(b"\x1b7")
+
+ def restoreCursor(self):
+ self.cursorPos = self._savedCursorPos
+ del self._savedCursorPos
+ self.write(b"\x1b8")
+
+ def setModes(self, modes):
+ # XXX Support ANSI-Compatible private modes
+ modesBytes = b";".join(b"%d" % (mode,) for mode in modes)
+ self.write(b"\x1b[" + modesBytes + b"h")
+
+ def setPrivateModes(self, modes):
+ modesBytes = b";".join(b"%d" % (mode,) for mode in modes)
+ self.write(b"\x1b[?" + modesBytes + b"h")
+
+ def resetModes(self, modes):
+ # XXX Support ANSI-Compatible private modes
+ modesBytes = b";".join(b"%d" % (mode,) for mode in modes)
+ self.write(b"\x1b[" + modesBytes + b"l")
+
+ def resetPrivateModes(self, modes):
+ modesBytes = b";".join(b"%d" % (mode,) for mode in modes)
+ self.write(b"\x1b[?" + modesBytes + b"l")
+
+ def applicationKeypadMode(self):
+ self.write(b"\x1b=")
+
+ def numericKeypadMode(self):
+ self.write(b"\x1b>")
+
+ def selectCharacterSet(self, charSet, which):
+ # XXX Rewrite these as dict lookups
+ if which == G0:
+ which = b"("
+ elif which == G1:
+ which = b")"
+ else:
+ raise ValueError("`which' argument to selectCharacterSet must be G0 or G1")
+ if charSet == CS_UK:
+ charSet = b"A"
+ elif charSet == CS_US:
+ charSet = b"B"
+ elif charSet == CS_DRAWING:
+ charSet = b"0"
+ elif charSet == CS_ALTERNATE:
+ charSet = b"1"
+ elif charSet == CS_ALTERNATE_SPECIAL:
+ charSet = b"2"
+ else:
+ raise ValueError("Invalid `charSet' argument to selectCharacterSet")
+ self.write(b"\x1b" + which + charSet)
+
+ def shiftIn(self):
+ self.write(b"\x15")
+
+ def shiftOut(self):
+ self.write(b"\x14")
+
+ def singleShift2(self):
+ self.write(b"\x1bN")
+
+ def singleShift3(self):
+ self.write(b"\x1bO")
+
+ def selectGraphicRendition(self, *attributes):
+ # each member of attributes must be a native string
+ attrs = []
+ for a in attributes:
+ attrs.append(networkString(a))
+ self.write(b"\x1b[" + b";".join(attrs) + b"m")
+
+ def horizontalTabulationSet(self):
+ self.write(b"\x1bH")
+
+ def tabulationClear(self):
+ self.write(b"\x1b[q")
+
+ def tabulationClearAll(self):
+ self.write(b"\x1b[3q")
+
+ def doubleHeightLine(self, top=True):
+ if top:
+ self.write(b"\x1b#3")
+ else:
+ self.write(b"\x1b#4")
+
+ def singleWidthLine(self):
+ self.write(b"\x1b#5")
+
+ def doubleWidthLine(self):
+ self.write(b"\x1b#6")
+
+ def eraseToLineEnd(self):
+ self.write(b"\x1b[K")
+
+ def eraseToLineBeginning(self):
+ self.write(b"\x1b[1K")
+
+ def eraseLine(self):
+ self.write(b"\x1b[2K")
+
+ def eraseToDisplayEnd(self):
+ self.write(b"\x1b[J")
+
+ def eraseToDisplayBeginning(self):
+ self.write(b"\x1b[1J")
+
+ def eraseDisplay(self):
+ self.write(b"\x1b[2J")
+
+ def deleteCharacter(self, n=1):
+ self.write(b"\x1b[%dP" % (n,))
+
+ def insertLine(self, n=1):
+ self.write(b"\x1b[%dL" % (n,))
+
+ def deleteLine(self, n=1):
+ self.write(b"\x1b[%dM" % (n,))
+
+ def setScrollRegion(self, first=None, last=None):
+ if first is not None:
+ first = b"%d" % (first,)
+ else:
+ first = b""
+ if last is not None:
+ last = b"%d" % (last,)
+ else:
+ last = b""
+ self.write(b"\x1b[%b;%br" % (first, last))
+
+ def resetScrollRegion(self):
+ self.setScrollRegion()
+
+ def reportCursorPosition(self):
+ d = defer.Deferred()
+ self._cursorReports.append(d)
+ self.write(b"\x1b[6n")
+ return d
+
+ def reset(self):
+ self.cursorPos.x = self.cursorPos.y = 0
+ try:
+ del self._savedCursorPos
+ except AttributeError:
+ pass
+ self.write(b"\x1bc")
+
+ # ITransport
+ def write(self, data):
+ if data:
+ if not isinstance(data, bytes):
+ data = data.encode("utf-8")
+ self.lastWrite = data
+ self.transport.write(b"\r\n".join(data.split(b"\n")))
+
+ def writeSequence(self, data):
+ self.write(b"".join(data))
+
+ def loseConnection(self):
+ self.reset()
+ self.transport.loseConnection()
+
+ def connectionLost(self, reason):
+ if self.terminalProtocol is not None:
+ try:
+ self.terminalProtocol.connectionLost(reason)
+ finally:
+ self.terminalProtocol = None
+
+
+# Add symbolic names for function keys
+for name, const in zip(_KEY_NAMES, FUNCTION_KEYS):
+ setattr(ServerProtocol, name, const)
+
+
+class ClientProtocol(protocol.Protocol):
+ terminalFactory = None
+ terminal = None
+
+ state = b"data"
+
+ _escBuf = None
+
+ _shorts = {
+ b"D": b"index",
+ b"M": b"reverseIndex",
+ b"E": b"nextLine",
+ b"7": b"saveCursor",
+ b"8": b"restoreCursor",
+ b"=": b"applicationKeypadMode",
+ b">": b"numericKeypadMode",
+ b"N": b"singleShift2",
+ b"O": b"singleShift3",
+ b"H": b"horizontalTabulationSet",
+ b"c": b"reset",
+ }
+
+ _longs = {
+ b"[": b"bracket-escape",
+ b"(": b"select-g0",
+ b")": b"select-g1",
+ b"#": b"select-height-width",
+ }
+
+ _charsets = {
+ b"A": CS_UK,
+ b"B": CS_US,
+ b"0": CS_DRAWING,
+ b"1": CS_ALTERNATE,
+ b"2": CS_ALTERNATE_SPECIAL,
+ }
+
+ # Factory who instantiated me
+ factory = None
+
+ def __init__(self, terminalFactory=None, *a, **kw):
+ """
+ @param terminalFactory: A callable which will be invoked with
+ *a, **kw and should return an ITerminalTransport provider.
+ This will be invoked when this ClientProtocol establishes a
+ connection.
+
+ @param a: Any positional arguments to pass to terminalFactory.
+ @param kw: Any keyword arguments to pass to terminalFactory.
+ """
+ # assert terminalFactory is None or ITerminalTransport.implementedBy(terminalFactory), "ClientProtocol.__init__ must be passed an ITerminalTransport implementor"
+ if terminalFactory is not None:
+ self.terminalFactory = terminalFactory
+ self.terminalArgs = a
+ self.terminalKwArgs = kw
+
+ def connectionMade(self):
+ if self.terminalFactory is not None:
+ self.terminal = self.terminalFactory(
+ *self.terminalArgs, **self.terminalKwArgs
+ )
+ self.terminal.factory = self.factory
+ self.terminal.makeConnection(self)
+
+ def connectionLost(self, reason):
+ if self.terminal is not None:
+ try:
+ self.terminal.connectionLost(reason)
+ finally:
+ del self.terminal
+
+ def dataReceived(self, data):
+ """
+ Parse the given data from a terminal server, dispatching to event
+ handlers defined by C{self.terminal}.
+ """
+ toWrite = []
+ for b in iterbytes(data):
+ if self.state == b"data":
+ if b == b"\x1b":
+ if toWrite:
+ self.terminal.write(b"".join(toWrite))
+ del toWrite[:]
+ self.state = b"escaped"
+ elif b == b"\x14":
+ if toWrite:
+ self.terminal.write(b"".join(toWrite))
+ del toWrite[:]
+ self.terminal.shiftOut()
+ elif b == b"\x15":
+ if toWrite:
+ self.terminal.write(b"".join(toWrite))
+ del toWrite[:]
+ self.terminal.shiftIn()
+ elif b == b"\x08":
+ if toWrite:
+ self.terminal.write(b"".join(toWrite))
+ del toWrite[:]
+ self.terminal.cursorBackward()
+ else:
+ toWrite.append(b)
+ elif self.state == b"escaped":
+ fName = self._shorts.get(b)
+ if fName is not None:
+ self.state = b"data"
+ getattr(self.terminal, fName.decode("ascii"))()
+ else:
+ state = self._longs.get(b)
+ if state is not None:
+ self.state = state
+ else:
+ self.terminal.unhandledControlSequence(b"\x1b" + b)
+ self.state = b"data"
+ elif self.state == b"bracket-escape":
+ if self._escBuf is None:
+ self._escBuf = []
+ if b.isalpha() or b == b"~":
+ self._handleControlSequence(b"".join(self._escBuf), b)
+ del self._escBuf
+ self.state = b"data"
+ else:
+ self._escBuf.append(b)
+ elif self.state == b"select-g0":
+ self.terminal.selectCharacterSet(self._charsets.get(b, b), G0)
+ self.state = b"data"
+ elif self.state == b"select-g1":
+ self.terminal.selectCharacterSet(self._charsets.get(b, b), G1)
+ self.state = b"data"
+ elif self.state == b"select-height-width":
+ self._handleHeightWidth(b)
+ self.state = b"data"
+ else:
+ raise ValueError("Illegal state")
+ if toWrite:
+ self.terminal.write(b"".join(toWrite))
+
+ def _handleControlSequence(self, buf, terminal):
+ f = getattr(
+ self.controlSequenceParser,
+ CST.get(terminal, terminal).decode("ascii"),
+ None,
+ )
+ if f is None:
+ self.terminal.unhandledControlSequence(b"\x1b[" + buf + terminal)
+ else:
+ f(self, self.terminal, buf)
+
+ class ControlSequenceParser:
+ def _makeSimple(ch, fName):
+ n = "cursor" + fName
+
+ def simple(self, proto, handler, buf):
+ if not buf:
+ getattr(handler, n)(1)
+ else:
+ try:
+ m = int(buf)
+ except ValueError:
+ handler.unhandledControlSequence(b"\x1b[" + buf + ch)
+ else:
+ getattr(handler, n)(m)
+
+ return simple
+
+ for ch, fName in (
+ ("A", "Up"),
+ ("B", "Down"),
+ ("C", "Forward"),
+ ("D", "Backward"),
+ ):
+ exec(ch + " = _makeSimple(ch, fName)")
+ del _makeSimple
+
+ def h(self, proto, handler, buf):
+ # XXX - Handle '?' to introduce ANSI-Compatible private modes.
+ try:
+ modes = [int(mode) for mode in buf.split(b";")]
+ except ValueError:
+ handler.unhandledControlSequence(b"\x1b[" + buf + b"h")
+ else:
+ handler.setModes(modes)
+
+ def l(self, proto, handler, buf):
+ # XXX - Handle '?' to introduce ANSI-Compatible private modes.
+ try:
+ modes = [int(mode) for mode in buf.split(b";")]
+ except ValueError:
+ handler.unhandledControlSequence(b"\x1b[" + buf + "l")
+ else:
+ handler.resetModes(modes)
+
+ def r(self, proto, handler, buf):
+ parts = buf.split(b";")
+ if len(parts) == 1:
+ handler.setScrollRegion(None, None)
+ elif len(parts) == 2:
+ try:
+ if parts[0]:
+ pt = int(parts[0])
+ else:
+ pt = None
+ if parts[1]:
+ pb = int(parts[1])
+ else:
+ pb = None
+ except ValueError:
+ handler.unhandledControlSequence(b"\x1b[" + buf + b"r")
+ else:
+ handler.setScrollRegion(pt, pb)
+ else:
+ handler.unhandledControlSequence(b"\x1b[" + buf + b"r")
+
+ def K(self, proto, handler, buf):
+ if not buf:
+ handler.eraseToLineEnd()
+ elif buf == b"1":
+ handler.eraseToLineBeginning()
+ elif buf == b"2":
+ handler.eraseLine()
+ else:
+ handler.unhandledControlSequence(b"\x1b[" + buf + b"K")
+
+ def H(self, proto, handler, buf):
+ handler.cursorHome()
+
+ def J(self, proto, handler, buf):
+ if not buf:
+ handler.eraseToDisplayEnd()
+ elif buf == b"1":
+ handler.eraseToDisplayBeginning()
+ elif buf == b"2":
+ handler.eraseDisplay()
+ else:
+ handler.unhandledControlSequence(b"\x1b[" + buf + b"J")
+
+ def P(self, proto, handler, buf):
+ if not buf:
+ handler.deleteCharacter(1)
+ else:
+ try:
+ n = int(buf)
+ except ValueError:
+ handler.unhandledControlSequence(b"\x1b[" + buf + b"P")
+ else:
+ handler.deleteCharacter(n)
+
+ def L(self, proto, handler, buf):
+ if not buf:
+ handler.insertLine(1)
+ else:
+ try:
+ n = int(buf)
+ except ValueError:
+ handler.unhandledControlSequence(b"\x1b[" + buf + b"L")
+ else:
+ handler.insertLine(n)
+
+ def M(self, proto, handler, buf):
+ if not buf:
+ handler.deleteLine(1)
+ else:
+ try:
+ n = int(buf)
+ except ValueError:
+ handler.unhandledControlSequence(b"\x1b[" + buf + b"M")
+ else:
+ handler.deleteLine(n)
+
+ def n(self, proto, handler, buf):
+ if buf == b"6":
+ x, y = handler.reportCursorPosition()
+ proto.transport.write(b"\x1b[%d;%dR" % (x + 1, y + 1))
+ else:
+ handler.unhandledControlSequence(b"\x1b[" + buf + b"n")
+
+ def m(self, proto, handler, buf):
+ if not buf:
+ handler.selectGraphicRendition(NORMAL)
+ else:
+ attrs = []
+ for a in buf.split(b";"):
+ try:
+ a = int(a)
+ except ValueError:
+ pass
+ attrs.append(a)
+ handler.selectGraphicRendition(*attrs)
+
+ controlSequenceParser = ControlSequenceParser()
+
+ def _handleHeightWidth(self, b):
+ if b == b"3":
+ self.terminal.doubleHeightLine(True)
+ elif b == b"4":
+ self.terminal.doubleHeightLine(False)
+ elif b == b"5":
+ self.terminal.singleWidthLine()
+ elif b == b"6":
+ self.terminal.doubleWidthLine()
+ else:
+ self.terminal.unhandledControlSequence(b"\x1b#" + b)
+
+
+__all__ = [
+ # Interfaces
+ "ITerminalProtocol",
+ "ITerminalTransport",
+ # Symbolic constants
+ "modes",
+ "privateModes",
+ "FUNCTION_KEYS",
+ "CS_US",
+ "CS_UK",
+ "CS_DRAWING",
+ "CS_ALTERNATE",
+ "CS_ALTERNATE_SPECIAL",
+ "G0",
+ "G1",
+ "G2",
+ "G3",
+ "UNDERLINE",
+ "REVERSE_VIDEO",
+ "BLINK",
+ "BOLD",
+ "NORMAL",
+ # Protocol classes
+ "ServerProtocol",
+ "ClientProtocol",
+]
diff --git a/contrib/python/Twisted/py3/twisted/conch/insults/text.py b/contrib/python/Twisted/py3/twisted/conch/insults/text.py
new file mode 100644
index 0000000000..37a736f4f6
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/conch/insults/text.py
@@ -0,0 +1,176 @@
+# -*- test-case-name: twisted.conch.test.test_text -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Character attribute manipulation API.
+
+This module provides a domain-specific language (using Python syntax)
+for the creation of text with additional display attributes associated
+with it. It is intended as an alternative to manually building up
+strings containing ECMA 48 character attribute control codes. It
+currently supports foreground and background colors (black, red,
+green, yellow, blue, magenta, cyan, and white), intensity selection,
+underlining, blinking and reverse video. Character set selection
+support is planned.
+
+Character attributes are specified by using two Python operations:
+attribute lookup and indexing. For example, the string \"Hello
+world\" with red foreground and all other attributes set to their
+defaults, assuming the name twisted.conch.insults.text.attributes has
+been imported and bound to the name \"A\" (with the statement C{from
+twisted.conch.insults.text import attributes as A}, for example) one
+uses this expression::
+
+ A.fg.red[\"Hello world\"]
+
+Other foreground colors are set by substituting their name for
+\"red\". To set both a foreground and a background color, this
+expression is used::
+
+ A.fg.red[A.bg.green[\"Hello world\"]]
+
+Note that either A.bg.green can be nested within A.fg.red or vice
+versa. Also note that multiple items can be nested within a single
+index operation by separating them with commas::
+
+ A.bg.green[A.fg.red[\"Hello\"], " ", A.fg.blue[\"world\"]]
+
+Other character attributes are set in a similar fashion. To specify a
+blinking version of the previous expression::
+
+ A.blink[A.bg.green[A.fg.red[\"Hello\"], " ", A.fg.blue[\"world\"]]]
+
+C{A.reverseVideo}, C{A.underline}, and C{A.bold} are also valid.
+
+A third operation is actually supported: unary negation. This turns
+off an attribute when an enclosing expression would otherwise have
+caused it to be on. For example::
+
+ A.underline[A.fg.red[\"Hello\", -A.underline[\" world\"]]]
+
+A formatting structure can then be serialized into a string containing the
+necessary VT102 control codes with L{assembleFormattedText}.
+
+@see: L{twisted.conch.insults.text._CharacterAttributes}
+@author: Jp Calderone
+"""
+
+from incremental import Version
+
+from twisted.conch.insults import helper, insults
+from twisted.python import _textattributes
+from twisted.python.deprecate import deprecatedModuleAttribute
+
+flatten = _textattributes.flatten
+
+deprecatedModuleAttribute(
+ Version("Twisted", 13, 1, 0),
+ "Use twisted.conch.insults.text.assembleFormattedText instead.",
+ "twisted.conch.insults.text",
+ "flatten",
+)
+
+_TEXT_COLORS = {
+ "black": helper.BLACK,
+ "red": helper.RED,
+ "green": helper.GREEN,
+ "yellow": helper.YELLOW,
+ "blue": helper.BLUE,
+ "magenta": helper.MAGENTA,
+ "cyan": helper.CYAN,
+ "white": helper.WHITE,
+}
+
+
+class _CharacterAttributes(_textattributes.CharacterAttributesMixin):
+ """
+ Factory for character attributes, including foreground and background color
+ and non-color attributes such as bold, reverse video and underline.
+
+ Character attributes are applied to actual text by using object
+ indexing-syntax (C{obj['abc']}) after accessing a factory attribute, for
+ example::
+
+ attributes.bold['Some text']
+
+ These can be nested to mix attributes::
+
+ attributes.bold[attributes.underline['Some text']]
+
+ And multiple values can be passed::
+
+ attributes.normal[attributes.bold['Some'], ' text']
+
+ Non-color attributes can be accessed by attribute name, available
+ attributes are:
+
+ - bold
+ - blink
+ - reverseVideo
+ - underline
+
+ Available colors are:
+
+ 0. black
+ 1. red
+ 2. green
+ 3. yellow
+ 4. blue
+ 5. magenta
+ 6. cyan
+ 7. white
+
+ @ivar fg: Foreground colors accessed by attribute name, see above
+ for possible names.
+
+ @ivar bg: Background colors accessed by attribute name, see above
+ for possible names.
+ """
+
+ fg = _textattributes._ColorAttribute(
+ _textattributes._ForegroundColorAttr, _TEXT_COLORS
+ )
+ bg = _textattributes._ColorAttribute(
+ _textattributes._BackgroundColorAttr, _TEXT_COLORS
+ )
+
+ attrs = {
+ "bold": insults.BOLD,
+ "blink": insults.BLINK,
+ "underline": insults.UNDERLINE,
+ "reverseVideo": insults.REVERSE_VIDEO,
+ }
+
+
+def assembleFormattedText(formatted):
+ """
+ Assemble formatted text from structured information.
+
+ Currently handled formatting includes: bold, blink, reverse, underline and
+ color codes.
+
+ For example::
+
+ from twisted.conch.insults.text import attributes as A
+ assembleFormattedText(
+ A.normal[A.bold['Time: '], A.fg.lightRed['Now!']])
+
+ Would produce "Time: " in bold formatting, followed by "Now!" with a
+ foreground color of light red and without any additional formatting.
+
+ @param formatted: Structured text and attributes.
+
+ @rtype: L{str}
+ @return: String containing VT102 control sequences that mimic those
+ specified by C{formatted}.
+
+ @see: L{twisted.conch.insults.text._CharacterAttributes}
+ @since: 13.1
+ """
+ return _textattributes.flatten(formatted, helper._FormattingState(), "toVT102")
+
+
+attributes = _CharacterAttributes()
+
+__all__ = ["attributes", "flatten"]
diff --git a/contrib/python/Twisted/py3/twisted/conch/insults/window.py b/contrib/python/Twisted/py3/twisted/conch/insults/window.py
new file mode 100644
index 0000000000..c93fae7b21
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/conch/insults/window.py
@@ -0,0 +1,928 @@
+# -*- test-case-name: twisted.conch.test.test_window -*-
+
+"""
+Simple insults-based widget library
+
+@author: Jp Calderone
+"""
+
+import array
+
+from twisted.conch.insults import helper, insults
+from twisted.python import text as tptext
+
+
+class YieldFocus(Exception):
+ """
+ Input focus manipulation exception
+ """
+
+
+class BoundedTerminalWrapper:
+ def __init__(self, terminal, width, height, xoff, yoff):
+ self.width = width
+ self.height = height
+ self.xoff = xoff
+ self.yoff = yoff
+ self.terminal = terminal
+ self.cursorForward = terminal.cursorForward
+ self.selectCharacterSet = terminal.selectCharacterSet
+ self.selectGraphicRendition = terminal.selectGraphicRendition
+ self.saveCursor = terminal.saveCursor
+ self.restoreCursor = terminal.restoreCursor
+
+ def cursorPosition(self, x, y):
+ return self.terminal.cursorPosition(
+ self.xoff + min(self.width, x), self.yoff + min(self.height, y)
+ )
+
+ def cursorHome(self):
+ return self.terminal.cursorPosition(self.xoff, self.yoff)
+
+ def write(self, data):
+ return self.terminal.write(data)
+
+
+class Widget:
+ focused = False
+ parent = None
+ dirty = False
+ width = height = None
+
+ def repaint(self):
+ if not self.dirty:
+ self.dirty = True
+ if self.parent is not None and not self.parent.dirty:
+ self.parent.repaint()
+
+ def filthy(self):
+ self.dirty = True
+
+ def redraw(self, width, height, terminal):
+ self.filthy()
+ self.draw(width, height, terminal)
+
+ def draw(self, width, height, terminal):
+ if width != self.width or height != self.height or self.dirty:
+ self.width = width
+ self.height = height
+ self.dirty = False
+ self.render(width, height, terminal)
+
+ def render(self, width, height, terminal):
+ pass
+
+ def sizeHint(self):
+ return None
+
+ def keystrokeReceived(self, keyID, modifier):
+ if keyID == b"\t":
+ self.tabReceived(modifier)
+ elif keyID == b"\x7f":
+ self.backspaceReceived()
+ elif keyID in insults.FUNCTION_KEYS:
+ self.functionKeyReceived(keyID, modifier)
+ else:
+ self.characterReceived(keyID, modifier)
+
+ def tabReceived(self, modifier):
+ # XXX TODO - Handle shift+tab
+ raise YieldFocus()
+
+ def focusReceived(self):
+ """
+ Called when focus is being given to this widget.
+
+ May raise YieldFocus is this widget does not want focus.
+ """
+ self.focused = True
+ self.repaint()
+
+ def focusLost(self):
+ self.focused = False
+ self.repaint()
+
+ def backspaceReceived(self):
+ pass
+
+ def functionKeyReceived(self, keyID, modifier):
+ name = keyID
+ if not isinstance(keyID, str):
+ name = name.decode("utf-8")
+ func = getattr(self, "func_" + name, None)
+ if func is not None:
+ func(modifier)
+
+ def characterReceived(self, keyID, modifier):
+ pass
+
+
+class ContainerWidget(Widget):
+ """
+ @ivar focusedChild: The contained widget which currently has
+ focus, or None.
+ """
+
+ focusedChild = None
+ focused = False
+
+ def __init__(self):
+ Widget.__init__(self)
+ self.children = []
+
+ def addChild(self, child):
+ assert child.parent is None
+ child.parent = self
+ self.children.append(child)
+ if self.focusedChild is None and self.focused:
+ try:
+ child.focusReceived()
+ except YieldFocus:
+ pass
+ else:
+ self.focusedChild = child
+ self.repaint()
+
+ def remChild(self, child):
+ assert child.parent is self
+ child.parent = None
+ self.children.remove(child)
+ self.repaint()
+
+ def filthy(self):
+ for ch in self.children:
+ ch.filthy()
+ Widget.filthy(self)
+
+ def render(self, width, height, terminal):
+ for ch in self.children:
+ ch.draw(width, height, terminal)
+
+ def changeFocus(self):
+ self.repaint()
+
+ if self.focusedChild is not None:
+ self.focusedChild.focusLost()
+ focusedChild = self.focusedChild
+ self.focusedChild = None
+ try:
+ curFocus = self.children.index(focusedChild) + 1
+ except ValueError:
+ raise YieldFocus()
+ else:
+ curFocus = 0
+ while curFocus < len(self.children):
+ try:
+ self.children[curFocus].focusReceived()
+ except YieldFocus:
+ curFocus += 1
+ else:
+ self.focusedChild = self.children[curFocus]
+ return
+ # None of our children wanted focus
+ raise YieldFocus()
+
+ def focusReceived(self):
+ self.changeFocus()
+ self.focused = True
+
+ def keystrokeReceived(self, keyID, modifier):
+ if self.focusedChild is not None:
+ try:
+ self.focusedChild.keystrokeReceived(keyID, modifier)
+ except YieldFocus:
+ self.changeFocus()
+ self.repaint()
+ else:
+ Widget.keystrokeReceived(self, keyID, modifier)
+
+
+class TopWindow(ContainerWidget):
+ """
+ A top-level container object which provides focus wrap-around and paint
+ scheduling.
+
+ @ivar painter: A no-argument callable which will be invoked when this
+ widget needs to be redrawn.
+
+ @ivar scheduler: A one-argument callable which will be invoked with a
+ no-argument callable and should arrange for it to invoked at some point in
+ the near future. The no-argument callable will cause this widget and all
+ its children to be redrawn. It is typically beneficial for the no-argument
+ callable to be invoked at the end of handling for whatever event is
+ currently active; for example, it might make sense to call it at the end of
+ L{twisted.conch.insults.insults.ITerminalProtocol.keystrokeReceived}.
+ Note, however, that since calls to this may also be made in response to no
+ apparent event, arrangements should be made for the function to be called
+ even if an event handler such as C{keystrokeReceived} is not on the call
+ stack (eg, using
+ L{reactor.callLater<twisted.internet.interfaces.IReactorTime.callLater>}
+ with a short timeout).
+ """
+
+ focused = True
+
+ def __init__(self, painter, scheduler):
+ ContainerWidget.__init__(self)
+ self.painter = painter
+ self.scheduler = scheduler
+
+ _paintCall = None
+
+ def repaint(self):
+ if self._paintCall is None:
+ self._paintCall = object()
+ self.scheduler(self._paint)
+ ContainerWidget.repaint(self)
+
+ def _paint(self):
+ self._paintCall = None
+ self.painter()
+
+ def changeFocus(self):
+ try:
+ ContainerWidget.changeFocus(self)
+ except YieldFocus:
+ try:
+ ContainerWidget.changeFocus(self)
+ except YieldFocus:
+ pass
+
+ def keystrokeReceived(self, keyID, modifier):
+ try:
+ ContainerWidget.keystrokeReceived(self, keyID, modifier)
+ except YieldFocus:
+ self.changeFocus()
+
+
+class AbsoluteBox(ContainerWidget):
+ def moveChild(self, child, x, y):
+ for n in range(len(self.children)):
+ if self.children[n][0] is child:
+ self.children[n] = (child, x, y)
+ break
+ else:
+ raise ValueError("No such child", child)
+
+ def render(self, width, height, terminal):
+ for ch, x, y in self.children:
+ wrap = BoundedTerminalWrapper(terminal, width - x, height - y, x, y)
+ ch.draw(width, height, wrap)
+
+
+class _Box(ContainerWidget):
+ TOP, CENTER, BOTTOM = range(3)
+
+ def __init__(self, gravity=CENTER):
+ ContainerWidget.__init__(self)
+ self.gravity = gravity
+
+ def sizeHint(self):
+ height = 0
+ width = 0
+ for ch in self.children:
+ hint = ch.sizeHint()
+ if hint is None:
+ hint = (None, None)
+
+ if self.variableDimension == 0:
+ if hint[0] is None:
+ width = None
+ elif width is not None:
+ width += hint[0]
+ if hint[1] is None:
+ height = None
+ elif height is not None:
+ height = max(height, hint[1])
+ else:
+ if hint[0] is None:
+ width = None
+ elif width is not None:
+ width = max(width, hint[0])
+ if hint[1] is None:
+ height = None
+ elif height is not None:
+ height += hint[1]
+
+ return width, height
+
+ def render(self, width, height, terminal):
+ if not self.children:
+ return
+
+ greedy = 0
+ wants = []
+ for ch in self.children:
+ hint = ch.sizeHint()
+ if hint is None:
+ hint = (None, None)
+ if hint[self.variableDimension] is None:
+ greedy += 1
+ wants.append(hint[self.variableDimension])
+
+ length = (width, height)[self.variableDimension]
+ totalWant = sum(w for w in wants if w is not None)
+ if greedy:
+ leftForGreedy = int((length - totalWant) / greedy)
+
+ widthOffset = heightOffset = 0
+
+ for want, ch in zip(wants, self.children):
+ if want is None:
+ want = leftForGreedy
+
+ subWidth, subHeight = width, height
+ if self.variableDimension == 0:
+ subWidth = want
+ else:
+ subHeight = want
+
+ wrap = BoundedTerminalWrapper(
+ terminal,
+ subWidth,
+ subHeight,
+ widthOffset,
+ heightOffset,
+ )
+ ch.draw(subWidth, subHeight, wrap)
+ if self.variableDimension == 0:
+ widthOffset += want
+ else:
+ heightOffset += want
+
+
+class HBox(_Box):
+ variableDimension = 0
+
+
+class VBox(_Box):
+ variableDimension = 1
+
+
+class Packer(ContainerWidget):
+ def render(self, width, height, terminal):
+ if not self.children:
+ return
+
+ root = int(len(self.children) ** 0.5 + 0.5)
+ boxes = [VBox() for n in range(root)]
+ for n, ch in enumerate(self.children):
+ boxes[n % len(boxes)].addChild(ch)
+ h = HBox()
+ map(h.addChild, boxes)
+ h.render(width, height, terminal)
+
+
+class Canvas(Widget):
+ focused = False
+
+ contents = None
+
+ def __init__(self):
+ Widget.__init__(self)
+ self.resize(1, 1)
+
+ def resize(self, width, height):
+ contents = array.array("B", b" " * width * height)
+ if self.contents is not None:
+ for x in range(min(width, self._width)):
+ for y in range(min(height, self._height)):
+ contents[width * y + x] = self[x, y]
+ self.contents = contents
+ self._width = width
+ self._height = height
+ if self.x >= width:
+ self.x = width - 1
+ if self.y >= height:
+ self.y = height - 1
+
+ def __getitem__(self, index):
+ (x, y) = index
+ return self.contents[(self._width * y) + x]
+
+ def __setitem__(self, index, value):
+ (x, y) = index
+ self.contents[(self._width * y) + x] = value
+
+ def clear(self):
+ self.contents = array.array("B", b" " * len(self.contents))
+
+ def render(self, width, height, terminal):
+ if not width or not height:
+ return
+
+ if width != self._width or height != self._height:
+ self.resize(width, height)
+ for i in range(height):
+ terminal.cursorPosition(0, i)
+ text = self.contents[
+ self._width * i : self._width * i + self._width
+ ].tobytes()
+ text = text[:width]
+ terminal.write(text)
+
+
+def horizontalLine(terminal, y, left, right):
+ terminal.selectCharacterSet(insults.CS_DRAWING, insults.G0)
+ terminal.cursorPosition(left, y)
+ terminal.write(b"\161" * (right - left))
+ terminal.selectCharacterSet(insults.CS_US, insults.G0)
+
+
+def verticalLine(terminal, x, top, bottom):
+ terminal.selectCharacterSet(insults.CS_DRAWING, insults.G0)
+ for n in range(top, bottom):
+ terminal.cursorPosition(x, n)
+ terminal.write(b"\170")
+ terminal.selectCharacterSet(insults.CS_US, insults.G0)
+
+
+def rectangle(terminal, position, dimension):
+ """
+ Draw a rectangle
+
+ @type position: L{tuple}
+ @param position: A tuple of the (top, left) coordinates of the rectangle.
+ @type dimension: L{tuple}
+ @param dimension: A tuple of the (width, height) size of the rectangle.
+ """
+ (top, left) = position
+ (width, height) = dimension
+ terminal.selectCharacterSet(insults.CS_DRAWING, insults.G0)
+
+ terminal.cursorPosition(top, left)
+ terminal.write(b"\154")
+ terminal.write(b"\161" * (width - 2))
+ terminal.write(b"\153")
+ for n in range(height - 2):
+ terminal.cursorPosition(left, top + n + 1)
+ terminal.write(b"\170")
+ terminal.cursorForward(width - 2)
+ terminal.write(b"\170")
+ terminal.cursorPosition(0, top + height - 1)
+ terminal.write(b"\155")
+ terminal.write(b"\161" * (width - 2))
+ terminal.write(b"\152")
+
+ terminal.selectCharacterSet(insults.CS_US, insults.G0)
+
+
+class Border(Widget):
+ def __init__(self, containee):
+ Widget.__init__(self)
+ self.containee = containee
+ self.containee.parent = self
+
+ def focusReceived(self):
+ return self.containee.focusReceived()
+
+ def focusLost(self):
+ return self.containee.focusLost()
+
+ def keystrokeReceived(self, keyID, modifier):
+ return self.containee.keystrokeReceived(keyID, modifier)
+
+ def sizeHint(self):
+ hint = self.containee.sizeHint()
+ if hint is None:
+ hint = (None, None)
+ if hint[0] is None:
+ x = None
+ else:
+ x = hint[0] + 2
+ if hint[1] is None:
+ y = None
+ else:
+ y = hint[1] + 2
+ return x, y
+
+ def filthy(self):
+ self.containee.filthy()
+ Widget.filthy(self)
+
+ def render(self, width, height, terminal):
+ if self.containee.focused:
+ terminal.write(b"\x1b[31m")
+ rectangle(terminal, (0, 0), (width, height))
+ terminal.write(b"\x1b[0m")
+ wrap = BoundedTerminalWrapper(terminal, width - 2, height - 2, 1, 1)
+ self.containee.draw(width - 2, height - 2, wrap)
+
+
+class Button(Widget):
+ def __init__(self, label, onPress):
+ Widget.__init__(self)
+ self.label = label
+ self.onPress = onPress
+
+ def sizeHint(self):
+ return len(self.label), 1
+
+ def characterReceived(self, keyID, modifier):
+ if keyID == b"\r":
+ self.onPress()
+
+ def render(self, width, height, terminal):
+ terminal.cursorPosition(0, 0)
+ if self.focused:
+ terminal.write(b"\x1b[1m" + self.label + b"\x1b[0m")
+ else:
+ terminal.write(self.label)
+
+
+class TextInput(Widget):
+ def __init__(self, maxwidth, onSubmit):
+ Widget.__init__(self)
+ self.onSubmit = onSubmit
+ self.maxwidth = maxwidth
+ self.buffer = b""
+ self.cursor = 0
+
+ def setText(self, text):
+ self.buffer = text[: self.maxwidth]
+ self.cursor = len(self.buffer)
+ self.repaint()
+
+ def func_LEFT_ARROW(self, modifier):
+ if self.cursor > 0:
+ self.cursor -= 1
+ self.repaint()
+
+ def func_RIGHT_ARROW(self, modifier):
+ if self.cursor < len(self.buffer):
+ self.cursor += 1
+ self.repaint()
+
+ def backspaceReceived(self):
+ if self.cursor > 0:
+ self.buffer = self.buffer[: self.cursor - 1] + self.buffer[self.cursor :]
+ self.cursor -= 1
+ self.repaint()
+
+ def characterReceived(self, keyID, modifier):
+ if keyID == b"\r":
+ self.onSubmit(self.buffer)
+ else:
+ if len(self.buffer) < self.maxwidth:
+ self.buffer = (
+ self.buffer[: self.cursor] + keyID + self.buffer[self.cursor :]
+ )
+ self.cursor += 1
+ self.repaint()
+
+ def sizeHint(self):
+ return self.maxwidth + 1, 1
+
+ def render(self, width, height, terminal):
+ currentText = self._renderText()
+ terminal.cursorPosition(0, 0)
+ if self.focused:
+ terminal.write(currentText[: self.cursor])
+ cursor(terminal, currentText[self.cursor : self.cursor + 1] or b" ")
+ terminal.write(currentText[self.cursor + 1 :])
+ terminal.write(b" " * (self.maxwidth - len(currentText) + 1))
+ else:
+ more = self.maxwidth - len(currentText)
+ terminal.write(currentText + b"_" * more)
+
+ def _renderText(self):
+ return self.buffer
+
+
+class PasswordInput(TextInput):
+ def _renderText(self):
+ return "*" * len(self.buffer)
+
+
+class TextOutput(Widget):
+ text = b""
+
+ def __init__(self, size=None):
+ Widget.__init__(self)
+ self.size = size
+
+ def sizeHint(self):
+ return self.size
+
+ def render(self, width, height, terminal):
+ terminal.cursorPosition(0, 0)
+ text = self.text[:width]
+ terminal.write(text + b" " * (width - len(text)))
+
+ def setText(self, text):
+ self.text = text
+ self.repaint()
+
+ def focusReceived(self):
+ raise YieldFocus()
+
+
+class TextOutputArea(TextOutput):
+ WRAP, TRUNCATE = range(2)
+
+ def __init__(self, size=None, longLines=WRAP):
+ TextOutput.__init__(self, size)
+ self.longLines = longLines
+
+ def render(self, width, height, terminal):
+ n = 0
+ inputLines = self.text.splitlines()
+ outputLines = []
+ while inputLines:
+ if self.longLines == self.WRAP:
+ line = inputLines.pop(0)
+ if not isinstance(line, str):
+ line = line.decode("utf-8")
+ wrappedLines = []
+ for wrappedLine in tptext.greedyWrap(line, width):
+ if not isinstance(wrappedLine, bytes):
+ wrappedLine = wrappedLine.encode("utf-8")
+ wrappedLines.append(wrappedLine)
+ outputLines.extend(wrappedLines or [b""])
+ else:
+ outputLines.append(inputLines.pop(0)[:width])
+ if len(outputLines) >= height:
+ break
+ for n, L in enumerate(outputLines[:height]):
+ terminal.cursorPosition(0, n)
+ terminal.write(L)
+
+
+class Viewport(Widget):
+ _xOffset = 0
+ _yOffset = 0
+
+ @property
+ def xOffset(self):
+ return self._xOffset
+
+ @xOffset.setter
+ def xOffset(self, value):
+ if self._xOffset != value:
+ self._xOffset = value
+ self.repaint()
+
+ @property
+ def yOffset(self):
+ return self._yOffset
+
+ @yOffset.setter
+ def yOffset(self, value):
+ if self._yOffset != value:
+ self._yOffset = value
+ self.repaint()
+
+ _width = 160
+ _height = 24
+
+ def __init__(self, containee):
+ Widget.__init__(self)
+ self.containee = containee
+ self.containee.parent = self
+
+ self._buf = helper.TerminalBuffer()
+ self._buf.width = self._width
+ self._buf.height = self._height
+ self._buf.connectionMade()
+
+ def filthy(self):
+ self.containee.filthy()
+ Widget.filthy(self)
+
+ def render(self, width, height, terminal):
+ self.containee.draw(self._width, self._height, self._buf)
+
+ # XXX /Lame/
+ for y, line in enumerate(
+ self._buf.lines[self._yOffset : self._yOffset + height]
+ ):
+ terminal.cursorPosition(0, y)
+ n = 0
+ for n, (ch, attr) in enumerate(line[self._xOffset : self._xOffset + width]):
+ if ch is self._buf.void:
+ ch = b" "
+ terminal.write(ch)
+ if n < width:
+ terminal.write(b" " * (width - n - 1))
+
+
+class _Scrollbar(Widget):
+ def __init__(self, onScroll):
+ Widget.__init__(self)
+ self.onScroll = onScroll
+ self.percent = 0.0
+
+ def smaller(self):
+ self.percent = min(1.0, max(0.0, self.onScroll(-1)))
+ self.repaint()
+
+ def bigger(self):
+ self.percent = min(1.0, max(0.0, self.onScroll(+1)))
+ self.repaint()
+
+
+class HorizontalScrollbar(_Scrollbar):
+ def sizeHint(self):
+ return (None, 1)
+
+ def func_LEFT_ARROW(self, modifier):
+ self.smaller()
+
+ def func_RIGHT_ARROW(self, modifier):
+ self.bigger()
+
+ _left = "\N{BLACK LEFT-POINTING TRIANGLE}"
+ _right = "\N{BLACK RIGHT-POINTING TRIANGLE}"
+ _bar = "\N{LIGHT SHADE}"
+ _slider = "\N{DARK SHADE}"
+
+ def render(self, width, height, terminal):
+ terminal.cursorPosition(0, 0)
+ n = width - 3
+ before = int(n * self.percent)
+ after = n - before
+ me = (
+ self._left
+ + (self._bar * before)
+ + self._slider
+ + (self._bar * after)
+ + self._right
+ )
+ terminal.write(me.encode("utf-8"))
+
+
+class VerticalScrollbar(_Scrollbar):
+ def sizeHint(self):
+ return (1, None)
+
+ def func_UP_ARROW(self, modifier):
+ self.smaller()
+
+ def func_DOWN_ARROW(self, modifier):
+ self.bigger()
+
+ _up = "\N{BLACK UP-POINTING TRIANGLE}"
+ _down = "\N{BLACK DOWN-POINTING TRIANGLE}"
+ _bar = "\N{LIGHT SHADE}"
+ _slider = "\N{DARK SHADE}"
+
+ def render(self, width, height, terminal):
+ terminal.cursorPosition(0, 0)
+ knob = int(self.percent * (height - 2))
+ terminal.write(self._up.encode("utf-8"))
+ for i in range(1, height - 1):
+ terminal.cursorPosition(0, i)
+ if i != (knob + 1):
+ terminal.write(self._bar.encode("utf-8"))
+ else:
+ terminal.write(self._slider.encode("utf-8"))
+ terminal.cursorPosition(0, height - 1)
+ terminal.write(self._down.encode("utf-8"))
+
+
+class ScrolledArea(Widget):
+ """
+ A L{ScrolledArea} contains another widget wrapped in a viewport and
+ vertical and horizontal scrollbars for moving the viewport around.
+ """
+
+ def __init__(self, containee):
+ Widget.__init__(self)
+ self._viewport = Viewport(containee)
+ self._horiz = HorizontalScrollbar(self._horizScroll)
+ self._vert = VerticalScrollbar(self._vertScroll)
+
+ for w in self._viewport, self._horiz, self._vert:
+ w.parent = self
+
+ def _horizScroll(self, n):
+ self._viewport.xOffset += n
+ self._viewport.xOffset = max(0, self._viewport.xOffset)
+ return self._viewport.xOffset / 25.0
+
+ def _vertScroll(self, n):
+ self._viewport.yOffset += n
+ self._viewport.yOffset = max(0, self._viewport.yOffset)
+ return self._viewport.yOffset / 25.0
+
+ def func_UP_ARROW(self, modifier):
+ self._vert.smaller()
+
+ def func_DOWN_ARROW(self, modifier):
+ self._vert.bigger()
+
+ def func_LEFT_ARROW(self, modifier):
+ self._horiz.smaller()
+
+ def func_RIGHT_ARROW(self, modifier):
+ self._horiz.bigger()
+
+ def filthy(self):
+ self._viewport.filthy()
+ self._horiz.filthy()
+ self._vert.filthy()
+ Widget.filthy(self)
+
+ def render(self, width, height, terminal):
+ wrapper = BoundedTerminalWrapper(terminal, width - 2, height - 2, 1, 1)
+ self._viewport.draw(width - 2, height - 2, wrapper)
+ if self.focused:
+ terminal.write(b"\x1b[31m")
+ horizontalLine(terminal, 0, 1, width - 1)
+ verticalLine(terminal, 0, 1, height - 1)
+ self._vert.draw(
+ 1, height - 1, BoundedTerminalWrapper(terminal, 1, height - 1, width - 1, 0)
+ )
+ self._horiz.draw(
+ width, 1, BoundedTerminalWrapper(terminal, width, 1, 0, height - 1)
+ )
+ terminal.write(b"\x1b[0m")
+
+
+def cursor(terminal, ch):
+ terminal.saveCursor()
+ terminal.selectGraphicRendition(str(insults.REVERSE_VIDEO))
+ terminal.write(ch)
+ terminal.restoreCursor()
+ terminal.cursorForward()
+
+
+class Selection(Widget):
+ # Index into the sequence
+ focusedIndex = 0
+
+ # Offset into the displayed subset of the sequence
+ renderOffset = 0
+
+ def __init__(self, sequence, onSelect, minVisible=None):
+ Widget.__init__(self)
+ self.sequence = sequence
+ self.onSelect = onSelect
+ self.minVisible = minVisible
+ if minVisible is not None:
+ self._width = max(map(len, self.sequence))
+
+ def sizeHint(self):
+ if self.minVisible is not None:
+ return self._width, self.minVisible
+
+ def func_UP_ARROW(self, modifier):
+ if self.focusedIndex > 0:
+ self.focusedIndex -= 1
+ if self.renderOffset > 0:
+ self.renderOffset -= 1
+ self.repaint()
+
+ def func_PGUP(self, modifier):
+ if self.renderOffset != 0:
+ self.focusedIndex -= self.renderOffset
+ self.renderOffset = 0
+ else:
+ self.focusedIndex = max(0, self.focusedIndex - self.height)
+ self.repaint()
+
+ def func_DOWN_ARROW(self, modifier):
+ if self.focusedIndex < len(self.sequence) - 1:
+ self.focusedIndex += 1
+ if self.renderOffset < self.height - 1:
+ self.renderOffset += 1
+ self.repaint()
+
+ def func_PGDN(self, modifier):
+ if self.renderOffset != self.height - 1:
+ change = self.height - self.renderOffset - 1
+ if change + self.focusedIndex >= len(self.sequence):
+ change = len(self.sequence) - self.focusedIndex - 1
+ self.focusedIndex += change
+ self.renderOffset = self.height - 1
+ else:
+ self.focusedIndex = min(
+ len(self.sequence) - 1, self.focusedIndex + self.height
+ )
+ self.repaint()
+
+ def characterReceived(self, keyID, modifier):
+ if keyID == b"\r":
+ self.onSelect(self.sequence[self.focusedIndex])
+
+ def render(self, width, height, terminal):
+ self.height = height
+ start = self.focusedIndex - self.renderOffset
+ if start > len(self.sequence) - height:
+ start = max(0, len(self.sequence) - height)
+
+ elements = self.sequence[start : start + height]
+
+ for n, ele in enumerate(elements):
+ terminal.cursorPosition(0, n)
+ if n == self.renderOffset:
+ terminal.saveCursor()
+ if self.focused:
+ modes = str(insults.REVERSE_VIDEO), str(insults.BOLD)
+ else:
+ modes = (str(insults.REVERSE_VIDEO),)
+ terminal.selectGraphicRendition(*modes)
+ text = ele[:width]
+ terminal.write(text + (b" " * (width - len(text))))
+ if n == self.renderOffset:
+ terminal.restoreCursor()