liveConsole 1.6.1__py3-none-any.whl → 1.7.11__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/__init__.py +1 -0
- liveconsole-1.7.11.dist-info/METADATA +217 -0
- liveconsole-1.7.11.dist-info/RECORD +19 -0
- liveconsole-1.7.11.dist-info/top_level.txt +2 -0
- pysole/__init__.py +3 -0
- pysole/mainConsole.py +100 -38
- pysole/pysole.py +131 -13
- pysole/settings.json +1 -0
- pysole/styledTextbox.py +6 -4
- pysole/suggestionManager.py +5 -2
- pysole/themes.json +4 -0
- pysole/utils.py +41 -0
- liveconsole-1.6.1.dist-info/METADATA +0 -138
- liveconsole-1.6.1.dist-info/RECORD +0 -18
- liveconsole-1.6.1.dist-info/top_level.txt +0 -1
- {liveconsole-1.6.1.dist-info → liveconsole-1.7.11.dist-info}/WHEEL +0 -0
- {liveconsole-1.6.1.dist-info → liveconsole-1.7.11.dist-info}/entry_points.txt +0 -0
- {liveconsole-1.6.1.dist-info → liveconsole-1.7.11.dist-info}/licenses/LICENSE +0 -0
liveConsole/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from pysole import *
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: liveConsole
|
|
3
|
+
Version: 1.7.11
|
|
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
|
+
Project-URL: Homepage, https://github.com/TzurSoffer/Pysole
|
|
8
|
+
Project-URL: Repository, https://github.com/TzurSoffer/Pysole
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: customtkinter
|
|
13
|
+
Requires-Dist: pygments
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# PYSOLE
|
|
17
|
+
|
|
18
|
+
## You can finally test your code in real time without using idle!
|
|
19
|
+
### If you found [this repository](https://github.com/TzurSoffer/Pysole) useful, please give it a ⭐!.
|
|
20
|
+
|
|
21
|
+
## Showcase (click to watch on Youtube)
|
|
22
|
+
[](https://www.youtube.com/shorts/pjoelNjc3O0)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
Table of contents
|
|
26
|
+
|
|
27
|
+
- Features
|
|
28
|
+
- Installation
|
|
29
|
+
- Usage
|
|
30
|
+
- Parameters
|
|
31
|
+
- Keyboard Shortcuts
|
|
32
|
+
- Troubleshooting/Notes
|
|
33
|
+
- Contributing
|
|
34
|
+
- License
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
Pysole provides a compact but powerful set of features designed to make interactive debugging and live testing fast and pleasant.
|
|
39
|
+
|
|
40
|
+
1. Live GUI console (syntax highlighting)
|
|
41
|
+
- Real-time syntax highlighting using Pygments.
|
|
42
|
+
- Monokai style by default, configurable through themes.
|
|
43
|
+
|
|
44
|
+
2. Autocomplete & suggestions
|
|
45
|
+
- Autocomplete for Python keywords, built-ins and variables in scope.
|
|
46
|
+
- Popup suggestions after typing (configurable behavior) and insert-on-confirm.
|
|
47
|
+
|
|
48
|
+
3. Run remaining script code at startup
|
|
49
|
+
- `runRemainingCode=True` will execute the remainder of the calling script after `probe()` is invoked.
|
|
50
|
+
- `printStartupCode=True` prints the captured code chunks as they execute.
|
|
51
|
+
|
|
52
|
+
4. Thread-safe execution + output capture
|
|
53
|
+
- User code runs in a background thread to avoid blocking the GUI.
|
|
54
|
+
- `stdout` and `stderr` are redirected into the GUI console output area.
|
|
55
|
+
|
|
56
|
+
5. Multi-line input, indentation & history
|
|
57
|
+
- Shift+Enter inserts a newline with proper indentation.
|
|
58
|
+
- Command history with clickable entries for easy reuse.
|
|
59
|
+
|
|
60
|
+
6. Integrated Help Panel and Features tab
|
|
61
|
+
- Right-hand help panel shows `help(obj)` output.
|
|
62
|
+
- A Features view is available from the File menu and shows the built-in features summary.
|
|
63
|
+
|
|
64
|
+
7. Themes & persistent settings
|
|
65
|
+
- Theme picker in the File menu; selected theme is written to `settings.json`.
|
|
66
|
+
- Settings (THEME, BEHAVIOR, FONT) are loaded at startup from `src/pysole/settings.json`.
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
## Installation
|
|
70
|
+
|
|
71
|
+
Quick install
|
|
72
|
+
|
|
73
|
+
```powershell
|
|
74
|
+
pip install liveConsole
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Notes: the package is published under the name `liveConsole` (see `pyproject.toml`).
|
|
78
|
+
|
|
79
|
+
If you prefer to install from source, clone this repo and run:
|
|
80
|
+
|
|
81
|
+
```powershell
|
|
82
|
+
pip install -e .
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Command-line entry points
|
|
86
|
+
|
|
87
|
+
After installation, two console commands are provided:
|
|
88
|
+
|
|
89
|
+
- `pysole` — open the GUI console (same as `liveconsole`)
|
|
90
|
+
- `liveconsole` — open the GUI console
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
## Usage
|
|
94
|
+
|
|
95
|
+
Programmatic usage (embed in scripts):
|
|
96
|
+
|
|
97
|
+
- Basic, automatic caller capture:
|
|
98
|
+
```python
|
|
99
|
+
import pysole
|
|
100
|
+
pysole.probe()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- Run the remaining code in the current file inside the console and print the code as it executes
|
|
104
|
+
```python
|
|
105
|
+
pysole.probe(runRemainingCode=True, printStartupCode=True)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- Override appearance and prompt
|
|
109
|
+
```python
|
|
110
|
+
pysole.probe(primaryPrompt='PY> ', font='Consolas', fontSize=14)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Parameters
|
|
114
|
+
|
|
115
|
+
This section documents the parameters accepted by `pysole.probe()` and the `InteractiveConsole` constructor in `src/pysole/pysole.py`. Most of these parameters are optional; reasonable defaults are taken from the calling frame and `settings.json`.
|
|
116
|
+
|
|
117
|
+
Note: `probe(...)` forwards its arguments to `InteractiveConsole(...)` so you can pass any of the parameters below to `probe()` directly.
|
|
118
|
+
|
|
119
|
+
runRemainingCode
|
|
120
|
+
- Meaning: When `True`, Pysole will read the remainder of the source file that contains the `probe()` call and run those lines inside the console's namespace.
|
|
121
|
+
- Type: bool
|
|
122
|
+
- Default: `False`
|
|
123
|
+
- Behavior: The implementation inspects `callerFrame.f_code.co_filename` for the filename and `callerFrame.f_lineno` for the line number where `probe()` was called. Lines after that line are captured into `startupCode` and executed on console startup.
|
|
124
|
+
- Caution: This requires the source file to be readable from disk (not packaged/compiled away). Large files will be read into memory.
|
|
125
|
+
|
|
126
|
+
printStartupCode
|
|
127
|
+
- Meaning: Controls how the startup code (captured by `runRemainingCode=True`) is executed: printed chunk-by-chunk to the console and executed interactively, or executed silently.
|
|
128
|
+
- Type: bool
|
|
129
|
+
- Default: `False`
|
|
130
|
+
- Behavior: If `True`, the startup code is split into logical top-level chunks (top-level statements and their indented blocks). Each chunk is printed and executed sequentially so you can see what runs. If `False`, the entire remaining code is executed silently in one go (but output is still captured and shown).
|
|
131
|
+
|
|
132
|
+
primaryPrompt
|
|
133
|
+
- Meaning: Overrides the primary prompt string (for example `>>>`). This updates the in-memory `BEHAVIOR['PRIMARY_PROMPT']` used by the console and the default can also be changed in the settings file directly.
|
|
134
|
+
- Type: string
|
|
135
|
+
- Default: The prompt value defined in `settings.json` under `BEHAVIOR -> PRIMARY_PROMPT`.
|
|
136
|
+
- Notes: Passing this parameter changes the prompt for the current session only.
|
|
137
|
+
|
|
138
|
+
font
|
|
139
|
+
- Meaning: Overrides the font family used in console widgets.
|
|
140
|
+
- Type: string (font family name, e.g. "Consolas", "Courier New")
|
|
141
|
+
- Default: The font specified by `settings.json` -> `THEME` -> `FONT`.
|
|
142
|
+
|
|
143
|
+
fontSize
|
|
144
|
+
- Meaning: Overrides the font size used in console widgets.
|
|
145
|
+
- Type: int (font size in points / pixels depending on the platform and Tk configuration)
|
|
146
|
+
- Default: Value from `settings.json` -> `THEME` -> `FONT_SIZE`.
|
|
147
|
+
|
|
148
|
+
removeWaterMark
|
|
149
|
+
- Meaning: Controls whether a short welcome watermark message (with a GitHub link and request to star the project) is printed at startup.
|
|
150
|
+
- Type: bool
|
|
151
|
+
- Default: `False` (watermark shown)
|
|
152
|
+
|
|
153
|
+
userGlobals
|
|
154
|
+
- Meaning: The `globals()` mapping that the console will use as its global namespace. Variables, functions, and imports in this mapping will be visible to code executed in the console.
|
|
155
|
+
- Type: dict-like (typically the dict returned by `globals()`)
|
|
156
|
+
- Default: If omitted, the console infers the caller's globals using `callerFrame.f_globals` (or from `inspect.currentframe().f_back` if `callerFrame` is also omitted).
|
|
157
|
+
- When to pass: Provide this when you want the console to operate on a specific module or custom namespace.
|
|
158
|
+
|
|
159
|
+
userLocals
|
|
160
|
+
- Meaning: The `locals()` mapping used as the console's local namespace. Local variables available at the call site will be visible here.
|
|
161
|
+
- Type: dict-like (typically the dict returned by `locals()`)
|
|
162
|
+
- Default: Inferred from the caller's frame (`callerFrame.f_locals`) if not provided.
|
|
163
|
+
|
|
164
|
+
callerFrame
|
|
165
|
+
- Meaning: An `inspect` frame object used to infer both `userGlobals` and `userLocals` when they are not supplied. It's also used to determine the source file and line number for the "run remaining code" feature.
|
|
166
|
+
- Type: frame object (as returned by `inspect.currentframe()` and `frame.f_back`)
|
|
167
|
+
- Default: If omitted, `probe()` sets `callerFrame = inspect.currentframe().f_back` to automatically capture the frame of the caller.
|
|
168
|
+
- When to pass: Use an explicit frame when calling `probe()` from helper wrappers or non-standard contexts where automatic frame detection would be wrong.
|
|
169
|
+
|
|
170
|
+
Behavioral notes and edge cases
|
|
171
|
+
- `probe()` replaces `sys.stdout`, `sys.stderr`, and `sys.stdin` with console-aware redirectors while the console is running. These streams are restored when the console's `onClose()` runs (but be mindful when embedding Pysole in larger apps).
|
|
172
|
+
- If `runRemainingCode=True` but the source file cannot be read (packaged app, missing file, permission issues), the attempt to read the file will fail — in that case either run Pysole without `runRemainingCode` or pass an explicit `startupCode` (if you extend the API).
|
|
173
|
+
- When `printStartupCode=True`, chunks are determined by top-level lines (zero indent) and their following indented lines. This makes printed execution easier to follow for functions, classes and loops.
|
|
174
|
+
|
|
175
|
+
## Keyboard Shortcuts
|
|
176
|
+
|
|
177
|
+
| Key | Action |
|
|
178
|
+
| --- | --- |
|
|
179
|
+
| `Enter` | Execute command (if complete) |
|
|
180
|
+
| `Shift+Enter` | Insert newline with auto-indent |
|
|
181
|
+
| `Tab` | Complete the current word / show suggestions |
|
|
182
|
+
| `Up/Down` | Navigate suggestion list |
|
|
183
|
+
| `Escape` | Hide suggestions |
|
|
184
|
+
| `Ctrl Click` | open help panel on the current method/func/class... |
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
## Troubleshooting/Notes
|
|
188
|
+
|
|
189
|
+
Behavioral notes and edge cases
|
|
190
|
+
|
|
191
|
+
- `probe()` temporarily replaces `sys.stdout`, `sys.stderr` and `sys.stdin` with redirectors that send text to the GUI console. These are restored on close (`onClose()`). Embedding Pysole in larger apps should take this into account.
|
|
192
|
+
- `runRemainingCode=True` requires the calling module's source file to be available on disk. Running this in frozen/packaged environments may fail.
|
|
193
|
+
- `printStartupCode=True` prints chunks determined by top-level statements (zero indent) and their indented blocks so function/class/loop definitions are grouped with their bodies.
|
|
194
|
+
|
|
195
|
+
Settings and themes
|
|
196
|
+
|
|
197
|
+
Default UI and behavior settings are loaded from `src/pysole/settings.json` (path built from `src/pysole/utils.py`). Themes are listed in `src/pysole/themes.json`. The in-app Theme Picker writes the selected theme back to `settings.json` to persist across sessions.
|
|
198
|
+
|
|
199
|
+
Troubleshooting
|
|
200
|
+
|
|
201
|
+
- If the GUI doesn't start, make sure `customtkinter` is installed for your Python version.
|
|
202
|
+
- On Linux, ensure your Tk/Tcl support is present (system package) and `DISPLAY` is set when running headful UIs.
|
|
203
|
+
- If `runRemainingCode` appears to run the wrong code, check where `probe()` is called (wrappers can shift the caller frame). Use `callerFrame=` to pass an explicit frame if needed.
|
|
204
|
+
|
|
205
|
+
## Contributing
|
|
206
|
+
|
|
207
|
+
- Bug reports and PRs welcome. Please open issues on the upstream GitHub repository: https://github.com/TzurSoffer/Pysole
|
|
208
|
+
- Keep test changes small and focused. Include a short description of the error / feature and steps to reproduce.
|
|
209
|
+
|
|
210
|
+
Changelog (high level)
|
|
211
|
+
|
|
212
|
+
- See `pyproject.toml` for the current package version.
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
This project is available under the MIT license — see the `LICENSE` file in the repository root.
|
|
217
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
liveConsole/__init__.py,sha256=GyV_Y3iiiS12JoEiqE55uZPQ8ZDYfOSKvxh5rcfaD1U,20
|
|
2
|
+
liveconsole-1.7.11.dist-info/licenses/LICENSE,sha256=7dZ0zL72aGaFE0C9DxacOpnaSkC5jajhG6iL7lqhWmU,1064
|
|
3
|
+
pysole/__init__.py,sha256=0Jq2s5WxFVveZW-pGwThGZLt_mVSdGcsT9lpJNEZ6ds,124
|
|
4
|
+
pysole/__main__.py,sha256=QvVFH8J2yzgQaF9MosQ6ajCW67uiRbYliePsTEUCIAg,82
|
|
5
|
+
pysole/commandHistory.py,sha256=xJtWbJ_vgJo2QGgaZJsApTOi_Hm8Yz0V9_zqQUCItj8,1084
|
|
6
|
+
pysole/helpTab.py,sha256=o0uSY-8sw7FuuBrt0FQwcgK6ljNVx3IgRnW7heZ6GvY,2454
|
|
7
|
+
pysole/liveConsole.py,sha256=lzS3dphAQ1i8pQC4E2FY-FyMMSKi-dAN0xr6XUgrNmo,168
|
|
8
|
+
pysole/mainConsole.py,sha256=TSiqbR4WLUuI0hOzjuI9X_TwIEYa6vGnIbgGhKBHJ3Q,15537
|
|
9
|
+
pysole/pysole.py,sha256=PxZAWFkxVqF2OcgecTe9LZ0xZdEdHiS7UYSCbNPf22c,14136
|
|
10
|
+
pysole/settings.json,sha256=cmOtIhRDWHMwmQMESuykWuzJd_jG6iT2tJ-uhSps_3U,722
|
|
11
|
+
pysole/styledTextbox.py,sha256=qQnkShALNbvbZY7hYDM8Gigke63TfbtQYPUsrm4Qk6Q,2145
|
|
12
|
+
pysole/suggestionManager.py,sha256=CGR1wsRIU4le3Bq_6CPAytM1CzTQAV_jUl5KZbg9LPU,6544
|
|
13
|
+
pysole/themes.json,sha256=2KvEfxm-eDsfVKIdBhWk-Qd93wYQub3YwkxbS6CGKH0,2525
|
|
14
|
+
pysole/utils.py,sha256=cKsSPWeYRxPhkiM8Xrb-aMW6w0SU5Xuu9WCgsu6_xtA,1364
|
|
15
|
+
liveconsole-1.7.11.dist-info/METADATA,sha256=hUYw5e9NhozaoUvJvjLnruc6j9uSnCmhmPZa5_gXfmE,10408
|
|
16
|
+
liveconsole-1.7.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
17
|
+
liveconsole-1.7.11.dist-info/entry_points.txt,sha256=qtvuJHcex4QqM97x_UawFWJYnfhQRl0jhqLcWRpnAGo,84
|
|
18
|
+
liveconsole-1.7.11.dist-info/top_level.txt,sha256=YGhC2H7bvcDnMwEtvLXF6qQNIZT2Saayyk1KOJyndN8,19
|
|
19
|
+
liveconsole-1.7.11.dist-info/RECORD,,
|
pysole/__init__.py
CHANGED
pysole/mainConsole.py
CHANGED
|
@@ -3,6 +3,7 @@ import traceback
|
|
|
3
3
|
from .suggestionManager import CodeSuggestionManager
|
|
4
4
|
from .commandHistory import CommandHistory
|
|
5
5
|
from .styledTextbox import StyledTextWindow
|
|
6
|
+
from .utils import stdPrint
|
|
6
7
|
|
|
7
8
|
import tkinter as tk
|
|
8
9
|
|
|
@@ -11,26 +12,27 @@ class InteractiveConsoleText(StyledTextWindow):
|
|
|
11
12
|
def __init__(self, master, helpTab, theme, font, behavior, userLocals=None, userGlobals=None, **kwargs):
|
|
12
13
|
super().__init__(master, theme=theme, font=font, **kwargs)
|
|
13
14
|
self.font=(font["FONT"], font["FONT_SIZE"])
|
|
14
|
-
|
|
15
|
+
|
|
15
16
|
# Initialize components
|
|
16
17
|
self.PROMPT = behavior["PRIMARY_PROMPT"]
|
|
17
18
|
self.PROMPT_LENGTH = len(self.PROMPT)
|
|
18
19
|
self.suggestionManager = CodeSuggestionManager(self, userLocals=userLocals, userGlobals=userGlobals, theme=theme, font=font)
|
|
19
20
|
self.helpTab = helpTab
|
|
20
|
-
|
|
21
|
+
|
|
21
22
|
self.navigatingHistory = False
|
|
22
23
|
self.history = CommandHistory()
|
|
23
|
-
|
|
24
|
+
|
|
24
25
|
self.inputVar = tk.StringVar()
|
|
25
26
|
self.waitingForInput = False
|
|
27
|
+
self.inputLine = "1.0"
|
|
26
28
|
|
|
27
29
|
# Track current command
|
|
28
30
|
self.currentCommandLine = 1
|
|
29
31
|
self.isExecuting = False
|
|
30
|
-
|
|
32
|
+
|
|
31
33
|
# Setup bindings
|
|
32
34
|
self._setupBindings()
|
|
33
|
-
|
|
35
|
+
|
|
34
36
|
# Initialize with first prompt
|
|
35
37
|
self.addPrompt()
|
|
36
38
|
|
|
@@ -41,32 +43,59 @@ class InteractiveConsoleText(StyledTextWindow):
|
|
|
41
43
|
self.bind("<Control-c>", self.cancel)
|
|
42
44
|
self.bind("<Tab>", self.onTab)
|
|
43
45
|
self.bind("<BackSpace>", self.onBackspace)
|
|
46
|
+
self.bind("<Delete>", self.onDelete)
|
|
44
47
|
self.bind("<KeyRelease>", self.onKeyRelease)
|
|
45
48
|
self.bind("<KeyPress>", self.onKeyPress)
|
|
46
49
|
self.bind("<Button-1>", self.onClick)
|
|
47
50
|
self.bind("<Up>", self.onUp)
|
|
48
51
|
self.bind("<Down>", self.onDown)
|
|
49
52
|
|
|
53
|
+
def getPromptPosition(self):
|
|
54
|
+
"""Get the position right after the prompt on current command line."""
|
|
55
|
+
return(f"{self.currentCommandLine}.{self.PROMPT_LENGTH}")
|
|
56
|
+
|
|
50
57
|
def getCurrentLineNumber(self):
|
|
51
58
|
"""Get the line number where current command starts."""
|
|
52
59
|
return(int(self.index("end-1c").split(".")[0]))
|
|
53
|
-
|
|
60
|
+
|
|
61
|
+
def resetCurrentLineNumber(self):
|
|
62
|
+
self.currentCommandLine = self.getCurrentLineNumber()
|
|
63
|
+
|
|
54
64
|
def getCommandStartPosition(self):
|
|
55
65
|
"""Get the starting position of the current command."""
|
|
56
66
|
return(f"{self.currentCommandLine}.0")
|
|
57
67
|
|
|
58
|
-
def
|
|
59
|
-
"""
|
|
68
|
+
def writeToPrompt(self, text):
|
|
69
|
+
"""Write text to the prompt of the console"""
|
|
60
70
|
if self.isExecuting:
|
|
61
71
|
return
|
|
62
|
-
|
|
72
|
+
|
|
73
|
+
if text:
|
|
74
|
+
self.insert("end", text)
|
|
75
|
+
self.mark_set("insert", "end")
|
|
76
|
+
self.see("end")
|
|
77
|
+
self.updateStyling(start=self.getPromptPosition()) #< Ensure styling/lexer applied after programmatic change:
|
|
78
|
+
|
|
79
|
+
def replaceCurrentCommand(self, newCommand):
|
|
80
|
+
"""Replace the current command with new text."""
|
|
63
81
|
start = self.getPromptPosition()
|
|
64
82
|
end = "end-1c"
|
|
65
|
-
|
|
66
83
|
self.delete(start, end)
|
|
67
|
-
self.
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
self.writeToPrompt(newCommand)
|
|
85
|
+
|
|
86
|
+
def runCommand(self, command, printCommand=False, clearPrompt=True):
|
|
87
|
+
"""Insert code into the console prompt and execute it as if Enter was pressed."""
|
|
88
|
+
if self.isExecuting:
|
|
89
|
+
return(False)
|
|
90
|
+
if printCommand:
|
|
91
|
+
if clearPrompt:
|
|
92
|
+
self.replaceCurrentCommand(command) #< Replace current command with the new code
|
|
93
|
+
else:
|
|
94
|
+
self.writeToPrompt(command)
|
|
95
|
+
self.onEnter(None, insertWhitespace=False) #< Simulate pressing Enter to run the command
|
|
96
|
+
else:
|
|
97
|
+
self.executeCommandThreaded(command, addPrompt=False)
|
|
98
|
+
|
|
70
99
|
def isCursorInEditableArea(self):
|
|
71
100
|
"""Check if cursor is in the editable command area."""
|
|
72
101
|
if self.isExecuting:
|
|
@@ -78,7 +107,7 @@ class InteractiveConsoleText(StyledTextWindow):
|
|
|
78
107
|
return((cursorLine >= self.currentCommandLine and
|
|
79
108
|
(cursorLine > self.currentCommandLine or cursorCol >= self.PROMPT_LENGTH)))
|
|
80
109
|
|
|
81
|
-
def onEnter(self, event):
|
|
110
|
+
def onEnter(self, event, insertWhitespace=True):
|
|
82
111
|
"""Handle Enter key - execute command."""
|
|
83
112
|
self.suggestionManager.hideSuggestions()
|
|
84
113
|
|
|
@@ -88,25 +117,25 @@ class InteractiveConsoleText(StyledTextWindow):
|
|
|
88
117
|
self.inputVar.set(line)
|
|
89
118
|
self.waitingForInput = False
|
|
90
119
|
return("break")
|
|
91
|
-
|
|
120
|
+
|
|
92
121
|
if self.isExecuting:
|
|
93
122
|
return("break")
|
|
94
|
-
|
|
123
|
+
|
|
95
124
|
command = self.getCurrentCommand()
|
|
96
|
-
|
|
125
|
+
# print(command)
|
|
126
|
+
|
|
97
127
|
if not command.strip():
|
|
98
128
|
return("break")
|
|
99
|
-
|
|
129
|
+
|
|
100
130
|
# Check if statement is incomplete
|
|
101
|
-
if self.isIncompleteStatement(command):
|
|
131
|
+
if self.isIncompleteStatement(command) and insertWhitespace:
|
|
102
132
|
return(self.onShiftEnter(event))
|
|
103
|
-
|
|
104
133
|
# Execute the command
|
|
105
134
|
self.history.add(command)
|
|
106
135
|
self.mark_set("insert", "end")
|
|
107
136
|
self.insert("end", "\n")
|
|
108
137
|
self.see("end")
|
|
109
|
-
|
|
138
|
+
|
|
110
139
|
# Execute in thread
|
|
111
140
|
self.isExecuting = True
|
|
112
141
|
threading.Thread(
|
|
@@ -114,16 +143,17 @@ class InteractiveConsoleText(StyledTextWindow):
|
|
|
114
143
|
args=(command,),
|
|
115
144
|
daemon=True
|
|
116
145
|
).start()
|
|
117
|
-
|
|
146
|
+
|
|
118
147
|
return("break")
|
|
119
148
|
|
|
120
149
|
def readInput(self):
|
|
121
150
|
"""Return the last entered line when input() is called."""
|
|
122
151
|
self.waitingForInput = True
|
|
152
|
+
self.inputLine = self.index("end -1c")
|
|
123
153
|
self.wait_variable(self.inputVar) #< waits until Enter is pressed
|
|
124
154
|
line = self.inputVar.get()
|
|
125
155
|
self.inputVar.set("") #< reset
|
|
126
|
-
return(line)
|
|
156
|
+
return(line or "\n")
|
|
127
157
|
|
|
128
158
|
def onShiftEnter(self, event):
|
|
129
159
|
"""Handle Shift+Enter - new line with auto-indent."""
|
|
@@ -162,15 +192,35 @@ class InteractiveConsoleText(StyledTextWindow):
|
|
|
162
192
|
|
|
163
193
|
def onBackspace(self, event):
|
|
164
194
|
"""Prevent backspace from deleting the prompt."""
|
|
195
|
+
|
|
196
|
+
if self.waitingForInput: #< During input mode, only allow editing after input start
|
|
197
|
+
if self.compare("insert", "<=", self.inputLine):
|
|
198
|
+
return "break"
|
|
199
|
+
return None
|
|
200
|
+
|
|
165
201
|
if not self.isCursorInEditableArea():
|
|
166
|
-
return
|
|
202
|
+
return "break"
|
|
167
203
|
|
|
168
204
|
# Check if we're at the prompt boundary
|
|
169
205
|
cursorPos = self.index("insert")
|
|
170
206
|
promptPos = self.getPromptPosition()
|
|
171
207
|
|
|
172
208
|
if self.compare(cursorPos, "<=", promptPos):
|
|
209
|
+
return "break"
|
|
210
|
+
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def onDelete(self, event):
|
|
214
|
+
"""Prevent delete from deleting protected content."""
|
|
215
|
+
if self.waitingForInput: #< During input mode, only allow editing after input start
|
|
216
|
+
if self.compare("insert", "<=", self.inputLine):
|
|
217
|
+
return("break")
|
|
218
|
+
return(None)
|
|
219
|
+
|
|
220
|
+
if not self.isCursorInEditableArea():
|
|
173
221
|
return("break")
|
|
222
|
+
|
|
223
|
+
return(None)
|
|
174
224
|
|
|
175
225
|
def onClick(self, event):
|
|
176
226
|
"""Handle mouse clicks - Ctrl+Click opens help for the clicked word."""
|
|
@@ -206,7 +256,11 @@ class InteractiveConsoleText(StyledTextWindow):
|
|
|
206
256
|
|
|
207
257
|
def onKeyPress(self, event):
|
|
208
258
|
"""Handle key press events."""
|
|
209
|
-
|
|
259
|
+
if self.waitingForInput: #< During input mode, only allow editing after input start
|
|
260
|
+
if (self.compare("insert", "<=", self.inputLine) and event.keysym == "Left"):
|
|
261
|
+
return("break")
|
|
262
|
+
return(None)
|
|
263
|
+
|
|
210
264
|
if self.suggestionManager.suggestionWindow and \
|
|
211
265
|
self.suggestionManager.suggestionWindow.winfo_viewable():
|
|
212
266
|
if event.keysym == "Escape":
|
|
@@ -293,17 +347,24 @@ class InteractiveConsoleText(StyledTextWindow):
|
|
|
293
347
|
|
|
294
348
|
return(currentIndent)
|
|
295
349
|
|
|
296
|
-
def writeOutput(self, text, tag="output"):
|
|
350
|
+
def writeOutput(self, text, tag="output", loc="end", timeout=None):
|
|
297
351
|
"""Write output to the console (thread-safe)."""
|
|
352
|
+
doneEvent = threading.Event()
|
|
353
|
+
|
|
298
354
|
def _write():
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
355
|
+
try:
|
|
356
|
+
self.insert(loc, text + "\n", tag)
|
|
357
|
+
self.see("end")
|
|
358
|
+
finally:
|
|
359
|
+
doneEvent.set() # Signal completion
|
|
360
|
+
|
|
302
361
|
self.after(0, _write)
|
|
303
362
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
363
|
+
doneEvent.wait(timeout)
|
|
364
|
+
|
|
365
|
+
def newline(self):
|
|
366
|
+
"""Insert a newline at the end."""
|
|
367
|
+
self.writeOutput("")
|
|
307
368
|
|
|
308
369
|
def getCurrentCommand(self):
|
|
309
370
|
"""Extract the current command text (without prompt)."""
|
|
@@ -326,16 +387,15 @@ class InteractiveConsoleText(StyledTextWindow):
|
|
|
326
387
|
self.mark_set("insert", "end")
|
|
327
388
|
self.see("end")
|
|
328
389
|
self.isExecuting = False
|
|
329
|
-
|
|
390
|
+
|
|
330
391
|
if self.isExecuting:
|
|
331
392
|
self.after(0, _add)
|
|
332
393
|
else:
|
|
333
394
|
_add()
|
|
334
|
-
|
|
335
|
-
def executeCommandThreaded(self, command):
|
|
395
|
+
|
|
396
|
+
def executeCommandThreaded(self, command, addPrompt=True):
|
|
336
397
|
"""Execute a command in a separate thread."""
|
|
337
398
|
try:
|
|
338
|
-
# Try eval first for expressions
|
|
339
399
|
result = eval(command, self.master.userGlobals, self.master.userLocals)
|
|
340
400
|
if result is not None:
|
|
341
401
|
self.writeOutput(str(result), "result")
|
|
@@ -349,5 +409,7 @@ class InteractiveConsoleText(StyledTextWindow):
|
|
|
349
409
|
except Exception:
|
|
350
410
|
self.writeOutput(traceback.format_exc(), "error")
|
|
351
411
|
|
|
352
|
-
|
|
353
|
-
|
|
412
|
+
if addPrompt:
|
|
413
|
+
self.addPrompt()
|
|
414
|
+
else:
|
|
415
|
+
self.isExecuting = False
|
pysole/pysole.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
1
3
|
import customtkinter as ctk
|
|
2
4
|
import tkinter as tk
|
|
3
5
|
from tkinter import messagebox
|
|
@@ -5,8 +7,9 @@ import inspect
|
|
|
5
7
|
import sys
|
|
6
8
|
import io
|
|
7
9
|
import json
|
|
10
|
+
import signal
|
|
8
11
|
|
|
9
|
-
from .utils import settingsPath, themesPath
|
|
12
|
+
from .utils import settingsPath, themesPath, stdPrint, normalizeWhitespace, findUnindentedLine
|
|
10
13
|
from .helpTab import HelpTab
|
|
11
14
|
from .mainConsole import InteractiveConsoleText
|
|
12
15
|
|
|
@@ -35,23 +38,30 @@ class StdinRedirect(io.StringIO):
|
|
|
35
38
|
|
|
36
39
|
class InteractiveConsole(ctk.CTk):
|
|
37
40
|
"""Main console window application."""
|
|
38
|
-
|
|
39
|
-
def __init__(self, userGlobals=None, userLocals=None, callerFrame=None,
|
|
41
|
+
|
|
42
|
+
def __init__(self, userGlobals=None, userLocals=None, callerFrame=None,
|
|
43
|
+
defaultSize=None, primaryPrompt=None, font=None, fontSize=None,
|
|
44
|
+
runRemainingCode=False, printStartupCode=False,
|
|
45
|
+
removeWaterMark=False):
|
|
40
46
|
super().__init__()
|
|
41
47
|
with open(settingsPath, "r") as f:
|
|
42
48
|
settings = json.load(f)
|
|
43
49
|
self.THEME = settings["THEME"]
|
|
44
50
|
self.FONT = self.THEME["FONT"]
|
|
45
51
|
self.BEHAVIOR = settings["BEHAVIOR"]
|
|
46
|
-
|
|
52
|
+
|
|
47
53
|
if primaryPrompt != None:
|
|
48
54
|
self.BEHAVIOR["PRIMARY_PROMPT"] = primaryPrompt
|
|
49
55
|
if defaultSize != None:
|
|
50
56
|
self.BEHAVIOR["DEFAULT_SIZE"] = defaultSize
|
|
57
|
+
if font != None:
|
|
58
|
+
self.FONT["FONT"] = font
|
|
59
|
+
if fontSize != None:
|
|
60
|
+
self.FONT["FONT_SIZE"] = fontSize
|
|
51
61
|
|
|
52
62
|
self.title("Live Interactive Console")
|
|
53
63
|
self.geometry(self.BEHAVIOR["DEFAULT_SIZE"])
|
|
54
|
-
|
|
64
|
+
|
|
55
65
|
ctk.set_appearance_mode(self.THEME["APPEARANCE"])
|
|
56
66
|
ctk.set_default_color_theme("blue")
|
|
57
67
|
|
|
@@ -63,17 +73,54 @@ class InteractiveConsole(ctk.CTk):
|
|
|
63
73
|
userGlobals = callerFrame.f_globals
|
|
64
74
|
if userLocals is None:
|
|
65
75
|
userLocals = callerFrame.f_locals
|
|
66
|
-
|
|
76
|
+
|
|
77
|
+
self.callerFrame = callerFrame
|
|
67
78
|
self.userGlobals = userGlobals
|
|
68
79
|
self.userLocals = userLocals
|
|
69
|
-
|
|
80
|
+
|
|
70
81
|
# Create UI
|
|
71
82
|
self._createMenu()
|
|
72
83
|
self._createUi()
|
|
84
|
+
|
|
85
|
+
self.protocol("WM_DELETE_WINDOW", self.onClose) #< Wire up window close button (X)
|
|
73
86
|
|
|
87
|
+
# Set up signal handler for Ctrl+C from terminal to close gracefully
|
|
88
|
+
def sigint_handler(signum, frame):
|
|
89
|
+
self.onClose()
|
|
90
|
+
|
|
91
|
+
signal.signal(signal.SIGINT, sigint_handler)
|
|
92
|
+
|
|
74
93
|
# Redirect stdout/stderr
|
|
75
94
|
self._setupOutputRedirect()
|
|
76
95
|
self._setupInputRedirect()
|
|
96
|
+
|
|
97
|
+
self.runRemainingCode = runRemainingCode
|
|
98
|
+
self.printStartupCode = printStartupCode
|
|
99
|
+
self.removeWaterMark = removeWaterMark
|
|
100
|
+
self.startupCode = []
|
|
101
|
+
if runRemainingCode:
|
|
102
|
+
self.startupCode = self._getStartupCode()
|
|
103
|
+
|
|
104
|
+
def _getStartupCode(self):
|
|
105
|
+
code_obj = self.callerFrame.f_code
|
|
106
|
+
callStartLineIndex = inspect.getframeinfo(self.callerFrame).positions.lineno #< start of the probe call
|
|
107
|
+
callEndLineIndex = inspect.getframeinfo(self.callerFrame).positions.end_lineno #< end of the probe call
|
|
108
|
+
filename = code_obj.co_filename
|
|
109
|
+
|
|
110
|
+
# Read the rest of the file after the call to probe()
|
|
111
|
+
with open(filename, "r", encoding="utf-8") as f:
|
|
112
|
+
lines = f.readlines()
|
|
113
|
+
|
|
114
|
+
startLine = lines[callStartLineIndex-1]
|
|
115
|
+
for line in lines[callStartLineIndex:callEndLineIndex]:
|
|
116
|
+
startLine += line.strip()
|
|
117
|
+
|
|
118
|
+
startupCode = normalizeWhitespace(lines[callEndLineIndex:]) #< ensure the code is not indented too much (egg if in __name__ == "__main__")
|
|
119
|
+
firstUnindentedLine = findUnindentedLine(startupCode)
|
|
120
|
+
while firstUnindentedLine != 0 and firstUnindentedLine != None: #< handle if probe is inside a loop/if/etc by simply unindenting the call (while is for nested calls)
|
|
121
|
+
startupCode[:firstUnindentedLine] = normalizeWhitespace(startupCode[:firstUnindentedLine])
|
|
122
|
+
firstUnindentedLine = findUnindentedLine(startupCode)
|
|
123
|
+
return(startupCode)
|
|
77
124
|
|
|
78
125
|
def _createMenu(self):
|
|
79
126
|
"""Create a menu bar using CTkOptionMenu."""
|
|
@@ -99,7 +146,7 @@ class InteractiveConsole(ctk.CTk):
|
|
|
99
146
|
self._editSettings()
|
|
100
147
|
elif choice == "Load Theme":
|
|
101
148
|
self._loadTheme()
|
|
102
|
-
|
|
149
|
+
|
|
103
150
|
def _loadTheme(self):
|
|
104
151
|
"""
|
|
105
152
|
Open a CTk popup to let the user choose a theme from themes.json.
|
|
@@ -175,7 +222,7 @@ class InteractiveConsole(ctk.CTk):
|
|
|
175
222
|
def saveSettings():
|
|
176
223
|
try:
|
|
177
224
|
newSettings = json.loads(textbox.get("0.0", "end-1c"))
|
|
178
|
-
with open(
|
|
225
|
+
with open(settingsPath, "w") as f:
|
|
179
226
|
json.dump(newSettings, f, indent=4)
|
|
180
227
|
messagebox.showinfo("Success", "Settings saved!")
|
|
181
228
|
editor.destroy()
|
|
@@ -218,28 +265,99 @@ class InteractiveConsole(ctk.CTk):
|
|
|
218
265
|
self.console.pack(fill="both", expand=True, padx=5, pady=5)
|
|
219
266
|
self.console.master = self
|
|
220
267
|
|
|
221
|
-
|
|
222
268
|
def _setupOutputRedirect(self):
|
|
223
269
|
"""Setup stdout/stderr redirection to console."""
|
|
224
270
|
sys.stdout = StdoutRedirect(self.console.writeOutput)
|
|
225
271
|
sys.stderr = StdoutRedirect(
|
|
226
|
-
lambda text, tag: self.console.writeOutput(text, "error")
|
|
272
|
+
lambda text, tag: self.console.writeOutput(text, "error", "end")
|
|
227
273
|
)
|
|
228
274
|
|
|
229
275
|
def _setupInputRedirect(self):
|
|
230
276
|
"""Setup stdin redirection to console."""
|
|
231
277
|
sys.stdin = StdinRedirect(self.console.readInput)
|
|
278
|
+
|
|
279
|
+
def onClose(self):
|
|
280
|
+
sys.stdout = sys.__stdout__
|
|
281
|
+
sys.stderr = sys.__stderr__
|
|
282
|
+
self.destroy()
|
|
283
|
+
|
|
284
|
+
def _printWaterMark(self):
|
|
285
|
+
m = (
|
|
286
|
+
"Welcome to Pysole, if you find me useful, please star me on GitHub:\n"
|
|
287
|
+
"https://github.com/TzurSoffer/Pysole"
|
|
288
|
+
)
|
|
289
|
+
stdPrint(m)
|
|
290
|
+
self.console.newline()
|
|
291
|
+
self.console.writeOutput(m, "instruction")
|
|
292
|
+
time.sleep(0.1)
|
|
293
|
+
self.console.addPrompt()
|
|
294
|
+
if self.runRemainingCode and self.printStartupCode == False:
|
|
295
|
+
self.console.newline()
|
|
296
|
+
|
|
297
|
+
def _splitCodeIntoChunks(self):
|
|
298
|
+
codeChunks = []
|
|
299
|
+
currentChunk = []
|
|
300
|
+
|
|
301
|
+
for line in self.startupCode:
|
|
302
|
+
strippedLine = line.lstrip()
|
|
303
|
+
indentLevel = len(line) - len(strippedLine)
|
|
304
|
+
|
|
305
|
+
if not strippedLine: #< Blank line, keep it in current chunk
|
|
306
|
+
currentChunk.append(line)
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
if indentLevel != 0:
|
|
310
|
+
currentChunk.append(line)
|
|
311
|
+
else:
|
|
312
|
+
codeChunks.append(currentChunk)
|
|
313
|
+
currentChunk = [line]
|
|
314
|
+
|
|
315
|
+
if currentChunk:
|
|
316
|
+
codeChunks.append(currentChunk)
|
|
317
|
+
|
|
318
|
+
return(["\n".join(chunk).strip() for chunk in codeChunks if any(line.strip() for line in chunk)])
|
|
319
|
+
|
|
232
320
|
|
|
321
|
+
def _runStartup(self):
|
|
322
|
+
if self.removeWaterMark == False:
|
|
323
|
+
self._printWaterMark()
|
|
324
|
+
|
|
325
|
+
if self.runRemainingCode == False:
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
if self.printStartupCode == False:
|
|
329
|
+
self.console.newline()
|
|
330
|
+
chunks = self._splitCodeIntoChunks()
|
|
331
|
+
for chunk in chunks:
|
|
332
|
+
while self.console.isExecuting:
|
|
333
|
+
time.sleep(0.01)
|
|
334
|
+
if self.printStartupCode == False:
|
|
335
|
+
self.console.executeCommandThreaded(chunk, addPrompt=False)
|
|
336
|
+
else:
|
|
337
|
+
self.console.runCommand(chunk, printCommand=True, clearPrompt=True)
|
|
338
|
+
if self.printStartupCode == False:
|
|
339
|
+
self.console.addPrompt()
|
|
340
|
+
|
|
233
341
|
def probe(self, *args, **kwargs):
|
|
234
342
|
"""Start the console main loop."""
|
|
343
|
+
self.after(0, threading.Thread(target=self._runStartup).start)
|
|
235
344
|
self.mainloop(*args, **kwargs)
|
|
236
345
|
|
|
237
|
-
def probe(userGlobals=None, userLocals=None, callerFrame=None
|
|
346
|
+
def probe(userGlobals=None, userLocals=None, callerFrame=None,
|
|
347
|
+
runRemainingCode=False, printStartupCode=False,
|
|
348
|
+
primaryPrompt=None, font=None, fontSize=None, removeWaterMark=False, **kwargs):
|
|
238
349
|
if callerFrame == None:
|
|
239
350
|
callerFrame = inspect.currentframe().f_back
|
|
240
351
|
InteractiveConsole(userGlobals=userGlobals,
|
|
241
352
|
userLocals=userLocals,
|
|
242
|
-
callerFrame=callerFrame
|
|
353
|
+
callerFrame=callerFrame,
|
|
354
|
+
runRemainingCode=runRemainingCode,
|
|
355
|
+
printStartupCode=printStartupCode,
|
|
356
|
+
primaryPrompt=primaryPrompt,
|
|
357
|
+
font=font,
|
|
358
|
+
fontSize=fontSize,
|
|
359
|
+
removeWaterMark=removeWaterMark,
|
|
360
|
+
**kwargs).probe()
|
|
243
361
|
|
|
244
362
|
def _standalone():
|
|
245
363
|
import pysole
|
pysole/settings.json
CHANGED
pysole/styledTextbox.py
CHANGED
|
@@ -6,7 +6,8 @@ from pygments.styles import get_style_by_name
|
|
|
6
6
|
class StyledTextWindow(tk.Text):
|
|
7
7
|
def __init__(self, master, theme, font, **kwargs):
|
|
8
8
|
super().__init__(master, **kwargs)
|
|
9
|
-
|
|
9
|
+
self.configure(font=(font["FONT"], font["FONT_SIZE"])) #< default
|
|
10
|
+
|
|
10
11
|
# Syntax highlighting setup
|
|
11
12
|
self.lexer = PythonLexer()
|
|
12
13
|
self.style = get_style_by_name(theme["LEXER_STYLE"])
|
|
@@ -16,9 +17,10 @@ class StyledTextWindow(tk.Text):
|
|
|
16
17
|
def _setupTags(self, theme, font):
|
|
17
18
|
"""Configure text tags for different output types."""
|
|
18
19
|
self.tag_configure("prompt", foreground=theme["PROMPT"], font=(font["FONT"], font["FONT_SIZE"], "bold"))
|
|
19
|
-
self.tag_configure("output", foreground=theme["OUTPUT"]
|
|
20
|
-
self.tag_configure("error", foreground=theme["ERROR"]
|
|
21
|
-
self.tag_configure("result", foreground=theme["RESULT"]
|
|
20
|
+
self.tag_configure("output", foreground=theme["OUTPUT"])
|
|
21
|
+
self.tag_configure("error", foreground=theme["ERROR"])
|
|
22
|
+
self.tag_configure("result", foreground=theme["RESULT"])
|
|
23
|
+
self.tag_configure("instruction", foreground=theme["INSTRUCTION"])
|
|
22
24
|
|
|
23
25
|
# Configure syntax highlighting tags
|
|
24
26
|
for token, style in self.style:
|
pysole/suggestionManager.py
CHANGED
|
@@ -107,8 +107,11 @@ class CodeSuggestionManager:
|
|
|
107
107
|
self.suggestionListbox.selection_set(0)
|
|
108
108
|
|
|
109
109
|
# Position window near cursor
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
try: #< some weird errors idk
|
|
111
|
+
self._positionSuggestionWindow()
|
|
112
|
+
self.suggestionWindow.deiconify()
|
|
113
|
+
except:
|
|
114
|
+
pass
|
|
112
115
|
|
|
113
116
|
def _createSuggestionWindow(self):
|
|
114
117
|
"""Create the suggestion popup window."""
|
pysole/themes.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"OUTPUT": "#ffffff",
|
|
12
12
|
"ERROR": "#ff0000",
|
|
13
13
|
"RESULT": "#66ccff",
|
|
14
|
+
"INSTRUCTION": "#ffccdd",
|
|
14
15
|
"SUGGESTION_BOX_BG": "#2d2d2d",
|
|
15
16
|
"SUGGESTION_BOX_SELECTION_BG": "#0066cc",
|
|
16
17
|
"FONT": {
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
"OUTPUT": "#000000",
|
|
31
32
|
"ERROR": "#ff0000",
|
|
32
33
|
"RESULT": "#0066cc",
|
|
34
|
+
"INSTRUCTION": "#ffccdd",
|
|
33
35
|
"SUGGESTION_BOX_BG": "#f0f0f0",
|
|
34
36
|
"SUGGESTION_BOX_SELECTION_BG": "#cce6ff",
|
|
35
37
|
"FONT": {
|
|
@@ -49,6 +51,7 @@
|
|
|
49
51
|
"OUTPUT": "#586e75",
|
|
50
52
|
"ERROR": "#dc322f",
|
|
51
53
|
"RESULT": "#2aa198",
|
|
54
|
+
"INSTRUCTION": "#ffccdd",
|
|
52
55
|
"SUGGESTION_BOX_BG": "#eee8d5",
|
|
53
56
|
"SUGGESTION_BOX_SELECTION_BG": "#b58900",
|
|
54
57
|
"FONT": {
|
|
@@ -68,6 +71,7 @@
|
|
|
68
71
|
"OUTPUT": "#f8f8f2",
|
|
69
72
|
"ERROR": "#ff5555",
|
|
70
73
|
"RESULT": "#8be9fd",
|
|
74
|
+
"INSTRUCTION": "#ffccdd",
|
|
71
75
|
"SUGGESTION_BOX_BG": "#44475a",
|
|
72
76
|
"SUGGESTION_BOX_SELECTION_BG": "#6272a4",
|
|
73
77
|
"FONT": {
|
pysole/utils.py
CHANGED
|
@@ -1,4 +1,45 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import sys
|
|
3
|
+
import re
|
|
2
4
|
|
|
3
5
|
settingsPath = os.path.join(os.path.dirname(__file__), "settings.json")
|
|
4
6
|
themesPath = os.path.join(os.path.dirname(__file__), "themes.json")
|
|
7
|
+
|
|
8
|
+
def stdPrint(text):
|
|
9
|
+
"""Print text to the terminal."""
|
|
10
|
+
try:
|
|
11
|
+
sys.__stdout__.write(f"{text}\n")
|
|
12
|
+
sys.__stdout__.flush()
|
|
13
|
+
except:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def normalizeWhitespace(lines):
|
|
17
|
+
"""
|
|
18
|
+
Normalize leading whitespace across a list of code lines.
|
|
19
|
+
Removes common leading indentation and trims excess on over-indented lines.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
if type(lines) == str:
|
|
23
|
+
lines = lines.split('\n')
|
|
24
|
+
|
|
25
|
+
# Remove empty lines and preserve original line endings
|
|
26
|
+
strippedLines = [line.rstrip('\n') for line in lines if line.strip()]
|
|
27
|
+
if not strippedLines:
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
# Find minimum indentation across non-empty lines
|
|
31
|
+
indentLevels = [
|
|
32
|
+
len(re.match(r'^[ \t]*', line).group())
|
|
33
|
+
for line in strippedLines
|
|
34
|
+
]
|
|
35
|
+
minIndent = min(indentLevels)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
normalized = [line[minIndent:] if len(line) >= minIndent else line for line in lines] #< Normalize by removing minIndent from each line
|
|
39
|
+
return(normalized)
|
|
40
|
+
|
|
41
|
+
def findUnindentedLine(lines):
|
|
42
|
+
for i, line in enumerate(lines):
|
|
43
|
+
if re.match(r'^\S', line): # Line starts with non-whitespace
|
|
44
|
+
return(i)
|
|
45
|
+
return(None)
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: liveConsole
|
|
3
|
-
Version: 1.6.1
|
|
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
|
-
# PYSOLE
|
|
15
|
-
|
|
16
|
-
## You can finally test your code in real time without using idle!
|
|
17
|
-
### If you found [this repository](https://github.com/TzurSoffer/Pysole) useful, please give it a ⭐!.
|
|
18
|
-
|
|
19
|
-
A fully-featured, **live Python console GUI** built with **CustomTkinter** and **Tkinter**, featuring:
|
|
20
|
-
|
|
21
|
-
* Python syntax highlighting via **Pygments**
|
|
22
|
-
|
|
23
|
-
* Autocomplete for **keywords, built-ins, and local/global variables**
|
|
24
|
-
|
|
25
|
-
* Thread-safe execution of Python code
|
|
26
|
-
|
|
27
|
-
* Output capturing for `stdout` and `stderr`
|
|
28
|
-
|
|
29
|
-
* Multi-line input with auto-indentation
|
|
30
|
-
|
|
31
|
-
* History of previous commands
|
|
32
|
-
|
|
33
|
-
* **Integrated Help Panel** for quick access to Python object documentation
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
## Installation
|
|
37
|
-
|
|
38
|
-
`pip install liveConsole`
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
## Features
|
|
42
|
-
|
|
43
|
-
### Standalone Launch
|
|
44
|
-
|
|
45
|
-
* Once installed, you can launch the console directly by simply typing ```pysole``` or ```liveconsole``` in the terminal
|
|
46
|
-
|
|
47
|
-
* This opens the full GUI without needing to write any code. Perfect for quick debugging and experimenting.
|
|
48
|
-
|
|
49
|
-
### Syntax Highlighting
|
|
50
|
-
|
|
51
|
-
* Real-time syntax highlighting using **Pygments** and the **Monokai** style.
|
|
52
|
-
|
|
53
|
-
* Highlights Python keywords, built-ins, and expressions in the console.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
### Autocomplete
|
|
57
|
-
|
|
58
|
-
* Suggests **keywords**, **built-in functions**, and **variables** in scope.
|
|
59
|
-
|
|
60
|
-
* Popup list appears after typing at least 2 characters.
|
|
61
|
-
|
|
62
|
-
* Only inserts the **missing portion** of a word.
|
|
63
|
-
|
|
64
|
-
* Navigate suggestions with **Up/Down arrows**, confirm with **Tab/Return**.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
### Multi-Line Input
|
|
68
|
-
|
|
69
|
-
* Supports **Shift+Enter** for inserting a new line with proper indentation.
|
|
70
|
-
|
|
71
|
-
* Automatically detects incomplete statements and continues the prompt.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
### Thread-Safe Execution
|
|
75
|
-
|
|
76
|
-
* Executes user code in a separate thread to prevent GUI freezing.
|
|
77
|
-
|
|
78
|
-
* Captures both `stdout` and `stderr` output and prints them in the console.
|
|
79
|
-
|
|
80
|
-
* Supports both **expressions (`eval`)** and **statements (`exec`)**.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
### Clickable History
|
|
84
|
-
|
|
85
|
-
* Hover previous commands to see them highlighted.
|
|
86
|
-
|
|
87
|
-
* Click to copy them back to the prompt for editing or re-execution.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
### Help Panel
|
|
91
|
-
|
|
92
|
-
* A resizable right-hand panel that displays Python documentation (help()) for any object.
|
|
93
|
-
|
|
94
|
-
* Opens when clicking ctrl+click on a function/method and can be closed with the "X" button.
|
|
95
|
-
|
|
96
|
-
* Scrollable and syntax-styled.
|
|
97
|
-
|
|
98
|
-
* Perfect for quick reference without leaving the console.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
### Easy Integration
|
|
102
|
-
|
|
103
|
-
* Automatically grabs **caller frame globals and locals** if not provided.
|
|
104
|
-
|
|
105
|
-
* Can be used standalone or embedded in larger CustomTkinter applications.
|
|
106
|
-
|
|
107
|
-
## Usage
|
|
108
|
-
|
|
109
|
-
```
|
|
110
|
-
import pysole
|
|
111
|
-
pysole.probe()
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
* Type Python commands in the `>>>` prompt and see live output.
|
|
115
|
-
|
|
116
|
-
## Keyboard Shortcuts
|
|
117
|
-
|
|
118
|
-
| Key | Action |
|
|
119
|
-
| --- | --- |
|
|
120
|
-
| `Enter` | Execute command (if complete) |
|
|
121
|
-
| `Shift+Enter` | Insert newline with auto-indent |
|
|
122
|
-
| `Tab` | Complete the current word / show suggestions |
|
|
123
|
-
| `Up/Down` | Navigate suggestion list |
|
|
124
|
-
| `Escape` | Hide suggestions |
|
|
125
|
-
| `Mouse Click` | Select previous command from history |
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
## Customization
|
|
129
|
-
|
|
130
|
-
* **Appearance mode**: Dark mode is default, but can be changed via files menu
|
|
131
|
-
|
|
132
|
-
* **Themes**: Pysole has multiple preconfigured themes. You can choose a theme via the Theme Picker, which updates the console colors and appearance. Preconfigured themes are loaded from themes.json and the selected theme is saved in settings.json so it persists across sessions.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
## License
|
|
137
|
-
|
|
138
|
-
MIT License – free to use, modify, and distribute.
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
liveconsole-1.6.1.dist-info/licenses/LICENSE,sha256=7dZ0zL72aGaFE0C9DxacOpnaSkC5jajhG6iL7lqhWmU,1064
|
|
2
|
-
pysole/__init__.py,sha256=SfaSBmFVSmhyf55UedBCqhi2Ss6Tre-BCKtZb3bSr2k,60
|
|
3
|
-
pysole/__main__.py,sha256=QvVFH8J2yzgQaF9MosQ6ajCW67uiRbYliePsTEUCIAg,82
|
|
4
|
-
pysole/commandHistory.py,sha256=xJtWbJ_vgJo2QGgaZJsApTOi_Hm8Yz0V9_zqQUCItj8,1084
|
|
5
|
-
pysole/helpTab.py,sha256=o0uSY-8sw7FuuBrt0FQwcgK6ljNVx3IgRnW7heZ6GvY,2454
|
|
6
|
-
pysole/liveConsole.py,sha256=lzS3dphAQ1i8pQC4E2FY-FyMMSKi-dAN0xr6XUgrNmo,168
|
|
7
|
-
pysole/mainConsole.py,sha256=9Kdjpe9rQzx3Nk9aBaECR3PlwAXszgRt4VfIJmVJnRw,13141
|
|
8
|
-
pysole/pysole.py,sha256=vTmPvS4nzapVudNPtf2JrnwBWB-x_n-XxBsr4jW7MK8,9230
|
|
9
|
-
pysole/settings.json,sha256=6wCdMlemV6PblPKeQQsIiCrGWPLDLOMFD3hLMVAXL24,687
|
|
10
|
-
pysole/styledTextbox.py,sha256=zpbaN0qX5FduhNyuWGU7y8Ll8J9p9YqczpSuRLo4PX0,2120
|
|
11
|
-
pysole/suggestionManager.py,sha256=EUFeCQoZnLS8EjPPNuZpzjY0BR07m2KP5TloyaMDVGY,6457
|
|
12
|
-
pysole/themes.json,sha256=2RG3ohn2ZBc2-h9bdooQZD6hW5pD41a5c2jdaKDA4MA,2385
|
|
13
|
-
pysole/utils.py,sha256=VN42ukHMJpOvGb7ZV-qhF_zRUt13hzJKV_uRn9t6580,155
|
|
14
|
-
liveconsole-1.6.1.dist-info/METADATA,sha256=ScK9vL5BuDsaRSoUZicUfWx2wDDqYlonVtJlH5Di_f0,4011
|
|
15
|
-
liveconsole-1.6.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
16
|
-
liveconsole-1.6.1.dist-info/entry_points.txt,sha256=qtvuJHcex4QqM97x_UawFWJYnfhQRl0jhqLcWRpnAGo,84
|
|
17
|
-
liveconsole-1.6.1.dist-info/top_level.txt,sha256=DlpA93ScJbRZcF8kGSc5YoO8Ntu1ib1_MEZMb4xea_Q,7
|
|
18
|
-
liveconsole-1.6.1.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
pysole
|
|
File without changes
|
|
File without changes
|
|
File without changes
|