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