liveConsole 1.4.0__py3-none-any.whl → 1.4.1__py3-none-any.whl
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.
- commandHistory.py +34 -0
- helpTab.py +64 -0
- liveConsole.py +16 -642
- {liveconsole-1.4.0.dist-info → liveconsole-1.4.1.dist-info}/METADATA +15 -1
- liveconsole-1.4.1.dist-info/RECORD +12 -0
- liveconsole-1.4.1.dist-info/top_level.txt +7 -0
- mainConsole.py +354 -0
- styledTextbox.py +51 -0
- suggestionManager.py +165 -0
- liveconsole-1.4.0.dist-info/RECORD +0 -7
- liveconsole-1.4.0.dist-info/top_level.txt +0 -2
- {liveconsole-1.4.0.dist-info → liveconsole-1.4.1.dist-info}/WHEEL +0 -0
- {liveconsole-1.4.0.dist-info → liveconsole-1.4.1.dist-info}/licenses/LICENSE +0 -0
liveConsole.py
CHANGED
@@ -1,15 +1,10 @@
|
|
1
1
|
import customtkinter as ctk
|
2
|
-
import tkinter as tk
|
3
|
-
import traceback
|
4
2
|
import inspect
|
5
|
-
import threading
|
6
3
|
import sys
|
7
4
|
import io
|
8
|
-
|
9
|
-
from
|
10
|
-
from
|
11
|
-
import keyword
|
12
|
-
import builtins
|
5
|
+
|
6
|
+
from helpTab import HelpTab
|
7
|
+
from mainConsole import InteractiveConsoleText
|
13
8
|
|
14
9
|
|
15
10
|
class StdoutRedirect(io.StringIO):
|
@@ -26,641 +21,15 @@ class StdoutRedirect(io.StringIO):
|
|
26
21
|
def flush(self):
|
27
22
|
pass
|
28
23
|
|
24
|
+
class StdinRedirect(io.StringIO):
|
25
|
+
"""Redirects stdin to capture input() from the console."""
|
26
|
+
def __init__(self, readCallback):
|
27
|
+
super().__init__()
|
28
|
+
self.readCallback = readCallback
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
def __init__(self, textWidget, userLocals, userGlobals):
|
34
|
-
self.userLocals = userLocals
|
35
|
-
self.userGlobals = userGlobals
|
36
|
-
self.textWidget = textWidget
|
37
|
-
self.suggestionWindow = None
|
38
|
-
self.suggestionListbox = None
|
39
|
-
self.suggestions = []
|
40
|
-
self.selectedSuggestion = 0
|
41
|
-
|
42
|
-
# Build suggestion sources
|
43
|
-
self.keywords = keyword.kwlist
|
44
|
-
self.builtins = [name for name in dir(builtins) if not name.startswith('_')]
|
45
|
-
|
46
|
-
def getCurrentWord(self):
|
47
|
-
"""Extract the word being typed at cursor position and suggest dir() if applicable."""
|
48
|
-
suggestions = []
|
49
|
-
cursorPos = self.textWidget.index(tk.INSERT)
|
50
|
-
lineStart = self.textWidget.index(f"{cursorPos} linestart")
|
51
|
-
currentLine = self.textWidget.get(lineStart, cursorPos)
|
52
|
-
|
53
|
-
# Find the current word
|
54
|
-
words = currentLine.split()
|
55
|
-
if not words:
|
56
|
-
return "", suggestions
|
57
|
-
|
58
|
-
currentWord = words[-1]
|
59
|
-
|
60
|
-
|
61
|
-
# If the word contains a dot, try to evaluate the base and get its dir()
|
62
|
-
if '.' in currentWord:
|
63
|
-
try:
|
64
|
-
base_expr = '.'.join(currentWord.split('.')[:-1])
|
65
|
-
obj = eval(base_expr, self.userLocals, self.userGlobals)
|
66
|
-
suggestions = dir(obj)
|
67
|
-
except:
|
68
|
-
pass
|
69
|
-
for char in "([{,.":
|
70
|
-
if char in currentWord:
|
71
|
-
currentWord = currentWord.split(char)[-1]
|
72
|
-
|
73
|
-
return currentWord, suggestions
|
74
|
-
|
75
|
-
def getSuggestions(self, partialWord, suggestions=[]):
|
76
|
-
"""Get code suggestions for partial word."""
|
77
|
-
if len(partialWord) < 2:
|
78
|
-
return suggestions
|
79
|
-
|
80
|
-
if suggestions != []:
|
81
|
-
suggestions = [suggestion for suggestion in suggestions if suggestion.lower().startswith(partialWord.lower())]
|
82
|
-
else:
|
83
|
-
# Add matching keywords
|
84
|
-
for kw in self.keywords:
|
85
|
-
if kw.startswith(partialWord.lower()):
|
86
|
-
suggestions.append(kw)
|
87
|
-
|
88
|
-
# Add matching builtins
|
89
|
-
for builtin in self.builtins:
|
90
|
-
if builtin.startswith(partialWord):
|
91
|
-
suggestions.append(builtin)
|
92
|
-
|
93
|
-
# Add matching variables from namespace
|
94
|
-
master = self.textWidget.master
|
95
|
-
if hasattr(master, 'userLocals'):
|
96
|
-
for var in master.userLocals:
|
97
|
-
if var.startswith(partialWord) and not var.startswith('_'):
|
98
|
-
suggestions.append(var)
|
99
|
-
|
100
|
-
if hasattr(master, 'userGlobals'):
|
101
|
-
for var in master.userGlobals:
|
102
|
-
if var.startswith(partialWord) and not var.startswith('_'):
|
103
|
-
suggestions.append(var)
|
104
|
-
|
105
|
-
# Remove duplicates and sort
|
106
|
-
return sorted(list(set(suggestions)))
|
107
|
-
|
108
|
-
def showSuggestions(self):
|
109
|
-
"""Display the suggestions popup."""
|
110
|
-
currentWord, extraSuggestions = self.getCurrentWord()
|
111
|
-
suggestions = self.getSuggestions(currentWord, extraSuggestions)
|
112
|
-
|
113
|
-
if not suggestions:
|
114
|
-
self.hideSuggestions()
|
115
|
-
return
|
116
|
-
|
117
|
-
self.suggestions = suggestions
|
118
|
-
self.selectedSuggestion = 0
|
119
|
-
|
120
|
-
# Create suggestion window if needed
|
121
|
-
if not self.suggestionWindow:
|
122
|
-
self._createSuggestionWindow()
|
123
|
-
|
124
|
-
# Update listbox content
|
125
|
-
self.suggestionListbox.delete(0, tk.END)
|
126
|
-
for suggestion in suggestions:
|
127
|
-
self.suggestionListbox.insert(tk.END, suggestion)
|
128
|
-
|
129
|
-
self.suggestionListbox.selection_set(0)
|
130
|
-
|
131
|
-
# Position window near cursor
|
132
|
-
self._positionSuggestionWindow()
|
133
|
-
self.suggestionWindow.deiconify()
|
134
|
-
|
135
|
-
def _createSuggestionWindow(self):
|
136
|
-
"""Create the suggestion popup window."""
|
137
|
-
self.suggestionWindow = tk.Toplevel(self.textWidget)
|
138
|
-
self.suggestionWindow.wm_overrideredirect(True)
|
139
|
-
self.suggestionWindow.configure(bg="#2d2d2d")
|
140
|
-
|
141
|
-
self.suggestionListbox = tk.Listbox(
|
142
|
-
self.suggestionWindow,
|
143
|
-
bg="#2d2d2d",
|
144
|
-
fg="white",
|
145
|
-
selectbackground="#0066cc",
|
146
|
-
font=("Consolas", 10),
|
147
|
-
height=8
|
148
|
-
)
|
149
|
-
self.suggestionListbox.pack()
|
150
|
-
|
151
|
-
def _positionSuggestionWindow(self):
|
152
|
-
"""Position the suggestion window near the cursor."""
|
153
|
-
cursorPos = self.textWidget.index(tk.INSERT)
|
154
|
-
x, y, _, _ = self.textWidget.bbox(cursorPos)
|
155
|
-
x += self.textWidget.winfo_rootx()
|
156
|
-
y += self.textWidget.winfo_rooty() + 20
|
157
|
-
self.suggestionWindow.geometry(f"+{x}+{y}")
|
158
|
-
|
159
|
-
def hideSuggestions(self):
|
160
|
-
"""Hide the suggestions popup."""
|
161
|
-
if self.suggestionWindow:
|
162
|
-
self.suggestionWindow.withdraw()
|
163
|
-
|
164
|
-
def applySuggestion(self, suggestion=None):
|
165
|
-
"""Apply the selected suggestion at cursor position."""
|
166
|
-
if not suggestion and self.suggestions:
|
167
|
-
suggestion = self.suggestions[self.selectedSuggestion]
|
168
|
-
if not suggestion:
|
169
|
-
return
|
170
|
-
|
171
|
-
currentWord, _ = self.getCurrentWord()
|
172
|
-
# Only insert the missing part
|
173
|
-
missingPart = suggestion[len(currentWord):]
|
174
|
-
cursorPos = self.textWidget.index(tk.INSERT)
|
175
|
-
self.textWidget.insert(cursorPos, missingPart)
|
176
|
-
|
177
|
-
self.hideSuggestions()
|
178
|
-
|
179
|
-
def handleNavigation(self, direction):
|
180
|
-
"""Handle up/down navigation in suggestions."""
|
181
|
-
if not self.suggestions:
|
182
|
-
return
|
183
|
-
|
184
|
-
if direction == "down":
|
185
|
-
self.selectedSuggestion = min(self.selectedSuggestion + 1, len(self.suggestions) - 1)
|
186
|
-
else: # up
|
187
|
-
self.selectedSuggestion = max(self.selectedSuggestion - 1, 0)
|
188
|
-
|
189
|
-
self.suggestionListbox.selection_clear(0, tk.END)
|
190
|
-
self.suggestionListbox.selection_set(self.selectedSuggestion)
|
191
|
-
|
192
|
-
|
193
|
-
class CommandHistory:
|
194
|
-
"""Manages command history and navigation."""
|
195
|
-
|
196
|
-
def __init__(self):
|
197
|
-
self.history = []
|
198
|
-
self.index = -1
|
199
|
-
self.tempCommand = ""
|
200
|
-
|
201
|
-
def add(self, command):
|
202
|
-
"""Add a command to history."""
|
203
|
-
if command.strip():
|
204
|
-
self.history.append(command)
|
205
|
-
self.index = len(self.history)
|
206
|
-
|
207
|
-
def navigateUp(self):
|
208
|
-
"""Get previous command from history."""
|
209
|
-
if self.index > 0:
|
210
|
-
self.index -= 1
|
211
|
-
return self.history[self.index]
|
212
|
-
return None
|
213
|
-
|
214
|
-
def navigateDown(self):
|
215
|
-
"""Get next command from history."""
|
216
|
-
if self.index < len(self.history) - 1:
|
217
|
-
self.index += 1
|
218
|
-
return self.history[self.index]
|
219
|
-
elif self.index == len(self.history) - 1:
|
220
|
-
self.index = len(self.history)
|
221
|
-
return self.tempCommand
|
222
|
-
return None
|
223
|
-
|
224
|
-
def setTemp(self, command):
|
225
|
-
"""Store temporary command while navigating history."""
|
226
|
-
self.tempCommand = command
|
227
|
-
|
228
|
-
|
229
|
-
class StyledTextWindow(tk.Text):
|
230
|
-
def __init__(self, master, **kwargs):
|
231
|
-
super().__init__(master, **kwargs)
|
232
|
-
|
233
|
-
# Syntax highlighting setup
|
234
|
-
self.lexer = PythonLexer()
|
235
|
-
self.style = get_style_by_name("monokai")
|
236
|
-
|
237
|
-
# Setup tags
|
238
|
-
self._setupTags()
|
239
|
-
|
240
|
-
def _setupTags(self):
|
241
|
-
"""Configure text tags for different output types."""
|
242
|
-
self.tag_configure("prompt", foreground="#00ff00", font=("Consolas", 12, "bold"))
|
243
|
-
self.tag_configure("output", foreground="#ffffff", font=("Consolas", 12))
|
244
|
-
self.tag_configure("error", foreground="#ff6666", font=("Consolas", 12))
|
245
|
-
self.tag_configure("result", foreground="#66ccff", font=("Consolas", 12))
|
246
|
-
|
247
|
-
# Configure syntax highlighting tags
|
248
|
-
for token, style in self.style:
|
249
|
-
if style["color"]:
|
250
|
-
fg = f"#{style['color']}"
|
251
|
-
font = ("Consolas", 12, "bold" if style["bold"] else "normal")
|
252
|
-
self.tag_configure(str(token), foreground=fg, font=font)
|
253
|
-
|
254
|
-
|
255
|
-
def updateStyling(self, start="1.0"):
|
256
|
-
"""Apply syntax highlighting to the current command."""
|
257
|
-
end = "end-1c"
|
258
|
-
|
259
|
-
for token, _ in self.style:
|
260
|
-
self.tag_remove(str(token), start, end)
|
261
|
-
|
262
|
-
# Get and highlight the command
|
263
|
-
command = self.get(start, "end-1c")
|
264
|
-
if not command:
|
265
|
-
return(-1)
|
266
|
-
|
267
|
-
self.mark_set("highlight_pos", start)
|
268
|
-
|
269
|
-
for token, content in pygments.lex(command, self.lexer):
|
270
|
-
if content:
|
271
|
-
endPos = f"highlight_pos + {len(content)}c"
|
272
|
-
if content.strip(): # Only highlight non-whitespace
|
273
|
-
self.tag_add(str(token), "highlight_pos", endPos)
|
274
|
-
self.mark_set("highlight_pos", endPos)
|
275
|
-
|
276
|
-
class InteractiveConsoleText(StyledTextWindow):
|
277
|
-
"""TBD"""
|
278
|
-
|
279
|
-
PROMPT = ">>> "
|
280
|
-
PROMPT_LENGTH = 4
|
281
|
-
|
282
|
-
def __init__(self, master, helpTab, userLocals=None, userGlobals=None, **kwargs):
|
283
|
-
super().__init__(master, **kwargs)
|
284
|
-
|
285
|
-
# Initialize components
|
286
|
-
self.suggestionManager = CodeSuggestionManager(self, userLocals=userLocals, userGlobals=userGlobals)
|
287
|
-
self.helpTab = helpTab
|
288
|
-
|
289
|
-
self.navigatingHistory = False
|
290
|
-
self.history = CommandHistory()
|
291
|
-
|
292
|
-
# Track current command
|
293
|
-
self.currentCommandLine = 1
|
294
|
-
self.isExecuting = False
|
295
|
-
|
296
|
-
# Setup bindings
|
297
|
-
self._setupBindings()
|
298
|
-
|
299
|
-
# Initialize with first prompt
|
300
|
-
self.addPrompt()
|
301
|
-
|
302
|
-
def _setupBindings(self):
|
303
|
-
"""Setup all key and mouse bindings."""
|
304
|
-
self.bind("<Return>", self.onEnter)
|
305
|
-
self.bind("<Shift-Return>", self.onShiftEnter)
|
306
|
-
self.bind("<Control-c>", self.cancel)
|
307
|
-
self.bind("<Tab>", self.onTab)
|
308
|
-
self.bind("<BackSpace>", self.onBackspace)
|
309
|
-
self.bind("<KeyRelease>", self.onKeyRelease)
|
310
|
-
self.bind("<KeyPress>", self.onKeyPress)
|
311
|
-
self.bind("<Button-1>", self.onClick)
|
312
|
-
self.bind("<Up>", self.onUp)
|
313
|
-
self.bind("<Down>", self.onDown)
|
314
|
-
|
315
|
-
def getCurrentLineNumber(self):
|
316
|
-
"""Get the line number where current command starts."""
|
317
|
-
return int(self.index("end-1c").split(".")[0])
|
318
|
-
|
319
|
-
def getCommandStartPosition(self):
|
320
|
-
"""Get the starting position of the current command."""
|
321
|
-
return f"{self.currentCommandLine}.0"
|
322
|
-
|
323
|
-
def replaceCurrentCommand(self, newCommand):
|
324
|
-
"""Replace the current command with new text."""
|
325
|
-
if self.isExecuting:
|
326
|
-
return
|
327
|
-
|
328
|
-
start = self.getPromptPosition()
|
329
|
-
end = "end-1c"
|
330
|
-
|
331
|
-
self.delete(start, end)
|
332
|
-
self.insert(start, newCommand)
|
333
|
-
self.see("end")
|
334
|
-
|
335
|
-
def isCursorInEditableArea(self):
|
336
|
-
"""Check if cursor is in the editable command area."""
|
337
|
-
if self.isExecuting:
|
338
|
-
return False
|
339
|
-
|
340
|
-
cursorLine = int(self.index("insert").split(".")[0])
|
341
|
-
cursorCol = int(self.index("insert").split(".")[1])
|
342
|
-
|
343
|
-
return (cursorLine >= self.currentCommandLine and
|
344
|
-
(cursorLine > self.currentCommandLine or cursorCol >= self.PROMPT_LENGTH))
|
345
|
-
|
346
|
-
def onEnter(self, event):
|
347
|
-
"""Handle Enter key - execute command."""
|
348
|
-
self.suggestionManager.hideSuggestions()
|
349
|
-
|
350
|
-
if self.isExecuting:
|
351
|
-
return "break"
|
352
|
-
|
353
|
-
command = self.getCurrentCommand()
|
354
|
-
|
355
|
-
if not command.strip():
|
356
|
-
return "break"
|
357
|
-
|
358
|
-
# Check if statement is incomplete
|
359
|
-
if self.isIncompleteStatement(command):
|
360
|
-
return self.onShiftEnter(event)
|
361
|
-
|
362
|
-
# Execute the command
|
363
|
-
self.history.add(command)
|
364
|
-
self.mark_set("insert", "end")
|
365
|
-
self.insert("end", "\n")
|
366
|
-
self.see("end")
|
367
|
-
|
368
|
-
# Execute in thread
|
369
|
-
self.isExecuting = True
|
370
|
-
threading.Thread(
|
371
|
-
target=self.executeCommandThreaded,
|
372
|
-
args=(command,),
|
373
|
-
daemon=True
|
374
|
-
).start()
|
375
|
-
|
376
|
-
return "break"
|
377
|
-
|
378
|
-
def onShiftEnter(self, event):
|
379
|
-
"""Handle Shift+Enter - new line with auto-indent."""
|
380
|
-
self.suggestionManager.hideSuggestions()
|
381
|
-
|
382
|
-
if self.isExecuting:
|
383
|
-
return "break"
|
384
|
-
|
385
|
-
# Get current line for indent calculation
|
386
|
-
cursorPos = self.index("insert")
|
387
|
-
lineStart = self.index(f"{cursorPos} linestart")
|
388
|
-
lineEnd = self.index(f"{cursorPos} lineend")
|
389
|
-
currentLine = self.get(lineStart, lineEnd)
|
390
|
-
|
391
|
-
# Calculate indentation
|
392
|
-
indent = self.calculateIndent(currentLine)
|
393
|
-
|
394
|
-
# Insert newline with indent
|
395
|
-
self.insert("insert", "\n" + " " * indent)
|
396
|
-
self.see("end")
|
397
|
-
|
398
|
-
return "break"
|
399
|
-
|
400
|
-
def onTab(self, event):
|
401
|
-
"""Handle Tab key for autocompletion."""
|
402
|
-
if self.isExecuting:
|
403
|
-
return "break"
|
404
|
-
|
405
|
-
if self.suggestionManager.suggestionWindow and \
|
406
|
-
self.suggestionManager.suggestionWindow.winfo_viewable():
|
407
|
-
self.suggestionManager.applySuggestion()
|
408
|
-
else:
|
409
|
-
self.suggestionManager.showSuggestions()
|
410
|
-
|
411
|
-
return "break"
|
412
|
-
|
413
|
-
def onBackspace(self, event):
|
414
|
-
"""Prevent backspace from deleting the prompt."""
|
415
|
-
if not self.isCursorInEditableArea():
|
416
|
-
return "break"
|
417
|
-
|
418
|
-
# Check if we're at the prompt boundary
|
419
|
-
cursorPos = self.index("insert")
|
420
|
-
promptPos = self.getPromptPosition()
|
421
|
-
|
422
|
-
if self.compare(cursorPos, "<=", promptPos):
|
423
|
-
return "break"
|
424
|
-
|
425
|
-
def onClick(self, event):
|
426
|
-
"""Handle mouse clicks - Ctrl+Click opens help for the clicked word."""
|
427
|
-
self.suggestionManager.hideSuggestions()
|
428
|
-
|
429
|
-
if event.state & 0x4: #< Ctrl pressed
|
430
|
-
clickIndex = self.index(f"@{event.x},{event.y}") # mouse index
|
431
|
-
|
432
|
-
# Get the full line
|
433
|
-
i = int(clickIndex.split('.')[1])
|
434
|
-
lineNum = clickIndex.split('.')[0]
|
435
|
-
lineStart = f"{lineNum}.0"
|
436
|
-
lineEnd = f"{lineNum}.end"
|
437
|
-
lineText = self.get(lineStart, lineEnd)
|
438
|
-
|
439
|
-
wordEndIndex = self.index(f"{clickIndex} wordend")
|
440
|
-
# obj = self.get(f"{clickIndex} wordstart", f"{clickIndex} wordend").strip() #< Get the word at that index
|
441
|
-
obj = ""
|
442
|
-
for i in range (i-1,2, -1):
|
443
|
-
letter = lineText[i]
|
444
|
-
if (not (letter.isalnum() or letter == "_" or letter == ".")): #< or (letter in " \n\t\r")
|
445
|
-
obj = lineText[i+1: int(wordEndIndex.split('.')[1])]
|
446
|
-
break
|
447
|
-
|
448
|
-
|
449
|
-
if obj:
|
450
|
-
self.helpTab.updateHelp(obj)
|
451
|
-
self.helpTab.open()
|
452
|
-
|
453
|
-
return "break" #< Prevent default cursor behavior
|
454
|
-
|
455
|
-
return None #< Normal click behavior
|
456
|
-
|
457
|
-
def onKeyPress(self, event):
|
458
|
-
"""Handle key press events."""
|
459
|
-
# print(event.keysym)
|
460
|
-
if self.suggestionManager.suggestionWindow and \
|
461
|
-
self.suggestionManager.suggestionWindow.winfo_viewable():
|
462
|
-
if event.keysym == "Escape":
|
463
|
-
self.suggestionManager.hideSuggestions()
|
464
|
-
return "break"
|
465
|
-
|
466
|
-
# Prevent editing outside command area
|
467
|
-
if not event.keysym in ["Shift_L", "Shift_R", "Control_L", "Control_R"]:
|
468
|
-
self.navigatingHistory = False
|
469
|
-
if not self.isCursorInEditableArea():
|
470
|
-
self.mark_set("insert", "end")
|
471
|
-
|
472
|
-
if event.keysym in ["Left", "Right"]:
|
473
|
-
if self.index("insert") == self.getPromptPosition():
|
474
|
-
self.mark_set("insert", "1.4")
|
475
|
-
return "break"
|
476
|
-
|
477
|
-
def onKeyRelease(self, event):
|
478
|
-
"""Handle key release events."""
|
479
|
-
if event.keysym in ["Return", "Escape", "Left", "Right", "Home", "End"]:
|
480
|
-
self.suggestionManager.hideSuggestions()
|
481
|
-
elif event.keysym not in ["Up", "Down", "Shift_L", "Shift_R", "Control_L", "Control_R"]:
|
482
|
-
if not self.isExecuting:
|
483
|
-
self.after_idle(self.suggestionManager.showSuggestions)
|
484
|
-
if not self.isExecuting:
|
485
|
-
self.after_idle(lambda: self.updateStyling(start=self.getPromptPosition()))
|
486
|
-
|
487
|
-
def cancel(self, event):
|
488
|
-
self.history.add(self.getCurrentCommand())
|
489
|
-
self.replaceCurrentCommand("")
|
490
|
-
|
491
|
-
def historyReplace(self, command):
|
492
|
-
if self.getCurrentCommand() == "" or self.navigatingHistory:
|
493
|
-
if self.isExecuting:
|
494
|
-
return "break"
|
495
|
-
|
496
|
-
if self.history.index == len(self.history.history):
|
497
|
-
self.history.setTemp(self.getCurrentCommand())
|
498
|
-
|
499
|
-
if command is not None:
|
500
|
-
self.replaceCurrentCommand(command)
|
501
|
-
self.navigatingHistory = True
|
502
|
-
return("break")
|
503
|
-
|
504
|
-
def onUp(self, event):
|
505
|
-
if self.suggestionManager.suggestionWindow and \
|
506
|
-
self.suggestionManager.suggestionWindow.winfo_viewable():
|
507
|
-
if event.keysym == "Up":
|
508
|
-
self.suggestionManager.handleNavigation("up")
|
509
|
-
return "break"
|
510
|
-
command = self.history.navigateUp()
|
511
|
-
return(self.historyReplace(command))
|
512
|
-
# self.mark_set("insert", "insert -1 line")
|
513
|
-
|
514
|
-
def onDown(self, event):
|
515
|
-
if self.suggestionManager.suggestionWindow and \
|
516
|
-
self.suggestionManager.suggestionWindow.winfo_viewable():
|
517
|
-
if event.keysym == "Down":
|
518
|
-
self.suggestionManager.handleNavigation("down")
|
519
|
-
return "break"
|
520
|
-
command = self.history.navigateDown()
|
521
|
-
return(self.historyReplace(command))
|
522
|
-
|
523
|
-
def isIncompleteStatement(self, code):
|
524
|
-
"""Check if the code is an incomplete statement."""
|
525
|
-
lines = code.split("\n")
|
526
|
-
if not lines[-1].strip():
|
527
|
-
return False
|
528
|
-
|
529
|
-
# Check for line ending with colon
|
530
|
-
for line in lines:
|
531
|
-
if line.strip().endswith(":"):
|
532
|
-
return True
|
533
|
-
|
534
|
-
return False
|
535
|
-
|
536
|
-
def calculateIndent(self, line):
|
537
|
-
"""Calculate the indentation level for the next line."""
|
538
|
-
currentIndent = len(line) - len(line.lstrip())
|
539
|
-
|
540
|
-
# If line ends with colon, increase indent
|
541
|
-
if line.strip().endswith(":"):
|
542
|
-
return currentIndent + 4
|
543
|
-
|
544
|
-
return currentIndent
|
545
|
-
|
546
|
-
def writeOutput(self, text, tag="output"):
|
547
|
-
"""Write output to the console (thread-safe)."""
|
548
|
-
def _write():
|
549
|
-
self.insert("end", text + "\n", tag)
|
550
|
-
self.see("end")
|
551
|
-
|
552
|
-
self.after(0, _write)
|
553
|
-
|
554
|
-
def getPromptPosition(self):
|
555
|
-
"""Get the position right after the prompt on current command line."""
|
556
|
-
return f"{self.currentCommandLine}.{self.PROMPT_LENGTH}"
|
557
|
-
|
558
|
-
def getCurrentCommand(self):
|
559
|
-
"""Extract the current command text (without prompt)."""
|
560
|
-
start = self.getPromptPosition()
|
561
|
-
end = "end-1c"
|
562
|
-
return self.get(start, end)
|
563
|
-
|
564
|
-
def addPrompt(self):
|
565
|
-
"""Add a new command prompt."""
|
566
|
-
def _add():
|
567
|
-
# Store the line number for the new command
|
568
|
-
self.currentCommandLine = self.getCurrentLineNumber()
|
569
|
-
|
570
|
-
# Insert prompt
|
571
|
-
self.insert("end", self.PROMPT)
|
572
|
-
promptStart = f"{self.currentCommandLine}.0"
|
573
|
-
promptEnd = f"{self.currentCommandLine}.{self.PROMPT_LENGTH}"
|
574
|
-
self.tag_add("prompt", promptStart, promptEnd)
|
575
|
-
|
576
|
-
self.mark_set("insert", "end")
|
577
|
-
self.see("end")
|
578
|
-
self.isExecuting = False
|
579
|
-
|
580
|
-
if self.isExecuting:
|
581
|
-
self.after(0, _add)
|
582
|
-
else:
|
583
|
-
_add()
|
584
|
-
|
585
|
-
def executeCommandThreaded(self, command):
|
586
|
-
"""Execute a command in a separate thread."""
|
587
|
-
try:
|
588
|
-
# Try eval first for expressions
|
589
|
-
result = eval(command, self.master.userGlobals, self.master.userLocals)
|
590
|
-
if result is not None:
|
591
|
-
self.writeOutput(str(result), "result")
|
592
|
-
self.master.userLocals["_"] = result
|
593
|
-
except SyntaxError:
|
594
|
-
try:
|
595
|
-
# Try exec for statements
|
596
|
-
exec(command, self.master.userGlobals, self.master.userLocals)
|
597
|
-
except Exception:
|
598
|
-
self.writeOutput(traceback.format_exc(), "error")
|
599
|
-
except Exception:
|
600
|
-
self.writeOutput(traceback.format_exc(), "error")
|
601
|
-
|
602
|
-
# Add new prompt after execution
|
603
|
-
self.addPrompt()
|
604
|
-
|
605
|
-
|
606
|
-
class HelpTab(ctk.CTkFrame):
|
607
|
-
"""A right-hand help tab with closable and updateable text content."""
|
608
|
-
|
609
|
-
def __init__(self, parent, width=500, title="Help", **kwargs):
|
610
|
-
super().__init__(parent, width=width, **kwargs)
|
611
|
-
self.parent = parent
|
612
|
-
self.visible = False
|
613
|
-
|
614
|
-
# Ensure initial width is respected
|
615
|
-
self.pack_propagate(False)
|
616
|
-
|
617
|
-
# Header frame with title and close button
|
618
|
-
headerFrame = ctk.CTkFrame(self, height=30)
|
619
|
-
headerFrame.pack(fill="x")
|
620
|
-
self.style = get_style_by_name("monokai")
|
621
|
-
|
622
|
-
self.titleLabel = ctk.CTkLabel(headerFrame, text=title, font=("Consolas", 12, "bold"))
|
623
|
-
self.titleLabel.pack(side="left", padx=5)
|
624
|
-
|
625
|
-
self.closeButton = ctk.CTkButton(headerFrame, text="X", height=20, command=self.close)
|
626
|
-
self.closeButton.pack(side="right", padx=5)
|
627
|
-
|
628
|
-
# Scrollable text area
|
629
|
-
self.textBox = StyledTextWindow(self, wrap="word", font=("Consolas", 11), bg="#2e2e2e")
|
630
|
-
self.textBox.pack(fill="both", expand=True, padx=5, pady=5)
|
631
|
-
self.textBox.configure(state="disabled") # read-only
|
632
|
-
|
633
|
-
def close(self):
|
634
|
-
"""Hide the help tab."""
|
635
|
-
if self.visible:
|
636
|
-
self.pack_forget()
|
637
|
-
self.visible = False
|
638
|
-
|
639
|
-
def open(self):
|
640
|
-
"""Show the help tab."""
|
641
|
-
if not self.visible:
|
642
|
-
self.pack(side="left", fill="y")
|
643
|
-
# self.configure(width=self.minWidth)
|
644
|
-
self.visible = True
|
645
|
-
|
646
|
-
def _getHelp(self, obj):
|
647
|
-
"""Return the output of help(obj) as a string."""
|
648
|
-
old_stdout = sys.stdout # save current stdout
|
649
|
-
sys.stdout = buffer = io.StringIO() # redirect stdout to a string buffer
|
650
|
-
try:
|
651
|
-
help(obj)
|
652
|
-
return buffer.getvalue()
|
653
|
-
finally:
|
654
|
-
sys.stdout = old_stdout # restore original stdout
|
655
|
-
|
656
|
-
def updateHelp(self, obj):
|
657
|
-
"""Update the help tab content."""
|
30
|
+
def readline(self, *args, **kwargs):
|
31
|
+
return(self.readCallback())
|
658
32
|
|
659
|
-
self.textBox.configure(state="normal")
|
660
|
-
self.textBox.delete("1.0", "end")
|
661
|
-
self.textBox.insert("1.0", self._getHelp(obj))
|
662
|
-
self.textBox.updateStyling()
|
663
|
-
self.textBox.configure(state="disabled")
|
664
33
|
|
665
34
|
class InteractiveConsole(ctk.CTk):
|
666
35
|
"""Main console window application."""
|
@@ -691,6 +60,7 @@ class InteractiveConsole(ctk.CTk):
|
|
691
60
|
|
692
61
|
# Redirect stdout/stderr
|
693
62
|
self._setupOutputRedirect()
|
63
|
+
self._setupInputRedirect()
|
694
64
|
|
695
65
|
def _createUi(self):
|
696
66
|
"""Create UI with console and help tab."""
|
@@ -730,6 +100,10 @@ class InteractiveConsole(ctk.CTk):
|
|
730
100
|
sys.stderr = StdoutRedirect(
|
731
101
|
lambda text, tag: self.console.writeOutput(text, "error")
|
732
102
|
)
|
103
|
+
|
104
|
+
def _setupInputRedirect(self):
|
105
|
+
"""Setup stdin redirection to console."""
|
106
|
+
sys.stdin = StdinRedirect(self.console.readInput)
|
733
107
|
|
734
108
|
def probe(self, *args, **kwargs):
|
735
109
|
"""Start the console main loop."""
|
@@ -743,7 +117,7 @@ if __name__ == "__main__":
|
|
743
117
|
|
744
118
|
def greet(name):
|
745
119
|
print(f"Hello {name}!")
|
746
|
-
return
|
120
|
+
return(f"Greeted {name}")
|
747
121
|
|
748
122
|
# Create the list for testing autocomplete
|
749
123
|
exampleList = [1, 2, 3, 4, 5]
|