text-console 2.0.2__tar.gz → 2.0.3__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.
- text_console-2.0.3/PKG-INFO +83 -0
- {text_console-2.0.2 → text_console-2.0.3}/README.md +55 -24
- text_console-2.0.3/text_console/__version__.py +1 -0
- {text_console-2.0.2 → text_console-2.0.3}/text_console/text_console.py +474 -70
- text_console-2.0.3/text_console.egg-info/PKG-INFO +83 -0
- text_console-2.0.2/PKG-INFO +0 -58
- text_console-2.0.2/text_console/__version__.py +0 -1
- text_console-2.0.2/text_console.egg-info/PKG-INFO +0 -58
- {text_console-2.0.2 → text_console-2.0.3}/LICENSE +0 -0
- {text_console-2.0.2 → text_console-2.0.3}/setup.cfg +0 -0
- {text_console-2.0.2 → text_console-2.0.3}/setup.py +0 -0
- {text_console-2.0.2 → text_console-2.0.3}/text_console/__init__.py +0 -0
- {text_console-2.0.2 → text_console-2.0.3}/text_console/__main__.py +0 -0
- {text_console-2.0.2 → text_console-2.0.3}/text_console/history.py +0 -0
- {text_console-2.0.2 → text_console-2.0.3}/text_console.egg-info/SOURCES.txt +0 -0
- {text_console-2.0.2 → text_console-2.0.3}/text_console.egg-info/dependency_links.txt +0 -0
- {text_console-2.0.2 → text_console-2.0.3}/text_console.egg-info/entry_points.txt +0 -0
- {text_console-2.0.2 → text_console-2.0.3}/text_console.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: text_console
|
|
3
|
+
Version: 2.0.3
|
|
4
|
+
Summary: ('A customizable Tkinter-based text console widget, in which a user types in commands to be sent to the Python interpreter.',)
|
|
5
|
+
Home-page: https://github.com/Ircama/text_console
|
|
6
|
+
Author: Ircama
|
|
7
|
+
License: EUPL-1.2
|
|
8
|
+
Keywords: shell console tkinter
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1.2)
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Requires-Python: >3.6
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: classifier
|
|
21
|
+
Dynamic: description
|
|
22
|
+
Dynamic: description-content-type
|
|
23
|
+
Dynamic: home-page
|
|
24
|
+
Dynamic: keywords
|
|
25
|
+
Dynamic: license
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: requires-python
|
|
28
|
+
Dynamic: summary
|
|
29
|
+
|
|
30
|
+
# text_console
|
|
31
|
+
|
|
32
|
+
*text_console* is a customizable, Tkinter-based interactive shell widget that lets users type commands into a console pane and have them evaluated in your application’s Python environment. It supports arrows and command history. It’s designed for embedding in GUI apps, debugging, scripting, and educational tools as an API Playground.
|
|
33
|
+
|
|
34
|
+
It can also be used as a standalone Python command interpreter.
|
|
35
|
+
|
|
36
|
+
This program is similar to [wxPython Shell](https://github.com/wxWidgets/wxPython-Classic/blob/master/wx/py/shell.py), implemented using Tkinter.
|
|
37
|
+
|
|
38
|
+
## Key Features
|
|
39
|
+
|
|
40
|
+
- **Live code execution**
|
|
41
|
+
|
|
42
|
+
Send single- or multi-line Python code to the interpreter and see results immediately.
|
|
43
|
+
|
|
44
|
+
- **Integrated application context**
|
|
45
|
+
|
|
46
|
+
Access and modify your app’s variables and functions via the console_locals namespace.
|
|
47
|
+
|
|
48
|
+
- **Advanced editing**
|
|
49
|
+
- **Keyboard shortcuts:** all standard keyboard shortcut are allowed
|
|
50
|
+
- **Multiline editing:** Single line and multiline editing is allowed, also with copy/paste features. When pressing enter within an edited line, a popup appears to ask the requested action (execute the command, add a new line, abort). Shift-Enter is also allowed.
|
|
51
|
+
- **Prompt Protection:** The prompt area (`>>> ` or `... `) is protected. The cursor cannot move into or before the prompt, and editing actions (insertion, deletion) are blocked in the prompt area.
|
|
52
|
+
- **Smart Arrow Navigation:** Left and right arrow keys skip over prompt tags and any protected regions, ensuring the cursor only lands in editable areas. Arrow navigation also respects line boundaries and prompt positions.
|
|
53
|
+
- **Home/End Navigation:** The `Home` and `End` keys move the cursor to the beginning or end of the current line, but never into the prompt area.
|
|
54
|
+
- **Undo/Redo Support:** Full undo/redo support is enabled (`Ctrl+Z`/`Ctrl+Y`), with fine-grained control for character-by-character undo.
|
|
55
|
+
- **Tab and Shift+Tab:** Pressing `Tab` inserts four spaces. Pressing `Shift+Tab` removes up to four spaces.
|
|
56
|
+
- **Selection Awareness:** Editing and navigation actions are aware of text selection. For example, custom arrow key logic is bypassed when a selection is active.
|
|
57
|
+
- **Clear Console:** The console can be cleared with a single command, automatically restoring the prompt and positioning the cursor for new input.
|
|
58
|
+
|
|
59
|
+
- **Command history**
|
|
60
|
+
|
|
61
|
+
Navigate previous commands with ↑/↓ arrows; history is saved to a file you choose.
|
|
62
|
+
|
|
63
|
+
- **Cut/Copy/Paste/Clear**
|
|
64
|
+
|
|
65
|
+
Right-click context menu (and customizable via context_menu_items) for text editing.
|
|
66
|
+
|
|
67
|
+
- **Customizable UI**
|
|
68
|
+
|
|
69
|
+
The package provides flexibility to customize:
|
|
70
|
+
|
|
71
|
+
- `history_file`: Change the location of the history file
|
|
72
|
+
- `console_locals`: Add custom variables and functions to the console's namespace
|
|
73
|
+
- `context_menu_items`: Modify the right-click context menu
|
|
74
|
+
- `show_about_message`: Customize the about dialog content
|
|
75
|
+
- `show_help_content`: Customize the help window content
|
|
76
|
+
- `create_menu`: Override to completely customize the menu bar
|
|
77
|
+
|
|
78
|
+
- **Subclass-friendly**
|
|
79
|
+
|
|
80
|
+
Extend the TextConsole class and override any of the above to fit your needs.
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
Full information and usage details at the [text_console GitHub repository](https://github.com/Ircama/text_console).
|
|
@@ -1,47 +1,78 @@
|
|
|
1
1
|
# text_console
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
*text_console* is a customizable, Tkinter-based interactive shell widget that lets users type commands into a console pane and have them evaluated in your application’s Python environment. It supports arrows and command history. It’s designed for embedding in GUI apps, debugging, scripting, and educational tools as an API Playground.
|
|
4
4
|
|
|
5
|
-
It
|
|
5
|
+
It can also be used as a standalone Python command interpreter.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
This program is similar to [wxPython Shell](https://github.com/wxWidgets/wxPython-Classic/blob/master/wx/py/shell.py), implemented using Tkinter.
|
|
8
8
|
|
|
9
|
-
## Features
|
|
9
|
+
## Key Features
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
- Full multi-line commands support.
|
|
13
|
-
- Customizable history file.
|
|
14
|
-
- Context menu with options for Cut, Copy, Paste, and Clear.
|
|
15
|
-
- Extendable for additional functionality.
|
|
11
|
+
- **Live code execution**
|
|
16
12
|
|
|
17
|
-
|
|
13
|
+
Send single- or multi-line Python code to the interpreter and see results immediately.
|
|
18
14
|
|
|
19
|
-
-
|
|
20
|
-
- `console_locals`: Add custom variables and functions to the console's namespace
|
|
21
|
-
- `context_menu_items`: Modify the right-click context menu
|
|
22
|
-
- `show_about_message`: Customize the about dialog content
|
|
23
|
-
- `show_help_content`: Customize the help window content
|
|
24
|
-
- `create_menu`: Override to completely customize the menu bar
|
|
15
|
+
- **Integrated application context**
|
|
25
16
|
|
|
26
|
-
|
|
17
|
+
Access and modify your app’s variables and functions via the console_locals namespace.
|
|
27
18
|
|
|
28
|
-
|
|
19
|
+
- **Advanced editing**
|
|
20
|
+
- **Keyboard shortcuts:** all standard keyboard shortcut are allowed
|
|
21
|
+
- **Multiline editing:** Single line and multiline editing is allowed, also with copy/paste features. When pressing enter within an edited line, a popup appears to ask the requested action (execute the command, add a new line, abort). Shift-Enter is also allowed.
|
|
22
|
+
- **Prompt Protection:** The prompt area (`>>> ` or `... `) is protected. The cursor cannot move into or before the prompt, and editing actions (insertion, deletion) are blocked in the prompt area.
|
|
23
|
+
- **Smart Arrow Navigation:** Left and right arrow keys skip over prompt tags and any protected regions, ensuring the cursor only lands in editable areas. Arrow navigation also respects line boundaries and prompt positions.
|
|
24
|
+
- **Home/End Navigation:** The `Home` and `End` keys move the cursor to the beginning or end of the current line, but never into the prompt area.
|
|
25
|
+
- **Undo/Redo Support:** Full undo/redo support is enabled (`Ctrl+Z`/`Ctrl+Y`), with fine-grained control for character-by-character undo.
|
|
26
|
+
- **Tab and Shift+Tab:** Pressing `Tab` inserts four spaces. Pressing `Shift+Tab` removes up to four spaces.
|
|
27
|
+
- **Selection Awareness:** Editing and navigation actions are aware of text selection. For example, custom arrow key logic is bypassed when a selection is active.
|
|
28
|
+
- **Clear Console:** The console can be cleared with a single command, automatically restoring the prompt and positioning the cursor for new input.
|
|
29
|
+
|
|
30
|
+
- **Command history**
|
|
31
|
+
|
|
32
|
+
Navigate previous commands with ↑/↓ arrows; history is saved to a file you choose.
|
|
33
|
+
|
|
34
|
+
- **Cut/Copy/Paste/Clear**
|
|
35
|
+
|
|
36
|
+
Right-click context menu (and customizable via context_menu_items) for text editing.
|
|
37
|
+
|
|
38
|
+
- **Customizable UI**
|
|
39
|
+
|
|
40
|
+
The package provides flexibility to customize:
|
|
41
|
+
|
|
42
|
+
- `history_file`: Change the location of the history file
|
|
43
|
+
- `console_locals`: Add custom variables and functions to the console's namespace
|
|
44
|
+
- `context_menu_items`: Modify the right-click context menu
|
|
45
|
+
- `show_about_message`: Customize the about dialog content
|
|
46
|
+
- `show_help_content`: Customize the help window content
|
|
47
|
+
- `create_menu`: Override to completely customize the menu bar
|
|
48
|
+
|
|
49
|
+
- **Subclass-friendly**
|
|
29
50
|
|
|
30
|
-
|
|
51
|
+
Extend the TextConsole class and override any of the above to fit your needs.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
31
54
|
|
|
32
55
|
```bash
|
|
33
56
|
pip install text-console
|
|
34
57
|
```
|
|
35
58
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Navigate to the directory containing *setup.py*. Run:
|
|
59
|
+
## Playground
|
|
39
60
|
|
|
40
61
|
```
|
|
41
|
-
|
|
62
|
+
python -m text_console
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Available options:
|
|
66
|
+
|
|
42
67
|
```
|
|
68
|
+
Python Console [-h] [-V]
|
|
43
69
|
|
|
44
|
-
|
|
70
|
+
optional arguments:
|
|
71
|
+
-h, --help show this help message and exit
|
|
72
|
+
-V, --version Print version and exit
|
|
73
|
+
|
|
74
|
+
A customizable Tkinter-based text console widget.
|
|
75
|
+
```
|
|
45
76
|
|
|
46
77
|
### Basic usage with default settings
|
|
47
78
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.0.3"
|
|
@@ -66,7 +66,10 @@ class BaseTextConsole(tk.Text):
|
|
|
66
66
|
self._prompt1 = kw.pop('prompt1')
|
|
67
67
|
self._prompt2 = kw.pop('prompt2')
|
|
68
68
|
banner = kw.pop('banner', 'Python %s\n' % sys.version)
|
|
69
|
-
|
|
69
|
+
|
|
70
|
+
# Enable undo/redo
|
|
71
|
+
kw.setdefault('undo', True)
|
|
72
|
+
|
|
70
73
|
super().__init__(master, **kw)
|
|
71
74
|
|
|
72
75
|
# Initialize console with merged locals
|
|
@@ -81,7 +84,7 @@ class BaseTextConsole(tk.Text):
|
|
|
81
84
|
|
|
82
85
|
# Initialize history
|
|
83
86
|
self.history = History(self.history_file)
|
|
84
|
-
self._hist_item =
|
|
87
|
+
self._hist_item = len(self.history)
|
|
85
88
|
self._hist_match = ''
|
|
86
89
|
|
|
87
90
|
# Initialize settings
|
|
@@ -118,22 +121,42 @@ class BaseTextConsole(tk.Text):
|
|
|
118
121
|
foreground="green",
|
|
119
122
|
font=("Courier", font_size - 2)
|
|
120
123
|
)
|
|
121
|
-
self.tag_configure("
|
|
124
|
+
self.tag_configure("output", foreground="#00178c")
|
|
122
125
|
|
|
123
126
|
def setup_bindings(self):
|
|
124
127
|
"""Set up key bindings"""
|
|
125
|
-
self.bind('<
|
|
126
|
-
self.bind('<
|
|
127
|
-
self.bind('<KeyPress>', self.on_key_press)
|
|
128
|
-
self.bind('<KeyRelease>', self.on_key_release)
|
|
128
|
+
self.bind('<Shift-Return>', self.insert_line)
|
|
129
|
+
self.bind('<Control-Return>', self.go_to_end)
|
|
129
130
|
self.bind('<Tab>', self.on_tab)
|
|
131
|
+
self.bind("<Shift-Tab>", self.on_shift_tab)
|
|
130
132
|
self.bind('<Down>', self.on_down)
|
|
131
133
|
self.bind('<Up>', self.on_up)
|
|
134
|
+
self.bind("<Escape>", self.on_escape)
|
|
132
135
|
self.bind('<Return>', self.on_return)
|
|
133
136
|
self.bind('<BackSpace>', self.on_backspace)
|
|
134
137
|
self.bind('<Control-c>', self.on_ctrl_c)
|
|
135
138
|
self.bind('<<Paste>>', self.on_paste)
|
|
136
139
|
self.bind("<Button-3>", self.show_context_menu)
|
|
140
|
+
self.bind("<Control-z>", lambda e: self._safe_undo())
|
|
141
|
+
self.bind("<Control-y>", lambda e: self._safe_redo())
|
|
142
|
+
self.bind("<KeyRelease>", lambda e: self.edit_separator())
|
|
143
|
+
self.bind("<Left>", lambda e: self.after_idle(self._process_arrows, "Left"))
|
|
144
|
+
self.bind("<Right>", lambda e: self.after_idle(self._process_arrows, "Right"))
|
|
145
|
+
self.bind("<KeyPress>", self.on_key_press)
|
|
146
|
+
self.bind("<Home>", lambda e: self._move_to_line_start(e))
|
|
147
|
+
self.bind("<End>", lambda e: self._move_to_line_end(e))
|
|
148
|
+
|
|
149
|
+
def _safe_undo(self):
|
|
150
|
+
try:
|
|
151
|
+
self.edit_undo()
|
|
152
|
+
except tk.TclError:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
def _safe_redo(self):
|
|
156
|
+
try:
|
|
157
|
+
self.edit_redo()
|
|
158
|
+
except tk.TclError:
|
|
159
|
+
pass
|
|
137
160
|
|
|
138
161
|
def setup_context_menu(self):
|
|
139
162
|
"""Set up the context menu"""
|
|
@@ -153,7 +176,7 @@ class BaseTextConsole(tk.Text):
|
|
|
153
176
|
|
|
154
177
|
# File menu
|
|
155
178
|
file_menu = Menu(menu_bar, tearoff=0)
|
|
156
|
-
file_menu.add_command(label="Clear Console", command=self.
|
|
179
|
+
file_menu.add_command(label="Clear Console", command=self.clear)
|
|
157
180
|
if master != main:
|
|
158
181
|
file_menu.add_command(label="Close Window", command=master.destroy)
|
|
159
182
|
file_menu.add_command(label="Quit Application", command=self.quit)
|
|
@@ -245,10 +268,6 @@ class BaseTextConsole(tk.Text):
|
|
|
245
268
|
help_text.pack(fill="both", expand=True)
|
|
246
269
|
scrollbar.config(command=help_text.yview)
|
|
247
270
|
|
|
248
|
-
def clear_console(self):
|
|
249
|
-
"""Clear the text in the console."""
|
|
250
|
-
self.clear()
|
|
251
|
-
|
|
252
271
|
def show_context_menu(self, event):
|
|
253
272
|
"""Show the context menu at the cursor position."""
|
|
254
273
|
self.context_menu.post(event.x_root, event.y_root)
|
|
@@ -277,8 +296,10 @@ class BaseTextConsole(tk.Text):
|
|
|
277
296
|
def clear(self):
|
|
278
297
|
"""Clear all text from the console."""
|
|
279
298
|
self.delete("1.0", "end")
|
|
280
|
-
self.insert(
|
|
281
|
-
self.
|
|
299
|
+
self.insert('end', self._prompt1, 'prompt')
|
|
300
|
+
self.mark_set('input', 'end-1c')
|
|
301
|
+
self.edit_reset()
|
|
302
|
+
|
|
282
303
|
|
|
283
304
|
def dump_history(self):
|
|
284
305
|
"""Open a separate window with the output of the history."""
|
|
@@ -355,7 +376,7 @@ class BaseTextConsole(tk.Text):
|
|
|
355
376
|
# Fallback if widget not yet rendered
|
|
356
377
|
widget_width = max(100, (history_window.winfo_width() - 80) // 8)
|
|
357
378
|
|
|
358
|
-
# Find the longest command to determine if we need horizontal scrolling
|
|
379
|
+
# Find the longest command to determine if we need to use horizontal scrolling
|
|
359
380
|
max_command_length = 0
|
|
360
381
|
for command in self.history:
|
|
361
382
|
for line in str(command).split('\n'):
|
|
@@ -502,8 +523,36 @@ class BaseTextConsole(tk.Text):
|
|
|
502
523
|
if sel:
|
|
503
524
|
self.delete('sel.first', 'sel.last')
|
|
504
525
|
txt = self.clipboard_get()
|
|
505
|
-
|
|
506
|
-
|
|
526
|
+
|
|
527
|
+
# Check if input is blank (no user command present)
|
|
528
|
+
current_input = self.get('input', 'end-1c')
|
|
529
|
+
if not current_input.strip():
|
|
530
|
+
self.insert("insert", txt, "input")
|
|
531
|
+
self.insert_cmd(self.get("input", "end"))
|
|
532
|
+
else:
|
|
533
|
+
if '\n' not in current_input:
|
|
534
|
+
self.insert("insert", txt, "input")
|
|
535
|
+
else:
|
|
536
|
+
# Multiline: insert at cursor, preserving line breaks and adding prompt
|
|
537
|
+
cursor_index = self.index("insert")
|
|
538
|
+
line_idx, col_idx = map(int, cursor_index.split('.'))
|
|
539
|
+
prompt = self._prompt2 if hasattr(self, '_prompt2') else '... '
|
|
540
|
+
lines = txt.splitlines()
|
|
541
|
+
|
|
542
|
+
# Insert the first line at the cursor position
|
|
543
|
+
self.insert("insert", lines[0])
|
|
544
|
+
|
|
545
|
+
# For subsequent lines, insert a newline, prompt, and the line
|
|
546
|
+
for line in lines[1:]:
|
|
547
|
+
self.insert("insert", "\n")
|
|
548
|
+
self.insert("insert", prompt, "prompt")
|
|
549
|
+
self.insert("insert", line)
|
|
550
|
+
|
|
551
|
+
if len(lines) > 1:
|
|
552
|
+
# Move cursor to the end of the input multiline text
|
|
553
|
+
self.mark_set("insert", "end-1c")
|
|
554
|
+
self.see("insert")
|
|
555
|
+
|
|
507
556
|
return 'break'
|
|
508
557
|
|
|
509
558
|
def prompt(self, result=False):
|
|
@@ -513,29 +562,88 @@ class BaseTextConsole(tk.Text):
|
|
|
513
562
|
else:
|
|
514
563
|
self.insert('end', self._prompt1, 'prompt')
|
|
515
564
|
self.mark_set('input', 'end-1c')
|
|
565
|
+
self.edit_reset()
|
|
516
566
|
|
|
517
|
-
def
|
|
518
|
-
"""
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
567
|
+
def insert_prompt(self, prompt_type="primary", index="insert"):
|
|
568
|
+
"""
|
|
569
|
+
Insert the prompt at the given index with the correct tag.
|
|
570
|
+
prompt_type: "primary" for >>>, "secondary" for ...
|
|
571
|
+
"""
|
|
572
|
+
if prompt_type == "primary":
|
|
573
|
+
self.insert(index, self._prompt1, "prompt")
|
|
574
|
+
self.edit_reset()
|
|
575
|
+
elif prompt_type == "secondary":
|
|
576
|
+
self.insert(index, self._prompt2, "prompt")
|
|
526
577
|
|
|
527
|
-
def
|
|
528
|
-
"""
|
|
529
|
-
if
|
|
530
|
-
|
|
531
|
-
|
|
578
|
+
def is_command_edited(self):
|
|
579
|
+
"""
|
|
580
|
+
Returns True if the current input (with prompts removed) is different from the current history entry.
|
|
581
|
+
"""
|
|
582
|
+
# Get the raw input text
|
|
583
|
+
current_input = self.get('input', 'end-1c')
|
|
584
|
+
# Remove all prompts (">>> " and "... ") from the beginning of each line
|
|
585
|
+
lines = current_input.splitlines()
|
|
586
|
+
cleaned_lines = []
|
|
587
|
+
for line in lines:
|
|
588
|
+
if line.startswith(self._prompt1):
|
|
589
|
+
cleaned_lines.append(line[len(self._prompt1):])
|
|
590
|
+
elif line.startswith(self._prompt2):
|
|
591
|
+
cleaned_lines.append(line[len(self._prompt2):])
|
|
592
|
+
else:
|
|
593
|
+
cleaned_lines.append(line)
|
|
594
|
+
cleaned_input = '\n'.join(cleaned_lines)
|
|
595
|
+
|
|
596
|
+
# If _hist_item is None or out of range, treat as blank/new input
|
|
597
|
+
if getattr(self, '_hist_item', None) is not None and 0 <= self._hist_item < len(self.history):
|
|
598
|
+
history_entry = self.history[self._hist_item]
|
|
532
599
|
else:
|
|
533
|
-
|
|
534
|
-
|
|
600
|
+
history_entry = ''
|
|
601
|
+
return cleaned_input.strip() != history_entry.strip()
|
|
602
|
+
|
|
603
|
+
def flash_prompt_warning(self, duration=200):
|
|
604
|
+
"""
|
|
605
|
+
Temporarily reverse the foreground and background colors of the prompt tag as a warning.
|
|
606
|
+
"""
|
|
607
|
+
# Get current colors
|
|
608
|
+
self.tag_configure("prompt", background="red")
|
|
609
|
+
# Restore after duration
|
|
610
|
+
self.after(duration, lambda: self.tag_configure("prompt", background="white"))
|
|
611
|
+
|
|
612
|
+
def on_escape(self, event=None):
|
|
613
|
+
"""
|
|
614
|
+
When pressing Esc, jump to the last blank command (empty input) in history.
|
|
615
|
+
"""
|
|
616
|
+
"""
|
|
617
|
+
if self.is_command_edited():
|
|
618
|
+
self.flash_prompt_warning()
|
|
619
|
+
return "break"
|
|
620
|
+
"""
|
|
621
|
+
self.edit_separator()
|
|
622
|
+
line = self._hist_match
|
|
623
|
+
self._hist_item = len(self.history)
|
|
624
|
+
|
|
625
|
+
self._hist_item = len(self.history)
|
|
626
|
+
self.delete('input', 'end')
|
|
627
|
+
self.insert('insert', line)
|
|
628
|
+
self.edit_reset()
|
|
629
|
+
|
|
630
|
+
return 'break'
|
|
535
631
|
|
|
536
632
|
def on_up(self, event):
|
|
537
633
|
"""Handle up arrow key press: navigate history only from first line"""
|
|
634
|
+
try:
|
|
635
|
+
self.index("sel.first")
|
|
636
|
+
# There is a selection, do nothing
|
|
637
|
+
except tk.TclError:
|
|
638
|
+
# No selection
|
|
639
|
+
if self.tag_names("insert"):
|
|
640
|
+
self.edit_reset()
|
|
641
|
+
return "break"
|
|
642
|
+
self.edit_separator()
|
|
538
643
|
if self.compare('insert linestart', '==', 'input linestart'):
|
|
644
|
+
if self.is_command_edited():
|
|
645
|
+
self.flash_prompt_warning()
|
|
646
|
+
return "break"
|
|
539
647
|
# Get current input line for matching
|
|
540
648
|
first_line_input = self.get('input', 'insert')
|
|
541
649
|
|
|
@@ -575,6 +683,7 @@ class BaseTextConsole(tk.Text):
|
|
|
575
683
|
# No matches at all, restore to end position
|
|
576
684
|
self._hist_item = len(self.history)
|
|
577
685
|
|
|
686
|
+
self.edit_reset()
|
|
578
687
|
return 'break'
|
|
579
688
|
|
|
580
689
|
# Allow normal movement within multiline input
|
|
@@ -582,7 +691,19 @@ class BaseTextConsole(tk.Text):
|
|
|
582
691
|
|
|
583
692
|
def on_down(self, event):
|
|
584
693
|
"""Handle down arrow key press: navigate history only from last line"""
|
|
694
|
+
try:
|
|
695
|
+
self.index("sel.first")
|
|
696
|
+
# There is a selection, do nothing
|
|
697
|
+
except tk.TclError:
|
|
698
|
+
# No selection
|
|
699
|
+
if self.tag_names("insert"):
|
|
700
|
+
self.edit_reset()
|
|
701
|
+
return "break"
|
|
702
|
+
self.edit_separator()
|
|
585
703
|
if self.compare('insert lineend', '==', 'end-1c'):
|
|
704
|
+
if self.is_command_edited():
|
|
705
|
+
self.flash_prompt_warning()
|
|
706
|
+
return "break"
|
|
586
707
|
line = self._hist_match
|
|
587
708
|
self._hist_item += 1
|
|
588
709
|
|
|
@@ -600,12 +721,48 @@ class BaseTextConsole(tk.Text):
|
|
|
600
721
|
self.delete('input', 'end')
|
|
601
722
|
self.insert('insert', line)
|
|
602
723
|
|
|
724
|
+
self.edit_reset()
|
|
603
725
|
return 'break'
|
|
604
726
|
# Else: allow normal movement within multiline
|
|
605
727
|
return
|
|
606
728
|
|
|
729
|
+
def on_shift_tab(self, event):
|
|
730
|
+
"""
|
|
731
|
+
Move the cursor back by up to 4 spaces if possible (like un-indenting),
|
|
732
|
+
but do not move back if at the beginning of a line after the prompt.
|
|
733
|
+
"""
|
|
734
|
+
cursor_index = self.index("insert")
|
|
735
|
+
line_start = self.index(f"{cursor_index} linestart")
|
|
736
|
+
line_text = self.get(line_start, f"{line_start} lineend")
|
|
737
|
+
|
|
738
|
+
# Determine prompt length for this line
|
|
739
|
+
if line_text.startswith(self._prompt1):
|
|
740
|
+
prompt_len = len(self._prompt1)
|
|
741
|
+
elif line_text.startswith(self._prompt2):
|
|
742
|
+
prompt_len = len(self._prompt2)
|
|
743
|
+
else:
|
|
744
|
+
prompt_len = 0
|
|
745
|
+
|
|
746
|
+
prompt_end = f"{line_start}+{prompt_len}c"
|
|
747
|
+
# If at the beginning of input (just after prompt), do nothing
|
|
748
|
+
if self.compare(cursor_index, "==", prompt_end):
|
|
749
|
+
return "break"
|
|
750
|
+
|
|
751
|
+
before_cursor = self.get(line_start, cursor_index)
|
|
752
|
+
# Count spaces before cursor (up to 4)
|
|
753
|
+
spaces = 0
|
|
754
|
+
for c in reversed(before_cursor):
|
|
755
|
+
if c == " " and spaces < 4:
|
|
756
|
+
spaces += 1
|
|
757
|
+
else:
|
|
758
|
+
break
|
|
759
|
+
if spaces > 0:
|
|
760
|
+
self.delete(f"{cursor_index} -{spaces}c", cursor_index)
|
|
761
|
+
return "break"
|
|
762
|
+
|
|
607
763
|
def on_tab(self, event):
|
|
608
764
|
"""Handle tab key press"""
|
|
765
|
+
self.edit_separator()
|
|
609
766
|
if self.compare('insert', '<', 'input'):
|
|
610
767
|
self.mark_set('insert', 'input lineend')
|
|
611
768
|
return "break"
|
|
@@ -624,44 +781,219 @@ class BaseTextConsole(tk.Text):
|
|
|
624
781
|
self.insert('insert', ' ')
|
|
625
782
|
return "break"
|
|
626
783
|
|
|
627
|
-
def
|
|
628
|
-
"""
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
784
|
+
def go_to_end(self, event):
|
|
785
|
+
"""Move the cursor to the end of the last line of input."""
|
|
786
|
+
self.edit_separator()
|
|
787
|
+
self.mark_set('insert', 'end-1c')
|
|
788
|
+
return 'break'
|
|
789
|
+
|
|
790
|
+
def _move_to_line_start(self, event):
|
|
791
|
+
index = self.index("insert linestart")
|
|
792
|
+
self.mark_set("insert", index)
|
|
793
|
+
self.see(index)
|
|
794
|
+
return "break"
|
|
795
|
+
|
|
796
|
+
def _move_to_line_end(self, event):
|
|
797
|
+
index = self.index("insert lineend")
|
|
798
|
+
self.mark_set("insert", index)
|
|
799
|
+
self.see(index)
|
|
800
|
+
return "break"
|
|
637
801
|
|
|
638
802
|
def on_return(self, event=None):
|
|
639
|
-
"""Handle Return key press"""
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
if not self.compare('insert', '==', 'end-1c'):
|
|
650
|
-
# We're in the middle of the multiline input, just add a newline
|
|
651
|
-
self.insert('insert', '\n')
|
|
652
|
-
# Add appropriate prompt for continuation
|
|
653
|
-
self.insert('insert', self._prompt2, 'prompt')
|
|
654
|
-
return 'break'
|
|
655
|
-
|
|
656
|
-
# We're at the end, so execute the command
|
|
803
|
+
"""Handle Return key press with modal for mid-line or multiline editing."""
|
|
804
|
+
self.edit_separator()
|
|
805
|
+
input_start = self.index('input')
|
|
806
|
+
input_end = self.index('end-1c')
|
|
807
|
+
insert_pos = self.index('insert')
|
|
808
|
+
full_text = self.get('input', 'end-1c')
|
|
809
|
+
|
|
810
|
+
# Avoid popup if the command is blank (only whitespace or empty)
|
|
811
|
+
if not full_text.strip():
|
|
812
|
+
self.mark_set('insert', 'end-1c')
|
|
657
813
|
self.eval_current(True)
|
|
658
814
|
self.see('end')
|
|
815
|
+
self._hist_item = len(self.history)
|
|
659
816
|
self.history.save()
|
|
817
|
+
return 'break'
|
|
818
|
+
|
|
819
|
+
lines = full_text.splitlines()
|
|
820
|
+
is_multiline = len(lines) > 1
|
|
821
|
+
|
|
822
|
+
# Always show modal for multiline, or for single line if not at end
|
|
823
|
+
if is_multiline or not self.compare('insert', '==', 'end-1c'):
|
|
824
|
+
# Gather cursor info
|
|
825
|
+
cursor_index = self.index('insert')
|
|
826
|
+
cursor_line, cursor_col = map(int, cursor_index.split('.'))
|
|
827
|
+
input_line_start = int(input_start.split('.')[0])
|
|
828
|
+
rel_line = cursor_line - input_line_start
|
|
829
|
+
|
|
830
|
+
# Prepare display lines (no marker, just dual color)
|
|
831
|
+
display_lines = []
|
|
832
|
+
for i, line in enumerate(lines):
|
|
833
|
+
if i == 0:
|
|
834
|
+
# Add the prompt to the first line
|
|
835
|
+
display_lines.append(self._prompt1 + line)
|
|
836
|
+
else:
|
|
837
|
+
display_lines.append(line)
|
|
838
|
+
display_text = '\n'.join(display_lines)
|
|
839
|
+
|
|
840
|
+
def show_modal():
|
|
841
|
+
modal = tk.Toplevel(self)
|
|
842
|
+
modal.title("Edit Command")
|
|
843
|
+
modal.transient(self)
|
|
844
|
+
modal.grab_set()
|
|
845
|
+
|
|
846
|
+
frame = tk.Frame(modal)
|
|
847
|
+
frame.pack(fill="both", expand=True)
|
|
848
|
+
|
|
849
|
+
# Create the Text widget with no wrapping for horizontal scrolling
|
|
850
|
+
text_widget = tk.Text(frame, wrap="none")
|
|
851
|
+
text_widget.grid(row=0, column=0, sticky="nsew")
|
|
852
|
+
|
|
853
|
+
# Create vertical and horizontal scrollbars
|
|
854
|
+
yscroll = tk.Scrollbar(frame, orient="vertical", command=text_widget.yview)
|
|
855
|
+
xscroll = tk.Scrollbar(frame, orient="horizontal", command=text_widget.xview)
|
|
856
|
+
yscroll.grid(row=0, column=1, sticky="ns")
|
|
857
|
+
xscroll.grid(row=1, column=0, sticky="ew")
|
|
858
|
+
|
|
859
|
+
# Configure the Text widget to use the scrollbars
|
|
860
|
+
text_widget.configure(yscrollcommand=yscroll.set, xscrollcommand=xscroll.set)
|
|
861
|
+
|
|
862
|
+
# Make the frame expandable
|
|
863
|
+
frame.rowconfigure(0, weight=1)
|
|
864
|
+
frame.columnconfigure(0, weight=1)
|
|
865
|
+
|
|
866
|
+
text_widget.insert("1.0", display_text)
|
|
867
|
+
text_widget.focus_set()
|
|
868
|
+
|
|
869
|
+
# Dual color for the edited line
|
|
870
|
+
# Determine if editing the first line of the input
|
|
871
|
+
if rel_line == 0:
|
|
872
|
+
prompt_length = len(self._prompt1)
|
|
873
|
+
cursor_col_in_input = max(0, cursor_col - prompt_length)
|
|
874
|
+
# Add prompt length back for tag indices in the modal
|
|
875
|
+
before_cursor_idx = f"{rel_line+1}.0"
|
|
876
|
+
cursor_idx = f"{rel_line+1}.{cursor_col_in_input + prompt_length}"
|
|
877
|
+
else:
|
|
878
|
+
before_cursor_idx = f"{rel_line+1}.0"
|
|
879
|
+
cursor_idx = f"{rel_line+1}.{cursor_col}"
|
|
880
|
+
end_idx = f"{rel_line+1}.end"
|
|
881
|
+
|
|
882
|
+
# Center the edited line in the text widget
|
|
883
|
+
def center_line_in_text_widget(text_widget, rel_line1):
|
|
884
|
+
def do_scroll():
|
|
885
|
+
if not text_widget.winfo_exists():
|
|
886
|
+
return # Widget destroyed
|
|
887
|
+
dline = text_widget.dlineinfo("1.0")
|
|
888
|
+
if not dline:
|
|
889
|
+
text_widget.after(20, do_scroll)
|
|
890
|
+
return
|
|
891
|
+
line_height = dline[3]
|
|
892
|
+
visible_lines = int(text_widget.winfo_height() // line_height)
|
|
893
|
+
total_lines = int(text_widget.index("end-1c").split('.')[0])
|
|
894
|
+
# Clamp rel_line to valid range
|
|
895
|
+
rel_line = max(1, min(rel_line1, total_lines))
|
|
896
|
+
first_visible = max(rel_line - visible_lines // 2, 1)
|
|
897
|
+
yview_value = (first_visible - 1) / max(total_lines - visible_lines + 1, 1)
|
|
898
|
+
text_widget.yview_moveto(yview_value)
|
|
899
|
+
text_widget.after(0, do_scroll)
|
|
900
|
+
center_line_in_text_widget(text_widget, rel_line)
|
|
901
|
+
|
|
902
|
+
text_widget.tag_add("before_cursor", before_cursor_idx, cursor_idx)
|
|
903
|
+
text_widget.tag_configure("before_cursor", background="yellow")
|
|
904
|
+
text_widget.tag_add("after_cursor", cursor_idx, end_idx)
|
|
905
|
+
text_widget.tag_configure("after_cursor", background="lightgreen")
|
|
906
|
+
text_widget.config(state="disabled")
|
|
907
|
+
|
|
908
|
+
# Escape closes modal
|
|
909
|
+
modal.bind("<Escape>", lambda e: modal.destroy())
|
|
910
|
+
|
|
911
|
+
# Button actions
|
|
912
|
+
def confirm_and_execute():
|
|
913
|
+
modal.destroy()
|
|
914
|
+
self.mark_set('insert', 'end-1c')
|
|
915
|
+
self.eval_current(True)
|
|
916
|
+
self.see('end')
|
|
917
|
+
self.history.save()
|
|
918
|
+
|
|
919
|
+
def add_newline_here():
|
|
920
|
+
modal.destroy()
|
|
921
|
+
self.insert('insert', '\n')
|
|
922
|
+
self.insert('insert', self._prompt2, 'prompt')
|
|
923
|
+
self.see('insert')
|
|
924
|
+
total_lines = int(self.index('end-1c').split('.')[0])
|
|
925
|
+
current_line = int(self.index('insert').split('.')[0])
|
|
926
|
+
center_frac = max(0, (current_line - int(self['height']) // 2) / max(1, total_lines))
|
|
927
|
+
self.yview_moveto(center_frac)
|
|
928
|
+
|
|
929
|
+
def remove_current_line():
|
|
930
|
+
modal.destroy()
|
|
931
|
+
line_start = f"{cursor_line}.0"
|
|
932
|
+
line_end = f"{cursor_line}.end+1c"
|
|
933
|
+
self.delete(line_start, line_end)
|
|
934
|
+
|
|
935
|
+
def do_nothing():
|
|
936
|
+
modal.destroy()
|
|
937
|
+
|
|
938
|
+
btn_frame = tk.Frame(modal)
|
|
939
|
+
btn_frame.pack(pady=(0, 10))
|
|
940
|
+
|
|
941
|
+
confirm_btn = tk.Button(btn_frame, text="Confirm & Execute", command=confirm_and_execute)
|
|
942
|
+
confirm_btn.pack(side="left", padx=5)
|
|
943
|
+
confirm_btn.focus_set() # Set initial focus
|
|
944
|
+
confirm_btn.bind("<Return>", lambda e: confirm_and_execute())
|
|
945
|
+
|
|
946
|
+
add_btn = tk.Button(btn_frame, text="Add New Line Here", command=add_newline_here)
|
|
947
|
+
add_btn.pack(side="left", padx=5)
|
|
948
|
+
add_btn.bind("<Return>", lambda e: add_newline_here())
|
|
949
|
+
|
|
950
|
+
remove_btn = tk.Button(btn_frame, text="Remove This Line", command=remove_current_line)
|
|
951
|
+
remove_btn.pack(side="left", padx=5)
|
|
952
|
+
remove_btn.bind("<Return>", lambda e: remove_current_line())
|
|
953
|
+
|
|
954
|
+
def discard_everything():
|
|
955
|
+
modal.destroy()
|
|
956
|
+
self.on_escape() # Or your logic to clear input and reset state
|
|
957
|
+
|
|
958
|
+
discard_btn = tk.Button(btn_frame, text="Discard Everything", command=discard_everything)
|
|
959
|
+
discard_btn.pack(side="left", padx=5)
|
|
960
|
+
|
|
961
|
+
nothing_btn = tk.Button(btn_frame, text="Do Nothing", command=do_nothing)
|
|
962
|
+
nothing_btn.pack(side="left", padx=5)
|
|
963
|
+
nothing_btn.bind("<Return>", lambda e: do_nothing())
|
|
964
|
+
|
|
965
|
+
# After creating and packing the buttons:
|
|
966
|
+
buttons = [confirm_btn, add_btn, remove_btn, discard_btn, nothing_btn]
|
|
967
|
+
|
|
968
|
+
def focus_next(event):
|
|
969
|
+
idx = buttons.index(event.widget)
|
|
970
|
+
buttons[(idx + 1) % len(buttons)].focus_set()
|
|
971
|
+
return "break"
|
|
972
|
+
|
|
973
|
+
def focus_prev(event):
|
|
974
|
+
idx = buttons.index(event.widget)
|
|
975
|
+
buttons[(idx - 1) % len(buttons)].focus_set()
|
|
976
|
+
return "break"
|
|
977
|
+
|
|
978
|
+
for btn in buttons:
|
|
979
|
+
btn.bind("<Right>", focus_next)
|
|
980
|
+
btn.bind("<Left>", focus_prev)
|
|
981
|
+
|
|
982
|
+
modal.wait_window()
|
|
983
|
+
|
|
984
|
+
self.after(10, show_modal)
|
|
985
|
+
return 'break'
|
|
986
|
+
|
|
987
|
+
# Default: execute the command
|
|
988
|
+
self.eval_current(True)
|
|
989
|
+
self.see('end')
|
|
990
|
+
self.history.save()
|
|
660
991
|
return 'break'
|
|
661
992
|
|
|
662
|
-
def
|
|
993
|
+
def insert_line(self, event=None):
|
|
663
994
|
"""Handle Ctrl+Return key press"""
|
|
664
|
-
self.
|
|
995
|
+
self.edit_separator()
|
|
996
|
+
self.insert('insert', '\n' + self._prompt2, "prompt" )
|
|
665
997
|
return 'break'
|
|
666
998
|
|
|
667
999
|
def on_backspace(self, event):
|
|
@@ -692,13 +1024,13 @@ class BaseTextConsole(tk.Text):
|
|
|
692
1024
|
|
|
693
1025
|
# Record current insert position as new input mark
|
|
694
1026
|
input_index = self.index('insert')
|
|
695
|
-
self.insert('insert', lines[0][indent:]
|
|
1027
|
+
self.insert('insert', lines[0][indent:])
|
|
696
1028
|
|
|
697
1029
|
for line in lines[1:]:
|
|
698
1030
|
line = line[indent:]
|
|
699
|
-
self.insert('insert', '\n'
|
|
1031
|
+
self.insert('insert', '\n')
|
|
700
1032
|
self.prompt(True)
|
|
701
|
-
self.insert('insert', line
|
|
1033
|
+
self.insert('insert', line)
|
|
702
1034
|
|
|
703
1035
|
# Set the 'input' mark at the correct place (start of inserted block)
|
|
704
1036
|
self.mark_set('input', input_index)
|
|
@@ -709,6 +1041,7 @@ class BaseTextConsole(tk.Text):
|
|
|
709
1041
|
index = self.index('input')
|
|
710
1042
|
lines = self.get('input', 'insert lineend').splitlines() # commands to execute
|
|
711
1043
|
self.mark_set('insert', 'insert lineend')
|
|
1044
|
+
self._hist_item = len(self.history) # set history item to the end
|
|
712
1045
|
|
|
713
1046
|
if lines: # there is code to execute
|
|
714
1047
|
# remove prompts
|
|
@@ -717,7 +1050,7 @@ class BaseTextConsole(tk.Text):
|
|
|
717
1050
|
if l.endswith('?'):
|
|
718
1051
|
lines[i] = 'help(%s)' % l[:-1]
|
|
719
1052
|
cmds = '\n'.join(lines)
|
|
720
|
-
self.insert('insert', '\n')
|
|
1053
|
+
self.insert('insert', '\n', "output")
|
|
721
1054
|
out = StringIO() # command output
|
|
722
1055
|
err = StringIO() # command error traceback
|
|
723
1056
|
with redirect_stderr(err): # redirect error traceback to err
|
|
@@ -755,7 +1088,7 @@ class BaseTextConsole(tk.Text):
|
|
|
755
1088
|
self.mark_set('input', 'end')
|
|
756
1089
|
self.see('end')
|
|
757
1090
|
if not res and self.compare('insert linestart', '>', 'insert'):
|
|
758
|
-
self.insert('insert', '\n')
|
|
1091
|
+
self.insert('insert', '\n', "output")
|
|
759
1092
|
self.prompt(res)
|
|
760
1093
|
|
|
761
1094
|
# Handle auto-indentation logic
|
|
@@ -765,7 +1098,7 @@ class BaseTextConsole(tk.Text):
|
|
|
765
1098
|
line = lines[-1].strip()
|
|
766
1099
|
if line and line[-1] == ':':
|
|
767
1100
|
indent = indent + ' '
|
|
768
|
-
self.insert('insert', indent)
|
|
1101
|
+
self.insert('insert', indent, "output")
|
|
769
1102
|
# For complete commands (res is False), don't auto-indent - start fresh
|
|
770
1103
|
|
|
771
1104
|
self.see('end')
|
|
@@ -782,9 +1115,80 @@ class BaseTextConsole(tk.Text):
|
|
|
782
1115
|
out.close()
|
|
783
1116
|
err.close()
|
|
784
1117
|
else:
|
|
785
|
-
self.insert('insert', '\n')
|
|
1118
|
+
self.insert('insert', '\n', "output")
|
|
786
1119
|
self.prompt()
|
|
787
1120
|
|
|
1121
|
+
def on_key_press(self, event):
|
|
1122
|
+
"""
|
|
1123
|
+
Prevent character insertion if the cursor is over a character with any tag (e.g., prompt).
|
|
1124
|
+
"""
|
|
1125
|
+
# Only block printable characters (not navigation, etc.)
|
|
1126
|
+
if event.char and event.char.isprintable():
|
|
1127
|
+
cursor_index = self.index("insert")
|
|
1128
|
+
tags = self.tag_names(cursor_index)
|
|
1129
|
+
if tags:
|
|
1130
|
+
return "break"
|
|
1131
|
+
|
|
1132
|
+
def _process_arrows(self, direction):
|
|
1133
|
+
"""
|
|
1134
|
+
Handles Left and Right arrow navigation.
|
|
1135
|
+
Does nothing if there is an active selection.
|
|
1136
|
+
"""
|
|
1137
|
+
self.edit_separator()
|
|
1138
|
+
try:
|
|
1139
|
+
sel_start = self.index("sel.first")
|
|
1140
|
+
sel_end = self.index("sel.last")
|
|
1141
|
+
# If selection is active, do not process arrow keys
|
|
1142
|
+
return
|
|
1143
|
+
except tk.TclError:
|
|
1144
|
+
pass # No selection, proceed as normal
|
|
1145
|
+
|
|
1146
|
+
cursor_index = self.index("insert")
|
|
1147
|
+
line_start = self.index(f"{cursor_index} linestart")
|
|
1148
|
+
line_end = self.index(f"{line_start} lineend")
|
|
1149
|
+
|
|
1150
|
+
if direction == "Left":
|
|
1151
|
+
idx = cursor_index
|
|
1152
|
+
while self.compare(idx, ">", line_start):
|
|
1153
|
+
tags = self.tag_names(idx)
|
|
1154
|
+
if not tags:
|
|
1155
|
+
self.mark_set("insert", idx)
|
|
1156
|
+
return
|
|
1157
|
+
idx = self.index(f"{idx} -1c")
|
|
1158
|
+
# If we reach the start of the line, check the last character of the previous line
|
|
1159
|
+
if self.compare(idx, "==", line_start):
|
|
1160
|
+
prev_line_num = int(line_start.split('.')[0]) - 1
|
|
1161
|
+
if prev_line_num >= 1:
|
|
1162
|
+
prev_line_start = f"{prev_line_num}.0"
|
|
1163
|
+
prev_line_end = self.index(f"{prev_line_start} lineend")
|
|
1164
|
+
# Check if the last character of the previous line is not tagged
|
|
1165
|
+
last_char_idx = self.index(f"{prev_line_end} -1c")
|
|
1166
|
+
tags = self.tag_names(last_char_idx)
|
|
1167
|
+
if not tags:
|
|
1168
|
+
self.mark_set("insert", prev_line_end)
|
|
1169
|
+
return
|
|
1170
|
+
# Otherwise, scan forward to find the first non-tagged character (existing behaviour)
|
|
1171
|
+
scan_idx = line_start
|
|
1172
|
+
line_end = self.index(f"{line_start} lineend")
|
|
1173
|
+
while self.compare(scan_idx, "<", line_end):
|
|
1174
|
+
tags = self.tag_names(scan_idx)
|
|
1175
|
+
if not tags:
|
|
1176
|
+
self.mark_set("insert", scan_idx)
|
|
1177
|
+
return
|
|
1178
|
+
scan_idx = self.index(f"{scan_idx} +1c")
|
|
1179
|
+
self.mark_set("insert", line_end)
|
|
1180
|
+
|
|
1181
|
+
elif direction == "Right":
|
|
1182
|
+
idx = cursor_index
|
|
1183
|
+
while self.compare(idx, "<", line_end):
|
|
1184
|
+
tags = self.tag_names(idx)
|
|
1185
|
+
if not tags:
|
|
1186
|
+
self.mark_set("insert", idx)
|
|
1187
|
+
return
|
|
1188
|
+
idx = self.index(f"{idx} +1c")
|
|
1189
|
+
# If we reach the end of the line, set cursor there
|
|
1190
|
+
self.mark_set("insert", line_end)
|
|
1191
|
+
|
|
788
1192
|
|
|
789
1193
|
class TextConsole(BaseTextConsole):
|
|
790
1194
|
"""Default implementation of the console"""
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: text_console
|
|
3
|
+
Version: 2.0.3
|
|
4
|
+
Summary: ('A customizable Tkinter-based text console widget, in which a user types in commands to be sent to the Python interpreter.',)
|
|
5
|
+
Home-page: https://github.com/Ircama/text_console
|
|
6
|
+
Author: Ircama
|
|
7
|
+
License: EUPL-1.2
|
|
8
|
+
Keywords: shell console tkinter
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1.2)
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Requires-Python: >3.6
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: classifier
|
|
21
|
+
Dynamic: description
|
|
22
|
+
Dynamic: description-content-type
|
|
23
|
+
Dynamic: home-page
|
|
24
|
+
Dynamic: keywords
|
|
25
|
+
Dynamic: license
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: requires-python
|
|
28
|
+
Dynamic: summary
|
|
29
|
+
|
|
30
|
+
# text_console
|
|
31
|
+
|
|
32
|
+
*text_console* is a customizable, Tkinter-based interactive shell widget that lets users type commands into a console pane and have them evaluated in your application’s Python environment. It supports arrows and command history. It’s designed for embedding in GUI apps, debugging, scripting, and educational tools as an API Playground.
|
|
33
|
+
|
|
34
|
+
It can also be used as a standalone Python command interpreter.
|
|
35
|
+
|
|
36
|
+
This program is similar to [wxPython Shell](https://github.com/wxWidgets/wxPython-Classic/blob/master/wx/py/shell.py), implemented using Tkinter.
|
|
37
|
+
|
|
38
|
+
## Key Features
|
|
39
|
+
|
|
40
|
+
- **Live code execution**
|
|
41
|
+
|
|
42
|
+
Send single- or multi-line Python code to the interpreter and see results immediately.
|
|
43
|
+
|
|
44
|
+
- **Integrated application context**
|
|
45
|
+
|
|
46
|
+
Access and modify your app’s variables and functions via the console_locals namespace.
|
|
47
|
+
|
|
48
|
+
- **Advanced editing**
|
|
49
|
+
- **Keyboard shortcuts:** all standard keyboard shortcut are allowed
|
|
50
|
+
- **Multiline editing:** Single line and multiline editing is allowed, also with copy/paste features. When pressing enter within an edited line, a popup appears to ask the requested action (execute the command, add a new line, abort). Shift-Enter is also allowed.
|
|
51
|
+
- **Prompt Protection:** The prompt area (`>>> ` or `... `) is protected. The cursor cannot move into or before the prompt, and editing actions (insertion, deletion) are blocked in the prompt area.
|
|
52
|
+
- **Smart Arrow Navigation:** Left and right arrow keys skip over prompt tags and any protected regions, ensuring the cursor only lands in editable areas. Arrow navigation also respects line boundaries and prompt positions.
|
|
53
|
+
- **Home/End Navigation:** The `Home` and `End` keys move the cursor to the beginning or end of the current line, but never into the prompt area.
|
|
54
|
+
- **Undo/Redo Support:** Full undo/redo support is enabled (`Ctrl+Z`/`Ctrl+Y`), with fine-grained control for character-by-character undo.
|
|
55
|
+
- **Tab and Shift+Tab:** Pressing `Tab` inserts four spaces. Pressing `Shift+Tab` removes up to four spaces.
|
|
56
|
+
- **Selection Awareness:** Editing and navigation actions are aware of text selection. For example, custom arrow key logic is bypassed when a selection is active.
|
|
57
|
+
- **Clear Console:** The console can be cleared with a single command, automatically restoring the prompt and positioning the cursor for new input.
|
|
58
|
+
|
|
59
|
+
- **Command history**
|
|
60
|
+
|
|
61
|
+
Navigate previous commands with ↑/↓ arrows; history is saved to a file you choose.
|
|
62
|
+
|
|
63
|
+
- **Cut/Copy/Paste/Clear**
|
|
64
|
+
|
|
65
|
+
Right-click context menu (and customizable via context_menu_items) for text editing.
|
|
66
|
+
|
|
67
|
+
- **Customizable UI**
|
|
68
|
+
|
|
69
|
+
The package provides flexibility to customize:
|
|
70
|
+
|
|
71
|
+
- `history_file`: Change the location of the history file
|
|
72
|
+
- `console_locals`: Add custom variables and functions to the console's namespace
|
|
73
|
+
- `context_menu_items`: Modify the right-click context menu
|
|
74
|
+
- `show_about_message`: Customize the about dialog content
|
|
75
|
+
- `show_help_content`: Customize the help window content
|
|
76
|
+
- `create_menu`: Override to completely customize the menu bar
|
|
77
|
+
|
|
78
|
+
- **Subclass-friendly**
|
|
79
|
+
|
|
80
|
+
Extend the TextConsole class and override any of the above to fit your needs.
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
Full information and usage details at the [text_console GitHub repository](https://github.com/Ircama/text_console).
|
text_console-2.0.2/PKG-INFO
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: text_console
|
|
3
|
-
Version: 2.0.2
|
|
4
|
-
Summary: ('A customizable Tkinter-based text console widget, in which a user types in commands to be sent to the Python interpreter.',)
|
|
5
|
-
Home-page: https://github.com/Ircama/text_console
|
|
6
|
-
Author: Ircama
|
|
7
|
-
License: EUPL-1.2
|
|
8
|
-
Keywords: shell console tkinter
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1.2)
|
|
11
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
-
Classifier: Operating System :: OS Independent
|
|
15
|
-
Classifier: Intended Audience :: Developers
|
|
16
|
-
Requires-Python: >3.6
|
|
17
|
-
Description-Content-Type: text/markdown
|
|
18
|
-
License-File: LICENSE
|
|
19
|
-
Dynamic: author
|
|
20
|
-
Dynamic: classifier
|
|
21
|
-
Dynamic: description
|
|
22
|
-
Dynamic: description-content-type
|
|
23
|
-
Dynamic: home-page
|
|
24
|
-
Dynamic: keywords
|
|
25
|
-
Dynamic: license
|
|
26
|
-
Dynamic: license-file
|
|
27
|
-
Dynamic: requires-python
|
|
28
|
-
Dynamic: summary
|
|
29
|
-
|
|
30
|
-
# text_console
|
|
31
|
-
|
|
32
|
-
A customizable Tkinter-based text console widget with support for command history and interactive execution, in which a user types in commands to be sent to the Python interpreter.
|
|
33
|
-
|
|
34
|
-
It includes options for overriding specific variables through subclassing, by extending the `TextConsole` class.
|
|
35
|
-
|
|
36
|
-
The package is installable via pip.
|
|
37
|
-
|
|
38
|
-
## Features
|
|
39
|
-
|
|
40
|
-
- Tkinter-based package.
|
|
41
|
-
- Full multi-line commands support.
|
|
42
|
-
- Customizable history file.
|
|
43
|
-
- Context menu with options for Cut, Copy, Paste, and Clear.
|
|
44
|
-
- Extendable for additional functionality.
|
|
45
|
-
|
|
46
|
-
The package provides flexibility to customize:
|
|
47
|
-
|
|
48
|
-
- `history_file`: Change the location of the history file
|
|
49
|
-
- `console_locals`: Add custom variables and functions to the console's namespace
|
|
50
|
-
- `context_menu_items`: Modify the right-click context menu
|
|
51
|
-
- `show_about_message`: Customize the about dialog content
|
|
52
|
-
- `show_help_content`: Customize the help window content
|
|
53
|
-
- `create_menu`: Override to completely customize the menu bar
|
|
54
|
-
|
|
55
|
-
Each of these can be customized by subclassing TextConsole and overriding the appropriate attributes or methods.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
Full information and usage details at the [text_console GitHub repository](https://github.com/Ircama/text_console).
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.0.2"
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: text_console
|
|
3
|
-
Version: 2.0.2
|
|
4
|
-
Summary: ('A customizable Tkinter-based text console widget, in which a user types in commands to be sent to the Python interpreter.',)
|
|
5
|
-
Home-page: https://github.com/Ircama/text_console
|
|
6
|
-
Author: Ircama
|
|
7
|
-
License: EUPL-1.2
|
|
8
|
-
Keywords: shell console tkinter
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1.2)
|
|
11
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
-
Classifier: Operating System :: OS Independent
|
|
15
|
-
Classifier: Intended Audience :: Developers
|
|
16
|
-
Requires-Python: >3.6
|
|
17
|
-
Description-Content-Type: text/markdown
|
|
18
|
-
License-File: LICENSE
|
|
19
|
-
Dynamic: author
|
|
20
|
-
Dynamic: classifier
|
|
21
|
-
Dynamic: description
|
|
22
|
-
Dynamic: description-content-type
|
|
23
|
-
Dynamic: home-page
|
|
24
|
-
Dynamic: keywords
|
|
25
|
-
Dynamic: license
|
|
26
|
-
Dynamic: license-file
|
|
27
|
-
Dynamic: requires-python
|
|
28
|
-
Dynamic: summary
|
|
29
|
-
|
|
30
|
-
# text_console
|
|
31
|
-
|
|
32
|
-
A customizable Tkinter-based text console widget with support for command history and interactive execution, in which a user types in commands to be sent to the Python interpreter.
|
|
33
|
-
|
|
34
|
-
It includes options for overriding specific variables through subclassing, by extending the `TextConsole` class.
|
|
35
|
-
|
|
36
|
-
The package is installable via pip.
|
|
37
|
-
|
|
38
|
-
## Features
|
|
39
|
-
|
|
40
|
-
- Tkinter-based package.
|
|
41
|
-
- Full multi-line commands support.
|
|
42
|
-
- Customizable history file.
|
|
43
|
-
- Context menu with options for Cut, Copy, Paste, and Clear.
|
|
44
|
-
- Extendable for additional functionality.
|
|
45
|
-
|
|
46
|
-
The package provides flexibility to customize:
|
|
47
|
-
|
|
48
|
-
- `history_file`: Change the location of the history file
|
|
49
|
-
- `console_locals`: Add custom variables and functions to the console's namespace
|
|
50
|
-
- `context_menu_items`: Modify the right-click context menu
|
|
51
|
-
- `show_about_message`: Customize the about dialog content
|
|
52
|
-
- `show_help_content`: Customize the help window content
|
|
53
|
-
- `create_menu`: Override to completely customize the menu bar
|
|
54
|
-
|
|
55
|
-
Each of these can be customized by subclassing TextConsole and overriding the appropriate attributes or methods.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
Full information and usage details at the [text_console GitHub repository](https://github.com/Ircama/text_console).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|