From 18c0f7efa09ed9ca8ed102b8291f62a22fd42b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Parodi=2C=20Eugenio=20=F0=9F=8C=B6?= Date: Fri, 4 Apr 2025 17:58:45 +0100 Subject: [PATCH] apps: added tlogg to the monorepo --- .vscode/launch.json | 11 + apps/tlogg/Makefile | 35 ++ apps/tlogg/README.md | 56 ++++ apps/tlogg/pyproject.toml | 46 +++ apps/tlogg/tlogg/__init__.py | 27 ++ apps/tlogg/tlogg/__main__.py | 28 ++ apps/tlogg/tlogg/app/__init__.py | 28 ++ apps/tlogg/tlogg/app/about.py | 64 ++++ apps/tlogg/tlogg/app/cfg.py | 81 +++++ apps/tlogg/tlogg/app/filetree.py | 123 +++++++ apps/tlogg/tlogg/app/fileviewer.py | 307 ++++++++++++++++++ apps/tlogg/tlogg/app/glbl.py | 36 ++ apps/tlogg/tlogg/app/highlighters.py | 164 ++++++++++ apps/tlogg/tlogg/app/loggwidget.py | 106 ++++++ apps/tlogg/tlogg/app/main.py | 206 ++++++++++++ apps/tlogg/tlogg/app/notepad.py | 177 ++++++++++ apps/tlogg/tlogg/app/options.py | 76 +++++ apps/tlogg/tlogg/app/predefinedfilters.py | 180 ++++++++++ apps/tlogg/tlogg/helper.py | 82 +++++ apps/tlogg/tlogg/plugin.py | 41 +++ apps/tlogg/tlogg/plugins/fileexplorer.py | 127 ++++++++ apps/tlogg/tlogg/plugins/jsonnviewer.py | 56 ++++ apps/tlogg/tlogg/plugins/testplugin.py | 33 ++ apps/tlogg/tlogg/plugins/testplugin1.py | 36 ++ apps/tlogg/tlogg/proxy.py | 50 +++ apps/tlogg/tools/create_log.py | 70 ++++ apps/tlogg/tools/import.filters.from.klogg.py | 39 +++ apps/tlogg/tools/test.config/colors.yaml | 30 ++ apps/tlogg/tools/test.config/options.yaml | 3 + apps/ttkode/pyproject.toml | 2 +- apps/ttkode/ttkode/app/cfg.py | 6 +- 31 files changed, 2323 insertions(+), 3 deletions(-) create mode 100644 apps/tlogg/Makefile create mode 100644 apps/tlogg/README.md create mode 100644 apps/tlogg/pyproject.toml create mode 100755 apps/tlogg/tlogg/__init__.py create mode 100644 apps/tlogg/tlogg/__main__.py create mode 100644 apps/tlogg/tlogg/app/__init__.py create mode 100644 apps/tlogg/tlogg/app/about.py create mode 100644 apps/tlogg/tlogg/app/cfg.py create mode 100644 apps/tlogg/tlogg/app/filetree.py create mode 100644 apps/tlogg/tlogg/app/fileviewer.py create mode 100644 apps/tlogg/tlogg/app/glbl.py create mode 100644 apps/tlogg/tlogg/app/highlighters.py create mode 100644 apps/tlogg/tlogg/app/loggwidget.py create mode 100644 apps/tlogg/tlogg/app/main.py create mode 100644 apps/tlogg/tlogg/app/notepad.py create mode 100644 apps/tlogg/tlogg/app/options.py create mode 100644 apps/tlogg/tlogg/app/predefinedfilters.py create mode 100644 apps/tlogg/tlogg/helper.py create mode 100644 apps/tlogg/tlogg/plugin.py create mode 100644 apps/tlogg/tlogg/plugins/fileexplorer.py create mode 100644 apps/tlogg/tlogg/plugins/jsonnviewer.py create mode 100644 apps/tlogg/tlogg/plugins/testplugin.py create mode 100644 apps/tlogg/tlogg/plugins/testplugin1.py create mode 100644 apps/tlogg/tlogg/proxy.py create mode 100755 apps/tlogg/tools/create_log.py create mode 100755 apps/tlogg/tools/import.filters.from.klogg.py create mode 100644 apps/tlogg/tools/test.config/colors.yaml create mode 100644 apps/tlogg/tools/test.config/options.yaml diff --git a/.vscode/launch.json b/.vscode/launch.json index 7d9887e7..6151ff44 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -116,6 +116,17 @@ "PYTHONPATH": "./apps/ttkode" } }, + { + "name": "py Debug: Module tlogg", + "type": "debugpy", + "request": "launch", + "module": "tlogg", + "console": "integratedTerminal", + "justMyCode": true, + "env": { + "PYTHONPATH": "./apps/tlogg" + } + }, { "name": "Python: Demo", "type": "debugpy", diff --git a/apps/tlogg/Makefile b/apps/tlogg/Makefile new file mode 100644 index 00000000..36bfc4e0 --- /dev/null +++ b/apps/tlogg/Makefile @@ -0,0 +1,35 @@ +.PHONY: doc runGittk runDemo build deploy buildTest deployTest + +.venv: + python3 -m venv .venv + . .venv/bin/activate ; \ + pip install -r docs/requirements.txt + # Regen requirements; + # pip freeze > docs/requirements.txt + +build: .venv + . .venv/bin/activate ; \ + rm -rf dist ; \ + tools/prepareBuild.sh release ; \ + cd tmp ; \ + python3 -m build + +buildTest: .venv + . .venv/bin/activate ; \ + rm -rf dist ; \ + tools/prepareBuild.sh test ; \ + cd tmp ; \ + python3 -m build ; + +deployTest: .venv + . .venv/bin/activate ; \ + python3 -m twine upload --repository testpypi tmp/dist/* --verbose + +deploy: .venv + . .venv/bin/activate ; \ + python3 -m twine upload tmp/dist/* --repository tlogg --verbose + +#test: .venv +# . .venv/bin/activate ; \ +# flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude .venv,build,tmp ; \ +# pytest demo/demo.py diff --git a/apps/tlogg/README.md b/apps/tlogg/README.md new file mode 100644 index 00000000..b6786818 --- /dev/null +++ b/apps/tlogg/README.md @@ -0,0 +1,56 @@ +![Linux](https://img.shields.io/badge/-Linux-grey?logo=linux) +![Usage](https://img.shields.io/badge/Usage-Terminal%20User%20Interface-yellow) +![Python](https://img.shields.io/badge/Python-v3.8%5E-green?logo=python) +![tlogg_version](https://img.shields.io/github/v/tag/ceccopierangiolieugenio/tlogg?label=version) +[![pypi_version](https://img.shields.io/pypi/v/tlogg?label=pypi)](https://pypi.org/project/tlogg) +[![pypi_version](https://img.shields.io/twitter/follow/Pier95886803?style=social&logo=twitter)](https://twitter.com/hashtag/pyTermTk?src=hashtag_click&f=live) + +# tlogg +A fast, advanced [text-based](https://en.wikipedia.org/wiki/Text-based_user_interface) log explorer written in [pyTermTk](https://github.com/ceccopierangiolieugenio/pyTermTk), inspired by [glogg - the fast, smart log explorer](https://github.com/nickbnf/glogg) and [klogg - Faster log explorer](https://klogg.filimonov.dev)(fork of glogg) + +[![screenshot](https://raw.githubusercontent.com/ceccopierangiolieugenio/binaryRepo/master/tlogg/screenshot.003.png)](https://pypi.org/project/tlogg) +## Features +- Search Panel +- Highlight +- Bookmarks +- Shiny ASCII Red Peppers + +[![screenshot](https://raw.githubusercontent.com/ceccopierangiolieugenio/binaryRepo/master/tlogg/demo.001.gif)](https://pypi.org/project/tlogg) + +- _Draggable_ **Tiling tabs** + +[screenshot](https://github.com/ceccopierangiolieugenio/tlogg/assets/8876552/b3db13d9-48b4-485e-bc19-d655021479b6) + +# Install from [pypi](https://pypi.org/project/tlogg) +```bash +pip install tlogg +``` +## Enable the system Clipboard +[pyTermTk](https://github.com/ceccopierangiolieugenio/pyTermTk) automatically support the system clipboard through [pyperclip](https://pypi.org/project/pyperclip/) +```bash +pip install pyperclip +``` +# QuickRun +```bash + $ tlogg -h +usage: tlogg [-h] [-c C] filename [filename ...] + +positional arguments: + filename the filename/s + +optional arguments: + -h, --help show this help message and exit + -c C config folder (default: "/home/user/.config/tlogg") +``` + +# Test +### Clone +```bash +git clone https://github.com/ceccopierangiolieugenio/tlogg.git +cd tlogg +``` +### Run +``` +python3 -m tlogg +``` + diff --git a/apps/tlogg/pyproject.toml b/apps/tlogg/pyproject.toml new file mode 100644 index 00000000..e98499a2 --- /dev/null +++ b/apps/tlogg/pyproject.toml @@ -0,0 +1,46 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "tlogg" +dynamic = ["version"] +readme = {file = "README.md", content-type = "text/markdown"} +authors = [ + {name = "Eugenio Parodi", email = "ceccopierangiolieugenio@googlemail.com"}, +] +description = "A fast, advanced log explorer" +requires-python = ">=3.9" +license = {text = "MIT"} +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Topic :: Terminals", + "Topic :: Software Development :: User Interfaces", +] +dependencies = [ + 'pyTermTk>=0.41.17-a.0', + 'appdirs', + 'copykitten', + 'pyyaml' +] + +[project.urls] +Homepage = "https://github.com/ceccopierangiolieugenio/pyTermTk/tree/main/apps/tlogg" +Repository = "https://github.com/ceccopierangiolieugenio/pyTermTk.git" +Issues = "https://github.com/ceccopierangiolieugenio/pyTermTk/issues" +Changelog = "https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/apps/tlogg/CHANGELOG.md" + +[project.scripts] +tlogg = "tlogg.__main__:main" + +[tool.setuptools] +packages = ["tlogg", "tlogg.app", "tlogg.plugins"] + +[tool.setuptools.dynamic] +version = {attr = "tlogg.__version__"} diff --git a/apps/tlogg/tlogg/__init__.py b/apps/tlogg/tlogg/__init__.py new file mode 100755 index 00000000..1c5a9cc6 --- /dev/null +++ b/apps/tlogg/tlogg/__init__.py @@ -0,0 +1,27 @@ +# 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. + +__version__:str = '0.2.7-a.3' + +from .plugin import TloggPlugin +from .proxy import tloggProxy, TloggViewerProxy +from .helper import * diff --git a/apps/tlogg/tlogg/__main__.py b/apps/tlogg/tlogg/__main__.py new file mode 100644 index 00000000..22bab16d --- /dev/null +++ b/apps/tlogg/tlogg/__main__.py @@ -0,0 +1,28 @@ +#!/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 .app import main + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/apps/tlogg/tlogg/app/__init__.py b/apps/tlogg/tlogg/app/__init__.py new file mode 100644 index 00000000..09223734 --- /dev/null +++ b/apps/tlogg/tlogg/app/__init__.py @@ -0,0 +1,28 @@ + +#!/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 .cfg import * +from .glbl import * +from .main import * \ No newline at end of file diff --git a/apps/tlogg/tlogg/app/about.py b/apps/tlogg/tlogg/app/about.py new file mode 100644 index 00000000..2cb0e34f --- /dev/null +++ b/apps/tlogg/tlogg/app/about.py @@ -0,0 +1,64 @@ +#!/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. + +__all__ = ['About'] + +from TermTk.TTkCore.log import TTkLog +from TermTk.TTkCore.color import TTkColor +from TermTk import TTkAbout, TTkWindow +from .cfg import TloggCfg + +class About(TTkAbout): + tlogg = [ + " __ ___ ", + "/\ \__/\_ \ ", + "\ \ ,_\//\ \ ___ __ __ ", + " \ \ \/ \ \ \ / __`\ /'_ `\ /'_ `\ ", + " \ \ \_ \_\ \_/\ \L\ \/\ \L\ \/\ \L\ \ ", + " \ \__\/\____\ \____/\ \____ \ \____ \ ", + " \/__/\/____/\/___/ \/___L\ \/___L\ \\", + " /\____/ /\____/", + " \_/__/ \_/__/ "] + + __slots__=('_image') + def __init__(self, *args, **kwargs): + TTkAbout.__init__(self, *args, **kwargs) + self._name = kwargs.get('name' , 'About' ) + self.setTitle('[PierCecco Cecco] Eugenio Parodi proudly presents...') + self.resize(56,16) + + + + def paintEvent(self, canvas): + c = [0xFF,0xFF,0xFF] + for y, line in enumerate(About.tlogg): + canvas.drawText(pos=(13,3+y),text=line, color=TTkColor.fg(f'#{c[0]:02X}{c[1]:02X}{c[2]:02X}')) + c[2]-=0x18 + c[0]-=0x08 + canvas.drawText(pos=(26,4),text=f" Version: {TloggCfg.version}", color=TTkColor.fg('#AAAAFF')) + canvas.drawText(pos=(14,11),text=f"Powered By, pyTermTk") + canvas.drawText(pos=(2,13),text=f"https://github.com/ceccopierangiolieugenio/tlogg", color=TTkColor.fg('#44FFFF')) + canvas.drawText(pos=(2,14),text=f"https://github.com/ceccopierangiolieugenio/pyTermTk", color=TTkColor.fg('#44FFFF')) + + TTkWindow.paintEvent(self, canvas) diff --git a/apps/tlogg/tlogg/app/cfg.py b/apps/tlogg/tlogg/app/cfg.py new file mode 100644 index 00000000..dcf05649 --- /dev/null +++ b/apps/tlogg/tlogg/app/cfg.py @@ -0,0 +1,81 @@ +#!/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. + +__all__ = ['TloggCfg'] + +import os +import yaml + +from .. import __version__ + +class TloggCfg: + version=__version__ + name="tlogg" + cfgVersion = '1.0' + pathCfg="." + colors=[] + filters=[] + options={} + searches=[] + maxsearches=200 + + @staticmethod + def save(searches=True, filters=True, colors=True, options=True): + os.makedirs(TloggCfg.pathCfg, exist_ok=True) + colorsPath = os.path.join(TloggCfg.pathCfg,'colors.yaml') + filtersPath = os.path.join(TloggCfg.pathCfg,'filters.yaml') + optionsPath = os.path.join(TloggCfg.pathCfg,'options.yaml') + searchesPath = os.path.join(TloggCfg.pathCfg,'searches.yaml') + + def writeCfg(path, cfg): + fullCfg = { + 'version':TloggCfg.cfgVersion, + 'cfg':cfg } + with open(path, 'w') as f: + yaml.dump(fullCfg, f, sort_keys=False, default_flow_style=False) + + if colors: writeCfg(colorsPath, TloggCfg.colors) + if filters: writeCfg(filtersPath, TloggCfg.filters) + if options: writeCfg(optionsPath, TloggCfg.options) + if searches: writeCfg(searchesPath, TloggCfg.searches) + + @staticmethod + def load(): + colorsPath = os.path.join(TloggCfg.pathCfg,'colors.yaml') + filtersPath = os.path.join(TloggCfg.pathCfg,'filters.yaml') + optionsPath = os.path.join(TloggCfg.pathCfg,'options.yaml') + searchesPath = os.path.join(TloggCfg.pathCfg,'searches.yaml') + + if os.path.exists(colorsPath): + with open(colorsPath) as f: + TloggCfg.colors = yaml.load(f, Loader=yaml.SafeLoader)['cfg'] + if os.path.exists(filtersPath): + with open(filtersPath) as f: + TloggCfg.filters = yaml.load(f, Loader=yaml.SafeLoader)['cfg'] + if os.path.exists(optionsPath): + with open(optionsPath) as f: + TloggCfg.options = yaml.load(f, Loader=yaml.SafeLoader)['cfg'] + if os.path.exists(searchesPath): + with open(searchesPath) as f: + TloggCfg.searches = yaml.load(f, Loader=yaml.SafeLoader)['cfg'] diff --git a/apps/tlogg/tlogg/app/filetree.py b/apps/tlogg/tlogg/app/filetree.py new file mode 100644 index 00000000..703686ec --- /dev/null +++ b/apps/tlogg/tlogg/app/filetree.py @@ -0,0 +1,123 @@ +#!/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 soself._ftware and associated documentation files (the "Soself._ftware"), to deal +# in the Soself._ftware without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Soself._ftware, and to permit persons to whom the Soself._ftware 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 Soself._ftware. +# +# THE SOself._FTWARE 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 SOself._FTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOself._FTWARE. + +import os + +from TermTk import TTkK +from TermTk import TTkWidget, TTkCheckbox, TTkButton, TTkSpacer, TTkFileTree, TTkFileTreeWidgetItem +from TermTk import TTkVBoxLayout, TTkHBoxLayout +from TermTk import pyTTkSlot, pyTTkSignal + +from . import TloggCfg + +class FileTree(TTkWidget): + __slots__ = ( + '_recentPath', '_recentPathId', + '_controlsLayout', + '_ftCbFollow', '_ftBtnPrev', '_ftBtnNext', '_ftBtnUp', '_fileTree', + # Forwarded Methods + 'fileActivated') + + def __init__(self, *args, **kwargs): + TTkWidget.__init__(self, *args, **kwargs) + self._name = kwargs.get('name' , 'FileTree' ) + + self.setLayout(TTkVBoxLayout()) + + self._controlsLayout = TTkHBoxLayout() + self.layout().addItem(self._controlsLayout) + + self._ftCbFollow = TTkCheckbox(text="Follow", maxWidth=9, checked=False) + self._ftBtnPrev = TTkButton(text="<",maxWidth=3, enabled=False) + self._ftBtnNext = TTkButton(text=">",maxWidth=3, enabled=False) + self._ftBtnUp = TTkButton(text="^",maxWidth=3, enabled=True) + self._controlsLayout.addWidget(self._ftCbFollow) + self._controlsLayout.addWidget(TTkSpacer()) + self._controlsLayout.addWidget(self._ftBtnPrev) + self._controlsLayout.addWidget(self._ftBtnNext) + self._controlsLayout.addWidget(self._ftBtnUp) + + self._fileTree = TTkFileTree(parent=self, path=".") + + self._recentPath = [] + self._recentPathId = -1 + self._addFolder(os.path.abspath('.')) + + self._ftBtnPrev.clicked.connect(self._openPrev) + self._ftBtnNext.clicked.connect(self._openNext) + self._ftBtnUp.clicked.connect(self._openUp) + + self._fileTree.folderActivated.connect(self._openFolder) + + # Forward Functions + self.fileActivated = self._fileTree.fileActivated + + def follow(self, path): + if self._ftCbFollow.checkState() == TTkK.Checked: + if os.path.isfile(path): + path, _ = os.path.split(path) + self._fileTree.openPath(path) + + def _addFolder(self, dir): + if not self._recentPath or dir != self._recentPath[-1]: + self._recentPath.append(dir) + self._recentPathId = len(self._recentPath)-1 + if self._recentPathId: + self._ftBtnPrev.setEnabled() + self._ftBtnNext.setDisabled() + + @pyTTkSlot(TTkFileTreeWidgetItem) + def _openFolder(self, dir): + self._fileTree.openPath(dir.path()) + self._addFolder(dir.path()) + + @pyTTkSlot() + def _openPrev(self): + if self._recentPathId<=0 or self._recentPathId>=len(self._recentPath): + self._ftBtnPrev.setDisabled() + return + self._recentPathId -= 1 + self._fileTree.openPath(self._recentPath[self._recentPathId]) + if self._recentPathId<=0: + self._ftBtnPrev.setDisabled() + self._ftBtnNext.setEnabled() + + @pyTTkSlot() + def _openNext(self): + if self._recentPathId<0 or self._recentPathId>=len(self._recentPath)-1: + self._ftBtnNext.setDisabled() + return + self._recentPathId += 1 + self._fileTree.openPath(self._recentPath[self._recentPathId]) + if self._recentPathId>=len(self._recentPath)-1: + self._ftBtnNext.setDisabled() + self._ftBtnPrev.setEnabled() + + @pyTTkSlot() + def _openUp(self): + path = os.path.abspath(self._fileTree.getOpenPath()) + path, e = os.path.split(path) + if e: + self._addFolder(path) + self._fileTree.openPath(path) diff --git a/apps/tlogg/tlogg/app/fileviewer.py b/apps/tlogg/tlogg/app/fileviewer.py new file mode 100644 index 00000000..d58ba1c7 --- /dev/null +++ b/apps/tlogg/tlogg/app/fileviewer.py @@ -0,0 +1,307 @@ +# 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. + +__all__ = ['FileViewer','FileViewerSearch','FileViewerArea'] + +import TermTk as ttk + +from tlogg import TloggHelper, tloggProxy + +from . import TloggCfg + +class FileViewer(ttk.TTkAbstractScrollView): + __slots__ = ( + '_fileBuffer', '_indexesMark', '_indexesSearched', + '_selected', '_indexing', '_searchRe', + '_selection', '_pressed' + # Signals + 'selected', 'marked') + def __init__(self, *args, **kwargs): + self._indexesMark = [] + self._indexesSearched = [] + self._indexing = None + self._selected = -1 + self._selection = None + self._pressed = False + self._searchRe = "" + # Signals + self.selected = ttk.pyTTkSignal(int) + self.marked = ttk.pyTTkSignal(list) + self._fileBuffer = kwargs.get('filebuffer') + super().__init__(*args, **kwargs) + self.viewChanged.connect(self._viewChangedHandler) + self.setFocusPolicy(ttk.TTkK.ClickFocus) + + @ttk.pyTTkSlot() + def _viewChangedHandler(self): + self.update() + + def _copy(self): + clipboard = ttk.TTkClipboard() + lines = ttk.TTkString().join([ + self.getLine(i) for i in range(min(self._selection),max(self._selection)+1) ]) + clipboard.setText(lines) + + @ttk.pyTTkSlot(float) + def fileIndexing(self, percentage): + self._indexing = percentage + self.viewChanged.emit() + + @ttk.pyTTkSlot() + def fileIndexed(self): + self._indexing = None + self.viewChanged.emit() + + def markIndexes(self, indexes): + self._indexesMark = indexes + self.viewChanged.emit() + + def searchedIndexes(self, indexes): + self._indexesSearched = indexes + self.viewChanged.emit() + + def searchRe(self, searchRe): + self._searchRe = searchRe + self.update() + + def viewFullAreaSize(self) -> (int, int): + w = 10+self._fileBuffer.getWidth() + h = self._fileBuffer.getLen() + return ( w , h ) + + def viewDisplayedSize(self) -> (int, int): + return self.size() + + def keyEvent(self, evt): + # Enable CTRL+C = Copy + if ( evt.type == ttk.TTkK.SpecialKey and + evt.mod == ttk.TTkK.ControlModifier and + evt.key == ttk.TTkK.Key_C ): + self._copy() + + def mousePressEvent(self, evt): + x,y = evt.x, evt.y + ox,oy = self.getViewOffsets() + index = oy+y + self._selection = [index,index] + self._pressed = True + if 0 <= index < self._fileBuffer.getLen(): + if x<3: + if index in self._indexesMark: + self._indexesMark.pop(self._indexesMark.index(index)) + else: + self._indexesMark.append(index) + self.marked.emit(self._indexesMark) + else: + self._selected = index + self.selected.emit(self._selected) + tloggProxy.lineSelected.emit(self.getLine(self._selected)) + self.update() + return True + return super().mousePressEvent(evt) + + def mouseDragEvent(self, evt) -> bool: + if self._pressed and self._selection: + w,h = self.size() + x,y = evt.x, evt.y + lines=self._fileBuffer.getLen() + ox,oy = self.getViewOffsets() + index = oy+y + self._selection[1] = min(max(0,index),lines-1) + if y < 0: + self.viewMoveTo(ox, max(0,min(oy+min(y,3),lines-h))) + elif y >= h: + self.viewMoveTo(ox, max(0,min(oy+min(y-h,3),lines-h))) + else: + self.update() + return super().mouseDragEvent(evt) + + def mouseReleaseEvent(self, evt) -> bool: + self._pressed = False + if self._selection and self._selection[0] != self._selection[1]: + self._copy() + self.update() + return super().mouseReleaseEvent(evt) + + @ttk.pyTTkSlot(int) + def selectAndMove(self, line): + self._selected = line + self._selection = [line,line] + ox,_ = self.getViewOffsets() + self.viewMoveTo(ox, max(0,line-self.height()//2)) + self.update() + + def getLen(self): + return self._fileBuffer.getLen() + + def getLine(self, num) -> str: + return self._fileBuffer.getLine(num) + + def getLineNum(self, num) -> int: + return num + + def paintEvent(self, canvas): + ox,oy = self.getViewOffsets() + bufferLen = self.getLen() + for i in range(min(self.height(),bufferLen-oy)): + line = ttk.TTkString(self.getLine(i+oy).replace('\n','')).tab2spaces() + lineNum = self.getLineNum(i+oy) + if lineNum in self._indexesMark: + symbolcolor = ttk.TTkColor.fg("#00ffff") + numberColor = ttk.TTkColor.bg("#444444") + symbol='❥' + elif lineNum in self._indexesSearched: + symbolcolor = ttk.TTkColor.fg("#ff0000") + numberColor = ttk.TTkColor.bg("#444444") + symbol='●' + else: + symbolcolor = ttk.TTkColor.fg("#0000ff") + numberColor = ttk.TTkColor.bg("#444444") + symbol='○' + + if i+oy == self._selected: + selectedColor = ttk.TTkColor.bg("#008844") + searchedColor = ttk.TTkColor.fg("#FFFF00")+ttk.TTkColor.bg("#004400") + line = line.setColor(selectedColor) + elif self._selection and min(self._selection) <= i+oy <= max(self._selection): + selectedColor = ttk.TTkColor.bg("#008888") + searchedColor = ttk.TTkColor.fg("#FFFF00")+ttk.TTkColor.bg("#004400") + line = line.setColor(selectedColor) + else: + selectedColor = ttk.TTkColor.RST + searchedColor = ttk.TTkColor.fg("#000000")+ttk.TTkColor.bg("#AAAAAA") + # Check in the filters a matching color + for color in TloggCfg.colors: + #TTkLog.debug(f"{color['pattern']} - {line}") + if m := line.findall(regexp=color['pattern'], ignoreCase=color['ignorecase']): + selectedColor = ttk.TTkColor.fg(color['fg'])+ttk.TTkColor.bg(color['bg']) + searchedColor = ttk.TTkColor.fg(color['bg'])+ttk.TTkColor.bg(color['fg']) + line = line.setColor(selectedColor) + break + if self._searchRe: + if m := line.findall(regexp=self._searchRe, ignoreCase=True): + for match in m: + line = line.setColor(searchedColor, match=match) + + # Add Line Number + lenLineNumber = len(str(self.getLineNum(bufferLen-1))) + lineNumber = ttk.TTkString() + numberColor + str(lineNum).rjust(lenLineNumber) + ttk.TTkColor.RST + ' ' + # Compose print line + printLine = ttk.TTkString() + symbolcolor + symbol + ttk.TTkColor.RST + ' ' + lineNumber + line.substring(ox) + # stupid scramble + # printLine._text = ''.join([chr(121-(ord(l)-65)) if (65<=ord(l)<=121) else l for l in printLine._text]) + canvas.drawText(pos=(0,i), text=printLine, color=selectedColor, width=self.width(), ) + + # Draw the loading banner + if self._indexing is not None: + canvas.drawText(pos=(0,0), text=f" [ Indexed: {int(100*self._indexing)}% ] ") + +class FileViewerSearch(FileViewer): + __slots__ = ('_indexes') + def __init__(self, *args, **kwargs): + self._indexes = [] + FileViewer.__init__(self, *args, **kwargs) + self._name = kwargs.get('name' , 'FileViewerSearch' ) + + def markIndexes(self, indexes): + self._indexesMark = indexes + self._indexes = [i for i in sorted(set(self._indexesSearched+self._indexesMark))] + ox,oy = self.getViewOffsets() + self.viewMoveTo(ox,oy) + self.update() + + def searchedIndexes(self, indexes): + # Get the current lineSelected to be used to scroll + # the search viewer to the similar to previous position + lineSelected = -1 + if self._selected > -1: + lineSelected = self._indexes[self._selected] + + self._indexesSearched = indexes + self._indexes = [i for i in sorted(set(self._indexesSearched+self._indexesMark))] + ox,_ = self.getViewOffsets() + + lineToMove = 0 + if lineSelected > -1: + for i, lineNum in enumerate(self._indexes): + if lineNum >= lineSelected: + if lineNum == lineSelected: + self._selected = i + else: + self._selected = -1 + # Try to keep the selected area at the center of the widget + lineToMove = i if i <= self.height()/2 else int(i-self.height()/2) + break + + self.viewMoveTo(ox, lineToMove) + self.update() + + def viewFullAreaSize(self) -> (int, int): + if self._indexes is None: + w = 2+self._fileBuffer.getWidth() + h = self._fileBuffer.getLen() + else: + w = 2+self._fileBuffer.getWidth(self._indexes) + h = len(self._indexes) + return w , h + + def mousePressEvent(self, evt): + x,y = evt.x, evt.y + ox,oy = self.getViewOffsets() + index = oy+y + self._selection = [index,index] + self._pressed = True + if 0 <= index < len(self._indexes): + if x<3: + index = self._indexes[oy+y] + if index in self._indexesMark: + self._indexesMark.pop(self._indexesMark.index(index)) + else: + self._indexesMark.append(index) + self.markIndexes(self._indexesMark) + self.marked.emit(self._indexesMark) + else: + self._selected = index + self.selected.emit(self._indexes[self._selected]) + tloggProxy.lineSelected.emit(self.getLine(self._selected)) + self.update() + return True + return False + + def getLen(self): + return len(self._indexes) + + def getLine(self, num) -> str: + return self._fileBuffer.getLine(self._indexes[num]) + + def getLineNum(self, num) -> int: + return self._indexes[num] + +class FileViewerArea(ttk.TTkAbstractScrollArea): + __slots__ = ('_fileView') + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._name = kwargs.get('name' , 'FileViewer' ) + if 'parent' in kwargs: kwargs.pop('parent') + self._fileView = kwargs.get('fileView') + self.setFocusPolicy(ttk.TTkK.ClickFocus) + self.setViewport(self._fileView) diff --git a/apps/tlogg/tlogg/app/glbl.py b/apps/tlogg/tlogg/app/glbl.py new file mode 100644 index 00000000..b065decd --- /dev/null +++ b/apps/tlogg/tlogg/app/glbl.py @@ -0,0 +1,36 @@ +#!/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. + +__all__ = ['TloggGlbl'] + +class TloggGlbl: + _refViews = [] + @staticmethod + def addRefView(view): + TloggGlbl._refViews.append(view) + + @staticmethod + def refreshViews(): + for view in TloggGlbl._refViews: + view.update() \ No newline at end of file diff --git a/apps/tlogg/tlogg/app/highlighters.py b/apps/tlogg/tlogg/app/highlighters.py new file mode 100644 index 00000000..a809ba94 --- /dev/null +++ b/apps/tlogg/tlogg/app/highlighters.py @@ -0,0 +1,164 @@ +#!/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. + +__all__ = ['highlightersFormLayout'] + +import copy + +from . import TloggCfg, TloggGlbl + +from TermTk import * + +def highlightersFormLayout(win): + ''' + This form is inspired by glogg "Filters..." form + ''' + + colors = copy.deepcopy(TloggCfg.colors) + + leftRightLayout = TTkHBoxLayout() + leftLayout = TTkGridLayout() + rightLayout = TTkGridLayout() + bottomRightLayout = TTkGridLayout() + leftRightLayout.addItem(leftLayout) + leftRightLayout.addItem(rightLayout) + + frameColors = TTkFrame(border=True, layout=TTkGridLayout()) + leftLayout.addWidget(frameColors,0,0,1,5) + + listColors = TTkList(parent=frameColors, dragDropMode=TTkK.DragDropMode.AllowDragDrop) + + addButton = TTkButton(text="+",maxWidth=3) + removeButton = TTkButton(text="-",maxWidth=3) + upButton = TTkButton(text="▲",maxWidth=3) + downButton = TTkButton(text="▼",maxWidth=3) + + leftLayout.addWidget(addButton ,1,0) + leftLayout.addWidget(removeButton ,1,1) + leftLayout.addWidget(TTkSpacer() ,1,2) + leftLayout.addWidget(upButton ,1,3) + leftLayout.addWidget(downButton ,1,4) + + rightLayout.addWidget(TTkLabel(text="Pattern:"),0,0) + rightLayout.addWidget(pattern:=TTkLineEdit(text="-----"),0,1) + rightLayout.addWidget(TTkLabel(text="Ignore Case:"),1,0) + rightLayout.addWidget(ignoreCase:=TTkCheckbox(),1,1) + rightLayout.addWidget(TTkLabel(text="FG Color:"),2,0) + rightLayout.addWidget(fgColor := TTkColorButtonPicker(border=False, color=TTkColor.fg('#eeeeee') ),2,1) + rightLayout.addWidget(TTkLabel(text="BG Color:"),3,0) + rightLayout.addWidget(bgColor := TTkColorButtonPicker(border=False, color=TTkColor.bg('#333333') ),3,1) + rightLayout.addWidget(TTkSpacer() ,4,0,1,2) + + rightLayout.addItem(bottomRightLayout ,5,0,1,2) + bottomRightLayout.addWidget(applyBtn := TTkButton(text="Apply", border=True, maxHeight=3),0,1) + bottomRightLayout.addWidget(cancelBtn := TTkButton(text="Cancel", border=True, maxHeight=3),0,2) + bottomRightLayout.addWidget(okBtn := TTkButton(text="OK", border=True, maxHeight=3),0,3) + + def _move(offset): + def _moveUpDown(): + if items := listColors.selectedItems(): + item = items[0] + index = listColors.indexOf(item) + listColors.moveItem(index,index+offset) + return _moveUpDown + upButton.clicked.connect(_move(-1)) + downButton.clicked.connect(_move(1)) + + def _addCallback(): + color = {'pattern':"", 'ignorecase':True, 'fg':"#FFFFFF", 'bg':"#000000" } + colors.append(color) + listColors.addItem(item=color['pattern'],data=color) + addButton.clicked.connect(_addCallback) + + def _removeCallback(): + # Clear all the signals + pattern.textEdited.clear() + ignoreCase.clicked.clear() + fgColor.colorSelected.clear() + bgColor.colorSelected.clear() + if items := listColors.selectedItems(): + colors.remove(items[0].data) + listColors.removeItem(items[0]) + removeButton.clicked.connect(_removeCallback) + + def _saveColors(): + colors = [] + for item in listColors.items(): + colors.append(item.data()) + TloggCfg.colors = colors + TloggCfg.save(searches=False, filters=False, colors=True, options=False) + TloggGlbl.refreshViews() + # TTkHelper.updateAll() + + applyBtn.clicked.connect(_saveColors) + okBtn.clicked.connect(_saveColors) + okBtn.clicked.connect(win.close) + cancelBtn.clicked.connect(win.close) + + @ttk.pyTTkSlot(TTkAbstractListItem) + def _listCallback(item): + if color:=item.data(): + # Clear all the signals + pattern.textEdited.clear() + ignoreCase.clicked.clear() + fgColor.colorSelected.clear() + bgColor.colorSelected.clear() + # Config the color widgets + pattern.setText(color['pattern']) + ignoreCase.setCheckState(TTkK.Checked if color['ignorecase'] else TTkK.Unchecked) + fgColor.setColor(TTkColor.bg(color['fg'])) + bgColor.setColor(TTkColor.bg(color['bg'])) + # Connect the actions + ## Pattern Line Edit + @pyTTkSlot(str) + def _setPattern(p:TTkString): + item.setText(str(p)) + color['pattern'] = str(p) + pattern.textEdited.connect(_setPattern) + ## Case Sensitivity checkbox + def _setCase(c):color['ignorecase'] = c + ignoreCase.clicked.connect(_setCase) + ## Color Button + def _setFg(c):color['fg'] = c.getHex(TTkK.Background) + def _setBg(c):color['bg'] = c.getHex(TTkK.Background) + fgColor.colorSelected.connect(_setFg) + bgColor.colorSelected.connect(_setBg) + + + listColors.itemClicked.connect(_listCallback) + + for i,color in enumerate(colors): + # ali = TTkAbstractListItem(text=color['pattern'],data=color) + listColors.addItem(item=color['pattern'],data=color) + + return leftRightLayout + +def highlightersForm(root=None): + preferencesLayout = TTkGridLayout(columnMinWidth=1) + frame = TTkFrame(parent=root, layout=preferencesLayout, border=0) + + frameColors = TTkFrame(border=True, title="Highlighters...", layout=highlightersFormLayout()) + preferencesLayout.addWidget(frameColors,0,0) + + return frame \ No newline at end of file diff --git a/apps/tlogg/tlogg/app/loggwidget.py b/apps/tlogg/tlogg/app/loggwidget.py new file mode 100644 index 00000000..deaa604b --- /dev/null +++ b/apps/tlogg/tlogg/app/loggwidget.py @@ -0,0 +1,106 @@ +# MIT License +# +# Copyright (c) 2023 Eugenio Parodi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +__all__ = ['LoggWidget'] + +import TermTk as ttk + +from .cfg import TloggCfg +from .glbl import TloggGlbl +from .fileviewer import FileViewer, FileViewerArea, FileViewerSearch +from .predefinedfilters import PredefinedFilters + +class LoggWidget(ttk.TTkSplitter): + __slots__ = ('_btn_filters', '_bls_label_1', '_bls_cb_icase', '_bls_search', '_bls_searchbox', + '_topViewport', '_bottomViewport', + '_fileBuffer') + def __init__(self, filename, *args, **kwargs): + super().__init__(*args, **kwargs|{'orientation':ttk.TTkK.VERTICAL}) + + topFrame = ttk.TTkFrame(parent=self, border=False, layout=ttk.TTkVBoxLayout()) + bottomFrame = ttk.TTkFrame(parent=self, border=False, layout=ttk.TTkVBoxLayout()) + + + # Define the bottom layout widgets + bottomLayoutSearch = ttk.TTkHBoxLayout() + self._btn_filters = ttk.TTkButton(text="Filters ^", maxWidth=11) + self._bls_label_1 = ttk.TTkLabel(text=" Txt:", maxWidth=5) + self._bls_cb_icase = ttk.TTkCheckbox(text="Aa", maxWidth=5, checked=True) + self._bls_search = ttk.TTkButton(text="Search", maxWidth=10) + self._bls_searchbox = ttk.TTkComboBox(editable=True) + self._bls_searchbox.addItems(TloggCfg.searches) + self._bls_searchbox.setCurrentIndex(0) + + bottomLayoutSearch.addWidget(self._btn_filters) + bottomLayoutSearch.addWidget(self._bls_label_1) + bottomLayoutSearch.addWidget(self._bls_searchbox) + bottomLayoutSearch.addWidget(self._bls_cb_icase) + bottomLayoutSearch.addWidget(self._bls_search) + + bottomFrame.layout().addItem(bottomLayoutSearch) + + # Define the main file Viewer + self._fileBuffer = ttk.TTkFileBuffer(filename, 0x100, 0x1000) + self._topViewport = FileViewer(filebuffer=self._fileBuffer) + topViewer = FileViewerArea(parent=topFrame, fileView=self._topViewport) + self._fileBuffer.indexUpdated.connect(self._topViewport.fileIndexing) + self._fileBuffer.indexed.connect(self._topViewport.fileIndexed) + # Define the Search Viewer + self._bottomViewport = FileViewerSearch(filebuffer=self._fileBuffer) + bottomViewer = FileViewerArea(parent=bottomFrame, fileView=self._bottomViewport) + self._bottomViewport.selected.connect(self._topViewport.selectAndMove) + self._bottomViewport.marked.connect(self._topViewport.markIndexes) + self._topViewport.marked.connect(self._bottomViewport.markIndexes) + + # Add those viewpoers to the global list to allow dynamic refresh + # TODO: Try to get rid of this + TloggGlbl.addRefView(self._topViewport) + TloggGlbl.addRefView(self._bottomViewport) + + self._bls_search.clicked.connect(self._search) + self._bls_searchbox.editTextChanged.connect(self._search) + + def _openPredefinedFilters(): + ttk.TTkHelper.overlay(self._btn_filters, PredefinedFilters(self._bls_searchbox), -2, 1) + self._btn_filters.clicked.connect(_openPredefinedFilters) + + @ttk.pyTTkSlot() + def _search(self): + searchtext = str(self._bls_searchbox.currentText()) + ttk.TTkLog.debug(f"{searchtext=}") + indexes = self._fileBuffer.searchRe(searchtext, ignoreCase=self._bls_cb_icase.checkState() == ttk.TTkK.Checked) + self._bottomViewport.searchedIndexes(indexes) + self._bottomViewport.searchRe(searchtext) + self._topViewport.searchedIndexes(indexes) + self._topViewport.searchRe(searchtext) + if TloggCfg.searches: + x = set(TloggCfg.searches) + ttk.TTkLog.debug(f"{x}") + TloggCfg.searches = list(x) + if searchtext in TloggCfg.searches: + TloggCfg.searches.remove(searchtext) + TloggCfg.searches.insert(0, searchtext) + TloggCfg.searches = TloggCfg.searches[:TloggCfg.maxsearches] + TloggCfg.save(searches=True,filters=False) + self._bls_searchbox.clear() + self._bls_searchbox.addItems(TloggCfg.searches) + self._bls_searchbox.setCurrentIndex(0) diff --git a/apps/tlogg/tlogg/app/main.py b/apps/tlogg/tlogg/app/main.py new file mode 100644 index 00000000..a1c606be --- /dev/null +++ b/apps/tlogg/tlogg/app/main.py @@ -0,0 +1,206 @@ +# 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 re +import sys +import argparse + +import appdirs + +from TermTk import * + +from tlogg import TloggHelper, tloggProxy + +from .cfg import * +from .glbl import * +from .about import * +from .loggwidget import LoggWidget +from .options import optionsFormLayout, optionsLoadTheme +from .highlighters import highlightersFormLayout +from .predefinedfilters import PredefinedFiltersFormWindow +from .notepad import NotePad + + +class TLOGG(TTkGridLayout): + ''' + ┌──────────────────[Main Splitter]─────────┐ + │┌──────────╥────[File Tab Splitter]──────┐│ + ││ F.Tree ║ Tab ││ + ││ Controls ╟─────────────────────────────┤│ + │├──────────╢┌───────────────────────────┐││ + ││ File ║│ File Viewer │││ + ││ Tree ║│ │││ + ││ ║╞═══════════════════════════╡││ + ││ ║│ Search Widget │││ + ││ ║│ │││ + ││ ║└───────────────────────────┘││ + │└──────────╨─────────────────────────────┘│ + ╞══════════════════════════════════════════╡ + │ Logger,Debug View │ + └──────────────────────────────────────────┘ + ''' + __slots__ = ('_kodeTab', '_tloggProxy','_notepad') + def __init__(self, tloggProxy, *args, **kwargs) -> None: + self._tloggProxy = tloggProxy + self._notepad = NotePad() + + super().__init__(*args, **kwargs) + + self._tloggProxy.setOpenFile(self.openFile) + + self.addWidget(appTemplate:=TTkAppTemplate(border=False)) + + self._kodeTab = TTkKodeTab(border=False, closable=True) + + appTemplate.setMenuBar(appMenuBar:=TTkMenuBarLayout(), TTkAppTemplate.LEFT) + fileMenu = appMenuBar.addMenu("&File") + buttonOpen = fileMenu.addMenu("&Open") + buttonClose = fileMenu.addMenu("&Close") + fileMenu.addSpacer() + buttonColors = fileMenu.addMenu("C&olors...") + buttonFilters = fileMenu.addMenu("&Filters...") + buttonOptions = fileMenu.addMenu("O&ptions...") + fileMenu.addSpacer() + buttonExit = fileMenu.addMenu("E&xit") + buttonExit.menuButtonClicked.connect(TTkHelper.quit) + + extraMenu = appMenuBar.addMenu("E&xtra") + extraMenu.addMenu("Scratchpad").menuButtonClicked.connect(self.scratchpad) + extraMenu.addSpacer() + + helpMenu = appMenuBar.addMenu("&Help", alignment=TTkK.RIGHT_ALIGN) + helpMenu.addMenu("About ...").menuButtonClicked.connect(self.showAbout) + helpMenu.addMenu("About tlogg").menuButtonClicked.connect(self.showAboutTlogg) + + def _tabChanged(_,__,widget,data): + tloggProxy.tloggFocussed.emit(None, data) + + self._kodeTab.currentChanged.connect(_tabChanged) + + # fileTree.fileActivated.connect(lambda x: self.openFile(x.path())) + + buttonOpen.menuButtonClicked.connect(self.openFileCallback) + buttonColors.menuButtonClicked.connect(self.showColors) + buttonFilters.menuButtonClicked.connect(self.showFilters) + buttonOptions.menuButtonClicked.connect(self.showOptions ) + + for mod in TloggHelper._getPlugins(): + if mod.position: + appTemplate.setWidget(mod.widget, mod.position, 30) + if mod.menu: + _menu = extraMenu.addMenu(mod.name, checkable=True, checked=mod.visible) + mod.widget.setVisible(mod.visible) + _menu.toggled.connect(mod.widget.setVisible) + + + appTemplate.setWidget(self._kodeTab, TTkAppTemplate.MAIN) + appTemplate.setWidget(TTkLogViewer(),TTkAppTemplate.BOTTOM,size=1,title="Logs") + + @pyTTkSlot() + def scratchpad(self): + win = TTkWindow( + title="Mr Scratchpad", + size=(80,30), + layout=self._notepad, + flags=TTkK.WindowFlag.WindowMaximizeButtonHint|TTkK.WindowFlag.WindowCloseButtonHint) + TTkHelper.overlay(None, win, 2, 2, toolWindow=True) + + @pyTTkSlot(TTkMenuButton) + def openFileCallback(self, btn): + filePicker = TTkFileDialogPicker( + pos = (3,3), size=(90,30), + caption="Open a File", path=".", + filter="All Files (*);;Text Files (*.txt);;Log Files (*.log)") + filePicker.filePicked.connect(self.openFile) + TTkHelper.overlay(btn, filePicker, 10, 2, modal=True) + + @pyTTkSlot(TTkMenuButton) + def showColors(self, btn): + win = TTkWindow(title="Highlighters...", size=(70,20), border=True) + win.setLayout(highlightersFormLayout(win)) + TTkHelper.overlay(btn, win, 10,2, modal=True) + + @pyTTkSlot(TTkMenuButton) + def showFilters(self, btn): + win = PredefinedFiltersFormWindow(title="Predefined Filters...", size=(70,20), border=True) + TTkHelper.overlay(btn, win, 10,2, modal=True) + + @pyTTkSlot(TTkMenuButton) + def showOptions(self, btn): + win = TTkWindow(title="Options...", size=(70,20), border=True) + win.setLayout(optionsFormLayout(win)) + TTkHelper.overlay(btn, win, 10,2, modal=True) + + # tab.addTab(h + # @pyTTkSlot()ighlightersForm(), "-Setup-") + @pyTTkSlot(TTkMenuButton) + def showAbout(self, btn): + TTkHelper.overlay(btn, TTkAbout(), 20,5) + + @pyTTkSlot(TTkMenuButton) + def showAboutTlogg(self, btn): + TTkHelper.overlay(btn, About(), 20,5) + + def openFile(self, file): + # openedFiles.append(file) + loggWidget = LoggWidget(file) + self._kodeTab.addTab(widget=loggWidget, label=os.path.basename(file), data=file) + self._kodeTab.setCurrentWidget(loggWidget) + +def main(): + TloggCfg.pathCfg = appdirs.user_config_dir("tlogg") + + parser = argparse.ArgumentParser() + # parser.add_argument('-f', help='Full Screen', action='store_true') + parser.add_argument('-c', help=f'config folder (default: "{TloggCfg.pathCfg}")', default=TloggCfg.pathCfg) + parser.add_argument('filename', type=str, nargs='*', + help='the filename/s') + args = parser.parse_args() + + # TTkLog.use_default_file_logging() + + TloggCfg.pathCfg = args.c + TTkLog.debug(f"Config Path: {TloggCfg.pathCfg}") + + TloggCfg.load() + + if 'theme' not in TloggCfg.options: + TloggCfg.options['theme'] = 'UTF8' + optionsLoadTheme(TloggCfg.options['theme']) + + TloggHelper._loadPlugins() + + root = TTk( + title="tlogg", + layout=(tlogg:=TLOGG(tloggProxy=tloggProxy)), + sigmask=( + TTkTerm.Sigmask.CTRL_C | + TTkTerm.Sigmask.CTRL_Q | + TTkTerm.Sigmask.CTRL_S | + TTkTerm.Sigmask.CTRL_Z )) + TloggHelper._runPlugins() + + for file in args.filename: + tlogg.openFile(file) + + root.mainloop() \ No newline at end of file diff --git a/apps/tlogg/tlogg/app/notepad.py b/apps/tlogg/tlogg/app/notepad.py new file mode 100644 index 00000000..2fa80c35 --- /dev/null +++ b/apps/tlogg/tlogg/app/notepad.py @@ -0,0 +1,177 @@ +# MIT License +# +# Copyright (c) 2023 Eugenio Parodi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +__all__ = ['NotePad'] + +import TermTk as ttk + +class SuperSimpleHorizontalLine(ttk.TTkWidget): + def paintEvent(self, canvas): + w,h = self.size() + canvas.drawText(pos=(0,h-1), text='┕'+('━'*(w-2))+'┙',color=ttk.TTkColor.fg("#888888")) + +class NotePad(ttk.TTkGridLayout): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + te = ttk.TTkTextEdit(readOnly=False, lineNumber=True) + + self.addItem(wrapLayout := ttk.TTkGridLayout(), 0,0) + self.addItem(fontLayout := ttk.TTkGridLayout(columnMinWidth=1), 1,0) + self.addWidget(te,2,0) + + wrapLayout.addWidget(ttk.TTkLabel(text="Wrap: ", maxWidth=6),0,0) + wrapLayout.addWidget(lineWrap := ttk.TTkComboBox(list=['NoWrap','WidgetWidth','FixedWidth'], maxWidth=20),0,1) + wrapLayout.addWidget(ttk.TTkLabel(text=" Type: ",maxWidth=7),0,2) + wrapLayout.addWidget(wordWrap := ttk.TTkComboBox(list=['WordWrap','WrapAnywhere'], maxWidth=20, enabled=False),0,3) + wrapLayout.addWidget(ttk.TTkLabel(text=" FixW: ",maxWidth=7),0,4) + wrapLayout.addWidget(fixWidth := ttk.TTkSpinBox(value=te.wrapWidth(), maxWidth=5, maximum=500, minimum=10, enabled=False),0,5) + wrapLayout.addWidget(ttk.TTkSpacer(),0,10) + + # Empty columns/cells are 1 char wide due to "columnMinWidth=1" parameter in the GridLayout + # 1 3 8 11 + # 0 2 4 5 6 7 9 10 12 + # 0 [ ] FG [ ] BG [ ] LineNumber + # 1 ┌─────┐ ┌─────┐ ╒═══╕╒═══╕╒═══╕╒═══╕ ┌──────┐┌──────┐ + # 2 │ │ │ │ │ a ││ a ││ a ││ a │ │ UNDO ││ REDO │ + # 3 └─────┘ └─────┘ └───┘└───┘└───┘└───┘ ╘══════╛└──────┘ ┕━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┙ + + # Char Fg/Bg buttons + fontLayout.addWidget(cb_fg := ttk.TTkCheckbox(text=" FG"),0,0) + fontLayout.addWidget(btn_fgColor := ttk.TTkColorButtonPicker(border=True, enabled=False, maxSize=(7,3)),1,0) + + fontLayout.addWidget(cb_bg := ttk.TTkCheckbox(text=" BG"),0,2) + fontLayout.addWidget(btn_bgColor := ttk.TTkColorButtonPicker(border=True, enabled=False, maxSize=(7 ,3)),1,2) + + fontLayout.addWidget(cb_linenumber := ttk.TTkCheckbox(text=" LineNumber", checked=True),0,4,1,3) + + # Char style buttons + fontLayout.addWidget(btn_bold := ttk.TTkButton(border=True, maxSize=(5,3), checkable=True, text=ttk.TTkString( 'a' , ttk.TTkColor.BOLD) ),1,4) + fontLayout.addWidget(btn_italic := ttk.TTkButton(border=True, maxSize=(5,3), checkable=True, text=ttk.TTkString( 'a' , ttk.TTkColor.ITALIC) ),1,5) + fontLayout.addWidget(btn_underline := ttk.TTkButton(border=True, maxSize=(5,3), checkable=True, text=ttk.TTkString(' a ', ttk.TTkColor.UNDERLINE) ),1,6) + fontLayout.addWidget(btn_strikethrough := ttk.TTkButton(border=True, maxSize=(5,3), checkable=True, text=ttk.TTkString(' a ', ttk.TTkColor.STRIKETROUGH)),1,7) + + # Undo/Redo buttons + fontLayout.addWidget(btn_undo := ttk.TTkButton(border=True, maxSize=(8,3), enabled=te.isUndoAvailable(), text=' UNDO '),1,9 ) + fontLayout.addWidget(btn_redo := ttk.TTkButton(border=True, maxSize=(8,3), enabled=te.isRedoAvailable(), text=' REDO '),1,10) + # Undo/Redo events + te.undoAvailable.connect(btn_undo.setEnabled) + te.redoAvailable.connect(btn_redo.setEnabled) + btn_undo.clicked.connect(te.undo) + btn_redo.clicked.connect(te.redo) + + # Useless custom horizontal bar for aestetic reason + fontLayout.addWidget(SuperSimpleHorizontalLine(),0,12,2,1) + + @ttk.pyTTkSlot(ttk.TTkColor) + def _currentColorChangedCB(format): + if fg := format.foreground(): + cb_fg.setCheckState(ttk.TTkK.Checked) + btn_fgColor.setEnabled() + btn_fgColor.setColor(fg.invertFgBg()) + else: + cb_fg.setCheckState(ttk.TTkK.Unchecked) + btn_fgColor.setDisabled() + + if bg := format.background(): + cb_bg.setCheckState(ttk.TTkK.Checked) + btn_bgColor.setEnabled() + btn_bgColor.setColor(bg) + else: + cb_bg.setCheckState(ttk.TTkK.Unchecked) + btn_bgColor.setDisabled() + + btn_bold.setChecked(format.bold()) + btn_italic.setChecked(format.italic()) + btn_underline.setChecked(format.underline()) + btn_strikethrough.setChecked(format.strikethrough()) + # ttk.TTkLog.debug(f"{fg=} {bg=} {bold=} {italic=} {underline=} {strikethrough= }") + + te.currentColorChanged.connect(_currentColorChangedCB) + + def _setStyle(): + color = ttk.TTkColor() + if cb_fg.checkState() == ttk.TTkK.Checked: + color += btn_fgColor.color().invertFgBg() + if cb_bg.checkState() == ttk.TTkK.Checked: + color += btn_bgColor.color() + if btn_bold.isChecked(): + color += ttk.TTkColor.BOLD + if btn_italic.isChecked(): + color += ttk.TTkColor.ITALIC + if btn_underline.isChecked(): + color += ttk.TTkColor.UNDERLINE + if btn_strikethrough.isChecked(): + color += ttk.TTkColor.STRIKETROUGH + cursor = te.textCursor() + cursor.applyColor(color) + cursor.setColor(color) + te.setFocus() + + cb_fg.stateChanged.connect(lambda x: btn_fgColor.setEnabled(x==ttk.TTkK.Checked)) + cb_bg.stateChanged.connect(lambda x: btn_bgColor.setEnabled(x==ttk.TTkK.Checked)) + cb_fg.clicked.connect(lambda _: _setStyle()) + cb_bg.clicked.connect(lambda _: _setStyle()) + + cb_linenumber.stateChanged.connect(lambda x: te.setLineNumber(x==ttk.TTkK.Checked)) + + btn_fgColor.colorSelected.connect(lambda _: _setStyle()) + btn_bgColor.colorSelected.connect(lambda _: _setStyle()) + + btn_bold.clicked.connect(_setStyle) + btn_italic.clicked.connect(_setStyle) + btn_underline.clicked.connect(_setStyle) + btn_strikethrough.clicked.connect(_setStyle) + + lineWrap.setCurrentIndex(0) + wordWrap.setCurrentIndex(1) + + fixWidth.valueChanged.connect(te.setWrapWidth) + + @ttk.pyTTkSlot(int) + def _lineWrapCallback(index): + if index == 0: + te.setLineWrapMode(ttk.TTkK.NoWrap) + wordWrap.setDisabled() + fixWidth.setDisabled() + elif index == 1: + te.setLineWrapMode(ttk.TTkK.WidgetWidth) + wordWrap.setEnabled() + fixWidth.setDisabled() + else: + te.setLineWrapMode(ttk.TTkK.FixedWidth) + te.setWrapWidth(fixWidth.value()) + wordWrap.setEnabled() + fixWidth.setEnabled() + + lineWrap.currentIndexChanged.connect(_lineWrapCallback) + + @ttk.pyTTkSlot(int) + def _wordWrapCallback(index): + if index == 0: + te.setWordWrapMode(ttk.TTkK.WordWrap) + else: + te.setWordWrapMode(ttk.TTkK.WrapAnywhere) + + wordWrap.currentIndexChanged.connect(_wordWrapCallback) + + diff --git a/apps/tlogg/tlogg/app/options.py b/apps/tlogg/tlogg/app/options.py new file mode 100644 index 00000000..fb6726fd --- /dev/null +++ b/apps/tlogg/tlogg/app/options.py @@ -0,0 +1,76 @@ +#!/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. + +__all__ = ['optionsLoadTheme', 'optionsFormLayout'] + +import copy + +from . import TloggCfg, TloggGlbl + +from TermTk import * + +def optionsLoadTheme(theme): + if theme == 'ASCII': + TTkTheme.loadTheme(TTkTheme.ASCII) + elif theme == 'UTF8': + TTkTheme.loadTheme(TTkTheme.UTF8) + elif theme == 'NERD': + TTkTheme.loadTheme(TTkTheme.NERD) + +def optionsFormLayout(win): + options = copy.deepcopy(TloggCfg.options) + + retLayout = TTkGridLayout() + bottomLayout = TTkGridLayout() + + themesFrame = TTkFrame(title="Theme", border=True, layout=TTkVBoxLayout(), maxHeight=5, minHeight=5) + # Themes + themesFrame.layout().addWidget(r1 := TTkRadioButton(text="ASCII", name="theme", checked=options['theme'] == 'ASCII')) + themesFrame.layout().addWidget(r2 := TTkRadioButton(text="UTF-8", name="theme", checked=options['theme'] == 'UTF8')) + themesFrame.layout().addWidget(r3 := TTkRadioButton(text="Nerd", name="theme", checked=options['theme'] == 'NERD')) + + retLayout.addWidget(themesFrame,0,0) + retLayout.addWidget(TTkSpacer() ,1,0,1,2) + + retLayout.addItem(bottomLayout ,2,0,1,2) + bottomLayout.addWidget(applyBtn := TTkButton(text="Apply", border=True, maxHeight=3),0,1) + bottomLayout.addWidget(cancelBtn := TTkButton(text="Cancel", border=True, maxHeight=3),0,2) + bottomLayout.addWidget(okBtn := TTkButton(text="OK", border=True, maxHeight=3),0,3) + + def _saveOptions(): + if r1.checkState() == TTkK.Checked: options['theme'] = 'ASCII' + if r2.checkState() == TTkK.Checked: options['theme'] = 'UTF8' + if r3.checkState() == TTkK.Checked: options['theme'] = 'NERD' + TloggCfg.options = options + TloggCfg.save(searches=False, filters=False, colors=False, options=True) + optionsLoadTheme(options['theme']) + TloggGlbl.refreshViews() + TTkHelper.updateAll() + + applyBtn.clicked.connect(_saveOptions) + okBtn.clicked.connect(_saveOptions) + okBtn.clicked.connect(win.close) + cancelBtn.clicked.connect(win.close) + + return retLayout diff --git a/apps/tlogg/tlogg/app/predefinedfilters.py b/apps/tlogg/tlogg/app/predefinedfilters.py new file mode 100644 index 00000000..a6731fe6 --- /dev/null +++ b/apps/tlogg/tlogg/app/predefinedfilters.py @@ -0,0 +1,180 @@ +#!/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. + +__all__ = ['PredefinedFilters','PredefinedFiltersFormWindow'] + +import copy +from readline import insert_text + +from . import TloggCfg, TloggGlbl + +from TermTk import * + +class PredefinedFiltersFormWindow(TTkWindow): + ''' + This form is inspired by klogg "PredefinedFilters..." form + ''' + + def __init__(self, *args, **kwargs): + TTkWindow.__init__(self, *args, **kwargs) + self._name = kwargs.get('name' , 'PredefinedFiltersFormWindow' ) + + self._filters = copy.deepcopy(TloggCfg.filters) + self._selected = -1 + + self._retLayout = TTkVBoxLayout() # Full Layout + self._controlLayout = TTkGridLayout() # Layout including the add/remove/move buttons + self._filtersLayout = TTkGridLayout() # Layout having the filters form + self._bottomLayout = TTkGridLayout() # Layout including the OK,Cancel,Apply Buttons + + self.setLayout(self._retLayout) + + self._retLayout.addItem(self._controlLayout) + self._retLayout.addItem(self._filtersLayout) + self._retLayout.addItem(self._bottomLayout) + + self._addButton = TTkButton(text="+",maxWidth=3) + self._removeButton = TTkButton(text="-",maxWidth=3) + self._upButton = TTkButton(text="▲",maxWidth=3) + self._downButton = TTkButton(text="▼",maxWidth=3) + + self._controlLayout.addWidget(self._addButton ,1,0) + self._controlLayout.addWidget(self._removeButton ,1,1) + self._controlLayout.addWidget(TTkSpacer(maxHeight=1) ,1,2) + self._controlLayout.addWidget(self._upButton ,1,3) + self._controlLayout.addWidget(self._downButton ,1,4) + + self._bottomLayout.addWidget(applyBtn := TTkButton(text="Apply", border=True, maxHeight=3),0,1) + self._bottomLayout.addWidget(cancelBtn := TTkButton(text="Cancel", border=True, maxHeight=3),0,2) + self._bottomLayout.addWidget(okBtn := TTkButton(text="OK", border=True, maxHeight=3),0,3) + + self._refreshFilters() + + def _move(offset): + def _moveUpDown(): + if self._selected < 0: return + if self._selected + offset < 0: return + if self._selected + offset >= len(self._filters): return + item = self._filters.pop(self._selected) + self._filters = self._filters[:self._selected+offset] + [item] + self._filters[self._selected+offset:] + self._refreshFilters() + self._selected += offset + return _moveUpDown + self._upButton.clicked.connect(_move(-1)) + self._downButton.clicked.connect(_move(1)) + + def _addCallback(): + filter = {'name':f'n:{len(self._filters)+1}', 'pattern':'' } + self._filters.append(filter) + self._selected = -1 + self._refreshFilters() + self._addButton.clicked.connect(_addCallback) + + def _removeCallback(): + if self._selected > -1: + self._filters.pop(self._selected) + self.selected = -1 + self._refreshFilters() + self._removeButton.clicked.connect(_removeCallback) + + def _saveFilters(): + TloggCfg.filters = self._filters + TloggCfg.save(searches=False, filters=True, colors=False, options=False) + + applyBtn.clicked.connect(_saveFilters) + okBtn.clicked.connect(_saveFilters) + okBtn.clicked.connect(self.close) + cancelBtn.clicked.connect(self.close) + + def _refreshFilters(self): + for item in self._filtersLayout.children(): + self._filtersLayout.removeItem(item) + splitter = TTkSplitter(border=True) + names = TTkVBoxLayout() + patterns = TTkVBoxLayout() + splitter.addWidget(TTkFrame(border=False, layout=names)) + splitter.addWidget(TTkFrame(border=False, layout=patterns)) + splitter.setSizes([20,100]) + for i, filter in enumerate(self._filters): + names.addWidget( nl := TTkLineEdit(text=filter['name'])) + patterns.addWidget(pl := TTkLineEdit(text=filter['pattern'])) + # Stupid way to attach the textEdited signal directly to the array modification + # It could have been made much better but Lazyness is an Issue + def _setName(pos): + def _set(txt): self._filters[pos]['name'] = txt + return _set + def _setPattern(pos): + def _set(txt): self._filters[pos]['pattern'] = txt + return _set + def _setSelected(pos): + def _set(focus): + if focus: + self._selected = pos + return _set + nl.textEdited.connect(_setName(i)) + pl.textEdited.connect(_setPattern(i)) + nl.focusChanged.connect(_setSelected(i)) + pl.focusChanged.connect(_setSelected(i)) + + names.addWidget( TTkSpacer()) + patterns.addWidget(TTkSpacer()) + self._filtersLayout.addWidget(splitter) + + +class PredefinedFilters(TTkResizableFrame): + __slots__ = ('checked', 'unchecked', '_searchbox') + def __init__(self, searchbox): + layout=TTkVBoxLayout() + TTkResizableFrame.__init__(self, layout=layout) + self._name = 'PredefinedFilters' + + self._searchbox = searchbox + + filters = copy.deepcopy(TloggCfg.filters) + for filter in filters: + def _cb(p, sb): + def _ret(state): + txt = sb.currentText() + if state == TTkK.Checked: + if not txt: + txt = p + elif txt.find(p) == -1: + txt += '|'+p + else: + p1 = f"|{p}" + p2 = f"{p}|" + if txt.find(p1) != -1: + txt = txt.replace(p1,'') + elif txt.find(p2) != -1: + txt = txt.replace(p2,'') + else: + txt = txt.replace(p,'') + sb.setCurrentText(txt) + return _ret + + layout.addWidget(cb := TTkCheckbox(text=filter['name'],checked=searchbox.currentText().find(filter['pattern'])!=-1)) + cb.stateChanged.connect(_cb(filter['pattern'], searchbox)) + w,h = self.minimumSize() + self.resize(w,h) + diff --git a/apps/tlogg/tlogg/helper.py b/apps/tlogg/tlogg/helper.py new file mode 100644 index 00000000..9619375a --- /dev/null +++ b/apps/tlogg/tlogg/helper.py @@ -0,0 +1,82 @@ +# MIT License +# +# Copyright (c) 2023 Eugenio Parodi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import importlib, pkgutil +import runpy +import sys, os + +import TermTk as ttk +from tlogg import TloggPlugin + +class TloggHelper(): + @staticmethod + def _loadPlugins(): + # Check for the plugin folder + pluginFolder = os.path.join(os.path.dirname(os.path.abspath(__file__)),'plugins') + if not os.path.exists(pluginFolder): + ttk.TTkLog.error("No 'plugins' folder found in the 'tlogg' main directory") + else: + for fn in os.listdir(pluginFolder): + filePath = os.path.join(pluginFolder,fn) + if not os.path.isfile(filePath): continue + absolute_name = importlib.util.resolve_name(filePath, None) + ttk.TTkLog.debug(absolute_name) + loader = importlib.machinery.SourceFileLoader(fn, filePath) + spec = importlib.util.spec_from_loader(loader.name, loader) + mod = importlib.util.module_from_spec(spec) + loader.exec_module(mod) + + # Check installed plugins + for finder, name, ispkg in pkgutil.iter_modules(): + if name.startswith("tlogg_"): + loader = importlib.find_loader(name) + spec = importlib.util.find_spec(name) + mod = importlib.util.module_from_spec(spec) + # runpy.run_module(name) + loader.exec_module(mod) + + # check the plugin folder + for mod in TloggPlugin.instances: + if mod.init is not None: + mod.init() + + @staticmethod + def _runPlugins(): + for mod in TloggPlugin.instances: + if mod.apply is not None: + mod.apply() + + @staticmethod + def _getPlugins(): + return TloggPlugin.instances + + @staticmethod + def _getPluginPlacements(): + ret = ttk.TTkK.NONE + for mod in TloggPlugin.instances: + ret |= mod.position + return ret + + @staticmethod + def _getPlacedPlugins(placement): + return [mod for mod in TloggPlugin.instances if mod.position & placement] + diff --git a/apps/tlogg/tlogg/plugin.py b/apps/tlogg/tlogg/plugin.py new file mode 100644 index 00000000..c8ff919a --- /dev/null +++ b/apps/tlogg/tlogg/plugin.py @@ -0,0 +1,41 @@ +# MIT License +# +# Copyright (c) 2023 Eugenio Parodi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from dataclasses import dataclass +from typing import Callable + +import TermTk as ttk + +@dataclass +class TloggPlugin: + instances = [] + name : str + init : Callable[[],None] = None + apply : Callable[[],None] = None + run : Callable[[],None] = None + position : int = ttk.TTkK.NONE # Accepted Values are ; NONE, LEFT, RIGHT + widget : ttk.TTkWidget = None # Required if a position is defined + menu : bool = False + visible: bool = False + + def __post_init__(self): + TloggPlugin.instances.append(self) diff --git a/apps/tlogg/tlogg/plugins/fileexplorer.py b/apps/tlogg/tlogg/plugins/fileexplorer.py new file mode 100644 index 00000000..bf68246f --- /dev/null +++ b/apps/tlogg/tlogg/plugins/fileexplorer.py @@ -0,0 +1,127 @@ +# MIT License +# +# Copyright (c) 2023 Eugenio Parodi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os + +import TermTk as ttk + +import tlogg + +class FileTree(ttk.TTkContainer): + __slots__ = ( + '_recentPath', '_recentPathId', + '_controlsLayout', + '_ftCbFollow', '_ftBtnPrev', '_ftBtnNext', '_ftBtnUp', '_fileTree', + # Forwarded Methods + 'fileActivated') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._name = kwargs.get('name' , 'FileTree' ) + + self.setLayout(ttk.TTkVBoxLayout()) + + self._controlsLayout = ttk.TTkHBoxLayout() + self.layout().addItem(self._controlsLayout) + + self._ftCbFollow = ttk.TTkCheckbox(text="Follow", maxWidth=9, checked=False) + self._ftBtnPrev = ttk.TTkButton(text="<",maxWidth=3, enabled=False) + self._ftBtnNext = ttk.TTkButton(text=">",maxWidth=3, enabled=False) + self._ftBtnUp = ttk.TTkButton(text="^",maxWidth=3, enabled=True) + self._controlsLayout.addWidget(self._ftCbFollow) + self._controlsLayout.addWidget(ttk.TTkSpacer()) + self._controlsLayout.addWidget(self._ftBtnPrev) + self._controlsLayout.addWidget(self._ftBtnNext) + self._controlsLayout.addWidget(self._ftBtnUp) + + self._fileTree = ttk.TTkFileTree(parent=self, path=".") + + self._recentPath = [] + self._recentPathId = -1 + self._addFolder(os.path.abspath('.')) + + self._ftBtnPrev.clicked.connect(self._openPrev) + self._ftBtnNext.clicked.connect(self._openNext) + self._ftBtnUp.clicked.connect(self._openUp) + + self._fileTree.folderActivated.connect(self._openFolder) + + # Forward Functions + self._fileTree.fileActivated.connect(lambda x: tlogg.tloggProxy.openFile(x.path())) + + tlogg.tloggProxy.tloggFocussed.connect(self.follow) + + @ttk.pyTTkSlot(tlogg.TloggViewerProxy, str) + def follow(self, _, path): + if self._ftCbFollow.checkState() == ttk.TTkK.Checked: + if os.path.isfile(path): + path, _ = os.path.split(path) + self._fileTree.openPath(path) + + def _addFolder(self, dir): + if not self._recentPath or dir != self._recentPath[-1]: + self._recentPath.append(dir) + self._recentPathId = len(self._recentPath)-1 + if self._recentPathId: + self._ftBtnPrev.setEnabled() + self._ftBtnNext.setDisabled() + + @ttk.pyTTkSlot(ttk.TTkFileTreeWidgetItem) + def _openFolder(self, dir): + self._fileTree.openPath(dir.path()) + self._addFolder(dir.path()) + + @ttk.pyTTkSlot() + def _openPrev(self): + if self._recentPathId<=0 or self._recentPathId>=len(self._recentPath): + self._ftBtnPrev.setDisabled() + return + self._recentPathId -= 1 + self._fileTree.openPath(self._recentPath[self._recentPathId]) + if self._recentPathId<=0: + self._ftBtnPrev.setDisabled() + self._ftBtnNext.setEnabled() + + @ttk.pyTTkSlot() + def _openNext(self): + if self._recentPathId<0 or self._recentPathId>=len(self._recentPath)-1: + self._ftBtnNext.setDisabled() + return + self._recentPathId += 1 + self._fileTree.openPath(self._recentPath[self._recentPathId]) + if self._recentPathId>=len(self._recentPath)-1: + self._ftBtnNext.setDisabled() + self._ftBtnPrev.setEnabled() + + @ttk.pyTTkSlot() + def _openUp(self): + path = os.path.abspath(self._fileTree.getOpenPath()) + path, e = os.path.split(path) + if e: + self._addFolder(path) + self._fileTree.openPath(path) + +tlogg.TloggPlugin( + name="File Explorer", + position=ttk.TTkK.LEFT, + visible=True, + widget=FileTree()) diff --git a/apps/tlogg/tlogg/plugins/jsonnviewer.py b/apps/tlogg/tlogg/plugins/jsonnviewer.py new file mode 100644 index 00000000..bbf158b4 --- /dev/null +++ b/apps/tlogg/tlogg/plugins/jsonnviewer.py @@ -0,0 +1,56 @@ +# MIT License +# +# Copyright (c) 2023 Eugenio Parodi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import json + +from pygments import highlight +from pygments.lexers import PythonLexer, JavascriptLexer +from pygments.formatters import TerminalFormatter, Terminal256Formatter, TerminalTrueColorFormatter + +import TermTk as ttk + +import tlogg + +class JsonViewer(ttk.TTkTextEdit): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.setLineWrapMode(ttk.TTkK.WidgetWidth) + self.setWordWrapMode(ttk.TTkK.WordWrap) + tlogg.tloggProxy.lineSelected.connect(self._showLine) + + ttk.pyTTkSlot(str) + def _showLine(self, text): + try: + text = json.loads(text) + text = json.dumps(text, indent=4) + text = highlight(text, JavascriptLexer(), TerminalTrueColorFormatter(style='material')) + + except ValueError as e: + pass + self.setText(text) + +tlogg.TloggPlugin( + name="Json Viewer", + position=ttk.TTkK.RIGHT, + menu=True, + visible=False, + widget=JsonViewer(lineNumber=True, readOnly=False, )) diff --git a/apps/tlogg/tlogg/plugins/testplugin.py b/apps/tlogg/tlogg/plugins/testplugin.py new file mode 100644 index 00000000..370e5ef8 --- /dev/null +++ b/apps/tlogg/tlogg/plugins/testplugin.py @@ -0,0 +1,33 @@ +# MIT License +# +# Copyright (c) 2023 Eugenio Parodi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import TermTk as ttk + +import tlogg + +def init(): + ttk.TTkLog.debug("Test Plugin Init") + +def apply(): + ttk.TTkLog.debug("Test Plugin Apply") + +tlogg.TloggPlugin(name="Test Plugin", init=init, apply=apply) diff --git a/apps/tlogg/tlogg/plugins/testplugin1.py b/apps/tlogg/tlogg/plugins/testplugin1.py new file mode 100644 index 00000000..ed7e9bc7 --- /dev/null +++ b/apps/tlogg/tlogg/plugins/testplugin1.py @@ -0,0 +1,36 @@ +# MIT License +# +# Copyright (c) 2023 Eugenio Parodi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import TermTk as ttk + +import tlogg + +def init(): + ttk.TTkLog.debug("Test Plugin1 Init") + +def apply(): + ttk.TTkLog.debug("Test Plugin1 Apply") + +def run(): + ttk.TTkLog.debug("Test Plugin1 Run") + +tlogg.TloggPlugin(name="Test Plugin 1", init=init, apply=apply, run=run) \ No newline at end of file diff --git a/apps/tlogg/tlogg/proxy.py b/apps/tlogg/tlogg/proxy.py new file mode 100644 index 00000000..daa0e50a --- /dev/null +++ b/apps/tlogg/tlogg/proxy.py @@ -0,0 +1,50 @@ +# MIT License +# +# Copyright (c) 2023 Eugenio Parodi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +__all__ = ['TloggViewerProxy', 'tloggProxy'] + +import TermTk as ttk + +class TloggViewerProxy(): + __slots__ = ('_fileName') + def __init__(self, fileName) -> None: + self._fileName = fileName + + def fileName(self): + return self._fileName + +class TloggProxy(): + __slots__ = ('_openFileCb', + # Signals + 'tloggFocussed', 'lineSelected') + def __init__(self) -> None: + self._openFileCb = lambda _ : None + self.tloggFocussed = ttk.pyTTkSignal(TloggViewerProxy, str) + self.lineSelected = ttk.pyTTkSignal(str) + + def setOpenFile(self, cb): + self._openFileCb = cb + + def openFile(self, fileName): + return self._openFileCb(fileName) + +tloggProxy = TloggProxy() \ No newline at end of file diff --git a/apps/tlogg/tools/create_log.py b/apps/tlogg/tools/create_log.py new file mode 100755 index 00000000..5f2ff088 --- /dev/null +++ b/apps/tlogg/tools/create_log.py @@ -0,0 +1,70 @@ +#!/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 +import random, json + +words = ["Lorem", "ipsum", "dolor", "sit", "amet,", "consectetur", "adipiscing", "elit,", "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore", "magna", "aliqua.", "Ut", "enim", "ad", "minim", "veniam,", "quis", "nostrud", "exercitation", "ullamco", "laboris", "nisi", "ut", "aliquip", "ex", "ea", "commodo", "consequat.", "Duis", "aute", "irure", "dolor", "in", "reprehenderit", "in", "voluptate", "velit", "esse", "cillum", "dolore", "eu", "fugiat", "nulla", "pariatur.", "Excepteur", "sint", "occaecat", "cupidatat", "non", "proident,", "sunt", "in", "culpa", "qui", "officia", "deserunt", "mollit", "anim", "id", "est", "laborum."] +def getWord(): + return random.choice(words) +def getSentence(a,b): + return " ".join([getWord() for i in range(0,random.randint(a,b))]) + + +if len(sys.argv) != 3 : + print ("Missing filename") + print ("use %s " % sys.argv[0]) + exit(1) + +filename = sys.argv[1] +lines = int(sys.argv[2]) +print ("Lines=%d" % lines) + +with open(filename, 'a') as out: + for i in range(0,lines): + seconds = 1000 + i + m, s = divmod(seconds, 60) + h, m = divmod(m, 60) + if ((rnd:=random.random()) < 0.6): + out.write( "TEST;%d:%02d:%02d;COL1\tCOL2 COL3 c:COL4;LIN=%05X\tRND=%f %s %s\n" % (h, m, s, i, random.random(), getSentence(3,20), " Fill" * random.randint(1,5)) ) + elif (rnd < 0.9): + jsonLine = { + 'time':f"{h:02d}:{m:02d}:{s:02d}", + 'line':i, + 'data':{ + 'rnd':random.random(), + 'sentence1':getSentence(3,6), + 'sentence2':getSentence(3,6), + 'sentence3':getSentence(3,6), + 'sentence4':getSentence(3,6)}, + 'extra': { + 'text':getSentence(3,10), + 'list':[getSentence(1,2) for _ in range(random.randint(2,7))], + 'list2':[random.randint(2,128) for _ in range(random.randint(2,7))], + 'fill':" Fill" * random.randint(1,5) + } } + out.write( json.dumps(jsonLine) + '\n') + else: + out.write( "TEST;%d:%02d:%02d;COL1 --- (BROKEN LINE) --- LIN=%05X\tRND=%f %s %s\n" % (h, m, s, i, random.random(), getSentence(3,20), " Fill" * random.randint(1,5)) ) + out.write( " END LINE (No Newline)" ) diff --git a/apps/tlogg/tools/import.filters.from.klogg.py b/apps/tlogg/tools/import.filters.from.klogg.py new file mode 100755 index 00000000..5a2e0647 --- /dev/null +++ b/apps/tlogg/tools/import.filters.from.klogg.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# This is just a helper, don't rely on this script too much +# Usage: +# cat ~/.config/klogg/klogg.conf | tools/import.filters.from.klogg.py + +import re +import fileinput + +# I am trying o match things like: +# sets\1\HighlighterSet\highlighters\1\fore_colour=#ff000000 +# sets\1\HighlighterSet\highlighters\1\ignore_case=false +# sets\1\HighlighterSet\highlighters\1\match_only=false + +rr = re.compile('^sets\\\(\d*)\\\HighlighterSet\\\highlighters\\\(\d*)\\\([^=]*)=(.*)$') + +sets = {} + +for line in fileinput.input(): + # print(line.rstrip()) + if m:=rr.match(line.rstrip()): + set = m.group(1) + num = m.group(2) + name = m.group(3) + value = m.group(4) + if set not in sets: + sets[set] = {} + if num not in sets[set]: + sets[set][num] = {} + sets[set][num][name] = value + +for s in sets: + print(f"\nset {s}:") + for n in sets[s]: + f = sets[s][n] + # print (f) + print (f"- pattern: '{f['regexp']}'") + print (f" ignorecase: {f['ignore_case']}") + print (f" fg: '#{f['fore_colour'][3:]}'") + print (f" bg: '#{f['back_colour'][3:]}'") diff --git a/apps/tlogg/tools/test.config/colors.yaml b/apps/tlogg/tools/test.config/colors.yaml new file mode 100644 index 00000000..24195a6a --- /dev/null +++ b/apps/tlogg/tools/test.config/colors.yaml @@ -0,0 +1,30 @@ +version: '1.0' +cfg: +- pattern: ipsum dolore + ignorecase: true + fg: '#FFFFFF' + bg: '#0000ff' +- pattern: cillum sit + ignorecase: true + fg: '#101d46' + bg: '#ff00ff' +- pattern: excepteur + ignorecase: true + fg: '#ffff00' + bg: '#000000' +- pattern: dolore + ignorecase: true + fg: '#00ff00' + bg: '#002338' +- pattern: cupida + ignorecase: true + fg: '#00ffff' + bg: '#0000ff' +- pattern: consequa + ignorecase: true + fg: '#ffff00' + bg: '#0000ff' +- pattern: sunt ad + ignorecase: true + fg: '#ffff00' + bg: '#ff0000' diff --git a/apps/tlogg/tools/test.config/options.yaml b/apps/tlogg/tools/test.config/options.yaml new file mode 100644 index 00000000..2198ca6f --- /dev/null +++ b/apps/tlogg/tools/test.config/options.yaml @@ -0,0 +1,3 @@ +version: '1.0' +cfg: + theme: NERD diff --git a/apps/ttkode/pyproject.toml b/apps/ttkode/pyproject.toml index 33116666..032a3b2a 100644 --- a/apps/ttkode/pyproject.toml +++ b/apps/ttkode/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ Homepage = "https://github.com/ceccopierangiolieugenio/pyTermTk/tree/main/apps/ttkode" Repository = "https://github.com/ceccopierangiolieugenio/pyTermTk.git" Issues = "https://github.com/ceccopierangiolieugenio/pyTermTk/issues" -Changelog = "https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/apps/ttkDesigner/CHANGELOG.md" +Changelog = "https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/apps/ttkode/CHANGELOG.md" [project.scripts] ttkode = "ttkode:main" diff --git a/apps/ttkode/ttkode/app/cfg.py b/apps/ttkode/ttkode/app/cfg.py index f2c1ab3d..b6ec872c 100644 --- a/apps/ttkode/ttkode/app/cfg.py +++ b/apps/ttkode/ttkode/app/cfg.py @@ -25,9 +25,11 @@ import os import json +from .. import __version__ + class TTKodeCfg: - version="__VERSION__" - name="__NAME__" + version=__version__ + name="ttkode" cfgVersion = '1.0' pathCfg="." options={}