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.
@@ -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
+ [![Watch the demo](Showcase/thumbnail.png)](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,,
@@ -0,0 +1,2 @@
1
+ liveConsole
2
+ pysole
pysole/__init__.py CHANGED
@@ -1 +1,4 @@
1
1
  from .pysole import probe, _standalone, InteractiveConsole
2
+ import sys
3
+
4
+ sys.modules['liveConsole'] = sys.modules[__name__]
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 replaceCurrentCommand(self, newCommand):
59
- """Replace the current command with new text."""
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.insert(start, newCommand)
68
- self.see("end")
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("break")
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
- # print(event.keysym)
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
- self.insert("end", text + "\n", tag)
300
- self.see("end")
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
- def getPromptPosition(self):
305
- """Get the position right after the prompt on current command line."""
306
- return(f"{self.currentCommandLine}.{self.PROMPT_LENGTH}")
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
- # Add new prompt after execution
353
- self.addPrompt()
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, theme=None, defaultSize=None, primaryPrompt=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("settings.json", "w") as f:
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).probe()
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
@@ -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": {
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"], font=(font["FONT"], font["FONT_SIZE"]))
20
- self.tag_configure("error", foreground=theme["ERROR"], font=(font["FONT"], font["FONT_SIZE"]))
21
- self.tag_configure("result", foreground=theme["RESULT"], font=(font["FONT"], font["FONT_SIZE"]))
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:
@@ -107,8 +107,11 @@ class CodeSuggestionManager:
107
107
  self.suggestionListbox.selection_set(0)
108
108
 
109
109
  # Position window near cursor
110
- self._positionSuggestionWindow()
111
- self.suggestionWindow.deiconify()
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