liveConsole 1.1.0__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 CHANGED
@@ -13,480 +13,629 @@ 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
 
28
- class InteractiveConsoleText(tk.Text):
29
- """A tk.Text widget with Python syntax highlighting for interactive console."""
30
- def __init__(self, master, **kwargs):
31
- super().__init__(master, **kwargs)
32
-
33
- self.lexer = PythonLexer()
34
- self.style = get_style_by_name("monokai")
35
-
36
- # Configure tags for different output types
37
- self.tag_configure("prompt", foreground="#00ff00", font=("Consolas", 12, "bold"))
38
- self.tag_configure("output", foreground="#ffffff", font=("Consolas", 12))
39
- self.tag_configure("error", foreground="#ff6666", font=("Consolas", 12))
40
- self.tag_configure("result", foreground="#66ccff", font=("Consolas", 12))
41
- self.tag_configure("suggestion", background="#444444", foreground="#cccccc", font=("Consolas", 12))
42
-
43
- # Apply tag configs for syntax highlighting
44
- for token, style in self.style:
45
- if style["color"]:
46
- fg = f"#{style['color']}"
47
- font = ("Consolas", 12, "bold" if style["bold"] else "normal")
48
- self.tag_configure(str(token), foreground=fg, font=font)
49
-
50
- # Bind events
51
- self.bind("<KeyRelease>", self.on_key_release)
52
- self.bind("<Return>", self.on_enter)
53
- self.bind("<Shift-Return>", self.on_shift_enter)
54
- self.bind("<Button-1>", self.on_click)
55
- self.bind("<KeyPress>", self.on_key_press)
56
- self.bind("<Motion>", self.on_mouse_motion)
57
- self.bind("<Tab>", self.on_tab)
58
-
59
- # Track current command
60
- self.current_prompt_start = None
61
- self.current_prompt_end = None
62
- self.command_history = []
63
- self.history_index = -1
64
- self.hover_command = None
65
- self.suggestion_window = None
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.selected_suggestion = 0
68
-
69
- # Build suggestion lists
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
- # 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):
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(partial_word.lower()):
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(partial_word):
77
+ if builtin.startswith(partialWord):
91
78
  suggestions.append(builtin)
92
79
 
93
80
  # 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('_'):
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(self.master, 'userGlobals'):
100
- for var in self.master.userGlobals:
101
- if var.startswith(partial_word) and not 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
- suggestions = sorted(list(set(suggestions)))
106
- return suggestions[:10] # Limit to 10 suggestions
107
-
108
- def show_suggestions(self):
109
- """Show code suggestions popup."""
110
- # Get current word being typed
111
- cursor_pos = self.index(tk.INSERT)
112
- line_start = self.index(f"{cursor_pos} linestart")
113
- current_line = self.get(line_start, cursor_pos)
114
-
115
- # Find the current word
116
- words = current_line.split()
117
- if not words:
118
- return
119
-
120
- current_word = words[-1]
121
- # Handle cases like "print(" where we want to suggest after the parenthesis
122
- for char in "([{,.":
123
- if char in current_word:
124
- current_word = current_word.split(char)[-1]
125
-
126
- if len(current_word) < 2: # Only show suggestions for 2+ characters
127
- self.hide_suggestions()
128
- return
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.hide_suggestions()
101
+ self.hideSuggestions()
133
102
  return
134
103
 
135
104
  self.suggestions = suggestions
136
- self.selected_suggestion = 0
105
+ self.selectedSuggestion = 0
137
106
 
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)
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.suggestion_listbox.insert(tk.END, suggestion)
114
+ self.suggestionListbox.insert(tk.END, suggestion)
158
115
 
159
- self.suggestion_listbox.selection_set(0)
116
+ self.suggestionListbox.selection_set(0)
160
117
 
161
118
  # Position window near cursor
