From 6c9e86c8250efe9dd3e3d629c098a16162b809cc Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 8 Sep 2023 19:01:16 +0100 Subject: [PATCH] Fixed move mouse report in the terminal, added CSI_REP --- TermTk/TTkWidgets/TTkTerminal/terminal.py | 118 ++++++++++++++++-- TermTk/TTkWidgets/TTkTerminal/terminal_alt.py | 18 ++- docs/MDNotes/terminal/Vim Internals.md | 6 + tests/test.input.raw.py | 13 +- ...test.ui.001.py => test.ui.001.frame.01.py} | 1 + tests/test.ui.001.window.01.py | 34 +++++ tests/timeit/18.itertools.01.groupby.py | 106 ++++++++++++++++ 7 files changed, 273 insertions(+), 23 deletions(-) create mode 100644 docs/MDNotes/terminal/Vim Internals.md rename tests/{test.ui.001.py => test.ui.001.frame.01.py} (95%) create mode 100755 tests/test.ui.001.window.01.py create mode 100755 tests/timeit/18.itertools.01.groupby.py diff --git a/TermTk/TTkWidgets/TTkTerminal/terminal.py b/TermTk/TTkWidgets/TTkTerminal/terminal.py index 92c326b9..2f297a1b 100644 --- a/TermTk/TTkWidgets/TTkTerminal/terminal.py +++ b/TermTk/TTkWidgets/TTkTerminal/terminal.py @@ -191,6 +191,8 @@ class TTkTerminal(TTkWidget): re_OSC_ps_Pt = re.compile('^(\d*);(.*)$') + re_XTWINOPS = re.compile('^') + @pyTTkSlot() def _quit(self): if self._quit_pipe: @@ -244,7 +246,7 @@ class TTkTerminal(TTkWidget): for slice in escapeGenerator: leftUnhandled = "" ssss = slice.replace('\033','').replace('\n','\\n').replace('\r','\\r') - _termLog.debug(f"slice: {ssss}") + _termLog.debug(f"slice: '{ssss}'") ################################################ # CSI Modes @@ -829,6 +831,7 @@ class TTkTerminal(TTkWidget): return True if ( not self._mouse.reportDrag and evt.evt in (TTkK.Drag, TTkK.Move)): + _termLog.error(f"{self._mouse.reportDrag=} {evt.evt in (TTkK.Drag, TTkK.Move)=}") return True x,y = evt.x+1, evt.y+1 @@ -843,21 +846,21 @@ class TTkTerminal(TTkWidget): TTkK.Wheel : 64, }.get(evt.key, 0) - km,pr = { - TTkK.Press: ( 0,'M'), - TTkK.Release: ( 0,'m'), - TTkK.Move: ( 0,'M'), - TTkK.Drag: (32,'M'), - TTkK.WHEEL_Up: ( 0,'M'), - TTkK.WHEEL_Down:( 1,'M')}.get( - evt.evt,(0,'M')) - # _termLog.error(f'Mouse: [<{k+km};{x};{y}{pr}') + 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.error(f'Mouse: [<{k+km};{x};{y}{pr}') self._inout.write(f'\033[<{k+km};{x};{y}{pr}'.encode()) else: head = { TTkK.Press: b'\033[M ', TTkK.Release: b'\033[M#', - # TTkK.Move: 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( @@ -866,7 +869,7 @@ class TTkTerminal(TTkWidget): bah = bytearray(head) bah.append((x+32)%0xff) bah.append((y+32)%0xff) - # _termLog.error(f'Mouse: '+bah.decode().replace('\033','')) + _termLog.error(f'Mouse: '+bah.decode().replace('\033','')) self._inout.write(bah) return True @@ -923,6 +926,84 @@ class TTkTerminal(TTkWidget): elif ps==5: self._inout.write(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, } @@ -997,7 +1078,7 @@ class TTkTerminal(TTkWidget): self._mouse.reportPress = s self._mouse.reportDrag = False self._mouse.reportMove = False - _termLog.info(f"1002 Mouse Tracking {s=}") + _termLog.info(f"1000 Mouse Tracking {s=}") # CSI ? Pm h # DEC Private Mode Set (DECSET). @@ -1027,6 +1108,16 @@ class TTkTerminal(TTkWidget): self._mouse.reportMove = s _termLog.info(f"1003 Mouse Tracking {s=}") + # CSI ? Pm h + # DEC Private Mode Set (DECSET). + # Ps = 1 0 0 4 ⇒ Send FocusIn/FocusOut events, xterm. + # CSI ? Pm l + # DEC Private Mode Reset (DECRST). + # Ps = 1 0 0 4 ⇒ Don't send FocusIn/FocusOut events, xterm. + def _CSI_DEC_SR_1004(self, s): + _termLog.warn(f"Unhandled 1004 Focus In/Out event {s=}") + + # CSI ? Pm h # DEC Private Mode Set (DECSET). # Ps = 1 0 0 6 ⇒ Enable SGR Mouse Mode, xterm. @@ -1104,6 +1195,7 @@ class TTkTerminal(TTkWidget): 1000: _CSI_DEC_SR_1000, 1002: _CSI_DEC_SR_1002, 1003: _CSI_DEC_SR_1003, + 1004: _CSI_DEC_SR_1004, 1006: _CSI_DEC_SR_1006, 1015: _CSI_DEC_SR_1015, 1047: _CSI_DEC_SR_1047, diff --git a/TermTk/TTkWidgets/TTkTerminal/terminal_alt.py b/TermTk/TTkWidgets/TTkTerminal/terminal_alt.py index f17666ee..29bcfa59 100644 --- a/TermTk/TTkWidgets/TTkTerminal/terminal_alt.py +++ b/TermTk/TTkWidgets/TTkTerminal/terminal_alt.py @@ -46,6 +46,7 @@ class _TTkTerminalAltScreen(): '_scrollingRegion', '_bufferSize', '_bufferedLines', '_w', '_h', '_color', '_canvas', + '_last', # Signals 'bell' ) @@ -53,6 +54,7 @@ class _TTkTerminalAltScreen(): self.bell = pyTTkSignal() self._w = w self._h = h + self._last = None self._bufferSize = bufferSize self._bufferedLines = collections.deque(maxlen=bufferSize) self._terminalCursor = (0,0) @@ -91,6 +93,8 @@ class _TTkTerminalAltScreen(): x,y = self._terminalCursor w,h = self._w, self._h st,sb = self._scrollingRegion + self._last = txt[-1] if txt else None + for bi, tout in enumerate(txt.split('\a')): # grab the bells if bi: self.bell.emit() @@ -507,7 +511,9 @@ class _TTkTerminalAltScreen(): def _CSI_a_HPR(self, ps, _): pass # CSI Ps b Repeat the preceding graphic character Ps times (REP). - def _CSI_b_REP(self, ps, _): pass + def _CSI_b_REP(self, ps, _): + if self._last: + self._pushTxt(self._last*ps) # CSI Ps c Send Device Attributes (Primary DA). # Ps = 0 or omitted ⇒ request attributes from terminal. The @@ -1642,8 +1648,8 @@ class _TTkTerminalAltScreen(): 'J': _CSI_J_ED, # CSI Ps J Erase in Display (ED), VT100. [0:Below, 1:Above, 2:All, 3:SavedLines] 'K': _CSI_K_EL, # CSI Ps K Erase in Line (EL), VT100. [0:Right, 1:Left, 2:All] 'L': _CSI_L_IL, # CSI Ps L Insert Ps Line(s) (default = 1) (IL). - 'M': _CSI_M_DL, - 'P': _CSI_P_DCH, + 'M': _CSI_M_DL, # CSI Ps M Delete Ps Line(s) (default = 1) (DL). + 'P': _CSI_P_DCH, # CSI Ps P Delete Ps Character(s) (default = 1) (DCH). 'S': _CSI_S_SU, # CSI Ps S Scroll up Ps lines (default = 1) (SU), VT420, ECMA-48. 'T': _CSI_T_SD, # CSI Ps T Scroll down Ps lines (default = 1) (SD), VT420. # 'X': _CSI_X_ECH, @@ -1651,11 +1657,11 @@ class _TTkTerminalAltScreen(): # '^': _CSI___SD, # '`': _CSI___HPA, # 'a': _CSI_a_HPR, - # 'b': _CSI_b_REP, + 'b': _CSI_b_REP, # CSI Ps b Repeat the preceding graphic character Ps times (REP). # 'c': _CSI_c_Pri_DA, - 'd': _CSI_d_VPA, + 'd': _CSI_d_VPA, # CSI Ps d Line Position Absolute [row] (default = [1,column]) (VPA). # 'e': _CSI_e_VPR, - 'f': _CSI_f_HVP, # CSI Ps ; Ps f Horizontal and Vertical Position [row;column] (default = [1,1]) (HVP). + 'f': _CSI_f_HVP, # CSI Ps ; Ps f Horizontal and Vertical Position [row;column] (default = [1,1]) (HVP). # 'g': _CSI_g_TBC, # 'h': _CSI_h_SM, # 'i': _CSI_i_MC, diff --git a/docs/MDNotes/terminal/Vim Internals.md b/docs/MDNotes/terminal/Vim Internals.md new file mode 100644 index 00000000..998fa994 --- /dev/null +++ b/docs/MDNotes/terminal/Vim Internals.md @@ -0,0 +1,6 @@ +### mouse drag +https://github.com/vim/vim/blob/545c8a506e7e0921ded7eb7ffe3518279cbcb16a/src/os_unix.c +CSI ? 1000 h = enable the mouse press release reported by the terminal +CSI ? 1002 h = enable the mouse press release drag reported by the mouse terminal +if vim does not recognise the ttym flags either as (SGR, RXVT, XTERM2, XTERM), the mouse drag report is not requested +Note: My fault, my .vimrc does not set xterm2 if the environment is not inside tmux \ No newline at end of file diff --git a/tests/test.input.raw.py b/tests/test.input.raw.py index 9584f067..8d027920 100755 --- a/tests/test.input.raw.py +++ b/tests/test.input.raw.py @@ -40,6 +40,7 @@ def reset(): # Reset TTkTerm.push("\033[?1000l") TTkTerm.push("\033[?1002l") + TTkTerm.push("\033[?1003l") TTkTerm.push("\033[?1015l") TTkTerm.push("\033[?1006l") TTkTerm.push("\033[?1049l") # Switch to normal screen @@ -47,12 +48,16 @@ def reset(): reset() -TTkTerm.push("\033[?2004h") # Paste Bracketed mode -# TTkTerm.push("\033[?1000h") +# TTkTerm.push("\033[?2004h") # Paste Bracketed mode +TTkTerm.push("\033[?2004l") # disable Paste Bracketed mode +TTkTerm.push("\033[?1049h") # Switch to alternate screen +TTkTerm.push("\033[?1000h") # TTkTerm.push("\033[?1002h") +TTkTerm.push("\033[?1003h") # TTkTerm.push("\033[?1006h") -# TTkTerm.push("\033[?1015h") -TTkTerm.push("\033[?1049h") # Switch to alternate screen +TTkTerm.push("\033[?1015h") +TTkTerm.push("\033[?1006h") +TTkTerm.push("\033[?25l") # TTkTerm.push(TTkTerm.Mouse.ON) # TTkTerm.push(TTkTerm.Mouse.DIRECT_ON) diff --git a/tests/test.ui.001.py b/tests/test.ui.001.frame.01.py similarity index 95% rename from tests/test.ui.001.py rename to tests/test.ui.001.frame.01.py index adbc456f..a3a55dfd 100755 --- a/tests/test.ui.001.py +++ b/tests/test.ui.001.frame.01.py @@ -31,5 +31,6 @@ ttk.TTkLog.use_default_file_logging() root = ttk.TTk() ttk.TTkFrame(parent=root, x=5, y=3, width=20, height=15, border=True) +# ttk.TTkWindow(parent=root, x=5, y=3, width=20, height=15, border=True) # ttk.Button(root, text="Hello World").grid() root.mainloop() \ No newline at end of file diff --git a/tests/test.ui.001.window.01.py b/tests/test.ui.001.window.01.py new file mode 100755 index 00000000..31e1d196 --- /dev/null +++ b/tests/test.ui.001.window.01.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2021 Eugenio Parodi +# +# 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. + +import sys, os + +sys.path.append(os.path.join(sys.path[0],'..')) +import TermTk as ttk + +ttk.TTkLog.use_default_file_logging() + +root = ttk.TTk() +ttk.TTkWindow(parent=root, pops=(0,0), size=(30,5), border=True) +root.mainloop() \ No newline at end of file diff --git a/tests/timeit/18.itertools.01.groupby.py b/tests/timeit/18.itertools.01.groupby.py new file mode 100755 index 00000000..b67dd51c --- /dev/null +++ b/tests/timeit/18.itertools.01.groupby.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2023 Eugenio Parodi +# +# 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. + +# This test is bases on the example in: +# https://www.geeksforgeeks.org/python-consecutive-characters-frequency/ + +import sys, os + +import timeit + +from itertools import groupby + +sys.path.append(os.path.join(sys.path[0],'../..')) + +testString1 = "EugenioXXXXXXXXXXXXXXXXXXParodiYYYYYYmalleoloZZZZsassso_______________122333444455555666666777777888888889999999990000000000" + +def process1(txt): + ret = "" + for ch in txt: + ret += ch + return ret + +def process2(txt): + # return ''.join([ch*l if (l:=len(list(j)))>4 else f"[{l}b{ch}" for ch, j in groupby(txt)]) + return ''.join([ch*l if (l:=len(list(j)))<=4 else f"[{l}b{ch}" for ch, j in groupby(txt)]) + +def process3(txt): + chBk = txt[0] + count = 0 + ret = "" + for ch in txt: + if ch == chBk: + count +=1 + else: + if count>4: + ret += f"[{count}b{chBk}" + else: + ret += chBk*count + chBk = ch + count = 1 + if count>4: + ret += f"[{count}b{chBk}" + else: + ret += chBk*count + return ret + +def process4(txt): + chBk = txt[0] + count = 0 + ret = "" + # genStr = (c for c in txt) + genStr = iter(txt) + ch = next(genStr) + while ch: + count = 1 + while ch == (_ch:=next(genStr,None)): + count +=1 + if count>4: + ret += f"[{count}b{ch}" + else: + ret += ch*count + ch = _ch + # if count>4: + # ret += f"[{count}b{ch}" + # else: + # ret += ch*count + return ret + + +def test1(): return process1(testString1) +def test2(): return process2(testString1) +def test3(): return process3(testString1) +def test4(): return process4(testString1) + +loop = 100000 + +a={} + +iii = 1 +while (testName := f'test{iii}') and (testName in globals()): + result = timeit.timeit(f'{testName}(*a)', globals=globals(), number=loop) + # print(f"test{iii}) fps {loop / result :.3f} - s {result / loop:.10f} - {result / loop} {globals()[testName](*a)}") + print(f"test{iii:02}) | {result / loop:.10f} sec. | {loop / result : 15.3f} Fps ╞╡-> {globals()[testName](*a)}") + iii+=1 +