ttkode 0.3.2a2__tar.gz → 0.5.0a2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/PKG-INFO +2 -2
  2. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/pyproject.toml +1 -1
  3. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode/__init__.py +1 -1
  4. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode/__main__.py +1 -5
  5. ttkode-0.5.0a2/ttkode/app/__init__.py +27 -0
  6. ttkode-0.3.2a2/ttkode/app/__init__.py → ttkode-0.5.0a2/ttkode/app/state.py +4 -4
  7. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode/app/ttkode.py +195 -41
  8. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode/plugins/_010/findwidget.py +146 -56
  9. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode/proxy.py +17 -13
  10. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode.egg-info/PKG-INFO +2 -2
  11. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode.egg-info/SOURCES.txt +1 -0
  12. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode.egg-info/requires.txt +1 -1
  13. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/README.md +0 -0
  14. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/setup.cfg +0 -0
  15. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode/app/about.py +0 -0
  16. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode/app/activitybar.py +0 -0
  17. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode/app/cfg.py +0 -0
  18. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode/app/options.py +0 -0
  19. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode/helper.py +0 -0
  20. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode/plugin.py +0 -0
  21. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode/plugins/_010/__init__.py +0 -0
  22. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode/plugins/_010_findplugin.py +0 -0
  23. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode/plugins/_020_debugplugin.py +0 -0
  24. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode/plugins/testplugin.py +0 -0
  25. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode/plugins/testplugin1.py +0 -0
  26. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode.egg-info/dependency_links.txt +0 -0
  27. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode.egg-info/entry_points.txt +0 -0
  28. {ttkode-0.3.2a2 → ttkode-0.5.0a2}/ttkode.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ttkode
3
- Version: 0.3.2a2
3
+ Version: 0.5.0a2
4
4
  Summary: Terminal ToolKit Studio Code editor
5
5
  Author-email: Eugenio Parodi <ceccopierangiolieugenio@googlemail.com>
6
6
  License: MIT
@@ -19,7 +19,7 @@ Classifier: Topic :: Terminals
19
19
  Classifier: Topic :: Software Development :: User Interfaces
20
20
  Requires-Python: >=3.9
21
21
  Description-Content-Type: text/markdown
22
- Requires-Dist: pyTermTk>=0.42.1-a.0
22
+ Requires-Dist: pyTermTk>=0.46.0-a.0
23
23
  Requires-Dist: appdirs
24
24
  Requires-Dist: copykitten
25
25
  Requires-Dist: pygments
@@ -24,7 +24,7 @@ classifiers = [
24
24
  "Topic :: Software Development :: User Interfaces",
25
25
  ]
