diff --git a/README.md b/README.md index 2f751ef8..be52465f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,22 @@ # pyTermTk Python Terminal Toolkit + +## Quick Test/Try + +#### Clone +```shell +clone git@github.com:ceccopierangiolieugenio/pyTermTk.git +pyTermTk +``` + +#### Run Basic input test +```shell +python3 tests/test.input.py +``` + +#### Run Terminal resize test +```shell +# Press CTRL-C to exit +# the logs are written to "session.log" +python3 tests/test.ui.002.py +``` diff --git a/TermTk/TTk/__init__.py b/TermTk/TTk/__init__.py deleted file mode 100644 index 98fed2c6..00000000 --- a/TermTk/TTk/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .log import * \ No newline at end of file diff --git a/TermTk/TTkCore/__init__.py b/TermTk/TTkCore/__init__.py new file mode 100644 index 00000000..2472d25a --- /dev/null +++ b/TermTk/TTkCore/__init__.py @@ -0,0 +1,3 @@ +from .log import * +from .cfg import * +from .ttk import * \ No newline at end of file diff --git a/TermTk/TTkCore/canvas.py b/TermTk/TTkCore/canvas.py new file mode 100644 index 00000000..bf0c1f91 --- /dev/null +++ b/TermTk/TTkCore/canvas.py @@ -0,0 +1,120 @@ +#!/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 TermTk.libbpytop as lbt +from TermTk.TTkCore.log import TTkLog +from TermTk.TTkCore.cfg import * +from TermTk.TTkCore.helper import * + +class TTkCanvas: + ''' + TTkCanvas + canvas window primitives + ... + Attributes + ---------- + Methods + ------- + __init__({}) + input obj{ width, height} + + resize(w, h) + - resize the canvas keeping or cutting the current one + in w = the width of the new canvas + in h = the height of the new canvas + ''' + def __init__(self, *args, **kwargs): + self._widget = kwargs.get('widget', None) + self._width = kwargs.get('width', 0 ) + self._height = kwargs.get('height', 0 ) + self.resize(self._width, self._height) + TTkLog.debug((self._width, self._height)) + + def getWidget(self): return self._widget + + def move(self, x, y): + npos = TTkHelper.absParentPos(self._widget) + # CuTCore.cuDebug("Move: x:"+str(nx+x)+" y:"+str(ny+y)) + # self._bufPaint['move']={'x':npos.x()+x, 'y':npos.y()+y} + TTkHelper.addPaintBuffer(self) + + def resize(self, w, h): + # TTkLog.debug(f"CanvasResize:{(w,h)}") + self._data = [[]]*h + self._colors = [[]]*h + for i in range(0,h): + self._data[i] = [' ']*w + self._colors[i] = [None]*w + self._width = w + self._height = h + TTkHelper.addPaintBuffer(self) + + def zTop(self): + # TODO: Figure out how to use this + pass + + def drawBox(self, x, y, w, h): + def _set(_y,_x,_c): + if _y 2: + for i in range(x+1,x+w-1): + _set(y, i, "═") + _set(y+h-1, i, "═") + if h > 2: + for i in range(y+1,y+h-1): + _set(i, x, "║") + _set(i, x+w-1, "║") + TTkHelper.addPaintBuffer(self) + + def execPaint(self, winw, winh): + pass + + def paintCanvas(self, canvas, x, y, w, h): + # TTkLog.debug(f"PaintCanvas:{(x,y,w,h)}") + x = x if x +# +# 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. + +class TTkCfg: + DEP_2: int = 0x02 + DEP_4: int = 0x04 + DEP_8: int = 0x08 + DEP_24: int = 0x18 + + color_depth: int = DEP_24 + +class TTkGlbl: + term_w: int = 0 + term_h: int = 0 + + diff --git a/TermTk/TTkCore/draw.py b/TermTk/TTkCore/draw.py new file mode 100644 index 00000000..f6922bfc --- /dev/null +++ b/TermTk/TTkCore/draw.py @@ -0,0 +1,24 @@ +#!/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. + diff --git a/TermTk/TTkCore/helper.py b/TermTk/TTkCore/helper.py new file mode 100644 index 00000000..82f81c2d --- /dev/null +++ b/TermTk/TTkCore/helper.py @@ -0,0 +1,87 @@ +#!/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 TermTk.libbpytop as lbt +from TermTk.TTkCore.log import TTkLog +from TermTk.TTkCore.cfg import * + +class TTkHelper: + # TODO: Add Setter/Getter + _rootCanvas = None + _updateWidget = [] + _paintBuffer = [] + + @staticmethod + def addUpdateWidget(widget): + if widget not in TTkHelper._updateWidget: + TTkHelper._updateWidget.append(widget) + + @staticmethod + def addPaintBuffer(canvas): + if canvas is not TTkHelper._rootCanvas: + if canvas not in TTkHelper._paintBuffer: + TTkHelper._paintBuffer.append(canvas) + + @staticmethod + def registerRootCanvas(canvas): + TTkHelper._rootCanvas = canvas + TTkHelper._paintBuffer = [] + TTkHelper._updateWidget = [] + + @staticmethod + def execPaint(cw, ch): + if TTkHelper._rootCanvas is None : + return + for canvas in TTkHelper._paintBuffer: + widget = canvas.getWidget() + x = widget.getX() + y = widget.getY() + w = widget.getWidth() + h = widget.getHeight() + TTkHelper._rootCanvas.paintCanvas(canvas, x, y, w, h) + TTkHelper._paintBuffer = [] + + @staticmethod + def paintAll(): + if TTkHelper._rootCanvas is None: + return + for widget in TTkHelper._updateWidget: + widget.paintEvent() + TTkHelper._updateWidget = [] + TTkHelper.execPaint(TTkGlbl.term_w,TTkGlbl.term_h) + TTkHelper._rootCanvas.pushToTerminal(0, 0, TTkGlbl.term_w, TTkGlbl.term_h) + + # curses.panel.update_panels() + # TTkGlbl.GLBL['screen'].refresh() + + @staticmethod + def absPos(widget) -> (int,int): + pos = TTkHelper.absParentPos(widget) + return widget.pos() + pos + + @staticmethod + def absParentPos(widget) -> (int,int): + if widget is None or widget.parentWidget() is None: + return (0, 0) + return TTkHelper.absPos(widget.parentWidget()) \ No newline at end of file diff --git a/TermTk/TTk/log.py b/TermTk/TTkCore/log.py similarity index 73% rename from TermTk/TTk/log.py rename to TermTk/TTkCore/log.py index 79147d67..d1d2c602 100644 --- a/TermTk/TTk/log.py +++ b/TermTk/TTkCore/log.py @@ -26,6 +26,7 @@ # https://github.com/ceccopierangiolieugenio/pyCuT/blob/master/cupy/CuTCore/CuDebug.py import inspect +import logging from collections.abc import Callable class _TTkContext: @@ -48,6 +49,30 @@ class TTkLog: _messageHandler: Callable = None + @staticmethod + def _logging_message_handler(mode, context, message): + log = logging.debug + if mode == TTkLog.InfoMsg: log = logging.info + elif mode == TTkLog.WarningMsg: log = logging.warning + elif mode == TTkLog.CriticalMsg: log = logging.critical + elif mode == TTkLog.FatalMsg: log = logging.fatal + elif mode == TTkLog.ErrorMsg: log = logging.error + log(f"{context.file}:{context.line} {message}") + + @staticmethod + def use_default_file_logging(): + logging.basicConfig(level=logging.DEBUG, + filename='session.log', + format='%(levelname)s:(%(threadName)-9s) %(message)s',) + TTkLog.installMessageHandler(TTkLog._logging_message_handler) + + @staticmethod + def use_default_stdout_logging(): + logging.basicConfig(level=logging.DEBUG, + format='%(levelname)s:(%(threadName)-9s) %(message)s',) + TTkLog.installMessageHandler(TTkLog._logging_message_handler) + + @staticmethod def _process_msg(mode: int, msg: str): if TTkLog._messageHandler is not None: diff --git a/TermTk/TTkCore/ttk.py b/TermTk/TTkCore/ttk.py new file mode 100644 index 00000000..33fda681 --- /dev/null +++ b/TermTk/TTkCore/ttk.py @@ -0,0 +1,143 @@ +#!/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 os +import signal +import time +import threading, queue + +import TermTk.libbpytop as lbt +from TermTk.TTkCore.log import TTkLog +from TermTk.TTkCore.cfg import * +from TermTk.TTkCore.canvas import * +from TermTk.TTkWidgets.layout import * +from TermTk.TTkWidgets.widget import * + +class TTk(TTkWidget): + running: bool = False + events = None + key_events = None + mouse_events = None + screen_events = None + + MOUSE_EVENT = 0x01 + KEY_EVENT = 0x02 + SCREEN_EVENT = 0x04 + QUIT_EVENT = 0x08 + TIME_EVENT = 0x10 + + def __init__(self, *args, **kwargs): + TTkWidget.__init__(self, *args, **kwargs) + self.events = queue.Queue() + self.key_events = queue.Queue() + self.mouse_events = queue.Queue() + self.screen_events = queue.Queue() + TTkHelper.registerRootCanvas(self._canvas) + + def mainloop(self): + TTkLog.debug("Starting Main Loop...") + # Register events + try: + signal.signal(signal.SIGTSTP, self._SIGSTOP) # Ctrl-Z + signal.signal(signal.SIGCONT, self._SIGCONT) # Resume + signal.signal(signal.SIGINT, self._SIGINT) # Ctrl-C + except Exception as e: + TTkLog.error(f"{e}") + exit(1) + else: + TTkLog.debug("Signal Event Registered") + + lbt.Term.registerResizeCb(self._win_resize_cb) + threading.Thread(target=self._input_thread, daemon=True).start() + # threading.Timer(30.0, hello) + + self.running = True + lbt.Term.init() + while self.running: + # Main Loop + evt = self.events.get() + if evt is TTk.MOUSE_EVENT: + mevt = self.mouse_events.get() + TTkLog.info(f"Mouse Event: {mevt}") + elif evt is TTk.KEY_EVENT: + kevt = self.key_events.get() + TTkLog.info(f"Key Event: {kevt}") + pass + elif evt is TTk.TIME_EVENT: + pass + elif evt is TTk.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 TTk.QUIT_EVENT: + TTkLog.debug(f"Quit.") + break + else: + TTkLog.error(f"Unhandled Event {evt}") + break + + TTkHelper.paintAll() + + lbt.Term.exit() + pass + + def _win_resize_cb(self, width, height): + TTkGlbl.term_w = int(width) + TTkGlbl.term_h = int(height) + self.events.put(TTk.SCREEN_EVENT) + + def _input_thread(self): + def _inputCallback(kevt=None, mevt=None): + if kevt is not None: + self.key_events.put(kevt) + self.events.put(TTk.KEY_EVENT) + if mevt is not None: + self.mouse_events.put(mevt) + self.events.put(TTk.MOUSE_EVENT) + return self.running + # Start input key loop + lbt.Input.get_key(_inputCallback) + + def _canvas_thread(self): + pass + + def quit(self): + self.events.put(TTk.QUIT_EVENT) + self.running = False + + def _SIGSTOP(self, signum, frame): + """Reset terminal settings and stop background input read before putting to sleep""" + TTkLog.debug("Captured SIGSTOP ") + lbt.Term.stop() + # TODO: stop the threads + os.kill(os.getpid(), signal.SIGSTOP) + + def _SIGCONT(self, signum, frame): + """Set terminal settings and restart background input read""" + TTkLog.debug("Captured SIGCONT 'fg/bg'") + lbt.Term.cont() + # TODO: Restart threads + # TODO: Redraw the screen + + def _SIGINT(self, signum, frame): + TTkLog.debug("Captured SIGINT ") + self.quit() diff --git a/TermTk/TTkWidgets/__init__.py b/TermTk/TTkWidgets/__init__.py new file mode 100644 index 00000000..93b9ca9d --- /dev/null +++ b/TermTk/TTkWidgets/__init__.py @@ -0,0 +1 @@ +from .frame import * diff --git a/TermTk/TTkWidgets/button.py b/TermTk/TTkWidgets/button.py new file mode 100644 index 00000000..a0033998 --- /dev/null +++ b/TermTk/TTkWidgets/button.py @@ -0,0 +1,23 @@ +#!/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. \ No newline at end of file diff --git a/TermTk/TTkWidgets/frame.py b/TermTk/TTkWidgets/frame.py new file mode 100644 index 00000000..bb549d15 --- /dev/null +++ b/TermTk/TTkWidgets/frame.py @@ -0,0 +1,39 @@ +#!/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. + +from TermTk.TTkCore.log import TTkLog +from .widget import * + +class TTkFrame(TTkWidget): + def __init__(self, *args, **kwargs): + TTkWidget.__init__(self, *args, **kwargs) + self._border = kwargs.get('border', True ) + self.update() + + def paintEvent(self): + if self._border: + self._canvas.drawBox(0,0,self._width,self._height) + + + diff --git a/TermTk/TTkWidgets/layout.py b/TermTk/TTkWidgets/layout.py new file mode 100644 index 00000000..1db94b65 --- /dev/null +++ b/TermTk/TTkWidgets/layout.py @@ -0,0 +1,277 @@ +''' + Layout System +''' + +class TTkLayoutItem: + __slots__ = ('_x', '_y', '_w', '_h', '_sMax', '_sMaxVal', '_sMin', '_sMinVal') + def __init__(self): + self._x, self._y = 0, 0 + self._w, self._h = 0, 0 + self._sMax, self._sMin = False, False + self._sMaxVal, self._sMinVal = 0, 0 + pass + def minimumSize(self): + return self.minimumWidth(), self.minimumHeight() + def minimumHeight(self): return 0 + def minimumWidth(self): return 0 + + def maximumSize(self): + return self.maximumWidth(), self.maximumHeight() + def maximumHeight(self): return 0x80000000 + def maximumWidth(self): return 0x80000000 + + def geometry(self): + return self._x, self._y, self._w, self._h + + def setGeometry(self, x, y, w, h): + self._x = x + self._y = y + self._w = w + self._h = h + + +class TTkLayout(TTkLayoutItem): + def __init__(self): + TTkLayoutItem.__init__(self) + self._items = [] + self._parent = None + pass + + def children(self): + return self._items + + def count(self): + return len(self._items) + + def itemAt(self, index): + if index < len(self._items): + return self._items[index] + return 0 + + def setParent(self, parent): + self._parent = parent + + def parentWidget(self): + return self._parent + + def addItem(self, item): + self._items.append(item) + + def addWidget(self, widget): + self.addItem(TTkWidgetItem(widget)) + + def removeWidget(self, widget): + for i in self._items: + if i.widget() == widget: + self._items.remove(i) + return + + def update(self): + for i in self.children(): + if isinstance(i, TTkWidgetItem) and not i.isEmpty(): + i.widget().update() + # TODO: Have a look at this: + # i.getCanvas().top() + elif isinstance(i, TTkLayout): + i.update() + +class TTkWidgetItem(TTkLayoutItem): + def __init__(self, widget): + TTkLayoutItem.__init__(self) + self._widget = widget + + def widget(self): + return self._widget + + def isEmpty(self): return self._widget is None + + def minimumSize(self): return self._widget.minimumSize() + def minimumHeight(self): return self._widget.minimumHeight() + def minimumWidth(self): return self._widget.minimumWidth() + def maximumSize(self): return self._widget.maximumSize() + def maximumHeight(self): return self._widget.maximumHeight() + def maximumWidth(self): return self._widget.maximumWidth() + + def geometry(self): return self._widget.geometry() + + def setGeometry(self, x, y, w, h): + self._widget.setGeometry(x, y, w, h) + + + +class TTkHBoxLayout(TTkLayout): + def __init__(self): + TTkLayout.__init__(self) + + def minimumWidth(self): + ''' process the widgets and get the min size ''' + minw = 0 + for item in self.children(): + w1 = item.minimumWidth() + minw += w1 + return minw + + def minimumHeight(self): + ''' process the widgets and get the min size ''' + minh = TTkLayout.minimumHeight(self) + for item in self.children(): + h1 = item.minimumHeight() + if h1 > minh : minh = h1 + return minh + + def maximumWidth(self): + ''' process the widgets and get the min size ''' + maxw = 0 + for item in self.children(): + w1 = item.maximumWidth() + maxw += w1 + return maxw + + def maximumHeight(self): + ''' process the widgets and get the min size ''' + maxh = TTkLayout.maximumHeight(self) + for item in self.children(): + h1 = item.maximumHeight() + if h1 < maxh : maxh = h1 + return maxh + + def update(self): + x, y, w, h = self.geometry() + numWidgets = self.count() + leftWidgets = numWidgets + freeWidth = w + newx, newy = x, y + # Loop to check the resizable space + for item in self.children(): + item._sMax = False + item._sMin = False + iterate = True + while iterate and leftWidgets > 0: + iterate = False + sliceSize = freeWidth//leftWidgets + for item in self.children(): + if item._sMax or item._sMin: continue + maxs = item.maximumWidth() + mins = item.minimumWidth() + if sliceSize > maxs: + freeWidth -= maxs + iterate = True + item._sMax = True + item._sMaxVal = maxs + leftWidgets -= 1 + elif sliceSize < mins: + freeWidth -= mins + leftWidgets -= 1 + # slicesize = freeWidth//leftWidgets + iterate = True + item._sMin = True + item._sMinVal = mins + + # loop and set the geometry of any item + for item in self.children(): + if item._sMax: + item.setGeometry(newx, newy, item._sMaxVal, h) + newy += item._sMaxVal + elif item._sMin: + item.setGeometry(newx, newy, item._sMinVal, h) + newy += item._sMinVal + else: + sliceSize = freeWidth//leftWidgets + item.setGeometry(newx, newy, sliceSize, h) + newx += sliceSize + freeWidth -= sliceSize + leftWidgets -= 1 + if isinstance(item, TTkWidgetItem) and not item.isEmpty(): + item.widget().update() + item.widget().getCanvas().zTop() + elif isinstance(item, TTkLayout): + item.update() + + +class TTkVBoxLayout(TTkLayout): + def __init__(self): + TTkLayout.__init__(self) + + def minimumWidth(self): + ''' process the widgets and get the min size ''' + minw = TTkLayout.minimumWidth(self) + for item in self.children(): + w1 = item.minimumWidth() + if w1 > minw : minw = w1 + return minw + + def minimumHeight(self): + ''' process the widgets and get the min size ''' + minh = 0 + for item in self.children(): + h1 = item.minimumHeight() + minh += h1 + return minh + + def maximumWidth(self): + ''' process the widgets and get the min size ''' + maxw = TTkLayout.maximumWidth(self) + for item in self.children(): + w1 = item.maximumWidth() + if w1 < maxw : maxw = w1 + return maxw + + def maximumHeight(self): + ''' process the widgets and get the min size ''' + maxh = 0 + for item in self.children(): + h1 = item.maximumHeight() + maxh += h1 + return maxh + + def update(self): + x, y, w, h = self.geometry() + numWidgets = self.count() + leftWidgets = numWidgets + freeHeight = h + newx, newy = x, y + # Loop to check the resizable space + for item in self.children(): + item._sMax = False + item._sMin = False + iterate = True + while iterate and leftWidgets > 0: + iterate = False + sliceSize = freeHeight//leftWidgets + for item in self.children(): + if item._sMax or item._sMin: continue + maxs = item.maximumHeight() + mins = item.minimumHeight() + if sliceSize > maxs: + freeHeight -= maxs + iterate = True + item._sMax = True + item._sMaxVal = maxs + leftWidgets -= 1 + elif sliceSize < mins: + freeHeight -= mins + leftWidgets -= 1 + # slicesize = freeHeight//leftWidgets + iterate = True + item._sMin = True + item._sMinVal = mins + + # loop and set the geometry of any item + for item in self.children(): + if item._sMax: + item.setGeometry(newx, newy, w, item._sMaxVal) + newy += item._sMaxVal + elif item._sMin: + item.setGeometry(newx, newy, w, item._sMinVal) + newy += item._sMinVal + else: + sliceSize = freeHeight//leftWidgets + item.setGeometry(newx, newy, w, sliceSize) + newy += sliceSize + freeHeight -= sliceSize + leftWidgets -= 1 + if isinstance(item, TTkWidgetItem) and not item.isEmpty(): + item.widget().update() + item.widget().getCanvas().zTop() + elif isinstance(item, TTkLayout): + item.update() diff --git a/TermTk/TTkWidgets/widget.py b/TermTk/TTkWidgets/widget.py new file mode 100644 index 00000000..37284ada --- /dev/null +++ b/TermTk/TTkWidgets/widget.py @@ -0,0 +1,172 @@ +#!/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. + +from TermTk.TTkCore.canvas import * +from TermTk.TTkCore.cfg import * +from .layout import * + +class TTkWidget: + def __init__(self, *args, **kwargs): + self._childs = [] + self._parent = kwargs.get('parent', None ) + self._x = kwargs.get('x', 0 ) + self._y = kwargs.get('y', 0 ) + self._width = kwargs.get('width' , 0 ) + self._height = kwargs.get('height', 0 ) + self._maxw = 0x80000000 + self._maxh = 0x80000000 + self._minw = 0x00000000 + self._minh = 0x00000000 + self._layout = TTkLayout() + self._canvas = TTkCanvas( + widget = self, + width = self._width , + height = self._height ) + if self._parent is not None and \ + self._parent._layout is not None: + self._parent._layout.addWidget(self) + + def getX(self): return self._x + def getY(self): return self._y + def getWidth(self): return self._width + def getHeight(self): return self._height + + def pos(self): + return (self._x, self._y) + + def addLayout(self, l): + self._layout = l + + def paintEvent(self): pass + + def move(self, x, y): + self._x = x + self._y = y + self._canvas.move(self._x, self._y) + if self._layout is not None: + self._layout.setGeometry(self._x, self._y, self._width, self._height) + self.update() + + def resize(self, w, h): + self._width = w + self._height = h + self._canvas.resize(self._width, self._height) + if self._layout is not None: + self._layout.setGeometry(self._x, self._y, self._width, self._height) + self.update() + + def setGeometry(self, x, y, w, h): + self.resize(w, h) + self.move(x, y) + + def mouseDoubleClickEvent(self, evt): pass + def mouseMoveEvent(self, evt): pass + def mousePressEvent(self, evt): pass + def mouseReleaseEvent(self, evt): pass + def wheelEvent(self, evt): pass + def enterEvent(self, evt): pass + def leaveEvent(self, evt): pass + def keyPressEvent(self, evt): pass + def keyReleaseEvent(self, evt): pass + + def event(self, evt): + pass +# # handle own events +# if evt.type() == CuEvent.MouseMove: +# if evt.button() == CuT.NoButton: +# self.mouseMoveEvent(evt) +# elif evt.type() == CuEvent.MouseButtonRelease: +# self.mouseReleaseEvent(evt) +# elif evt.type() == CuEvent.MouseButtonPress: +# self.mousePressEvent(evt) +# if self.focusPolicy() & CuT.ClickFocus == CuT.ClickFocus: +# self.setFocus() +# elif evt.type() == CuEvent.Wheel: +# self.wheelEvent(evt) +# if self.focusPolicy() & CuT.WheelFocus == CuT.WheelFocus: +# self.setFocus() +# elif evt.type() == CuEvent.KeyPress: +# self.keyPressEvent(evt) +# elif evt.type() == CuEvent.KeyRelease: +# self.keyReleaseEvent(evt) +# # Trigger this event to the childs +# if self._layout is not None: +# return CuWidget._eventLayoutHandle(evt, self._layout) + def maximumSize(self): + return self.maximumWidth(), self.maximumHeight() + def maximumHeight(self): + wMaxH = self._maxh + if self._layout is not None: + lMaxH = self._layout.maximumHeight() + if lMaxH < wMaxH: + return lMaxH + return wMaxH + def maximumWidth(self): + wMaxW = self._maxw + if self._layout is not None: + lMaxW = self._layout.maximumWidth() + if lMaxW < wMaxW: + return lMaxW + return wMaxW + + def minimumSize(self): + return self.minimumWidth(), self.minimumHeight() + def minimumHeight(self): + wMinH = self._minh + if self._layout is not None: + lMinH = self._layout.minimumHeight() + if lMinH > wMinH: + return lMinH + return wMinH + def minimumWidth(self): + wMinW = self._minw + if self._layout is not None: + lMinW = self._layout.minimumWidth() + if lMinW > wMinW: + return lMinW + return wMinW + + def setMaximumSize(self, maxw, maxh): self._maxw = maxw; self._maxh = maxh + def setMaximumHeight(self, maxh): self._maxh = maxh + def setMaximumWidth(self, maxw): self._maxw = maxw + + def setMinimumSize(self, minw, minh): self._minw = minw; self._minh = minh + def setMinimumHeight(self, minh): self._minh = minh + def setMinimumWidth(self, minw): self._minw = minw + + def update(self): + TTkHelper.addUpdateWidget(self) + if self._layout is not None: + self._layout.update() + + def getCanvas(self): + return self._canvas + + def layout(self): + return self._layout + + def setParent(self, parent): + self._parent = parent + def parentWidget(self): + return self._parent \ No newline at end of file diff --git a/TermTk/__init__.py b/TermTk/__init__.py index e69de29b..9af0948e 100644 --- a/TermTk/__init__.py +++ b/TermTk/__init__.py @@ -0,0 +1,2 @@ +from .TTkCore import * +from .TTkWidgets import * \ No newline at end of file diff --git a/TermTk/libbpytop/__init__.py b/TermTk/libbpytop/__init__.py index 0e3ca9fc..e1657596 100644 --- a/TermTk/libbpytop/__init__.py +++ b/TermTk/libbpytop/__init__.py @@ -1,2 +1,3 @@ -from .input import * -from .term import * \ No newline at end of file +from .input import * +from .term import * +from .colors import * \ No newline at end of file diff --git a/TermTk/libbpytop/canvas..py b/TermTk/libbpytop/canvas..py deleted file mode 100644 index 0033ce0a..00000000 --- a/TermTk/libbpytop/canvas..py +++ /dev/null @@ -1,16 +0,0 @@ -#!/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. \ No newline at end of file diff --git a/TermTk/libbpytop/colors.py b/TermTk/libbpytop/colors.py new file mode 100644 index 00000000..5e380a5a --- /dev/null +++ b/TermTk/libbpytop/colors.py @@ -0,0 +1,171 @@ +#!/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) + +from . import Term +import TermTk as ttk + + +# Ansi Escape Codes: +# https://conemu.github.io/en/AnsiEscapeCodes.html + +class Color: + '''Holds representations for a 24-bit color value + __init__(color, depth="fg", default=False) + -- color accepts 6 digit hexadecimal: string "#RRGGBB", 2 digit hexadecimal: string "#FF" or decimal RGB "255 255 255" as a string. + -- depth accepts "fg" or "bg" + __call__(*args) joins str arguments to a string and apply color + __str__ returns escape sequence to set color + __iter__ returns iteration over red, green and blue in integer values of 0-255. + * Values: .hexa: str | .dec: Tuple[int, int, int] | .red: int | .green: int | .blue: int | .depth: str | .escape: str + ''' + hexa: str; dec: Tuple[int, int, int]; red: int; green: int; blue: int; depth: str; escape: str; default: bool + + def __init__(self, color: str, depth: str = "fg", default: bool = False): + self.depth = depth + self.default = default + try: + if not color: + self.dec = (-1, -1, -1) + self.hexa = "" + self.red = self.green = self.blue = -1 + self.escape = "\033[49m" if depth == "bg" and default else "" + return + + elif color.startswith("#"): + self.hexa = color + if len(self.hexa) == 3: + self.hexa += self.hexa[1:3] + self.hexa[1:3] + c = int(self.hexa[1:3], base=16) + self.dec = (c, c, c) + elif len(self.hexa) == 7: + self.dec = (int(self.hexa[1:3], base=16), int(self.hexa[3:5], base=16), int(self.hexa[5:7], base=16)) + else: + raise ValueError(f'Incorrectly formatted hexadecimal rgb string: {self.hexa}') + + else: + c_t = tuple(map(int, color.split(" "))) + if len(c_t) == 3: + self.dec = c_t #type: ignore + else: + raise ValueError(f'RGB dec should be "0-255 0-255 0-255"') + + ct = self.dec[0] + self.dec[1] + self.dec[2] + if ct > 255*3 or ct < 0: + raise ValueError(f'RGB values out of range: {color}') + except Exception as e: + ttk.TTkLog.error(str(e)) + self.escape = "" + return + + if self.dec and not self.hexa: self.hexa = f'{hex(self.dec[0]).lstrip("0x").zfill(2)}{hex(self.dec[1]).lstrip("0x").zfill(2)}{hex(self.dec[2]).lstrip("0x").zfill(2)}' + + if self.dec and self.hexa: + self.red, self.green, self.blue = self.dec + self.escape = f'\033[{38 if self.depth == "fg" else 48};2;{";".join(str(c) for c in self.dec)}m' + + if ttk.TTkCfg.color_depth is not ttk.TTkCfg.DEP_24: + self.escape = f'{self.truecolor_to_256(rgb=self.dec, depth=self.depth)}' + + def __str__(self) -> str: + return self.escape + + def __repr__(self) -> str: + return repr(self.escape) + + def __iter__(self) -> Iterable: + for c in self.dec: yield c + + def __call__(self, *args: str) -> str: + if len(args) < 1: return "" + return f'{self.escape}{"".join(args)}{getattr(Term, self.depth)}' + + @staticmethod + def truecolor_to_256(rgb: Tuple[int, int, int], depth: str="fg") -> str: + out: str = "" + pre: str = f'\033[{"38" if depth == "fg" else "48"};5;' + + greyscale: Tuple[int, int, int] = ( rgb[0] // 11, rgb[1] // 11, rgb[2] // 11 ) + if greyscale[0] == greyscale[1] == greyscale[2]: + out = f'{pre}{232 + greyscale[0]}m' + else: + out = f'{pre}{round(rgb[0] / 51) * 36 + round(rgb[1] / 51) * 6 + round(rgb[2] / 51) + 16}m' + + return out + + @staticmethod + def escape_color(hexa: str = "", r: int = 0, g: int = 0, b: int = 0, depth: str = "fg") -> str: + """Returns escape sequence to set color + * accepts either 6 digit hexadecimal hexa="#RRGGBB", 2 digit hexadecimal: hexa="#FF" + * or decimal RGB: r=0-255, g=0-255, b=0-255 + * depth="fg" or "bg" + """ + dint: int = 38 if depth == "fg" else 48 + color: str = "" + if hexa: + try: + if len(hexa) == 3: + c = int(hexa[1:], base=16) + if ttk.TTkCfg.color_depth is ttk.TTkCfg.DEP_24: + color = f'\033[{dint};2;{c};{c};{c}m' + else: + color = f'{Color.truecolor_to_256(rgb=(c, c, c), depth=depth)}' + elif len(hexa) == 7: + if ttk.TTkCfg.color_depth is ttk.TTkCfg.DEP_24: + color = f'\033[{dint};2;{int(hexa[1:3], base=16)};{int(hexa[3:5], base=16)};{int(hexa[5:7], base=16)}m' + else: + color = f'{Color.truecolor_to_256(rgb=(int(hexa[1:3], base=16), int(hexa[3:5], base=16), int(hexa[5:7], base=16)), depth=depth)}' + except ValueError as e: + ttk.TTkLog.error(f'{e}') + + else: + if ttk.TTkCfg.color_depth is ttk.TTkCfg.DEP_24: + color = f'\033[{dint};2;{r};{g};{b}m' + else: + color = f'{Color.truecolor_to_256(rgb=(r, g, b), depth=depth)}' + return color + + @classmethod + def fg(cls, *args) -> str: + if len(args) > 2: return cls.escape_color(r=args[0], g=args[1], b=args[2], depth="fg") + else: return cls.escape_color(hexa=args[0], depth="fg") + + @classmethod + def bg(cls, *args) -> str: + if len(args) > 2: return cls.escape_color(r=args[0], g=args[1], b=args[2], depth="bg") + else: return cls.escape_color(hexa=args[0], depth="bg") + +#class Colors: +# '''Standard colors for menus and dialogs''' +# default = Color("#cc") +# white = Color("#ff") +# red = Color("#bf3636") +# green = Color("#68bf36") +# blue = Color("#0fd7ff") +# yellow = Color("#db8b00") +# black_bg = Color("#00", depth="bg") +# null = Color("") \ No newline at end of file diff --git a/TermTk/libbpytop/input.py b/TermTk/libbpytop/input.py index aa244af3..11e40648 100644 --- a/TermTk/libbpytop/input.py +++ b/TermTk/libbpytop/input.py @@ -27,7 +27,7 @@ except Exception as e: print(f'ERROR: {e}') exit(1) -from TermTk.TTk import TTkLog +import TermTk as ttk class MouseEvent: # Keys @@ -130,9 +130,7 @@ class Input: # 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: + if len(input_key) == 1: # Key Pressed kevt = KeyEvent(0, input_key) elif input_key.startswith("\033[<"): @@ -140,7 +138,7 @@ class Input: m = mouse_re.match(input_key) if not m: # TODO: Return Error - TTkLog.error("UNHANDLED: "+input_key.replace("\033","")) + ttk.TTkLog.error("UNHANDLED: "+input_key.replace("\033","")) continue code = int(m.group(1)) x = int(m.group(2)) @@ -174,10 +172,11 @@ class Input: evt = MouseEvent.Down mevt = MouseEvent(x, y, key, evt, m.group(0).replace("\033", "")) else: - TTkLog.error("UNHANDLED: "+input_key.replace("\033","")) + ttk.TTkLog.error("UNHANDLED: "+input_key.replace("\033","")) input_key = "" if callback is not None: - callback(kevt, mevt) + if not callback(kevt, mevt): + break def main(): print("Retrieve Keyboard, Mouse press/drag/wheel Events") diff --git a/TermTk/libbpytop/term.py b/TermTk/libbpytop/term.py index 4bf389e3..4ea45038 100644 --- a/TermTk/libbpytop/term.py +++ b/TermTk/libbpytop/term.py @@ -26,8 +26,36 @@ except Exception as e: print(f'ERROR: {e}') exit(1) +class Mv: + """Class with collection of cursor movement functions: .t[o](line, column) | .r[ight](columns) | .l[eft](columns) | .u[p](lines) | .d[own](lines) | .save() | .restore()""" + @staticmethod + def to(line: int, col: int) -> str: + return f'\033[{line};{col}f' #* Move cursor to line, column + @staticmethod + def right(x: int) -> str: #* Move cursor right x columns + return f'\033[{x}C' + @staticmethod + def left(x: int) -> str: #* Move cursor left x columns + return f'\033[{x}D' + @staticmethod + def up(x: int) -> str: #* Move cursor up x lines + return f'\033[{x}A' + @staticmethod + def down(x: int) -> str: #* Move cursor down x lines + return f'\033[{x}B' + + save: str = "\033[s" #* Save cursor position + restore: str = "\033[u" #* Restore saved cursor postion + t = to + r = right + l = left + u = up + d = down + class Term: """Terminal info and commands""" + title: str = "TermTk" + mouse: bool = True width: int = 0 height: int = 0 fg: str = "" #* Default foreground color @@ -46,14 +74,30 @@ class Term: @staticmethod def init(mouse: bool = True, title: str = "TermTk"): - Term.push(Term.alt_screen, Term.clear, Term.hide_cursor, Term.title("BpyTOP")) - if mouse: + Term.title = title + Term.mouse = mouse + Term.push(Term.alt_screen, Term.clear, Term.hide_cursor, Term.escTitle(Term.title)) + if Term.mouse: + Term.push(Term.mouse_on) + Term.echo(False) + + @staticmethod + def stop(): + Term.push(Term.mouse_off, Term.mouse_direct_off) + Term.push(Term.clear, Term.normal_screen, Term.show_cursor, Term.escTitle()) + Term.echo(True) + + @staticmethod + def cont(): + Term.push(Term.alt_screen, Term.clear, Term.hide_cursor, Term.escTitle(Term.title)) + if Term.mouse: Term.push(Term.mouse_on) Term.echo(False) @staticmethod def exit(): Term.push(Term.mouse_off, Term.mouse_direct_off) + Term.push(Term.clear, Term.normal_screen, Term.show_cursor, Term.escTitle()) Term.echo(True) @@ -77,7 +121,7 @@ class Term: print(*args, sep="", end="", flush=True) @staticmethod - def title(text: str = "") -> str: + def escTitle(text: str = "") -> str: out: str = f'{os.environ.get("TERMINAL_TITLE", "")}' if out and text: out += " " if text: out += f'{text}' @@ -95,4 +139,3 @@ class Term: # Dummy call to retrieve the terminal size Term._sigWinCh(signal.SIGWINCH, None) signal.signal(signal.SIGWINCH, Term._sigWinCh) - diff --git a/docs/TODO.md b/docs/TODO.md index e3fe4318..b65db7f1 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,9 +1,11 @@ # TODO - [ ] Follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) coding style -- [ ] Terminal Helper + +## Terminal Helper - [ ] Events - [x] Window : SIGWINCH triggered when the terminal is resized -- [ ] Input Class + +## Input Class - [ ] Return Error if Mouse RE does not match - [x] Handle the Paste Buffer - [ ] Investigate the middle mouse button paste @@ -14,9 +16,20 @@ https://www.pythontutorial.net/tkinter/tkinter-event-binding/ - [x] Keyboard - [x] Mouse -- [ ] Canvas Class -- [ ] Logs + +## Canvas Class + - [ ] Have a look to the Unicode chartable: https://www.utf8-chartable.de/unicode-utf8-table.pl + +## Signal Slots +- [ ] Implement Signal Slots + +## Logs - [x] Log Class - [ ] Log helpers + - [x] File and Stdout logger - [ ] logger auto integration - - [ ] stdout until mainLoop \ No newline at end of file + - [ ] stdout until mainLoop + +## Widgets +### Layout +- [ ] Add Weight in V and H Layout \ No newline at end of file diff --git a/tests/tailSession.sh b/tests/tailSession.sh new file mode 100755 index 00000000..b7342f5b --- /dev/null +++ b/tests/tailSession.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +esc=$(printf '\033') + +_RST_=${esc}'[0m' # resets color and format + +# Regular Colors +Black=${esc}'[38;5;0m' +Red=${esc}'[38;5;1m' +Green=${esc}'[38;5;2m' +Yellow=${esc}'[38;5;3m' +Blue=${esc}'[38;5;4m' +Magenta=${esc}'[38;5;5m' +Cyan=${esc}'[38;5;6m' +White=${esc}'[38;5;7m' + +# Background +On_Black=${esc}'[48;5;0m' +On_Red=${esc}'[48;5;1m' +On_Green=${esc}'[48;5;2m' +On_Yellow=${esc}'[48;5;3m' +On_Blue=${esc}'[48;5;4m' +On_Magenta=${esc}'[48;5;5m' +On_Cyan=${esc}'[48;5;6m' +On_White=${esc}'[48;5;7m' + +while read -r line; do + echo "$line" | + sed "s,/home.*/TermTk/,TermTk/," | + sed "s,^\(INFO:\),${Green}\1${_RST_}," | + sed "s,^\(ERROR:\),${Red}\1${_RST_}," | + sed "s,^\(DEBUG:\),${Blue}\1${_RST_}," +done < <(tail -F session.log) \ No newline at end of file diff --git a/tests/test.draw.001.py b/tests/test.draw.001.py new file mode 100755 index 00000000..1c002ab2 --- /dev/null +++ b/tests/test.draw.001.py @@ -0,0 +1,65 @@ +#!/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 +import logging +import time + +sys.path.append(os.path.join(sys.path[0],'..')) +from TermTk.libbpytop import Term, Mv +from TermTk import TTkLog + +def message_handler(mode, context, message): + log = logging.debug + if mode == TTkLog.InfoMsg: log = logging.info + elif mode == TTkLog.WarningMsg: log = logging.warning + elif mode == TTkLog.CriticalMsg: log = logging.critical + elif mode == TTkLog.FatalMsg: log = logging.fatal + elif mode == TTkLog.ErrorMsg: log = logging.error + log(f"{context.file} {message}") + +logging.basicConfig(level=logging.DEBUG, + filename='session.log', + format='%(levelname)s:(%(threadName)-9s) %(message)s',) +TTkLog.installMessageHandler(message_handler) + +Term.init(mouse=False) + +Term.push( + Mv.t(2,4) + + "Test Text 3" + ) +time.sleep(1) +Term.push( + Mv.d(1) + Mv.l(3) + + "Test Text 2" + ) +time.sleep(1) +Term.push( + Mv.d(1) + Mv.l(3) + + "Test Text 1" + ) +time.sleep(1) + +Term.exit() \ No newline at end of file diff --git a/tests/test.draw.002.py b/tests/test.draw.002.py new file mode 100755 index 00000000..262d4f4e --- /dev/null +++ b/tests/test.draw.002.py @@ -0,0 +1,74 @@ +#!/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 +import logging +import time + +sys.path.append(os.path.join(sys.path[0],'..')) +from TermTk.libbpytop import Term, Mv, Color +from TermTk import TTkLog + +def message_handler(mode, context, message): + log = logging.debug + if mode == TTkLog.InfoMsg: log = logging.info + elif mode == TTkLog.WarningMsg: log = logging.warning + elif mode == TTkLog.CriticalMsg: log = logging.critical + elif mode == TTkLog.FatalMsg: log = logging.fatal + elif mode == TTkLog.ErrorMsg: log = logging.error + log(f"{context.file} {message}") + +logging.basicConfig(level=logging.DEBUG, + filename='session.log', + format='%(levelname)s:(%(threadName)-9s) %(message)s',) +TTkLog.installMessageHandler(message_handler) + +Term.init(mouse=False) +TTkLog.info("Starting") +Term.push( + Mv.t(2,4) + # Cursor x:2, y:4 + Color.fg("#ff0000") + + "Test Text 3" + ) +time.sleep(1) +TTkLog.info("next : 2") + +Term.push( + Mv.d(1) + Mv.l(3) + # Cursor 1 Down, 3 Left + Color.bg("#550088") + + "Test Text 2" + ) +time.sleep(1) +TTkLog.info("next : 1") + +Term.push( + Mv.d(1) + Mv.l(3) + # Cursor 1 Down, 3 Left + Color.fg("#00ff00") + + Color.bg("#555500") + + "Test Text 1" + ) +time.sleep(1) +TTkLog.info("Ending") + +Term.exit() \ No newline at end of file diff --git a/tests/test.draw.py b/tests/test.draw.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/test.input.py b/tests/test.input.py old mode 100644 new mode 100755 index 61064aa9..a3d12f19 --- a/tests/test.input.py +++ b/tests/test.input.py @@ -1,9 +1,33 @@ +#!/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 import logging sys.path.append(os.path.join(sys.path[0],'..')) +from TermTk import TTkLog import TermTk.libbpytop as lbt -from TermTk.TTk import TTkLog def message_handler(mode, context, message): log = logging.debug @@ -25,10 +49,13 @@ lbt.Term.push(lbt.Term.mouse_on) lbt.Term.echo(False) def keyCallback(kevt=None, mevt=None): - if kevt is not None: - TTkLog.info(f"Key Event: {kevt}") if mevt is not None: TTkLog.info(f"Mouse Event: {mevt}") + if kevt is not None: + TTkLog.info(f"Key Event: {kevt}") + if kevt.key == "q": + return False + return True def winCallback(width, height): TTkLog.info(f"Resize: w:{width}, h:{height}") @@ -37,4 +64,4 @@ 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 +lbt.Term.echo(True) diff --git a/tests/test.ui.001.py b/tests/test.ui.001.py new file mode 100755 index 00000000..adbc456f --- /dev/null +++ b/tests/test.ui.001.py @@ -0,0 +1,35 @@ +#!/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.TTkFrame(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.002.py b/tests/test.ui.002.py new file mode 100755 index 00000000..35c68f2a --- /dev/null +++ b/tests/test.ui.002.py @@ -0,0 +1,47 @@ +#!/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() +root._layout = ttk.TTkHBoxLayout() +ttk.TTkFrame(parent=root,border=True) +rightframe = ttk.TTkFrame(parent=root) +rightframe._layout = ttk.TTkVBoxLayout() + +ttk.TTkFrame(parent=rightframe, border=True) +centerrightframe=ttk.TTkFrame(parent=rightframe) +ttk.TTkFrame(parent=rightframe, border=True) + +centerrightframe._layout = ttk.TTkHBoxLayout() + +ttk.TTkFrame(parent=centerrightframe, border=True) +ttk.TTkFrame(parent=centerrightframe, border=True) +ttk.TTkFrame(parent=centerrightframe, border=True) +root.mainloop() \ No newline at end of file diff --git a/tests/test.ui.102..py b/tests/test.ui.102..py new file mode 100755 index 00000000..ec5922bd --- /dev/null +++ b/tests/test.ui.102..py @@ -0,0 +1,63 @@ +#!/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 +import logging +import time + +sys.path.append(os.path.join(sys.path[0],'..')) +from TermTk import TTkLog + +from TermTk import TTk + +class Demo1: + def __init__(self, master): + self.master = master + self.frame = ttk.Frame(self.master) + self.button1 = ttk.Button(self.frame, text = 'New Window', width = 25, command = self.new_window) + self.button1.pack() + self.frame.pack() + + def new_window(self): + self.newWindow = ttk.Toplevel(self.master) + self.app = Demo2(self.newWindow) + +class Demo2: + def __init__(self, master): + self.master = master + self.frame = ttk.Frame(self.master) + self.quitButton = ttk.Button(self.frame, text = 'Quit', width = 25, command = self.close_windows) + self.quitButton.pack() + self.frame.pack() + + def close_windows(self): + self.master.destroy() + +def main(): + root = ttk.TTk() + app = Demo1(root) + root.mainloop() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/tests/utf-8.txt b/tests/utf-8.txt old mode 100755 new mode 100644