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.
@@ -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
- 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.
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 includes options for overriding specific variables through subclassing, by extending the `TextConsole` class.
5
+ It can also be used as a standalone Python command interpreter.
6
6
 
7
- The package is installable via pip.
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
- - Tkinter-based package.
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
- The package provides flexibility to customize:
13
+ Send single- or multi-line Python code to the interpreter and see results immediately.
18
14
 
19
- - `history_file`: Change the location of the history file
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
- Each of these can be customized by subclassing TextConsole and overriding the appropriate attributes or methods.
17
+ Access and modify your app’s variables and functions via the console_locals namespace.
27
18
 
28
- ## Installation
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
- ### From pypi
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
- ### From sources
37
-
38
- Navigate to the directory containing *setup.py*. Run:
59
+ ## Playground
39
60
 
40
61
  ```
41
- pip install .
62
+ python -m text_console
63
+ ```
64
+
65
+ Available options:
66
+
42
67
  ```
68
+ Python Console [-h] [-V]
43
69
 
44
- ## Usage
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 = 0
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("input_color", foreground="blue")
124
+ self.tag_configure("output", foreground="#00178c")
122
125
 
123
126
  def setup_bindings(self):
124
127
  """Set up key bindings"""
125
- self.bind('<Control-Return>', self.on_ctrl_return)
126
- self.bind('<Shift-Return>', self.on_shift_return)
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.clear_console)
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("1.0", self._prompt1) # Reinsert the prompt
281
- self.delete('input', 'insert lineend')
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
- self.insert("insert", txt)
506
- self.insert_cmd(self.get("input", "end"))
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 on_key_press(self, event):
518
- """Prevent text insertion in command history"""
519
- if self.compare('insert', '<', 'input') and event.keysym not in ['Left', 'Right']:
520
- self._hist_item = len(self.history)
521
- if not event.char.isalnum():
522
- return 'break'
523
- else:
524
- if event.keysym not in ['Return']:
525
- self.tag_add("input_color", "input", "insert lineend")
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 on_key_release(self, event):
528
- """Reset history scrolling"""
529
- if self.compare('insert', '<', 'input') and event.keysym not in ['Left', 'Right']:
530
- self._hist_item = len(self.history)
531
- return 'break'
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
- if event.keysym not in ['Return']:
534
- self.tag_add("input_color", "input", "insert lineend")
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 on_shift_return(self, event):
628
- """Handle Shift+Return key press"""
629
- if self.compare('insert', '<', 'input'):
630
- self.mark_set('insert', 'input lineend')
631
- return 'break'
632
- else: # execute commands
633
- self.mark_set('insert', 'end')
634
- self.insert('insert', '\n')
635
- self.insert('insert', self._prompt2, 'prompt')
636
- self.eval_current(True)
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
- if self.compare('insert', '<', 'input'):
641
- self.mark_set('insert', 'input lineend')
642
- return 'break'
643
- else:
644
- # Check if we're at the end of the input (last line, end position)
645
- input_end = self.index('end-1c') # End of all text
646
- insert_pos = self.index('insert')
647
-
648
- # If we're not at the very end of the input, just insert a new line
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 on_ctrl_return(self, event=None):
993
+ def insert_line(self, event=None):
663
994
  """Handle Ctrl+Return key press"""
664
- self.insert('insert', '\n' + self._prompt2, 'prompt')
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:], 'input_color')
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', 'input_color')
1031
+ self.insert('insert', '\n')
700
1032
  self.prompt(True)
701
- self.insert('insert', line, 'input_color')
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).
@@ -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