diff --git a/docs/HLD.md b/docs/HLD.md index b5b8e9af..357e4ca8 100644 --- a/docs/HLD.md +++ b/docs/HLD.md @@ -1 +1,2 @@ +# HLD ![diagram](hld.svg) diff --git a/docs/TODO.md b/docs/TODO.md index e38af2bb..191e65b8 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,10 +1,15 @@ - [ ] Handle Logs - [ ] Terminal Helper + - [ ] Events + - [x] Window : SIGWINCH triggered when the terminal is resized - [ ] Input Class + - [ ] Return Error if Mouse RE does not match + - [x] Handle the Paste Buffer + - [ ] Handle the middle button mouse paste + - [ ] Handle Special Keys (UP, Down, . . .) - [ ] Events https://tkinterexamples.com/events/events.html https://www.pythontutorial.net/tkinter/tkinter-event-binding/ - - [ ] Keyboard - - [ ] Mouse - - [ ] Window + - [x] Keyboard + - [x] Mouse - [ ] Canvas Class \ No newline at end of file diff --git a/docs/hld.svg b/docs/hld.svg index 0a281c57..6fba041b 100644 --- a/docs/hld.svg +++ b/docs/hld.svg @@ -1,3 +1,3 @@ -
Canvas - Threaded
Canvas - Threaded
Input - Threaded
Input - Threaded
queues
queues
Mouse Events
Mouse Events
Keyboard Events
Keyboard Events
 push 
 push 
MAIN
MAIN
Timer
Timer
Update
Update
queues
queues
Canvas Changes
Canvas Changes
Viewer does not support full SVG 1.1
\ No newline at end of file +
MAIN
MAIN
Canvas - Threaded
Canvas - Threaded
Input - Threaded
Input - Threaded
queues
queues
Mouse Events
Mouse Events
Keyboard Events
Keyboard Events
Timer
Timer
Update
Update
queues
queues
Canvas Changes
Canvas Changes
 push 
 push 
