textual-console 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ .claude/
2
+ .herdr/
3
+ .venv/
4
+ venv/
5
+ __pycache__/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ .DS_Store
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (unreleased)
4
+
5
+ - `ConsoleOutput`: Rich output with drag, word and line selection, and OSC 52 copy.
6
+ - `ConsoleInput` and `CommandHistory`: a `TextArea` command prompt that grows up to
7
+ `max_height` rows, with a `prompt` gutter, a `suggester`, history
8
+ (`command_history`), newline keys (shift+enter, alt+enter, ctrl+j), a quiet Tab and
9
+ middle-click paste of all selected lines.
10
+ - `ConsoleInput`: double Escape clears the input (`double_escape_clear`).
11
+ - `CommandAutoComplete`: completes the last word of the cursor's line and keeps quotes.
12
+ Tab opens the dropdown.
13
+ - `CommandItem` and `CompletionHint`: dropdown rows with a help column, and a hint row.
14
+ - `ArgparseCompleter` and `set_completer`: completion from `argparse` parsers.
15
+ - `ConsoleLog` and `strip_ansi`: plain-text transcript of the output.
16
+ - Demo (`python -m textual_console`, with `--log PATH` and `--max-lines N`) and
17
+ examples.
18
+ - Project skeleton and CI.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 thespicybyte
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,308 @@
1
+ Metadata-Version: 2.5
2
+ Name: textual-console
3
+ Version: 0.1.0
4
+ Summary: Console widgets for Textual: color output with word/line mouse selection and OSC 52 copy, a command input with history and autocomplete, and a plain-text transcript.
5
+ Project-URL: Repository, https://github.com/thespicybyte/textual-console
6
+ Author-email: spicybyte <spicybyte@git.sbs>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Environment :: Console
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: rich>=14
16
+ Requires-Dist: textual-autocomplete<4.1,>=4.0.6
17
+ Requires-Dist: textual<9,>=8.2
18
+ Description-Content-Type: text/markdown
19
+
20
+ # textual-console
21
+
22
+ Console widgets for [Textual](https://textual.textualize.io/): color output with
23
+ word and line mouse selection and OSC 52 copy, a multi-line command input with
24
+ history and autocomplete (also from `argparse` parsers), and a plain-text transcript.
25
+
26
+ ![The textual-console demo](https://raw.githubusercontent.com/thespicybyte/textual-console/main/docs/demo.svg)
27
+
28
+ - `ConsoleOutput`: read-only output for Rich renderables. Drag, double-click (word)
29
+ and triple-click (line) select text. `ctrl+c` copies it.
30
+ - `ConsoleInput`: a command prompt (a `TextArea`) that grows with its text, with a prompt
31
+ string, history, a quiet Tab and middle-click paste.
32
+ - `CommandAutoComplete`: completes the last word of the cursor's line. Tab opens it.
33
+ - `ArgparseCompleter`: gives `CommandAutoComplete` its candidates from `argparse` parsers.
34
+ - `ConsoleLog`: a plain-text transcript file of the output.
35
+
36
+ Python 3.11 or later. Tested on Textual 8.x.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install textual-console
42
+ ```
43
+
44
+ Try the demo:
45
+
46
+ ```bash
47
+ python -m textual_console
48
+ python -m textual_console --log session.log --max-lines 200
49
+ ```
50
+
51
+ ## Quick start
52
+
53
+ ```python
54
+ from rich.text import Text
55
+ from textual import on
56
+ from textual.app import App, ComposeResult
57
+
58
+ from textual_console import CommandAutoComplete, ConsoleInput, ConsoleOutput
59
+
60
+
61
+ class Repl(App[None]):
62
+ def compose(self) -> ComposeResult:
63
+ yield ConsoleOutput(id="output", max_lines=5000)
64
+ yield ConsoleInput(prompt="> ", placeholder="command", id="prompt")
65
+ yield CommandAutoComplete("#prompt", lambda line: ["help", "quit"])
66
+
67
+ @on(ConsoleInput.Submitted)
68
+ def run_command(self, event: ConsoleInput.Submitted) -> None:
69
+ self.query_one(ConsoleOutput).write(Text(f"> {event.value}", style="green"))
70
+
71
+
72
+ Repl().run()
73
+ ```
74
+
75
+ More programs are in [`examples/`](https://github.com/thespicybyte/textual-console/tree/main/examples): `basic.py` (text, tables, copy),
76
+ `repl.py` (all widgets, argparse completion and a transcript) and `ansi_stream.py` (ANSI output of a
77
+ subprocess).
78
+
79
+ ## ConsoleOutput
80
+
81
+ A `RichLog` that takes part in the Textual screen selection.
82
+
83
+ ```python
84
+ output = ConsoleOutput(max_lines=5000, copy_on_select=False)
85
+ output.write(Text("done", style="bold green"))
86
+ output.write_ansi("\x1b[31mred\x1b[0m")
87
+ ```
88
+
89
+ - Drag to select. The view scrolls at the edges.
90
+ - Double-click selects a word. Triple-click selects a line.
91
+ - `ctrl+c` copies the selection with `App.copy_to_clipboard` (OSC 52). `Escape` or a
92
+ single click clears the selection.
93
+ - `copy_on_select=True` copies when a selection ends and posts `ConsoleOutput.SelectionCopied`.
94
+ `ctrl+c` does not post `SelectionCopied`: only `copy_on_select` and `copy_selection()` do.
95
+ To react to every copy, override `App.copy_to_clipboard` (the demo does this to show a
96
+ notification).
97
+ - The widget cannot take focus, so the focus stays on your prompt.
98
+ - Copied text has one line for each rendered line, without trailing spaces.
99
+ - **Set `max_lines` for long-running apps.** The default is no limit, so memory grows
100
+ with the session.
101
+ - To change the highlight, style the `console-output--selection` component class.
102
+ If you do not, the theme `screen--selection` colors are used.
103
+
104
+ ## ConsoleInput
105
+
106
+ A `TextArea` that works as a command prompt. It is one row high when it is empty, grows
107
+ row by row up to `max_height` rows (the default is 10), and then scrolls.
108
+
109
+ ```python
110
+ history = CommandHistory(["help", "quit"])
111
+ prompt = ConsoleInput(prompt="> ", history=history, max_height=10)
112
+ ```
113
+
114
+ | Key | Action |
115
+ |---|---|
116
+ | Enter | Submit the whole text as one command. |
117
+ | shift+enter, alt+enter, ctrl+j | Insert a newline. |
118
+ | Up, down | Move the cursor between rows. On the first row up walks the history, and on the last row down does. |
119
+ | Tab | Nothing, so the focus stays in the prompt. Use `tab_moves_focus=True` to move the focus. |
120
+ | Esc Esc | Clear the input if both presses come within `double_escape_clear` seconds (default 0.5). Undo with ctrl+z. An Escape that closes the dropdown does not count. |
121
+ | Right | Accept the ghost text of a `suggester`. |
122
+
123
+ - `prompt` is drawn in the left gutter of the first row. It is not part of the text. Other
124
+ rows get blank padding of the same width. It can change at run time, and the
125
+ `console-input--prompt` component class styles it.
126
+ - `suggester` is a Textual `Suggester`. While the text is one line and the cursor is at
127
+ its end, the suggestion shows as ghost text.
128
+ - `max_height` is checked when the widget is created (a value below 1 raises `ValueError`)
129
+ and applied when the widget is mounted. `max_height=None` sets no limit, so a CSS
130
+ `max-height` applies.
131
+ - The text is `prompt.value`. The history is `prompt.command_history`, a `CommandHistory`.
132
+ Save it with `prompt.command_history.entries`.
133
+ - The first up saves the text you typed, and down past the newest entry restores it. A
134
+ recalled multi-line command is shown with all its rows.
135
+ - Submit records the command, clears the input (unless `clear_on_submit=False`) and posts
136
+ `ConsoleInput.Submitted` with `value`, the whole text.
137
+ - A `read_only` input keeps its text: paste, history, submit, completion and Esc Esc do
138
+ nothing.
139
+ - `ConsoleInput.Changed` is `TextArea.Changed`: use `textual.widgets.TextArea.Changed`.
140
+ - `ConsoleInput` is not an `Input` any more. The arguments that only `Input` has
141
+ (`password`, `restrict`, `type`, `max_length`, `validators`, `validate_on`,
142
+ `valid_empty`, `select_on_focus`) raise `TypeError`. The editor settings of `TextArea`
143
+ (`soft_wrap`, line numbers, `tab_behavior`) are fixed.
144
+
145
+ ### Multi-line text
146
+
147
+ **Enter submits the whole text, newlines included, as one command.** One submit gives one
148
+ `Submitted` message and one history entry. To run each line as its own command, split the
149
+ value in your handler. This is the choice of your app, not of the widget:
150
+
151
+ ```python
152
+ @on(ConsoleInput.Submitted)
153
+ def run_commands(self, event: ConsoleInput.Submitted) -> None:
154
+ for line in event.value.splitlines():
155
+ if line.strip():
156
+ self.run_command(line)
157
+ ```
158
+
159
+ **Terminals and the newline keys.** Many terminals send `shift+enter` as a plain Enter,
160
+ so the app cannot see the difference. Only terminals with the kitty keyboard protocol (or
161
+ a similar extension) report it. `alt+enter` and `ctrl+j` work in more terminals. Use
162
+ them as the fallback. In herdr, `shift+enter` arrives as Enter and submits.
163
+
164
+ ### Middle-click paste
165
+
166
+ **Middle-click pastes text from inside the app.** It pastes the text selected in the
167
+ output. If nothing is selected, it pastes the last text that the app copied.
168
+ **All lines of a multi-line selection are pasted.**
169
+ **To paste text from other apps, use the terminal paste** (`cmd+v` or `ctrl+shift+v`).
170
+ It keeps all lines too. `ctrl+v` is different: it pastes the last text that the app
171
+ copied (`app.clipboard`), with all lines, and not the system clipboard.
172
+
173
+ Some terminals use the middle-click for their own paste and do not send it to the
174
+ app. Then you get the terminal paste. Check 4 in
175
+ [docs/manual-testing.md](https://github.com/thespicybyte/textual-console/blob/main/docs/manual-testing.md)
176
+ shows which one your terminal does.
177
+
178
+ ## CommandAutoComplete
179
+
180
+ ```python
181
+ CommandAutoComplete("#prompt", lambda line: ["open", "close", "help"])
182
+ ```
183
+
184
+ The function gets the line of the cursor and returns the options for its last word. The
185
+ text before the last word stays as typed. A completion that needs quotes is quoted. In a
186
+ multi-line input only the cursor's line is used, and a completion changes only that line.
187
+
188
+ - **Automatic opening:** the dropdown opens while you type a word. It does not open after
189
+ a space, in an empty line, or while you walk the history.
190
+ - **Tab opens the dropdown** when it is closed, also after a space. If there is one
191
+ option, Tab applies it at once. If there are none, Tab does what the input does.
192
+ - **Key order:** when the dropdown is open, up and down move in the dropdown, and Tab or
193
+ Enter accept the option. When it is closed, up and down move between rows or walk the
194
+ history.
195
+ - **Items:** a function can return strings, `DropdownItem`s, `CommandItem(value, help)`
196
+ (shows `help` in a dim second column, aligned) and `CompletionHint(text)` (a dim,
197
+ disabled row that tells what to type. It is never applied).
198
+ - **The candidate source is synchronous.** It runs on the event loop. If the source is
199
+ slow (files, network), cache the result.
200
+ - The package needs `textual-autocomplete` 4.0.x, because `CommandAutoComplete` overrides
201
+ some of its private methods.
202
+
203
+ ## Argparse completion
204
+
205
+ `ArgparseCompleter` reads `argparse` parsers and gives the candidates for the word at the
206
+ cursor. It never calls `parse_args`, so nothing prints and nothing exits.
207
+
208
+ ```python
209
+ import argparse
210
+
211
+ from textual_console import ArgparseCompleter, CommandAutoComplete, set_completer
212
+
213
+ flood = argparse.ArgumentParser(prog="flood", description="Write many lines")
214
+ flood.add_argument("count", nargs="?", type=int, metavar="COUNT", help="how many lines")
215
+ flood.add_argument("--style", choices=["plain", "red", "green"], help="line style")
216
+ open_ = argparse.ArgumentParser(prog="open", description="Open a file")
217
+ set_completer(open_.add_argument("file", metavar="FILE", help="the file"), list_files)
218
+
219
+ parsers = {"flood": flood, "open": open_}
220
+ CommandAutoComplete("#prompt", ArgparseCompleter(parsers))
221
+ ```
222
+
223
+ - Describe the commands with a mapping `{name: parser}` (read on every call, so you can
224
+ add commands later) or with one root parser that has subparsers.
225
+ - It offers the command names, the options not used yet (with their help), the `choices`
226
+ of an option or positional (also after `--option=`), and the next positional, counted
227
+ with `nargs`. It descends into subparsers. After a lone `--` it offers only positionals.
228
+ - `set_completer(action, function)` attaches a function that gets the current word and
229
+ returns strings or items. It is stored in `action.completer`, the name that
230
+ `argcomplete` uses. If it raises, there are no candidates and the error is logged.
231
+ - `hints=True` (the default) adds a `CompletionHint` such as `FILE: the file` where the
232
+ user must type a value. `show_help_option=True` offers `-h` and `--help`.
233
+ - It reads some private `argparse` attributes that have been stable since Python 3.2. If
234
+ they change, completion is reduced and the error is logged.
235
+
236
+ **Run the command with the same parsers.** `parse_args` prints for `-h` and on errors, and
237
+ then raises `SystemExit`. Catch it and write the text to the output, so the app keeps
238
+ running:
239
+
240
+ ```python
241
+ import contextlib
242
+ import io
243
+ import shlex
244
+
245
+ name, *args = shlex.split(line)
246
+ buffer = io.StringIO()
247
+ try:
248
+ with contextlib.redirect_stdout(buffer), contextlib.redirect_stderr(buffer):
249
+ namespace = parsers[name].parse_args(args)
250
+ except SystemExit:
251
+ output.write(buffer.getvalue())
252
+ ```
253
+
254
+ The demo (`python -m textual_console`) does this, so `flood -h` shows the usage.
255
+
256
+ ## ConsoleLog
257
+
258
+ A plain-text copy of the output, with ANSI codes removed.
259
+
260
+ ```python
261
+ log = ConsoleLog("session.log")
262
+ yield ConsoleOutput(log=log)
263
+ ```
264
+
265
+ `ConsoleInput` does not write to the log. **To put commands in the transcript, echo
266
+ them into the output.** Then each command is written once, in order with its result:
267
+
268
+ ```python
269
+ @on(ConsoleInput.Submitted)
270
+ def run_command(self, event: ConsoleInput.Submitted) -> None:
271
+ output = self.query_one(ConsoleOutput)
272
+ output.write(f"> {event.value}")
273
+ ...
274
+ ```
275
+
276
+ `ConsoleOutput.clear()` does not change the file. If the file cannot be written,
277
+ `ConsoleOutput.LogFailed` is posted once and the app keeps running. `strip_ansi(text)`
278
+ is also public.
279
+
280
+ ## Terminals and OSC 52
281
+
282
+ Copy uses OSC 52, which asks the terminal to set the clipboard. The app cannot detect
283
+ whether it worked. `ctrl+c` is the reliable key. `cmd+c` reaches the app only on
284
+ terminals with the kitty keyboard protocol. Native terminal selection (shift-drag or
285
+ option-drag) is not changed.
286
+
287
+ Maintainers test these things by hand in each terminal. The steps and the results table
288
+ are in [docs/manual-testing.md](https://github.com/thespicybyte/textual-console/blob/main/docs/manual-testing.md).
289
+
290
+ **tmux:** add `set -g set-clipboard on` to `~/.tmux.conf`. Without it, OSC 52 copy does
291
+ not reach the outer terminal. Some terminals limit the size of the copied text or ask
292
+ for permission first.
293
+
294
+ ## Development
295
+
296
+ ```bash
297
+ uv sync --group test --group lint
298
+ uv run pytest
299
+ uv run ruff check . && uv run ruff format --check . && uv run mypy && uv run pyright
300
+ ```
301
+
302
+ Repository: <https://github.com/thespicybyte/textual-console>
303
+
304
+ For maintainers: see `RELEASING.md` for the release steps.
305
+
306
+ ## License
307
+
308
+ MIT. See `LICENSE`.
@@ -0,0 +1,289 @@
1
+ # textual-console
2
+
3
+ Console widgets for [Textual](https://textual.textualize.io/): color output with
4
+ word and line mouse selection and OSC 52 copy, a multi-line command input with
5
+ history and autocomplete (also from `argparse` parsers), and a plain-text transcript.
6
+
7
+ ![The textual-console demo](https://raw.githubusercontent.com/thespicybyte/textual-console/main/docs/demo.svg)
8
+
9
+ - `ConsoleOutput`: read-only output for Rich renderables. Drag, double-click (word)
10
+ and triple-click (line) select text. `ctrl+c` copies it.
11
+ - `ConsoleInput`: a command prompt (a `TextArea`) that grows with its text, with a prompt
12
+ string, history, a quiet Tab and middle-click paste.
13
+ - `CommandAutoComplete`: completes the last word of the cursor's line. Tab opens it.
14
+ - `ArgparseCompleter`: gives `CommandAutoComplete` its candidates from `argparse` parsers.
15
+ - `ConsoleLog`: a plain-text transcript file of the output.
16
+
17
+ Python 3.11 or later. Tested on Textual 8.x.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install textual-console
23
+ ```
24
+
25
+ Try the demo:
26
+
27
+ ```bash
28
+ python -m textual_console
29
+ python -m textual_console --log session.log --max-lines 200
30
+ ```
31
+
32
+ ## Quick start
33
+
34
+ ```python
35
+ from rich.text import Text
36
+ from textual import on
37
+ from textual.app import App, ComposeResult
38
+
39
+ from textual_console import CommandAutoComplete, ConsoleInput, ConsoleOutput
40
+
41
+
42
+ class Repl(App[None]):
43
+ def compose(self) -> ComposeResult:
44
+ yield ConsoleOutput(id="output", max_lines=5000)
45
+ yield ConsoleInput(prompt="> ", placeholder="command", id="prompt")
46
+ yield CommandAutoComplete("#prompt", lambda line: ["help", "quit"])
47
+
48
+ @on(ConsoleInput.Submitted)
49
+ def run_command(self, event: ConsoleInput.Submitted) -> None:
50
+ self.query_one(ConsoleOutput).write(Text(f"> {event.value}", style="green"))
51
+
52
+
53
+ Repl().run()
54
+ ```
55
+
56
+ More programs are in [`examples/`](https://github.com/thespicybyte/textual-console/tree/main/examples): `basic.py` (text, tables, copy),
57
+ `repl.py` (all widgets, argparse completion and a transcript) and `ansi_stream.py` (ANSI output of a
58
+ subprocess).
59
+
60
+ ## ConsoleOutput
61
+
62
+ A `RichLog` that takes part in the Textual screen selection.
63
+
64
+ ```python
65
+ output = ConsoleOutput(max_lines=5000, copy_on_select=False)
66
+ output.write(Text("done", style="bold green"))
67
+ output.write_ansi("\x1b[31mred\x1b[0m")
68
+ ```
69
+
70
+ - Drag to select. The view scrolls at the edges.
71
+ - Double-click selects a word. Triple-click selects a line.
72
+ - `ctrl+c` copies the selection with `App.copy_to_clipboard` (OSC 52). `Escape` or a
73
+ single click clears the selection.
74
+ - `copy_on_select=True` copies when a selection ends and posts `ConsoleOutput.SelectionCopied`.
75
+ `ctrl+c` does not post `SelectionCopied`: only `copy_on_select` and `copy_selection()` do.
76
+ To react to every copy, override `App.copy_to_clipboard` (the demo does this to show a
77
+ notification).
78
+ - The widget cannot take focus, so the focus stays on your prompt.
79
+ - Copied text has one line for each rendered line, without trailing spaces.
80
+ - **Set `max_lines` for long-running apps.** The default is no limit, so memory grows
81
+ with the session.
82
+ - To change the highlight, style the `console-output--selection` component class.
83
+ If you do not, the theme `screen--selection` colors are used.
84
+
85
+ ## ConsoleInput
86
+
87
+ A `TextArea` that works as a command prompt. It is one row high when it is empty, grows
88
+ row by row up to `max_height` rows (the default is 10), and then scrolls.
89
+
90
+ ```python
91
+ history = CommandHistory(["help", "quit"])
92
+ prompt = ConsoleInput(prompt="> ", history=history, max_height=10)
93
+ ```
94
+
95
+ | Key | Action |
96
+ |---|---|
97
+ | Enter | Submit the whole text as one command. |
98
+ | shift+enter, alt+enter, ctrl+j | Insert a newline. |
99
+ | Up, down | Move the cursor between rows. On the first row up walks the history, and on the last row down does. |
100
+ | Tab | Nothing, so the focus stays in the prompt. Use `tab_moves_focus=True` to move the focus. |
101
+ | Esc Esc | Clear the input if both presses come within `double_escape_clear` seconds (default 0.5). Undo with ctrl+z. An Escape that closes the dropdown does not count. |
102
+ | Right | Accept the ghost text of a `suggester`. |
103
+
104
+ - `prompt` is drawn in the left gutter of the first row. It is not part of the text. Other
105
+ rows get blank padding of the same width. It can change at run time, and the
106
+ `console-input--prompt` component class styles it.
107
+ - `suggester` is a Textual `Suggester`. While the text is one line and the cursor is at
108
+ its end, the suggestion shows as ghost text.
109
+ - `max_height` is checked when the widget is created (a value below 1 raises `ValueError`)
110
+ and applied when the widget is mounted. `max_height=None` sets no limit, so a CSS
111
+ `max-height` applies.
112
+ - The text is `prompt.value`. The history is `prompt.command_history`, a `CommandHistory`.
113
+ Save it with `prompt.command_history.entries`.
114
+ - The first up saves the text you typed, and down past the newest entry restores it. A
115
+ recalled multi-line command is shown with all its rows.
116
+ - Submit records the command, clears the input (unless `clear_on_submit=False`) and posts
117
+ `ConsoleInput.Submitted` with `value`, the whole text.
118
+ - A `read_only` input keeps its text: paste, history, submit, completion and Esc Esc do
119
+ nothing.
120
+ - `ConsoleInput.Changed` is `TextArea.Changed`: use `textual.widgets.TextArea.Changed`.
121
+ - `ConsoleInput` is not an `Input` any more. The arguments that only `Input` has
122
+ (`password`, `restrict`, `type`, `max_length`, `validators`, `validate_on`,
123
+ `valid_empty`, `select_on_focus`) raise `TypeError`. The editor settings of `TextArea`
124
+ (`soft_wrap`, line numbers, `tab_behavior`) are fixed.
125
+
126
+ ### Multi-line text
127
+
128
+ **Enter submits the whole text, newlines included, as one command.** One submit gives one
129
+ `Submitted` message and one history entry. To run each line as its own command, split the
130
+ value in your handler. This is the choice of your app, not of the widget:
131
+
132
+ ```python
133
+ @on(ConsoleInput.Submitted)
134
+ def run_commands(self, event: ConsoleInput.Submitted) -> None:
135
+ for line in event.value.splitlines():
136
+ if line.strip():
137
+ self.run_command(line)
138
+ ```
139
+
140
+ **Terminals and the newline keys.** Many terminals send `shift+enter` as a plain Enter,
141
+ so the app cannot see the difference. Only terminals with the kitty keyboard protocol (or
142
+ a similar extension) report it. `alt+enter` and `ctrl+j` work in more terminals. Use
143
+ them as the fallback. In herdr, `shift+enter` arrives as Enter and submits.
144
+
145
+ ### Middle-click paste
146
+
147
+ **Middle-click pastes text from inside the app.** It pastes the text selected in the
148
+ output. If nothing is selected, it pastes the last text that the app copied.
149
+ **All lines of a multi-line selection are pasted.**
150
+ **To paste text from other apps, use the terminal paste** (`cmd+v` or `ctrl+shift+v`).
151
+ It keeps all lines too. `ctrl+v` is different: it pastes the last text that the app
152
+ copied (`app.clipboard`), with all lines, and not the system clipboard.
153
+
154
+ Some terminals use the middle-click for their own paste and do not send it to the
155
+ app. Then you get the terminal paste. Check 4 in
156
+ [docs/manual-testing.md](https://github.com/thespicybyte/textual-console/blob/main/docs/manual-testing.md)
157
+ shows which one your terminal does.
158
+
159
+ ## CommandAutoComplete
160
+
161
+ ```python
162
+ CommandAutoComplete("#prompt", lambda line: ["open", "close", "help"])
163
+ ```
164
+
165
+ The function gets the line of the cursor and returns the options for its last word. The
166
+ text before the last word stays as typed. A completion that needs quotes is quoted. In a
167
+ multi-line input only the cursor's line is used, and a completion changes only that line.
168
+
169
+ - **Automatic opening:** the dropdown opens while you type a word. It does not open after
170
+ a space, in an empty line, or while you walk the history.
171
+ - **Tab opens the dropdown** when it is closed, also after a space. If there is one
172
+ option, Tab applies it at once. If there are none, Tab does what the input does.
173
+ - **Key order:** when the dropdown is open, up and down move in the dropdown, and Tab or
174
+ Enter accept the option. When it is closed, up and down move between rows or walk the
175
+ history.
176
+ - **Items:** a function can return strings, `DropdownItem`s, `CommandItem(value, help)`
177
+ (shows `help` in a dim second column, aligned) and `CompletionHint(text)` (a dim,
178
+ disabled row that tells what to type. It is never applied).
179
+ - **The candidate source is synchronous.** It runs on the event loop. If the source is
180
+ slow (files, network), cache the result.
181
+ - The package needs `textual-autocomplete` 4.0.x, because `CommandAutoComplete` overrides
182
+ some of its private methods.
183
+
184
+ ## Argparse completion
185
+
186
+ `ArgparseCompleter` reads `argparse` parsers and gives the candidates for the word at the
187
+ cursor. It never calls `parse_args`, so nothing prints and nothing exits.
188
+
189
+ ```python
190
+ import argparse
191
+
192
+ from textual_console import ArgparseCompleter, CommandAutoComplete, set_completer
193
+
194
+ flood = argparse.ArgumentParser(prog="flood", description="Write many lines")
195
+ flood.add_argument("count", nargs="?", type=int, metavar="COUNT", help="how many lines")
196
+ flood.add_argument("--style", choices=["plain", "red", "green"], help="line style")
197
+ open_ = argparse.ArgumentParser(prog="open", description="Open a file")
198
+ set_completer(open_.add_argument("file", metavar="FILE", help="the file"), list_files)
199
+
200
+ parsers = {"flood": flood, "open": open_}
201
+ CommandAutoComplete("#prompt", ArgparseCompleter(parsers))
202
+ ```
203
+
204
+ - Describe the commands with a mapping `{name: parser}` (read on every call, so you can
205
+ add commands later) or with one root parser that has subparsers.
206
+ - It offers the command names, the options not used yet (with their help), the `choices`
207
+ of an option or positional (also after `--option=`), and the next positional, counted
208
+ with `nargs`. It descends into subparsers. After a lone `--` it offers only positionals.
209
+ - `set_completer(action, function)` attaches a function that gets the current word and
210
+ returns strings or items. It is stored in `action.completer`, the name that
211
+ `argcomplete` uses. If it raises, there are no candidates and the error is logged.
212
+ - `hints=True` (the default) adds a `CompletionHint` such as `FILE: the file` where the
213
+ user must type a value. `show_help_option=True` offers `-h` and `--help`.
214
+ - It reads some private `argparse` attributes that have been stable since Python 3.2. If
215
+ they change, completion is reduced and the error is logged.
216
+
217
+ **Run the command with the same parsers.** `parse_args` prints for `-h` and on errors, and
218
+ then raises `SystemExit`. Catch it and write the text to the output, so the app keeps
219
+ running:
220
+
221
+ ```python
222
+ import contextlib
223
+ import io
224
+ import shlex
225
+
226
+ name, *args = shlex.split(line)
227
+ buffer = io.StringIO()
228
+ try:
229
+ with contextlib.redirect_stdout(buffer), contextlib.redirect_stderr(buffer):
230
+ namespace = parsers[name].parse_args(args)
231
+ except SystemExit:
232
+ output.write(buffer.getvalue())
233
+ ```
234
+
235
+ The demo (`python -m textual_console`) does this, so `flood -h` shows the usage.
236
+
237
+ ## ConsoleLog
238
+
239
+ A plain-text copy of the output, with ANSI codes removed.
240
+
241
+ ```python
242
+ log = ConsoleLog("session.log")
243
+ yield ConsoleOutput(log=log)
244
+ ```
245
+
246
+ `ConsoleInput` does not write to the log. **To put commands in the transcript, echo
247
+ them into the output.** Then each command is written once, in order with its result:
248
+
249
+ ```python
250
+ @on(ConsoleInput.Submitted)
251
+ def run_command(self, event: ConsoleInput.Submitted) -> None:
252
+ output = self.query_one(ConsoleOutput)
253
+ output.write(f"> {event.value}")
254
+ ...
255
+ ```
256
+
257
+ `ConsoleOutput.clear()` does not change the file. If the file cannot be written,
258
+ `ConsoleOutput.LogFailed` is posted once and the app keeps running. `strip_ansi(text)`
259
+ is also public.
260
+
261
+ ## Terminals and OSC 52
262
+
263
+ Copy uses OSC 52, which asks the terminal to set the clipboard. The app cannot detect
264
+ whether it worked. `ctrl+c` is the reliable key. `cmd+c` reaches the app only on
265
+ terminals with the kitty keyboard protocol. Native terminal selection (shift-drag or
266
+ option-drag) is not changed.
267
+
268
+ Maintainers test these things by hand in each terminal. The steps and the results table
269
+ are in [docs/manual-testing.md](https://github.com/thespicybyte/textual-console/blob/main/docs/manual-testing.md).
270
+
271
+ **tmux:** add `set -g set-clipboard on` to `~/.tmux.conf`. Without it, OSC 52 copy does
272
+ not reach the outer terminal. Some terminals limit the size of the copied text or ask
273
+ for permission first.
274
+
275
+ ## Development
276
+
277
+ ```bash
278
+ uv sync --group test --group lint
279
+ uv run pytest
280
+ uv run ruff check . && uv run ruff format --check . && uv run mypy && uv run pyright
281
+ ```
282
+
283
+ Repository: <https://github.com/thespicybyte/textual-console>
284
+
285
+ For maintainers: see `RELEASING.md` for the release steps.
286
+
287
+ ## License
288
+
289
+ MIT. See `LICENSE`.