Browse Source

chore: basic command palette implementation (#598)

pull/469/merge
Pier CeccoPierangioliEugenio 3 weeks ago committed by GitHub
parent
commit
c5d0e51aa4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 0
      apps/ttkode/ttkode/app/command_palette/__init__.py
  2. 239
      apps/ttkode/ttkode/app/command_palette/command_palette.py
  3. 70
      apps/ttkode/ttkode/app/command_palette/command_palette_items.py
  4. 95
      apps/ttkode/ttkode/app/command_palette/search_file_threading.py
  5. 0
      apps/ttkode/ttkode/app/helpers/__init__.py
  6. 98
      apps/ttkode/ttkode/app/helpers/search_file.py
  7. 11
      apps/ttkode/ttkode/app/ttkode.py
  8. 7
      apps/ttkode/ttkode/plugins/_010/findwidget.py
  9. 32
      tests/t.generic/test.generic.014.pathlib.01.py

0
apps/ttkode/ttkode/app/command_palette/__init__.py

239
apps/ttkode/ttkode/app/command_palette/command_palette.py

@ -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='')

70
apps/ttkode/ttkode/app/command_palette/command_palette_items.py

@ -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

95
apps/ttkode/ttkode/app/command_palette/search_file_threading.py

@ -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
apps/ttkode/ttkode/app/helpers/__init__.py

98
apps/ttkode/ttkode/app/helpers/search_file.py

@ -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

11
apps/ttkode/ttkode/app/ttkode.py

@ -33,6 +33,7 @@ import TermTk as ttk
from TermTk.TTkWidgets.tabwidget import _TTkNewTabWidgetDragData from TermTk.TTkWidgets.tabwidget import _TTkNewTabWidgetDragData
from .about import About from .about import About
from .command_palette.command_palette import TTKode_CommandPalette
from .activitybar import TTKodeActivityBar from .activitybar import TTKodeActivityBar
class TTKodeWidget(): class TTKodeWidget():
@ -257,6 +258,9 @@ class TTKode(ttk.TTkGridLayout):
def _showAboutTTk(btn): def _showAboutTTk(btn):
ttk.TTkHelper.overlay(None, ttk.TTkAbout(), 30,10) 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) appMenuBar.addMenu("&Quit", alignment=ttk.TTkK.RIGHT_ALIGN).menuButtonClicked.connect(self._quit)
helpMenu = appMenuBar.addMenu("&Help", alignment=ttk.TTkK.RIGHT_ALIGN) helpMenu = appMenuBar.addMenu("&Help", alignment=ttk.TTkK.RIGHT_ALIGN)
helpMenu.addMenu("About ...").menuButtonClicked.connect(_showAbout) helpMenu.addMenu("About ...").menuButtonClicked.connect(_showAbout)
@ -301,6 +305,7 @@ class TTKode(ttk.TTkGridLayout):
self._kodeTab.kodeTabCloseRequested.connect(self._handleTabCloseRequested) 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_S).activated.connect(self.saveLastDoc)
ttk.TTkShortcut(ttk.TTkK.CTRL | ttk.TTkK.Key_P).activated.connect(self.showCommandPalette)
@ttk.pyTTkSlot(_TextDocument) @ttk.pyTTkSlot(_TextDocument)
@ -331,6 +336,12 @@ class TTKode(ttk.TTkGridLayout):
else: else:
ttk.TTkHelper.quit() 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() @ttk.pyTTkSlot()
def saveLastDoc(self): def saveLastDoc(self):
if self._lastDoc: if self._lastDoc:

7
apps/ttkode/ttkode/plugins/_010/findwidget.py

@ -33,14 +33,9 @@ from typing import Generator,List,Tuple,Dict,Optional
import TermTk as ttk import TermTk as ttk
import os
import fnmatch
from ttkode.proxy import ttkodeProxy from ttkode.proxy import ttkodeProxy
from ttkode.app.ttkode import TTKodeFileWidgetItem from ttkode.app.ttkode import TTKodeFileWidgetItem
import mimetypes
def is_text_file(file_path, block_size=512): def is_text_file(file_path, block_size=512):
# Check MIME type # Check MIME type
mime_type, _ = mimetypes.guess_type(file_path) mime_type, _ = mimetypes.guess_type(file_path)
@ -325,8 +320,6 @@ class FindWidget(ttk.TTkContainer):
if not search_pattern: if not search_pattern:
self._results_tree.clear() self._results_tree.clear()
return return
if self._search_thread:
self._search_thread.join()
self._search_thread = Thread( self._search_thread = Thread(
target=self._search_threading, target=self._search_threading,
args=(search_pattern, include_patterns, exclude_patterns, replace_pattern)) args=(search_pattern, include_patterns, exclude_patterns, replace_pattern))

32
tests/t.generic/test.generic.014.pathlib.01.py

@ -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…
Cancel
Save