From c5d0e51aa4bd54f48c916618eb2708a7780fa149 Mon Sep 17 00:00:00 2001 From: Pier CeccoPierangioliEugenio Date: Wed, 25 Feb 2026 09:53:34 +0000 Subject: [PATCH] chore: basic command palette implementation (#598) --- .../ttkode/app/command_palette/__init__.py | 0 .../app/command_palette/command_palette.py | 239 ++++++++++++++++++ .../command_palette/command_palette_items.py | 70 +++++ .../command_palette/search_file_threading.py | 95 +++++++ apps/ttkode/ttkode/app/helpers/__init__.py | 0 apps/ttkode/ttkode/app/helpers/search_file.py | 98 +++++++ apps/ttkode/ttkode/app/ttkode.py | 11 + apps/ttkode/ttkode/plugins/_010/findwidget.py | 7 - .../t.generic/test.generic.014.pathlib.01.py | 32 +++ 9 files changed, 545 insertions(+), 7 deletions(-) create mode 100644 apps/ttkode/ttkode/app/command_palette/__init__.py create mode 100644 apps/ttkode/ttkode/app/command_palette/command_palette.py create mode 100644 apps/ttkode/ttkode/app/command_palette/command_palette_items.py create mode 100644 apps/ttkode/ttkode/app/command_palette/search_file_threading.py create mode 100644 apps/ttkode/ttkode/app/helpers/__init__.py create mode 100644 apps/ttkode/ttkode/app/helpers/search_file.py create mode 100755 tests/t.generic/test.generic.014.pathlib.01.py diff --git a/apps/ttkode/ttkode/app/command_palette/__init__.py b/apps/ttkode/ttkode/app/command_palette/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/ttkode/ttkode/app/command_palette/command_palette.py b/apps/ttkode/ttkode/app/command_palette/command_palette.py new file mode 100644 index 00000000..428c9a86 --- /dev/null +++ b/apps/ttkode/ttkode/app/command_palette/command_palette.py @@ -0,0 +1,239 @@ +# MIT License +# +# Copyright (c) 2026 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__ = ['TTKode_CommandPalette'] + +from pathlib import Path +from enum import Enum,auto +from typing import List, Tuple, Optional + +import TermTk as ttk + +from ttkode.app.command_palette.search_file_threading import TTKode_CP_SearchFileThreading, TTKode_CP_SearchFileItem +from ttkode.app.command_palette.command_palette_items import TTKodeCommandPaletteListItem, TTKodeCommandPaletteListItemFile + +class _ListAction(Enum): + UP=auto() + DOWN=auto() + SELECT=auto() + + +class _TTKodeCommandPaletteListWidget(ttk.TTkAbstractScrollView): + _color_hovered = ttk.TTkColor.bg('#444444') + _color_hilghlight = ttk.TTkColor.bg("#173C70") + + __slots__ = ('_items', '_highlight', '_hovered', 'selected') + + _items:List[TTKodeCommandPaletteListItem] + _highlight:Optional[TTKodeCommandPaletteListItem] + _hovered:Optional[TTKodeCommandPaletteListItem] + selected:ttk.pyTTkSignal + + def __init__(self, **kwargs): + self._items = [] + self._highlight = None + self._hovered = None + self.selected = ttk.pyTTkSignal(TTKodeCommandPaletteListItem) + super().__init__(**kwargs) + + def viewFullAreaSize(self) -> Tuple[int,int]: + w = self.width() + h = len(self._items) + return w, h + + def clean(self) -> None: + self._highlight = None + self._hovered = None + self._items = [] + self.viewMoveTo(0,0) + + def extend(self, items:List[TTKodeCommandPaletteListItem]): + self._items.extend(items) + self._items = sorted(self._items, key=lambda x: x.sorted_key()) + self.viewChanged.emit() + self.update() + + def _pushAction(self, action:_ListAction) -> None: + if not self._items: + return + _highlight = self._highlight + if _highlight is None: + _highlight = self._items[0] + self._highlight = _highlight + + _items = self._items + ox,oy = self.getViewOffsets() + h = self.height() + + index = _items.index(_highlight) if _highlight in _items else None + if action is _ListAction.UP: + index = -1 if index is None else index-1 + elif action is _ListAction.DOWN: + index = 0 if index is None or index>=len(_items)-1 else index+1 + elif action is _ListAction.SELECT: + if self._highlight: + self.selected.emit(self._highlight) + return + else: + index = 0 + self._highlight = _items[index] + index = _items.index(self._highlight) + if index < oy: + oy = index + elif oy+h <= index: + oy = index-h+1 + self.viewMoveTo(ox,oy) + self.update() + + def mouseReleaseEvent(self, evt): + ox,oy = self.getViewOffsets() + x,y = evt.x,evt.y + y+=oy + _items = self._items + if 0 <= y < len(_items): + self.selected.emit(_items[y]) + self.update() + return True + + def mouseMoveEvent(self, evt): + ox,oy = self.getViewOffsets() + x,y = evt.x,evt.y + y+=oy + _items = self._items + if 0 <= y < len(_items): + self._hovered = _items[y] + else: + self._hovered = None + self.update() + return True + + def leaveEvent(self, evt): + self._hovered = None + self.update() + return True + + def paintEvent(self, canvas): + w,h = self.size() + ox,oy = self.getViewOffsets() + for i,item in enumerate(self._items[oy:oy+h]): + color = ttk.TTkColor.RST + if item is self._hovered: + color = self._color_hovered + elif item is self._highlight: + color = self._color_hilghlight + elif self._highlight is None and i == 0: + color = self._color_hilghlight + + text = item.toTTkString(width=w).completeColor(color) + canvas.fill(pos=(0,i),size=(w,1),color=color) + canvas.drawTTkString(text=text, pos=(0,i)) + +class _TTKodeCommandPaletteList(ttk.TTkAbstractScrollArea): + __slots__ = ('_list_widget', 'selected') + selected:ttk.pyTTkSignal + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._list_widget = _TTKodeCommandPaletteListWidget() + self.selected = self._list_widget.selected + self.setViewport(self._list_widget) + + def clean(self) -> None: + return self._list_widget.clean() + + def extend(self, items:List[TTKodeCommandPaletteListItem]): + return self._list_widget.extend(items=items) + + def _pushAction(self, action:_ListAction) -> None: + return self._list_widget._pushAction(action=action) + + +class TTKode_CommandPalette(ttk.TTkResizableFrame): + __slots__ = ('_line_edit', '_cpl', '_sft') + def __init__(self, **kwargs): + layout = ttk.TTkGridLayout() + self._line_edit = le = ttk.TTkLineEdit(hint='Search files by name') + self._cpl = cpl = _TTKodeCommandPaletteList() + self._sft = TTKode_CP_SearchFileThreading() + layout.addWidget(le,0,0) + layout.addWidget(cpl,1,0) + super().__init__(layout=layout, **kwargs) + le.textEdited.connect(self._search) + self._sft.search_results.connect(self._process_search_results) + self._cpl.selected.connect(self._selected_item) + + @ttk.pyTTkSlot(TTKodeCommandPaletteListItem) + def _selected_item(self, item:TTKodeCommandPaletteListItem) -> None: + if isinstance(item, TTKodeCommandPaletteListItemFile): + from ttkode.proxy import ttkodeProxy + ttkodeProxy.openFile(item._file) + self.close() + + @ttk.pyTTkSlot(str) + def _search(self, pattern:ttk.TTkString) -> None: + self._cpl.clean() + self._sft.search(pattern=pattern.toAscii()) + + @ttk.pyTTkSlot(List[TTKode_CP_SearchFileItem]) + def _process_search_results(self, items:List[TTKode_CP_SearchFileItem]): + items_path = [ + TTKodeCommandPaletteListItemFile( + file=_f.file, + pattern=_f.match_pattern + ) for _f in items + ] + ttk.TTkLog.debug('\n'.join([str(f) for f in items])) + self._cpl.extend(items_path) + + # def setFocus(self): + # return self._line_edit.setFocus() + + def keyEvent(self, evt:ttk.TTkKeyEvent) -> bool: + if evt.type == ttk.TTkK.SpecialKey: + # Don't Handle the special focus switch key + if evt.key is ttk.TTkK.Key_Up: + self._cpl._pushAction(_ListAction.UP) + self.update() + return True + if evt.key is ttk.TTkK.Key_Down: + self._cpl._pushAction(_ListAction.DOWN) + self.update() + return True + if evt.key is ttk.TTkK.Key_Enter: + self._cpl._pushAction(_ListAction.SELECT) + self.update() + return True + if evt.key is ttk.TTkK.Key_Escape: + self.pippo='Esc' + self.close() + return True + if self._line_edit.keyEvent(evt=evt): + self._line_edit.setFocus() + return True + return False + + def paintEvent(self, canvas:ttk.TTkCanvas) -> None: + super().paintEvent(canvas) + w,h = self.size() + canvas.drawChar(pos=( 0, 0), char='╭') + canvas.drawChar(pos=(w-1, 0), char='╮') + canvas.drawChar(pos=( 0,h-1), char='╰') + canvas.drawChar(pos=(w-1,h-1), char='╯') diff --git a/apps/ttkode/ttkode/app/command_palette/command_palette_items.py b/apps/ttkode/ttkode/app/command_palette/command_palette_items.py new file mode 100644 index 00000000..1fb014e9 --- /dev/null +++ b/apps/ttkode/ttkode/app/command_palette/command_palette_items.py @@ -0,0 +1,70 @@ +# MIT License +# +# Copyright (c) 2026 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__ = ['TTKodeCommandPaletteListItem', 'TTKodeCommandPaletteListItemFile'] + +from pathlib import Path + +from typing import Tuple, Any + +import TermTk as ttk + +class TTKodeCommandPaletteListItem(): + def toTTkString(self, width:int) -> ttk.TTkString: + return ttk.TTkString() + def sorted_key(self) -> Tuple[int, Any]: + return (0,0) +_colorDirectory = ttk.TTkColor.fg('#AAAAAA') +_colorMatch = ttk.TTkColor.fg('#00AAAA') + +class TTKodeCommandPaletteListItemFile(): + __slots__ = ('_file', '_key', '_pattern') + _pattern:str + _file:Path + _key:int + def __init__(self, file:Path, pattern:str): + self._file = file + self._pattern = pattern + _match_ret = file.name.split(pattern)[-1] + _match_level = len(_match_ret) + _match_level += 0x10000 * _match_ret.count('/') + self._key = _match_level + + def sorted_key(self) -> Tuple[int, Any]: + return (self._key, self._file.name.lower()) + + def toTTkString(self, width:int) -> ttk.TTkString: + file = self._file + fileName = file.name + folder = file.parent + + text = ( + ttk.TTkString(ttk.TTkCfg.theme.fileIcon.getIcon(fileName), ttk.TTkCfg.theme.fileIconColor) + + ttk.TTkString(" " + fileName + " ") + ttk.TTkString( folder, _colorDirectory ) + ) + + text = text.setColor(match=self._pattern, color=_colorMatch) + + if len(text) > width: + text = text.substring(to=width-3) + '...' + + return text diff --git a/apps/ttkode/ttkode/app/command_palette/search_file_threading.py b/apps/ttkode/ttkode/app/command_palette/search_file_threading.py new file mode 100644 index 00000000..9939adde --- /dev/null +++ b/apps/ttkode/ttkode/app/command_palette/search_file_threading.py @@ -0,0 +1,95 @@ +# MIT License +# +# Copyright (c) 2026 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 fnmatch +from dataclasses import dataclass +from threading import Thread,Event,Lock + +from pathlib import Path +from typing import List, Generator, Tuple, Optional + +import TermTk as ttk + +from ttkode.app.helpers.search_file import TTKode_SearchFile + +@dataclass +class TTKode_CP_SearchFileItem(): + file:Path + match_pattern:str + +class TTKode_CP_SearchFileThreading(): + __slots__ = ( + '_runId', + '_search_thread', '_search_stop_event', '_search_lock', + 'search_results' + ) + + _runId:int + _search_lock:Lock + _search_thread:Optional[Thread] + _search_stop_event:Event + + search_results:ttk.pyTTkSignal + + def __init__(self, **kwargs): + self._runId = 0 + self._search_thread = None + self._search_lock = Lock() + self._search_stop_event = Event() + self.search_results = ttk.pyTTkSignal(List[TTKode_CP_SearchFileItem]) + + @ttk.pyTTkSlot(str) + def search(self, pattern:str) -> None: + ttk.TTkLog.debug(pattern) + with self._search_lock: + if self._search_thread: + self._search_stop_event.set() + self._search_thread.join() + self._search_stop_event.clear() + self._search_thread = None + if not pattern: + return + self._runId += 1 + self._search_thread = Thread( + target=self._search_threading, + args=(pattern,)) + self._search_thread.start() + + def _search_threading(self, search_pattern:str) -> None: + items = 1 + ret:List[Path] = [] + for file in TTKode_SearchFile.getFilesFromPattern('.', pattern=search_pattern): + if self._search_stop_event.is_set(): + return + ret.append( + TTKode_CP_SearchFileItem( + file=file, + match_pattern=search_pattern + ) + ) + if len(ret) >= items: + self.search_results.emit(ret) + items <<= 2 + ret = [] + if ret: + self.search_results.emit(ret) \ No newline at end of file diff --git a/apps/ttkode/ttkode/app/helpers/__init__.py b/apps/ttkode/ttkode/app/helpers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/ttkode/ttkode/app/helpers/search_file.py b/apps/ttkode/ttkode/app/helpers/search_file.py new file mode 100644 index 00000000..af5a697c --- /dev/null +++ b/apps/ttkode/ttkode/app/helpers/search_file.py @@ -0,0 +1,98 @@ +# MIT License +# +# Copyright (c) 2026 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__ = ['TTKode_SearchFile'] + +import os +import fnmatch +import mimetypes +from threading import Thread,Event,Lock + +from pathlib import Path +from typing import List, Generator, Tuple, Optional + +import TermTk as ttk + +def is_text_file(file_path, block_size=512): + # Check MIME type + mime_type, _ = mimetypes.guess_type(file_path) + text_based_mime_types = [ + 'text/', 'application/json', 'application/xml', + 'application/javascript', 'application/x-httpd-php', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + ] + if mime_type is not None and any(mime_type.startswith(mime) for mime in text_based_mime_types): + return True + + # Check for non-printable characters + try: + with open(file_path, 'rb') as file: + block = file.read(block_size) + if b'\0' in block: + return False + text_characters = bytearray({7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x100)) - {0x7f}) + return not bool(block.translate(None, text_characters)) + except Exception as e: + print(f"Error reading file: {e}") + return False + +def _load_gitignore_patterns(gitignore_path): + if os.path.exists(gitignore_path): + with open(gitignore_path, 'r') as f: + patterns = f.read().splitlines() + return patterns + return [] + +def _glob_match_patterns(path, patterns) -> bool: + if path == '.': + check_path = '' + elif path.startswith('./'): + check_path = path[2:] + else: + check_path = path + return any(f"/{_p}/" in path for _p in patterns if _p) or any(fnmatch.fnmatch(check_path, _p) for _p in patterns if _p) + +def _custom_walk(directory:str, include_patterns:List[str]=[], exclude_patterns:List[str]=[]) -> Generator[Tuple[str, str], None, None]: + gitignore_path = os.path.join(directory, '.gitignore') + exclude_patterns = exclude_patterns + _load_gitignore_patterns(gitignore_path) + for entry in sorted(os.listdir(directory)): + full_path = os.path.join(directory, entry) + if _glob_match_patterns(full_path, exclude_patterns): + continue + if not os.path.exists(full_path): + continue + if os.path.isdir(full_path): + if entry == '.git': + continue + yield from _custom_walk(full_path, include_patterns, exclude_patterns) + else: + if include_patterns and not _glob_match_patterns(full_path, include_patterns): + continue + yield directory, entry + +class TTKode_SearchFile(): + @staticmethod + def getFilesFromPattern(root_folder:Path, pattern:str) -> Generator[Tuple[Path], None, None]: + for _dir, _fileName in _custom_walk(directory=root_folder): + if not _glob_match_patterns(f"{_dir}/{_fileName}", [f"*{pattern}*"]): + continue + yield Path(_dir) / _fileName diff --git a/apps/ttkode/ttkode/app/ttkode.py b/apps/ttkode/ttkode/app/ttkode.py index e28b69d4..972596e4 100644 --- a/apps/ttkode/ttkode/app/ttkode.py +++ b/apps/ttkode/ttkode/app/ttkode.py @@ -33,6 +33,7 @@ import TermTk as ttk from TermTk.TTkWidgets.tabwidget import _TTkNewTabWidgetDragData from .about import About +from .command_palette.command_palette import TTKode_CommandPalette from .activitybar import TTKodeActivityBar class TTKodeWidget(): @@ -257,6 +258,9 @@ class TTKode(ttk.TTkGridLayout): def _showAboutTTk(btn): ttk.TTkHelper.overlay(None, ttk.TTkAbout(), 30,10) + appMenuBar.addMenu("TTKode", alignment=ttk.TTkK.CENTER_ALIGN).menuButtonClicked.connect(self.showCommandPalette) + + appMenuBar.addMenu("&Quit", alignment=ttk.TTkK.RIGHT_ALIGN).menuButtonClicked.connect(self._quit) helpMenu = appMenuBar.addMenu("&Help", alignment=ttk.TTkK.RIGHT_ALIGN) helpMenu.addMenu("About ...").menuButtonClicked.connect(_showAbout) @@ -301,6 +305,7 @@ class TTKode(ttk.TTkGridLayout): self._kodeTab.kodeTabCloseRequested.connect(self._handleTabCloseRequested) ttk.TTkShortcut(ttk.TTkK.CTRL | ttk.TTkK.Key_S).activated.connect(self.saveLastDoc) + ttk.TTkShortcut(ttk.TTkK.CTRL | ttk.TTkK.Key_P).activated.connect(self.showCommandPalette) @ttk.pyTTkSlot(_TextDocument) @@ -331,6 +336,12 @@ class TTKode(ttk.TTkGridLayout): else: ttk.TTkHelper.quit() + @ttk.pyTTkSlot() + def showCommandPalette(self): + w,h = self.size() + cp = TTKode_CommandPalette(size=(60,20)) + ttk.TTkHelper.overlay(None, cp, w//2-30,0) + @ttk.pyTTkSlot() def saveLastDoc(self): if self._lastDoc: diff --git a/apps/ttkode/ttkode/plugins/_010/findwidget.py b/apps/ttkode/ttkode/plugins/_010/findwidget.py index 680e9f41..5f279990 100644 --- a/apps/ttkode/ttkode/plugins/_010/findwidget.py +++ b/apps/ttkode/ttkode/plugins/_010/findwidget.py @@ -33,14 +33,9 @@ from typing import Generator,List,Tuple,Dict,Optional import TermTk as ttk -import os -import fnmatch - from ttkode.proxy import ttkodeProxy from ttkode.app.ttkode import TTKodeFileWidgetItem -import mimetypes - def is_text_file(file_path, block_size=512): # Check MIME type mime_type, _ = mimetypes.guess_type(file_path) @@ -325,8 +320,6 @@ class FindWidget(ttk.TTkContainer): if not search_pattern: self._results_tree.clear() return - if self._search_thread: - self._search_thread.join() self._search_thread = Thread( target=self._search_threading, args=(search_pattern, include_patterns, exclude_patterns, replace_pattern)) diff --git a/tests/t.generic/test.generic.014.pathlib.01.py b/tests/t.generic/test.generic.014.pathlib.01.py new file mode 100755 index 00000000..0d4d6a6c --- /dev/null +++ b/tests/t.generic/test.generic.014.pathlib.01.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2026 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 pathlib import Path + + +root = Path('.') + +for _p in root.glob('**/*ttkwidgets/widget.py*', case_sensitive=False): + print(_p) +