text-console 2.0.3__tar.gz → 2.0.4__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 → text_console-2.0.4}/PKG-INFO +32 -2
- {text_console-2.0.3 → text_console-2.0.4}/README.md +31 -1
- text_console-2.0.4/text_console/__init__.py +5 -0
- {text_console-2.0.3 → text_console-2.0.4}/text_console/__main__.py +2 -2
- text_console-2.0.4/text_console/__version__.py +1 -0
- text_console-2.0.4/text_console/command_history.py +311 -0
- {text_console-2.0.3 → text_console-2.0.4}/text_console/text_console.py +68 -200
- {text_console-2.0.3 → text_console-2.0.4}/text_console.egg-info/PKG-INFO +32 -2
- {text_console-2.0.3 → text_console-2.0.4}/text_console.egg-info/SOURCES.txt +1 -0
- text_console-2.0.3/text_console/__init__.py +0 -5
- text_console-2.0.3/text_console/__version__.py +0 -1
- {text_console-2.0.3 → text_console-2.0.4}/LICENSE +0 -0
- {text_console-2.0.3 → text_console-2.0.4}/setup.cfg +0 -0
- {text_console-2.0.3 → text_console-2.0.4}/setup.py +0 -0
- {text_console-2.0.3 → text_console-2.0.4}/text_console/history.py +0 -0
- {text_console-2.0.3 → text_console-2.0.4}/text_console.egg-info/dependency_links.txt +0 -0
- {text_console-2.0.3 → text_console-2.0.4}/text_console.egg-info/entry_points.txt +0 -0
- {text_console-2.0.3 → text_console-2.0.4}/text_console.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: text_console
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.4
|
|
4
4
|
Summary: ('A customizable Tkinter-based text console widget, in which a user types in commands to be sent to the Python interpreter.',)
|
|
5
5
|
Home-page: https://github.com/Ircama/text_console
|
|
6
6
|
Author: Ircama
|
|
@@ -33,7 +33,7 @@ Dynamic: summary
|
|
|
33
33
|
|
|
34
34
|
It can also be used as a standalone Python command interpreter.
|
|
35
35
|
|
|
36
|
-
This program
|
|
36
|
+
This program reflects the core functionality of [wxPython Shell](https://github.com/wxWidgets/wxPython-Classic/blob/master/wx/py/shell.py), offering an even richer feature set.
|
|
37
37
|
|
|
38
38
|
## Key Features
|
|
39
39
|
|
|
@@ -79,5 +79,35 @@ This program is similar to [wxPython Shell](https://github.com/wxWidgets/wxPytho
|
|
|
79
79
|
|
|
80
80
|
Extend the TextConsole class and override any of the above to fit your needs.
|
|
81
81
|
|
|
82
|
+
## Keyboard shortcuts
|
|
83
|
+
|
|
84
|
+
| Shortcut | Description |
|
|
85
|
+
|-------------------------|--------------------------------------------------------------------------------------------------|
|
|
86
|
+
| Return | Execute current command; if editing mid-line or multiline, show modal allowing to select either to run the code or insert a linefeed in the cursor position. |
|
|
87
|
+
| Shift Return | Insert a new line within the edited multiline command. |
|
|
88
|
+
| Control Return | Move the cursor to the end of the last line of input. |
|
|
89
|
+
| Tab | Indent code (up to 4 spaces); if selection, indent all selected lines. |
|
|
90
|
+
| Shift Tab | Un-indent (remove up to 4 spaces before cursor). |
|
|
91
|
+
| Down Arrow | Navigate to next command in history. |
|
|
92
|
+
| Up Arrow | Navigate to previous command in history. |
|
|
93
|
+
| Left Arrow | Move to previous non-tagged character. |
|
|
94
|
+
| Right Arrow | Move to next non-tagged character. |
|
|
95
|
+
| Control Left Arrow | Move to previous word. |
|
|
96
|
+
| Control Right Arrow | Move to next word. |
|
|
97
|
+
|
|
98
|
+
| Escape | Jump to last blank command (empty input) in history. |
|
|
99
|
+
| BackSpace | Delete character before cursor. |
|
|
100
|
+
| Control C | Copy selected code, removing prompts first. |
|
|
101
|
+
| Control V | Paste text from clipboard, handling prompts and multiline input. |
|
|
102
|
+
| Button-3 (Right Click) | Show context menu (Cut, Copy, Paste, Clear). |
|
|
103
|
+
| Control Z | Undo last edit (safe, ignores errors). |
|
|
104
|
+
| Control Y | Redo last undone edit (safe, ignores errors). |
|
|
105
|
+
| Control K | Remove the current element from the history. |
|
|
106
|
+
| Home | Move cursor to start of current line. |
|
|
107
|
+
| End | Move cursor to end of current line. |
|
|
108
|
+
| Control + | Increase font size. |
|
|
109
|
+
| Control - | Decrease font size. |
|
|
110
|
+
| Control 0 | Reset font size. |
|
|
111
|
+
|
|
82
112
|
|
|
83
113
|
Full information and usage details at the [text_console GitHub repository](https://github.com/Ircama/text_console).
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
It can also be used as a standalone Python command interpreter.
|
|
6
6
|
|
|
7
|
-
This program
|
|
7
|
+
This program reflects the core functionality of [wxPython Shell](https://github.com/wxWidgets/wxPython-Classic/blob/master/wx/py/shell.py), offering an even richer feature set.
|
|
8
8
|
|
|
9
9
|
## Key Features
|
|
10
10
|
|
|
@@ -50,6 +50,36 @@ This program is similar to [wxPython Shell](https://github.com/wxWidgets/wxPytho
|
|
|
50
50
|
|
|
51
51
|
Extend the TextConsole class and override any of the above to fit your needs.
|
|
52
52
|
|
|
53
|
+
## Keyboard shortcuts
|
|
54
|
+
|
|
55
|
+
| Shortcut | Description |
|
|
56
|
+
|-------------------------|--------------------------------------------------------------------------------------------------|
|
|
57
|
+
| Return | Execute current command; if editing mid-line or multiline, show modal allowing to select either to run the code or insert a linefeed in the cursor position. |
|
|
58
|
+
| Shift Return | Insert a new line within the edited multiline command. |
|
|
59
|
+
| Control Return | Move the cursor to the end of the last line of input. |
|
|
60
|
+
| Tab | Indent code (up to 4 spaces); if selection, indent all selected lines. |
|
|
61
|
+
| Shift Tab | Un-indent (remove up to 4 spaces before cursor). |
|
|
62
|
+
| Down Arrow | Navigate to next command in history. |
|
|
63
|
+
| Up Arrow | Navigate to previous command in history. |
|
|
64
|
+
| Left Arrow | Move to previous non-tagged character. |
|
|
65
|
+
| Right Arrow | Move to next non-tagged character. |
|
|
66
|
+
| Control Left Arrow | Move to previous word. |
|
|
67
|
+
| Control Right Arrow | Move to next word. |
|
|
68
|
+
|
|
69
|
+
| Escape | Jump to last blank command (empty input) in history. |
|
|
70
|
+
| BackSpace | Delete character before cursor. |
|
|
71
|
+
| Control C | Copy selected code, removing prompts first. |
|
|
72
|
+
| Control V | Paste text from clipboard, handling prompts and multiline input. |
|
|
73
|
+
| Button-3 (Right Click) | Show context menu (Cut, Copy, Paste, Clear). |
|
|
74
|
+
| Control Z | Undo last edit (safe, ignores errors). |
|
|
75
|
+
| Control Y | Redo last undone edit (safe, ignores errors). |
|
|
76
|
+
| Control K | Remove the current element from the history. |
|
|
77
|
+
| Home | Move cursor to start of current line. |
|
|
78
|
+
| End | Move cursor to end of current line. |
|
|
79
|
+
| Control + | Increase font size. |
|
|
80
|
+
| Control - | Decrease font size. |
|
|
81
|
+
| Control 0 | Reset font size. |
|
|
82
|
+
|
|
53
83
|
## Installation
|
|
54
84
|
|
|
55
85
|
```bash
|
|
@@ -3,11 +3,11 @@ import argparse
|
|
|
3
3
|
import tkinter as tk
|
|
4
4
|
import webbrowser
|
|
5
5
|
|
|
6
|
-
from . import
|
|
6
|
+
from . import BaseTextConsole
|
|
7
7
|
from .__version__ import __version__
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class TkTextConsole(
|
|
10
|
+
class TkTextConsole(BaseTextConsole):
|
|
11
11
|
|
|
12
12
|
"""Subclass that adds the second element to the Help menu."""
|
|
13
13
|
def create_menu(self, main, master):
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.0.4"
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
|
|
3
|
+
class CommandHistoryPanel(tk.Toplevel):
|
|
4
|
+
def __init__(self, master, history, insert_cmd_callback, hist_item_ref, close_callback=None):
|
|
5
|
+
super().__init__(master)
|
|
6
|
+
self.history = history
|
|
7
|
+
self.insert_cmd_callback = insert_cmd_callback
|
|
8
|
+
self.hist_item_ref = hist_item_ref
|
|
9
|
+
self.close_callback = close_callback
|
|
10
|
+
self.title("Command History")
|
|
11
|
+
self.geometry("600x600")
|
|
12
|
+
self.protocol("WM_DELETE_WINDOW", self.on_close)
|
|
13
|
+
self._build_ui()
|
|
14
|
+
self.history_txt.see("1.0")
|
|
15
|
+
self.after(200, self.delayed_setup)
|
|
16
|
+
|
|
17
|
+
def _build_ui(self):
|
|
18
|
+
main_frame = tk.Frame(self)
|
|
19
|
+
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
|
20
|
+
header_frame = tk.Frame(main_frame, bg="white", relief="solid", bd=1)
|
|
21
|
+
header_frame.pack(fill="x", pady=(0, 5))
|
|
22
|
+
self.header_label = tk.Label(
|
|
23
|
+
header_frame,
|
|
24
|
+
text="№ │ Command",
|
|
25
|
+
font=("Consolas", 10, "bold"),
|
|
26
|
+
fg="#000080",
|
|
27
|
+
bg="white",
|
|
28
|
+
anchor="w",
|
|
29
|
+
padx=5,
|
|
30
|
+
pady=3
|
|
31
|
+
)
|
|
32
|
+
self.header_label.pack(fill="x")
|
|
33
|
+
# --- Search bar frame ---
|
|
34
|
+
search_frame = tk.Frame(main_frame)
|
|
35
|
+
search_frame.pack(fill="x", pady=(0, 5))
|
|
36
|
+
self.search_var = tk.StringVar()
|
|
37
|
+
self.search_entry = tk.Entry(search_frame, textvariable=self.search_var, font=("Consolas", 10))
|
|
38
|
+
self.search_entry.pack(side="left", fill="x", expand=True, padx=(0, 4))
|
|
39
|
+
self.btn_up = tk.Button(search_frame, text="↑", width=2)
|
|
40
|
+
self.btn_up.pack(side="left")
|
|
41
|
+
self.btn_down = tk.Button(search_frame, text="↓", width=2)
|
|
42
|
+
self.btn_down.pack(side="left")
|
|
43
|
+
self.after(300, self.focus_search_entry_delayed)
|
|
44
|
+
# --- End search bar frame ---
|
|
45
|
+
text_frame = tk.Frame(main_frame)
|
|
46
|
+
text_frame.pack(fill="both", expand=True)
|
|
47
|
+
v_scrollbar = tk.Scrollbar(text_frame, orient="vertical")
|
|
48
|
+
h_scrollbar = tk.Scrollbar(text_frame, orient="horizontal")
|
|
49
|
+
self.history_txt = tk.Text(
|
|
50
|
+
text_frame,
|
|
51
|
+
wrap="none",
|
|
52
|
+
yscrollcommand=v_scrollbar.set,
|
|
53
|
+
xscrollcommand=h_scrollbar.set,
|
|
54
|
+
font=("Consolas", 10),
|
|
55
|
+
bg="white",
|
|
56
|
+
fg="black",
|
|
57
|
+
selectbackground="#cce7ff"
|
|
58
|
+
)
|
|
59
|
+
v_scrollbar.config(command=self.history_txt.yview)
|
|
60
|
+
h_scrollbar.config(command=self.history_txt.xview)
|
|
61
|
+
v_scrollbar.pack(side="right", fill="y")
|
|
62
|
+
h_scrollbar.pack(side="bottom", fill="x")
|
|
63
|
+
self.history_txt.pack(fill="both", expand=True)
|
|
64
|
+
self.history_txt.tag_configure("number", foreground="#0066cc", font=("Consolas", 10, "bold"))
|
|
65
|
+
self.history_txt.tag_configure("number_hover", background="#e0f0ff")
|
|
66
|
+
self.history_txt.tag_configure("separator", foreground="#888888")
|
|
67
|
+
self.history_txt.tag_configure("command", foreground="#000000", font=("Consolas", 10))
|
|
68
|
+
self.history_txt.tag_configure("divider", foreground="#cccccc", selectbackground="white", selectforeground="#cccccc")
|
|
69
|
+
self.history_txt.tag_configure("nonselectable", foreground="#0066cc", font=("Consolas", 10, "bold"), selectbackground="white", selectforeground="#0066cc")
|
|
70
|
+
self.history_txt.config(state="disabled")
|
|
71
|
+
status_frame = tk.Frame(main_frame)
|
|
72
|
+
status_frame.pack(fill="x", pady=(5, 0))
|
|
73
|
+
self.status_label = tk.Label(
|
|
74
|
+
status_frame,
|
|
75
|
+
text=f"Total commands: {len(self.history)}. Right-click or Ctrl+C to copy. Double-click to edit the command.",
|
|
76
|
+
relief="sunken",
|
|
77
|
+
anchor="w"
|
|
78
|
+
)
|
|
79
|
+
self.status_label.pack(fill="x")
|
|
80
|
+
self.history_txt.see("1.0")
|
|
81
|
+
# Context menu
|
|
82
|
+
self.context_menu = tk.Menu(self, tearoff=0)
|
|
83
|
+
self.context_menu.add_command(label="Copy Selected", command=self.copy_selected_command)
|
|
84
|
+
self.context_menu.add_command(label="Close", command=self.destroy)
|
|
85
|
+
self.bind("<Button-3>", lambda e: self.context_menu.post(e.x_root, e.y_root))
|
|
86
|
+
self.bind("<Control-w>", lambda e: self.destroy())
|
|
87
|
+
self.bind("<Escape>", lambda e: self.destroy())
|
|
88
|
+
# Search navigation state
|
|
89
|
+
self.search_matches = []
|
|
90
|
+
self.search_index = [0]
|
|
91
|
+
# Bindings
|
|
92
|
+
self.history_txt.bind("<Control-c>", self.copy_selected_command)
|
|
93
|
+
self.history_txt.bind("<Control-C>", self.copy_selected_command)
|
|
94
|
+
self.search_entry.bind("<Return>", self.on_search_enter)
|
|
95
|
+
self.btn_up.config(command=self.on_search_up)
|
|
96
|
+
self.btn_down.config(command=self.on_search_down)
|
|
97
|
+
self.search_entry.bind("<Control-Up>", lambda e: self.on_search_up())
|
|
98
|
+
self.search_entry.bind("<Control-Down>", lambda e: self.on_search_down())
|
|
99
|
+
self.bind('<Control-s>', self.load_selected_to_main)
|
|
100
|
+
self.bind('<Control-n>', lambda e: self.on_search_down())
|
|
101
|
+
self.bind('<Control-b>', lambda e: self.on_search_up())
|
|
102
|
+
self.bind('<Escape>', self.close_history_panel)
|
|
103
|
+
self.history_txt.bind("<ButtonRelease-1>", self.on_selection)
|
|
104
|
+
self.history_txt.bind("<B1-Motion>", self.on_selection)
|
|
105
|
+
self.history_txt.bind("<Double-Button-1>", self.on_number_double_click)
|
|
106
|
+
self.history_txt.bind("<Enter>", self.on_number_enter)
|
|
107
|
+
self.history_txt.bind("<Leave>", self.on_number_leave)
|
|
108
|
+
self.bind('<Configure>', self.on_window_configure)
|
|
109
|
+
|
|
110
|
+
def delayed_setup(self):
|
|
111
|
+
self.update_display()
|
|
112
|
+
self.history_txt.focus_set()
|
|
113
|
+
self.history_txt.see("1.0")
|
|
114
|
+
|
|
115
|
+
def focus_search_entry_delayed(self):
|
|
116
|
+
self.search_entry.focus_set()
|
|
117
|
+
|
|
118
|
+
def on_close(self):
|
|
119
|
+
if self.close_callback:
|
|
120
|
+
self.close_callback()
|
|
121
|
+
self.destroy()
|
|
122
|
+
|
|
123
|
+
def calculate_layout(self):
|
|
124
|
+
try:
|
|
125
|
+
text_width_pixels = self.history_txt.winfo_width()
|
|
126
|
+
char_width = 8
|
|
127
|
+
widget_width = max(80, text_width_pixels // char_width)
|
|
128
|
+
except:
|
|
129
|
+
widget_width = max(100, (self.winfo_width() - 80) // 8)
|
|
130
|
+
max_command_length = 0
|
|
131
|
+
for command in self.history:
|
|
132
|
+
for line in str(command).split('\n'):
|
|
133
|
+
max_command_length = max(max_command_length, len(line))
|
|
134
|
+
num_width = max(5, len(str(len(self.history))))
|
|
135
|
+
cmd_width = max(50, widget_width - num_width - 3)
|
|
136
|
+
return num_width, cmd_width, max_command_length, widget_width
|
|
137
|
+
|
|
138
|
+
def on_window_configure(self, event=None):
|
|
139
|
+
if event and event.widget == self:
|
|
140
|
+
self.update_display()
|
|
141
|
+
|
|
142
|
+
def on_number_double_click(self, event):
|
|
143
|
+
index = self.history_txt.index(f"@{event.x},{event.y}")
|
|
144
|
+
line = int(index.split('.')[0])
|
|
145
|
+
while line > 0:
|
|
146
|
+
num_text = self.history_txt.get(f"{line}.0", f"{line}.end").split('│')[0].strip()
|
|
147
|
+
if num_text.isdigit():
|
|
148
|
+
hist_index = int(num_text) - 1 # assuming history is 1-based in the panel
|
|
149
|
+
break
|
|
150
|
+
line -= 1
|
|
151
|
+
else:
|
|
152
|
+
hist_index = None # Not found
|
|
153
|
+
|
|
154
|
+
if hist_index is not None:
|
|
155
|
+
command = self.history[hist_index]
|
|
156
|
+
self.hist_item_ref[0] = hist_index
|
|
157
|
+
self.insert_cmd_callback(command)
|
|
158
|
+
self.master.focus_set()
|
|
159
|
+
self.close()
|
|
160
|
+
|
|
161
|
+
def on_number_enter(self, event):
|
|
162
|
+
index = self.history_txt.index(f"@{event.x},{event.y}")
|
|
163
|
+
line = index.split('.')[0]
|
|
164
|
+
col = index.split('.')[1]
|
|
165
|
+
sep_index = self.history_txt.get(f"{line}.0", f"{line}.end").find("│")
|
|
166
|
+
if int(col) < 7 and sep_index > 0:
|
|
167
|
+
self.history_txt.config(cursor="hand2")
|
|
168
|
+
if sep_index != -1:
|
|
169
|
+
self.history_txt.tag_add("number_hover", f"{line}.0", f"{line}.{sep_index}")
|
|
170
|
+
|
|
171
|
+
def on_number_leave(self, event):
|
|
172
|
+
self.history_txt.config(cursor="")
|
|
173
|
+
self.history_txt.tag_remove("number_hover", "1.0", "end")
|
|
174
|
+
|
|
175
|
+
def on_selection(self, event=None):
|
|
176
|
+
try:
|
|
177
|
+
sel_start = self.history_txt.index("sel.first")
|
|
178
|
+
sel_end = self.history_txt.index("sel.last")
|
|
179
|
+
start_line = int(sel_start.split('.')[0])
|
|
180
|
+
end_line = int(sel_end.split('.')[0])
|
|
181
|
+
for line in range(start_line, end_line + 1):
|
|
182
|
+
line_content = self.history_txt.get(f"{line}.0", f"{line}.end")
|
|
183
|
+
sep_index = line_content.find("│")
|
|
184
|
+
if sep_index != -1:
|
|
185
|
+
self.history_txt.tag_remove("sel", f"{line}.0", f"{line}.{sep_index+1}")
|
|
186
|
+
if set(line_content.strip()) == {"─"}:
|
|
187
|
+
self.history_txt.tag_remove("sel", f"{line}.0", f"{line}.end")
|
|
188
|
+
except tk.TclError:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
def update_display(self):
|
|
192
|
+
self.history_txt.config(state="normal")
|
|
193
|
+
self.history_txt.delete("1.0", "end")
|
|
194
|
+
num_width, cmd_width, max_cmd_length, total_width = self.calculate_layout()
|
|
195
|
+
header_text = f"{'№':<{num_width}}│ Command"
|
|
196
|
+
self.header_label.config(text=header_text)
|
|
197
|
+
for i, command in enumerate(reversed(self.history)):
|
|
198
|
+
item_number = len(self.history) - i
|
|
199
|
+
command_text = str(command).strip()
|
|
200
|
+
command_lines = command_text.split('\n')
|
|
201
|
+
first_line = command_lines[0] if command_lines else ""
|
|
202
|
+
tag_name = f"numtag_{item_number}"
|
|
203
|
+
start_idx = self.history_txt.index("end-1c")
|
|
204
|
+
self.history_txt.insert("end", f"{item_number:<{num_width}}", ("number", "nonselectable", tag_name))
|
|
205
|
+
end_idx = self.history_txt.index("end-1c")
|
|
206
|
+
self.history_txt.insert("end", " │ ", ("separator", "nonselectable"))
|
|
207
|
+
self.history_txt.insert("end", f"{first_line}\n", "command")
|
|
208
|
+
# Bind number column events for this tag
|
|
209
|
+
self.history_txt.tag_bind(tag_name, "<Double-Button-1>", self.on_number_double_click)
|
|
210
|
+
self.history_txt.tag_bind(tag_name, "<Enter>", self.on_number_enter)
|
|
211
|
+
self.history_txt.tag_bind(tag_name, "<Leave>", self.on_number_leave)
|
|
212
|
+
for line in command_lines[1:]:
|
|
213
|
+
self.history_txt.insert("end", f"{'':<{num_width}}", "nonselectable")
|
|
214
|
+
self.history_txt.insert("end", " │ ", ("separator", "nonselectable"))
|
|
215
|
+
self.history_txt.insert("end", f"{line}\n", "command")
|
|
216
|
+
if i < len(self.history) - 1:
|
|
217
|
+
self.history_txt.insert("end", "─" * total_width, "divider")
|
|
218
|
+
self.history_txt.insert("end", "\n")
|
|
219
|
+
self.history_txt.config(state="disabled")
|
|
220
|
+
|
|
221
|
+
def copy_selected_command(self, event=None):
|
|
222
|
+
try:
|
|
223
|
+
sel_start = self.history_txt.index("sel.first")
|
|
224
|
+
sel_end = self.history_txt.index("sel.last")
|
|
225
|
+
start_line = int(sel_start.split('.')[0])
|
|
226
|
+
end_line = int(sel_end.split('.')[0])
|
|
227
|
+
result_lines = []
|
|
228
|
+
for line in range(start_line, end_line + 1):
|
|
229
|
+
line_content = self.history_txt.get(f"{line}.0", f"{line}.end")
|
|
230
|
+
if set(line_content.strip()) == {"─"}:
|
|
231
|
+
continue
|
|
232
|
+
sep_index = line_content.find("│")
|
|
233
|
+
if sep_index != -1:
|
|
234
|
+
command_part = line_content[sep_index+1:]
|
|
235
|
+
sel_line_start = max(int(sel_start.split('.')[1]), 0) if line == start_line else 0
|
|
236
|
+
sel_line_end = int(sel_end.split('.')[1]) if line == end_line else len(line_content)
|
|
237
|
+
if sel_line_end > sep_index:
|
|
238
|
+
result_lines.append(command_part.rstrip("\n"))
|
|
239
|
+
else:
|
|
240
|
+
result_lines.append(line_content.rstrip("\n"))
|
|
241
|
+
command_text = '\n'.join(result_lines).rstrip("\n")
|
|
242
|
+
if command_text:
|
|
243
|
+
self.clipboard_clear()
|
|
244
|
+
self.clipboard_append(command_text)
|
|
245
|
+
except tk.TclError:
|
|
246
|
+
pass
|
|
247
|
+
return "break"
|
|
248
|
+
|
|
249
|
+
def search_history(self, forward=True):
|
|
250
|
+
pattern = self.search_var.get().strip()
|
|
251
|
+
self.history_txt.tag_remove("sel", "1.0", "end")
|
|
252
|
+
if not pattern:
|
|
253
|
+
return
|
|
254
|
+
matches = []
|
|
255
|
+
for i in range(1, int(self.history_txt.index("end-1c").split(".")[0])):
|
|
256
|
+
line_content = self.history_txt.get(f"{i}.0", f"{i}.end")
|
|
257
|
+
sep_index = line_content.find("│")
|
|
258
|
+
if sep_index != -1:
|
|
259
|
+
command_part = line_content[sep_index+1:]
|
|
260
|
+
col_offset = sep_index + 1
|
|
261
|
+
idx = command_part.lower().find(pattern.lower())
|
|
262
|
+
if idx != -1:
|
|
263
|
+
matches.append((i, col_offset + idx, col_offset + idx + len(pattern)))
|
|
264
|
+
if not matches:
|
|
265
|
+
self.search_index[0] = 0
|
|
266
|
+
return
|
|
267
|
+
if forward:
|
|
268
|
+
self.search_index[0] = (self.search_index[0] + 1) % len(matches) if self.search_index[0] < len(matches) else 0
|
|
269
|
+
else:
|
|
270
|
+
self.search_index[0] = (self.search_index[0] - 1) % len(matches)
|
|
271
|
+
line, start_col, end_col = matches[self.search_index[0]]
|
|
272
|
+
self.history_txt.see(f"{line}.0")
|
|
273
|
+
self.history_txt.tag_add("sel", f"{line}.{start_col}", f"{line}.{end_col}")
|
|
274
|
+
self.history_txt.mark_set("insert", f"{line}.{start_col}")
|
|
275
|
+
self.history_txt.focus_set()
|
|
276
|
+
self.search_matches.clear()
|
|
277
|
+
self.search_matches.extend(matches)
|
|
278
|
+
|
|
279
|
+
def on_search_enter(self, event=None):
|
|
280
|
+
self.search_index[0] = -1
|
|
281
|
+
self.search_history(forward=True)
|
|
282
|
+
|
|
283
|
+
def on_search_up(self, event=None):
|
|
284
|
+
if self.search_matches:
|
|
285
|
+
self.search_history(forward=False)
|
|
286
|
+
|
|
287
|
+
def on_search_down(self, event=None):
|
|
288
|
+
if self.search_matches:
|
|
289
|
+
self.search_history(forward=True)
|
|
290
|
+
|
|
291
|
+
def load_selected_to_main(self, event=None):
|
|
292
|
+
if self.search_matches and 0 <= self.search_index[0] < len(self.search_matches):
|
|
293
|
+
line, start_col, end_col = self.search_matches[self.search_index[0]]
|
|
294
|
+
num_text = self.history_txt.get(f"{line}.0", f"{line}.end").split('│')[0].strip()
|
|
295
|
+
try:
|
|
296
|
+
hist_index = int(num_text) - 1
|
|
297
|
+
if 0 <= hist_index < len(self.history):
|
|
298
|
+
self.hist_item_ref[0] = hist_index
|
|
299
|
+
self.insert_cmd_callback(self.history[hist_index])
|
|
300
|
+
self.lift()
|
|
301
|
+
except Exception as e:
|
|
302
|
+
print(f"[DEBUG] Error in load_selected_to_main: {e}")
|
|
303
|
+
pass
|
|
304
|
+
|
|
305
|
+
def close_history_panel(self, event=None):
|
|
306
|
+
if hasattr(self.master, '_history_window') and self.master._history_window is not None:
|
|
307
|
+
self.master._history_window.destroy()
|
|
308
|
+
self.master._history_window = None
|
|
309
|
+
|
|
310
|
+
def close(self):
|
|
311
|
+
self.after(300, self.destroy)
|
|
@@ -8,6 +8,7 @@ from io import StringIO
|
|
|
8
8
|
from contextlib import redirect_stdout, redirect_stderr
|
|
9
9
|
|
|
10
10
|
from .history import History
|
|
11
|
+
from .command_history import CommandHistoryPanel
|
|
11
12
|
from .__version__ import __version__
|
|
12
13
|
|
|
13
14
|
|
|
@@ -100,6 +101,7 @@ class BaseTextConsole(tk.Text):
|
|
|
100
101
|
self.prompt()
|
|
101
102
|
self.mark_set('input', 'insert')
|
|
102
103
|
self.mark_gravity('input', 'left')
|
|
104
|
+
self.focus_set()
|
|
103
105
|
|
|
104
106
|
def setup_tags(self):
|
|
105
107
|
"""Set up text tags for styling"""
|
|
@@ -122,6 +124,10 @@ class BaseTextConsole(tk.Text):
|
|
|
122
124
|
font=("Courier", font_size - 2)
|
|
123
125
|
)
|
|
124
126
|
self.tag_configure("output", foreground="#00178c")
|
|
127
|
+
self.tag_configure("number", foreground="#0066cc", font=("Consolas", 10, "bold"))
|
|
128
|
+
self.tag_configure("number_hover", background="#e0f0ff")
|
|
129
|
+
self.tag_configure("nonselectable", foreground="#0066cc", font=("Consolas", 10, "bold"), selectbackground="white", selectforeground="#0066cc")
|
|
130
|
+
self.tag_configure("divider", foreground="#cccccc", selectbackground="white", selectforeground="#cccccc")
|
|
125
131
|
|
|
126
132
|
def setup_bindings(self):
|
|
127
133
|
"""Set up key bindings"""
|
|
@@ -145,6 +151,56 @@ class BaseTextConsole(tk.Text):
|
|
|
145
151
|
self.bind("<KeyPress>", self.on_key_press)
|
|
146
152
|
self.bind("<Home>", lambda e: self._move_to_line_start(e))
|
|
147
153
|
self.bind("<End>", lambda e: self._move_to_line_end(e))
|
|
154
|
+
self.bind('<Control-k>', self.remove_current_history_entry)
|
|
155
|
+
self.bind('<Control-plus>', self.increase_font_size)
|
|
156
|
+
self.bind('<Control-minus>', self.decrease_font_size)
|
|
157
|
+
self.bind('<Control-0>', self.reset_font_size)
|
|
158
|
+
self.bind('<Control-r>', lambda e: self.show_command_history_panel())
|
|
159
|
+
|
|
160
|
+
def get_font(self):
|
|
161
|
+
font_name = self.cget("font")
|
|
162
|
+
return tkfont.nametofont(font_name)
|
|
163
|
+
|
|
164
|
+
def set_font_size(self, size):
|
|
165
|
+
font_obj = self.get_font()
|
|
166
|
+
font_obj.configure(size=size)
|
|
167
|
+
self.setup_tags() # Update tag fonts as well
|
|
168
|
+
|
|
169
|
+
def increase_font_size(self, event=None):
|
|
170
|
+
font_obj = self.get_font()
|
|
171
|
+
size = font_obj.actual("size")
|
|
172
|
+
self.set_font_size(size + 1)
|
|
173
|
+
return "break"
|
|
174
|
+
|
|
175
|
+
def decrease_font_size(self, event=None):
|
|
176
|
+
font_obj = self.get_font()
|
|
177
|
+
size = font_obj.actual("size")
|
|
178
|
+
if size > 4:
|
|
179
|
+
self.set_font_size(size - 1)
|
|
180
|
+
return "break"
|
|
181
|
+
|
|
182
|
+
def reset_font_size(self, event=None):
|
|
183
|
+
self.set_font_size(12) # Default size
|
|
184
|
+
return "break"
|
|
185
|
+
|
|
186
|
+
def remove_current_history_entry(self, event=None):
|
|
187
|
+
"""
|
|
188
|
+
Remove the currently retrieved element from the history.
|
|
189
|
+
Only works if a history item is currently loaded (not blank input).
|
|
190
|
+
"""
|
|
191
|
+
# Only remove if _hist_item is valid and in range
|
|
192
|
+
if getattr(self, '_hist_item', None) is not None and 0 <= self._hist_item < len(self.history):
|
|
193
|
+
del self.history[self._hist_item]
|
|
194
|
+
# After deletion, adjust _hist_item to point to next item or end
|
|
195
|
+
if self._hist_item >= len(self.history):
|
|
196
|
+
self._hist_item = len(self.history)
|
|
197
|
+
self.delete('input', 'end')
|
|
198
|
+
self.insert('insert', '')
|
|
199
|
+
else:
|
|
200
|
+
# Show the next item in history if available
|
|
201
|
+
self.insert_cmd(self.history[self._hist_item] if self._hist_item < len(self.history) else '')
|
|
202
|
+
self.history.save()
|
|
203
|
+
return "break"
|
|
148
204
|
|
|
149
205
|
def _safe_undo(self):
|
|
150
206
|
try:
|
|
@@ -192,7 +248,7 @@ class BaseTextConsole(tk.Text):
|
|
|
192
248
|
# History menu
|
|
193
249
|
history_menu = Menu(menu_bar, tearoff=0)
|
|
194
250
|
history_menu.add_command(
|
|
195
|
-
label="List history", command=self.
|
|
251
|
+
label="List history", command=self.show_command_history_panel
|
|
196
252
|
)
|
|
197
253
|
history_menu.add_checkbutton(
|
|
198
254
|
label="Save Errors in History",
|
|
@@ -300,204 +356,6 @@ class BaseTextConsole(tk.Text):
|
|
|
300
356
|
self.mark_set('input', 'end-1c')
|
|
301
357
|
self.edit_reset()
|
|
302
358
|
|
|
303
|
-
|
|
304
|
-
def dump_history(self):
|
|
305
|
-
"""Open a separate window with the output of the history."""
|
|
306
|
-
history_window = tk.Toplevel(self)
|
|
307
|
-
history_window.title("Command History")
|
|
308
|
-
history_window.geometry("600x600")
|
|
309
|
-
|
|
310
|
-
# Create main frame
|
|
311
|
-
main_frame = tk.Frame(history_window)
|
|
312
|
-
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
|
313
|
-
|
|
314
|
-
# Create fixed header frame
|
|
315
|
-
header_frame = tk.Frame(main_frame, bg="white", relief="solid", bd=1)
|
|
316
|
-
header_frame.pack(fill="x", pady=(0, 5))
|
|
317
|
-
|
|
318
|
-
# Header label
|
|
319
|
-
header_label = tk.Label(
|
|
320
|
-
header_frame,
|
|
321
|
-
text="№ │ Command",
|
|
322
|
-
font=("Consolas", 10, "bold"),
|
|
323
|
-
fg="#000080",
|
|
324
|
-
bg="white",
|
|
325
|
-
anchor="w",
|
|
326
|
-
padx=5,
|
|
327
|
-
pady=3
|
|
328
|
-
)
|
|
329
|
-
header_label.pack(fill="x")
|
|
330
|
-
|
|
331
|
-
# Create text widget frame
|
|
332
|
-
text_frame = tk.Frame(main_frame)
|
|
333
|
-
text_frame.pack(fill="both", expand=True)
|
|
334
|
-
|
|
335
|
-
# Scrollbars
|
|
336
|
-
v_scrollbar = tk.Scrollbar(text_frame, orient="vertical")
|
|
337
|
-
h_scrollbar = tk.Scrollbar(text_frame, orient="horizontal")
|
|
338
|
-
|
|
339
|
-
# Text widget
|
|
340
|
-
history_txt = tk.Text(
|
|
341
|
-
text_frame,
|
|
342
|
-
wrap="none", # No wrapping to maintain table format
|
|
343
|
-
yscrollcommand=v_scrollbar.set,
|
|
344
|
-
xscrollcommand=h_scrollbar.set,
|
|
345
|
-
font=("Consolas", 10),
|
|
346
|
-
bg="white",
|
|
347
|
-
fg="black",
|
|
348
|
-
selectbackground="#cce7ff"
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
# Configure scrollbars
|
|
352
|
-
v_scrollbar.config(command=history_txt.yview)
|
|
353
|
-
h_scrollbar.config(command=history_txt.xview)
|
|
354
|
-
|
|
355
|
-
# Pack scrollbars and text widget
|
|
356
|
-
v_scrollbar.pack(side="right", fill="y")
|
|
357
|
-
h_scrollbar.pack(side="bottom", fill="x")
|
|
358
|
-
history_txt.pack(fill="both", expand=True)
|
|
359
|
-
|
|
360
|
-
# Configure text tags for styling
|
|
361
|
-
history_txt.tag_configure("number", foreground="#0066cc", font=("Consolas", 10, "bold"))
|
|
362
|
-
history_txt.tag_configure("separator", foreground="#888888")
|
|
363
|
-
history_txt.tag_configure("command", foreground="#000000", font=("Consolas", 10))
|
|
364
|
-
history_txt.tag_configure("divider", foreground="#cccccc")
|
|
365
|
-
|
|
366
|
-
# Calculate column width based on window size and longest command
|
|
367
|
-
def calculate_layout():
|
|
368
|
-
# Get actual text widget width in characters
|
|
369
|
-
try:
|
|
370
|
-
# Get the actual width of the text widget in pixels
|
|
371
|
-
text_width_pixels = history_txt.winfo_width()
|
|
372
|
-
# Convert to approximate character width (assuming monospace font)
|
|
373
|
-
char_width = 8 # Approximate width of Consolas 10pt character
|
|
374
|
-
widget_width = max(80, text_width_pixels // char_width)
|
|
375
|
-
except:
|
|
376
|
-
# Fallback if widget not yet rendered
|
|
377
|
-
widget_width = max(100, (history_window.winfo_width() - 80) // 8)
|
|
378
|
-
|
|
379
|
-
# Find the longest command to determine if we need to use horizontal scrolling
|
|
380
|
-
max_command_length = 0
|
|
381
|
-
for command in self.history:
|
|
382
|
-
for line in str(command).split('\n'):
|
|
383
|
-
max_command_length = max(max_command_length, len(line))
|
|
384
|
-
|
|
385
|
-
# Number column width (always fixed)
|
|
386
|
-
num_width = max(5, len(str(len(self.history))))
|
|
387
|
-
|
|
388
|
-
# Command column gets remaining width, but ensure minimum readability
|
|
389
|
-
cmd_width = max(50, widget_width - num_width - 3) # 3 for separators
|
|
390
|
-
|
|
391
|
-
return num_width, cmd_width, max_command_length, widget_width
|
|
392
|
-
|
|
393
|
-
# Update layout when window resizes
|
|
394
|
-
def on_window_configure(event=None):
|
|
395
|
-
if event and event.widget == history_window:
|
|
396
|
-
update_display()
|
|
397
|
-
|
|
398
|
-
def update_display():
|
|
399
|
-
history_txt.config(state="normal")
|
|
400
|
-
history_txt.delete("1.0", "end")
|
|
401
|
-
|
|
402
|
-
num_width, cmd_width, max_cmd_length, total_width = calculate_layout()
|
|
403
|
-
|
|
404
|
-
# Update header to match current layout
|
|
405
|
-
header_text = f"{'№':<{num_width}}│ Command"
|
|
406
|
-
header_label.config(text=header_text)
|
|
407
|
-
|
|
408
|
-
# Add commands (most recent first)
|
|
409
|
-
for i, command in enumerate(reversed(self.history)):
|
|
410
|
-
item_number = len(self.history) - i
|
|
411
|
-
command_text = str(command).strip()
|
|
412
|
-
|
|
413
|
-
# Split command into lines
|
|
414
|
-
command_lines = command_text.split('\n')
|
|
415
|
-
|
|
416
|
-
# First line with number
|
|
417
|
-
first_line = command_lines[0] if command_lines else ""
|
|
418
|
-
|
|
419
|
-
history_txt.insert("end", f"{item_number:<{num_width}}", "number")
|
|
420
|
-
history_txt.insert("end", " │ ", "separator")
|
|
421
|
-
history_txt.insert("end", f"{first_line}\n", "command")
|
|
422
|
-
|
|
423
|
-
# Additional lines (if multiline command)
|
|
424
|
-
for line in command_lines[1:]:
|
|
425
|
-
history_txt.insert("end", f"{'':<{num_width}}", "")
|
|
426
|
-
history_txt.insert("end", " │ ", "separator")
|
|
427
|
-
history_txt.insert("end", f"{line}\n", "command")
|
|
428
|
-
|
|
429
|
-
# Add horizontal line separator between commands (except for the last one)
|
|
430
|
-
if i < len(self.history) - 1:
|
|
431
|
-
# More efficient: insert a single line character repeated
|
|
432
|
-
history_txt.insert("end", "─" * total_width, "divider")
|
|
433
|
-
history_txt.insert("end", "\n")
|
|
434
|
-
|
|
435
|
-
# Copy functionality
|
|
436
|
-
def copy_selected_command():
|
|
437
|
-
try:
|
|
438
|
-
# Get selected text
|
|
439
|
-
selected_text = history_txt.selection_get()
|
|
440
|
-
if selected_text:
|
|
441
|
-
history_window.clipboard_clear()
|
|
442
|
-
history_window.clipboard_append(selected_text)
|
|
443
|
-
except tk.TclError:
|
|
444
|
-
# No selection, try to get the line under cursor
|
|
445
|
-
current_line = history_txt.index(tk.INSERT).split('.')[0]
|
|
446
|
-
line_content = history_txt.get(f"{current_line}.0", f"{current_line}.end")
|
|
447
|
-
|
|
448
|
-
# Extract command part (after the │ separator)
|
|
449
|
-
if " │ " in line_content:
|
|
450
|
-
command_part = line_content.split(" │ ", 1)[1]
|
|
451
|
-
history_window.clipboard_clear()
|
|
452
|
-
history_window.clipboard_append(command_part)
|
|
453
|
-
|
|
454
|
-
# Context menu
|
|
455
|
-
context_menu = tk.Menu(history_window, tearoff=0)
|
|
456
|
-
context_menu.add_command(label="Copy Selected", command=copy_selected_command)
|
|
457
|
-
context_menu.add_command(label="Select All", command=lambda: history_txt.tag_add("sel", "1.0", "end"))
|
|
458
|
-
|
|
459
|
-
def show_context_menu(event):
|
|
460
|
-
try:
|
|
461
|
-
context_menu.tk_popup(event.x_root, event.y_root)
|
|
462
|
-
finally:
|
|
463
|
-
context_menu.grab_release()
|
|
464
|
-
|
|
465
|
-
# Bind events
|
|
466
|
-
history_txt.bind("<Button-3>", show_context_menu) # Right-click
|
|
467
|
-
history_txt.bind("<Control-c>", lambda e: copy_selected_command())
|
|
468
|
-
history_txt.bind("<Double-Button-1>", lambda e: copy_selected_command())
|
|
469
|
-
|
|
470
|
-
# Bind window resize
|
|
471
|
-
history_window.bind("<Configure>", on_window_configure)
|
|
472
|
-
|
|
473
|
-
# Initial display with proper timing
|
|
474
|
-
def delayed_setup():
|
|
475
|
-
update_display()
|
|
476
|
-
# Set focus to the text widget for keyboard navigation
|
|
477
|
-
history_txt.focus_set()
|
|
478
|
-
history_txt.see("1.0") # Scroll to top (most recent command)
|
|
479
|
-
|
|
480
|
-
history_window.after(200, delayed_setup) # Increased delay to ensure widget is fully rendered
|
|
481
|
-
|
|
482
|
-
# Make text read-only
|
|
483
|
-
history_txt.config(state="disabled")
|
|
484
|
-
|
|
485
|
-
# Status bar
|
|
486
|
-
status_frame = tk.Frame(main_frame)
|
|
487
|
-
status_frame.pack(fill="x", pady=(5, 0))
|
|
488
|
-
|
|
489
|
-
status_label = tk.Label(
|
|
490
|
-
status_frame,
|
|
491
|
-
text=f"Total commands: {len(self.history)}. Right-click or Ctrl+C to copy. Double-click to copy line",
|
|
492
|
-
relief="sunken",
|
|
493
|
-
anchor="w"
|
|
494
|
-
)
|
|
495
|
-
status_label.pack(fill="x")
|
|
496
|
-
|
|
497
|
-
# Focus on the most recent command (scroll to top)
|
|
498
|
-
history_txt.see("1.0")
|
|
499
|
-
|
|
500
|
-
|
|
501
359
|
def on_ctrl_c(self, event):
|
|
502
360
|
"""Copy selected code, removing prompts first"""
|
|
503
361
|
sel = self.tag_ranges('sel')
|
|
@@ -646,7 +504,6 @@ class BaseTextConsole(tk.Text):
|
|
|
646
504
|
return "break"
|
|
647
505
|
# Get current input line for matching
|
|
648
506
|
first_line_input = self.get('input', 'insert')
|
|
649
|
-
|
|
650
507
|
# If we're starting a new search (first up arrow press), initialize
|
|
651
508
|
if self._hist_item == len(self.history):
|
|
652
509
|
self._hist_match = first_line_input
|
|
@@ -990,6 +847,9 @@ class BaseTextConsole(tk.Text):
|
|
|
990
847
|
self.history.save()
|
|
991
848
|
return 'break'
|
|
992
849
|
|
|
850
|
+
def show_command_history_panel(self):
|
|
851
|
+
CommandHistoryPanel(self, self.history, self.insert_cmd, [self._hist_item])
|
|
852
|
+
|
|
993
853
|
def insert_line(self, event=None):
|
|
994
854
|
"""Handle Ctrl+Return key press"""
|
|
995
855
|
self.edit_separator()
|
|
@@ -1118,6 +978,14 @@ class BaseTextConsole(tk.Text):
|
|
|
1118
978
|
self.insert('insert', '\n', "output")
|
|
1119
979
|
self.prompt()
|
|
1120
980
|
|
|
981
|
+
# Close history panel if open
|
|
982
|
+
if hasattr(self, '_history_window') and self._history_window is not None:
|
|
983
|
+
try:
|
|
984
|
+
self._history_window.destroy()
|
|
985
|
+
except Exception:
|
|
986
|
+
pass
|
|
987
|
+
self._history_window = None
|
|
988
|
+
|
|
1121
989
|
def on_key_press(self, event):
|
|
1122
990
|
"""
|
|
1123
991
|
Prevent character insertion if the cursor is over a character with any tag (e.g., prompt).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: text_console
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.4
|
|
4
4
|
Summary: ('A customizable Tkinter-based text console widget, in which a user types in commands to be sent to the Python interpreter.',)
|
|
5
5
|
Home-page: https://github.com/Ircama/text_console
|
|
6
6
|
Author: Ircama
|
|
@@ -33,7 +33,7 @@ Dynamic: summary
|
|
|
33
33
|
|
|
34
34
|
It can also be used as a standalone Python command interpreter.
|
|
35
35
|
|
|
36
|
-
This program
|
|
36
|
+
This program reflects the core functionality of [wxPython Shell](https://github.com/wxWidgets/wxPython-Classic/blob/master/wx/py/shell.py), offering an even richer feature set.
|
|
37
37
|
|
|
38
38
|
## Key Features
|
|
39
39
|
|
|
@@ -79,5 +79,35 @@ This program is similar to [wxPython Shell](https://github.com/wxWidgets/wxPytho
|
|
|
79
79
|
|
|
80
80
|
Extend the TextConsole class and override any of the above to fit your needs.
|
|
81
81
|
|
|
82
|
+
## Keyboard shortcuts
|
|
83
|
+
|
|
84
|
+
| Shortcut | Description |
|
|
85
|
+
|-------------------------|--------------------------------------------------------------------------------------------------|
|
|
86
|
+
| Return | Execute current command; if editing mid-line or multiline, show modal allowing to select either to run the code or insert a linefeed in the cursor position. |
|
|
87
|
+
| Shift Return | Insert a new line within the edited multiline command. |
|
|
88
|
+
| Control Return | Move the cursor to the end of the last line of input. |
|
|
89
|
+
| Tab | Indent code (up to 4 spaces); if selection, indent all selected lines. |
|
|
90
|
+
| Shift Tab | Un-indent (remove up to 4 spaces before cursor). |
|
|
91
|
+
| Down Arrow | Navigate to next command in history. |
|
|
92
|
+
| Up Arrow | Navigate to previous command in history. |
|
|
93
|
+
| Left Arrow | Move to previous non-tagged character. |
|
|
94
|
+
| Right Arrow | Move to next non-tagged character. |
|
|
95
|
+
| Control Left Arrow | Move to previous word. |
|
|
96
|
+
| Control Right Arrow | Move to next word. |
|
|
97
|
+
|
|
98
|
+
| Escape | Jump to last blank command (empty input) in history. |
|
|
99
|
+
| BackSpace | Delete character before cursor. |
|
|
100
|
+
| Control C | Copy selected code, removing prompts first. |
|
|
101
|
+
| Control V | Paste text from clipboard, handling prompts and multiline input. |
|
|
102
|
+
| Button-3 (Right Click) | Show context menu (Cut, Copy, Paste, Clear). |
|
|
103
|
+
| Control Z | Undo last edit (safe, ignores errors). |
|
|
104
|
+
| Control Y | Redo last undone edit (safe, ignores errors). |
|
|
105
|
+
| Control K | Remove the current element from the history. |
|
|
106
|
+
| Home | Move cursor to start of current line. |
|
|
107
|
+
| End | Move cursor to end of current line. |
|
|
108
|
+
| Control + | Increase font size. |
|
|
109
|
+
| Control - | Decrease font size. |
|
|
110
|
+
| Control 0 | Reset font size. |
|
|
111
|
+
|
|
82
112
|
|
|
83
113
|
Full information and usage details at the [text_console GitHub repository](https://github.com/Ircama/text_console).
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.0.3"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|