162
- x, y, _, _ = self.bbox(cursor_pos)
163
- x += self.winfo_rootx()
164
- y += self.winfo_rooty() + 20
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.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)."""
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.selected_suggestion]
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)
180
179
 
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
180
 
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")
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
199
215
 
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 on_tab(self, event):
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.suggestion_window and self.suggestion_window.winfo_viewable():
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.show_suggestions()
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
- 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")
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
- 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
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
- 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()
417
+ if event.keysym in ["Left", "Right"]:
418
+ if self.index("insert") == self.getPromptPosition():
419
+ self.mark_set("insert", "1.4")
276
420
  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
421
 
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
422
+ def onKeyRelease(self, event):
423
+ """Handle key release events."""
294
424
  if event.keysym in ["Return", "Escape", "Left", "Right", "Home", "End"]:
295
- self.hide_suggestions()
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.after_idle(self.show_suggestions)
299
-
300
- # Only highlight the current command line
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 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"
431
+ def cancel(self, event):
432
+ self.history.add(self.getCurrentCommand())
433
+ self.replaceCurrentCommand("")
325
434
 
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")
435
+ def historyReplace(self, command):
436
+ if self.getCurrentCommand() == "" or self.navigatingHistory:
437
+ if self.isExecuting:
351
438
  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
- # 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")
440
+ if self.history.index == len(self.history.history):
441
+ self.history.setTemp(self.getCurrentCommand())
366
442
 
367
- return "break"
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 highlight_current_line(self):
370
- if not self.current_prompt_start:
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
- # Remove all token tags from current line
378
- for token, style in self.style:
379
- self.tag_remove(str(token), line_start, line_end)
495
+ # Clear existing highlighting
496
+ start = self.getPromptPosition()
497
+ end = "end-1c"
380
498
 
381
- # Get the command text (without the prompt)
382
- command = self.get(line_start, line_end)
499
+ for token, _ in self.style:
500
+ self.tag_remove(str(token), start, end)
383
501
 
384
- if not command.strip():
502
+ # Get and highlight the command
503
+ command = self.getCurrentCommand()
504
+ if not command:
385
505
  return
386
-
387
- # Highlight the command
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.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."""
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 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")
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.after(0, _add_prompt)
416
-
417
- def execute_command_and_add_prompt(self, command):
418
- """Execute a command and then add a new prompt."""
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.write_output(str(result), "result")
551
+ self.writeOutput(str(result), "result")
424
552
  self.master.userLocals["_"] = result
425
553
  except SyntaxError:
426
554
  try:
427
- # If eval fails, try exec for statements
555
+ # Try exec for statements
428
556
  exec(command, self.master.userGlobals, self.master.userLocals)
429
557
  except Exception:
430
- self.write_output(traceback.format_exc(), "error")
558
+ self.writeOutput(traceback.format_exc(), "error")
431
559
  except Exception:
432
- self.write_output(traceback.format_exc(), "error")
560
+ self.writeOutput(traceback.format_exc(), "error")
433
561
 
434
- # Add new prompt after execution is complete
435
- self.add_new_prompt()
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
- # If no globals/locals provided, get them from caller frame
578
+
579
+ # Get namespace from caller if not provided
448
580
  if userGlobals is None or userLocals is None:
449
- caller_frame = inspect.currentframe().f_back
581
+ callerFrame = inspect.currentframe().f_back
450
582
  if userGlobals is None:
451
- userGlobals = caller_frame.f_globals
583
+ userGlobals = callerFrame.f_globals
452
584
  if userLocals is None:
453
- userLocals = caller_frame.f_locals
454
-
455
- # Create frame for the text widget
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
- # Single console text widget
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
- # 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
612
+
613
+ # Give console access to namespace
479
614
  self.console.master = self
480
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
+
481
623
  def probe(self, *args, **kwargs):
624
+ """Start the console main loop."""
482
625
  self.mainloop(*args, **kwargs)
483
626
 
627
+
484
628
  # Example usage
485
629
  if __name__ == "__main__":
630
+ # Example variables and functions for testing
486
631
  foo = 42
487
-
632
+
488
633
  def greet(name):
489
634
  print(f"Hello {name}!")
490
635
  return f"Greeted {name}"
491
-
636
+
637
+ # Create the list for testing autocomplete
638
+ exampleList = [1, 2, 3, 4, 5]
639
+
640
+ # Start the console
492
641
  InteractiveConsole().probe()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: liveConsole
3
- Version: 1.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
@@ -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=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,,