Source code for TermTk.TTkWidgets.TTkTerminal.terminalview

# MIT License
#
# Copyright (c) 2023 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

__all__ = ['TTkTerminalView']

import re

from dataclasses import dataclass

from TermTk.TTkCore.canvas import TTkCanvas
from TermTk.TTkCore.color import TTkColor
from TermTk.TTkCore.log import TTkLog
from TermTk.TTkCore.constant import TTkK
from TermTk.TTkCore.string import TTkString
from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot

from TermTk.TTkGui.clipboard import TTkClipboard

from TermTk.TTkAbstract.abstractscrollview import TTkAbstractScrollView

from TermTk.TTkWidgets.TTkTerminal.terminal_screen  import _TTkTerminalScreen
from TermTk.TTkWidgets.TTkTerminal.mode             import TTkTerminalModes

from TermTk.TTkCore.TTkTerm.colors import TTkTermColor
from TermTk.TTkCore.TTkTerm.colors_ansi_map import ansiMap16, ansiMap256

from .terminalview_CSI_DEC import _TTkTerminal_CSI_DEC

class _termLog():
    # debug = TTkLog.debug
    # info  = TTkLog.info
    error = TTkLog.error
    warn  = TTkLog.warn
    fatal = TTkLog.fatal
    # mouse = TTkLog.error

    debug = lambda _:None
    info  = lambda _:None
    # error = lambda _:None
    # warn  = lambda _:None
    # fatal = lambda _:None
    mouse = lambda _:None

