liveConsole 1.0.1__tar.gz → 1.2.0__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.
- {liveconsole-1.0.1/src/liveConsole.egg-info → liveconsole-1.2.0}/PKG-INFO +7 -9
- {liveconsole-1.0.1 → liveconsole-1.2.0}/README.md +6 -8
- {liveconsole-1.0.1 → liveconsole-1.2.0}/pyproject.toml +1 -1
- {liveconsole-1.0.1 → liveconsole-1.2.0/src/liveConsole.egg-info}/PKG-INFO +7 -9
- liveconsole-1.2.0/src/liveConsole.py +641 -0
- liveconsole-1.0.1/src/liveConsole.py +0 -489
- {liveconsole-1.0.1 → liveconsole-1.2.0}/LICENSE +0 -0
- {liveconsole-1.0.1 → liveconsole-1.2.0}/setup.cfg +0 -0
- {liveconsole-1.0.1 → liveconsole-1.2.0}/src/__init__.py +0 -0
- {liveconsole-1.0.1 → liveconsole-1.2.0}/src/liveConsole.egg-info/SOURCES.txt +0 -0
- {liveconsole-1.0.1 → liveconsole-1.2.0}/src/liveConsole.egg-info/dependency_links.txt +0 -0
- {liveconsole-1.0.1 → liveconsole-1.2.0}/src/liveConsole.egg-info/requires.txt +0 -0
- {liveconsole-1.0.1 → liveconsole-1.2.0}/src/liveConsole.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: liveConsole
|
3
|
-
Version: 1.0
|
3
|
+
Version: 1.2.0
|
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,11 @@ A fully-featured, **live Python console GUI** built with **CustomTkinter** and *
|
|
29
29
|
|
30
30
|
* Clickable history of previous commands
|
31
31
|
|
32
|
+
## Installation
|
33
|
+
|
34
|
+
`pip install pip install liveConsole`
|
35
|
+
|
36
|
+
|
32
37
|
## Features
|
33
38
|
|
34
39
|
### Syntax Highlighting
|
@@ -77,19 +82,12 @@ A fully-featured, **live Python console GUI** built with **CustomTkinter** and *
|
|
77
82
|
* Automatically grabs **caller frame globals and locals** if not provided.
|
78
83
|
|
79
84
|
* Can be used standalone or embedded in larger CustomTkinter applications.
|
80
|
-
|
81
|
-
|
82
|
-
## Installation
|
83
|
-
|
84
|
-
`pip install customtkinter pygments`
|
85
|
-
|
86
|
-
> `Tkinter` is included with Python on most platforms.
|
87
85
|
|
88
86
|
## Usage
|
89
87
|
|
90
88
|
```
|
91
89
|
from liveConsole import InteractiveConsole
|
92
|
-
InteractiveConsole().
|
90
|
+
InteractiveConsole().probe()
|
93
91
|
```
|
94
92
|
|
95
93
|
* Type Python commands in the `>>>` prompt and see live output.
|
@@ -16,6 +16,11 @@ A fully-featured, **live Python console GUI** built with **CustomTkinter** and *
|
|
16
16
|
|
17
17
|
* Clickable history of previous commands
|
18
18
|
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
`pip install pip install liveConsole`
|
22
|
+
|
23
|
+
|
19
24
|
## Features
|
20
25
|
|
21
26
|
### Syntax Highlighting
|
@@ -64,19 +69,12 @@ A fully-featured, **live Python console GUI** built with **CustomTkinter** and *
|
|
64
69
|
* Automatically grabs **caller frame globals and locals** if not provided.
|
65
70
|
|
66
71
|
* Can be used standalone or embedded in larger CustomTkinter applications.
|
67
|
-
|
68
|
-
|
69
|
-
## Installation
|
70
|
-
|
71
|
-
`pip install customtkinter pygments`
|
72
|
-
|
73
|
-
> `Tkinter` is included with Python on most platforms.
|
74
72
|
|
75
73
|
## Usage
|
76
74
|
|
77
75
|
```
|
78
76
|
from liveConsole import InteractiveConsole
|
79
|
-
InteractiveConsole().
|
77
|
+
InteractiveConsole().probe()
|
80
78
|
```
|
81
79
|
|
82
80
|
* Type Python commands in the `>>>` prompt and see live output.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "liveConsole"
|
3
|
-
version = "1.0
|
3
|
+
version = "1.2.0"
|
4
4
|
description = "An IDLE-like debugger to allow for real-time command injection for debugging and testing python code"
|
5
5
|
authors = [{ name="Tzur Soffer", email="tzur.soffer@gmail.com" }]
|
6
6
|
license = {text = "MIT"}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: liveConsole
|
3
|
-
Version: 1.0
|
3
|
+
Version: 1.2.0
|
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,11 @@ A fully-featured, **live Python console GUI** built with **CustomTkinter** and *
|
|
29
29
|
|
30
30
|
* Clickable history of previous commands
|
31
31
|
|
32
|
+
## Installation
|
33
|
+
|
34
|
+
`pip install pip install liveConsole`
|
35
|
+
|
36
|
+
|
32
37
|
## Features
|
33
38
|
|
34
39
|
### Syntax Highlighting
|
@@ -77,19 +82,12 @@ A fully-featured, **live Python console GUI** built with **CustomTkinter** and *
|
|
77
82
|
* Automatically grabs **caller frame globals and locals** if not provided.
|
78
83
|
|
79
84
|
* Can be used standalone or embedded in larger CustomTkinter applications.
|
80
|
-
|
81
|
-
|
82
|
-
## Installation
|
83
|
-
|
84
|
-
`pip install customtkinter pygments`
|
85
|
-
|
86
|
-
> `Tkinter` is included with Python on most platforms.
|
87
85
|
|
88
86
|
## Usage
|
89
87
|
|
90
88
|
```
|
91
89
|
from liveConsole import InteractiveConsole
|
92
|
-
InteractiveConsole().
|
90
|
+
InteractiveConsole().probe()
|
93
91
|
```
|
94
92
|
|
95
93
|
* Type Python commands in the `>>>` prompt and see live output.
|
@@ -0,0 +1,641 @@
|
|
1
|
+
import customtkinter as ctk
|
2
|
+
import tkinter as tk
|
3
|
+
import traceback
|
4
|
+
import inspect
|
5
|
+
import threading
|
6
|
+
import sys
|
7
|
+
import io
|
8
|
+
import pygments
|
9
|
+
from pygments.lexers import PythonLexer
|
10
|
+
from pygments.styles import get_style_by_name
|
11
|
+
import keyword
|
12
|
+
import builtins
|
13
|
+
|
14
|
+
|
15
|
+
class StdoutRedirect(io.StringIO):
|
16
|
+
"""Redirects stdout/stderr to a callback function."""
|
17
|
+
|
18
|
+
def __init__(self, writeCallback):
|
19
|
+
super().__init__()
|
20
|
+
self.writeCallback = writeCallback
|
21
|
+
|
22
|
+
def write(self, s):
|
23
|
+
if s.strip():
|
24
|
+
self.writeCallback(s, "output")
|
25
|
+
|
26
|
+
def flush(self):
|
27
|
+
pass
|
28
|
+
|
29
|
+
|
30
|
+
class CodeSuggestionManager:
|
31
|
+
"""Manages code suggestions and autocomplete functionality."""
|
32
|
+
|
33
|
+
def __init__(self, textWidget):
|
34
|
+
self.textWidget = textWidget
|
35
|
+
self.suggestionWindow = None
|
36
|
+
self.suggestionListbox = None
|
37
|
+
self.suggestions = []
|
38
|
+
self.selectedSuggestion = 0
|
39
|
+
|
40
|
+
# Build suggestion sources
|
41
|
+
self.keywords = keyword.kwlist
|
42
|
+
self.builtins = [name for name in dir(builtins) if not name.startswith('_')]
|
43
|
+
|
44
|
+
def getCurrentWord(self):
|
45
|
+
"""Extract the word being typed at cursor position."""
|
46
|
+
cursorPos = self.textWidget.index(tk.INSERT)
|
47
|
+
lineStart = self.textWidget.index(f"{cursorPos} linestart")
|
48
|
+
currentLine = self.textWidget.get(lineStart, cursorPos)
|
49
|
+
|
50
|
+
# Find the current word
|
51
|
+
words = currentLine.split()
|
52
|
+
if not words:
|
53
|
+
return ""
|
54
|
+
|
55
|
+
currentWord = words[-1]
|
56
|
+
# Handle cases like "print(" where we want to suggest after special chars
|
57
|
+
for char in "([{,.":
|
58
|
+
if char in currentWord:
|
59
|
+
currentWord = currentWord.split(char)[-1]
|
60
|
+
|
61
|
+
return currentWord
|
62
|
+
|
63
|
+
def getSuggestions(self, partialWord):
|
64
|
+
"""Get code suggestions for partial word."""
|
65
|
+
if len(partialWord) < 2:
|
66
|
+
return []
|
67
|
+
|
68
|
+
suggestions = []
|
69
|
+
|
70
|
+
# Add matching keywords
|
71
|
+
for kw in self.keywords:
|
72
|
+
if kw.startswith(partialWord.lower()):
|
73
|
+
suggestions.append(kw)
|
74
|
+
|
75
|
+
# Add matching builtins
|
76
|
+
for builtin in self.builtins:
|
77
|
+
if builtin.startswith(partialWord):
|
78
|
+
suggestions.append(builtin)
|
79
|
+
|
80
|
+
# Add matching variables from namespace
|
81
|
+
master = self.textWidget.master
|
82
|
+
if hasattr(master, 'userLocals'):
|
83
|
+
for var in master.userLocals:
|
84
|
+
if var.startswith(partialWord) and not var.startswith('_'):
|
85
|
+
suggestions.append(var)
|
86
|
+
|
87
|
+
if hasattr(master, 'userGlobals'):
|
88
|
+
for var in master.userGlobals:
|
89
|
+
if var.startswith(partialWord) and not var.startswith('_'):
|
90
|
+
suggestions.append(var)
|
91
|
+
|
92
|
+
# Remove duplicates and sort
|
93
|
+
return sorted(list(set(suggestions)))[:10]
|
94
|
+
|
95
|
+
def showSuggestions(self):
|
96
|
+
"""Display the suggestions popup."""
|
97
|
+
currentWord = self.getCurrentWord()
|
98
|
+
suggestions = self.getSuggestions(currentWord)
|
99
|
+
|
100
|
+
if not suggestions:
|
101
|
+
self.hideSuggestions()
|
102
|
+
return
|
103
|
+
|
104
|
+
self.suggestions = suggestions
|
105
|
+
self.selectedSuggestion = 0
|
106
|
+
|
107
|
+
# Create suggestion window if needed
|
108
|
+
if not self.suggestionWindow:
|
109
|
+
self._createSuggestionWindow()
|
110
|
+
|
111
|
+
# Update listbox content
|
112
|
+
self.suggestionListbox.delete(0, tk.END)
|
113
|
+
for suggestion in suggestions:
|
114
|
+
self.suggestionListbox.insert(tk.END, suggestion)
|
115
|
+
|
116
|
+
self.suggestionListbox.selection_set(0)
|
117
|
+
|
118
|
+
# Position window near cursor
|
119
|
+
self._positionSuggestionWindow()
|
120
|
+
self.suggestionWindow.deiconify()
|
121
|
+
|
122
|
+
def _createSuggestionWindow(self):
|
123
|
+
"""Create the suggestion popup window."""
|
124
|
+
self.suggestionWindow = tk.Toplevel(self.textWidget)
|
125
|
+
self.suggestionWindow.wm_overrideredirect(True)
|
126
|
+
self.suggestionWindow.configure(bg="#2d2d2d")
|
127
|
+
|
128
|
+
self.suggestionListbox = tk.Listbox(
|
129
|
+
self.suggestionWindow,
|
130
|
+
bg="#2d2d2d",
|
131
|
+
fg="white",
|
132
|
+
selectbackground="#0066cc",
|
133
|
+
font=("Consolas", 10),
|
134
|
+
height=8
|
135
|
+
)
|
136
|
+
self.suggestionListbox.pack()
|
137
|
+
|
138
|
+
def _positionSuggestionWindow(self):
|
139
|
+
"""Position the suggestion window near the cursor."""
|
140
|
+
cursorPos = self.textWidget.index(tk.INSERT)
|
141
|
+
x, y, _, _ = self.textWidget.bbox(cursorPos)
|
142
|
+
x += self.textWidget.winfo_rootx()
|
143
|
+
y += self.textWidget.winfo_rooty() + 20
|
144
|
+
self.suggestionWindow.geometry(f"+{x}+{y}")
|
145
|
+
|
146
|
+
def hideSuggestions(self):
|
147
|
+
"""Hide the suggestions popup."""
|
148
|
+
if self.suggestionWindow:
|
149
|
+
self.suggestionWindow.withdraw()
|
150
|
+
|
151
|
+
def applySuggestion(self, suggestion=None):
|
152
|
+
"""Apply the selected suggestion at cursor position."""
|
153
|
+
if not suggestion and self.suggestions:
|
154
|
+
suggestion = self.suggestions[self.selectedSuggestion]
|
155
|
+
if not suggestion:
|
156
|
+
return
|
157
|
+
|
158
|
+
currentWord = self.getCurrentWord()
|
159
|
+
if suggestion.startswith(currentWord):
|
160
|
+
# Only insert the missing part
|
161
|
+
missingPart = suggestion[len(currentWord):]
|
162
|
+
cursorPos = self.textWidget.index(tk.INSERT)
|
163
|
+
self.textWidget.insert(cursorPos, missingPart)
|
164
|
+
|
165
|
+
self.hideSuggestions()
|
166
|
+
|
167
|
+
def handleNavigation(self, direction):
|
168
|
+
"""Handle up/down navigation in suggestions."""
|
169
|
+
if not self.suggestions:
|
170
|
+
return
|
171
|
+
|
172
|
+
if direction == "down":
|
173
|
+
self.selectedSuggestion = min(self.selectedSuggestion + 1, len(self.suggestions) - 1)
|
174
|
+
else: # up
|
175
|
+
self.selectedSuggestion = max(self.selectedSuggestion - 1, 0)
|
176
|
+
|
177
|
+
self.suggestionListbox.selection_clear(0, tk.END)
|
178
|
+
self.suggestionListbox.selection_set(self.selectedSuggestion)
|
179
|
+
|
180
|
+
|
181
|
+
class CommandHistory:
|
182
|
+
"""Manages command history and navigation."""
|
183
|
+
|
184
|
+
def __init__(self):
|
185
|
+
self.history = []
|
186
|
+
self.index = -1
|
187
|
+
self.tempCommand = ""
|
188
|
+
|
189
|
+
def add(self, command):
|
190
|
+
"""Add a command to history."""
|
191
|
+
if command.strip():
|
192
|
+
self.history.append(command)
|
193
|
+
self.index = len(self.history)
|
194
|
+
|
195
|
+
def navigateUp(self):
|
196
|
+
"""Get previous command from history."""
|
197
|
+
if self.index > 0:
|
198
|
+
self.index -= 1
|
199
|
+
return self.history[self.index]
|
200
|
+
return None
|
201
|
+
|
202
|
+
def navigateDown(self):
|
203
|
+
"""Get next command from history."""
|
204
|
+
if self.index < len(self.history) - 1:
|
205
|
+
self.index += 1
|
206
|
+
return self.history[self.index]
|
207
|
+
elif self.index == len(self.history) - 1:
|
208
|
+
self.index = len(self.history)
|
209
|
+
return self.tempCommand
|
210
|
+
return None
|
211
|
+
|
212
|
+
def setTemp(self, command):
|
213
|
+
"""Store temporary command while navigating history."""
|
214
|
+
self.tempCommand = command
|
215
|
+
|
216
|
+
|
217
|
+
class InteractiveConsoleText(tk.Text):
|
218
|
+
"""A tk.Text widget with Python syntax highlighting for interactive console."""
|
219
|
+
|
220
|
+
PROMPT = ">>> "
|
221
|
+
PROMPT_LENGTH = 4
|
222
|
+
|
223
|
+
def __init__(self, master, **kwargs):
|
224
|
+
super().__init__(master, **kwargs)
|
225
|
+
|
226
|
+
# Initialize components
|
227
|
+
self.suggestionManager = CodeSuggestionManager(self)
|
228
|
+
|
229
|
+
self.navigatingHistory = False
|
230
|
+
self.history = CommandHistory()
|
231
|
+
|
232
|
+
# Syntax highlighting setup
|
233
|
+
self.lexer = PythonLexer()
|
234
|
+
self.style = get_style_by_name("monokai")
|
235
|
+
|
236
|
+
# Track current command
|
237
|
+
self.currentCommandLine = 1
|
238
|
+
self.isExecuting = False
|
239
|
+
|
240
|
+
# Setup tags and bindings
|
241
|
+
self._setupTags()
|
242
|
+
self._setupBindings()
|
243
|
+
|
244
|
+
# Initialize with first prompt
|
245
|
+
self.addPrompt()
|
246
|
+
|
247
|
+
def _setupTags(self):
|
248
|
+
"""Configure text tags for different output types."""
|
249
|
+
self.tag_configure("prompt", foreground="#00ff00", font=("Consolas", 12, "bold"))
|
250
|
+
self.tag_configure("output", foreground="#ffffff", font=("Consolas", 12))
|
251
|
+
self.tag_configure("error", foreground="#ff6666", font=("Consolas", 12))
|
252
|
+
self.tag_configure("result", foreground="#66ccff", font=("Consolas", 12))
|
253
|
+
|
254
|
+
# Configure syntax highlighting tags
|
255
|
+
for token, style in self.style:
|
256
|
+
if style["color"]:
|
257
|
+
fg = f"#{style['color']}"
|
258
|
+
font = ("Consolas", 12, "bold" if style["bold"] else "normal")
|
259
|
+
self.tag_configure(str(token), foreground=fg, font=font)
|
260
|
+
|
261
|
+
def _setupBindings(self):
|
262
|
+
"""Setup all key and mouse bindings."""
|
263
|
+
self.bind("<Return>", self.onEnter)
|
264
|
+
self.bind("<Shift-Return>", self.onShiftEnter)
|
265
|
+
self.bind("<Control-c>", self.cancel)
|
266
|
+
self.bind("<Tab>", self.onTab)
|
267
|
+
self.bind("<BackSpace>", self.onBackspace)
|
268
|
+
self.bind("<KeyRelease>", self.onKeyRelease)
|
269
|
+
self.bind("<KeyPress>", self.onKeyPress)
|
270
|
+
self.bind("<Button-1>", self.onClick)
|
271
|
+
self.bind("<Up>", self.onUp)
|
272
|
+
self.bind("<Down>", self.onDown)
|
273
|
+
|
274
|
+
def getCurrentLineNumber(self):
|
275
|
+
"""Get the line number where current command starts."""
|
276
|
+
return int(self.index("end-1c").split(".")[0])
|
277
|
+
|
278
|
+
def getPromptPosition(self):
|
279
|
+
"""Get the position right after the prompt on current command line."""
|
280
|
+
return f"{self.currentCommandLine}.{self.PROMPT_LENGTH}"
|
281
|
+
|
282
|
+
def getCommandStartPosition(self):
|
283
|
+
"""Get the starting position of the current command."""
|
284
|
+
return f"{self.currentCommandLine}.0"
|
285
|
+
|
286
|
+
def getCurrentCommand(self):
|
287
|
+
"""Extract the current command text (without prompt)."""
|
288
|
+
if self.isExecuting:
|
289
|
+
return ""
|
290
|
+
|
291
|
+
start = self.getPromptPosition()
|
292
|
+
end = "end-1c"
|
293
|
+
return self.get(start, end)
|
294
|
+
|
295
|
+
def replaceCurrentCommand(self, newCommand):
|
296
|
+
"""Replace the current command with new text."""
|
297
|
+
if self.isExecuting:
|
298
|
+
return
|
299
|
+
|
300
|
+
start = self.getPromptPosition()
|
301
|
+
end = "end-1c"
|
302
|
+
|
303
|
+
self.delete(start, end)
|
304
|
+
self.insert(start, newCommand)
|
305
|
+
self.see("end")
|
306
|
+
|
307
|
+
def isCursorInEditableArea(self):
|
308
|
+
"""Check if cursor is in the editable command area."""
|
309
|
+
if self.isExecuting:
|
310
|
+
return False
|
311
|
+
|
312
|
+
cursorLine = int(self.index("insert").split(".")[0])
|
313
|
+
cursorCol = int(self.index("insert").split(".")[1])
|
314
|
+
|
315
|
+
return (cursorLine >= self.currentCommandLine and
|
316
|
+
(cursorLine > self.currentCommandLine or cursorCol >= self.PROMPT_LENGTH))
|
317
|
+
|
318
|
+
def onEnter(self, event):
|
319
|
+
"""Handle Enter key - execute command."""
|
320
|
+
self.suggestionManager.hideSuggestions()
|
321
|
+
|
322
|
+
if self.isExecuting:
|
323
|
+
return "break"
|
324
|
+
|
325
|
+
command = self.getCurrentCommand()
|
326
|
+
|
327
|
+
if not command.strip():
|
328
|
+
return "break"
|
329
|
+
|
330
|
+
# Check if statement is incomplete
|
331
|
+
if self.isIncompleteStatement(command):
|
332
|
+
return self.onShiftEnter(event)
|
333
|
+
|
334
|
+
# Execute the command
|
335
|
+
self.history.add(command)
|
336
|
+
self.mark_set("insert", "end")
|
337
|
+
self.insert("end", "\n")
|
338
|
+
self.see("end")
|
339
|
+
|
340
|
+
# Execute in thread
|
341
|
+
self.isExecuting = True
|
342
|
+
threading.Thread(
|
343
|
+
target=self.executeCommandThreaded,
|
344
|
+
args=(command,),
|
345
|
+
daemon=True
|
346
|
+
).start()
|
347
|
+
|
348
|
+
return "break"
|
349
|
+
|
350
|
+
def onShiftEnter(self, event):
|
351
|
+
"""Handle Shift+Enter - new line with auto-indent."""
|
352
|
+
self.suggestionManager.hideSuggestions()
|
353
|
+
|
354
|
+
if self.isExecuting:
|
355
|
+
return "break"
|
356
|
+
|
357
|
+
# Get current line for indent calculation
|
358
|
+
cursorPos = self.index("insert")
|
359
|
+
lineStart = self.index(f"{cursorPos} linestart")
|
360
|
+
lineEnd = self.index(f"{cursorPos} lineend")
|
361
|
+
currentLine = self.get(lineStart, lineEnd)
|
362
|
+
|
363
|
+
# Calculate indentation
|
364
|
+
indent = self.calculateIndent(currentLine)
|
365
|
+
|
366
|
+
# Insert newline with indent
|
367
|
+
self.insert("insert", "\n" + " " * indent)
|
368
|
+
self.see("end")
|
369
|
+
|
370
|
+
return "break"
|
371
|
+
|
372
|
+
def onTab(self, event):
|
373
|
+
"""Handle Tab key for autocompletion."""
|
374
|
+
if self.isExecuting:
|
375
|
+
return "break"
|
376
|
+
|
377
|
+
if self.suggestionManager.suggestionWindow and \
|
378
|
+
self.suggestionManager.suggestionWindow.winfo_viewable():
|
379
|
+
self.suggestionManager.applySuggestion()
|
380
|
+
else:
|
381
|
+
self.suggestionManager.showSuggestions()
|
382
|
+
|
383
|
+
return "break"
|
384
|
+
|
385
|
+
def onBackspace(self, event):
|
386
|
+
"""Prevent backspace from deleting the prompt."""
|
387
|
+
if not self.isCursorInEditableArea():
|
388
|
+
return "break"
|
389
|
+
|
390
|
+
# Check if we're at the prompt boundary
|
391
|
+
cursorPos = self.index("insert")
|
392
|
+
promptPos = self.getPromptPosition()
|
393
|
+
|
394
|
+
if self.compare(cursorPos, "<=", promptPos):
|
395
|
+
return "break"
|
396
|
+
|
397
|
+
def onClick(self, event):
|
398
|
+
"""Handle mouse clicks - prevent clicking before prompt."""
|
399
|
+
self.suggestionManager.hideSuggestions()
|
400
|
+
return None
|
401
|
+
|
402
|
+
def onKeyPress(self, event):
|
403
|
+
"""Handle key press events."""
|
404
|
+
# print(event.keysym)
|
405
|
+
if self.suggestionManager.suggestionWindow and \
|
406
|
+
self.suggestionManager.suggestionWindow.winfo_viewable():
|
407
|
+
if event.keysym == "Escape":
|
408
|
+
self.suggestionManager.hideSuggestions()
|
409
|
+
return "break"
|
410
|
+
|
411
|
+
# Prevent editing outside command area
|
412
|
+
if not event.keysym in ["Shift_L", "Shift_R", "Control_L", "Control_R"]:
|
413
|
+
self.navigatingHistory = False
|
414
|
+
if not self.isCursorInEditableArea():
|
415
|
+
self.mark_set("insert", "end")
|
416
|
+
|
417
|
+
if event.keysym in ["Left", "Right"]:
|
418
|
+
if self.index("insert") == self.getPromptPosition():
|
419
|
+
self.mark_set("insert", "1.4")
|
420
|
+
return "break"
|
421
|
+
|
422
|
+
def onKeyRelease(self, event):
|
423
|
+
"""Handle key release events."""
|
424
|
+
if event.keysym in ["Return", "Escape", "Left", "Right", "Home", "End"]:
|
425
|
+
self.suggestionManager.hideSuggestions()
|
426
|
+
elif event.keysym not in ["Up", "Down", "Shift_L", "Shift_R", "Control_L", "Control_R"]:
|
427
|
+
if not self.isExecuting:
|
428
|
+
self.after_idle(self.suggestionManager.showSuggestions)
|
429
|
+
self.after_idle(self.highlightCurrentCommand)
|
430
|
+
|
431
|
+
def cancel(self, event):
|
432
|
+
self.history.add(self.getCurrentCommand())
|
433
|
+
self.replaceCurrentCommand("")
|
434
|
+
|
435
|
+
def historyReplace(self, command):
|
436
|
+
if self.getCurrentCommand() == "" or self.navigatingHistory:
|
437
|
+
if self.isExecuting:
|
438
|
+
return "break"
|
439
|
+
|
440
|
+
if self.history.index == len(self.history.history):
|
441
|
+
self.history.setTemp(self.getCurrentCommand())
|
442
|
+
|
443
|
+
if command is not None:
|
444
|
+
self.replaceCurrentCommand(command)
|
445
|
+
self.navigatingHistory = True
|
446
|
+
return("break")
|
447
|
+
|
448
|
+
def onUp(self, event):
|
449
|
+
if self.suggestionManager.suggestionWindow and \
|
450
|
+
self.suggestionManager.suggestionWindow.winfo_viewable():
|
451
|
+
if event.keysym == "Up":
|
452
|
+
self.suggestionManager.handleNavigation("up")
|
453
|
+
return "break"
|
454
|
+
command = self.history.navigateUp()
|
455
|
+
return(self.historyReplace(command))
|
456
|
+
# self.mark_set("insert", "insert -1 line")
|
457
|
+
|
458
|
+
def onDown(self, event):
|
459
|
+
if self.suggestionManager.suggestionWindow and \
|
460
|
+
self.suggestionManager.suggestionWindow.winfo_viewable():
|
461
|
+
if event.keysym == "Down":
|
462
|
+
self.suggestionManager.handleNavigation("down")
|
463
|
+
return "break"
|
464
|
+
command = self.history.navigateDown()
|
465
|
+
return(self.historyReplace(command))
|
466
|
+
|
467
|
+
def isIncompleteStatement(self, code):
|
468
|
+
"""Check if the code is an incomplete statement."""
|
469
|
+
lines = code.split("\n")
|
470
|
+
if not lines[-1].strip():
|
471
|
+
return False
|
472
|
+
|
473
|
+
# Check for line ending with colon
|
474
|
+
for line in lines:
|
475
|
+
if line.strip().endswith(":"):
|
476
|
+
return True
|
477
|
+
|
478
|
+
return False
|
479
|
+
|
480
|
+
def calculateIndent(self, line):
|
481
|
+
"""Calculate the indentation level for the next line."""
|
482
|
+
currentIndent = len(line) - len(line.lstrip())
|
483
|
+
|
484
|
+
# If line ends with colon, increase indent
|
485
|
+
if line.strip().endswith(":"):
|
486
|
+
return currentIndent + 4
|
487
|
+
|
488
|
+
return currentIndent
|
489
|
+
|
490
|
+
def highlightCurrentCommand(self):
|
491
|
+
"""Apply syntax highlighting to the current command."""
|
492
|
+
if self.isExecuting:
|
493
|
+
return
|
494
|
+
|
495
|
+
# Clear existing highlighting
|
496
|
+
start = self.getPromptPosition()
|
497
|
+
end = "end-1c"
|
498
|
+
|
499
|
+
for token, _ in self.style:
|
500
|
+
self.tag_remove(str(token), start, end)
|
501
|
+
|
502
|
+
# Get and highlight the command
|
503
|
+
command = self.getCurrentCommand()
|
504
|
+
if not command:
|
505
|
+
return
|
506
|
+
|
507
|
+
self.mark_set("highlight_pos", start)
|
508
|
+
|
509
|
+
for token, content in pygments.lex(command, self.lexer):
|
510
|
+
if content:
|
511
|
+
endPos = f"highlight_pos + {len(content)}c"
|
512
|
+
if content.strip(): # Only highlight non-whitespace
|
513
|
+
self.tag_add(str(token), "highlight_pos", endPos)
|
514
|
+
self.mark_set("highlight_pos", endPos)
|
515
|
+
|
516
|
+
def writeOutput(self, text, tag="output"):
|
517
|
+
"""Write output to the console (thread-safe)."""
|
518
|
+
def _write():
|
519
|
+
self.insert("end", text + "\n", tag)
|
520
|
+
self.see("end")
|
521
|
+
|
522
|
+
self.after(0, _write)
|
523
|
+
|
524
|
+
def addPrompt(self):
|
525
|
+
"""Add a new command prompt."""
|
526
|
+
def _add():
|
527
|
+
# Store the line number for the new command
|
528
|
+
self.currentCommandLine = self.getCurrentLineNumber()
|
529
|
+
|
530
|
+
# Insert prompt
|
531
|
+
self.insert("end", self.PROMPT)
|
532
|
+
promptStart = f"{self.currentCommandLine}.0"
|
533
|
+
promptEnd = f"{self.currentCommandLine}.{self.PROMPT_LENGTH}"
|
534
|
+
self.tag_add("prompt", promptStart, promptEnd)
|
535
|
+
|
536
|
+
self.mark_set("insert", "end")
|
537
|
+
self.see("end")
|
538
|
+
self.isExecuting = False
|
539
|
+
|
540
|
+
if self.isExecuting:
|
541
|
+
self.after(0, _add)
|
542
|
+
else:
|
543
|
+
_add()
|
544
|
+
|
545
|
+
def executeCommandThreaded(self, command):
|
546
|
+
"""Execute a command in a separate thread."""
|
547
|
+
try:
|
548
|
+
# Try eval first for expressions
|
549
|
+
result = eval(command, self.master.userGlobals, self.master.userLocals)
|
550
|
+
if result is not None:
|
551
|
+
self.writeOutput(str(result), "result")
|
552
|
+
self.master.userLocals["_"] = result
|
553
|
+
except SyntaxError:
|
554
|
+
try:
|
555
|
+
# Try exec for statements
|
556
|
+
exec(command, self.master.userGlobals, self.master.userLocals)
|
557
|
+
except Exception:
|
558
|
+
self.writeOutput(traceback.format_exc(), "error")
|
559
|
+
except Exception:
|
560
|
+
self.writeOutput(traceback.format_exc(), "error")
|
561
|
+
|
562
|
+
# Add new prompt after execution
|
563
|
+
self.addPrompt()
|
564
|
+
|
565
|
+
|
566
|
+
class InteractiveConsole(ctk.CTk):
|
567
|
+
"""Main console window application."""
|
568
|
+
|
569
|
+
def __init__(self, userGlobals=None, userLocals=None):
|
570
|
+
super().__init__()
|
571
|
+
|
572
|
+
# Window setup
|
573
|
+
self.title("Live Interactive Console")
|
574
|
+
self.geometry("900x600")
|
575
|
+
|
576
|
+
ctk.set_appearance_mode("dark")
|
577
|
+
ctk.set_default_color_theme("blue")
|
578
|
+
|
579
|
+
# Get namespace from caller if not provided
|
580
|
+
if userGlobals is None or userLocals is None:
|
581
|
+
callerFrame = inspect.currentframe().f_back
|
582
|
+
if userGlobals is None:
|
583
|
+
userGlobals = callerFrame.f_globals
|
584
|
+
if userLocals is None:
|
585
|
+
userLocals = callerFrame.f_locals
|
586
|
+
|
587
|
+
self.userGlobals = userGlobals
|
588
|
+
self.userLocals = userLocals
|
589
|
+
|
590
|
+
# Create UI
|
591
|
+
self._createUi()
|
592
|
+
|
593
|
+
# Redirect stdout/stderr
|
594
|
+
self._setupOutputRedirect()
|
595
|
+
|
596
|
+
def _createUi(self):
|
597
|
+
"""Create the user interface."""
|
598
|
+
# Main frame
|
599
|
+
frame = ctk.CTkFrame(self)
|
600
|
+
frame.pack(padx=10, pady=10, fill="both", expand=True)
|
601
|
+
|
602
|
+
# Console text widget
|
603
|
+
self.console = InteractiveConsoleText(
|
604
|
+
frame,
|
605
|
+
wrap="word",
|
606
|
+
bg="#1e1e1e",
|
607
|
+
fg="white",
|
608
|
+
insertbackground="white",
|
609
|
+
font=("Consolas", 12)
|
610
|
+
)
|
611
|
+
self.console.pack(fill="both", expand=True, padx=5, pady=5)
|
612
|
+
|
613
|
+
# Give console access to namespace
|
614
|
+
self.console.master = self
|
615
|
+
|
616
|
+
def _setupOutputRedirect(self):
|
617
|
+
"""Setup stdout/stderr redirection to console."""
|
618
|
+
sys.stdout = StdoutRedirect(self.console.writeOutput)
|
619
|
+
sys.stderr = StdoutRedirect(
|
620
|
+
lambda text, tag: self.console.writeOutput(text, "error")
|
621
|
+
)
|
622
|
+
|
623
|
+
def probe(self, *args, **kwargs):
|
624
|
+
"""Start the console main loop."""
|
625
|
+
self.mainloop(*args, **kwargs)
|
626
|
+
|
627
|
+
|
628
|
+
# Example usage
|
629
|
+
if __name__ == "__main__":
|
630
|
+
# Example variables and functions for testing
|
631
|
+
foo = 42
|
632
|
+
|
633
|
+
def greet(name):
|
634
|
+
print(f"Hello {name}!")
|
635
|
+
return f"Greeted {name}"
|
636
|
+
|
637
|
+
# Create the list for testing autocomplete
|
638
|
+
exampleList = [1, 2, 3, 4, 5]
|
639
|
+
|
640
|
+
# Start the console
|
641
|
+
InteractiveConsole().probe()
|
@@ -1,489 +0,0 @@
|
|
1
|
-
import customtkinter as ctk
|
2
|
-
import tkinter as tk
|
3
|
-
import traceback
|
4
|
-
import inspect
|
5
|
-
import threading
|
6
|
-
import sys
|
7
|
-
import io
|
8
|
-
import pygments
|
9
|
-
from pygments.lexers import PythonLexer
|
10
|
-
from pygments.styles import get_style_by_name
|
11
|
-
import keyword
|
12
|
-
import builtins
|
13
|
-
|
14
|
-
|
15
|
-
class StdoutRedirect(io.StringIO):
|
16
|
-
def __init__(self, write_callback):
|
17
|
-
super().__init__()
|
18
|
-
self.write_callback = write_callback
|
19
|
-
|
20
|
-
def write(self, s):
|
21
|
-
if s.strip():
|
22
|
-
self.write_callback(s, "output")
|
23
|
-
|
24
|
-
def flush(self):
|
25
|
-
pass
|
26
|
-
|
27
|
-
|
28
|
-
class InteractiveConsoleText(tk.Text):
|
29
|
-
"""A tk.Text widget with Python syntax highlighting for interactive console."""
|
30
|
-
def __init__(self, master, **kwargs):
|
31
|
-
super().__init__(master, **kwargs)
|
32
|
-
|
33
|
-
self.lexer = PythonLexer()
|
34
|
-
self.style = get_style_by_name("monokai")
|
35
|
-
|
36
|
-
# Configure tags for different output types
|
37
|
-
self.tag_configure("prompt", foreground="#00ff00", font=("Consolas", 12, "bold"))
|
38
|
-
self.tag_configure("output", foreground="#ffffff", font=("Consolas", 12))
|
39
|
-
self.tag_configure("error", foreground="#ff6666", font=("Consolas", 12))
|
40
|
-
self.tag_configure("result", foreground="#66ccff", font=("Consolas", 12))
|
41
|
-
self.tag_configure("suggestion", background="#444444", foreground="#cccccc", font=("Consolas", 12))
|
42
|
-
|
43
|
-
# Apply tag configs for syntax highlighting
|
44
|
-
for token, style in self.style:
|
45
|
-
if style["color"]:
|
46
|
-
fg = f"#{style['color']}"
|
47
|
-
font = ("Consolas", 12, "bold" if style["bold"] else "normal")
|
48
|
-
self.tag_configure(str(token), foreground=fg, font=font)
|
49
|
-
|
50
|
-
# Bind events
|
51
|
-
self.bind("<KeyRelease>", self.on_key_release)
|
52
|
-
self.bind("<Return>", self.on_enter)
|
53
|
-
self.bind("<Shift-Return>", self.on_shift_enter)
|
54
|
-
self.bind("<Button-1>", self.on_click)
|
55
|
-
self.bind("<KeyPress>", self.on_key_press)
|
56
|
-
self.bind("<Motion>", self.on_mouse_motion)
|
57
|
-
self.bind("<Tab>", self.on_tab)
|
58
|
-
|
59
|
-
# Track current command
|
60
|
-
self.current_prompt_start = None
|
61
|
-
self.current_prompt_end = None
|
62
|
-
self.command_history = []
|
63
|
-
self.history_index = -1
|
64
|
-
self.hover_command = None
|
65
|
-
self.suggestion_window = None
|
66
|
-
self.suggestions = []
|
67
|
-
self.selected_suggestion = 0
|
68
|
-
|
69
|
-
# Build suggestion lists
|
70
|
-
self.keywords = keyword.kwlist
|
71
|
-
self.builtins = [name for name in dir(builtins) if not name.startswith('_')]
|
72
|
-
|
73
|
-
# Initialize with prompt
|
74
|
-
self.insert("end", ">>> ")
|
75
|
-
self.tag_add("prompt", "end-4c", "end")
|
76
|
-
self.current_prompt_start = self.index("end-4c")
|
77
|
-
self.mark_set("insert", "end")
|
78
|
-
|
79
|
-
def get_suggestions(self, partial_word):
|
80
|
-
"""Get code suggestions for partial word."""
|
81
|
-
suggestions = []
|
82
|
-
|
83
|
-
# Add matching keywords
|
84
|
-
for kw in self.keywords:
|
85
|
-
if kw.startswith(partial_word.lower()):
|
86
|
-
suggestions.append(kw)
|
87
|
-
|
88
|
-
# Add matching builtins
|
89
|
-
for builtin in self.builtins:
|
90
|
-
if builtin.startswith(partial_word):
|
91
|
-
suggestions.append(builtin)
|
92
|
-
|
93
|
-
# Add matching variables from namespace
|
94
|
-
if hasattr(self.master, 'userLocals'):
|
95
|
-
for var in self.master.userLocals:
|
96
|
-
if var.startswith(partial_word) and not var.startswith('_'):
|
97
|
-
suggestions.append(var)
|
98
|
-
|
99
|
-
if hasattr(self.master, 'userGlobals'):
|
100
|
-
for var in self.master.userGlobals:
|
101
|
-
if var.startswith(partial_word) and not var.startswith('_'):
|
102
|
-
suggestions.append(var)
|
103
|
-
|
104
|
-
# Remove duplicates and sort
|
105
|
-
suggestions = sorted(list(set(suggestions)))
|
106
|
-
return suggestions[:10] # Limit to 10 suggestions
|
107
|
-
|
108
|
-
def show_suggestions(self):
|
109
|
-
"""Show code suggestions popup."""
|
110
|
-
# Get current word being typed
|
111
|
-
cursor_pos = self.index(tk.INSERT)
|
112
|
-
line_start = self.index(f"{cursor_pos} linestart")
|
113
|
-
current_line = self.get(line_start, cursor_pos)
|
114
|
-
|
115
|
-
# Find the current word
|
116
|
-
words = current_line.split()
|
117
|
-
if not words:
|
118
|
-
return
|
119
|
-
|
120
|
-
current_word = words[-1]
|
121
|
-
# Handle cases like "print(" where we want to suggest after the parenthesis
|
122
|
-
for char in "([{,.":
|
123
|
-
if char in current_word:
|
124
|
-
current_word = current_word.split(char)[-1]
|
125
|
-
|
126
|
-
if len(current_word) < 2: # Only show suggestions for 2+ characters
|
127
|
-
self.hide_suggestions()
|
128
|
-
return
|
129
|
-
|
130
|
-
suggestions = self.get_suggestions(current_word)
|
131
|
-
if not suggestions:
|
132
|
-
self.hide_suggestions()
|
133
|
-
return
|
134
|
-
|
135
|
-
self.suggestions = suggestions
|
136
|
-
self.selected_suggestion = 0
|
137
|
-
|
138
|
-
# Create or update suggestion window
|
139
|
-
if not self.suggestion_window:
|
140
|
-
self.suggestion_window = tk.Toplevel(self)
|
141
|
-
self.suggestion_window.wm_overrideredirect(True)
|
142
|
-
self.suggestion_window.configure(bg="#2d2d2d")
|
143
|
-
|
144
|
-
self.suggestion_listbox = tk.Listbox(
|
145
|
-
self.suggestion_window,
|
146
|
-
bg="#2d2d2d",
|
147
|
-
fg="white",
|
148
|
-
selectbackground="#0066cc",
|
149
|
-
font=("Consolas", 10),
|
150
|
-
height=min(len(suggestions), 8)
|
151
|
-
)
|
152
|
-
self.suggestion_listbox.pack()
|
153
|
-
|
154
|
-
# Clear and populate listbox
|
155
|
-
self.suggestion_listbox.delete(0, tk.END)
|
156
|
-
for suggestion in suggestions:
|
157
|
-
self.suggestion_listbox.insert(tk.END, suggestion)
|
158
|
-
|
159
|
-
self.suggestion_listbox.selection_set(0)
|
160
|
-
|
161
|
-
# Position window near cursor
|
162
|
-
x, y, _, _ = self.bbox(cursor_pos)
|
163
|
-
x += self.winfo_rootx()
|
164
|
-
y += self.winfo_rooty() + 20
|
165
|
-
|
166
|
-
self.suggestion_window.geometry(f"+{x}+{y}")
|
167
|
-
self.suggestion_window.deiconify()
|
168
|
-
|
169
|
-
def hide_suggestions(self):
|
170
|
-
"""Hide suggestions popup."""
|
171
|
-
if self.suggestion_window:
|
172
|
-
self.suggestion_window.withdraw()
|
173
|
-
|
174
|
-
def apply_suggestion(self, suggestion=None):
|
175
|
-
"""Apply selected suggestion at the cursor position (only missing letters)."""
|
176
|
-
if not suggestion and self.suggestions:
|
177
|
-
suggestion = self.suggestions[self.selected_suggestion]
|
178
|
-
if not suggestion:
|
179
|
-
return
|
180
|
-
|
181
|
-
# Current cursor position
|
182
|
-
cursor_pos = self.index(tk.INSERT)
|
183
|
-
|
184
|
-
# Get the word fragment before the cursor
|
185
|
-
line_start = self.index(f"{cursor_pos} linestart")
|
186
|
-
current_line = self.get(line_start, cursor_pos)
|
187
|
-
|
188
|
-
fragment = ""
|
189
|
-
for i in range(len(current_line)):
|
190
|
-
if current_line[-(i+1)] in " \t([{,.)":
|
191
|
-
break
|
192
|
-
fragment = current_line[-(i+1):]
|
193
|
-
|
194
|
-
# Only insert the missing part
|
195
|
-
if suggestion.startswith(fragment):
|
196
|
-
missing_part = suggestion[len(fragment):]
|
197
|
-
self.insert(cursor_pos, missing_part)
|
198
|
-
self.mark_set("insert", f"{cursor_pos} + {len(missing_part)}c")
|
199
|
-
|
200
|
-
self.hide_suggestions()
|
201
|
-
|
202
|
-
|
203
|
-
def on_tab(self, event):
|
204
|
-
"""Handle Tab key for autocompletion."""
|
205
|
-
if self.suggestion_window and self.suggestion_window.winfo_viewable():
|
206
|
-
self.apply_suggestion()
|
207
|
-
return "break"
|
208
|
-
else:
|
209
|
-
self.show_suggestions()
|
210
|
-
return "break"
|
211
|
-
|
212
|
-
def is_incomplete_statement(self, code):
|
213
|
-
"""Check if the code is an incomplete statement that needs more lines."""
|
214
|
-
code = code.split("\n")
|
215
|
-
if code[-1].strip() == "":
|
216
|
-
return(False)
|
217
|
-
if code[0].strip().endswith(":"):
|
218
|
-
return(True)
|
219
|
-
return(False)
|
220
|
-
|
221
|
-
def get_indent_level(self, line):
|
222
|
-
"""Get the indentation level of a line."""
|
223
|
-
return len(line) - len(line.lstrip(' '))
|
224
|
-
|
225
|
-
def should_auto_indent(self, line):
|
226
|
-
"""Check if we should add indentation after this line."""
|
227
|
-
stripped = line.strip()
|
228
|
-
return (stripped and stripped[-1] == ':')
|
229
|
-
|
230
|
-
def on_click(self, event):
|
231
|
-
self.hide_suggestions()
|
232
|
-
click_pos = self.index(f"@{event.x},{event.y}")
|
233
|
-
|
234
|
-
if self.current_prompt_start:
|
235
|
-
click_pos = self.index(tk.CURRENT)
|
236
|
-
if self.compare(click_pos, "<", self.current_prompt_start):
|
237
|
-
self.mark_set("insert", "end")
|
238
|
-
return "break"
|
239
|
-
|
240
|
-
def on_mouse_motion(self, event):
|
241
|
-
"""Handle mouse motion for hover copying previous commands."""
|
242
|
-
|
243
|
-
mouse_pos = self.index(f"@{event.x},{event.y}")
|
244
|
-
line_start = self.index(f"{mouse_pos} linestart")
|
245
|
-
line_end = self.index(f"{mouse_pos} lineend")
|
246
|
-
line_text = self.get(line_start, line_end)
|
247
|
-
|
248
|
-
# Check if this line starts with ">>> " (a previous command)
|
249
|
-
if line_text.startswith(">>> ") and line_start != self.current_prompt_start:
|
250
|
-
command = line_text[4:] # Remove ">>> "
|
251
|
-
if command.strip():
|
252
|
-
# Change cursor to indicate clickable
|
253
|
-
self.config(cursor="hand2")
|
254
|
-
self.hover_command = command.strip()
|
255
|
-
else:
|
256
|
-
self.config(cursor="xterm")
|
257
|
-
self.hover_command = None
|
258
|
-
else:
|
259
|
-
self.config(cursor="xterm")
|
260
|
-
self.hover_command = None
|
261
|
-
|
262
|
-
def on_key_press(self, event):
|
263
|
-
if self.suggestion_window and self.suggestion_window.winfo_viewable():
|
264
|
-
if event.keysym == "Down":
|
265
|
-
self.selected_suggestion = min(self.selected_suggestion + 1, len(self.suggestions) - 1)
|
266
|
-
self.suggestion_listbox.selection_clear(0, tk.END)
|
267
|
-
self.suggestion_listbox.selection_set(self.selected_suggestion)
|
268
|
-
return "break"
|
269
|
-
elif event.keysym == "Up":
|
270
|
-
self.selected_suggestion = max(self.selected_suggestion - 1, 0)
|
271
|
-
self.suggestion_listbox.selection_clear(0, tk.END)
|
272
|
-
self.suggestion_listbox.selection_set(self.selected_suggestion)
|
273
|
-
return "break"
|
274
|
-
elif event.keysym == "Escape":
|
275
|
-
self.hide_suggestions()
|
276
|
-
return "break"
|
277
|
-
elif event.keysym in ["Return", "Tab"]:
|
278
|
-
self.apply_suggestion()
|
279
|
-
return "break"
|
280
|
-
|
281
|
-
# Ensure cursor is always at least 4 chars after current_prompt_start
|
282
|
-
prompt_end_index = f"{self.current_prompt_start} + 3c"
|
283
|
-
|
284
|
-
if event.keysym not in ["Up", "Down", "Left", "Right", "Shift_L", "Shift_R", "Control_L", "Control_R"]:
|
285
|
-
if self.compare("insert", "<", prompt_end_index):
|
286
|
-
self.mark_set("insert", prompt_end_index)
|
287
|
-
|
288
|
-
# Block Backspace if at or before prompt
|
289
|
-
if event.keysym == "BackSpace" and self.compare("insert", "<=", prompt_end_index):
|
290
|
-
return "break"
|
291
|
-
|
292
|
-
def on_key_release(self, event):
|
293
|
-
# Hide suggestions on certain keys
|
294
|
-
if event.keysym in ["Return", "Escape", "Left", "Right", "Home", "End"]:
|
295
|
-
self.hide_suggestions()
|
296
|
-
# Show suggestions on typing
|
297
|
-
elif event.keysym not in ["Up", "Down", "Shift_L", "Shift_R", "Control_L", "Control_R"]:
|
298
|
-
self.after_idle(self.show_suggestions)
|
299
|
-
|
300
|
-
# Only highlight the current command line
|
301
|
-
if self.current_prompt_start:
|
302
|
-
self.highlight_current_line()
|
303
|
-
|
304
|
-
def on_shift_enter(self, event):
|
305
|
-
"""Handle Shift+Enter for new line with auto-indent."""
|
306
|
-
self.hide_suggestions()
|
307
|
-
|
308
|
-
if self.current_prompt_start:
|
309
|
-
# Get current line to determine indent
|
310
|
-
current_line_start = self.index("insert linestart")
|
311
|
-
current_line_end = self.index("insert lineend")
|
312
|
-
current_line = self.get(current_line_start, current_line_end)
|
313
|
-
|
314
|
-
# Calculate indent level
|
315
|
-
base_indent = self.get_indent_level(current_line)
|
316
|
-
|
317
|
-
# If the current line should increase indent, add 4 spaces
|
318
|
-
if self.should_auto_indent(current_line):
|
319
|
-
base_indent += 4
|
320
|
-
|
321
|
-
# Insert newline with proper indentation
|
322
|
-
self.insert("insert", "\n" + " " * base_indent)
|
323
|
-
self.mark_set("insert", "end")
|
324
|
-
return "break"
|
325
|
-
|
326
|
-
def on_enter(self, event):
|
327
|
-
"""Handle Enter key - execute if complete, newline if incomplete."""
|
328
|
-
self.hide_suggestions()
|
329
|
-
|
330
|
-
if self.current_prompt_start:
|
331
|
-
# Get text from after the prompt to end
|
332
|
-
prompt_end = f"{self.current_prompt_start} + 3c" # Skip ">>> "
|
333
|
-
command = self.get(prompt_end, "end-1c")
|
334
|
-
|
335
|
-
if not command.strip():
|
336
|
-
return "break"
|
337
|
-
|
338
|
-
# Check if it's an incomplete statement
|
339
|
-
if self.is_incomplete_statement(command):
|
340
|
-
# Add newline with auto-indent
|
341
|
-
current_line_start = self.index("insert linestart")
|
342
|
-
current_line_end = self.index("insert lineend")
|
343
|
-
current_line = self.get(current_line_start, current_line_end)
|
344
|
-
base_indent = self.get_indent_level(current_line)
|
345
|
-
|
346
|
-
if self.should_auto_indent(current_line):
|
347
|
-
base_indent += 4
|
348
|
-
|
349
|
-
self.insert("insert", "\n" + " " * base_indent)
|
350
|
-
self.see("end")
|
351
|
-
return "break"
|
352
|
-
|
353
|
-
# Execute the complete command
|
354
|
-
if command.strip():
|
355
|
-
self.command_history.append(command)
|
356
|
-
self.history_index = len(self.command_history)
|
357
|
-
|
358
|
-
# Move to end and add newline for the executed command
|
359
|
-
self.mark_set("insert", "end")
|
360
|
-
self.insert("end", "\n")
|
361
|
-
self.see("end")
|
362
|
-
|
363
|
-
# Execute the command in a thread to prevent freezing
|
364
|
-
threading.Thread(target=self.execute_command_and_add_prompt, args=(command,), daemon=True).start()
|
365
|
-
# self.see("end")
|
366
|
-
|
367
|
-
return "break"
|
368
|
-
|
369
|
-
def highlight_current_line(self):
|
370
|
-
if not self.current_prompt_start:
|
371
|
-
return
|
372
|
-
|
373
|
-
# Clear existing syntax highlighting tags from current line
|
374
|
-
line_start = self.current_prompt_start
|
375
|
-
line_end = "end-1c"
|
376
|
-
|
377
|
-
# Remove all token tags from current line
|
378
|
-
for token, style in self.style:
|
379
|
-
self.tag_remove(str(token), line_start, line_end)
|
380
|
-
|
381
|
-
# Get the command text (without the prompt)
|
382
|
-
command = self.get(line_start, line_end)
|
383
|
-
|
384
|
-
if not command.strip():
|
385
|
-
return
|
386
|
-
|
387
|
-
# Highlight the command
|
388
|
-
self.mark_set("range_start", line_start)
|
389
|
-
|
390
|
-
for token, content in pygments.lex(command, self.lexer):
|
391
|
-
if content.strip(): # Only highlight non-whitespace
|
392
|
-
self.mark_set("range_end", f"range_start + {len(content)}c")
|
393
|
-
self.tag_add(str(token), "range_start", "range_end")
|
394
|
-
self.mark_set("range_start", f"range_start + {len(content)}c")
|
395
|
-
|
396
|
-
def write_output(self, text, tag="output"):
|
397
|
-
"""Write output to the console - thread safe."""
|
398
|
-
def _write():
|
399
|
-
# Insert output at the end
|
400
|
-
self.insert("end", text + "\n", tag)
|
401
|
-
self.see("end")
|
402
|
-
|
403
|
-
# Use after() to ensure GUI updates happen on main thread
|
404
|
-
self.after(0, _write)
|
405
|
-
|
406
|
-
def add_new_prompt(self):
|
407
|
-
"""Add a new prompt - thread safe."""
|
408
|
-
def _add_prompt():
|
409
|
-
self.insert("end", ">>> ")
|
410
|
-
self.tag_add("prompt", "end-4c", "end")
|
411
|
-
self.current_prompt_start = self.index("end-4c")
|
412
|
-
self.mark_set("insert", "end")
|
413
|
-
self.see("end")
|
414
|
-
|
415
|
-
self.after(0, _add_prompt)
|
416
|
-
|
417
|
-
def execute_command_and_add_prompt(self, command):
|
418
|
-
"""Execute a command and then add a new prompt."""
|
419
|
-
try:
|
420
|
-
# Try eval first for expressions
|
421
|
-
result = eval(command, self.master.userGlobals, self.master.userLocals)
|
422
|
-
if result is not None:
|
423
|
-
self.write_output(str(result), "result")
|
424
|
-
self.master.userLocals["_"] = result
|
425
|
-
except SyntaxError:
|
426
|
-
try:
|
427
|
-
# If eval fails, try exec for statements
|
428
|
-
exec(command, self.master.userGlobals, self.master.userLocals)
|
429
|
-
except Exception:
|
430
|
-
self.write_output(traceback.format_exc(), "error")
|
431
|
-
except Exception:
|
432
|
-
self.write_output(traceback.format_exc(), "error")
|
433
|
-
|
434
|
-
# Add new prompt after execution is complete
|
435
|
-
self.add_new_prompt()
|
436
|
-
|
437
|
-
|
438
|
-
class InteractiveConsole(ctk.CTk):
|
439
|
-
def __init__(self, userGlobals=None, userLocals=None):
|
440
|
-
super().__init__()
|
441
|
-
self.title("Live Interactive Console")
|
442
|
-
self.geometry("900x600")
|
443
|
-
|
444
|
-
ctk.set_appearance_mode("dark")
|
445
|
-
ctk.set_default_color_theme("blue")
|
446
|
-
|
447
|
-
# If no globals/locals provided, get them from caller frame
|
448
|
-
if userGlobals is None or userLocals is None:
|
449
|
-
caller_frame = inspect.currentframe().f_back
|
450
|
-
if userGlobals is None:
|
451
|
-
userGlobals = caller_frame.f_globals
|
452
|
-
if userLocals is None:
|
453
|
-
userLocals = caller_frame.f_locals
|
454
|
-
|
455
|
-
# Create frame for the text widget
|
456
|
-
frame = ctk.CTkFrame(self)
|
457
|
-
frame.pack(padx=10, pady=10, fill="both", expand=True)
|
458
|
-
|
459
|
-
# Single console text widget
|
460
|
-
self.console = InteractiveConsoleText(
|
461
|
-
frame,
|
462
|
-
wrap="word",
|
463
|
-
bg="#1e1e1e",
|
464
|
-
fg="white",
|
465
|
-
insertbackground="white",
|
466
|
-
font=("Consolas", 12)
|
467
|
-
)
|
468
|
-
self.console.pack(fill="both", expand=True, padx=5, pady=5)
|
469
|
-
|
470
|
-
# Namespace
|
471
|
-
self.userGlobals = userGlobals
|
472
|
-
self.userLocals = userLocals
|
473
|
-
|
474
|
-
# Redirect stdout/stderr to write to console
|
475
|
-
sys.stdout = StdoutRedirect(self.console.write_output)
|
476
|
-
sys.stderr = StdoutRedirect(lambda text, tag: self.console.write_output(text, "error"))
|
477
|
-
|
478
|
-
# Give console access to namespaces
|
479
|
-
self.console.master = self
|
480
|
-
|
481
|
-
# Example usage
|
482
|
-
if __name__ == "__main__":
|
483
|
-
foo = 42
|
484
|
-
|
485
|
-
def greet(name):
|
486
|
-
print(f"Hello {name}!")
|
487
|
-
return f"Greeted {name}"
|
488
|
-
|
489
|
-
InteractiveConsole().mainloop()
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|