liveConsole 1.1.0__py3-none-any.whl → 1.3.0__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.
- liveConsole.py +529 -365
- {liveconsole-1.1.0.dist-info → liveconsole-1.3.0.dist-info}/METADATA +1 -1
- liveconsole-1.3.0.dist-info/RECORD +7 -0
- liveconsole-1.1.0.dist-info/RECORD +0 -7
- {liveconsole-1.1.0.dist-info → liveconsole-1.3.0.dist-info}/WHEEL +0 -0
- {liveconsole-1.1.0.dist-info → liveconsole-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {liveconsole-1.1.0.dist-info → liveconsole-1.3.0.dist-info}/top_level.txt +0 -0
liveConsole.py
CHANGED
@@ -13,480 +13,644 @@ import builtins
|
|
13
13
|
|
14
14
|
|
15
15
|
class StdoutRedirect(io.StringIO):
|
16
|
-
|
16
|
+
"""Redirects stdout/stderr to a callback function."""
|
17
|
+
|
18
|
+
def __init__(self, writeCallback):
|
17
19
|
super().__init__()
|
18
|
-
self.
|
20
|
+
self.writeCallback = writeCallback
|
19
21
|
|
20
22
|
def write(self, s):
|
21
23
|
if s.strip():
|
22
|
-
self.
|
24
|
+
self.writeCallback(s, "output")
|
23
25
|
|
24
26
|
def flush(self):
|
25
27
|
pass
|
26
28
|
|
27
29
|
|
30
|
+
class CodeSuggestionManager:
|
31
|
+
"""Manages code suggestions and autocomplete functionality."""
|
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 Exception as e:
|
68
|
+
print(e)
|
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
|
+
|
28
230
|
class InteractiveConsoleText(tk.Text):
|
29
231
|
"""A tk.Text widget with Python syntax highlighting for interactive console."""
|
30
|
-
|
232
|
+
|
233
|
+
PROMPT = ">>> "
|
234
|
+
PROMPT_LENGTH = 4
|
235
|
+
|
236
|
+
def __init__(self, master, userLocals=None, userGlobals=None, **kwargs):
|
31
237
|
super().__init__(master, **kwargs)
|
32
|
-
|
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
|
33
246
|
self.lexer = PythonLexer()
|
34
247
|
self.style = get_style_by_name("monokai")
|
35
|
-
|
36
|
-
#
|
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."""
|
37
262
|
self.tag_configure("prompt", foreground="#00ff00", font=("Consolas", 12, "bold"))
|
38
263
|
self.tag_configure("output", foreground="#ffffff", font=("Consolas", 12))
|
39
264
|
self.tag_configure("error", foreground="#ff6666", font=("Consolas", 12))
|
40
265
|
self.tag_configure("result", foreground="#66ccff", font=("Consolas", 12))
|
41
|
-
|
42
|
-
|
43
|
-
# Apply tag configs for syntax highlighting
|
266
|
+
|
267
|
+
# Configure syntax highlighting tags
|
44
268
|
for token, style in self.style:
|
45
269
|
if style["color"]:
|
46
270
|
fg = f"#{style['color']}"
|
47
271
|
font = ("Consolas", 12, "bold" if style["bold"] else "normal")
|
48
272
|
self.tag_configure(str(token), foreground=fg, font=font)
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
self.bind("<Return>", self.
|
53
|
-
self.bind("<Shift-Return>", self.
|
54
|
-
self.bind("<
|
55
|
-
self.bind("<
|
56
|
-
self.bind("<
|
57
|
-
self.bind("<
|
58
|
-
|
59
|
-
|
60
|
-
self.
|
61
|
-
self.
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
self.
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
self.
|
78
|
-
|
79
|
-
def get_suggestions(self, partial_word):
|
80
|
-
"""Get code suggestions for partial word."""
|
81
|
-
suggestions = []
|
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 ""
|
82
303
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
87
312
|
|
88
|
-
|
89
|
-
|
90
|
-
if builtin.startswith(partial_word):
|
91
|
-
suggestions.append(builtin)
|
313
|
+
start = self.getPromptPosition()
|
314
|
+
end = "end-1c"
|
92
315
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
98
324
|
|
99
|
-
|
100
|
-
|
101
|
-
if var.startswith(partial_word) and not var.startswith('_'):
|
102
|
-
suggestions.append(var)
|
325
|
+
cursorLine = int(self.index("insert").split(".")[0])
|
326
|
+
cursorCol = int(self.index("insert").split(".")[1])
|
103
327
|
|
104
|
-
|
105
|
-
|
106
|
-
return suggestions[:10] # Limit to 10 suggestions
|
328
|
+
return (cursorLine >= self.currentCommandLine and
|
329
|
+
(cursorLine > self.currentCommandLine or cursorCol >= self.PROMPT_LENGTH))
|
107
330
|
|
108
|
-
def
|
109
|
-
"""
|
110
|
-
|
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)
|
331
|
+
def onEnter(self, event):
|
332
|
+
"""Handle Enter key - execute command."""
|
333
|
+
self.suggestionManager.hideSuggestions()
|
114
334
|
|
115
|
-
|
116
|
-
|
117
|
-
if not words:
|
118
|
-
return
|
335
|
+
if self.isExecuting:
|
336
|
+
return "break"
|
119
337
|
|
120
|
-
|
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]
|
338
|
+
command = self.getCurrentCommand()
|
125
339
|
|
126
|
-
if
|
127
|
-
|
128
|
-
return
|
340
|
+
if not command.strip():
|
341
|
+
return "break"
|
129
342
|
|
130
|
-
|
131
|
-
if
|
132
|
-
self.
|
133
|
-
return
|
343
|
+
# Check if statement is incomplete
|
344
|
+
if self.isIncompleteStatement(command):
|
345
|
+
return self.onShiftEnter(event)
|
134
346
|
|
135
|
-
|
136
|
-
self.
|
347
|
+
# Execute the command
|
348
|
+
self.history.add(command)
|
349
|
+
self.mark_set("insert", "end")
|
350
|
+
self.insert("end", "\n")
|
351
|
+
self.see("end")
|
137
352
|
|
138
|
-
#
|
139
|
-
|
140
|
-
|
141
|
-
self.
|
142
|
-
|
143
|
-
|
144
|
-
|
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)
|
353
|
+
# Execute in thread
|
354
|
+
self.isExecuting = True
|
355
|
+
threading.Thread(
|
356
|
+
target=self.executeCommandThreaded,
|
357
|
+
args=(command,),
|
358
|
+
daemon=True
|
359
|
+
).start()
|
158
360
|
|
159
|
-
|
361
|
+
return "break"
|
362
|
+
|
363
|
+
def onShiftEnter(self, event):
|
364
|
+
"""Handle Shift+Enter - new line with auto-indent."""
|
365
|
+
self.suggestionManager.hideSuggestions()
|
160
366
|
|
161
|
-
|
162
|
-
|
163
|
-
x += self.winfo_rootx()
|
164
|
-
y += self.winfo_rooty() + 20
|
367
|
+
if self.isExecuting:
|
368
|
+
return "break"
|
165
369
|
|
166
|
-
|
167
|
-
self.
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
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):
|
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):
|
204
386
|
"""Handle Tab key for autocompletion."""
|
205
|
-
if self.
|
206
|
-
self.apply_suggestion()
|
387
|
+
if self.isExecuting:
|
207
388
|
return "break"
|
389
|
+
|
390
|
+
if self.suggestionManager.suggestionWindow and \
|
391
|
+
self.suggestionManager.suggestionWindow.winfo_viewable():
|
392
|
+
self.suggestionManager.applySuggestion()
|
208
393
|
else:
|
209
|
-
self.
|
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():
|
210
401
|
return "break"
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
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")
|
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()
|
238
422
|
return "break"
|
239
423
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
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
|
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")
|
261
429
|
|
262
|
-
|
263
|
-
|
264
|
-
|
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()
|
430
|
+
if event.keysym in ["Left", "Right"]:
|
431
|
+
if self.index("insert") == self.getPromptPosition():
|
432
|
+
self.mark_set("insert", "1.4")
|
276
433
|
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
434
|
|
288
|
-
|
289
|
-
|
290
|
-
return "break"
|
291
|
-
|
292
|
-
def on_key_release(self, event):
|
293
|
-
# Hide suggestions on certain keys
|
435
|
+
def onKeyRelease(self, event):
|
436
|
+
"""Handle key release events."""
|
294
437
|
if event.keysym in ["Return", "Escape", "Left", "Right", "Home", "End"]:
|
295
|
-
self.
|
296
|
-
# Show suggestions on typing
|
438
|
+
self.suggestionManager.hideSuggestions()
|
297
439
|
elif event.keysym not in ["Up", "Down", "Shift_L", "Shift_R", "Control_L", "Control_R"]:
|
298
|
-
self.
|
299
|
-
|
300
|
-
|
301
|
-
if self.current_prompt_start:
|
302
|
-
self.highlight_current_line()
|
440
|
+
if not self.isExecuting:
|
441
|
+
self.after_idle(self.suggestionManager.showSuggestions)
|
442
|
+
self.after_idle(self.highlightCurrentCommand)
|
303
443
|
|
304
|
-
def
|
305
|
-
|
306
|
-
self.
|
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"
|
444
|
+
def cancel(self, event):
|
445
|
+
self.history.add(self.getCurrentCommand())
|
446
|
+
self.replaceCurrentCommand("")
|
325
447
|
|
326
|
-
def
|
327
|
-
|
328
|
-
|
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")
|
448
|
+
def historyReplace(self, command):
|
449
|
+
if self.getCurrentCommand() == "" or self.navigatingHistory:
|
450
|
+
if self.isExecuting:
|
351
451
|
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
452
|
|
363
|
-
|
364
|
-
|
365
|
-
# self.see("end")
|
453
|
+
if self.history.index == len(self.history.history):
|
454
|
+
self.history.setTemp(self.getCurrentCommand())
|
366
455
|
|
367
|
-
|
456
|
+
if command is not None:
|
457
|
+
self.replaceCurrentCommand(command)
|
458
|
+
self.navigatingHistory = True
|
459
|
+
return("break")
|
368
460
|
|
369
|
-
def
|
370
|
-
if
|
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:
|
371
506
|
return
|
372
|
-
|
373
|
-
# Clear existing syntax highlighting tags from current line
|
374
|
-
line_start = self.current_prompt_start
|
375
|
-
line_end = "end-1c"
|
376
507
|
|
377
|
-
#
|
378
|
-
|
379
|
-
|
508
|
+
# Clear existing highlighting
|
509
|
+
start = self.getPromptPosition()
|
510
|
+
end = "end-1c"
|
380
511
|
|
381
|
-
|
382
|
-
|
512
|
+
for token, _ in self.style:
|
513
|
+
self.tag_remove(str(token), start, end)
|
383
514
|
|
384
|
-
|
515
|
+
# Get and highlight the command
|
516
|
+
command = self.getCurrentCommand()
|
517
|
+
if not command:
|
385
518
|
return
|
386
|
-
|
387
|
-
|
388
|
-
self.mark_set("range_start", line_start)
|
519
|
+
|
520
|
+
self.mark_set("highlight_pos", start)
|
389
521
|
|
390
522
|
for token, content in pygments.lex(command, self.lexer):
|
391
|
-
if content
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
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)."""
|
398
531
|
def _write():
|
399
|
-
# Insert output at the end
|
400
532
|
self.insert("end", text + "\n", tag)
|
401
533
|
self.see("end")
|
402
534
|
|
403
|
-
# Use after() to ensure GUI updates happen on main thread
|
404
535
|
self.after(0, _write)
|
405
|
-
|
406
|
-
def
|
407
|
-
"""Add a new prompt
|
408
|
-
def
|
409
|
-
|
410
|
-
self.
|
411
|
-
|
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
|
+
|
412
549
|
self.mark_set("insert", "end")
|
413
550
|
self.see("end")
|
551
|
+
self.isExecuting = False
|
414
552
|
|
415
|
-
self.
|
416
|
-
|
417
|
-
|
418
|
-
|
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."""
|
419
560
|
try:
|
420
561
|
# Try eval first for expressions
|
421
562
|
result = eval(command, self.master.userGlobals, self.master.userLocals)
|
422
563
|
if result is not None:
|
423
|
-
self.
|
564
|
+
self.writeOutput(str(result), "result")
|
424
565
|
self.master.userLocals["_"] = result
|
425
566
|
except SyntaxError:
|
426
567
|
try:
|
427
|
-
#
|
568
|
+
# Try exec for statements
|
428
569
|
exec(command, self.master.userGlobals, self.master.userLocals)
|
429
570
|
except Exception:
|
430
|
-
self.
|
571
|
+
self.writeOutput(traceback.format_exc(), "error")
|
431
572
|
except Exception:
|
432
|
-
self.
|
573
|
+
self.writeOutput(traceback.format_exc(), "error")
|
433
574
|
|
434
|
-
# Add new prompt after execution
|
435
|
-
self.
|
575
|
+
# Add new prompt after execution
|
576
|
+
self.addPrompt()
|
436
577
|
|
437
578
|
|
438
579
|
class InteractiveConsole(ctk.CTk):
|
580
|
+
"""Main console window application."""
|
581
|
+
|
439
582
|
def __init__(self, userGlobals=None, userLocals=None):
|
440
583
|
super().__init__()
|
584
|
+
|
585
|
+
# Window setup
|
441
586
|
self.title("Live Interactive Console")
|
442
587
|
self.geometry("900x600")
|
443
|
-
|
588
|
+
|
444
589
|
ctk.set_appearance_mode("dark")
|
445
590
|
ctk.set_default_color_theme("blue")
|
446
|
-
|
447
|
-
#
|
591
|
+
|
592
|
+
# Get namespace from caller if not provided
|
448
593
|
if userGlobals is None or userLocals is None:
|
449
|
-
|
594
|
+
callerFrame = inspect.currentframe().f_back
|
450
595
|
if userGlobals is None:
|
451
|
-
userGlobals =
|
596
|
+
userGlobals = callerFrame.f_globals
|
452
597
|
if userLocals is None:
|
453
|
-
userLocals =
|
454
|
-
|
455
|
-
|
598
|
+
userLocals = callerFrame.f_locals
|
599
|
+
|
600
|
+
self.userGlobals = userGlobals
|
601
|
+
self.userLocals = userLocals
|
602
|
+
|
603
|
+
# Create UI
|
604
|
+
self._createUi()
|
605
|
+
|
606
|
+
# Redirect stdout/stderr
|
607
|
+
# self._setupOutputRedirect()
|
608
|
+
|
609
|
+
def _createUi(self):
|
610
|
+
"""Create the user interface."""
|
611
|
+
# Main frame
|
456
612
|
frame = ctk.CTkFrame(self)
|
457
613
|
frame.pack(padx=10, pady=10, fill="both", expand=True)
|
458
|
-
|
459
|
-
#
|
614
|
+
|
615
|
+
# Console text widget
|
460
616
|
self.console = InteractiveConsoleText(
|
461
|
-
frame,
|
462
|
-
|
463
|
-
|
464
|
-
|
617
|
+
frame,
|
618
|
+
userGlobals=self.userGlobals,
|
619
|
+
userLocals=self.userLocals,
|
620
|
+
wrap="word",
|
621
|
+
bg="#1e1e1e",
|
622
|
+
fg="white",
|
465
623
|
insertbackground="white",
|
466
624
|
font=("Consolas", 12)
|
467
625
|
)
|
468
626
|
self.console.pack(fill="both", expand=True, padx=5, pady=5)
|
469
|
-
|
470
|
-
#
|
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
|
627
|
+
|
628
|
+
# Give console access to namespace
|
479
629
|
self.console.master = self
|
480
630
|
|
631
|
+
def _setupOutputRedirect(self):
|
632
|
+
"""Setup stdout/stderr redirection to console."""
|
633
|
+
sys.stdout = StdoutRedirect(self.console.writeOutput)
|
634
|
+
sys.stderr = StdoutRedirect(
|
635
|
+
lambda text, tag: self.console.writeOutput(text, "error")
|
636
|
+
)
|
637
|
+
|
481
638
|
def probe(self, *args, **kwargs):
|
639
|
+
"""Start the console main loop."""
|
482
640
|
self.mainloop(*args, **kwargs)
|
483
641
|
|
642
|
+
|
484
643
|
# Example usage
|
485
644
|
if __name__ == "__main__":
|
645
|
+
# Example variables and functions for testing
|
486
646
|
foo = 42
|
487
|
-
|
647
|
+
|
488
648
|
def greet(name):
|
489
649
|
print(f"Hello {name}!")
|
490
650
|
return f"Greeted {name}"
|
491
|
-
|
651
|
+
|
652
|
+
# Create the list for testing autocomplete
|
653
|
+
exampleList = [1, 2, 3, 4, 5]
|
654
|
+
|
655
|
+
# Start the console
|
492
656
|
InteractiveConsole().probe()
|
@@ -0,0 +1,7 @@
|
|
1
|
+
__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
liveConsole.py,sha256=JfdKL-tqpz1-yuCsCLVGvvvgTlj6RvDsY0M6v48Hn1k,23200
|
3
|
+
liveconsole-1.3.0.dist-info/licenses/LICENSE,sha256=7dZ0zL72aGaFE0C9DxacOpnaSkC5jajhG6iL7lqhWmU,1064
|
4
|
+
liveconsole-1.3.0.dist-info/METADATA,sha256=kq6XZfv13P9uL_pDYa4OFB2-Kc5BkRCcbW-5zhV9iM8,3264
|
5
|
+
liveconsole-1.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
6
|
+
liveconsole-1.3.0.dist-info/top_level.txt,sha256=0gva5OCe9lWcj5T88SwGimDDbqsdVqIGQNs98NBH1K0,21
|
7
|
+
liveconsole-1.3.0.dist-info/RECORD,,
|
@@ -1,7 +0,0 @@
|
|
1
|
-
__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
liveConsole.py,sha256=v4GZn6YoeRcfmro2uHHrmnfgxSE7_ia9K6xl059IwnU,18992
|
3
|
-
liveconsole-1.1.0.dist-info/licenses/LICENSE,sha256=7dZ0zL72aGaFE0C9DxacOpnaSkC5jajhG6iL7lqhWmU,1064
|
4
|
-
liveconsole-1.1.0.dist-info/METADATA,sha256=zuhb5dU2MuDwymoJrvpQmxgIj_cVWedVfg9-TgeWl8A,3264
|
5
|
-
liveconsole-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
6
|
-
liveconsole-1.1.0.dist-info/top_level.txt,sha256=0gva5OCe9lWcj5T88SwGimDDbqsdVqIGQNs98NBH1K0,21
|
7
|
-
liveconsole-1.1.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|