[docs] class TTkTerminalView(TTkAbstractScrollView, _TTkTerminal_CSI_DEC): ''' :py:class:`TTkTerminalView` is a terminal emulator fot TermTk .. warning:: This is an Alpha feature, it is not optimized and the API definition may change in the future TermShot from: `tests/t.pty/test.pty.006.terminal.07.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/tests/t.pty/test.pty.006.terminal.07.py>`_ :: ╔════════════════════════════════════════════════════════════════════════════════════╗ ║ Terminello n.1 [_][^][x]║ ╟────────────────────────────────────────────────────────────────────────────────────╢ ║ $ neofetch ▲║ ║`.-::---.. PierCecco@FrankenstOne ┊║ ║ .:++++ooooosssoo:. --------------------------- ┊║ ║ .+o++::. `.:oos+. OS: LMDE 6 (faye) x86_64 ┊║ ║ :oo:.` -+oo: Host: Lemur Pro lemp11 ┊║ ║ `+o/` .::::::-. .++-` Kernel: 6.1.0-26-amd64 ▓║ ║`/s/ .yyyyyyyyyyo: +o-` Uptime: 27 days, 2 hours, 33 mins ▓║ ║`so .ss ohyo` :s-: Packages: 2787 (dpkg), 38 (flatpak) ▓║ ║`s/ .ss h m myy/ /s`` Shell: bash 5.2.15 ▓║ ║`s: `oo s m Myy+-o:` Resolution: 1920x1080, 3440x1440 ▓║ ║`oo :+sdoohyoydyso/. DE: Cinnamon 6.2.9 ▓║ ║ :o. .:////////++: WM: Mutter (Muffin) ▓║ ║ `/++ -:::::- WM Theme: Mint-Y-Dark-Orange (Mint-Y) ▓║ ║ `++- Theme: Mint-L-Dark [GTK2/3] ▓║ ║ `/+- Icons: Mint-X-Orange [GTK2/3] ▓║ ║ .+/. Terminal: tmux ▓║ ║ .:+-. CPU: 12th Gen Intel i7-1255U (12) @ 4.700GHz ▓║ ║ `--.`` GPU: Intel Alder Lake-UP3 GT2 [Iris Xe Graphics] ▓║ ║ Memory: 32111MiB / 39956MiB ▓║ ║ ▓║ ║23 PierCecco FrankenstOne < 12:24 > (1052) ~/github/Varie/pyTermTk ▓║ ║ $ ▼║ ╚════════════════════════════════════════════════════════════════════════════════════╝ Quickstart: .. code-block:: python import TermTk as ttk root = ttk.TTk(mouseTrack=True) win = ttk.TTkWindow(parent=root, title="Terminal", size=(80+2,24+4), layout=ttk.TTkGridLayout()) term = ttk.TTkTerminal(parent=win) th = ttk.TTkTerminalHelper(term=term) th.runShell() root.mainloop() More examples are available :ref:`here <Examples-Terminal>`. ''' @dataclass class _Terminal(): bracketedMode: bool = False DCSstring: str = "" IRM: bool = False @dataclass class _Keyboard(): flags: int = 0 @dataclass class _Mouse(): reportPress: bool = False reportDrag: bool = False reportMove: bool = False sgrMode: bool = False bell:pyTTkSignal ''' This signal is emitted when the `bell <https://en.wikipedia.org/wiki/Bell_character>`__ is received. ''' terminalClosed:pyTTkSignal ''' This signal is emitted when the terminl is closed. ''' titleChanged:pyTTkSignal ''' This signal is emitted when the terminal title change through OSC "ESC ]0;" :param title: the new title :type title: str ''' textSelected:pyTTkSignal ''' This signal is emitted when a text is selected. :param text: the selected text :type text: :py:class:`ttkString` ''' termData:pyTTkSignal ''' This signal is emitted when data event fires. This happens for example when the user types or pastes into the terminal. The event value is whatever 'str' results, in a typical setup, this should be passed on to the backing pty. This signal is used in :py:class:`TTkTerminalHelper` through :py:meth:`TTkTerminalHelper.attachTTkTerminal` to frward all the terminal events to the pty interface. :param data: the event data :type data: str ''' termResized:pyTTkSignal ''' This signal is emitted when the terminal is resized. :param size: the new size [width, height] of the terminal :type size: (int,int) ''' __slots__ = ( '_termLoop', '_newSize', '_clipboard', '_selecting', '_buffer_lines', '_buffer_screen', '_keyboard', '_mouse', '_terminal', '_screen_current', '_screen_normal', '_screen_alt', # Signals 'bell', 'titleChanged', 'terminalClosed', 'textSelected', 'termData','termResized') def __init__(self, **kwargs) -> None: #Signals self.bell = pyTTkSignal() self.terminalClosed = pyTTkSignal() self.titleChanged = pyTTkSignal(str) self.textSelected = pyTTkSignal(TTkString) self.termData = pyTTkSignal(str) self.termResized = pyTTkSignal(int,int) self._newSize = None self._terminal = TTkTerminalView._Terminal() self._keyboard = TTkTerminalView._Keyboard() self._mouse = TTkTerminalView._Mouse() self._buffer_lines = [TTkString()] self._screen_normal = _TTkTerminalScreen() self._screen_alt = _TTkTerminalScreen() self._screen_current = self._screen_normal self._clipboard = TTkClipboard() self._selecting = False self._screen_normal.bell.connect(self.bell.emit) self._screen_alt.bell.connect( self.bell.emit) self._termLoop = self._loopGenerator() next(self._termLoop) self._termLoop.send("") # Do NOT use super() due to the multiple imports TTkAbstractScrollView.__init__(self, **kwargs) w,h = self.size() self._screen_alt.resize(w,h) self._screen_normal.resize(w,h) self.setFocusPolicy(TTkK.ClickFocus + TTkK.TabFocus) self.enableWidgetCursor() self.viewChanged.connect(self._viewChangedHandler) self._screen_normal.bufferedLinesChanged.connect(self._screenChanged) self._screen_alt.bufferedLinesChanged.connect(self._screenChanged) @pyTTkSlot() def _screenChanged(self): self.viewMoveTo(0, len(self._screen_current._bufferedLines)) self.viewChanged.emit() @pyTTkSlot() def _viewChangedHandler(self): self.update()
[docs] def viewFullAreaSize(self) -> (int, int): w,h = self.size() h += len(self._screen_current._bufferedLines) return w,h
[docs] def termSize(self) -> tuple[int,int]: ''' This property holds the size of the terminal :return: a tuple of 2 integers (width, height) :rtype: tuple ''' return self.size()
def resizeEvent(self, w: int, h: int): self._newSize = (w,h) # self._screen_alt.resize(w,h) # self._screen_normal.resize(w,h) self.termResized.emit(w,h) return super().resizeEvent(w, h) # xterm escape sequences from: # https://invisible-island.net/xterm/ctlseqs/ctlseqs.html # https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ re_CURSOR = re.compile(r'^\[((\d*)(;(\d*))*)([@^`A-Za-z])') # https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ # Basic Re for CSI Ps matches: # CSI : Control Sequence Introducer "<ESC>[" = '\033[' # Ps : A single (usually optional) numeric parameter, composed of one or more digits. # fn : the single char defining the function re_CSI_Ps_fu = re.compile(r'^\[(\d*)([@ABCDEFGIJKLMPSTXZ^`abcdeghinqx])') re_CSI_Ps_Ps_fu = re.compile(r'^\[(\d*);(\d*)([Hf])') re_DEC_SET_RST = re.compile(r'^\[(\??)([\d;]+)([lh])') # re_CURSOR_1 = re.compile(r'^(\d+)([ABCDEFGIJKLMPSTXZHf])') re_OSC_ps_Pt = re.compile(r'^(\d*);(.*)$') re_XTWINOPS = re.compile(r'^')
[docs] def termWrite(self, data:str) -> None: ''' Write data to the terminal. :params data: the data to write :type data: str ''' if data: self._termLoop.send(data)
def _loopGenerator(self): def _checkSize(): if self._newSize: TTkLog.debug(f"{self._newSize=}") self._screen_alt.resize(*self._newSize) self._screen_normal.resize(*self._newSize) self._newSize = None yield SGR_SET = TTkTermColor.SGR_SET # Precacing those variables to SGR_RST = TTkTermColor.SGR_RST # speedup the search leftUnhandled = "" while True: _checkSize() # Entry Point 1 out = yield if not out: return _checkSize() sout = (leftUnhandled+out).split('\033') _termLog.debug(f"{leftUnhandled=} - {sout[0]=}") # The first element is not an escaped sequence if sout[0]: self._screen_current.pushLine(sout[0],irm=self._terminal.IRM) escapeGenerator = (i for i in sout[1:]) for slice in escapeGenerator: leftUnhandled = "" ssss = slice.replace('\033','<ESC>').replace('\n','\\n').replace('\r','\\r') _termLog.debug(f"slice: '{ssss}'") ################################################ # CSI Modes # CSI Pm ; Pm ; ... h # Set Mode (SM). # CSI Pm ; Pm ; ... l # Reset Mode (RM). # CSI ? Pm ; Pm ; ... h # DEC Private Mode Set (DECSET). # CSI ? Pm ; Pm ; ... l # DEC Private Mode Reset (DECRST). ################################################ if m := TTkTerminalView.re_DEC_SET_RST.match(slice): en = m.end() qm = (m.group(1) == '?') sr = (m.group(3) == 'h') pms = [int(_pm) for _pm in m.group(2).split(';')] if qm: for pm in pms: self._CSI_DEC_SET_RST(pm,sr) else: for pm in pms: self._CSI_SM_RM(pm,sr) slice = slice[en:] ################################################ # CSI Color Functions # CSI Pm ; Pm ; ... m ################################################ elif ((m := TTkTerminalView.re_CURSOR.match(slice)) and (mg := m.groups()) and mg[-1] == 'm' ): # _termLog.debug(f"Handle color {mg[0]=}") en = m.end() color=TTkColor.RST if mg[0] not in ['0','']: values = mg[0].split(';') color = self._screen_current.color() fg = color._fg bg = color._bg mod = color._mod clean = False while values: s = int(values.pop(0)) if s==0: # Reset Color/Format fg = None bg = None mod = 0 clean = True elif s==39: # Ps = 3 9 ⇒ Set foreground color to default, ECMA-48 3rd. fg = None elif s==49: # Ps = 4 9 ⇒ Set background color to default, ECMA-48 3rd. bg = None elif ( # Ansi 16 colors 30 <= s <= 37 or # fg [ 30 - 37] 90 <= s <= 97 ): # fg [ 90 - 97] Bright fg = ansiMap16.get(s) elif ( # Ansi 16 colors 40 <= s <= 47 or # bg [ 40 - 47] 100 <= s <= 107 ) : # bg [100 - 107] Bright bg = ansiMap16.get(s) elif s == 38: t = int(values.pop(0)) if t == 5:# 256 fg fg = ansiMap256.get(int(values.pop(0))) if t == 2:# 24 bit fg fg = (int(values.pop(0)),int(values.pop(0)),int(values.pop(0))) elif s == 48: t = int(values.pop(0)) if t == 5:# 256 bg bg = ansiMap256.get(int(values.pop(0))) if t == 2:# 24 bit bg bg = (int(values.pop(0)),int(values.pop(0)),int(values.pop(0))) elif _sgr:=SGR_SET.get(s,None): mod |= _sgr elif _sgr:=SGR_RST.get(s,None): mod &= _sgr else: _termLog.warn(f"Unhandled color: <ESC>{slice}") color = TTkColor(fg=fg, bg=bg, mod=mod, clean=clean) self._screen_alt.setColor(color) self._screen_normal.setColor(color) # _termLog.debug(TTkString(f"color - <ESC>[{mg[0]}",color).toAnsi()) slice = slice[en:] ################################################ # CSI Common Functions # CSI Pm ; Pm ; ... X ################################################ elif m: en = m.end() fn = m.group(5) defval = self._CSI_Default_MAP.get(fn,(1,1)) y = ps = int(y) if (y:=m.group(2)) else defval[0] sep = m.group(3) x = int(x) if (x:=m.group(4)) else defval[1] _termLog.debug(f"{mg[0]}{fn} = ps:{y=} {sep=} {x=} {fn=}") if fn in ['n']: # Handle the non screen related functions _ex = self._CSI_MAP.get( fn, lambda a,b,c: _termLog.error(f"Unhandled <ESC>[{mg[0]}{fn} = ps:{y=} {sep=} {x=} {fn=}")) _ex(self,y,x) else: # Handle the screen related functions _ex = self._screen_current._CSI_MAP.get( fn, lambda a,b,c: _termLog.error(f"Unhandled <ESC>[{mg[0]}{fn} = ps:{y=} {sep=} {x=} {fn=}")) _ex(self._screen_current,y,x) slice = slice[en:] ################################################ # ESC = Application Keypad (DECKPAM). ################################################ elif slice and slice[0] in ['=','>']: # ESC = Application Keypad (DECKPAM). if slice[0] == '=': self._keyboard.flags |= TTkTerminalModes.MODE_DECKPAM _termLog.warn("Unhandled <ESC> = (DECKPAM)") # ESC > Normal Keypad (DECKPNM), VT100. elif slice[0] == '>': self._keyboard.flags &= ~TTkTerminalModes.MODE_DECKPAM _termLog.warn("Unhandled <ESC> > (DECKPNM)") slice = slice[1:] ################################################ # ESC P # Device Control String (DCS is 0x90). # ESC \ # String Terminator (ST is 0x9c). # # ESC P Pt ESC \ # Device Control String (DCS) xterm implements no DCS functions; Pt is ignored. Pt need not be printable characters. # # NOTE: # DCS is an escape sequence that does not happen often # So far I saw it only during vim initialization and # only to check if the TERM=screen is capable to handle DCS # I try to keep everything related in a self contained place ################################################ elif slice and slice[0] == 'P': # Sarting DCS (ESC P) and wait for ST (ESC \) self._terminal.DCSstring = slice[1:] slice = "" # I am using a closure in order to exit the routine at the end def __processDCSEscapeGenerator(): for dcs in escapeGenerator: if dcs[0] == '\\': _termLog.warn(f"DCS: {self._terminal.DCSstring} - Not Handled") self._terminal.DCSstring = "" return True, dcs[1:] self._terminal.DCSstring += dcs return False, "" ret, slice = __processDCSEscapeGenerator() if not ret: # If the terminator is not in the current escaped slices # I need to fetch from the input until I got all of them # This is not the nicest thing but it save a bunch of extra # hcecks at any input just to find if we are in DCS mode or not while True: # Entry Point 2 out = yield if not out: return (), "" sout = out.split('\033') self._terminal.DCSstring += sout[0] escapeGenerator = (i for i in sout[1:]) ret, slice = __processDCSEscapeGenerator() if ret: break if not slice: continue # Operating System Commands = <ESC> ] # # OSC Ps ; Pt BEL # # OSC Ps ; Pt ST # Set Text Parameters. Some control sequences return # information: # o For colors and font, if Pt is a "?", the control sequence # elicits a response which consists of the control sequence # which would set the corresponding value. # o The dtterm control sequences allow you to determine the # icon name and window title. # # XTerm accepts either BEL or ST for terminating OSC # sequences, and when returning information, uses the same # terminator used in a query. While the latter is preferred, # the former is supported for legacy applications: # o Although documented in the changes for X.V10R4 (December # 1986), BEL as a string terminator dates from X11R4 # (December 1989). # o Since XFree86-3.1.2Ee (August 1996), xterm has accepted ST # (the documented string terminator in ECMA-48). # # Ps specifies the type of operation to perform: # Ps = 0 -> Change Icon Name and Window Title to Pt. # Ps = 1 -> Change Icon Name to Pt. # Ps = 2 -> Change Window Title to Pt. # Ps = 3 -> Set X property on top-level window. Pt should be # in the form "prop=value", or just "prop" to delete the # property. # Ps = 4 ; c ; spec -> Change Color Number c to the color # specified by spec. # # The spec can be a name or RGB specification as per # XParseColor. Any number of c/spec pairs may be given. The # color numbers correspond to the ANSI colors 0-7, their bright # versions 8-15, and if supported, the remainder of the 88-color # or 256-color table. # # If a "?" is given rather than a name or RGB specification, # xterm replies with a control sequence of the same form which # can be used to set the corresponding color. Because more than # one pair of color number and specification can be given in one # control sequence, xterm can make more than one reply. # # Ps = 5 ; c ; spec -> Change Special Color Number c to the # color specified by spec. # # The spec parameter can be a name or RGB specification as per # XParseColor. Any number of c/spec pairs may be given. The # special colors can also be set by adding the maximum number of # colors (e.g., 88 or 256) to these codes in an OSC 4 control: # # Pc = 0 <- resource colorBD (BOLD). # Pc = 1 <- resource colorUL (UNDERLINE). # Pc = 2 <- resource colorBL (BLINK). # Pc = 3 <- resource colorRV (REVERSE). # Pc = 4 <- resource colorIT (ITALIC). # # Ps = 6 ; c ; f -> Enable/disable Special Color Number c. # The second parameter tells xterm to enable the corresponding # color mode if nonzero, disable it if zero. OSC 6 is the same # as OSC 1 0 6 . # # If no parameters are given, this control has no effect. # # The 10 colors (below) which may be set or queried using 1 0 # through 1 9 are denoted dynamic colors, since the # corresponding control sequences were the first means for # setting xterm's colors dynamically, i.e., after it was # started. They are not the same as the ANSI colors (however, # the dynamic text foreground and background colors are used # when ANSI colors are reset using SGR 3 9 and 4 9 , # respectively). These controls may be disabled using the # allowColorOps resource. At least one parameter is expected # for Pt. Each successive parameter changes the next color in # the list. The value of Ps tells the starting point in the # list. The colors are specified by name or RGB specification # as per XParseColor. # # If a "?" is given rather than a name or RGB specification, # xterm replies with a control sequence of the same form which # can be used to set the corresponding dynamic color. Because # more than one pair of color number and specification can be # given in one control sequence, xterm can make more than one # reply. # # Ps = 1 0 -> Change VT100 text foreground color to Pt. # Ps = 1 1 -> Change VT100 text background color to Pt. # Ps = 1 2 -> Change text cursor color to Pt. # Ps = 1 3 -> Change pointer foreground color to Pt. # Ps = 1 4 -> Change pointer background color to Pt. # Ps = 1 5 -> Change Tektronix foreground color to Pt. # Ps = 1 6 -> Change Tektronix background color to Pt. # Ps = 1 7 -> Change highlight background color to Pt. # Ps = 1 8 -> Change Tektronix cursor color to Pt. # Ps = 1 9 -> Change highlight foreground color to Pt. # # Ps = 2 2 -> Change pointer cursor to Pt. # # Ps = 4 6 -> Change Log File to Pt. This is normally # disabled by a compile-time option. # # Ps = 5 0 -> Set Font to Pt. These controls may be disabled # using the allowFontOps resource. If Pt begins with a "#", # index in the font menu, relative (if the next character is a # plus or minus sign) or absolute. A number is expected but not # required after the sign (the default is the current entry for # relative, zero for absolute indexing). # # The same rule (plus or minus sign, optional number) is used # when querying the font. The remainder of Pt is ignored. # # A font can be specified after a "#" index expression, by # adding a space and then the font specifier. # # If the TrueType Fonts menu entry is set (the renderFont # resource), then this control sets/queries the faceName # resource. # # Ps = 5 1 -> reserved for Emacs shell. # # Ps = 5 2 -> Manipulate Selection Data. These controls may # be disabled using the allowWindowOps resource. The parameter # Pt is parsed as # Pc ; Pd # # The first, Pc, may contain zero or more characters from the # set c , p , q , s , 0 , 1 , 2 , 3 , 4 , 5 , 6 , and 7 . It is # used to construct a list of selection parameters for # clipboard, primary, secondary, select, or cut-buffers 0 # through 7 respectively, in the order given. If the parameter # is empty, xterm uses s 0 , to specify the configurable # primary/clipboard selection and cut-buffer 0. # # The second parameter, Pd, gives the selection data. Normally # this is a string encoded in base64 (RFC-4648). The data # becomes the new selection, which is then available for pasting # by other applications. # # If the second parameter is a ? , xterm replies to the host # with the selection data encoded using the same protocol. It # uses the first selection found by asking successively for each # item from the list of selection parameters. # # If the second parameter is neither a base64 string nor ? , # then the selection is cleared. # # Ps = 6 0 -> Query allowed features (XTQALLOWED). XTerm # replies with # # OSC 6 0 ; Pt ST # # where Pt is a comma-separated list of the allowed optional # runtime features, i.e., zero or more of these resource names: # # allowColorOps # allowFontOps # allowMouseOps # allowPasteControls # allowTcapOps # allowTitleOps # allowWindowOps # # Ps = 6 1 -> Query disallowed features (XTQDISALLOWED). The # second parameter (i.e., the main feature) must be one of the # resource names returned by OSC 6 0 . XTerm replies with # # OSC 6 1 ; Pt ST # # where Pt is a comma-separated list of the optional runtime # features which would be disallowed if the main feature is # disabled. # # Ps = 1 0 4 ; c -> Reset Color Number c. It is reset to the # color specified by the corresponding X resource. Any number # of c parameters may be given. These parameters correspond to # the ANSI colors 0-7, their bright versions 8-15, and if # supported, the remainder of the 88-color or 256-color table. # If no parameters are given, the entire table will be reset. # # Ps = 1 0 5 ; c -> Reset Special Color Number c. It is reset # to the color specified by the corresponding X resource. Any # number of c parameters may be given. These parameters # correspond to the special colors which can be set using an OSC # 5 control (or by adding the maximum number of colors using an # OSC 4 control). # # If no parameters are given, all special colors will be reset. # # Ps = 1 0 6 ; c ; f -> Enable/disable Special Color Number c. # The second parameter tells xterm to enable the corresponding # color mode if nonzero, disable it if zero. # # Pc = 0 <- resource colorBDMode (BOLD). # Pc = 1 <- resource colorULMode (UNDERLINE). # Pc = 2 <- resource colorBLMode (BLINK). # Pc = 3 <- resource colorRVMode (REVERSE). # Pc = 4 <- resource colorITMode (ITALIC). # Pc = 5 <- resource colorAttrMode (Override ANSI). # # If no parameters are given, this control has no effect. # # The dynamic colors can also be reset to their default # (resource) values: # Ps = 1 1 0 -> Reset VT100 text foreground color. # Ps = 1 1 1 -> Reset VT100 text background color. # Ps = 1 1 2 -> Reset text cursor color. # Ps = 1 1 3 -> Reset pointer foreground color. # Ps = 1 1 4 -> Reset pointer background color. # Ps = 1 1 5 -> Reset Tektronix foreground color. # Ps = 1 1 6 -> Reset Tektronix background color. # Ps = 1 1 7 -> Reset highlight color. # Ps = 1 1 8 -> Reset Tektronix cursor color. # Ps = 1 1 9 -> Reset highlight foreground color. # # Ps = I ; c -> Set icon to file. Sun shelltool, CDE dtterm. # The file is expected to be XPM format, and uses the same # search logic as the iconHint resource. # # Ps = l ; c -> Set window title. Sun shelltool, CDE dtterm. # # Ps = L ; c -> Set icon label. Sun shelltool, CDE dtterm. elif slice and slice[0] == ']': # Sarting OSC (ESC ]) and wait for ST (ESC \) or BEL (\a) slice = slice[1:] oscString = "" def __checkOSCBell(__osc:str, __oscString:str): if '\a' in __osc: # BEL (Ctrl-G) (\a) as terminator belIndex = __osc.index('\a') __oscString += __osc[:belIndex] return True, __osc[belIndex+1:], __oscString __oscString += __osc return False, "", __oscString # I am using a closure in order to exit the routine at the end def __processOSCEscapeGenerator(__oscString:str): _slice = "" for __osc in escapeGenerator: if __osc[0] == '\\': # ST (0x9c) (<ESC> \) as terminator return True, __osc[1:], __oscString ret, _slice = __checkOSCBell(__osc, __oscString) if ret: return ret, _slice, __oscString return False, "", __oscString ret, slice, oscString = __checkOSCBell(slice,oscString) if not ret: ret, slice, oscString = __processOSCEscapeGenerator(oscString) if not ret: # If the terminator is not in the current escaped slices # I need to fetch from the input until I got all of them # This is not the nicest thing but it save a bunch of extra # hcecks at any input just to find if we are in OSC mode or not while True: # Entry Point 3 out = yield if not out: return (), "" sout = out.split('\033') self._terminal.DCSstring += sout[0] escapeGenerator = (i for i in sout[1:]) ret, slice, oscString = __processOSCEscapeGenerator(oscString) if ret: break _termLog.info(f"OSC: {oscString}") if m := TTkTerminalView.re_OSC_ps_Pt.match(oscString): en = m.end() ps = int(m.group(1)) pt = m.group(2) # Ps specifies the type of operation to perform: # Ps = 0 -> Change Icon Name and Window Title to Pt. # Ps = 1 -> Change Icon Name to Pt. # Ps = 2 -> Change Window Title to Pt. if ps in (0,1,2): self.titleChanged.emit(pt) else: _termLog.warn(f"OSC: {oscString} - Not Recognised - yet") if not slice: continue # C1 Escape Codes # ESC E Next Line # ESC D Index = '\n' # ESC M Reverse Index # ESC H Horizontal Tab Set elif slice and slice[0] in ('D','E','H','M','O','P','V','W','X','Z'): # Handle the screen related functions _ex = self._screen_current._C1_MAP.get( slice[0], lambda : _termLog.error(f"Unhandled C1 <ESC>{slice[0]}")) _ex(self._screen_current) slice = slice[1:] ################################################ # Everything else ################################################ else: _termLog.warn(f"Unhandled Slice:'<ESC>{slice}'".replace('\n','\\n')) leftUnhandled = "\033"+slice continue self._screen_current.pushLine(slice,irm=self._terminal.IRM) self.update() self.setWidgetCursor(pos=self._screen_current.getCursor()) _termLog.debug(f"wc:{self._screen_current.getCursor()}") def pasteEvent(self, txt:str): if self._terminal.bracketedMode: txt = "\033[200~"+txt+"\033[201~" self.termData.emit(txt.encode()) return True def keyEvent(self, evt): # _termLog.debug(f"Key: {evt.code=}") _termLog.debug(f"Key: {str(evt)=}") if evt.type == TTkK.SpecialKey: if evt.mod == TTkK.ControlModifier and evt.key == TTkK.Key_V: txt = self._clipboard.text() self.pasteEvent(str(txt)) return True if self._keyboard.flags & TTkTerminalModes.MODE_DECCKM: if code := {TTkK.Key_Up: b"\033OA", TTkK.Key_Down: b"\033OB", TTkK.Key_Right: b"\033OC", TTkK.Key_Left: b"\033OD"}.get(evt.key): self.termData.emit(code) return True # if evt.key == TTkK.Key_Enter: # _termLog.debug(f"Key: Enter") # # self.termData.emit(b'\n') # # self.termData.emit(evt.code.encode()) else: # Input char _termLog.debug(f"Key: {evt.key=}") # self.termData.emit(evt.key.encode()) self.termData.emit(evt.code.encode()) return True # Extended coordinates # The original X10 mouse protocol limits the Cx and Cy ordinates to 223 # (=255 - 32). XTerm supports more than one scheme for extending this # range, by changing the protocol encoding: # # UTF-8 (1005) # This enables UTF-8 encoding for Cx and Cy under all tracking # modes, expanding the maximum encodable position from 223 to # 2015. For positions less than 95, the resulting output is # identical under both modes. Under extended mouse mode, # positions greater than 95 generate "extra" bytes which will # confuse applications which do not treat their input as a UTF-8 # stream. Likewise, Cb will be UTF-8 encoded, to reduce # confusion with wheel mouse events. # # Under normal mouse mode, positions outside (160,94) result in # byte pairs which can be interpreted as a single UTF-8 # character; applications which do treat their input as UTF-8 # will almost certainly be confused unless extended mouse mode # is active. # # This scheme has the drawback that the encoded coordinates will # not pass through luit(1) unchanged, e.g., for locales using # non-UTF-8 encoding. # # SGR (1006) # The normal mouse response is altered to use # # o CSI < followed by semicolon-separated # # o encoded button value, # # o Px and Py ordinates and # # o a final character which is M for button press and m for # button release. # # The encoded button value in this case does not add 32 since # that was useful only in the X10 scheme for ensuring that the # byte containing the button value is a printable code. # # o The modifiers are encoded in the same way. # # o A different final character is used for button release to # resolve the X10 ambiguity regarding which button was # released. # # The highlight tracking responses are also modified to an SGR- # like format, using the same SGR-style scheme and button- # encodings. # # URXVT (1015) # The normal mouse response is altered to use # # o CSI followed by semicolon-separated # # o encoded button value, # # o the Px and Py ordinates and final character M . # # This uses the same button encoding as X10, but printing it as # a decimal integer rather than as a single byte. # # However, CSI M can be mistaken for DL (delete lines), while # the highlight tracking CSI T can be mistaken for SD (scroll # down), and the Window manipulation controls. For these # reasons, the 1015 control is not recommended; it is not an # improvement over 1006. # # SGR-Pixels (1016) # Use the same mouse response format as the 1006 control, but # report position in pixels rather than character cells. def _sendMouse(self, evt): if not self._mouse.reportPress: return False if ( not self._mouse.reportDrag and evt.evt in (TTkK.Drag, TTkK.Move)): # _termLog.mouse(f"{self._mouse.reportDrag=} {evt.evt in (TTkK.Drag, TTkK.Move)=}") return False x,y = evt.x+1, evt.y+1 if self._mouse.sgrMode: k = { TTkK.NoButton : 3, TTkK.AllButtons : 0, TTkK.LeftButton : 0, TTkK.RightButton : 2, TTkK.MidButton : 1, TTkK.MiddleButton : 1, TTkK.Wheel : 64, }.get(evt.key, 0) k,km,pr = { TTkK.Press: (k, 0,'M'), TTkK.Release: (k, 0,'m'), TTkK.Move: (35, 0,'M'), TTkK.Drag: (k, 32,'M'), TTkK.WHEEL_Up: (k, 0,'M'), TTkK.WHEEL_Down:(k, 1,'M')}.get( evt.evt,(0,0,'M')) # _termLog.mouse(f'Mouse: <ESC>[<{k+km};{x};{y}{pr}') self.termData.emit(f'\033[<{k+km};{x};{y}{pr}'.encode()) else: head = { TTkK.Press: b'\033[M ', TTkK.Release: b'\033[M#', TTkK.Move: b'\033[MC', TTkK.Drag: b'\033[M@', TTkK.WHEEL_Up: b'\033[M`', TTkK.WHEEL_Down:b'\033[Ma'}.get( evt.evt, b'') bah = bytearray(head) bah.append((x+32)%0xff) bah.append((y+32)%0xff) # _termLog.mouse(f'Mouse: '+bah.decode().replace('\033','<ESC>')) self.termData.emit(bah) return True def mousePressEvent(self, evt): if self._mouse.reportPress: self._screen_current.select(0,0) return self._sendMouse(evt) | True self._selecting = True x,y = evt.x,evt.y ox,oy = self.getViewOffsets() self._screen_current.select(x+ox,y+oy) self.update() return True def mouseDragEvent(self, evt): if self._mouse.reportPress: self._screen_current.select(0,0) return self._sendMouse(evt) x,y = evt.x,evt.y ox,oy = self.getViewOffsets() self._screen_current.select(x+ox,y+oy,moveAnchor=False) self.update() return True def mouseReleaseEvent(self, evt): self._selecting = False if (selected := self._screen_current.getSelected()): # self._clipboard.setText(selected) self.textSelected.emit(selected) return self._sendMouse(evt) def wheelEvent(self, evt): if self._sendMouse(evt): return True ret = super().wheelEvent(evt) if self._selecting: x,y = evt.x,evt.y ox,oy = self.getViewOffsets() self._screen_current.select(x+ox,y+oy,moveAnchor=False) self.update() return ret def mouseTapEvent(self, evt): return self._sendMouse(evt) def mouseDoubleClickEvent(self, evt): return self._sendMouse(evt) def mouseMoveEvent(self, evt): if self._mouse.reportMove: return self._sendMouse(evt) return False def paintEvent(self, canvas: TTkCanvas): w,h = self.size() ox,oy = self.getViewOffsets() self._screen_current.paintEvent(canvas,w,h,ox,oy) # This map include the default values in case Ps or Row/Col are empty # Since almost all the CSI use 1 as default, I am including here only # the ones that are different _CSI_Default_MAP = { 'J' : (0,None), 'K' : (0,None) } # CSI Ps n Device Status Report (DSR). # Ps = 5 ⇒ Status Report. # Result ("OK") is CSI 0 n # Ps = 6 ⇒ Report Cursor Position (CPR) [row;column]. # Result is CSI r ; c R # # Note: it is possible for this sequence to be sent by a # function key. For example, with the default keyboard # configuration the shifted F1 key may send (with shift-, # control-, alt-modifiers) # # CSI 1 ; 2 R , or # CSI 1 ; 5 R , or # CSI 1 ; 6 R , etc. # # The second parameter encodes the modifiers; values range from # 2 to 16. See the section PC-Style Function Keys for the # codes. The modifyFunctionKeys and modifyKeyboard resources # can change the form of the string sent from the modified F1 # key. def _CSI_n_DSR(self, ps, _): x,y = self._screen_current.getCursor() if ps==6: self.termData.emit(f"\033[{y+1};{x+1}R".encode()) elif ps==5: self.termData.emit(f"\033[0n".encode()) # CSI Ps ; Ps ; Ps t # Window manipulation (XTWINOPS), dtterm, extended by xterm. # These controls may be disabled using the allowWindowOps # resource. # # xterm uses Extended Window Manager Hints (EWMH) to maximize # the window. Some window managers have incomplete support for # EWMH. For instance, fvwm, flwm and quartz-wm advertise # support for maximizing windows horizontally or vertically, but # in fact equate those to the maximize operation. # # Valid values for the first (and any additional parameters) # are: # Ps = 1 ⇒ De-iconify window. # Ps = 2 ⇒ Iconify window. # Ps = 3 ; x ; y ⇒ Move window to [x, y]. # Ps = 4 ; height ; width ⇒ Resize the xterm window to # given height and width in pixels. Omitted parameters reuse # the current height or width. Zero parameters use the # display's height or width. # Ps = 5 ⇒ Raise the xterm window to the front of the # stacking order. # Ps = 6 ⇒ Lower the xterm window to the bottom of the # stacking order. # Ps = 7 ⇒ Refresh the xterm window. # Ps = 8 ; height ; width ⇒ Resize the text area to given # height and width in characters. Omitted parameters reuse the # current height or width. Zero parameters use the display's # height or width. # Ps = 9 ; 0 ⇒ Restore maximized window. # Ps = 9 ; 1 ⇒ Maximize window (i.e., resize to screen # size). # Ps = 9 ; 2 ⇒ Maximize window vertically. # Ps = 9 ; 3 ⇒ Maximize window horizontally. # Ps = 1 0 ; 0 ⇒ Undo full-screen mode. # Ps = 1 0 ; 1 ⇒ Change to full-screen. # Ps = 1 0 ; 2 ⇒ Toggle full-screen. # Ps = 1 1 ⇒ Report xterm window state. # If the xterm window is non-iconified, it returns CSI 1 t . # If the xterm window is iconified, it returns CSI 2 t . # Ps = 1 3 ⇒ Report xterm window position. # Note: X Toolkit positions can be negative, but the reported # values are unsigned, in the range 0-65535. Negative values # correspond to 32768-65535. # Result is CSI 3 ; x ; y t # Ps = 1 3 ; 2 ⇒ Report xterm text-area position. # Result is CSI 3 ; x ; y t # Ps = 1 4 ⇒ Report xterm text area size in pixels. # Result is CSI 4 ; height ; width t # Ps = 1 4 ; 2 ⇒ Report xterm window size in pixels. # Normally xterm's window is larger than its text area, since it # includes the frame (or decoration) applied by the window # manager, as well as the area used by a scroll-bar. # Result is CSI 4 ; height ; width t # Ps = 1 5 ⇒ Report size of the screen in pixels. # Result is CSI 5 ; height ; width t # Ps = 1 6 ⇒ Report xterm character cell size in pixels. # Result is CSI 6 ; height ; width t # Ps = 1 8 ⇒ Report the size of the text area in characters. # Result is CSI 8 ; height ; width t # Ps = 1 9 ⇒ Report the size of the screen in characters. # Result is CSI 9 ; height ; width t # Ps = 2 0 ⇒ Report xterm window's icon label. # Result is OSC L label ST # Ps = 2 1 ⇒ Report xterm window's title. # Result is OSC l label ST # Ps = 2 2 ; 0 ⇒ Save xterm icon and window title on stack. # Ps = 2 2 ; 1 ⇒ Save xterm icon title on stack. # Ps = 2 2 ; 2 ⇒ Save xterm window title on stack. # Ps = 2 3 ; 0 ⇒ Restore xterm icon and window title from # stack. # Ps = 2 3 ; 1 ⇒ Restore xterm icon title from stack. # Ps = 2 3 ; 2 ⇒ Restore xterm window title from stack. # Ps >= 2 4 ⇒ Resize to Ps lines (DECSLPP), VT340 and VT420. # xterm adapts this by resizing its window. def _CSI_t_XTWINOPS(self, ps, _): pass _CSI_MAP = { 'n': _CSI_n_DSR, } # CSI Pm h Set Mode (SM). # Ps = 2 ⇒ Keyboard Action Mode (KAM). # Ps = 4 ⇒ Insert Mode (IRM). # Ps = 1 2 ⇒ Send/receive (SRM). # Ps = 2 0 ⇒ Automatic Newline (LNM). # Ps = 3 4 ⇒ Normal Cursor Visibility # CSI Pm l Reset Mode (RM). # Ps = 2 ⇒ Keyboard Action Mode (KAM). # Ps = 4 ⇒ Replace Mode (IRM). # Ps = 1 2 ⇒ Send/receive (SRM). # Ps = 2 0 ⇒ Normal Linefeed (LNM). # Ps = 3 4 ⇒ Normal Cursor Visibility def _CSI_SM_RM(self, ps, s): if ps == 4: # Insert/Replace Mode self._terminal.IRM = s elif ps == 34: _termLog.warn(f"Unhandled (SM) <ESC>{ps}{'h' if s else 'l'} Normal Cursor Visibility") else: _termLog.error(f"Unhandled (SM) <ESC>{ps}{'h' if s else 'l'}") def _CSI_DEC_SET_RST(self, ps, s): _dec = self._CSI_DEC_SET_RST_MAP.get( ps, lambda a,b: _termLog.error(f"Unhandled (DEC) <ESC>[{ps}{'h' if s else 'l'}")) _dec(self, s)