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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: text_console
3
- Version: 2.0.3
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 is similar to [wxPython Shell](https://github.com/wxWidgets/wxPython-Classic/blob/master/wx/py/shell.py), implemented using Tkinter.
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 is similar to [wxPython Shell](https://github.com/wxWidgets/wxPython-Classic/blob/master/wx/py/shell.py), implemented using Tkinter.
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
@@ -0,0 +1,5 @@
1
+ from .text_console import BaseTextConsole
2
+ from .history import History
3
+ from .__version__ import __version__
4
+
5
+ __all__ = ["BaseTextConsole", "History"]
@@ -3,11 +3,11 @@ import argparse
3
3
  import tkinter as tk
4
4
  import webbrowser
5
5
 
6
- from . import TextConsole
6
+ from . import BaseTextConsole
7
7
  from .__version__ import __version__
8
8
 
9
9
 
10
- class TkTextConsole(TextConsole):
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.dump_history
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
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 is similar to [wxPython Shell](https://github.com/wxWidgets/wxPython-Classic/blob/master/wx/py/shell.py), implemented using Tkinter.
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,6 +4,7 @@ setup.py
4
4
  text_console/__init__.py
5
5
  text_console/__main__.py
6
6
  text_console/__version__.py
7
+ text_console/command_history.py
7
8
  text_console/history.py
8
9
  text_console/text_console.py
9
10
  text_console.egg-info/PKG-INFO
@@ -1,5 +0,0 @@
1
- from .text_console import TextConsole
2
- from .history import History
3
- from .__version__ import __version__
4
-
5
- __all__ = ["TextConsole", "History"]
@@ -1 +0,0 @@
1
- __version__ = "2.0.3"
File without changes
File without changes
File without changes