26
26
  dependencies = [
27
- 'pyTermTk>=0.42.1-a.0',
27
+ 'pyTermTk>=0.46.0-a.0',
28
28
  'appdirs',
29
29
  'copykitten',
30
30
  'pygments'
@@ -22,7 +22,7 @@
22
22
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
23
  # SOFTWARE.
24
24
 
25
- __version__:str = '0.3.2-a.2'
25
+ __version__:str = '0.5.0-a.2'
26
26
 
27
27
  from .plugin import TTkodePlugin, TTkodePluginActivity
28
28
  from .proxy import TTKodeViewerProxy, ttkodeProxy
@@ -33,7 +33,6 @@ from TermTk import TTkLog
33
33
 
34
34
  from ttkode import TTkodeHelper
35
35
  from ttkode import ttkodeProxy
36
- from ttkode.app.ttkode import TTKode
37
36
  from ttkode.app.cfg import TTKodeCfg
38
37
 
39
38
 
@@ -62,10 +61,7 @@ def main():
62
61
 
63
62
  TTkodeHelper._loadPlugins()
64
63
 
65
- ttkode = TTKode()
66
- ttkodeProxy.setTTKode(ttkode)
67
-
68
- root = TTk( layout=ttkode,
64
+ root = TTk( layout=ttkodeProxy.ttkode(),
69
65
  title="TTkode",
70
66
  mouseTrack=True,
71
67
  sigmask=(
@@ -0,0 +1,27 @@
1
+
2
+ # MIT License
3
+ #
4
+ # Copyright (c) 2021 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in all
14
+ # copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+
24
+ from .cfg import *
25
+ from .state import TTKodeState
26
+ # from .glbl import *
27
+ from .ttkode import TTKode, TTKodeWidget
@@ -1,4 +1,3 @@
1
-
2
1
  # MIT License
3
2
  #
4
3
  # Copyright (c) 2021 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
@@ -21,6 +20,7 @@
21
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
21
  # SOFTWARE.
23
22
 
24
- from .cfg import *
25
- # from .glbl import *
26
- from .ttkode import *
23
+ __all__ = ['TTKodeState']
24
+
25
+ class TTKodeState():
26
+ pass
@@ -20,7 +20,7 @@
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  # SOFTWARE.
22
22
 
23
- __all__ = ['TTKode']
23
+ __all__ = ['TTKode', 'TTKodeWidget']
24
24
 
25
25
  import os
26
26
  from typing import List,Tuple,Optional,Any
@@ -31,6 +31,10 @@ from TermTk.TTkWidgets.tabwidget import _TTkNewTabWidgetDragData
31
31
  from .about import About
32
32
  from .activitybar import TTKodeActivityBar
33
33
 
34
+ class TTKodeWidget():
35
+ def closeRequested(self, tab:ttk.TTkTabWidget, num:int):
36
+ raise NotImplementedError()
37
+
34
38
  class TTKodeFileWidgetItem(ttk.TTkTreeWidgetItem):
35
39
  __slots__ = ('_path', '_lineNumber')
36
40
  def __init__(self, *args, path:str, lineNumber:int=0, **kwargs) -> None:
@@ -45,16 +49,24 @@ class TTKodeFileWidgetItem(ttk.TTkTreeWidgetItem):
45
49
  class _TextDocument(ttk.TextDocumentHighlight):
46
50
  __slots__ = ('_filePath', '_tabText',
47
51
  'fileChangedStatus', '_changedStatus', '_savedSnapshot')
48
- def __init__(self, filePath:str="", tabText:ttk.TTkString=ttk.TTkString(), **kwargs):
52
+ def __init__(self, filePath:str="", **kwargs):
49
53
  self.fileChangedStatus:ttk.pyTTkSignal = ttk.pyTTkSignal(bool, _TextDocument)
50
54
  self._filePath:str = filePath
51
- self._tabText = tabText
52
55
  self._changedStatus:bool = False
56
+ self._genTabText()
53
57
  super().__init__(**kwargs)
54
58
  self._savedSnapshot = self.snapshootId()
55
59
  self.guessLexerFromFilename(filePath)
56
60
  self.contentsChanged.connect(self._handleContentChanged)
57
61
 
62
+ def _genTabText(self):
63
+ self._tabText = (
64
+ ttk.TTkString(ttk.TTkCfg.theme.fileIcon.getIcon(self._filePath),ttk.TTkCfg.theme.fileIconColor) +
65
+ ttk.TTkColor.RST + " " + os.path.basename(self._filePath) )
66
+
67
+ def isChanged(self) -> bool:
68
+ return self._changedStatus
69
+
58
70
  def getTabButtonStyle(self) -> dict:
59
71
  if self._changedStatus:
60
72
  return {'default':{'closeGlyph':' ● '}}
@@ -70,17 +82,67 @@ class _TextDocument(ttk.TextDocumentHighlight):
70
82
  ttk.TTkLog.debug(f"{self.isUndoAvailable()=} == {self._changedStatus=}")
71
83
  self.fileChangedStatus.emit(self._changedStatus, self)
72
84
 
73
- def save(self):
74
- self._changedStatus = False
75
- self._savedSnapshot = self.snapshootId()
76
- self.fileChangedStatus.emit(self._changedStatus, self)
77
- pass
78
-
79
- class _TextEdit(ttk.TTkTextEdit):
85
+ def save(self) -> None:
86
+ try:
87
+ with open(self._filePath, 'w') as f:
88
+ f.write(self.toPlainText())
89
+ self._changedStatus = False
90
+ self._savedSnapshot = self.snapshootId()
91
+ self.fileChangedStatus.emit(self._changedStatus, self)
92
+ except Exception as e:
93
+ ttk.TTkLog.error(f"Error saving file: {e}")
94
+
95
+ def saveToFile(self, fileName:str) -> None:
96
+ self._filePath = fileName
97
+ self._genTabText()
98
+ self.save()
99
+ class _TextEdit(ttk.TTkTextEdit, TTKodeWidget):
100
+ __slots__ = ('docFocussed')
80
101
  def __init__(self, **kwargs):
102
+ self.docFocussed = ttk.pyTTkSignal(_TextDocument)
81
103
  super().__init__(**kwargs)
82
104
  self.cursorPositionChanged.connect(self._positionChanged)
83
-
105
+ self.textEditView().focusChanged.connect(self._handleFocusChanged)
106
+
107
+ @ttk.pyTTkSlot(bool)
108
+ def _handleFocusChanged(self, focus:bool) -> None:
109
+ if focus:
110
+ self.docFocussed.emit(self.document())
111
+
112
+ def closeRequested(self, tab:ttk.TTkTabWidget, num:int):
113
+ from ttkode import ttkodeProxy
114
+ doc = self.document()
115
+ docs = [wid.document() for wid in ttkodeProxy.iterWidgets(_TextEdit) if wid is not self]
116
+ if not doc.isChanged() or doc in docs:
117
+ ttkodeProxy.closeTab(self)
118
+ else:
119
+ pass
120
+ # Do you want to save the changes you made to ""?
121
+ # Your saves will be lost if you don't save them.
122
+ # Save, Don't Save, Cancel
123
+ messageBox = ttk.TTkMessageBox(
124
+ title="🚨 Close? 🚨",
125
+ text=ttk.TTkString(f"Do you want to save the change\nyou made to {os.path.basename(doc._filePath)}?\n\nYour saves will be lost\nif you don't save them."),
126
+ icon=ttk.TTkMessageBox.Icon.Warning,
127
+ standardButtons=
128
+ ttk.TTkMessageBox.StandardButton.Discard|
129
+ ttk.TTkMessageBox.StandardButton.Save|
130
+ ttk.TTkMessageBox.StandardButton.Cancel)
131
+
132
+ @ttk.pyTTkSlot(ttk.TTkMessageBox.StandardButton)
133
+ def _cb(btn):
134
+ if btn == ttk.TTkMessageBox.StandardButton.Save:
135
+ doc.save()
136
+ self.textEditView().focusChanged.clear()
137
+ ttkodeProxy.closeTab(self)
138
+ if btn == ttk.TTkMessageBox.StandardButton.Discard:
139
+ self.textEditView().focusChanged.clear()
140
+ ttkodeProxy.closeTab(self)
141
+ elif btn == ttk.TTkMessageBox.StandardButton.Cancel:
142
+ return
143
+ messageBox.buttonSelected.clear()
144
+ messageBox.buttonSelected.connect(_cb)
145
+ ttk.TTkHelper.overlay(None, messageBox, 5, 5, True)
84
146
 
85
147
  @ttk.pyTTkSlot(ttk.TTkTextCursor)
86
148
  def _positionChanged(self, cursor:ttk.TTkTextCursor):
@@ -106,8 +168,10 @@ class _TextEdit(ttk.TTkTextEdit):
106
168
  tedit.textCursor().setPosition(line=linenum,pos=0)
107
169
 
108
170
  class TTKode(ttk.TTkGridLayout):
109
- __slots__ = ('_kodeTab', '_activityBar')
171
+ __slots__ = ('_kodeTab', '_activityBar', '_lastDoc')
172
+ _lastDoc:Optional[_TextDocument]
110
173
  def __init__(self, **kwargs):
174
+ self._lastDoc = None
111
175
  super().__init__(**kwargs)
112
176
 
113
177
  appTemplate = ttk.TTkAppTemplate(border=False)
@@ -119,20 +183,22 @@ class TTKode(ttk.TTkGridLayout):
119
183
  appTemplate.setMenuBar(appMenuBar:=ttk.TTkMenuBarLayout(), ttk.TTkAppTemplate.MAIN)
120
184
  fileMenu = appMenuBar.addMenu("&File")
121
185
  fileMenu.addMenu("Open").menuButtonClicked.connect(self._showFileDialog)
186
+ fileMenu.addMenu("&Save").menuButtonClicked.connect(self.saveLastDoc)
187
+ fileMenu.addMenu("Save &As...").menuButtonClicked.connect(self.saveLastDocAs)
122
188
  fileMenu.addMenu("Close") # .menuButtonClicked.connect(self._closeFile)
123
- fileMenu.addMenu("Exit").menuButtonClicked.connect(lambda _:ttk.TTkHelper.quit())
189
+ fileMenu.addMenu("Exit").menuButtonClicked.connect(self._quit)
124
190
 
125
191
  def _showAbout(btn):
126
192
  ttk.TTkHelper.overlay(None, About(), 30,10)
127
193
  def _showAboutTTk(btn):
128
194
  ttk.TTkHelper.overlay(None, ttk.TTkAbout(), 30,10)
129
195
 
130
- appMenuBar.addMenu("&Quit", alignment=ttk.TTkK.RIGHT_ALIGN).menuButtonClicked.connect(ttk.TTkHelper.quit)
196
+ appMenuBar.addMenu("&Quit", alignment=ttk.TTkK.RIGHT_ALIGN).menuButtonClicked.connect(self._quit)
131
197
  helpMenu = appMenuBar.addMenu("&Help", alignment=ttk.TTkK.RIGHT_ALIGN)
132
198
  helpMenu.addMenu("About ...").menuButtonClicked.connect(_showAbout)
133
199
  helpMenu.addMenu("About ttk").menuButtonClicked.connect(_showAboutTTk)
134
200
 
135
- fileTree = ttk.TTkFileTree(path='.', dragDropMode=ttk.TTkK.DragDropMode.AllowDrag)
201
+ fileTree = ttk.TTkFileTree(path='.', dragDropMode=ttk.TTkK.DragDropMode.AllowDrag, selectionMode=ttk.TTkK.SelectionMode.MultiSelection)
136
202
  self._activityBar = TTKodeActivityBar()
137
203
  self._activityBar.addActivity(name="Explorer", icon=ttk.TTkString("╔██\n╚═╝"), widget=fileTree, select=True)
138
204
 
@@ -159,11 +225,88 @@ class TTKode(ttk.TTkGridLayout):
159
225
 
160
226
  fileTree.fileActivated.connect(lambda x: self._openFile(x.path()))
161
227
  self._kodeTab.tabAdded.connect(self._tabAdded)
228
+ self._kodeTab.kodeTabCloseRequested.connect(self._handleTabCloseRequested)
229
+
230
+ ttk.TTkShortcut(ttk.TTkK.CTRL | ttk.TTkK.Key_S).activated.connect(self.saveLastDoc)
231
+
232
+
233
+ @ttk.pyTTkSlot(_TextDocument)
234
+ def _handleDocFocussed(self, doc:_TextDocument):
235
+ self._lastDoc = doc
236
+
237
+ @ttk.pyTTkSlot()
238
+ def _quit(self):
239
+ from ttkode import ttkodeProxy
240
+ docs = set(os.path.basename(wid.document()._filePath) for wid in ttkodeProxy.iterWidgets(_TextEdit) if wid.document().isChanged())
241
+ if docs:
242
+ messageBox = ttk.TTkMessageBox(
243
+ title="🚨 Quit? 🚨",
244
+ text=ttk.TTkString("Do you want to quit?\nThere are still unsaved documents,\nif you quit those changes will be lost" + ''.join([f"\n - {_d}" for _d in docs])),
245
+ icon=ttk.TTkMessageBox.Icon.Warning,
246
+ standardButtons=
247
+ ttk.TTkMessageBox.StandardButton.Ok|
248
+ ttk.TTkMessageBox.StandardButton.Cancel)
249
+ @ttk.pyTTkSlot(ttk.TTkMessageBox.StandardButton)
250
+ def _cb(btn):
251
+ if btn == ttk.TTkMessageBox.StandardButton.Ok:
252
+ ttk.TTkHelper.quit()
253
+ elif btn == ttk.TTkMessageBox.StandardButton.Cancel:
254
+ return
255
+ messageBox.buttonSelected.clear()
256
+ messageBox.buttonSelected.connect(_cb)
257
+ ttk.TTkHelper.overlay(None, messageBox, 5, 5, True)
258
+ else:
259
+ ttk.TTkHelper.quit()
260
+
261
+ @ttk.pyTTkSlot()
262
+ def saveLastDoc(self):
263
+ if self._lastDoc:
264
+ self._lastDoc.save()
265
+
266
+ @ttk.pyTTkSlot()
267
+ def saveLastDocAs(self):
268
+ if not self._lastDoc:
269
+ return
270
+ def _approveFile(fileName):
271
+ if os.path.exists(fileName):
272
+ @ttk.pyTTkSlot(ttk.TTkMessageBox.StandardButton)
273
+ def _cb(btn):
274
+ if btn == ttk.TTkMessageBox.StandardButton.Save:
275
+ self._lastDoc.saveToFile(fileName)
276
+ elif btn == ttk.TTkMessageBox.StandardButton.Cancel:
277
+ return
278
+ messageBox = ttk.TTkMessageBox(
279
+ text= (
280
+ ttk.TTkString( f'A file named "{os.path.basename(fileName)}" already exists.\nDo you want to replace it?', ttk.TTkColor.BOLD) +
281
+ ttk.TTkString( f'\n\nReplacing it will overwrite its contents.') ),
282
+ icon=ttk.TTkMessageBox.Icon.Warning,
283
+ standardButtons=
284
+ ttk.TTkMessageBox.StandardButton.Discard|
285
+ ttk.TTkMessageBox.StandardButton.Save|
286
+ ttk.TTkMessageBox.StandardButton.Cancel)
287
+ messageBox.buttonSelected.connect(_cb)
288
+ ttk.TTkHelper.overlay(None, messageBox, 5, 5, True)
289
+ else:
290
+ self._lastDoc.saveToFile(fileName)
291
+ filePicker = ttk.TTkFileDialogPicker(
292
+ pos = (3,3), size=(80,30),
293
+ acceptMode=ttk.TTkK.AcceptMode.AcceptSave,
294
+ caption="Save As...",
295
+ path=self._lastDoc._filePath,
296
+ fileMode=ttk.TTkK.FileMode.AnyFile ,
297
+ filter="All Files (*)")
298
+ filePicker.pathPicked.connect(_approveFile)
299
+ ttk.TTkHelper.overlay(None, filePicker, 5, 5, True)
300
+
301
+ @ttk.pyTTkSlot(ttk.TTkTabWidget, int)
302
+ def _handleTabCloseRequested(self, tab:ttk.TTkTabWidget, num:int):
303
+ # tab.removeTab(num)
304
+ tab.widget(num).closeRequested(tab, num)
162
305
 
163
306
  def _getTabButtonFromWidget(self, widget:ttk.TTkWidget) -> ttk.TTkTabButton:
164
- for item, tab in self._kodeTab.iterWidgets():
165
- if item == widget:
166
- return tab
307
+ for kt, index in self._kodeTab.iterItems():
308
+ if kt.widget(index) == widget:
309
+ return kt.tabButton(index)
167
310
  return None
168
311
 
169
312
  ttk.pyTTkSlot(ttk.TTkTabWidget, int)
@@ -180,34 +323,37 @@ class TTKode(ttk.TTkGridLayout):
180
323
  ttk.TTkHelper.overlay(None, filePicker, 20, 5, True)
181
324
 
182
325
  def _getDocument(self, filePath) -> Tuple[_TextDocument, Optional[_TextEdit]]:
183
- for item, _ in self._kodeTab.iterWidgets():
184
- if issubclass(type(item), _TextEdit):
185
- doc = item.document()
326
+ for kt, index in self._kodeTab.iterItems():
327
+ if issubclass(type(wid:=kt.widget(index)), _TextEdit):
328
+ doc = wid.document()
186
329
  if issubclass(type(doc), _TextDocument):
187
330
  if filePath == doc._filePath:
188
- return doc, item
331
+ return doc, wid
189
332
  with open(filePath, 'r') as f:
190
333
  content = f.read()
191
- tabText = ttk.TTkString(ttk.TTkCfg.theme.fileIcon.getIcon(filePath),ttk.TTkCfg.theme.fileIconColor) + ttk.TTkColor.RST + " " + os.path.basename(filePath)
192
- td = _TextDocument(text=content, filePath=filePath, tabText=tabText)
334
+ td = _TextDocument(text=content, filePath=filePath)
193
335
  td.fileChangedStatus.connect(self._handleFileChangedStatus)
194
336
  return td, None
195
337
 
196
338
  ttk.pyTTkSlot(bool, _TextDocument)
197
339
  def _handleFileChangedStatus(self, status:bool, doc:_TextDocument) -> None:
198
340
  # ttk.TTkLog.debug(f"Status ({status}) -> {doc._filePath}")
199
- for item, tab in self._kodeTab.iterWidgets():
200
- if issubclass(type(item), _TextEdit):
201
- if doc == item.document():
202
- tab.mergeStyle(doc.getTabButtonStyle())
341
+ for kt, index in self._kodeTab.iterItems():
342
+ if issubclass(type(wid:=kt.widget(index)), _TextEdit):
343
+ if doc == wid.document():
344
+ kt.tabButton(index).mergeStyle(doc.getTabButtonStyle())
345
+ kt.tabButton(index).setText(doc._tabText)
203
346
 
204
- def _openFile(self, filePath, line:int=0, pos:int=0):
347
+ def _openFile(self, filePath, line:int=0, pos:int=0) -> None:
205
348
  filePath = os.path.realpath(filePath)
349
+ if not os.path.isfile(filePath):
350
+ return
206
351
  doc, tedit = self._getDocument(filePath=filePath)
207
352
  if tedit:
208
353
  self._kodeTab.setCurrentWidget(tedit)
209
354
  else:
210
355
  tedit = _TextEdit(document=doc, readOnly=False, lineNumber=True)
356
+ tedit.docFocussed.connect(self._handleDocFocussed)
211
357
  self._kodeTab.addTab(tedit, doc._tabText)
212
358
  self._kodeTab.setCurrentWidget(tedit)
213
359
  tedit.goToLine(line)
@@ -215,28 +361,36 @@ class TTKode(ttk.TTkGridLayout):
215
361
 
216
362
  def _dropEventProxyFile(self, evt:ttk.TTkDnDEvent):
217
363
  data = evt.data()
218
- filePath = None
364
+ filePaths = []
365
+ linenums = []
219
366
 
220
367
  if ( issubclass(type(data), ttk.TTkTreeWidget._DropTreeData) and data.items ):
221
368
  if issubclass(type(data.items[0]), ttk.TTkFileTreeWidgetItem):
222
- linenum:int = 0
223
- ftwi:ttk.TTkFileTreeWidgetItem = data.items[0]
224
- filePath = os.path.realpath(ftwi.path())
369
+ ftwis:List[ttk.TTkFileTreeWidgetItem] = data.items
370
+ filePaths = [os.path.realpath(_f.path()) for _f in ftwis]
371
+ linenums:List[int] = [0]*len(filePaths)
225
372
  elif issubclass(type(data.items[0]), TTKodeFileWidgetItem):
226
373
  kfwi:TTKodeFileWidgetItem = data.items[0]
227
- linenum:int = kfwi.lineNumber()
228
- filePath = os.path.realpath(kfwi.path())
374
+ linenums:List[int] = [kfwi.lineNumber()]
375
+ filePaths = [os.path.realpath(kfwi.path())]
229
376
 
230
- if filePath:
377
+ newData = []
378
+ for filePath,linenum in zip(filePaths,linenums) :
379
+ if not os.path.isfile(filePath):
380
+ continue
231
381
  doc, _ = self._getDocument(filePath=filePath)
232
382
  tedit = _TextEdit(document=doc, readOnly=False, lineNumber=True)
383
+ tedit.docFocussed.connect(self._handleDocFocussed)
233
384
  tedit.goToLine(linenum)
234
- newData = _TTkNewTabWidgetDragData(
235
- widget=tedit,
236
- label=doc._tabText,
237
- data=None,
238
- closable=True
385
+ newData.append(
386
+ _TTkNewTabWidgetDragData(
387
+ widget=tedit,
388
+ label=doc._tabText,
389
+ data=None,
390
+ closable=True
391
+ )
239
392
  )
393
+ if newData:
240
394
  newEvt = evt.clone()
241
395
  newEvt.setData(newData)
242
396
  return newEvt
@@ -27,8 +27,9 @@ import re
27
27
  import fnmatch
28
28
  import mimetypes
29
29
 
30
- from threading import Thread
31
- from typing import Generator,List,Tuple
30
+ from pathlib import Path
31
+ from threading import Thread,Event,Lock
32
+ from typing import Generator,List,Tuple,Dict,Optional
32
33
 
33
34
  import TermTk as ttk
34
35
 
@@ -70,24 +71,32 @@ def _load_gitignore_patterns(gitignore_path):
70
71
  return patterns
71
72
  return []
72
73
 
73
- def _should_ignore(path, patterns):
74
- for pattern in patterns:
75
- if fnmatch.fnmatch(path, pattern):
76
- return True
77
- return False
74
+ def _glob_match_patterns(path, patterns) -> bool:
75
+ if path == '.':
76
+ check_path = ''
77
+ elif path.startswith('./'):
78
+ check_path = path[2:]
79
+ else:
80
+ check_path = path
81
+ 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)
78
82
 
79
- def _custom_walk(directory:str, patterns:List[str]=[]) -> Generator[Tuple[str, str], None, None]:
83
+ def _custom_walk(directory:str, include_patterns:List[str]=[], exclude_patterns:List[str]=[]) -> Generator[Tuple[str, str], None, None]:
80
84
  gitignore_path = os.path.join(directory, '.gitignore')
81
- patterns = patterns + _load_gitignore_patterns(gitignore_path)
85
+ exclude_patterns = exclude_patterns + _load_gitignore_patterns(gitignore_path)
82
86
  for entry in sorted(os.listdir(directory)):
83
87
  full_path = os.path.join(directory, entry)
84
- if _should_ignore(full_path, patterns):
88
+ if _glob_match_patterns(full_path, exclude_patterns):
89
+ continue
90
+ if not os.path.exists(full_path):
85
91
  continue
86
92
  if os.path.isdir(full_path):
87
93
  if entry == '.git':
88
94
  continue
89
- yield from _custom_walk(full_path, patterns)
95
+ yield from _custom_walk(full_path, include_patterns, exclude_patterns)
90
96
  else:
97
+ if include_patterns and not _glob_match_patterns(full_path, include_patterns):
98
+ # ttk.TTkLog.debug(f"{include_patterns=} {full_path=}")
99
+ continue
91
100
  yield directory, entry
92
101
 
93
102
  def _walk_with_gitignore(root):
@@ -95,7 +104,7 @@ def _walk_with_gitignore(root):
95
104
  gitignore_path = os.path.join(dirpath, '.gitignore')
96
105
  patterns = _load_gitignore_patterns(gitignore_path)
97
106
 
98
- filenames[:] = [f for f in filenames if not _should_ignore(os.path.join(dirpath, f), patterns)]
107
+ filenames[:] = [f for f in filenames if not _glob_match_patterns(os.path.join(dirpath, f), patterns)]
99
108
 
100
109
  yield dirpath, filenames
101
110
 
@@ -126,10 +135,16 @@ class _MatchTreeWidgetItem(TTKodeFileWidgetItem):
126
135
 
127
136
  class FindWidget(ttk.TTkContainer):
128
137
  __slots__ = (
129
- '_runId'
138
+ '_runId',
139
+ '_replace_data',
130
140
  '_results_tree',
141
+ '_search_thread', '_search_stop_event', '_search_lock',
131
142
  '_search_le','_replace_le','_files_inc_le','_files_exc_le')
132
143
  _runId:int
144
+ _search_lock:Lock
145
+ _search_thread:Optional[Thread]
146
+ _search_stop_event:Event
147
+ _replace_data:Dict
133
148
  _results_tree:ttk.TTkTreeWidget
134
149
  _search_le:ttk.TTkLineEdit
135
150
  _replace_le:ttk.TTkLineEdit
@@ -137,6 +152,10 @@ class FindWidget(ttk.TTkContainer):
137
152
  _files_exc_le:ttk.TTkLineEdit
138
153
  def __init__(self, **kwargs):
139
154
  self._runId = 0
155
+ self._replace_data = {}
156
+ self._search_thread = None
157
+ self._search_lock = Lock()
158
+ self._search_stop_event = Event()
140
159
  super().__init__(**kwargs)
141
160
  self.setLayout(layout:=ttk.TTkGridLayout())
142
161
 
@@ -177,9 +196,20 @@ class FindWidget(ttk.TTkContainer):
177
196
  self._files_exc_le = ft_excl
178
197
 
179
198
  btn_search.clicked.connect(self._search)
199
+ btn_replace.clicked.connect(self._ask_replace)
180
200
  btn_expand.clicked.connect(self._results_tree.expandAll)
181
201
  btn_collapse.clicked.connect(self._results_tree.collapseAll)
202
+
182
203
  search.returnPressed.connect(self._search)
204
+ replace.returnPressed.connect(self._search)
205
+ ft_incl.returnPressed.connect(self._search)
206
+ ft_excl.returnPressed.connect(self._search)
207
+
208
+ search.textEdited.connect(self._search)
209
+ replace.textEdited.connect(self._search)
210
+ ft_incl.textEdited.connect(self._search)
211
+ ft_excl.textEdited.connect(self._search)
212
+
183
213
  res_tree.itemActivated.connect(self._activated)
184
214
 
185
215
  @ttk.pyTTkSlot(str)
@@ -196,53 +226,113 @@ class FindWidget(ttk.TTkContainer):
196
226
  line = item.lineNumber()
197
227
  ttkodeProxy.openFile(file, line)
198
228
 
199
-
200
229
  @ttk.pyTTkSlot()
201
- def _search(self):
202
- self._runId += 1
203
- search_pattern = str(self._search_le.text())
204
- if not search_pattern:
230
+ def _ask_replace(self) -> None:
231
+ if not self._replace_le.text():
205
232
  return
206
- def _search_threading():
207
- self._results_tree.clear()
208
- group = []
209
- groupSize = 1
210
- for (file,root,matches) in self._search_files('.',str(search_pattern),self._runId):
211
- ttk.TTkLog.debug((file,matches))
212
- item = ttk.TTkTreeWidgetItem([
213
- ttk.TTkString(
214
- ttk.TTkCfg.theme.fileIcon.getIcon(file),
215
- ttk.TTkCfg.theme.fileIconColor) + " " +
216
- ttk.TTkString(f" {file} ", ttk.TTkColor.YELLOW+ttk.TTkColor.BOLD+ttk.TTkColor.bg('#000088')) +
217
- ttk.TTkString(f" {root} ", ttk.TTkColor.fg("#888888"))
218
- ],expanded=True)
219
- for num,line in matches:
220
- line = line.lstrip(' ')
221
- # index = line.find(search_pattern)
222
- # outLine =
223
- item.addChild(
224
- _MatchTreeWidgetItem([
225
- ttk.TTkString(str(num)+" ",ttk.TTkColor.CYAN) +
226
- ttk.TTkString(line.replace('\n','')).completeColor(
227
- match=search_pattern,
228
- color=ttk.TTkColor.GREEN)
229
- ],
230
- match=line,
231
- lineNumber=num,
232
- path=os.path.join(root,file)))
233
- group.append(item)
234
- if len(group) > groupSize:
235
- self._results_tree.addTopLevelItems(group)
236
- group = []
237
- groupSize <<= 1
238
- # self._results_tree.addTopLevelItem(item)
239
- if group:
233
+ _numFiles = len(self._replace_data.get('files',[]))
234
+ _numMatches = sum(len(_r.get('matches',[])) for _r in self._replace_data.get('files',[]))
235
+ _search_pattern = str(self._search_le.text())
236
+ _replace_pattern = str(self._replace_le.text())
237
+
238
+ messageBox = ttk.TTkMessageBox(
239
+ title="🚨 Apply? 🚨",
240
+ text=ttk.TTkString(f"Do you want to repace {_numMatches} occurrences of\n'{_search_pattern}'\nin {_numFiles} files with\n'{_replace_pattern}'?"),
241
+ icon=ttk.TTkMessageBox.Icon.Warning,
242
+ standardButtons=
243
+ ttk.TTkMessageBox.StandardButton.Ok|
244
+ ttk.TTkMessageBox.StandardButton.Cancel)
245
+
246
+ @ttk.pyTTkSlot(ttk.TTkMessageBox.StandardButton)
247
+ def _cb(btn):
248
+ if btn == ttk.TTkMessageBox.StandardButton.Ok:
249
+ ttk.TTkLog.debug(f"Replace '{_search_pattern}' with '{_replace_pattern}'")
250
+ for _file_def in self._replace_data.get('files',[]):
251
+ _file = Path(_file_def['root']) / _file_def['file']
252
+ if not self._replace_data.get('files',[]):
253
+ ttk.TTkLog.error(f"{_file} does not exists!!!")
254
+ else:
255
+ _content = _file.read_text()
256
+ _new_content = _content.replace(_search_pattern,_replace_pattern)
257
+ _file.write_text(_new_content)
258
+ else:
259
+ ttk.TTkLog.debug(f"Discard")
260
+ messageBox.buttonSelected.connect(_cb)
261
+ ttk.TTkHelper.overlay(None, messageBox, 5, 5, True)
262
+
263
+ def _search_threading(self, search_pattern:str, include_patterns:str, exclude_patterns:str, replace_pattern:str) -> None:
264
+ self._results_tree.clear()
265
+ group = []
266
+ groupSize = 1
267
+ self._replace_data = {'files':[]}
268
+ for (file,root,matches) in self._search_files('.',search_pattern,self._runId,include_patterns,exclude_patterns):
269
+ if self._search_stop_event.is_set():
270
+ return
271
+
272
+ self._replace_data['files'].append({'file':file,'root':root,'matches':matches})
273
+ # ttk.TTkLog.debug((file,matches))
274
+ item = ttk.TTkTreeWidgetItem([
275
+ ttk.TTkString(
276
+ ttk.TTkCfg.theme.fileIcon.getIcon(file),
277
+ ttk.TTkCfg.theme.fileIconColor) + " " +
278
+ ttk.TTkString(f" {file} ", ttk.TTkColor.YELLOW+ttk.TTkColor.BOLD+ttk.TTkColor.bg('#000088')) +
279
+ ttk.TTkString(f" {root} ", ttk.TTkColor.fg("#888888"))
280
+ ],expanded=True)
281
+ children = []
282
+ for num,line in matches:
283
+ line = line.lstrip(' ')
284
+ # index = line.find(search_pattern)
285
+ # outLine =
286
+ if replace_pattern:
287
+ _s = line.replace('\n','').split(search_pattern)
288
+ _j = (
289
+ ttk.TTkString(search_pattern,ttk.TTkColor.RED + ttk.TTkColor.STRIKETROUGH) +
290
+ ttk.TTkString(replace_pattern,ttk.TTkColor.GREEN) + ttk.TTkColor.RST)
291
+ ttkLine = _j.join(_s)
292
+ else:
293
+ ttkLine = ttk.TTkString(line.replace('\n','')).completeColor(
294
+ match=search_pattern,
295
+ color=ttk.TTkColor.GREEN)
296
+ children.append(_MatchTreeWidgetItem([ttk.TTkString(str(num)+" ",ttk.TTkColor.CYAN) + ttkLine] ,
297
+ match=line,
298
+ lineNumber=num,
299
+ path=os.path.join(root,file)))
300
+ item.addChildren(children)
301
+ group.append(item)
302
+ if len(group) > groupSize:
240
303
  self._results_tree.addTopLevelItems(group)
241
- Thread(target=_search_threading).start()
304
+ group = []
305
+ if groupSize < 0x400:
306
+ groupSize <<= 1
307
+ # self._results_tree.addTopLevelItem(item)
308
+ if group:
309
+ self._results_tree.addTopLevelItems(group)
242
310
 
243
- def _search_files(self, root_folder, match, runId):
244
- matches = []
245
- for root, file in _custom_walk(root_folder):
311
+ @ttk.pyTTkSlot()
312
+ def _search(self) -> None:
313
+ with self._search_lock:
314
+ if self._search_thread:
315
+ self._search_stop_event.set()
316
+ self._search_thread.join()
317
+ self._search_stop_event.clear()
318
+ self._search_thread = None
319
+ self._runId += 1
320
+ search_pattern = str(self._search_le.text())
321
+ replace_pattern = str(self._replace_le.text())
322
+ include_patterns = _s.split(',') if (_s:=str(self._files_inc_le.text())) else []
323
+ exclude_patterns = _s.split(',') if (_s:=str(self._files_exc_le.text())) else []
324
+ if not search_pattern:
325
+ self._results_tree.clear()
326
+ return
327
+ if self._search_thread:
328
+ self._search_thread.join()
329
+ self._search_thread = Thread(
330
+ target=self._search_threading,
331
+ args=(search_pattern, include_patterns, exclude_patterns, replace_pattern))
332
+ self._search_thread.start()
333
+
334
+ def _search_files(self, root_folder, match, runId, include_patterns, exclude_patterns):
335
+ for root, file in _custom_walk(root_folder,include_patterns,exclude_patterns):
246
336
  if runId != self._runId:
247
337
  return
248
338
  if True: # file.endswith('.py'): # file.endswith(file_extension):
@@ -26,7 +26,7 @@ from typing import Optional, Callable, Any
26
26
 
27
27
  import TermTk as ttk
28
28
 
29
- from ttkode.app.ttkode import TTKode
29
+ from ttkode.app.ttkode import TTKode, TTKodeWidget
30
30
 
31
31
  class TTKodeViewerProxy():
32
32
  __slots__ = ('_fileName')
@@ -41,26 +41,30 @@ class TTKodeProxy():
41
41
  '_ttkode',
42
42
  # Signals
43
43
  )
44
- _ttkode:Optional[TTKode]
45
- _openFileCb:Optional[Callable[[Any, int, int], Any]]
46
- def __init__(self) -> None:
47
- self._openFileCb = None
48
- self._ttkode = None
49
-
50
- def setTTKode(self, ttkode:TTKode) -> None:
44
+ _ttkode:TTKode
45
+ _openFileCb:Callable[[Any, int, int], Any]
46
+ def __init__(self, ttkode:TTKode) -> None:
51
47
  self._ttkode = ttkode
52
48
  self._openFileCb = ttkode._openFile
53
49
 
54
50
  def ttkode(self) -> TTKode:
55
- if not self._ttkode:
56
- raise Exception("TTkode uninitialized")
57
51
  return self._ttkode
58
52
 
53
+ def iterWidgets(self, widType=TTKodeWidget):
54
+ for kt, index in self._ttkode._kodeTab.iterItems():
55
+ if issubclass(type(wid:=kt.widget(index)), widType):
56
+ yield wid
57
+
58
+ @ttk.pyTTkSlot(TTKodeWidget)
59
+ def closeTab(self, widget:TTKodeWidget) -> None:
60
+ for kt, index in self._ttkode._kodeTab.iterItems():
61
+ if kt.widget(index)==widget:
62
+ kt.removeTab(index)
63
+
59
64
  def setOpenFile(self, cb):
60
65
  self._openFileCb = cb
61
66
 
62
67
  def openFile(self, fileName:str, line:int=0, pos:int=0):
63
- if self._ttkode and self._openFileCb:
64
- return self._openFileCb(fileName, line, pos)
68
+ return self._openFileCb(fileName, line, pos)
65
69
 
66
- ttkodeProxy:TTKodeProxy = TTKodeProxy()
70
+ ttkodeProxy:TTKodeProxy = TTKodeProxy(ttkode=TTKode())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ttkode
3
- Version: 0.3.2a2
3
+ Version: 0.5.0a2
4
4
  Summary: Terminal ToolKit Studio Code editor
5
5
  Author-email: Eugenio Parodi <ceccopierangiolieugenio@googlemail.com>
6
6
  License: MIT
@@ -19,7 +19,7 @@ Classifier: Topic :: Terminals
19
19
  Classifier: Topic :: Software Development :: User Interfaces
20
20
  Requires-Python: >=3.9
21
21
  Description-Content-Type: text/markdown
22
- Requires-Dist: pyTermTk>=0.42.1-a.0
22
+ Requires-Dist: pyTermTk>=0.46.0-a.0
23
23
  Requires-Dist: appdirs
24
24
  Requires-Dist: copykitten
25
25
  Requires-Dist: pygments
@@ -16,6 +16,7 @@ ttkode/app/about.py
16
16
  ttkode/app/activitybar.py
17
17
  ttkode/app/cfg.py
18
18
  ttkode/app/options.py
19
+ ttkode/app/state.py
19
20
  ttkode/app/ttkode.py
20
21
  ttkode/plugins/_010_findplugin.py
21
22
  ttkode/plugins/_020_debugplugin.py
@@ -1,4 +1,4 @@
1
- pyTermTk>=0.42.1-a.0
1
+ pyTermTk>=0.46.0-a.0
2
2
  appdirs
3
3
  copykitten
4
4
  pygments
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes