diff --git a/TermTk/TTkCore/TTkTerm/input.py b/TermTk/TTkCore/TTkTerm/input.py index 757e63cb..b44c1bf8 100644 --- a/TermTk/TTkCore/TTkTerm/input.py +++ b/TermTk/TTkCore/TTkTerm/input.py @@ -35,18 +35,26 @@ elif platform.system() == 'Darwin': elif platform.system() == 'Windows': raise NotImplementedError('Windows OS not yet supported') elif platform.system() == 'Emscripten': - raise NotImplementedError('Pyodide not yet supported') + pass from TermTk.TTkCore.log import TTkLog from TermTk.TTkCore.constant import TTkK +from TermTk.TTkCore.signal import pyTTkSignal from TermTk.TTkCore.TTkTerm.inputkey import TTkKeyEvent from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent class TTkInput: - __slots__ = ('_readInput', '_leftLastTime', '_midLastTime', '_rightLastTime', '_leftTap', '_midTap', '_rightTap') + __slots__ = ( + '_readInput', + '_leftLastTime', '_midLastTime', '_rightLastTime', + '_leftTap', '_midTap', '_rightTap', + # Signals + 'inputEvent' + ) def __init__(self): - self._readInput = ReadInput() + self.inputEvent = pyTTkSignal(TTkKeyEvent, TTkMouseEvent) + self._readInput = None self._leftLastTime = 0 self._midLastTime = 0 self._rightLastTime = 0 @@ -55,94 +63,100 @@ class TTkInput: self._rightTap = 0 def close(self): - self._readInput.close() + if self._readInput: + self._readInput.close() def stop(self): pass def cont(self): - self._readInput.cont() + if self._readInput: + self._readInput.cont() - def get_key(self, callback=None): - mouse_re = re.compile(r"\033\[<(\d+);(\d+);(\d+)([mM])") + def start(self): + self._readInput = ReadInput() while stdinRead := self._readInput.read(): - mevt,kevt = None, None - if not stdinRead.startswith("\033[<"): - # Key Event - kevt = TTkKeyEvent.parse(stdinRead) - else: - # Mouse Event - m = mouse_re.match(stdinRead) - if not m: - # TODO: Return Error - hex = [f"0x{ord(x):02x}" for x in stdinRead] - TTkLog.error("UNHANDLED (mouse): "+stdinRead.replace("\033","") + " - "+",".join(hex)) - continue - code = int(m.group(1)) - x = int(m.group(2))-1 - y = int(m.group(3))-1 - state = m.group(4) - key = TTkMouseEvent.NoButton - evt = TTkMouseEvent.NoEvent - tap = 0 - - def _checkTap(lastTime, tap): - if state=="M": - t = time() - if (t-lastTime) < 0.4: - return t, tap+1 - else: - return t, 1 - return lastTime, tap - - mod = TTkK.NoModifier - if code & 0x10: - code &= ~0x10 - mod |= TTkK.ControlModifier - if code & 0x08: - code &= ~0x08 - mod |= TTkK.AltModifier - - if code == 0x00: - self._leftLastTime, self._leftTap = _checkTap(self._leftLastTime, self._leftTap) - tap = self._leftTap - key = TTkMouseEvent.LeftButton - evt = TTkMouseEvent.Press if state=="M" else TTkMouseEvent.Release - elif code == 0x01: - self._midLastTime, self._midTap = _checkTap(self._midLastTime, self._midTap) - tap = self._midTap - key = TTkMouseEvent.MidButton - evt = TTkMouseEvent.Press if state=="M" else TTkMouseEvent.Release - elif code == 0x02: - self._rightLastTime, self._rightTap = _checkTap(self._rightLastTime, self._rightTap) - tap = self._rightTap - key = TTkMouseEvent.RightButton - evt = TTkMouseEvent.Press if state=="M" else TTkMouseEvent.Release - elif code == 0x20: - key = TTkMouseEvent.LeftButton - evt = TTkMouseEvent.Drag - elif code == 0x21: - key = TTkMouseEvent.MidButton - evt = TTkMouseEvent.Drag - elif code == 0x22: - key = TTkMouseEvent.RightButton - evt = TTkMouseEvent.Drag - elif code == 0x40: - key = TTkMouseEvent.Wheel - evt = TTkMouseEvent.Up - elif code == 0x41: - key = TTkMouseEvent.Wheel - evt = TTkMouseEvent.Down - mevt = TTkMouseEvent(x, y, key, evt, mod, tap, m.group(0).replace("\033", "")) - - if kevt is None and mevt is None: + kevt, mevt = self.key_process(stdinRead) + self.inputEvent.emit(kevt, mevt) + TTkLog.debug("Close TTkInput") + + mouse_re = re.compile(r"\033\[<(\d+);(\d+);(\d+)([mM])") + def key_process(self, stdinRead): + mevt,kevt = None, None + if not stdinRead.startswith("\033[<"): + # Key Event + kevt = TTkKeyEvent.parse(stdinRead) + else: + # Mouse Event + m = self.mouse_re.match(stdinRead) + if not m: + # TODO: Return Error hex = [f"0x{ord(x):02x}" for x in stdinRead] - TTkLog.error("UNHANDLED: "+stdinRead.replace("\033","") + " - "+",".join(hex)) + TTkLog.error("UNHANDLED (mouse): "+stdinRead.replace("\033","") + " - "+",".join(hex)) + return None, None + code = int(m.group(1)) + x = int(m.group(2))-1 + y = int(m.group(3))-1 + state = m.group(4) + key = TTkMouseEvent.NoButton + evt = TTkMouseEvent.NoEvent + tap = 0 + + def _checkTap(lastTime, tap): + if state=="M": + t = time() + if (t-lastTime) < 0.4: + return t, tap+1 + else: + return t, 1 + return lastTime, tap + + mod = TTkK.NoModifier + if code & 0x10: + code &= ~0x10 + mod |= TTkK.ControlModifier + if code & 0x08: + code &= ~0x08 + mod |= TTkK.AltModifier + + if code == 0x00: + self._leftLastTime, self._leftTap = _checkTap(self._leftLastTime, self._leftTap) + tap = self._leftTap + key = TTkMouseEvent.LeftButton + evt = TTkMouseEvent.Press if state=="M" else TTkMouseEvent.Release + elif code == 0x01: + self._midLastTime, self._midTap = _checkTap(self._midLastTime, self._midTap) + tap = self._midTap + key = TTkMouseEvent.MidButton + evt = TTkMouseEvent.Press if state=="M" else TTkMouseEvent.Release + elif code == 0x02: + self._rightLastTime, self._rightTap = _checkTap(self._rightLastTime, self._rightTap) + tap = self._rightTap + key = TTkMouseEvent.RightButton + evt = TTkMouseEvent.Press if state=="M" else TTkMouseEvent.Release + elif code == 0x20: + key = TTkMouseEvent.LeftButton + evt = TTkMouseEvent.Drag + elif code == 0x21: + key = TTkMouseEvent.MidButton + evt = TTkMouseEvent.Drag + elif code == 0x22: + key = TTkMouseEvent.RightButton + evt = TTkMouseEvent.Drag + elif code == 0x40: + key = TTkMouseEvent.Wheel + evt = TTkMouseEvent.Up + elif code == 0x41: + key = TTkMouseEvent.Wheel + evt = TTkMouseEvent.Down + mevt = TTkMouseEvent(x, y, key, evt, mod, tap, m.group(0).replace("\033", "")) + + if kevt is None and mevt is None: + hex = [f"0x{ord(x):02x}" for x in stdinRead] + TTkLog.error("UNHANDLED: "+stdinRead.replace("\033","") + " - "+",".join(hex)) + + return kevt, mevt - if callback is not None: - if not callback(kevt, mevt): - break - TTkLog.debug("Close TTkInput") def main(): print("Retrieve Keyboard, Mouse press/drag/wheel Events") diff --git a/TermTk/TTkCore/TTkTerm/term.py b/TermTk/TTkCore/TTkTerm/term.py index 6117bef5..b2ad30b7 100644 --- a/TermTk/TTkCore/TTkTerm/term.py +++ b/TermTk/TTkCore/TTkTerm/term.py @@ -22,198 +22,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import sys, os, signal +import importlib.util -try: import termios -except Exception as e: - print(f'ERROR: {e}') - exit(1) - -class TTkTerm(): - CLEAR = "\033[2J\033[0;0f" # Clear screen and set cursor to position 0,0 - ALT_SCREEN = "\033[?1049h" #* Switch to alternate screen - NORMAL_SCREEN = "\033[?1049l" #* Switch to normal screen - - class Mouse(): - ON = "\033[?1002h\033[?1015h\033[?1006h" # Enable reporting of mouse position on click and release - OFF = "\033[?1002l" # Disable mouse reporting - DIRECT_ON = "\033[?1003h" # Enable reporting of mouse position at any movement - DIRECT_OFF = "\033[?1003l" # Disable direct mouse reporting - - class Cursor(): - # from: - # https://superuser.com/questions/607478/how-do-you-change-the-xterm-cursor-to-an-i-beam-or-vertical-bar - # echo -e -n "\x1b[\x30 q" # changes to blinking block - # echo -e -n "\x1b[\x31 q" # changes to blinking block also - # echo -e -n "\x1b[\x32 q" # changes to steady block - # echo -e -n "\x1b[\x33 q" # changes to blinking underline - # echo -e -n "\x1b[\x34 q" # changes to steady underline - # echo -e -n "\x1b[\x35 q" # changes to blinking bar - # echo -e -n "\x1b[\x36 q" # changes to steady bar - BLINKING_BLOCK = "\033[\x30 q" - BLINKING_BLOCK_ALSO = "\033[\x31 q" - STEADY_BLOCK = "\033[\x32 q" - BLINKING_UNDERLINE = "\033[\x33 q" - STEADY_UNDERLINE = "\033[\x34 q" - BLINKING_BAR = "\033[\x35 q" - STEADY_BAR = "\033[\x36 q" - - HIDE = "\033[?25l" - SHOW = "\033[?25h" - - @staticmethod - def moveTo(y:int,x:int)->str: return f'\033[{y};{x}f' - @staticmethod - def moveRight(n:int)->str: return f'\033[{n}C' - @staticmethod - def moveLeft(n:int)->str: return f'\033[{n}D' - @staticmethod - def modeUp(n:int)->str: return f'\033[{n}A' - @staticmethod - def moveDown(n:int)->str: return f'\033[{n}B' - - @staticmethod - def show(cursorType): - TTkTerm.push(cursorType) - TTkTerm.push(TTkTerm.Cursor.SHOW) - @staticmethod - def hide(): - TTkTerm.push(TTkTerm.Cursor.HIDE) - - class Sigmask(): - CTRL_C = 0x0001 - CTRL_S = 0x0002 - CTRL_Z = 0x0004 - CTRL_Q = 0x0008 - - title: str = "TermTk" - mouse: bool = True - width: int = 0 - height: int = 0 - - _sigWinChCb = None - - # Save treminal attributes during the initialization in order to - # restore later the original states - _termAttr = termios.tcgetattr(sys.stdin) - - _termAttrBk = [] - @staticmethod - def saveTermAttr(): - TTkTerm._termAttrBk.append(termios.tcgetattr(sys.stdin)) - - @staticmethod - def restoreTermAttr(): - if TTkTerm._termAttrBk: - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, TTkTerm._termAttrBk.pop()) - else: - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, TTkTerm._termAttr) - - @staticmethod - def setSigmask(mask, value=True): - attr = termios.tcgetattr(sys.stdin) - if mask & TTkTerm.Sigmask.CTRL_C: - attr[6][termios.VINTR]= b'\x03' if value else 0 - if mask & TTkTerm.Sigmask.CTRL_S: - attr[6][termios.VSTOP]= b'\x13' if value else 0 - if mask & TTkTerm.Sigmask.CTRL_Z: - attr[6][termios.VSUSP]= b'\x1a' if value else 0 - if mask & TTkTerm.Sigmask.CTRL_Q: - attr[6][termios.VSTART]= b'\x11' if value else 0 - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, attr) - - @staticmethod - def getSigmask(): - mask = 0x00 - attr = termios.tcgetattr(sys.stdin) - mask |= TTkTerm.Sigmask.CTRL_C if attr[6][termios.VINTR] else 0 - mask |= TTkTerm.Sigmask.CTRL_S if attr[6][termios.VSTOP] else 0 - mask |= TTkTerm.Sigmask.CTRL_Z if attr[6][termios.VSUSP] else 0 - mask |= TTkTerm.Sigmask.CTRL_Q if attr[6][termios.VSTART] else 0 - return mask - - @staticmethod - def init(mouse: bool = True, title: str = "TermTk", sigmask=0): - TTkTerm.title = title - TTkTerm.mouse = mouse - TTkTerm.push(TTkTerm.ALT_SCREEN + TTkTerm.CLEAR + TTkTerm.Cursor.HIDE + TTkTerm.escTitle(TTkTerm.title)) - if TTkTerm.mouse: - TTkTerm.push(TTkTerm.Mouse.ON) - TTkTerm.setEcho(False) - TTkTerm.CRNL(False) - TTkTerm.setSigmask(sigmask, False) - - @staticmethod - def exit(): - TTkTerm.push(TTkTerm.Mouse.OFF + TTkTerm.Mouse.DIRECT_OFF) - TTkTerm.push(TTkTerm.CLEAR + TTkTerm.NORMAL_SCREEN + TTkTerm.Cursor.SHOW + TTkTerm.escTitle()) - TTkTerm.setEcho(True) - TTkTerm.CRNL(True) - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, TTkTerm._termAttr) - - @staticmethod - def stop(): - TTkTerm.push(TTkTerm.Mouse.OFF + TTkTerm.Mouse.DIRECT_OFF) - TTkTerm.push(TTkTerm.CLEAR + TTkTerm.NORMAL_SCREEN + TTkTerm.Cursor.SHOW + TTkTerm.escTitle()) - TTkTerm.setEcho(True) - TTkTerm.CRNL(True) - - @staticmethod - def cont(): - TTkTerm.push(TTkTerm.ALT_SCREEN + TTkTerm.CLEAR + TTkTerm.Cursor.HIDE + TTkTerm.escTitle(TTkTerm.title)) - if TTkTerm.mouse: - TTkTerm.push(TTkTerm.Mouse.ON) - TTkTerm.setEcho(False) - TTkTerm.CRNL(False) - - @staticmethod - def escTitle(txt = "") -> str: - tt = os.environ.get("TERMINAL_TITLE", "") - if tt and txt: - return f'\033]0;{tt} {txt}\a' - else: - return f'\033]0;{tt}{txt}\a' - - @staticmethod - def push(*args): - sys.stdout.write(str(*args)) - sys.stdout.flush() - - @staticmethod - def flush(): - sys.stdout.flush() - - @staticmethod - def setEcho(val: bool): - # Set/Unset Terminal Input Echo - (i,o,c,l,isp,osp,cc) = termios.tcgetattr(sys.stdin.fileno()) - if val: l |= termios.ECHO - else: l &= ~termios.ECHO - termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, [i,o,c,l,isp,osp,cc]) - - @staticmethod - def CRNL(val: bool): - #Translate carriage return to newline on input (unless IGNCR is set). - # '\n' CTRL-J - # '\r' CTRL-M (Enter) - (i,o,c,l,isp,osp,cc) = termios.tcgetattr(sys.stdin.fileno()) - if val: i |= termios.ICRNL - else: i &= ~termios.ICRNL - termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, [i,o,c,l,isp,osp,cc]) - - @staticmethod - def getTerminalSize(): - return os.get_terminal_size() - - @staticmethod - def _sigWinCh(signum, frame): - TTkTerm.width, TTkTerm.height = TTkTerm.getTerminalSize() - if TTkTerm._sigWinChCb is not None: - TTkTerm._sigWinChCb(TTkTerm.width, TTkTerm.height) - - @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) \ No newline at end of file +if importlib.util.find_spec('pyodideProxy'): + from .term_pyodide import TTkTerm +else: + from .term_unix import TTkTerm \ No newline at end of file diff --git a/TermTk/TTkCore/TTkTerm/term_base.py b/TermTk/TTkCore/TTkTerm/term_base.py new file mode 100644 index 00000000..5c90f6a2 --- /dev/null +++ b/TermTk/TTkCore/TTkTerm/term_base.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2022 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 os + +class TTkTermBase(): + CLEAR = "\033[2J\033[0;0f" # Clear screen and set cursor to position 0,0 + ALT_SCREEN = "\033[?1049h" #* Switch to alternate screen + NORMAL_SCREEN = "\033[?1049l" #* Switch to normal screen + + class Mouse(): + ON = "\033[?1002h\033[?1015h\033[?1006h" # Enable reporting of mouse position on click and release + OFF = "\033[?1002l" # Disable mouse reporting + DIRECT_ON = "\033[?1003h" # Enable reporting of mouse position at any movement + DIRECT_OFF = "\033[?1003l" # Disable direct mouse reporting + + class Cursor(): + # from: + # https://superuser.com/questions/607478/how-do-you-change-the-xterm-cursor-to-an-i-beam-or-vertical-bar + # echo -e -n "\x1b[\x30 q" # changes to blinking block + # echo -e -n "\x1b[\x31 q" # changes to blinking block also + # echo -e -n "\x1b[\x32 q" # changes to steady block + # echo -e -n "\x1b[\x33 q" # changes to blinking underline + # echo -e -n "\x1b[\x34 q" # changes to steady underline + # echo -e -n "\x1b[\x35 q" # changes to blinking bar + # echo -e -n "\x1b[\x36 q" # changes to steady bar + BLINKING_BLOCK = "\033[\x30 q" + BLINKING_BLOCK_ALSO = "\033[\x31 q" + STEADY_BLOCK = "\033[\x32 q" + BLINKING_UNDERLINE = "\033[\x33 q" + STEADY_UNDERLINE = "\033[\x34 q" + BLINKING_BAR = "\033[\x35 q" + STEADY_BAR = "\033[\x36 q" + + HIDE = "\033[?25l" + SHOW = "\033[?25h" + + @staticmethod + def moveTo(y:int,x:int)->str: return f'\033[{y};{x}f' + @staticmethod + def moveRight(n:int)->str: return f'\033[{n}C' + @staticmethod + def moveLeft(n:int)->str: return f'\033[{n}D' + @staticmethod + def modeUp(n:int)->str: return f'\033[{n}A' + @staticmethod + def moveDown(n:int)->str: return f'\033[{n}B' + + @staticmethod + def show(cursorType): + TTkTermBase.push(cursorType) + TTkTermBase.push(TTkTermBase.Cursor.SHOW) + @staticmethod + def hide(): + TTkTermBase.push(TTkTermBase.Cursor.HIDE) + + class Sigmask(): + CTRL_C = 0x0001 + CTRL_S = 0x0002 + CTRL_Z = 0x0004 + CTRL_Q = 0x0008 + + title: str = "TermTk" + mouse: bool = True + width: int = 0 + height: int = 0 + + _sigWinChCb = None + + @staticmethod + def init(mouse: bool = True, title: str = "TermTk", sigmask=0): + TTkTermBase.title = title + TTkTermBase.mouse = mouse + TTkTermBase.push(TTkTermBase.ALT_SCREEN + TTkTermBase.CLEAR + TTkTermBase.Cursor.HIDE + TTkTermBase.escTitle(TTkTermBase.title)) + if TTkTermBase.mouse: + TTkTermBase.push(TTkTermBase.Mouse.ON) + TTkTermBase.setEcho(False) + TTkTermBase.CRNL(False) + TTkTermBase.setSigmask(sigmask, False) + + @staticmethod + def exit(): + TTkTermBase.push(TTkTermBase.Mouse.OFF + TTkTermBase.Mouse.DIRECT_OFF) + TTkTermBase.push(TTkTermBase.CLEAR + TTkTermBase.NORMAL_SCREEN + TTkTermBase.Cursor.SHOW + TTkTermBase.escTitle()) + TTkTermBase.setEcho(True) + TTkTermBase.CRNL(True) + + @staticmethod + def stop(): + TTkTermBase.push(TTkTermBase.Mouse.OFF + TTkTermBase.Mouse.DIRECT_OFF) + TTkTermBase.push(TTkTermBase.CLEAR + TTkTermBase.NORMAL_SCREEN + TTkTermBase.Cursor.SHOW + TTkTermBase.escTitle()) + TTkTermBase.setEcho(True) + TTkTermBase.CRNL(True) + + @staticmethod + def cont(): + TTkTermBase.push(TTkTermBase.ALT_SCREEN + TTkTermBase.CLEAR + TTkTermBase.Cursor.HIDE + TTkTermBase.escTitle(TTkTermBase.title)) + if TTkTermBase.mouse: + TTkTermBase.push(TTkTermBase.Mouse.ON) + TTkTermBase.setEcho(False) + TTkTermBase.CRNL(False) + + @staticmethod + def escTitle(txt = "") -> str: + tt = os.environ.get("TERMINAL_TITLE", "") + if tt and txt: + return f'\033]0;{tt} {txt}\a' + else: + return f'\033]0;{tt}{txt}\a' + + # NOTE: Due to "I have no idea how to do it in a better way", + # those methods are supposed to be overwritten with the + # compatible one in "term_unix.py" or "term_pyodide.py" + setSigmask = lambda *args: None + push = lambda *args: None + flush = lambda *args: None + setEcho = lambda *args: None + CRNL = lambda *args: None + getTerminalSize = lambda *args: None + registerResizeCb = lambda *args: None \ No newline at end of file diff --git a/TermTk/TTkCore/TTkTerm/term_pyodide.py b/TermTk/TTkCore/TTkTerm/term_pyodide.py new file mode 100644 index 00000000..2b4f2573 --- /dev/null +++ b/TermTk/TTkCore/TTkTerm/term_pyodide.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2022 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 pyodideProxy + +from .term_base import TTkTermBase + +class TTkTerm(TTkTermBase): + @staticmethod + def _push(*args): + pyodideProxy.termPush(str(*args)) + TTkTermBase.push = _push + + @staticmethod + def _getTerminalSize(): + return pyodideProxy.termSize() + TTkTermBase.getTerminalSize = _getTerminalSize + + @staticmethod + def _registerResizeCb(callback): + TTkTerm._sigWinChCb = callback + TTkTermBase.registerResizeCb = _registerResizeCb diff --git a/TermTk/TTkCore/TTkTerm/term_unix.py b/TermTk/TTkCore/TTkTerm/term_unix.py new file mode 100644 index 00000000..56bbefa7 --- /dev/null +++ b/TermTk/TTkCore/TTkTerm/term_unix.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2022 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, signal + +try: import termios +except Exception as e: + print(f'ERROR: {e}') + exit(1) + +from .term_base import TTkTermBase + +class TTkTerm(TTkTermBase): + _sigWinChCb = None + + # Save treminal attributes during the initialization in order to + # restore later the original states + _termAttr = termios.tcgetattr(sys.stdin) + + _termAttrBk = [] + @staticmethod + def saveTermAttr(): + TTkTerm._termAttrBk.append(termios.tcgetattr(sys.stdin)) + + @staticmethod + def restoreTermAttr(): + if TTkTerm._termAttrBk: + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, TTkTerm._termAttrBk.pop()) + else: + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, TTkTerm._termAttr) + + @staticmethod + def _setSigmask(mask, value=True): + attr = termios.tcgetattr(sys.stdin) + if mask & TTkTerm.Sigmask.CTRL_C: + attr[6][termios.VINTR]= b'\x03' if value else 0 + if mask & TTkTerm.Sigmask.CTRL_S: + attr[6][termios.VSTOP]= b'\x13' if value else 0 + if mask & TTkTerm.Sigmask.CTRL_Z: + attr[6][termios.VSUSP]= b'\x1a' if value else 0 + if mask & TTkTerm.Sigmask.CTRL_Q: + attr[6][termios.VSTART]= b'\x11' if value else 0 + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, attr) + TTkTermBase.setSigmask = _setSigmask + + @staticmethod + def getSigmask(): + mask = 0x00 + attr = termios.tcgetattr(sys.stdin) + mask |= TTkTerm.Sigmask.CTRL_C if attr[6][termios.VINTR] else 0 + mask |= TTkTerm.Sigmask.CTRL_S if attr[6][termios.VSTOP] else 0 + mask |= TTkTerm.Sigmask.CTRL_Z if attr[6][termios.VSUSP] else 0 + mask |= TTkTerm.Sigmask.CTRL_Q if attr[6][termios.VSTART] else 0 + return mask + + @staticmethod + def exit(): + TTkTermBase.exit() + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, TTkTerm._termAttr) + + @staticmethod + def _push(*args): + sys.stdout.write(str(*args)) + sys.stdout.flush() + TTkTermBase.push = _push + + @staticmethod + def _flush(): + sys.stdout.flush() + TTkTermBase.flush = _flush + + @staticmethod + def _setEcho(val: bool): + # Set/Unset Terminal Input Echo + (i,o,c,l,isp,osp,cc) = termios.tcgetattr(sys.stdin.fileno()) + if val: l |= termios.ECHO + else: l &= ~termios.ECHO + termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, [i,o,c,l,isp,osp,cc]) + TTkTermBase.setEcho = _setEcho + + @staticmethod + def _CRNL(val: bool): + #Translate carriage return to newline on input (unless IGNCR is set). + # '\n' CTRL-J + # '\r' CTRL-M (Enter) + (i,o,c,l,isp,osp,cc) = termios.tcgetattr(sys.stdin.fileno()) + if val: i |= termios.ICRNL + else: i &= ~termios.ICRNL + termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, [i,o,c,l,isp,osp,cc]) + TTkTermBase.CRNL = _CRNL + + @staticmethod + def _getTerminalSize(): + try: + return os.get_terminal_size() + except OSError as e: + print(f'ERROR: {e}') + TTkTermBase.getTerminalSize = _getTerminalSize + + @staticmethod + def _sigWinCh(signum, frame): + TTkTerm.width, TTkTerm.height = TTkTerm.getTerminalSize() + if TTkTerm._sigWinChCb is not None: + TTkTerm._sigWinChCb(TTkTerm.width, TTkTerm.height) + + @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 diff --git a/TermTk/TTkCore/ttk.py b/TermTk/TTkCore/ttk.py index b32f25c6..2e206284 100644 --- a/TermTk/TTkCore/ttk.py +++ b/TermTk/TTkCore/ttk.py @@ -26,6 +26,7 @@ import os import signal import time import queue +import threading from TermTk.TTkCore.TTkTerm.input import TTkInput from TermTk.TTkCore.TTkTerm.inputkey import TTkKeyEvent @@ -45,30 +46,27 @@ class TTk(TTkWidget): '_events', '_key_events', '_mouse_events', '_screen_events', '_title', '_sigmask', - '_lastMultiTap', - #Signals - 'eventKeyPress', 'eventMouse' ) + '_drawMutex', + '_lastMultiTap') def __init__(self, *args, **kwargs): TTkWidget.__init__(self, *args, **kwargs) self._name = kwargs.get('name' , 'TTk' ) - self.eventKeyPress = pyTTkSignal(TTkKeyEvent) - self.eventMouse = pyTTkSignal(TTkMouseEvent) self._running = False - self._input = None + self._input = TTkInput() + self._input.inputEvent.connect(self._processInput) self._events = queue.Queue() self._key_events = queue.Queue() self._mouse_events = queue.Queue() self._screen_events = queue.Queue() self._title = kwargs.get('title','TermTk') self._sigmask = kwargs.get('sigmask', TTkK.NONE) + self._drawMutex = threading.Lock() self.setFocusPolicy(TTkK.ClickFocus) self.hide() - try: - w,h = TTkTerm.getTerminalSize() - self.setGeometry(0,0,w,h) - except OSError as e: - print(f'ERROR: {e}') + w,h = TTkTerm.getTerminalSize() + self.setGeometry(0,0,w,h) + TTkCfg.theme = TTkTheme() TTkHelper.registerRootWidget(self) @@ -107,7 +105,7 @@ class TTk(TTkWidget): TTkLog.debug("Signal Event Registered") TTkTerm.registerResizeCb(self._win_resize_cb) - threading.Thread(target=self._input_thread, daemon=True).start() + self._timer = TTkTimer() self._timer.timeout.connect(self._time_event) self._timer.start(0.1) @@ -117,41 +115,21 @@ class TTk(TTkWidget): # Keep track of the multiTap to avoid the extra key release self._lastMultiTap = False TTkTerm.init(title=self._title, sigmask=self._sigmask) - self._mainloop() - #except Exception as e: - # TTkLog.error(f"{e}") + self._input.start() finally: self.quit() TTkTerm.exit() - def _mainloop(self): - while self._running: - # Main Loop - evt = self._events.get() - if evt is TTkK.MOUSE_EVENT: - self._mouse_event() - elif evt is TTkK.KEY_EVENT: - self._key_event() - elif evt is TTkK.TIME_EVENT: - w,h = TTkTerm.getTerminalSize() - self.setGeometry(0,0,w,h) - TTkHelper.paintAll() - self._timer.start(1/TTkCfg.maxFps) - self._fps() - pass - elif evt is TTkK.SCREEN_EVENT: - self.setGeometry(0,0,TTkGlbl.term_w,TTkGlbl.term_h) - TTkLog.info(f"Resize: w:{TTkGlbl.term_w}, h:{TTkGlbl.term_h}") - elif evt is TTkK.QUIT_EVENT: - TTkLog.debug("Quit.") - break - else: - TTkLog.error(f"Unhandled Event {evt}") - break + @pyTTkSlot(TTkKeyEvent, TTkMouseEvent) + def _processInput(self, kevt, mevt): + self._drawMutex.acquire() + if kevt is not None: + self._key_event(kevt) + if mevt is not None: + self._mouse_event(mevt) + self._drawMutex.release() - def _mouse_event(self): - mevt = self._mouse_events.get() - self.eventMouse.emit(mevt) + def _mouse_event(self, mevt): # Upload the global mouse position # Mainly used by the drag pixmap display TTkHelper.setMousePos((mevt.x,mevt.y)) @@ -199,9 +177,7 @@ class TTk(TTkWidget): if mevt.evt == TTkK.Release: TTkHelper.dndEnd() - def _key_event(self): - kevt = self._key_events.get() - self.eventKeyPress.emit(kevt) + def _key_event(self, kevt): keyHandled = False # TTkLog.debug(f"Key: {kevt}") focusWidget = TTkHelper.getFocus() @@ -223,31 +199,26 @@ class TTk(TTkWidget): TTkHelper.prevFocus(focusWidget if focusWidget else self) def _time_event(self): - self._events.put(TTkK.TIME_EVENT) + w,h = TTkTerm.getTerminalSize() + self.setGeometry(0,0,w,h) + self._drawMutex.acquire() + TTkHelper.paintAll() + self._drawMutex.release() + self._timer.start(1/TTkCfg.maxFps) + self._fps() def _win_resize_cb(self, width, height): TTkGlbl.term_w = int(width) TTkGlbl.term_h = int(height) - self._events.put(TTkK.SCREEN_EVENT) + self.setGeometry(0,0,TTkGlbl.term_w,TTkGlbl.term_h) + TTkLog.info(f"Resize: w:{TTkGlbl.term_w}, h:{TTkGlbl.term_h}") - def _input_thread(self): - def _inputCallback(kevt=None, mevt=None): - if kevt is not None: - self._key_events.put(kevt) - self._events.put(TTkK.KEY_EVENT) - if mevt is not None: - self._mouse_events.put(mevt) - self._events.put(TTkK.MOUSE_EVENT) - return self._running - # Start input key loop - self._input = TTkInput() - self._input.get_key(_inputCallback) - self._input.close() def quit(self): '''Tells the application to exit with a return code.''' self._events.put(TTkK.QUIT_EVENT) TTkTimer.quitAll() + self._input.close() self._running = False def _SIGSTOP(self, signum, frame): diff --git a/TermTk/TTkTestWidgets/keypressview.py b/TermTk/TTkTestWidgets/keypressview.py index e5983968..2b754f8b 100644 --- a/TermTk/TTkTestWidgets/keypressview.py +++ b/TermTk/TTkTestWidgets/keypressview.py @@ -39,8 +39,7 @@ class TTkKeyPressView(TTkWidget): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._name = kwargs.get('name' , 'TTkAbstractScrollView') - TTkHelper._rootWidget.eventKeyPress.connect(self._addKey) - TTkHelper._rootWidget.eventMouse.connect(self._addMouse) + TTkHelper._rootWidget._input.inputEvent.connect(self._processInput) self._keys = [] self._period = 0.1 self._fade = 10 @@ -48,6 +47,11 @@ class TTkKeyPressView(TTkWidget): self._timer.timeout.connect(self._timerEvent) self._timer.start(self._period) + @pyTTkSlot(TTkKeyEvent, TTkMouseEvent) + def _processInput(self, kevt, mevt): + if kevt is not None: self._addKey(kevt) + if mevt is not None: self._addMouse(mevt) + @pyTTkSlot(TTkKeyEvent) def _addKey(self, evt): if evt.type == TTkK.Character: