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