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 CHANGED
@@ -13,480 +13,644 @@ import builtins
13
13
 
14
14
 
15
15
  class StdoutRedirect(io.StringIO):
16
- def __init__(self, write_callback):
16
+ """Redirects stdout/stderr to a callback function."""
17
+
18
+ def __init__(self, writeCallback):
17
19
  super().__init__()
18
- self.write_callback = write_callback
20
+ self.writeCallback = writeCallback
19
21
 
20
22
  def write(self, s):
21
23
  if s.strip():
22
- self.write_callback(s, "output")
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
- def __init__(self, master, **kwargs):
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
- # Configure tags for different output types
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
- self.tag_configure("suggestion", background="#444444", foreground="#cccccc", font=("Consolas", 12))
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
- # 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 = []
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
- # Add matching keywords
84
- for kw in self.keywords:
85
- if kw.startswith(partial_word.lower()):
86
- suggestions.append(kw)
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
- # Add matching builtins
89
- for builtin in self.builtins:
90
- if builtin.startswith(partial_word):
91
- suggestions.append(builtin)
313
+ start = self.getPromptPosition()
314
+ end = "end-1c"
92
315
 
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)
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
- 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)
325
+ cursorLine = int(self.index("insert").split(".")[0])
326
+ cursorCol = int(self.index("insert").split(".")[1])
103
327
 
104
- # Remove duplicates and sort
105
- suggestions = sorted(list(set(suggestions)))
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 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)
331
+ def onEnter(self, event):
332
+ """Handle Enter key - execute command."""
333
+ self.suggestionManager.hideSuggestions()
114
334
 
115
- # Find the current word
116
- words = current_line.split()
117
- if not words:
118
- return
335
+ if self.isExecuting:
336
+ return "break"
119
337
 
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]
338
+ command = self.getCurrentCommand()
125
339
 
126
- if len(current_word) < 2: # Only show suggestions for 2+ characters
127
- self.hide_suggestions()
128
- return
340
+ if not command.strip():
341
+ return "break"
129
342
 
130
- suggestions = self.get_suggestions(current_word)
131
- if not suggestions:
132
- self.hide_suggestions()
133
- return
343
+ # Check if statement is incomplete
344
+ if self.isIncompleteStatement(command):
345
+ return self.onShiftEnter(event)
134
346
 
135
- self.suggestions = suggestions
136
- self.selected_suggestion = 0
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
- # 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)
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
- self.suggestion_listbox.selection_set(0)
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
- # Position window near cursor
162
- x, y, _, _ = self.bbox(cursor_pos)
163
- x += self.winfo_rootx()
164
- y += self.winfo_rooty() + 20
367
+ if self.isExecuting:
368
+ return "break"
165
369
 
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):
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.suggestion_window and self.suggestion_window.winfo_viewable():
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.show_suggestions()
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
- 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")
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
- 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
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
- 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()
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
- # 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
435
+ def onKeyRelease(self, event):
436
+ """Handle key release events."""
294
437
  if event.keysym in ["Return", "Escape", "Left", "Right", "Home", "End"]:
295
- self.hide_suggestions()
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.after_idle(self.show_suggestions)
299
-
300
- # Only highlight the current command line
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 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"
444
+ def cancel(self, event):
445
+ self.history.add(self.getCurrentCommand())
446
+ self.replaceCurrentCommand("")
325
447
 
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")
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
- # 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")
453
+ if self.history.index == len(self.history.history):
454
+ self.history.setTemp(self.getCurrentCommand())
366
455
 
367
- return "break"
456
+ if command is not None:
457
+ self.replaceCurrentCommand(command)
458
+ self.navigatingHistory = True
459
+ return("break")
368
460
 
369
- def highlight_current_line(self):
370
- if not self.current_prompt_start:
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
- # Remove all token tags from current line
378
- for token, style in self.style:
379
- self.tag_remove(str(token), line_start, line_end)
508
+ # Clear existing highlighting
509
+ start = self.getPromptPosition()
510
+ end = "end-1c"
380
511
 
381
- # Get the command text (without the prompt)
382
- command = self.get(line_start, line_end)
512
+ for token, _ in self.style:
513
+ self.tag_remove(str(token), start, end)
383
514
 
384
- if not command.strip():
515
+ # Get and highlight the command
516
+ command = self.getCurrentCommand()
517
+ if not command:
385
518
  return
386
-
387
- # Highlight the command
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.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."""
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 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")
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.after(0, _add_prompt)
416
-
417
- def execute_command_and_add_prompt(self, command):
418
- """Execute a command and then add a new prompt."""
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.write_output(str(result), "result")
564
+ self.writeOutput(str(result), "result")
424
565
  self.master.userLocals["_"] = result
425
566
  except SyntaxError:
426
567
  try:
427
- # If eval fails, try exec for statements
568
+ # Try exec for statements
428
569
  exec(command, self.master.userGlobals, self.master.userLocals)
429
570
  except Exception:
430
- self.write_output(traceback.format_exc(), "error")
571
+ self.writeOutput(traceback.format_exc(), "error")
431
572
  except Exception:
432
- self.write_output(traceback.format_exc(), "error")
573
+ self.writeOutput(traceback.format_exc(), "error")
433
574
 
434
- # Add new prompt after execution is complete
435
- self.add_new_prompt()
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
- # If no globals/locals provided, get them from caller frame
591
+
592
+ # Get namespace from caller if not provided
448
593
  if userGlobals is None or userLocals is None:
449
- caller_frame = inspect.currentframe().f_back
594
+ callerFrame = inspect.currentframe().f_back
450
595
  if userGlobals is None:
451
- userGlobals = caller_frame.f_globals
596
+ userGlobals = callerFrame.f_globals
452
597
  if userLocals is None:
453
- userLocals = caller_frame.f_locals
454
-
455
- # Create frame for the text widget
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
- # Single console text widget
614
+
615
+ # Console text widget
460
616
  self.console = InteractiveConsoleText(
461
- frame,
462
- wrap="word",
463
- bg="#1e1e1e",
464
- fg="white",
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
- # 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
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: liveConsole
3
- Version: 1.1.0
3
+ Version: 1.3.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
@@ -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,,