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.
- {liveconsole-1.4.0/src/liveConsole.egg-info → liveconsole-1.5.0}/PKG-INFO +20 -5
- {liveconsole-1.4.0 → liveconsole-1.5.0}/README.md +19 -4
- {liveconsole-1.4.0 → liveconsole-1.5.0}/pyproject.toml +4 -1
- liveconsole-1.5.0/src/__init__.py +1 -0
- liveconsole-1.5.0/src/commandHistory.py +34 -0
- liveconsole-1.5.0/src/helpTab.py +64 -0
- {liveconsole-1.4.0 → liveconsole-1.5.0/src/liveConsole.egg-info}/PKG-INFO +20 -5
- {liveconsole-1.4.0 → liveconsole-1.5.0}/src/liveConsole.egg-info/SOURCES.txt +7 -0
- liveconsole-1.5.0/src/liveConsole.egg-info/entry_points.txt +2 -0
- liveconsole-1.5.0/src/liveConsole.egg-info/top_level.txt +8 -0
- liveconsole-1.5.0/src/liveConsole.py +8 -0
- liveconsole-1.5.0/src/mainConsole.py +354 -0
- liveconsole-1.5.0/src/pysole.py +132 -0
- liveconsole-1.5.0/src/styledTextbox.py +51 -0
- liveconsole-1.5.0/src/suggestionManager.py +165 -0
- liveconsole-1.4.0/src/__init__.py +0 -0
- liveconsole-1.4.0/src/liveConsole.egg-info/top_level.txt +0 -2
- liveconsole-1.4.0/src/liveConsole.py +0 -752
- {liveconsole-1.4.0 → liveconsole-1.5.0}/LICENSE +0 -0
- {liveconsole-1.4.0 → liveconsole-1.5.0}/setup.cfg +0 -0
- {liveconsole-1.4.0 → liveconsole-1.5.0}/src/liveConsole.egg-info/dependency_links.txt +0 -0
- {liveconsole-1.4.0 → liveconsole-1.5.0}/src/liveConsole.egg-info/requires.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: liveConsole
|
3
|
-
Version: 1.
|
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
|
-
#
|
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
|
-
*
|
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
|
-
|
90
|
-
|
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
|
-
#
|
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
|
-
*
|
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
|
-
|
77
|
-
|
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.
|
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.
|
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
|
-
#
|
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
|
-
*
|
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
|
-
|
90
|
-
|
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,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()
|