liveConsole 1.4.0__tar.gz → 1.5.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: liveConsole
3
- Version: 1.4.0
3
+ Version: 1.5.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
@@ -11,9 +11,10 @@ Requires-Dist: customtkinter
11
11
  Requires-Dist: pygments
12
12
  Dynamic: license-file
13
13
 
14
- # Live Interactive Python Console
14
+ # PYSOLE
15
15
 
16
16
  ## You can finally test your code in real time without using idle!
17
+ ### If you found [this repository](https://github.com/TzurSoffer/LiveDebugger) useful, please give it a ⭐!.
17
18
 
18
19
  A fully-featured, **live Python console GUI** built with **CustomTkinter** and **Tkinter**, featuring:
19
20
 
@@ -27,7 +28,10 @@ A fully-featured, **live Python console GUI** built with **CustomTkinter** and *
27
28
 
28
29
  * Multi-line input with auto-indentation
29
30
 
30
- * Clickable history of previous commands
31
+ * History of previous commands
32
+
33
+ * **Integrated Help Panel** for quick access to Python object documentation
34
+
31
35
 
32
36
  ## Installation
33
37
 
@@ -77,6 +81,17 @@ A fully-featured, **live Python console GUI** built with **CustomTkinter** and *
77
81
  * Click to copy them back to the prompt for editing or re-execution.
78
82
 
79
83
 
84
+ ### Help Panel
85
+
86
+ * A resizable right-hand panel that displays Python documentation (help()) for any object.
87
+
88
+ * Opens when clicking ctrl+click on a function/method and can be closed with the "X" button.
89
+
90
+ * Scrollable and syntax-styled.
91
+
92
+ * Perfect for quick reference without leaving the console.
93
+
94
+
80
95
  ### Easy Integration
81
96
 
82
97
  * Automatically grabs **caller frame globals and locals** if not provided.
@@ -86,8 +101,8 @@ A fully-featured, **live Python console GUI** built with **CustomTkinter** and *
86
101
  ## Usage
87
102
 
88
103
  ```
89
- from liveConsole import InteractiveConsole
90
- InteractiveConsole().probe()
104
+ import pysole
105
+ pysole.probe()
91
106
  ```
92
107
 
93
108
  * Type Python commands in the `>>>` prompt and see live output.
@@ -1,6 +1,7 @@
1
- # Live Interactive Python Console
1
+ # PYSOLE
2
2
 
3
3
  ## You can finally test your code in real time without using idle!
4
+ ### If you found [this repository](https://github.com/TzurSoffer/LiveDebugger) useful, please give it a ⭐!.
4
5
 
5
6
  A fully-featured, **live Python console GUI** built with **CustomTkinter** and **Tkinter**, featuring:
6
7
 
@@ -14,7 +15,10 @@ A fully-featured, **live Python console GUI** built with **CustomTkinter** and *
14
15
 
15
16
  * Multi-line input with auto-indentation
16
17
 
17
- * Clickable history of previous commands
18
+ * History of previous commands
19
+
20
+ * **Integrated Help Panel** for quick access to Python object documentation
21
+
18
22
 
19
23
  ## Installation
20
24
 
@@ -64,6 +68,17 @@ A fully-featured, **live Python console GUI** built with **CustomTkinter** and *
64
68
  * Click to copy them back to the prompt for editing or re-execution.
65
69
 
66
70
 
71
+ ### Help Panel
72
+
73
+ * A resizable right-hand panel that displays Python documentation (help()) for any object.
74
+
75
+ * Opens when clicking ctrl+click on a function/method and can be closed with the "X" button.
76
+
77
+ * Scrollable and syntax-styled.
78
+
79
+ * Perfect for quick reference without leaving the console.
80
+
81
+
67
82
  ### Easy Integration
68
83
 
69
84
  * Automatically grabs **caller frame globals and locals** if not provided.
@@ -73,8 +88,8 @@ A fully-featured, **live Python console GUI** built with **CustomTkinter** and *
73
88
  ## Usage
74
89
 
75
90
  ```
76
- from liveConsole import InteractiveConsole
77
- InteractiveConsole().probe()
91
+ import pysole
92
+ pysole.probe()
78
93
  ```
79
94
 
80
95
  * Type Python commands in the `>>>` prompt and see live output.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "liveConsole"
