9 changed files with 545 additions and 7 deletions
@ -0,0 +1,239 @@
|
||||
# MIT License |
||||
# |
||||
# Copyright (c) 2026 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com> |
||||
# |
||||
# 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='╯') |
||||
@ -0,0 +1,70 @@
|
||||
# MIT License |
||||
# |
||||
# Copyright (c) 2026 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com> |
||||
# |
||||
# 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 |
||||
@ -0,0 +1,95 @@
|
||||
# MIT License |
||||
# |
||||
# Copyright (c) 2026 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com> |
||||
# |
||||
# 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) |
||||
@ -0,0 +1,98 @@
|
||||
# MIT License |
||||
# |
||||
# Copyright (c) 2026 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com> |
||||
# |
||||
# 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 |
||||
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3 |
||||
|
||||
# MIT License |
||||
# |
||||
# Copyright (c) 2026 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com> |
||||
# |
||||
# 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) |
||||
|
||||
Loading…
Reference in new issue