Term
Term
Input Callback
Input Callback
Win Resize Callback
Win Resize Callback
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/tests/test.draw.py b/tests/test.draw.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test.input.py b/tests/test.input.py new file mode 100644 index 00000000..7540b858 --- /dev/null +++ b/tests/test.input.py @@ -0,0 +1,39 @@ +import sys, os +import logging + +sys.path.append(os.path.join(sys.path[0],'..')) +import ttk.libbpytop as lbt +import ttk + +def message_handler(mode, context, message): + if mode == ttk.InfoMsg: mode = 'INFO' + elif mode == ttk.WarningMsg: mode = 'WARNING' + elif mode == ttk.CriticalMsg: mode = 'CRITICAL' + elif mode == ttk.FatalMsg: mode = 'FATAL' + else: mode = 'DEBUG' + logging.debug(f"{mode} {context.file} {message}") + +logging.basicConfig(level=logging.DEBUG, + format='(%(threadName)-9s) %(message)s',) +ttk.installMessageHandler(message_handler) + +ttk.info("Retrieve Keyboard, Mouse press/drag/wheel Events") +ttk.info("Press q or to exit") + +lbt.Term.push(lbt.Term.mouse_on) +lbt.Term.echo(False) + +def keyCallback(kevt=None, mevt=None): + if kevt is not None: + ttk.info(f"Key Event: {kevt}") + if mevt is not None: + ttk.info(f"Mouse Event: {mevt}") + +def winCallback(width, height): + ttk.info(f"Resize: w:{width}, h:{height}") + +lbt.Term.registerResizeCb(winCallback) +lbt.Input.get_key(keyCallback) + +lbt.Term.push(lbt.Term.mouse_off, lbt.Term.mouse_direct_off) +lbt.Term.echo(True) \ No newline at end of file diff --git a/tests/utf-8.txt b/tests/utf-8.txt new file mode 100755 index 00000000..257bfdcd --- /dev/null +++ b/tests/utf-8.txt @@ -0,0 +1,4 @@ +This is a testing file with some UTF-8 chars: +x x x x x x x x x x x x +£ @ £ ¬ ` 漢 _ _ あ _ _ +x x x x x xx x x xx x x \ No newline at end of file diff --git a/ttk/__init__.py b/ttk/__init__.py new file mode 100644 index 00000000..98fed2c6 --- /dev/null +++ b/ttk/__init__.py @@ -0,0 +1 @@ +from .log import * \ No newline at end of file diff --git a/ttk/libbpytop/__init__.py b/ttk/libbpytop/__init__.py new file mode 100644 index 00000000..0e3ca9fc --- /dev/null +++ b/ttk/libbpytop/__init__.py @@ -0,0 +1,2 @@ +from .input import * +from .term import * \ No newline at end of file diff --git a/ttk/libbpytop/input.py b/ttk/libbpytop/input.py new file mode 100644 index 00000000..3538692a --- /dev/null +++ b/ttk/libbpytop/input.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 + +# Copyright 2021 Eugenio Parodi +# Copyright 2020 Aristocratos (https://github.com/aristocratos/bpytop) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os, sys, io, threading, signal, re, subprocess, logging, logging.handlers, argparse +import re +import queue +from select import select +from time import time, sleep, strftime, localtime +from typing import TextIO, List, Set, Dict, Tuple, Optional, Union, Any, Callable, ContextManager, Iterable, Type, NamedTuple + +try: import fcntl, termios, tty, pwd +except Exception as e: + print(f'ERROR: {e}') + exit(1) + +class MouseEvent: + # Keys + NoButton = 0x00000000 # The button state does not refer to any button (see QMouseEvent::button()). + AllButtons = 0x07ffffff # This value corresponds to a mask of all possible mouse buttons. Use to set the 'acceptedButtons' property of a MouseArea to accept ALL mouse buttons. + LeftButton = 0x00000001 # The left button is pressed, or an event refers to the left button. (The left button may be the right button on left-handed mice.) + RightButton = 0x00000002 # The right button. + MidButton = 0x00000004 # The middle button. + MiddleButton = MidButton # The middle button. + Wheel = 0x00000008 + + # Events + NoEvent = 0x00000000 + Press = 0x00010000 + Release = 0x00020000 + Drag = 0x00040000 + Move = 0x00080000 + Up = 0x00100000 # Wheel Up + Down = 0x00200000 # Wheel Down + + __slots__ = ('x','y','key','evt','raw') + def __init__(self, x: int, y: int, key: int, evt: int, raw: str): + self.x = x + self.y = y + self.key = key + self.evt = evt + self.raw = raw + + def key2str(self): + return { + MouseEvent.NoButton : "NoButton", + MouseEvent.AllButtons : "AllButtons", + MouseEvent.LeftButton : "LeftButton", + MouseEvent.RightButton : "RightButton", + MouseEvent.MidButton : "MidButton", + MouseEvent.MiddleButton : "MiddleButton", + MouseEvent.Wheel : "Wheel", + }.get(self.key, "Undefined") + + def evt2str(self): + return { + MouseEvent.NoEvent : "NoEvent", + MouseEvent.Press : "Press", + MouseEvent.Release : "Release", + MouseEvent.Drag : "Drag", + MouseEvent.Move : "Move", + MouseEvent.Up : "Up", + MouseEvent.Down : "Down", + }.get(self.evt, "Undefined") + + def __str__(self): + return f"MouseEvent ({self.x},{self.y}) {self.key2str()} {self.evt2str()} - {self.raw}" + +class KeyEvent: + __slots__ = ('id', 'key') + def __init__(self, id:int, key: str): + self.id = id + self.key = key + def __str__(self): + return f"KeyEvent: {self.key}" + +class Input: + class _nonblocking(object): + """Set nonblocking mode for device""" + def __init__(self, stream: TextIO): + self.stream = stream + self.fd = self.stream.fileno() + def __enter__(self): + self.orig_fl = fcntl.fcntl(self.fd, fcntl.F_GETFL) + fcntl.fcntl(self.fd, fcntl.F_SETFL, self.orig_fl | os.O_NONBLOCK) + def __exit__(self, *args): + fcntl.fcntl(self.fd, fcntl.F_SETFL, self.orig_fl) + + class _raw(object): + """Set raw input mode for device""" + def __init__(self, stream: TextIO): + self.stream = stream + self.fd = self.stream.fileno() + def __enter__(self): + self.original_stty = termios.tcgetattr(self.stream) + tty.setcbreak(self.stream) + def __exit__(self, type, value, traceback): + termios.tcsetattr(self.stream, termios.TCSANOW, self.original_stty) + + @staticmethod + def get_key(callback=None): + input_key: str = "" + mouse_re = re.compile(r"\033\[<(\d+);(\d+);(\d+)([mM])") + + while not False: + with Input._raw(sys.stdin): + #if not select([sys.stdin], [], [], 0.1)[0]: #* Wait 100ms for input on stdin then restart loop to check for stop flag + # continue + input_key += sys.stdin.read(1) #* Read 1 key safely with blocking on + if input_key == "\033": #* If first character is a escape sequence keep reading + with Input._nonblocking(sys.stdin): #* Set non blocking to prevent read stall + input_key += sys.stdin.read(20) + if input_key.startswith("\033[<"): + _ = sys.stdin.read(1000) + # ttk.debug("INPUT: "+input_key.replace("\033","")) + mevt = None + kevt = None + if input_key == "\033" or input_key == "q": #* Key is "escape" key if only containing \033 + break + elif len(input_key) == 1: + # Key Pressed + kevt = KeyEvent(0, input_key) + elif input_key.startswith("\033[<"): + # Mouse Event + m = mouse_re.match(input_key) + if not m: + # TODO: Return Error + continue + code = int(m.group(1)) + x = int(m.group(2)) + y = int(m.group(3)) + state = m.group(4) + key = MouseEvent.NoButton + evt = MouseEvent.NoEvent + if code == 0x00: + key = MouseEvent.LeftButton + evt = MouseEvent.Press if state=="M" else MouseEvent.Release + elif code == 0x01: + key = MouseEvent.MidButton + evt = MouseEvent.Press if state=="M" else MouseEvent.Release + elif code == 0x02: + key = MouseEvent.RightButton + evt = MouseEvent.Press if state=="M" else MouseEvent.Release + elif code == 0x20: + key = MouseEvent.LeftButton + evt = MouseEvent.Drag + elif code == 0x21: + key = MouseEvent.MidButton + evt = MouseEvent.Drag + elif code == 0x22: + key = MouseEvent.RightButton + evt = MouseEvent.Drag + elif code == 0x40: + key = MouseEvent.Wheel + evt = MouseEvent.Up + elif code == 0x41: + key = MouseEvent.Wheel + evt = MouseEvent.Down + mevt = MouseEvent(x, y, key, evt, m.group(0).replace("\033", "")) + + input_key = "" + if callback is not None: + callback(kevt, mevt) + +def main(): + print("Retrieve Keyboard, Mouse press/drag/wheel Events") + print("Press q or to exit") + import term as t + + t.Term.push(t.Term.mouse_on) + t.Term.echo(False) + + def callback(kevt=None, mevt=None): + if kevt is not None: + print(f"Key Event: {kevt}") + if mevt is not None: + print(f"Mouse Event: {mevt}") + + Input.get_key(callback) + + t.Term.push(t.Term.mouse_off, t.Term.mouse_direct_off) + t.Term.echo(True) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ttk/libbpytop/term.py b/ttk/libbpytop/term.py new file mode 100644 index 00000000..5ad60db3 --- /dev/null +++ b/ttk/libbpytop/term.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 + +# Copyright 2021 Eugenio Parodi +# Copyright 2020 Aristocratos (https://github.com/aristocratos/bpytop) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os, sys, io, threading, signal, re, subprocess, logging, logging.handlers, argparse +import queue +from select import select +from time import time, sleep, strftime, localtime +from typing import List, Set, Dict, Tuple, Optional, Union, Any, Callable, ContextManager, Iterable, Type, NamedTuple + +try: import fcntl, termios, tty, pwd +except Exception as e: + print(f'ERROR: {e}') + exit(1) + +class Term: + """Terminal info and commands""" + width: int = 0 + height: int = 0 + fg: str = "" #* Default foreground color + bg: str = "" #* Default background color + hide_cursor = "\033[?25l" #* Hide terminal cursor + show_cursor = "\033[?25h" #* Show terminal cursor + alt_screen = "\033[?1049h" #* Switch to alternate screen + normal_screen = "\033[?1049l" #* Switch to normal screen + clear = "\033[2J\033[0;0f" #* Clear screen and set cursor to position 0,0 + mouse_on = "\033[?1002h\033[?1015h\033[?1006h" #* Enable reporting of mouse position on click and release + mouse_off = "\033[?1002l" #* Disable mouse reporting + mouse_direct_on = "\033[?1003h" #* Enable reporting of mouse position at any movement + mouse_direct_off = "\033[?1003l" #* Disable direct mouse reporting + + _sigWinChCb = None + + @staticmethod + def echo(on: bool): + """Toggle input echo""" + (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(sys.stdin.fileno()) + if on: + lflag |= termios.ECHO # type: ignore + else: + lflag &= ~termios.ECHO # type: ignore + new_attr = [iflag, oflag, cflag, lflag, ispeed, ospeed, cc] + termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, new_attr) + + @staticmethod + def push(*args): + try: + print(*args, sep="", end="", flush=True) + except BlockingIOError: + pass + print(*args, sep="", end="", flush=True) + + @staticmethod + def title(text: str = "") -> str: + out: str = f'{os.environ.get("TERMINAL_TITLE", "")}' + if out and text: out += " " + if text: out += f'{text}' + return f'\033]0;{out}\a' + + @staticmethod + def _sigWinCh(signum, frame): + Term.width, Term.height = os.get_terminal_size() + if Term._sigWinChCb is not None: + Term._sigWinChCb(Term.width, Term.height) + + @staticmethod + def registerResizeCb(callback): + Term._sigWinChCb = callback + signal.signal(signal.SIGWINCH, Term._sigWinCh) + diff --git a/ttk/log.py b/ttk/log.py new file mode 100644 index 00000000..49dc260d --- /dev/null +++ b/ttk/log.py @@ -0,0 +1,72 @@ +#!/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. + +# This code is inspired by +# https://github.com/ceccopierangiolieugenio/pyCuT/blob/master/cupy/CuTCore/CuDebug.py + +import inspect + +DebugMsg = 0 # A message generated by the Debug() function. +InfoMsg = 4 # A message generated by the Info() function. +WarningMsg = 1 # A message generated by the Warning() function. +CriticalMsg = 2 # A message generated by the Critical() function. +FatalMsg = 3 # A message generated by the Fatal() function. +SystemMsg = CriticalMsg + +_MessageHandler = None + +def _process_msg(mode, msg): + global _MessageHandler + if _MessageHandler is not None: + curframe = inspect.currentframe() + calframe = inspect.getouterframes(curframe,1) + if len(calframe) > 2: + class context: + __slots__ = ('file', 'line', 'function') + def __str__(self): + return f"{self.file}:{self.line} [{self.function}]" + ctx = context() + ctx.file = calframe[2][1] + ctx.line = calframe[2][2] + ctx.function = calframe[2][3] + _MessageHandler(mode, ctx, msg) + +def debug(msg): + _process_msg(DebugMsg, msg) + +def info(msg): + _process_msg(InfoMsg, msg) + +def warn(msg): + _process_msg(WarningMsg, msg) + +def critical(msg): + _process_msg(CriticalMsg, msg) + +def fatal(msg): + _process_msg(FatalMsg, msg) + +def installMessageHandler(mh): + global _MessageHandler + _MessageHandler = mh \ No newline at end of file