liveConsole 1.0.1__py3-none-any.whl → 1.2.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 +509 -357
- {liveconsole-1.0.1.dist-info → liveconsole-1.2.0.dist-info}/METADATA +7 -9
- liveconsole-1.2.0.dist-info/RECORD +7 -0
- liveconsole-1.0.1.dist-info/RECORD +0 -7
- {liveconsole-1.0.1.dist-info → liveconsole-1.2.0.dist-info}/WHEEL +0 -0
- {liveconsole-1.0.1.dist-info → liveconsole-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {liveconsole-1.0.1.dist-info → liveconsole-1.2.0.dist-info}/top_level.txt +0 -0
liveConsole.py
CHANGED
@@ -13,477 +13,629 @@ 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
|
|
28
|
-
class
|
29
|
-
"""
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
self.
|
34
|
-
self.
|
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
|
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
|
66
37
|
self.suggestions = []
|
67
|
-
self.
|
68
|
-
|
69
|
-
# Build suggestion
|
38
|
+
self.selectedSuggestion = 0
|
39
|
+
|
40
|
+
# Build suggestion sources
|
70
41
|
self.keywords = keyword.kwlist
|
71
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)
|
72
49
|
|
73
|
-
#
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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):
|
80
64
|
"""Get code suggestions for partial word."""
|
65
|
+
if len(partialWord) < 2:
|
66
|
+
return []
|
67
|
+
|
81
68
|
suggestions = []
|
82
69
|
|
83
70
|
# Add matching keywords
|
84
71
|
for kw in self.keywords:
|
85
|
-
if kw.startswith(
|
72
|
+
if kw.startswith(partialWord.lower()):
|
86
73
|
suggestions.append(kw)
|
87
74
|
|
88
75
|
# Add matching builtins
|
89
76
|
for builtin in self.builtins:
|
90
|
-
if builtin.startswith(
|
77
|
+
if builtin.startswith(partialWord):
|
91
78
|
suggestions.append(builtin)
|
92
79
|
|
93
80
|
# Add matching variables from namespace
|
94
|
-
|
95
|
-
|
96
|
-
|
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('_'):
|
97
85
|
suggestions.append(var)
|
98
86
|
|
99
|
-
if hasattr(
|
100
|
-
for var in
|
101
|
-
if var.startswith(
|
87
|
+
if hasattr(master, 'userGlobals'):
|
88
|
+
for var in master.userGlobals:
|
89
|
+
if var.startswith(partialWord) and not var.startswith('_'):
|
102
90
|
suggestions.append(var)
|
103
91
|
|
104
92
|
# Remove duplicates and sort
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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)
|
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
|
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)
|
129
99
|
|
130
|
-
suggestions = self.get_suggestions(current_word)
|
131
100
|
if not suggestions:
|
132
|
-
self.
|
101
|
+
self.hideSuggestions()
|
133
102
|
return
|
134
103
|
|
135
104
|
self.suggestions = suggestions
|
136
|
-
self.
|
105
|
+
self.selectedSuggestion = 0
|
137
106
|
|
138
|
-
# Create
|
139
|
-
if not self.
|
140
|
-
self.
|
141
|
-
|
142
|
-
|
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)
|
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)
|
156
113
|
for suggestion in suggestions:
|
157
|
-
self.
|
114
|
+
self.suggestionListbox.insert(tk.END, suggestion)
|
158
115
|
|
159
|
-
self.
|
116
|
+
self.suggestionListbox.selection_set(0)
|
160
117
|
|
161
118
|
# Position window near cursor
|
162
|
-
|
163
|
-
|
164
|
-
|
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")
|
165
127
|
|
166
|
-
self.
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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."""
|
176
153
|
if not suggestion and self.suggestions:
|
177
|
-
suggestion = self.suggestions[self.
|
154
|
+
suggestion = self.suggestions[self.selectedSuggestion]
|
178
155
|
if not suggestion:
|
179
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
|
180
215
|
|
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
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))
|
202
317
|
|
203
|
-
def
|
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):
|
204
373
|
"""Handle Tab key for autocompletion."""
|
205
|
-
if self.
|
206
|
-
self.apply_suggestion()
|
374
|
+
if self.isExecuting:
|
207
375
|
return "break"
|
376
|
+
|
377
|
+
if self.suggestionManager.suggestionWindow and \
|
378
|
+
self.suggestionManager.suggestionWindow.winfo_viewable():
|
379
|
+
self.suggestionManager.applySuggestion()
|
208
380
|
else:
|
209
|
-
self.
|
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():
|
210
388
|
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")
|
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()
|
238
409
|
return "break"
|
239
410
|
|
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
|
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")
|
261
416
|
|
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)
|
417
|
+
if event.keysym in ["Left", "Right"]:
|
418
|
+
if self.index("insert") == self.getPromptPosition():
|
419
|
+
self.mark_set("insert", "1.4")
|
273
420
|
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
421
|
|
292
|
-
def
|
293
|
-
|
422
|
+
def onKeyRelease(self, event):
|
423
|
+
"""Handle key release events."""
|
294
424
|
if event.keysym in ["Return", "Escape", "Left", "Right", "Home", "End"]:
|
295
|
-
self.
|
296
|
-
# Show suggestions on typing
|
425
|
+
self.suggestionManager.hideSuggestions()
|
297
426
|
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()
|
427
|
+
if not self.isExecuting:
|
428
|
+
self.after_idle(self.suggestionManager.showSuggestions)
|
429
|
+
self.after_idle(self.highlightCurrentCommand)
|
303
430
|
|
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"
|
431
|
+
def cancel(self, event):
|
432
|
+
self.history.add(self.getCurrentCommand())
|
433
|
+
self.replaceCurrentCommand("")
|
325
434
|
|
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():
|
435
|
+
def historyReplace(self, command):
|
436
|
+
if self.getCurrentCommand() == "" or self.navigatingHistory:
|
437
|
+
if self.isExecuting:
|
336
438
|
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
439
|
|
363
|
-
|
364
|
-
|
365
|
-
# self.see("end")
|
440
|
+
if self.history.index == len(self.history.history):
|
441
|
+
self.history.setTemp(self.getCurrentCommand())
|
366
442
|
|
367
|
-
|
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")
|
368
457
|
|
369
|
-
def
|
370
|
-
if
|
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:
|
371
493
|
return
|
372
|
-
|
373
|
-
# Clear existing syntax highlighting tags from current line
|
374
|
-
line_start = self.current_prompt_start
|
375
|
-
line_end = "end-1c"
|
376
494
|
|
377
|
-
#
|
378
|
-
|
379
|
-
|
495
|
+
# Clear existing highlighting
|
496
|
+
start = self.getPromptPosition()
|
497
|
+
end = "end-1c"
|
380
498
|
|
381
|
-
|
382
|
-
|
499
|
+
for token, _ in self.style:
|
500
|
+
self.tag_remove(str(token), start, end)
|
383
501
|
|
384
|
-
|
502
|
+
# Get and highlight the command
|
503
|
+
command = self.getCurrentCommand()
|
504
|
+
if not command:
|
385
505
|
return
|
386
|
-
|
387
|
-
|
388
|
-
self.mark_set("range_start", line_start)
|
506
|
+
|
507
|
+
self.mark_set("highlight_pos", start)
|
389
508
|
|
390
509
|
for token, content in pygments.lex(command, self.lexer):
|
391
|
-
if content
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
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)."""
|
398
518
|
def _write():
|
399
|
-
# Insert output at the end
|
400
519
|
self.insert("end", text + "\n", tag)
|
401
520
|
self.see("end")
|
402
521
|
|
403
|
-
# Use after() to ensure GUI updates happen on main thread
|
404
522
|
self.after(0, _write)
|
405
|
-
|
406
|
-
def
|
407
|
-
"""Add a new prompt
|
408
|
-
def
|
409
|
-
|
410
|
-
self.
|
411
|
-
|
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
|
+
|
412
536
|
self.mark_set("insert", "end")
|
413
537
|
self.see("end")
|
538
|
+
self.isExecuting = False
|
414
539
|
|
415
|
-
self.
|
416
|
-
|
417
|
-
|
418
|
-
|
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."""
|
419
547
|
try:
|
420
548
|
# Try eval first for expressions
|
421
549
|
result = eval(command, self.master.userGlobals, self.master.userLocals)
|
422
550
|
if result is not None:
|
423
|
-
self.
|
551
|
+
self.writeOutput(str(result), "result")
|
424
552
|
self.master.userLocals["_"] = result
|
425
553
|
except SyntaxError:
|
426
554
|
try:
|
427
|
-
#
|
555
|
+
# Try exec for statements
|
428
556
|
exec(command, self.master.userGlobals, self.master.userLocals)
|
429
557
|
except Exception:
|
430
|
-
self.
|
558
|
+
self.writeOutput(traceback.format_exc(), "error")
|
431
559
|
except Exception:
|
432
|
-
self.
|
560
|
+
self.writeOutput(traceback.format_exc(), "error")
|
433
561
|
|
434
|
-
# Add new prompt after execution
|
435
|
-
self.
|
562
|
+
# Add new prompt after execution
|
563
|
+
self.addPrompt()
|
436
564
|
|
437
565
|
|
438
566
|
class InteractiveConsole(ctk.CTk):
|
567
|
+
"""Main console window application."""
|
568
|
+
|
439
569
|
def __init__(self, userGlobals=None, userLocals=None):
|
440
570
|
super().__init__()
|
571
|
+
|
572
|
+
# Window setup
|
441
573
|
self.title("Live Interactive Console")
|
442
574
|
self.geometry("900x600")
|
443
|
-
|
575
|
+
|
444
576
|
ctk.set_appearance_mode("dark")
|
445
577
|
ctk.set_default_color_theme("blue")
|
446
|
-
|
447
|
-
#
|
578
|
+
|
579
|
+
# Get namespace from caller if not provided
|
448
580
|
if userGlobals is None or userLocals is None:
|
449
|
-
|
581
|
+
callerFrame = inspect.currentframe().f_back
|
450
582
|
if userGlobals is None:
|
451
|
-
userGlobals =
|
583
|
+
userGlobals = callerFrame.f_globals
|
452
584
|
if userLocals is None:
|
453
|
-
userLocals =
|
454
|
-
|
455
|
-
|
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
|
456
599
|
frame = ctk.CTkFrame(self)
|
457
600
|
frame.pack(padx=10, pady=10, fill="both", expand=True)
|
458
|
-
|
459
|
-
#
|
601
|
+
|
602
|
+
# Console text widget
|
460
603
|
self.console = InteractiveConsoleText(
|
461
|
-
frame,
|
462
|
-
wrap="word",
|
463
|
-
bg="#1e1e1e",
|
464
|
-
fg="white",
|
604
|
+
frame,
|
605
|
+
wrap="word",
|
606
|
+
bg="#1e1e1e",
|
607
|
+
fg="white",
|
465
608
|
insertbackground="white",
|
466
609
|
font=("Consolas", 12)
|
467
610
|
)
|
468
611
|
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
|
612
|
+
|
613
|
+
# Give console access to namespace
|
479
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
|
+
|
480
627
|
|
481
628
|
# Example usage
|
482
629
|
if __name__ == "__main__":
|
630
|
+
# Example variables and functions for testing
|
483
631
|
foo = 42
|
484
|
-
|
632
|
+
|
485
633
|
def greet(name):
|
486
634
|
print(f"Hello {name}!")
|
487
635
|
return f"Greeted {name}"
|
488
|
-
|
489
|
-
|
636
|
+
|
637
|
+
# Create the list for testing autocomplete
|
638
|
+
exampleList = [1, 2, 3, 4, 5]
|
639
|
+
|
640
|
+
# Start the console
|
641
|
+
InteractiveConsole().probe()
|
@@ -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,7 @@
|
|
1
|
+
__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
liveConsole.py,sha256=xpGmtFXzYhPNSN2_SLn4ZNfkqs0N-ZTPCHOzfh3I-X4,22333
|
3
|
+
liveconsole-1.2.0.dist-info/licenses/LICENSE,sha256=7dZ0zL72aGaFE0C9DxacOpnaSkC5jajhG6iL7lqhWmU,1064
|
4
|
+
liveconsole-1.2.0.dist-info/METADATA,sha256=Kjpd8EstbIfYrfedOMg8kRFKYwR2pawG_Lviu_IemvM,3264
|
5
|
+
liveconsole-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
6
|
+
liveconsole-1.2.0.dist-info/top_level.txt,sha256=0gva5OCe9lWcj5T88SwGimDDbqsdVqIGQNs98NBH1K0,21
|
7
|
+
liveconsole-1.2.0.dist-info/RECORD,,
|
@@ -1,7 +0,0 @@
|
|
1
|
-
__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
liveConsole.py,sha256=DeY7Iwz35r1hUo1k8XonR5Cn8FJTJaj0kA7ujp0LXs0,18910
|
3
|
-
liveconsole-1.0.1.dist-info/licenses/LICENSE,sha256=7dZ0zL72aGaFE0C9DxacOpnaSkC5jajhG6iL7lqhWmU,1064
|
4
|
-
liveconsole-1.0.1.dist-info/METADATA,sha256=9oIw-oA3oiK-a0peBgjAwnJBhMTjHCPaXclyxmRSZIQ,3328
|
5
|
-
liveconsole-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
6
|
-
liveconsole-1.0.1.dist-info/top_level.txt,sha256=0gva5OCe9lWcj5T88SwGimDDbqsdVqIGQNs98NBH1K0,21
|
7
|
-
liveconsole-1.0.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|