3
- version = "1.4.0"
3
+ version = "1.5.0"
4
4
  description = "An IDLE-like debugger to allow for real-time command injection for debugging and testing python code"
5
5
  authors = [{ name="Tzur Soffer", email="tzur.soffer@gmail.com" }]
6
6
  license = {text = "MIT"}
@@ -8,6 +8,9 @@ readme = "README.md"
8
8
  requires-python = ">=3.7"
9
9
  dependencies = ["customtkinter", "pygments"]
10
10
 
11
+ [project.scripts]
12
+ liveconsole = "liveConsole:main"
13
+
11
14
  [build-system]
12
15
  requires = ["setuptools>=61.0", "wheel"]
13
16
  build-backend = "setuptools.build_meta"
@@ -0,0 +1 @@
1
+ from pysole import probe, InteractiveConsole
@@ -0,0 +1,34 @@
1
+ class CommandHistory:
2
+ """Manages command history and navigation."""
3
+
4
+ def __init__(self):
5
+ self.history = []
6
+ self.index = -1
7
+ self.tempCommand = ""
8
+
9
+ def add(self, command):
10
+ """Add a command to history."""
11
+ if command.strip():
12
+ self.history.append(command)
13
+ self.index = len(self.history)
14
+
15
+ def navigateUp(self):
16
+ """Get previous command from history."""
17
+ if self.index > 0:
18
+ self.index -= 1
19
+ return(self.history[self.index])
20
+ return(None)
21
+
22
+ def navigateDown(self):
23
+ """Get next command from history."""
24
+ if self.index < len(self.history) - 1:
25
+ self.index += 1
26
+ return(self.history[self.index])
27
+ elif self.index == len(self.history) - 1:
28
+ self.index = len(self.history)
29
+ return(self.tempCommand)
30
+ return(None)
31
+
32
+ def setTemp(self, command):
33
+ """Store temporary command while navigating history."""
34
+ self.tempCommand = command
@@ -0,0 +1,64 @@
1
+ import sys
2
+ import io
3
+ from pygments.styles import get_style_by_name
4
+ import customtkinter as ctk
5
+ from styledTextbox import StyledTextWindow
6
+
7
+ class HelpTab(ctk.CTkFrame):
8
+ """A right-hand help tab with closable and updateable text content."""
9
+
10
+ def __init__(self, parent, width=500, title="Help", **kwargs):
11
+ super().__init__(parent, width=width, **kwargs)
12
+ self.parent = parent
13
+ self.visible = False
14
+
15
+ # Ensure initial width is respected
16
+ self.pack_propagate(False)
17
+
18
+ # Header frame with title and close button
19
+ headerFrame = ctk.CTkFrame(self, height=30)
20
+ headerFrame.pack(fill="x")
21
+ self.style = get_style_by_name("monokai")
22
+
23
+ self.titleLabel = ctk.CTkLabel(headerFrame, text=title, font=("Consolas", 12, "bold"))
24
+ self.titleLabel.pack(side="left", padx=5)
25
+
26
+ self.closeButton = ctk.CTkButton(headerFrame, text="X", height=20, command=self.close)
27
+ self.closeButton.pack(side="right", padx=5)
28
+
29
+ # Scrollable text area
30
+ self.textBox = StyledTextWindow(self, wrap="word", font=("Consolas", 11), bg="#2e2e2e")
31
+ self.textBox.pack(fill="both", expand=True, padx=5, pady=5)
32
+ self.textBox.configure(state="disabled") # read-only
33
+
34
+ def close(self):
35
+ """Hide the help tab."""
36
+ if self.visible:
37
+ self.pack_forget()
38
+ self.visible = False
39
+
40
+ def open(self):
41
+ """Show the help tab."""
42
+ if not self.visible:
43
+ self.pack(side="left", fill="y")
44
+ # self.configure(width=self.minWidth)
45
+ self.visible = True
46
+
47
+ def _getHelp(self, obj):
48
+ """Return the output of help(obj) as a string."""
49
+ old_stdout = sys.stdout # save current stdout
50
+ sys.stdout = buffer = io.StringIO() # redirect stdout to a string buffer
51
+ try:
52
+ help(obj)
53
+ return(buffer.getvalue())
54
+ finally:
55
+ sys.stdout = old_stdout # restore original stdout
56
+
57
+ def updateHelp(self, obj):
58
+ """Update the help tab content."""
59
+
60
+ self.textBox.configure(state="normal")
61
+ self.textBox.delete("1.0", "end")
62
+ self.textBox.insert("1.0", self._getHelp(obj))
63
+ self.textBox.updateStyling()
64
+ self.textBox.configure(state="disabled")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: liveConsole
3
- Version: 1.4.0
3
+ Version: 1.5.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
@@ -11,9 +11,10 @@ Requires-Dist: customtkinter
11
11
  Requires-Dist: pygments
12
12
  Dynamic: license-file
13
13
 
14
- # Live Interactive Python Console
14
+ # PYSOLE
15
15
 
16
16
  ## You can finally test your code in real time without using idle!
17
+ ### If you found [this repository](https://github.com/TzurSoffer/LiveDebugger) useful, please give it a ⭐!.
17
18
 
18
19
  A fully-featured, **live Python console GUI** built with **CustomTkinter** and **Tkinter**, featuring:
19
20
 
@@ -27,7 +28,10 @@ A fully-featured, **live Python console GUI** built with **CustomTkinter** and *
27
28
 
28
29
  * Multi-line input with auto-indentation
29
30
 
30
- * Clickable history of previous commands
31
+ * History of previous commands
32
+
33
+ * **Integrated Help Panel** for quick access to Python object documentation
34
+
31
35
 
32
36
  ## Installation
33
37
 
@@ -77,6 +81,17 @@ A fully-featured, **live Python console GUI** built with **CustomTkinter** and *
77
81
  * Click to copy them back to the prompt for editing or re-execution.
78
82
 
79
83
 
84
+ ### Help Panel
85
+
86
+ * A resizable right-hand panel that displays Python documentation (help()) for any object.
87
+
88
+ * Opens when clicking ctrl+click on a function/method and can be closed with the "X" button.
89
+
90
+ * Scrollable and syntax-styled.
91
+
92
+ * Perfect for quick reference without leaving the console.
93
+
94
+
80
95
  ### Easy Integration
81
96
 
82
97
  * Automatically grabs **caller frame globals and locals** if not provided.
@@ -86,8 +101,8 @@ A fully-featured, **live Python console GUI** built with **CustomTkinter** and *
86
101
  ## Usage
87
102
 
88
103
  ```
89
- from liveConsole import InteractiveConsole
90
- InteractiveConsole().probe()
104
+ import pysole
105
+ pysole.probe()
91
106
  ```
92
107
 
93
108
  * Type Python commands in the `>>>` prompt and see live output.
@@ -2,9 +2,16 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  src/__init__.py
5
+ src/commandHistory.py
6
+ src/helpTab.py
5
7
  src/liveConsole.py
8
+ src/mainConsole.py
9
+ src/pysole.py
10
+ src/styledTextbox.py
11
+ src/suggestionManager.py
6
12
  src/liveConsole.egg-info/PKG-INFO
7
13
  src/liveConsole.egg-info/SOURCES.txt
8
14
  src/liveConsole.egg-info/dependency_links.txt
15
+ src/liveConsole.egg-info/entry_points.txt
9
16
  src/liveConsole.egg-info/requires.txt
10
17
  src/liveConsole.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ liveconsole = liveConsole:main
@@ -0,0 +1,8 @@
1
+ __init__
2
+ commandHistory
3
+ helpTab
4
+ liveConsole
5
+ mainConsole
6
+ pysole
7
+ styledTextbox
8
+ suggestionManager
@@ -0,0 +1,8 @@
1
+ import warnings
2
+ warnings.warn(
3
+ "Module 'liveConsole' is deprecated. Use 'pysole' instead.",
4
+ DeprecationWarning,
5
+ stacklevel=2
6
+ )
7
+
8
+ from pysole import *
@@ -0,0 +1,354 @@
1
+ import threading
2
+ import traceback
3
+ from suggestionManager import CodeSuggestionManager
4
+ from commandHistory import CommandHistory
5
+ from styledTextbox import StyledTextWindow
6
+
7
+ import tkinter as tk
8
+
9
+ class InteractiveConsoleText(StyledTextWindow):
10
+ """TBD"""
11
+
12
+ PROMPT = ">>> "
13
+ PROMPT_LENGTH = 4
14
+
15
+ def __init__(self, master, helpTab, userLocals=None, userGlobals=None, **kwargs):
16
+ super().__init__(master, **kwargs)
17
+
18
+ # Initialize components
19
+ self.suggestionManager = CodeSuggestionManager(self, userLocals=userLocals, userGlobals=userGlobals)
20
+ self.helpTab = helpTab
21
+
22
+ self.navigatingHistory = False
23
+ self.history = CommandHistory()
24
+
25
+ self.inputVar = tk.StringVar()
26
+ self.waitingForInput = False
27
+
28
+ # Track current command
29
+ self.currentCommandLine = 1
30
+ self.isExecuting = False
31
+
32
+ # Setup bindings
33
+ self._setupBindings()
34
+
35
+ # Initialize with first prompt
36
+ self.addPrompt()
37
+
38
+ def _setupBindings(self):
39
+ """Setup all key and mouse bindings."""
40
+ self.bind("<Return>", self.onEnter)
41
+ self.bind("<Shift-Return>", self.onShiftEnter)
42
+ self.bind("<Control-c>", self.cancel)
43
+ self.bind("<Tab>", self.onTab)
44
+ self.bind("<BackSpace>", self.onBackspace)
45
+ self.bind("<KeyRelease>", self.onKeyRelease)
46
+ self.bind("<KeyPress>", self.onKeyPress)
47
+ self.bind("<Button-1>", self.onClick)
48
+ self.bind("<Up>", self.onUp)
49
+ self.bind("<Down>", self.onDown)
50
+
51
+ def getCurrentLineNumber(self):
52
+ """Get the line number where current command starts."""
53
+ return(int(self.index("end-1c").split(".")[0]))
54
+
55
+ def getCommandStartPosition(self):
56
+ """Get the starting position of the current command."""
57
+ return(f"{self.currentCommandLine}.0")
58
+
59
+ def replaceCurrentCommand(self, newCommand):
60
+ """Replace the current command with new text."""
61
+ if self.isExecuting:
62
+ return
63
+
64
+ start = self.getPromptPosition()
65
+ end = "end-1c"
66
+
67
+ self.delete(start, end)
68
+ self.insert(start, newCommand)
69
+ self.see("end")
70
+
71
+ def isCursorInEditableArea(self):
72
+ """Check if cursor is in the editable command area."""
73
+ if self.isExecuting:
74
+ return(False)
75
+
76
+ cursorLine = int(self.index("insert").split(".")[0])
77
+ cursorCol = int(self.index("insert").split(".")[1])
78
+
79
+ return((cursorLine >= self.currentCommandLine and
80
+ (cursorLine > self.currentCommandLine or cursorCol >= self.PROMPT_LENGTH)))
81
+
82
+ def onEnter(self, event):
83
+ """Handle Enter key - execute command."""
84
+ self.suggestionManager.hideSuggestions()
85
+
86
+ if self.waitingForInput:
87
+ line = self.get("insert linestart", "insert lineend").strip()
88
+ self.insert("end", "\n") # move to next line like normal console
89
+ self.inputVar.set(line)
90
+ self.waitingForInput = False
91
+ return("break")
92
+
93
+ if self.isExecuting:
94
+ return("break")
95
+
96
+ command = self.getCurrentCommand()
97
+
98
+ if not command.strip():
99
+ return("break")
100
+
101
+ # Check if statement is incomplete
102
+ if self.isIncompleteStatement(command):
103
+ return(self.onShiftEnter(event))
104
+
105
+ # Execute the command
106
+ self.history.add(command)
107
+ self.mark_set("insert", "end")
108
+ self.insert("end", "\n")
109
+ self.see("end")
110
+
111
+ # Execute in thread
112
+ self.isExecuting = True
113
+ threading.Thread(
114
+ target=self.executeCommandThreaded,
115
+ args=(command,),
116
+ daemon=True
117
+ ).start()
118
+
119
+ return("break")
120
+
121
+ def readInput(self):
122
+ """Return the last entered line when input() is called."""
123
+ self.waitingForInput = True
124
+ self.wait_variable(self.inputVar) #< waits until Enter is pressed
125
+ line = self.inputVar.get()
126
+ self.inputVar.set("") #< reset
127
+ return(line)
128
+
129
+ def onShiftEnter(self, event):
130
+ """Handle Shift+Enter - new line with auto-indent."""
131
+ self.suggestionManager.hideSuggestions()
132
+
133
+ if self.isExecuting:
134
+ return("break")
135
+
136
+ # Get current line for indent calculation
137
+ cursorPos = self.index("insert")
138
+ lineStart = self.index(f"{cursorPos} linestart")
139
+ lineEnd = self.index(f"{cursorPos} lineend")
140
+ currentLine = self.get(lineStart, lineEnd)
141
+
142
+ # Calculate indentation
143
+ indent = self.calculateIndent(currentLine)
144
+
145
+ # Insert newline with indent
146
+ self.insert("insert", "\n" + " " * indent)
147
+ self.see("end")
148
+
149
+ return("break")
150
+
151
+ def onTab(self, event):
152
+ """Handle Tab key for autocompletion."""
153
+ if self.isExecuting:
154
+ return("break")
155
+
156
+ if self.suggestionManager.suggestionWindow and \
157
+ self.suggestionManager.suggestionWindow.winfo_viewable():
158
+ self.suggestionManager.applySuggestion()
159
+ else:
160
+ self.suggestionManager.showSuggestions()
161
+
162
+ return("break")
163
+
164
+ def onBackspace(self, event):
165
+ """Prevent backspace from deleting the prompt."""
166
+ if not self.isCursorInEditableArea():
167
+ return("break")
168
+
169
+ # Check if we're at the prompt boundary
170
+ cursorPos = self.index("insert")
171
+ promptPos = self.getPromptPosition()
172
+
173
+ if self.compare(cursorPos, "<=", promptPos):
174
+ return("break")
175
+
176
+ def onClick(self, event):
177
+ """Handle mouse clicks - Ctrl+Click opens help for the clicked word."""
178
+ self.suggestionManager.hideSuggestions()
179
+
180
+ if event.state & 0x4: #< Ctrl pressed
181
+ clickIndex = self.index(f"@{event.x},{event.y}") # mouse index
182
+
183
+ # Get the full line
184
+ i = int(clickIndex.split('.')[1])
185
+ lineNum = clickIndex.split('.')[0]
186
+ lineStart = f"{lineNum}.0"
187
+ lineEnd = f"{lineNum}.end"
188
+ lineText = self.get(lineStart, lineEnd)
189
+
190
+ wordEndIndex = self.index(f"{clickIndex} wordend")
191
+ # obj = self.get(f"{clickIndex} wordstart", f"{clickIndex} wordend").strip() #< Get the word at that index
192
+ obj = ""
193
+ for i in range (i-1,2, -1):
194
+ letter = lineText[i]
195
+ if (not (letter.isalnum() or letter == "_" or letter == ".")): #< or (letter in " \n\t\r")
196
+ obj = lineText[i+1: int(wordEndIndex.split('.')[1])]
197
+ break
198
+
199
+
200
+ if obj:
201
+ self.helpTab.updateHelp(obj)
202
+ self.helpTab.open()
203
+
204
+ return("break") #< Prevent default cursor behavior
205
+
206
+ return(None) #< Normal click behavior
207
+
208
+ def onKeyPress(self, event):
209
+ """Handle key press events."""
210
+ # print(event.keysym)
211
+ if self.suggestionManager.suggestionWindow and \
212
+ self.suggestionManager.suggestionWindow.winfo_viewable():
213
+ if event.keysym == "Escape":
214
+ self.suggestionManager.hideSuggestions()
215
+ return("break")
216
+
217
+ # Prevent editing outside command area
218
+ if not event.keysym in ["Shift_L", "Shift_R", "Control_L", "Control_R"]:
219
+ self.navigatingHistory = False
220
+ if not self.isCursorInEditableArea():
221
+ self.mark_set("insert", "end")
222
+
223
+ if event.keysym in ["Left", "Right"]:
224
+ if self.index("insert") == self.getPromptPosition():
225
+ self.mark_set("insert", "1.4")
226
+ return("break")
227
+
228
+ def onKeyRelease(self, event):
229
+ """Handle key release events."""
230
+ if event.keysym in ["Return", "Escape", "Left", "Right", "Home", "End"]:
231
+ self.suggestionManager.hideSuggestions()
232
+ elif event.keysym not in ["Up", "Down", "Shift_L", "Shift_R", "Control_L", "Control_R"]:
233
+ if not self.isExecuting:
234
+ self.after_idle(self.suggestionManager.showSuggestions)
235
+ if not self.isExecuting:
236
+ self.after_idle(lambda: self.updateStyling(start=self.getPromptPosition()))
237
+
238
+ def cancel(self, event):
239
+ self.history.add(self.getCurrentCommand())
240
+ self.replaceCurrentCommand("")
241
+
242
+ def historyReplace(self, command):
243
+ if self.getCurrentCommand() == "" or self.navigatingHistory:
244
+ if self.isExecuting:
245
+ return("break")
246
+
247
+ if self.history.index == len(self.history.history):
248
+ self.history.setTemp(self.getCurrentCommand())
249
+
250
+ if command is not None:
251
+ self.replaceCurrentCommand(command)
252
+ self.navigatingHistory = True
253
+ return("break")
254
+
255
+ def onUp(self, event):
256
+ if self.suggestionManager.suggestionWindow and \
257
+ self.suggestionManager.suggestionWindow.winfo_viewable():
258
+ if event.keysym == "Up":
259
+ self.suggestionManager.handleNavigation("up")
260
+ return("break")
261
+ command = self.history.navigateUp()
262
+ return(self.historyReplace(command))
263
+ # self.mark_set("insert", "insert -1 line")
264
+
265
+ def onDown(self, event):
266
+ if self.suggestionManager.suggestionWindow and \
267
+ self.suggestionManager.suggestionWindow.winfo_viewable():
268
+ if event.keysym == "Down":
269
+ self.suggestionManager.handleNavigation("down")
270
+ return("break")
271
+ command = self.history.navigateDown()
272
+ return(self.historyReplace(command))
273
+
274
+ def isIncompleteStatement(self, code):
275
+ """Check if the code is an incomplete statement."""
276
+ lines = code.split("\n")
277
+ if not lines[-1].strip():
278
+ return(False)
279
+
280
+ # Check for line ending with colon
281
+ for line in lines:
282
+ if line.strip().endswith(":"):
283
+ return(True)
284
+
285
+ return(False)
286
+
287
+ def calculateIndent(self, line):
288
+ """Calculate the indentation level for the next line."""
289
+ currentIndent = len(line) - len(line.lstrip())
290
+
291
+ # If line ends with colon, increase indent
292
+ if line.strip().endswith(":"):
293
+ return(currentIndent + 4)
294
+
295
+ return(currentIndent)
296
+
297
+ def writeOutput(self, text, tag="output"):
298
+ """Write output to the console (thread-safe)."""
299
+ def _write():
300
+ self.insert("end", text + "\n", tag)
301
+ self.see("end")
302
+
303
+ self.after(0, _write)
304
+
305
+ def getPromptPosition(self):
306
+ """Get the position right after the prompt on current command line."""
307
+ return(f"{self.currentCommandLine}.{self.PROMPT_LENGTH}")
308
+
309
+ def getCurrentCommand(self):
310
+ """Extract the current command text (without prompt)."""
311
+ start = self.getPromptPosition()
312
+ end = "end-1c"
313
+ return(self.get(start, end))
314
+
315
+ def addPrompt(self):
316
+ """Add a new command prompt."""
317
+ def _add():
318
+ # Store the line number for the new command
319
+ self.currentCommandLine = self.getCurrentLineNumber()
320
+
321
+ # Insert prompt
322
+ self.insert("end", self.PROMPT)
323
+ promptStart = f"{self.currentCommandLine}.0"
324
+ promptEnd = f"{self.currentCommandLine}.{self.PROMPT_LENGTH}"
325
+ self.tag_add("prompt", promptStart, promptEnd)
326
+
327
+ self.mark_set("insert", "end")
328
+ self.see("end")
329
+ self.isExecuting = False
330
+
331
+ if self.isExecuting:
332
+ self.after(0, _add)
333
+ else:
334
+ _add()
335
+
336
+ def executeCommandThreaded(self, command):
337
+ """Execute a command in a separate thread."""
338
+ try:
339
+ # Try eval first for expressions
340
+ result = eval(command, self.master.userGlobals, self.master.userLocals)
341
+ if result is not None:
342
+ self.writeOutput(str(result), "result")
343
+ self.master.userLocals["_"] = result
344
+ except SyntaxError:
345
+ try:
346
+ # Try exec for statements
347
+ exec(command, self.master.userGlobals, self.master.userLocals)
348
+ except Exception:
349
+ self.writeOutput(traceback.format_exc(), "error")
350
+ except Exception:
351
+ self.writeOutput(traceback.format_exc(), "error")
352
+
353
+ # Add new prompt after execution
354
+ self.addPrompt()