liveConsole 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ Copyright (c) <2025> <Tzur Soffer>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: liveConsole
3
+ Version: 1.0.0
4
+ Summary: An IDLE-like debugger to allow for real-time command injection for debugging and testing python code
5
+ Author-email: Tzur Soffer <tzur.soffer@gmail.com>
6
+ License: MIT
7
+ Requires-Python: >=3.7
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: customtkinter
11
+ Requires-Dist: pygments
12
+ Dynamic: license-file
13
+
14
+ # Live Interactive Python Console
15
+
16
+ ## You can finally test your code in real time without using idle!
17
+
18
+ A fully-featured, **live Python console GUI** built with **CustomTkinter** and **Tkinter**, featuring:
19
+
20
+ * Python syntax highlighting via **Pygments**
21
+
22
+ * Autocomplete for **keywords, built-ins, and local/global variables**
23
+
24
+ * Thread-safe execution of Python code
25
+
26
+ * Output capturing for `stdout` and `stderr`
27
+
28
+ * Multi-line input with auto-indentation
29
+
30
+ * Clickable history of previous commands
31
+
32
+ ## Features
33
+
34
+ ### Syntax Highlighting
35
+
36
+ * Real-time syntax highlighting using **Pygments** and the **Monokai** style.
37
+
38
+ * Highlights Python keywords, built-ins, and expressions in the console.
39
+
40
+
41
+ ### Autocomplete
42
+
43
+ * Suggests **keywords**, **built-in functions**, and **variables** in scope.
44
+
45
+ * Popup list appears after typing at least 2 characters.
46
+
47
+ * Only inserts the **missing portion** of a word.
48
+
49
+ * Navigate suggestions with **Up/Down arrows**, confirm with **Tab/Return**.
50
+
51
+
52
+ ### Multi-Line Input
53
+
54
+ * Supports **Shift+Enter** for inserting a new line with proper indentation.
55
+
56
+ * Automatically detects incomplete statements and continues the prompt.
57
+
58
+
59
+ ### Thread-Safe Execution
60
+
61
+ * Executes user code in a separate thread to prevent GUI freezing.
62
+
63
+ * Captures both `stdout` and `stderr` output and prints them in the console.
64
+
65
+ * Supports both **expressions (`eval`)** and **statements (`exec`)**.
66
+
67
+
68
+ ### Clickable History
69
+
70
+ * Hover previous commands to see them highlighted.
71
+
72
+ * Click to copy them back to the prompt for editing or re-execution.
73
+
74
+
75
+ ### Easy Integration
76
+
77
+ * Automatically grabs **caller frame globals and locals** if not provided.
78
+
79
+ * 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
+
88
+ ## Usage
89
+
90
+ ```
91
+ from liveConsole import InteractiveConsole
92
+ InteractiveConsole().mainloop()
93
+ ```
94
+
95
+ * Type Python commands in the `>>>` prompt and see live output.
96
+
97
+ ## Keyboard Shortcuts
98
+
99
+ | Key | Action |
100
+ | --- | --- |
101
+ | `Enter` | Execute command (if complete) |
102
+ | `Shift+Enter` | Insert newline with auto-indent |
103
+ | `Tab` | Complete the current word / show suggestions |
104
+ | `Up/Down` | Navigate suggestion list |
105
+ | `Escape` | Hide suggestions |
106
+ | `Mouse Click` | Select previous command from history |
107
+
108
+
109
+ ## Customization
110
+
111
+ * **Appearance mode**: Dark mode is default (`ctk.set_appearance_mode("dark")`)
112
+
113
+ * **Font**: Consolas, 12pt by default, configurable in `InteractiveConsoleText` constructor
114
+
115
+ * **Syntax Highlighting Style**: Change Pygments style by modifying `get_style_by_name("monokai")`
116
+
117
+
118
+
119
+ ## License
120
+
121
+ MIT License – free to use, modify, and distribute.
@@ -0,0 +1,108 @@
1
+ # Live Interactive Python Console
2
+
3
+ ## You can finally test your code in real time without using idle!
4
+
5
+ A fully-featured, **live Python console GUI** built with **CustomTkinter** and **Tkinter**, featuring:
6
+
7
+ * Python syntax highlighting via **Pygments**
8
+
9
+ * Autocomplete for **keywords, built-ins, and local/global variables**
10
+
11
+ * Thread-safe execution of Python code
12
+
13
+ * Output capturing for `stdout` and `stderr`
14
+
15
+ * Multi-line input with auto-indentation
16
+
17
+ * Clickable history of previous commands
18
+
19
+ ## Features
20
+
21
+ ### Syntax Highlighting
22
+
23
+ * Real-time syntax highlighting using **Pygments** and the **Monokai** style.
24
+
25
+ * Highlights Python keywords, built-ins, and expressions in the console.
26
+
27
+
28
+ ### Autocomplete
29
+
30
+ * Suggests **keywords**, **built-in functions**, and **variables** in scope.
31
+
32
+ * Popup list appears after typing at least 2 characters.
33
+
34
+ * Only inserts the **missing portion** of a word.
35
+
36
+ * Navigate suggestions with **Up/Down arrows**, confirm with **Tab/Return**.
37
+
38
+
39
+ ### Multi-Line Input
40
+
41
+ * Supports **Shift+Enter** for inserting a new line with proper indentation.
42
+
43
+ * Automatically detects incomplete statements and continues the prompt.
44
+
45
+
46
+ ### Thread-Safe Execution
47
+
48
+ * Executes user code in a separate thread to prevent GUI freezing.
49
+
50
+ * Captures both `stdout` and `stderr` output and prints them in the console.
51
+
52
+ * Supports both **expressions (`eval`)** and **statements (`exec`)**.
53
+
54
+
55
+ ### Clickable History
56
+
57
+ * Hover previous commands to see them highlighted.
58
+
59
+ * Click to copy them back to the prompt for editing or re-execution.
60
+
61
+
62
+ ### Easy Integration
63
+
64
+ * Automatically grabs **caller frame globals and locals** if not provided.
65
+
66
+ * Can be used standalone or embedded in larger CustomTkinter applications.
67
+
68
+
69
+ ## Installation
70
+
71
+ `pip install customtkinter pygments`
72
+
73
+ > `Tkinter` is included with Python on most platforms.
74
+
75
+ ## Usage
76
+
77
+ ```
78
+ from liveConsole import InteractiveConsole
79
+ InteractiveConsole().mainloop()
80
+ ```
81
+
82
+ * Type Python commands in the `>>>` prompt and see live output.
83
+
84
+ ## Keyboard Shortcuts
85
+
86
+ | Key | Action |
87
+ | --- | --- |
88
+ | `Enter` | Execute command (if complete) |
89
+ | `Shift+Enter` | Insert newline with auto-indent |
90
+ | `Tab` | Complete the current word / show suggestions |
91
+ | `Up/Down` | Navigate suggestion list |
92
+ | `Escape` | Hide suggestions |
93
+ | `Mouse Click` | Select previous command from history |
94
+
95
+
96
+ ## Customization
97
+
98
+ * **Appearance mode**: Dark mode is default (`ctk.set_appearance_mode("dark")`)
99
+
100
+ * **Font**: Consolas, 12pt by default, configurable in `InteractiveConsoleText` constructor
101
+
102
+ * **Syntax Highlighting Style**: Change Pygments style by modifying `get_style_by_name("monokai")`
103
+
104
+
105
+
106
+ ## License
107
+
108
+ MIT License – free to use, modify, and distribute.
@@ -0,0 +1,13 @@
1
+ [project]
2
+ name = "liveConsole"
3
+ version = "1.0.0"
4
+ description = "An IDLE-like debugger to allow for real-time command injection for debugging and testing python code"
5
+ authors = [{ name="Tzur Soffer", email="tzur.soffer@gmail.com" }]
6
+ license = {text = "MIT"}
7
+ readme = "README.md"
8
+ requires-python = ">=3.7"
9
+ dependencies = ["customtkinter", "pygments"]
10
+
11
+ [build-system]
12
+ requires = ["setuptools>=61.0", "wheel"]
13
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: liveConsole
3
+ Version: 1.0.0
4
+ Summary: An IDLE-like debugger to allow for real-time command injection for debugging and testing python code
5
+ Author-email: Tzur Soffer <tzur.soffer@gmail.com>
6
+ License: MIT
7
+ Requires-Python: >=3.7
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: customtkinter
11
+ Requires-Dist: pygments
12
+ Dynamic: license-file
13
+
14
+ # Live Interactive Python Console
15
+
16
+ ## You can finally test your code in real time without using idle!
17
+
18
+ A fully-featured, **live Python console GUI** built with **CustomTkinter** and **Tkinter**, featuring:
19
+
20
+ * Python syntax highlighting via **Pygments**
21
+
22
+ * Autocomplete for **keywords, built-ins, and local/global variables**
23
+
24
+ * Thread-safe execution of Python code
25
+
26
+ * Output capturing for `stdout` and `stderr`
27
+
28
+ * Multi-line input with auto-indentation
29
+
30
+ * Clickable history of previous commands
31
+
32
+ ## Features
33
+
34
+ ### Syntax Highlighting
35
+
36
+ * Real-time syntax highlighting using **Pygments** and the **Monokai** style.
37
+
38
+ * Highlights Python keywords, built-ins, and expressions in the console.
39
+
40
+
41
+ ### Autocomplete
42
+
43
+ * Suggests **keywords**, **built-in functions**, and **variables** in scope.
44
+
45
+ * Popup list appears after typing at least 2 characters.
46
+
47
+ * Only inserts the **missing portion** of a word.
48
+
49
+ * Navigate suggestions with **Up/Down arrows**, confirm with **Tab/Return**.
50
+
51
+
52
+ ### Multi-Line Input
53
+
54
+ * Supports **Shift+Enter** for inserting a new line with proper indentation.
55
+
56
+ * Automatically detects incomplete statements and continues the prompt.
57
+
58
+
59
+ ### Thread-Safe Execution
60
+
61
+ * Executes user code in a separate thread to prevent GUI freezing.
62
+
63
+ * Captures both `stdout` and `stderr` output and prints them in the console.
64
+
65
+ * Supports both **expressions (`eval`)** and **statements (`exec`)**.
66
+
67
+
68
+ ### Clickable History
69
+
70
+ * Hover previous commands to see them highlighted.
71
+
72
+ * Click to copy them back to the prompt for editing or re-execution.
73
+
74
+
75
+ ### Easy Integration
76
+
77
+ * Automatically grabs **caller frame globals and locals** if not provided.
78
+
79
+ * 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
+
88
+ ## Usage
89
+
90
+ ```
91
+ from liveConsole import InteractiveConsole
92
+ InteractiveConsole().mainloop()
93
+ ```
94
+
95
+ * Type Python commands in the `>>>` prompt and see live output.
96
+
97
+ ## Keyboard Shortcuts
98
+
99
+ | Key | Action |
100
+ | --- | --- |
101
+ | `Enter` | Execute command (if complete) |
102
+ | `Shift+Enter` | Insert newline with auto-indent |
103
+ | `Tab` | Complete the current word / show suggestions |
104
+ | `Up/Down` | Navigate suggestion list |
105
+ | `Escape` | Hide suggestions |
106
+ | `Mouse Click` | Select previous command from history |
107
+
108
+
109
+ ## Customization
110
+
111
+ * **Appearance mode**: Dark mode is default (`ctk.set_appearance_mode("dark")`)
112
+
113
+ * **Font**: Consolas, 12pt by default, configurable in `InteractiveConsoleText` constructor
114
+
115
+ * **Syntax Highlighting Style**: Change Pygments style by modifying `get_style_by_name("monokai")`
116
+
117
+
118
+
119
+ ## License
120
+
121
+ MIT License – free to use, modify, and distribute.
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/__init__.py
5
+ src/liveConsole.py
6
+ src/liveConsole.egg-info/PKG-INFO
7
+ src/liveConsole.egg-info/SOURCES.txt
8
+ src/liveConsole.egg-info/dependency_links.txt
9
+ src/liveConsole.egg-info/requires.txt
10
+ src/liveConsole.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ customtkinter
2
+ pygments
@@ -0,0 +1,2 @@
1
+ __init__
2
+ liveConsole
@@ -0,0 +1,485 @@
1
+ import customtkinter as ctk
2
+ import tkinter as tk
3
+ import traceback
4
+ import inspect
5
+ import threading
6
+ import sys
7
+ import io
8
+ import pygments
9
+ from pygments.lexers import PythonLexer
10
+ from pygments.styles import get_style_by_name
11
+ import keyword
12
+ import builtins
13
+
14
+
15
+ class StdoutRedirect(io.StringIO):
16
+ def __init__(self, write_callback):
17
+ super().__init__()
18
+ self.write_callback = write_callback
19
+
20
+ def write(self, s):
21
+ if s.strip():
22
+ self.write_callback(s, "output")
23
+
24
+ def flush(self):
25
+ pass
26
+
27
+
28
+ class InteractiveConsoleText(tk.Text):
29
+ """A tk.Text widget with Python syntax highlighting for interactive console."""
30
+ def __init__(self, master, **kwargs):
31
+ super().__init__(master, **kwargs)
32
+
33
+ self.lexer = PythonLexer()
34
+ self.style = get_style_by_name("monokai")
35
+
36
+ # Configure tags for different output types
37
+ self.tag_configure("prompt", foreground="#00ff00", font=("Consolas", 12, "bold"))
38
+ self.tag_configure("output", foreground="#ffffff", font=("Consolas", 12))
39
+ self.tag_configure("error", foreground="#ff6666", font=("Consolas", 12))
40
+ self.tag_configure("result", foreground="#66ccff", font=("Consolas", 12))
41
+ self.tag_configure("suggestion", background="#444444", foreground="#cccccc", font=("Consolas", 12))
42
+
43
+ # Apply tag configs for syntax highlighting
44
+ for token, style in self.style:
45
+ if style["color"]:
46
+ fg = f"#{style['color']}"
47
+ font = ("Consolas", 12, "bold" if style["bold"] else "normal")
48
+ self.tag_configure(str(token), foreground=fg, font=font)
49
+
50
+ # Bind events
51
+ self.bind("<KeyRelease>", self.on_key_release)
52
+ self.bind("<Return>", self.on_enter)
53
+ self.bind("<Shift-Return>", self.on_shift_enter)
54
+ self.bind("<Button-1>", self.on_click)
55
+ self.bind("<KeyPress>", self.on_key_press)
56
+ self.bind("<Motion>", self.on_mouse_motion)
57
+ self.bind("<Tab>", self.on_tab)
58
+
59
+ # Track current command
60
+ self.current_prompt_start = None
61
+ self.current_prompt_end = None
62
+ self.command_history = []
63
+ self.history_index = -1
64
+ self.hover_command = None
65
+ self.suggestion_window = None
66
+ self.suggestions = []
67
+ self.selected_suggestion = 0
68
+
69
+ # Build suggestion lists
70
+ self.keywords = keyword.kwlist
71
+ self.builtins = [name for name in dir(builtins) if not name.startswith('_')]
72
+
73
+ # Initialize with prompt
74
+ self.insert("end", ">>> ")
75
+ self.tag_add("prompt", "end-4c", "end")
76
+ self.current_prompt_start = self.index("end-4c")
77
+ self.mark_set("insert", "end")
78
+
79
+ def get_suggestions(self, partial_word):
80
+ """Get code suggestions for partial word."""
81
+ suggestions = []
82
+
83
+ # Add matching keywords
84
+ for kw in self.keywords:
85
+ if kw.startswith(partial_word.lower()):
86
+ suggestions.append(kw)
87
+
88
+ # Add matching builtins
89
+ for builtin in self.builtins:
90
+ if builtin.startswith(partial_word):
91
+ suggestions.append(builtin)
92
+
93
+ # Add matching variables from namespace
94
+ if hasattr(self.master, 'userLocals'):
95
+ for var in self.master.userLocals:
96
+ if var.startswith(partial_word) and not var.startswith('_'):
97
+ suggestions.append(var)
98
+
99
+ if hasattr(self.master, 'userGlobals'):
100
+ for var in self.master.userGlobals:
101
+ if var.startswith(partial_word) and not var.startswith('_'):
102
+ suggestions.append(var)
103
+
104
+ # Remove duplicates and sort
105
+ suggestions = sorted(list(set(suggestions)))
106
+ return suggestions[:10] # Limit to 10 suggestions
107
+
108
+ def show_suggestions(self):
109
+ """Show code suggestions popup."""
110
+ # Get current word being typed
111
+ cursor_pos = self.index(tk.INSERT)
112
+ line_start = self.index(f"{cursor_pos} linestart")
113
+ current_line = self.get(line_start, cursor_pos)
114
+
115
+ # Find the current word
116
+ words = current_line.split()
117
+ if not words:
118
+ return
119
+
120
+ current_word = words[-1]
121
+ # Handle cases like "print(" where we want to suggest after the parenthesis
122
+ for char in "([{,.":
123
+ if char in current_word:
124
+ current_word = current_word.split(char)[-1]
125
+
126
+ if len(current_word) < 2: # Only show suggestions for 2+ characters
127
+ self.hide_suggestions()
128
+ return
129
+
130
+ suggestions = self.get_suggestions(current_word)
131
+ if not suggestions:
132
+ self.hide_suggestions()
133
+ return
134
+
135
+ self.suggestions = suggestions
136
+ self.selected_suggestion = 0
137
+
138
+ # Create or update suggestion window
139
+ if not self.suggestion_window:
140
+ self.suggestion_window = tk.Toplevel(self)
141
+ self.suggestion_window.wm_overrideredirect(True)
142
+ self.suggestion_window.configure(bg="#2d2d2d")
143
+
144
+ self.suggestion_listbox = tk.Listbox(
145
+ self.suggestion_window,
146
+ bg="#2d2d2d",
147
+ fg="white",
148
+ selectbackground="#0066cc",
149
+ font=("Consolas", 10),
150
+ height=min(len(suggestions), 8)
151
+ )
152
+ self.suggestion_listbox.pack()
153
+
154
+ # Clear and populate listbox
155
+ self.suggestion_listbox.delete(0, tk.END)
156
+ for suggestion in suggestions:
157
+ self.suggestion_listbox.insert(tk.END, suggestion)
158
+
159
+ self.suggestion_listbox.selection_set(0)
160
+
161
+ # Position window near cursor
162
+ x, y, _, _ = self.bbox(cursor_pos)
163
+ x += self.winfo_rootx()
164
+ y += self.winfo_rooty() + 20
165
+
166
+ self.suggestion_window.geometry(f"+{x}+{y}")
167
+ self.suggestion_window.deiconify()
168
+
169
+ def hide_suggestions(self):
170
+ """Hide suggestions popup."""
171
+ if self.suggestion_window:
172
+ self.suggestion_window.withdraw()
173
+
174
+ def apply_suggestion(self, suggestion=None):
175
+ """Apply selected suggestion at the cursor position (only missing letters)."""
176
+ if not suggestion and self.suggestions:
177
+ suggestion = self.suggestions[self.selected_suggestion]
178
+ if not suggestion:
179
+ return
180
+
181
+ # Current cursor position
182
+ cursor_pos = self.index(tk.INSERT)
183
+
184
+ # Get the word fragment before the cursor
185
+ line_start = self.index(f"{cursor_pos} linestart")
186
+ current_line = self.get(line_start, cursor_pos)
187
+
188
+ fragment = ""
189
+ for i in range(len(current_line)):
190
+ if current_line[-(i+1)] in " \t([{,.)":
191
+ break
192
+ fragment = current_line[-(i+1):]
193
+
194
+ # Only insert the missing part
195
+ if suggestion.startswith(fragment):
196
+ missing_part = suggestion[len(fragment):]
197
+ self.insert(cursor_pos, missing_part)
198
+ self.mark_set("insert", f"{cursor_pos} + {len(missing_part)}c")
199
+
200
+ self.hide_suggestions()
201
+
202
+
203
+ def on_tab(self, event):
204
+ """Handle Tab key for autocompletion."""
205
+ if self.suggestion_window and self.suggestion_window.winfo_viewable():
206
+ self.apply_suggestion()
207
+ return "break"
208
+ else:
209
+ self.show_suggestions()
210
+ return "break"
211
+
212
+ def is_incomplete_statement(self, code):
213
+ """Check if the code is an incomplete statement that needs more lines."""
214
+ code = code.split("\n")
215
+ if code[-1].strip() == "":
216
+ return(False)
217
+ if code[0].strip().endswith(":"):
218
+ return(True)
219
+ return(False)
220
+
221
+ def get_indent_level(self, line):
222
+ """Get the indentation level of a line."""
223
+ return len(line) - len(line.lstrip(' '))
224
+
225
+ def should_auto_indent(self, line):
226
+ """Check if we should add indentation after this line."""
227
+ stripped = line.strip()
228
+ return (stripped and stripped[-1] == ':')
229
+
230
+ def on_click(self, event):
231
+ self.hide_suggestions()
232
+ click_pos = self.index(f"@{event.x},{event.y}")
233
+
234
+ if self.current_prompt_start:
235
+ click_pos = self.index(tk.CURRENT)
236
+ if self.compare(click_pos, "<", self.current_prompt_start):
237
+ self.mark_set("insert", "end")
238
+ return "break"
239
+
240
+ def on_mouse_motion(self, event):
241
+ """Handle mouse motion for hover copying previous commands."""
242
+
243
+ mouse_pos = self.index(f"@{event.x},{event.y}")
244
+ line_start = self.index(f"{mouse_pos} linestart")
245
+ line_end = self.index(f"{mouse_pos} lineend")
246
+ line_text = self.get(line_start, line_end)
247
+
248
+ # Check if this line starts with ">>> " (a previous command)
249
+ if line_text.startswith(">>> ") and line_start != self.current_prompt_start:
250
+ command = line_text[4:] # Remove ">>> "
251
+ if command.strip():
252
+ # Change cursor to indicate clickable
253
+ self.config(cursor="hand2")
254
+ self.hover_command = command.strip()
255
+ else:
256
+ self.config(cursor="xterm")
257
+ self.hover_command = None
258
+ else:
259
+ self.config(cursor="xterm")
260
+ self.hover_command = None
261
+
262
+ def on_key_press(self, event):
263
+ if self.suggestion_window and self.suggestion_window.winfo_viewable():
264
+ if event.keysym == "Down":
265
+ self.selected_suggestion = min(self.selected_suggestion + 1, len(self.suggestions) - 1)
266
+ self.suggestion_listbox.selection_clear(0, tk.END)
267
+ self.suggestion_listbox.selection_set(self.selected_suggestion)
268
+ return "break"
269
+ elif event.keysym == "Up":
270
+ self.selected_suggestion = max(self.selected_suggestion - 1, 0)
271
+ self.suggestion_listbox.selection_clear(0, tk.END)
272
+ self.suggestion_listbox.selection_set(self.selected_suggestion)
273
+ return "break"
274
+ elif event.keysym == "Escape":
275
+ self.hide_suggestions()
276
+ return "break"
277
+ elif event.keysym in ["Return", "Tab"]:
278
+ self.apply_suggestion()
279
+ return "break"
280
+
281
+ # Ensure cursor is always at least 4 chars after current_prompt_start
282
+ prompt_end_index = f"{self.current_prompt_start} + 3c"
283
+
284
+ if event.keysym not in ["Up", "Down", "Left", "Right", "Shift_L", "Shift_R", "Control_L", "Control_R"]:
285
+ if self.compare("insert", "<", prompt_end_index):
286
+ self.mark_set("insert", prompt_end_index)
287
+
288
+ # Block Backspace if at or before prompt
289
+ if event.keysym == "BackSpace" and self.compare("insert", "<=", prompt_end_index):
290
+ return "break"
291
+
292
+ def on_key_release(self, event):
293
+ # Hide suggestions on certain keys
294
+ if event.keysym in ["Return", "Escape", "Left", "Right", "Home", "End"]:
295
+ self.hide_suggestions()
296
+ # Show suggestions on typing
297
+ elif event.keysym not in ["Up", "Down", "Shift_L", "Shift_R", "Control_L", "Control_R"]:
298
+ self.after_idle(self.show_suggestions)
299
+
300
+ # Only highlight the current command line
301
+ if self.current_prompt_start:
302
+ self.highlight_current_line()
303
+
304
+ def on_shift_enter(self, event):
305
+ """Handle Shift+Enter for new line with auto-indent."""
306
+ self.hide_suggestions()
307
+
308
+ if self.current_prompt_start:
309
+ # Get current line to determine indent
310
+ current_line_start = self.index("insert linestart")
311
+ current_line_end = self.index("insert lineend")
312
+ current_line = self.get(current_line_start, current_line_end)
313
+
314
+ # Calculate indent level
315
+ base_indent = self.get_indent_level(current_line)
316
+
317
+ # If the current line should increase indent, add 4 spaces
318
+ if self.should_auto_indent(current_line):
319
+ base_indent += 4
320
+
321
+ # Insert newline with proper indentation
322
+ self.insert("insert", "\n" + " " * base_indent)
323
+ return "break"
324
+
325
+ def on_enter(self, event):
326
+ """Handle Enter key - execute if complete, newline if incomplete."""
327
+ self.hide_suggestions()
328
+
329
+ if self.current_prompt_start:
330
+ # Get text from after the prompt to end
331
+ prompt_end = f"{self.current_prompt_start} + 3c" # Skip ">>> "
332
+ command = self.get(prompt_end, "end-1c")
333
+
334
+ if not command.strip():
335
+ return "break"
336
+
337
+ # Check if it's an incomplete statement
338
+ if self.is_incomplete_statement(command):
339
+ # Add newline with auto-indent
340
+ current_line_start = self.index("insert linestart")
341
+ current_line_end = self.index("insert lineend")
342
+ current_line = self.get(current_line_start, current_line_end)
343
+ base_indent = self.get_indent_level(current_line)
344
+
345
+ if self.should_auto_indent(current_line):
346
+ base_indent += 4
347
+
348
+ self.insert("insert", "\n" + " " * base_indent)
349
+ return "break"
350
+
351
+ # Execute the complete command
352
+ if command.strip():
353
+ self.command_history.append(command)
354
+ self.history_index = len(self.command_history)
355
+
356
+ # Move to end and add newline for the executed command
357
+ self.mark_set("insert", "end")
358
+ self.insert("end", "\n")
359
+
360
+ # Execute the command in a thread to prevent freezing
361
+ threading.Thread(target=self.execute_command_and_add_prompt, args=(command,), daemon=True).start()
362
+
363
+ return "break"
364
+
365
+ def highlight_current_line(self):
366
+ if not self.current_prompt_start:
367
+ return
368
+
369
+ # Clear existing syntax highlighting tags from current line
370
+ line_start = self.current_prompt_start
371
+ line_end = "end-1c"
372
+
373
+ # Remove all token tags from current line
374
+ for token, style in self.style:
375
+ self.tag_remove(str(token), line_start, line_end)
376
+
377
+ # Get the command text (without the prompt)
378
+ command = self.get(line_start, line_end)
379
+
380
+ if not command.strip():
381
+ return
382
+
383
+ # Highlight the command
384
+ self.mark_set("range_start", line_start)
385
+
386
+ for token, content in pygments.lex(command, self.lexer):
387
+ if content.strip(): # Only highlight non-whitespace
388
+ self.mark_set("range_end", f"range_start + {len(content)}c")
389
+ self.tag_add(str(token), "range_start", "range_end")
390
+ self.mark_set("range_start", f"range_start + {len(content)}c")
391
+
392
+ def write_output(self, text, tag="output"):
393
+ """Write output to the console - thread safe."""
394
+ def _write():
395
+ # Insert output at the end
396
+ self.insert("end", text + "\n", tag)
397
+ self.see("end")
398
+
399
+ # Use after() to ensure GUI updates happen on main thread
400
+ self.after(0, _write)
401
+
402
+ def add_new_prompt(self):
403
+ """Add a new prompt - thread safe."""
404
+ def _add_prompt():
405
+ self.insert("end", ">>> ")
406
+ self.tag_add("prompt", "end-4c", "end")
407
+ self.current_prompt_start = self.index("end-4c")
408
+ self.mark_set("insert", "end")
409
+ self.see("end")
410
+
411
+ self.after(0, _add_prompt)
412
+
413
+ def execute_command_and_add_prompt(self, command):
414
+ """Execute a command and then add a new prompt."""
415
+ try:
416
+ # Try eval first for expressions
417
+ result = eval(command, self.master.userGlobals, self.master.userLocals)
418
+ if result is not None:
419
+ self.write_output(str(result), "result")
420
+ self.master.userLocals["_"] = result
421
+ except SyntaxError:
422
+ try:
423
+ # If eval fails, try exec for statements
424
+ exec(command, self.master.userGlobals, self.master.userLocals)
425
+ except Exception:
426
+ self.write_output(traceback.format_exc(), "error")
427
+ except Exception:
428
+ self.write_output(traceback.format_exc(), "error")
429
+
430
+ # Add new prompt after execution is complete
431
+ self.add_new_prompt()
432
+
433
+
434
+ class InteractiveConsole(ctk.CTk):
435
+ def __init__(self, userGlobals=None, userLocals=None):
436
+ super().__init__()
437
+ self.title("Live Interactive Console")
438
+ self.geometry("900x600")
439
+
440
+ ctk.set_appearance_mode("dark")
441
+ ctk.set_default_color_theme("blue")
442
+
443
+ # If no globals/locals provided, get them from caller frame
444
+ if userGlobals is None or userLocals is None:
445
+ caller_frame = inspect.currentframe().f_back
446
+ if userGlobals is None:
447
+ userGlobals = caller_frame.f_globals
448
+ if userLocals is None:
449
+ userLocals = caller_frame.f_locals
450
+
451
+ # Create frame for the text widget
452
+ frame = ctk.CTkFrame(self)
453
+ frame.pack(padx=10, pady=10, fill="both", expand=True)
454
+
455
+ # Single console text widget
456
+ self.console = InteractiveConsoleText(
457
+ frame,
458
+ wrap="word",
459
+ bg="#1e1e1e",
460
+ fg="white",
461
+ insertbackground="white",
462
+ font=("Consolas", 12)
463
+ )
464
+ self.console.pack(fill="both", expand=True, padx=5, pady=5)
465
+
466
+ # Namespace
467
+ self.userGlobals = userGlobals
468
+ self.userLocals = userLocals
469
+
470
+ # Redirect stdout/stderr to write to console
471
+ sys.stdout = StdoutRedirect(self.console.write_output)
472
+ sys.stderr = StdoutRedirect(lambda text, tag: self.console.write_output(text, "error"))
473
+
474
+ # Give console access to namespaces
475
+ self.console.master = self
476
+
477
+ # Example usage
478
+ if __name__ == "__main__":
479
+ foo = 42
480
+
481
+ def greet(name):
482
+ print(f"Hello {name}!")
483
+ return f"Greeted {name}"
484
+
485
+ InteractiveConsole().mainloop()