diff --git a/TermTk/TTkCore/TTkTerm/term.py b/TermTk/TTkCore/TTkTerm/term.py index dc4fa866..8f141038 100644 --- a/TermTk/TTkCore/TTkTerm/term.py +++ b/TermTk/TTkCore/TTkTerm/term.py @@ -22,14 +22,4 @@ __all__ = ['TTkTerm'] -import importlib.util -import platform - -if importlib.util.find_spec('pyodideProxy'): - from .term_pyodide import TTkTerm -elif platform.system() == 'Linux': - from .term_unix import TTkTerm -elif platform.system() == 'Darwin': - from .term_unix import TTkTerm -elif platform.system() == 'Windows': - from .term_windows import TTkTerm \ No newline at end of file +from ..drivers import * \ No newline at end of file diff --git a/TermTk/TTkCore/TTkTerm/term_base.py b/TermTk/TTkCore/TTkTerm/term_base.py index ff944a14..a65b5c69 100644 --- a/TermTk/TTkCore/TTkTerm/term_base.py +++ b/TermTk/TTkCore/TTkTerm/term_base.py @@ -91,7 +91,7 @@ class TTkTermBase(): _sigWinChCb = None @staticmethod - def init(title: str = "TermTk", sigmask=0): + def init(title: str = "TermTk", sigmask=0) -> None: TTkTermBase.title = title TTkTermBase.Cursor.hide() TTkTermBase.push(TTkTermBase.escTitle(TTkTermBase.title)) @@ -116,21 +116,21 @@ class TTkTermBase(): TTkTermBase.push(TTkTermBase.Mouse.DIRECT_OFF) @staticmethod - def exit(): + def exit() -> None: TTkTermBase.push(TTkTermBase.Mouse.OFF + TTkTermBase.Mouse.DIRECT_OFF) TTkTermBase.push(TTkTermBase.CLEAR + TTkTermBase.NORMAL_SCREEN + TTkTermBase.RESET_BRACKETED_PM + TTkTermBase.Cursor.SHOW + TTkTermBase.escTitle()) TTkTermBase.setEcho(True) TTkTermBase.CRNL(True) @staticmethod - def stop(): + def stop() -> None: TTkTermBase.push(TTkTermBase.Mouse.OFF + TTkTermBase.Mouse.DIRECT_OFF) TTkTermBase.push(TTkTermBase.CLEAR + TTkTermBase.NORMAL_SCREEN + TTkTermBase.RESET_BRACKETED_PM + TTkTermBase.Cursor.SHOW + TTkTermBase.escTitle()) TTkTermBase.setEcho(True) TTkTermBase.CRNL(True) @staticmethod - def cont(): + def cont() -> None: TTkTermBase.push(TTkTermBase.ALT_SCREEN + TTkTermBase.SET_BRACKETED_PM + TTkTermBase.CLEAR + TTkTermBase.Cursor.HIDE + TTkTermBase.escTitle(TTkTermBase.title)) TTkTermBase.setMouse(TTkTermBase.mouse, TTkTermBase.directMouse) TTkTermBase.setEcho(False) diff --git a/TermTk/TTkCore/drivers/__init__.py b/TermTk/TTkCore/drivers/__init__.py index e69de29b..c2a77f6f 100644 --- a/TermTk/TTkCore/drivers/__init__.py +++ b/TermTk/TTkCore/drivers/__init__.py @@ -0,0 +1,15 @@ +import importlib.util +import platform + +if importlib.util.find_spec('pyodideProxy'): + from .pyodide import * + from .term_pyodide import * +elif platform.system() == 'Linux': + from .unix import * + from .term_unix import * +elif platform.system() == 'Darwin': + from .unix import * + from .term_unix import * +elif platform.system() == 'Windows': + from .windows import * + from .term_windows import * diff --git a/TermTk/TTkCore/drivers/pyodide.py b/TermTk/TTkCore/drivers/pyodide.py index e69de29b..d81af6d7 100644 --- a/TermTk/TTkCore/drivers/pyodide.py +++ b/TermTk/TTkCore/drivers/pyodide.py @@ -0,0 +1,40 @@ +# 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. + +__all__ = ['TTkSignalDriver','TTkInputDriver'] + +from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot + +class TTkInputDriver(): + def close(self): pass + def cont(self): pass + def read(self): pass + + +class TTkSignalDriver(): + sigStop = pyTTkSignal() + sigCont = pyTTkSignal() + sigInt = pyTTkSignal() + + @staticmethod + def init(): pass + def exit(): pass \ No newline at end of file diff --git a/TermTk/TTkCore/TTkTerm/term_pyodide.py b/TermTk/TTkCore/drivers/term_pyodide.py similarity index 96% rename from TermTk/TTkCore/TTkTerm/term_pyodide.py rename to TermTk/TTkCore/drivers/term_pyodide.py index f3799976..17075987 100644 --- a/TermTk/TTkCore/TTkTerm/term_pyodide.py +++ b/TermTk/TTkCore/drivers/term_pyodide.py @@ -20,9 +20,11 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +__all__ = ['TTkTerm'] + import pyodideProxy -from .term_base import TTkTermBase +from ..TTkTerm.term_base import TTkTermBase class TTkTerm(TTkTermBase): @staticmethod diff --git a/TermTk/TTkCore/TTkTerm/term_unix.py b/TermTk/TTkCore/drivers/term_unix.py similarity index 98% rename from TermTk/TTkCore/TTkTerm/term_unix.py rename to TermTk/TTkCore/drivers/term_unix.py index e8b4cf1c..725cc71d 100644 --- a/TermTk/TTkCore/TTkTerm/term_unix.py +++ b/TermTk/TTkCore/drivers/term_unix.py @@ -20,6 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +__all__ = ['TTkTerm'] + import sys, os, signal from threading import Thread, Lock @@ -28,7 +30,7 @@ except Exception as e: print(f'ERROR: {e}') exit(1) -from .term_base import TTkTermBase +from ..TTkTerm.term_base import TTkTermBase from TermTk.TTkCore.log import TTkLog class TTkTerm(TTkTermBase): diff --git a/TermTk/TTkCore/TTkTerm/term_windows.py b/TermTk/TTkCore/drivers/term_windows.py similarity index 65% rename from TermTk/TTkCore/TTkTerm/term_windows.py rename to TermTk/TTkCore/drivers/term_windows.py index 74b4801b..d42da0b2 100644 --- a/TermTk/TTkCore/TTkTerm/term_windows.py +++ b/TermTk/TTkCore/drivers/term_windows.py @@ -20,13 +20,22 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +__all__ = ['TTkTerm'] + import sys, os, signal from threading import Thread, Lock -from .term_base import TTkTermBase +from ..TTkTerm.term_base import TTkTermBase from TermTk.TTkCore.log import TTkLog +from .windows import * class TTkTerm(TTkTermBase): + # force directMouse onn Windows + # otherwise the mouse events are not received + @staticmethod + def setMouse(mouse:bool=False, directMouse:bool=False) -> None: + TTkTermBase.setMouse(mouse|directMouse, mouse|directMouse) + @staticmethod def _push(*args): try: @@ -51,23 +60,21 @@ class TTkTerm(TTkTermBase): print(f'ERROR: {e}') TTkTermBase.getTerminalSize = _getTerminalSize - @staticmethod - def _sigWinChThreaded(): - if not TTkTerm._sigWinChMutex.acquire(blocking=False): return - while (TTkTerm.width, TTkTerm.height) != (wh:=TTkTerm.getTerminalSize()): - TTkTerm.width, TTkTerm.height = wh - if TTkTerm._sigWinChCb is not None: - TTkTerm._sigWinChCb(TTkTerm.width, TTkTerm.height) - TTkTerm._sigWinChMutex.release() + _sigWinChMutex = Lock() @staticmethod - def _sigWinCh(signum, frame): - Thread(target=TTkTerm._sigWinChThreaded).start() + def _sigWinCh(w,h): + def _sigWinChThreaded(): + if not TTkTerm._sigWinChMutex.acquire(blocking=False): return + while (TTkTerm.width, TTkTerm.height) != (wh:=TTkTerm.getTerminalSize()): + TTkTerm.width, TTkTerm.height = wh + if TTkTerm._sigWinChCb is not None: + TTkTerm._sigWinChCb(TTkTerm.width, TTkTerm.height) + TTkTerm._sigWinChMutex.release() + Thread(target=_sigWinChThreaded).start() - # @staticmethod - # def _registerResizeCb(callback): - # TTkTerm._sigWinChCb = callback - # # Dummy call to retrieve the terminal size - # TTkTerm._sigWinCh(signal.SIGWINCH, None) - # signal.signal(signal.SIGWINCH, TTkTerm._sigWinCh) - # TTkTermBase.registerResizeCb = _registerResizeCb \ No newline at end of file + @staticmethod + def _registerResizeCb(callback): + TTkTerm._sigWinChCb = callback + TTkInputDriver.windowResized.connect(TTkTerm._sigWinCh) + TTkTermBase.registerResizeCb = _registerResizeCb \ No newline at end of file diff --git a/TermTk/TTkCore/drivers/unix.py b/TermTk/TTkCore/drivers/unix.py index 4f2f81ca..66b3ab64 100644 --- a/TermTk/TTkCore/drivers/unix.py +++ b/TermTk/TTkCore/drivers/unix.py @@ -20,9 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -__all__ = [''] +__all__ = ['TTkSignalDriver','TTkInputDriver'] import sys, os, re +import signal from select import select try: import fcntl, termios, tty @@ -30,6 +31,8 @@ except Exception as e: print(f'ERROR: {e}') exit(1) +from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot + class TTkInputDriver(): __slots__ = ('_readPipe','_attr') @@ -66,3 +69,24 @@ class TTkInputDriver(): else: for ch in sr: yield ch + + + +class TTkSignalDriver(): + sigStop = pyTTkSignal() + sigCont = pyTTkSignal() + sigInt = pyTTkSignal() + + @staticmethod + def init(): + # Register events + signal.signal(signal.SIGTSTP, TTkSignalDriver._SIGSTOP) # Ctrl-Z + signal.signal(signal.SIGCONT, TTkSignalDriver._SIGCONT) # Resume + signal.signal(signal.SIGINT, TTkSignalDriver._SIGINT) # Ctrl-C + + def exit(): + signal.signal(signal.SIGINT, signal.SIG_DFL) + + def _SIGSTOP(signum, frame): TTkSignalDriver.sigStop.emit() + def _SIGCONT(signum, frame): TTkSignalDriver.sigCont.emit() + def _SIGINT( signum, frame): TTkSignalDriver.sigInt.emit() diff --git a/TermTk/TTkCore/drivers/windows.py b/TermTk/TTkCore/drivers/windows.py index d3ef3de0..51858363 100644 --- a/TermTk/TTkCore/drivers/windows.py +++ b/TermTk/TTkCore/drivers/windows.py @@ -20,15 +20,15 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -__all__ = [''] +__all__ = ['TTkSignalDriver','TTkInputDriver'] -import sys +import signal from ctypes import Structure, Union, byref, wintypes, windll -from TermTk.TTkCore.constant import TTkK +from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot + from TermTk.TTkCore.log import TTkLog -from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent # Based on the example ported from: # https://learn.microsoft.com/en-us/windows/console/reading-input-buffer-events @@ -199,6 +199,7 @@ class INPUT_RECORD(Structure): class TTkInputDriver(): + windowResized = pyTTkSignal(int,int) def __init__(self): self._run = True self._initTerminal() @@ -337,8 +338,28 @@ class TTkInputDriver(): # everything is received as ANSI sequence pass elif bb.EventType == WINDOW_BUFFER_SIZE_EVENT: - TTkLog.debug(f"{bb.Event.WindowBufferSizeEvent=}") - TTkLog.debug(f"{bb.Event.WindowBufferSizeEvent.dwSize.X=}") - TTkLog.debug(f"{bb.Event.WindowBufferSizeEvent.dwSize.Y=}") + # TTkLog.debug(f"{bb.Event.WindowBufferSizeEvent=}") + # TTkLog.debug(f"{bb.Event.WindowBufferSizeEvent.dwSize.X=}") + # TTkLog.debug(f"{bb.Event.WindowBufferSizeEvent.dwSize.Y=}") + TTkInputDriver.windowResized.emit(bb.Event.WindowBufferSizeEvent.dwSize.X, bb.Event.WindowBufferSizeEvent.dwSize.Y) yield saveKey + +class TTkSignalDriver(): + sigStop = pyTTkSignal() + sigCont = pyTTkSignal() + sigInt = pyTTkSignal() + + @staticmethod + def init(): + # Register events + # signal.signal(signal.SIGTSTP, TTkSignalDriver._SIGSTOP) # Ctrl-Z + # signal.signal(signal.SIGCONT, TTkSignalDriver._SIGCONT) # Resume + signal.signal(signal.SIGINT, TTkSignalDriver._SIGINT) # Ctrl-C + + def exit(): + signal.signal(signal.SIGINT, signal.SIG_DFL) + + def _SIGSTOP(signum, frame): TTkSignalDriver.sigStop.emit() + def _SIGCONT(signum, frame): TTkSignalDriver.sigCont.emit() + def _SIGINT( signum, frame): TTkSignalDriver.sigInt.emit() \ No newline at end of file diff --git a/TermTk/TTkCore/ttk.py b/TermTk/TTkCore/ttk.py index 3c584f17..2e095268 100644 --- a/TermTk/TTkCore/ttk.py +++ b/TermTk/TTkCore/ttk.py @@ -29,6 +29,7 @@ import queue import threading import platform +from TermTk.TTkCore.drivers import * from TermTk.TTkCore.TTkTerm.input import TTkInput from TermTk.TTkCore.TTkTerm.inputkey import TTkKeyEvent from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent @@ -100,6 +101,9 @@ class TTk(TTkContainer): self._termDirectMouse = kwargs.get('mouseTrack',False) TTkInput.inputEvent.connect(self._processInput) TTkInput.pasteEvent.connect(self._processPaste) + TTkSignalDriver.sigStop.connect(self._SIGSTOP) + TTkSignalDriver.sigCont.connect(self._SIGCONT) + TTkSignalDriver.sigInt.connect( self._SIGINT) self._title = kwargs.get('title','TermTk') self._sigmask = kwargs.get('sigmask', TTkK.NONE) self._showMouseCursor = os.environ.get("TTK_MOUSE",kwargs.get('mouseCursor', False)) @@ -146,9 +150,7 @@ class TTk(TTkContainer): TTkLog.debug(f"screen = ({TTkTerm.getTerminalSize()})") # Register events - signal.signal(signal.SIGTSTP, self._SIGSTOP) # Ctrl-Z - signal.signal(signal.SIGCONT, self._SIGCONT) # Resume - signal.signal(signal.SIGINT, self._SIGINT) # Ctrl-C + TTkSignalDriver.init() TTkLog.debug("Signal Event Registered") @@ -176,7 +178,7 @@ class TTk(TTkContainer): self._mainLoop() finally: if platform.system() != 'Emscripten': - signal.signal(signal.SIGINT, signal.SIG_DFL) + TTkSignalDriver.exit() self.quit() TTkTerm.exit() @@ -329,7 +331,8 @@ class TTk(TTkContainer): self._paintEvent.set() TTkInput.close() - def _SIGSTOP(self, signum, frame): + @pyTTkSlot() + def _SIGSTOP(self): """Reset terminal settings and stop background input read before putting to sleep""" TTkLog.debug("Captured SIGSTOP ") TTkTerm.stop() @@ -337,7 +340,8 @@ class TTk(TTkContainer): # TODO: stop the threads os.kill(os.getpid(), signal.SIGSTOP) - def _SIGCONT(self, signum, frame): + @pyTTkSlot() + def _SIGCONT(self): """Set terminal settings and restart background input read""" TTkLog.debug("Captured SIGCONT 'fg/bg'") TTkTerm.cont() @@ -346,7 +350,8 @@ class TTk(TTkContainer): # TODO: Restart threads # TODO: Redraw the screen - def _SIGINT(self, signum, fraTERMTK_STACKTRACEme): + @pyTTkSlot() + def _SIGINT(self): # If the "TERMTK_STACKTRACE" env variable is defined # a stacktrace file is generated once CTRL+C is pressed # i.e.