liveConsole 1.3.2__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 +33 -563
- {liveconsole-1.3.2.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.3.2.dist-info/RECORD +0 -7
- liveconsole-1.3.2.dist-info/top_level.txt +0 -2
- {liveconsole-1.3.2.dist-info → liveconsole-1.4.1.dist-info}/WHEEL +0 -0
- {liveconsole-1.3.2.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,554 +21,14 @@ 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
|
-
# print(partialWord)
|
81
|
-
if suggestions != []:
|
82
|
-
suggestions = [suggestion for suggestion in suggestions if suggestion.lower().startswith(partialWord.lower())]
|
83
|
-
else:
|
84
|
-
# Add matching keywords
|
85
|
-
for kw in self.keywords:
|
86
|
-
if kw.startswith(partialWord.lower()):
|
87
|
-
suggestions.append(kw)
|
88
|
-
|
89
|
-
# Add matching builtins
|
90
|
-
for builtin in self.builtins:
|
91
|
-
if builtin.startswith(partialWord):
|
92
|
-
suggestions.append(builtin)
|
93
|
-
|
94
|
-
# Add matching variables from namespace
|
95
|
-
master = self.textWidget.master
|
96
|
-
if hasattr(master, 'userLocals'):
|
97
|
-
for var in master.userLocals:
|
98
|
-
if var.startswith(partialWord) and not var.startswith('_'):
|
99
|
-
suggestions.append(var)
|
100
|
-
|
101
|
-
if hasattr(master, 'userGlobals'):
|
102
|
-
for var in master.userGlobals:
|
103
|
-
if var.startswith(partialWord) and not var.startswith('_'):
|
104
|
-
suggestions.append(var)
|
105
|
-
|
106
|
-
# Remove duplicates and sort
|
107
|
-
return sorted(list(set(suggestions)))
|
108
|
-
|
109
|
-
def showSuggestions(self):
|
110
|
-
"""Display the suggestions popup."""
|
111
|
-
currentWord, extraSuggestions = self.getCurrentWord()
|
112
|
-
suggestions = self.getSuggestions(currentWord, extraSuggestions)
|
113
|
-
|
114
|
-
if not suggestions:
|
115
|
-
self.hideSuggestions()
|
116
|
-
return
|
117
|
-
|
118
|
-
self.suggestions = suggestions
|
119
|
-
self.selectedSuggestion = 0
|
120
|
-
|
121
|
-
# Create suggestion window if needed
|
122
|
-
if not self.suggestionWindow:
|
123
|
-
self._createSuggestionWindow()
|
124
|
-
|
125
|
-
# Update listbox content
|
126
|
-
self.suggestionListbox.delete(0, tk.END)
|
127
|
-
for suggestion in suggestions:
|
128
|
-
self.suggestionListbox.insert(tk.END, suggestion)
|
129
|
-
|
130
|
-
self.suggestionListbox.selection_set(0)
|
131
|
-
|
132
|
-
# Position window near cursor
|
133
|
-
self._positionSuggestionWindow()
|
134
|
-
self.suggestionWindow.deiconify()
|
135
|
-
|
136
|
-
def _createSuggestionWindow(self):
|
137
|
-
"""Create the suggestion popup window."""
|
138
|
-
self.suggestionWindow = tk.Toplevel(self.textWidget)
|
139
|
-
self.suggestionWindow.wm_overrideredirect(True)
|
140
|
-
self.suggestionWindow.configure(bg="#2d2d2d")
|
141
|
-
|
142
|
-
self.suggestionListbox = tk.Listbox(
|
143
|
-
self.suggestionWindow,
|
144
|
-
bg="#2d2d2d",
|
145
|
-
fg="white",
|
146
|
-
selectbackground="#0066cc",
|
147
|
-
font=("Consolas", 10),
|
148
|
-
height=8
|
149
|
-
)
|
150
|
-
self.suggestionListbox.pack()
|
151
|
-
|
152
|
-
def _positionSuggestionWindow(self):
|
153
|
-
"""Position the suggestion window near the cursor."""
|
154
|
-
cursorPos = self.textWidget.index(tk.INSERT)
|
155
|
-
x, y, _, _ = self.textWidget.bbox(cursorPos)
|
156
|
-
x += self.textWidget.winfo_rootx()
|
157
|
-
y += self.textWidget.winfo_rooty() + 20
|
158
|
-
self.suggestionWindow.geometry(f"+{x}+{y}")
|
159
|
-
|
160
|
-
def hideSuggestions(self):
|
161
|
-
"""Hide the suggestions popup."""
|
162
|
-
if self.suggestionWindow:
|
163
|
-
self.suggestionWindow.withdraw()
|
164
|
-
|
165
|
-
def applySuggestion(self, suggestion=None):
|
166
|
-
"""Apply the selected suggestion at cursor position."""
|
167
|
-
if not suggestion and self.suggestions:
|
168
|
-
suggestion = self.suggestions[self.selectedSuggestion]
|
169
|
-
if not suggestion:
|
170
|
-
return
|
171
|
-
|
172
|
-
currentWord, _ = self.getCurrentWord()
|
173
|
-
# Only insert the missing part
|
174
|
-
missingPart = suggestion[len(currentWord):]
|
175
|
-
cursorPos = self.textWidget.index(tk.INSERT)
|
176
|
-
self.textWidget.insert(cursorPos, missingPart)
|
177
|
-
|
178
|
-
self.hideSuggestions()
|
179
|
-
|
180
|
-
def handleNavigation(self, direction):
|
181
|
-
"""Handle up/down navigation in suggestions."""
|
182
|
-
if not self.suggestions:
|
183
|
-
return
|
184
|
-
|
185
|
-
if direction == "down":
|
186
|
-
self.selectedSuggestion = min(self.selectedSuggestion + 1, len(self.suggestions) - 1)
|
187
|
-
else: # up
|
188
|
-
self.selectedSuggestion = max(self.selectedSuggestion - 1, 0)
|
189
|
-
|
190
|
-
self.suggestionListbox.selection_clear(0, tk.END)
|
191
|
-
self.suggestionListbox.selection_set(self.selectedSuggestion)
|
192
|
-
|
193
|
-
|
194
|
-
class CommandHistory:
|
195
|
-
"""Manages command history and navigation."""
|
196
|
-
|
197
|
-
def __init__(self):
|
198
|
-
self.history = []
|
199
|
-
self.index = -1
|
200
|
-
self.tempCommand = ""
|
201
|
-
|
202
|
-
def add(self, command):
|
203
|
-
"""Add a command to history."""
|
204
|
-
if command.strip():
|
205
|
-
self.history.append(command)
|
206
|
-
self.index = len(self.history)
|
207
|
-
|
208
|
-
def navigateUp(self):
|
209
|
-
"""Get previous command from history."""
|
210
|
-
if self.index > 0:
|
211
|
-
self.index -= 1
|
212
|
-
return self.history[self.index]
|
213
|
-
return None
|
214
|
-
|
215
|
-
def navigateDown(self):
|
216
|
-
"""Get next command from history."""
|
217
|
-
if self.index < len(self.history) - 1:
|
218
|
-
self.index += 1
|
219
|
-
return self.history[self.index]
|
220
|
-
elif self.index == len(self.history) - 1:
|
221
|
-
self.index = len(self.history)
|
222
|
-
return self.tempCommand
|
223
|
-
return None
|
224
|
-
|
225
|
-
def setTemp(self, command):
|
226
|
-
"""Store temporary command while navigating history."""
|
227
|
-
self.tempCommand = command
|
228
|
-
|
229
|
-
|
230
|
-
class InteractiveConsoleText(tk.Text):
|
231
|
-
"""A tk.Text widget with Python syntax highlighting for interactive console."""
|
232
|
-
|
233
|
-
PROMPT = ">>> "
|
234
|
-
PROMPT_LENGTH = 4
|
235
|
-
|
236
|
-
def __init__(self, master, userLocals=None, userGlobals=None, **kwargs):
|
237
|
-
super().__init__(master, **kwargs)
|
238
|
-
|
239
|
-
# Initialize components
|
240
|
-
self.suggestionManager = CodeSuggestionManager(self, userLocals=userLocals, userGlobals=userGlobals)
|
241
|
-
|
242
|
-
self.navigatingHistory = False
|
243
|
-
self.history = CommandHistory()
|
244
|
-
|
245
|
-
# Syntax highlighting setup
|
246
|
-
self.lexer = PythonLexer()
|
247
|
-
self.style = get_style_by_name("monokai")
|
248
|
-
|
249
|
-
# Track current command
|
250
|
-
self.currentCommandLine = 1
|
251
|
-
self.isExecuting = False
|
252
|
-
|
253
|
-
# Setup tags and bindings
|
254
|
-
self._setupTags()
|
255
|
-
self._setupBindings()
|
256
|
-
|
257
|
-
# Initialize with first prompt
|
258
|
-
self.addPrompt()
|
259
|
-
|
260
|
-
def _setupTags(self):
|
261
|
-
"""Configure text tags for different output types."""
|
262
|
-
self.tag_configure("prompt", foreground="#00ff00", font=("Consolas", 12, "bold"))
|
263
|
-
self.tag_configure("output", foreground="#ffffff", font=("Consolas", 12))
|
264
|
-
self.tag_configure("error", foreground="#ff6666", font=("Consolas", 12))
|
265
|
-
self.tag_configure("result", foreground="#66ccff", font=("Consolas", 12))
|
266
|
-
|
267
|
-
# Configure syntax highlighting tags
|
268
|
-
for token, style in self.style:
|
269
|
-
if style["color"]:
|
270
|
-
fg = f"#{style['color']}"
|
271
|
-
font = ("Consolas", 12, "bold" if style["bold"] else "normal")
|
272
|
-
self.tag_configure(str(token), foreground=fg, font=font)
|
273
|
-
|
274
|
-
def _setupBindings(self):
|
275
|
-
"""Setup all key and mouse bindings."""
|
276
|
-
self.bind("<Return>", self.onEnter)
|
277
|
-
self.bind("<Shift-Return>", self.onShiftEnter)
|
278
|
-
self.bind("<Control-c>", self.cancel)
|
279
|
-
self.bind("<Tab>", self.onTab)
|
280
|
-
self.bind("<BackSpace>", self.onBackspace)
|
281
|
-
self.bind("<KeyRelease>", self.onKeyRelease)
|
282
|
-
self.bind("<KeyPress>", self.onKeyPress)
|
283
|
-
self.bind("<Button-1>", self.onClick)
|
284
|
-
self.bind("<Up>", self.onUp)
|
285
|
-
self.bind("<Down>", self.onDown)
|
286
|
-
|
287
|
-
def getCurrentLineNumber(self):
|
288
|
-
"""Get the line number where current command starts."""
|
289
|
-
return int(self.index("end-1c").split(".")[0])
|
290
|
-
|
291
|
-
def getPromptPosition(self):
|
292
|
-
"""Get the position right after the prompt on current command line."""
|
293
|
-
return f"{self.currentCommandLine}.{self.PROMPT_LENGTH}"
|
294
|
-
|
295
|
-
def getCommandStartPosition(self):
|
296
|
-
"""Get the starting position of the current command."""
|
297
|
-
return f"{self.currentCommandLine}.0"
|
298
|
-
|
299
|
-
def getCurrentCommand(self):
|
300
|
-
"""Extract the current command text (without prompt)."""
|
301
|
-
if self.isExecuting:
|
302
|
-
return ""
|
303
|
-
|
304
|
-
start = self.getPromptPosition()
|
305
|
-
end = "end-1c"
|
306
|
-
return self.get(start, end)
|
307
|
-
|
308
|
-
def replaceCurrentCommand(self, newCommand):
|
309
|
-
"""Replace the current command with new text."""
|
310
|
-
if self.isExecuting:
|
311
|
-
return
|
312
|
-
|
313
|
-
start = self.getPromptPosition()
|
314
|
-
end = "end-1c"
|
315
|
-
|
316
|
-
self.delete(start, end)
|
317
|
-
self.insert(start, newCommand)
|
318
|
-
self.see("end")
|
319
|
-
|
320
|
-
def isCursorInEditableArea(self):
|
321
|
-
"""Check if cursor is in the editable command area."""
|
322
|
-
if self.isExecuting:
|
323
|
-
return False
|
324
|
-
|
325
|
-
cursorLine = int(self.index("insert").split(".")[0])
|
326
|
-
cursorCol = int(self.index("insert").split(".")[1])
|
327
|
-
|
328
|
-
return (cursorLine >= self.currentCommandLine and
|
329
|
-
(cursorLine > self.currentCommandLine or cursorCol >= self.PROMPT_LENGTH))
|
330
|
-
|
331
|
-
def onEnter(self, event):
|
332
|
-
"""Handle Enter key - execute command."""
|
333
|
-
self.suggestionManager.hideSuggestions()
|
334
|
-
|
335
|
-
if self.isExecuting:
|
336
|
-
return "break"
|
337
|
-
|
338
|
-
command = self.getCurrentCommand()
|
339
|
-
|
340
|
-
if not command.strip():
|
341
|
-
return "break"
|
342
|
-
|
343
|
-
# Check if statement is incomplete
|
344
|
-
if self.isIncompleteStatement(command):
|
345
|
-
return self.onShiftEnter(event)
|
346
|
-
|
347
|
-
# Execute the command
|
348
|
-
self.history.add(command)
|
349
|
-
self.mark_set("insert", "end")
|
350
|
-
self.insert("end", "\n")
|
351
|
-
self.see("end")
|
352
|
-
|
353
|
-
# Execute in thread
|
354
|
-
self.isExecuting = True
|
355
|
-
threading.Thread(
|
356
|
-
target=self.executeCommandThreaded,
|
357
|
-
args=(command,),
|
358
|
-
daemon=True
|
359
|
-
).start()
|
360
|
-
|
361
|
-
return "break"
|
362
|
-
|
363
|
-
def onShiftEnter(self, event):
|
364
|
-
"""Handle Shift+Enter - new line with auto-indent."""
|
365
|
-
self.suggestionManager.hideSuggestions()
|
366
|
-
|
367
|
-
if self.isExecuting:
|
368
|
-
return "break"
|
369
|
-
|
370
|
-
# Get current line for indent calculation
|
371
|
-
cursorPos = self.index("insert")
|
372
|
-
lineStart = self.index(f"{cursorPos} linestart")
|
373
|
-
lineEnd = self.index(f"{cursorPos} lineend")
|
374
|
-
currentLine = self.get(lineStart, lineEnd)
|
375
|
-
|
376
|
-
# Calculate indentation
|
377
|
-
indent = self.calculateIndent(currentLine)
|
378
|
-
|
379
|
-
# Insert newline with indent
|
380
|
-
self.insert("insert", "\n" + " " * indent)
|
381
|
-
self.see("end")
|
382
|
-
|
383
|
-
return "break"
|
384
|
-
|
385
|
-
def onTab(self, event):
|
386
|
-
"""Handle Tab key for autocompletion."""
|
387
|
-
if self.isExecuting:
|
388
|
-
return "break"
|
389
|
-
|
390
|
-
if self.suggestionManager.suggestionWindow and \
|
391
|
-
self.suggestionManager.suggestionWindow.winfo_viewable():
|
392
|
-
self.suggestionManager.applySuggestion()
|
393
|
-
else:
|
394
|
-
self.suggestionManager.showSuggestions()
|
395
|
-
|
396
|
-
return "break"
|
397
|
-
|
398
|
-
def onBackspace(self, event):
|
399
|
-
"""Prevent backspace from deleting the prompt."""
|
400
|
-
if not self.isCursorInEditableArea():
|
401
|
-
return "break"
|
402
|
-
|
403
|
-
# Check if we're at the prompt boundary
|
404
|
-
cursorPos = self.index("insert")
|
405
|
-
promptPos = self.getPromptPosition()
|
406
|
-
|
407
|
-
if self.compare(cursorPos, "<=", promptPos):
|
408
|
-
return "break"
|
409
|
-
|
410
|
-
def onClick(self, event):
|
411
|
-
"""Handle mouse clicks - prevent clicking before prompt."""
|
412
|
-
self.suggestionManager.hideSuggestions()
|
413
|
-
return None
|
414
|
-
|
415
|
-
def onKeyPress(self, event):
|
416
|
-
"""Handle key press events."""
|
417
|
-
# print(event.keysym)
|
418
|
-
if self.suggestionManager.suggestionWindow and \
|
419
|
-
self.suggestionManager.suggestionWindow.winfo_viewable():
|
420
|
-
if event.keysym == "Escape":
|
421
|
-
self.suggestionManager.hideSuggestions()
|
422
|
-
return "break"
|
423
|
-
|
424
|
-
# Prevent editing outside command area
|
425
|
-
if not event.keysym in ["Shift_L", "Shift_R", "Control_L", "Control_R"]:
|
426
|
-
self.navigatingHistory = False
|
427
|
-
if not self.isCursorInEditableArea():
|
428
|
-
self.mark_set("insert", "end")
|
429
|
-
|
430
|
-
if event.keysym in ["Left", "Right"]:
|
431
|
-
if self.index("insert") == self.getPromptPosition():
|
432
|
-
self.mark_set("insert", "1.4")
|
433
|
-
return "break"
|
434
|
-
|
435
|
-
def onKeyRelease(self, event):
|
436
|
-
"""Handle key release events."""
|
437
|
-
if event.keysym in ["Return", "Escape", "Left", "Right", "Home", "End"]:
|
438
|
-
self.suggestionManager.hideSuggestions()
|
439
|
-
elif event.keysym not in ["Up", "Down", "Shift_L", "Shift_R", "Control_L", "Control_R"]:
|
440
|
-
if not self.isExecuting:
|
441
|
-
self.after_idle(self.suggestionManager.showSuggestions)
|
442
|
-
self.after_idle(self.highlightCurrentCommand)
|
443
|
-
|
444
|
-
def cancel(self, event):
|
445
|
-
self.history.add(self.getCurrentCommand())
|
446
|
-
self.replaceCurrentCommand("")
|
447
|
-
|
448
|
-
def historyReplace(self, command):
|
449
|
-
if self.getCurrentCommand() == "" or self.navigatingHistory:
|
450
|
-
if self.isExecuting:
|
451
|
-
return "break"
|
452
|
-
|
453
|
-
if self.history.index == len(self.history.history):
|
454
|
-
self.history.setTemp(self.getCurrentCommand())
|
455
|
-
|
456
|
-
if command is not None:
|
457
|
-
self.replaceCurrentCommand(command)
|
458
|
-
self.navigatingHistory = True
|
459
|
-
return("break")
|
460
|
-
|
461
|
-
def onUp(self, event):
|
462
|
-
if self.suggestionManager.suggestionWindow and \
|
463
|
-
self.suggestionManager.suggestionWindow.winfo_viewable():
|
464
|
-
if event.keysym == "Up":
|
465
|
-
self.suggestionManager.handleNavigation("up")
|
466
|
-
return "break"
|
467
|
-
command = self.history.navigateUp()
|
468
|
-
return(self.historyReplace(command))
|
469
|
-
# self.mark_set("insert", "insert -1 line")
|
470
|
-
|
471
|
-
def onDown(self, event):
|
472
|
-
if self.suggestionManager.suggestionWindow and \
|
473
|
-
self.suggestionManager.suggestionWindow.winfo_viewable():
|
474
|
-
if event.keysym == "Down":
|
475
|
-
self.suggestionManager.handleNavigation("down")
|
476
|
-
return "break"
|
477
|
-
command = self.history.navigateDown()
|
478
|
-
return(self.historyReplace(command))
|
479
|
-
|
480
|
-
def isIncompleteStatement(self, code):
|
481
|
-
"""Check if the code is an incomplete statement."""
|
482
|
-
lines = code.split("\n")
|
483
|
-
if not lines[-1].strip():
|
484
|
-
return False
|
485
|
-
|
486
|
-
# Check for line ending with colon
|
487
|
-
for line in lines:
|
488
|
-
if line.strip().endswith(":"):
|
489
|
-
return True
|
490
|
-
|
491
|
-
return False
|
492
|
-
|
493
|
-
def calculateIndent(self, line):
|
494
|
-
"""Calculate the indentation level for the next line."""
|
495
|
-
currentIndent = len(line) - len(line.lstrip())
|
496
|
-
|
497
|
-
# If line ends with colon, increase indent
|
498
|
-
if line.strip().endswith(":"):
|
499
|
-
return currentIndent + 4
|
500
|
-
|
501
|
-
return currentIndent
|
502
|
-
|
503
|
-
def highlightCurrentCommand(self):
|
504
|
-
"""Apply syntax highlighting to the current command."""
|
505
|
-
if self.isExecuting:
|
506
|
-
return
|
507
|
-
|
508
|
-
# Clear existing highlighting
|
509
|
-
start = self.getPromptPosition()
|
510
|
-
end = "end-1c"
|
511
|
-
|
512
|
-
for token, _ in self.style:
|
513
|
-
self.tag_remove(str(token), start, end)
|
514
|
-
|
515
|
-
# Get and highlight the command
|
516
|
-
command = self.getCurrentCommand()
|
517
|
-
if not command:
|
518
|
-
return
|
519
|
-
|
520
|
-
self.mark_set("highlight_pos", start)
|
521
|
-
|
522
|
-
for token, content in pygments.lex(command, self.lexer):
|
523
|
-
if content:
|
524
|
-
endPos = f"highlight_pos + {len(content)}c"
|
525
|
-
if content.strip(): # Only highlight non-whitespace
|
526
|
-
self.tag_add(str(token), "highlight_pos", endPos)
|
527
|
-
self.mark_set("highlight_pos", endPos)
|
528
|
-
|
529
|
-
def writeOutput(self, text, tag="output"):
|
530
|
-
"""Write output to the console (thread-safe)."""
|
531
|
-
def _write():
|
532
|
-
self.insert("end", text + "\n", tag)
|
533
|
-
self.see("end")
|
534
|
-
|
535
|
-
self.after(0, _write)
|
536
|
-
|
537
|
-
def addPrompt(self):
|
538
|
-
"""Add a new command prompt."""
|
539
|
-
def _add():
|
540
|
-
# Store the line number for the new command
|
541
|
-
self.currentCommandLine = self.getCurrentLineNumber()
|
542
|
-
|
543
|
-
# Insert prompt
|
544
|
-
self.insert("end", self.PROMPT)
|
545
|
-
promptStart = f"{self.currentCommandLine}.0"
|
546
|
-
promptEnd = f"{self.currentCommandLine}.{self.PROMPT_LENGTH}"
|
547
|
-
self.tag_add("prompt", promptStart, promptEnd)
|
548
|
-
|
549
|
-
self.mark_set("insert", "end")
|
550
|
-
self.see("end")
|
551
|
-
self.isExecuting = False
|
552
|
-
|
553
|
-
if self.isExecuting:
|
554
|
-
self.after(0, _add)
|
555
|
-
else:
|
556
|
-
_add()
|
557
|
-
|
558
|
-
def executeCommandThreaded(self, command):
|
559
|
-
"""Execute a command in a separate thread."""
|
560
|
-
try:
|
561
|
-
# Try eval first for expressions
|
562
|
-
result = eval(command, self.master.userGlobals, self.master.userLocals)
|
563
|
-
if result is not None:
|
564
|
-
self.writeOutput(str(result), "result")
|
565
|
-
self.master.userLocals["_"] = result
|
566
|
-
except SyntaxError:
|
567
|
-
try:
|
568
|
-
# Try exec for statements
|
569
|
-
exec(command, self.master.userGlobals, self.master.userLocals)
|
570
|
-
except Exception:
|
571
|
-
self.writeOutput(traceback.format_exc(), "error")
|
572
|
-
except Exception:
|
573
|
-
self.writeOutput(traceback.format_exc(), "error")
|
574
|
-
|
575
|
-
# Add new prompt after execution
|
576
|
-
self.addPrompt()
|
30
|
+
def readline(self, *args, **kwargs):
|
31
|
+
return(self.readCallback())
|
577
32
|
|
578
33
|
|
579
34
|
class InteractiveConsole(ctk.CTk):
|
@@ -605,16 +60,28 @@ class InteractiveConsole(ctk.CTk):
|
|
605
60
|
|
606
61
|
# Redirect stdout/stderr
|
607
62
|
self._setupOutputRedirect()
|
63
|
+
self._setupInputRedirect()
|
608
64
|
|
609
65
|
def _createUi(self):
|
610
|
-
"""Create
|
611
|
-
# Main frame
|
66
|
+
"""Create UI with console and help tab."""
|
612
67
|
frame = ctk.CTkFrame(self)
|
613
68
|
frame.pack(padx=10, pady=10, fill="both", expand=True)
|
614
|
-
|
615
|
-
#
|
69
|
+
|
70
|
+
# Horizontal frame
|
71
|
+
self.horizFrame = ctk.CTkFrame(frame)
|
72
|
+
self.horizFrame.pack(fill="both", expand=True)
|
73
|
+
|
74
|
+
# Right: Help Tab
|
75
|
+
self.helpTab = HelpTab(self.horizFrame, width=500)
|
76
|
+
|
77
|
+
# Left: Console
|
78
|
+
self.consoleFrame = ctk.CTkFrame(self.horizFrame, width=600)
|
79
|
+
self.consoleFrame.pack(side="left", fill="both", expand=True)
|
80
|
+
self.consoleFrame.pack_propagate(False) # prevent shrinking to fit contents
|
81
|
+
|
616
82
|
self.console = InteractiveConsoleText(
|
617
|
-
|
83
|
+
self.consoleFrame,
|
84
|
+
self.helpTab,
|
618
85
|
userGlobals=self.userGlobals,
|
619
86
|
userLocals=self.userLocals,
|
620
87
|
wrap="word",
|
@@ -624,9 +91,8 @@ class InteractiveConsole(ctk.CTk):
|
|
624
91
|
font=("Consolas", 12)
|
625
92
|
)
|
626
93
|
self.console.pack(fill="both", expand=True, padx=5, pady=5)
|
627
|
-
|
628
|
-
# Give console access to namespace
|
629
94
|
self.console.master = self
|
95
|
+
|
630
96
|
|
631
97
|
def _setupOutputRedirect(self):
|
632
98
|
"""Setup stdout/stderr redirection to console."""
|
@@ -634,6 +100,10 @@ class InteractiveConsole(ctk.CTk):
|
|
634
100
|
sys.stderr = StdoutRedirect(
|
635
101
|
lambda text, tag: self.console.writeOutput(text, "error")
|
636
102
|
)
|
103
|
+
|
104
|
+
def _setupInputRedirect(self):
|
105
|
+
"""Setup stdin redirection to console."""
|
106
|
+
sys.stdin = StdinRedirect(self.console.readInput)
|
637
107
|
|
638
108
|
def probe(self, *args, **kwargs):
|
639
109
|
"""Start the console main loop."""
|
@@ -647,7 +117,7 @@ if __name__ == "__main__":
|
|
647
117
|
|
648
118
|
def greet(name):
|
649
119
|
print(f"Hello {name}!")
|
650
|
-
return
|
120
|
+
return(f"Greeted {name}")
|
651
121
|
|
652
122
|
# Create the list for testing autocomplete
|
653
123
|
exampleList = [1, 2, 3, 4, 5]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: liveConsole
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.4.1
|
4
4
|
Summary: An IDLE-like debugger to allow for real-time command injection for debugging and testing python code
|
5
5
|
Author-email: Tzur Soffer <tzur.soffer@gmail.com>
|
6
6
|
License: MIT
|
@@ -29,6 +29,9 @@ A fully-featured, **live Python console GUI** built with **CustomTkinter** and *
|
|
29
29
|
|
30
30
|
* Clickable history of previous commands
|
31
31
|
|
32
|
+
* **Integrated Help Panel** for quick access to Python object documentation
|
33
|
+
|
34
|
+
|
32
35
|
## Installation
|
33
36
|
|
34
37
|
`pip install liveConsole`
|
@@ -77,6 +80,17 @@ A fully-featured, **live Python console GUI** built with **CustomTkinter** and *
|
|
77
80
|
* Click to copy them back to the prompt for editing or re-execution.
|
78
81
|
|
79
82
|
|
83
|
+
### Help Panel
|
84
|
+
|
85
|
+
* A resizable right-hand panel that displays Python documentation (help()) for any object.
|
86
|
+
|
87
|
+
* Opens when clicking ctrl+click on a function/method and can be closed with the "X" button.
|
88
|
+
|
89
|
+
* Scrollable and syntax-styled.
|
90
|
+
|
91
|
+
* Perfect for quick reference without leaving the console.
|
92
|
+
|
93
|
+
|
80
94
|
### Easy Integration
|
81
95
|
|
82
96
|
* Automatically grabs **caller frame globals and locals** if not provided.
|
@@ -0,0 +1,12 @@
|
|
1
|
+
__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
commandHistory.py,sha256=xJtWbJ_vgJo2QGgaZJsApTOi_Hm8Yz0V9_zqQUCItj8,1084
|
3
|
+
helpTab.py,sha256=5Fxj_wTNTbuF6sBIN8GdbE6i_zbAEdlrFnbfhnDk5ms,2360
|
4
|
+
liveConsole.py,sha256=9Gwevh3q52Qirig9VJ_qnbTuCda2ydyZnUR29kl7Zlw,3839
|
5
|
+
mainConsole.py,sha256=MxgJJihgbPtKZ8FqXSFczj5neRVGRiM8RX4lcm0ewk0,12973
|
6
|
+
styledTextbox.py,sha256=pc-7gaq_pGTZGZmtr_ARbPuKlKgJYqzD6HTR7tFhx7k,1989
|
7
|
+
suggestionManager.py,sha256=GRde3c1gFAWt_3rvBoFt_-Xl0aOzzloBMD7MHJA2W8U,6256
|
8
|
+
liveconsole-1.4.1.dist-info/licenses/LICENSE,sha256=7dZ0zL72aGaFE0C9DxacOpnaSkC5jajhG6iL7lqhWmU,1064
|
9
|
+
liveconsole-1.4.1.dist-info/METADATA,sha256=fg47oHebYtfDRKCZaFvYOaSHqCTxzTYYWWq87W5_WO0,3648
|
10
|
+
liveconsole-1.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
11
|
+
liveconsole-1.4.1.dist-info/top_level.txt,sha256=QkTpA-HOQwPQRHYwLKYJOhu_zwarar6tdVSuPwYPvl8,88
|
12
|
+
liveconsole-1.4.1.dist-info/RECORD,,
|