diff options
author | shmel1k <shmel1k@ydb.tech> | 2023-11-26 18:16:14 +0300 |
---|---|---|
committer | shmel1k <shmel1k@ydb.tech> | 2023-11-26 18:43:30 +0300 |
commit | b8cf9e88f4c5c64d9406af533d8948deb050d695 (patch) | |
tree | 218eb61fb3c3b96ec08b4d8cdfef383104a87d63 /contrib/python/Twisted/py3/twisted/conch/insults | |
parent | 523f645a83a0ec97a0332dbc3863bb354c92a328 (diff) | |
download | ydb-b8cf9e88f4c5c64d9406af533d8948deb050d695.tar.gz |
add kikimr_configure
Diffstat (limited to 'contrib/python/Twisted/py3/twisted/conch/insults')
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() |