textual-console 0.1.0__py3-none-any.whl

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,26 @@
1
+ """Console widgets for Textual."""
2
+
3
+ from textual_console._ansi import strip_ansi
4
+ from textual_console._argparse import ArgparseCompleter, set_completer
5
+ from textual_console._autocomplete import (
6
+ CommandAutoComplete,
7
+ CommandItem,
8
+ CompletionHint,
9
+ )
10
+ from textual_console._console_input import ConsoleInput
11
+ from textual_console._console_log import ConsoleLog
12
+ from textual_console._console_output import ConsoleOutput
13
+ from textual_console._history import CommandHistory
14
+
15
+ __all__ = [
16
+ "ArgparseCompleter",
17
+ "CommandAutoComplete",
18
+ "CommandHistory",
19
+ "CommandItem",
20
+ "CompletionHint",
21
+ "ConsoleInput",
22
+ "ConsoleLog",
23
+ "ConsoleOutput",
24
+ "set_completer",
25
+ "strip_ansi",
26
+ ]
@@ -0,0 +1,254 @@
1
+ """Demo app. Run it with ``python -m textual_console``."""
2
+
3
+ import argparse
4
+ import contextlib
5
+ import io
6
+ import shlex
7
+ from collections.abc import Sequence
8
+ from pathlib import Path
9
+
10
+ from rich import box
11
+ from rich.table import Table
12
+ from rich.text import Text
13
+ from textual import on
14
+ from textual.app import App, ComposeResult
15
+ from textual.widgets import Footer, Static
16
+
17
+ from textual_console import (
18
+ ArgparseCompleter,
19
+ CommandAutoComplete,
20
+ ConsoleInput,
21
+ ConsoleLog,
22
+ ConsoleOutput,
23
+ )
24
+
25
+ COLORS = ("red", "green", "yellow", "blue", "magenta", "cyan")
26
+ """The colors of the ``colors`` command."""
27
+
28
+
29
+ def _build_parsers() -> dict[str, argparse.ArgumentParser]:
30
+ """Describe the commands of the demo with argparse parsers.
31
+
32
+ The same parsers drive the completion and the parsing of a command line.
33
+ """
34
+
35
+ def parser(name: str, description: str) -> argparse.ArgumentParser:
36
+ return argparse.ArgumentParser(prog=name, description=description)
37
+
38
+ parsers = {
39
+ "help": parser("help", "Show the commands"),
40
+ "colors": parser("colors", "Write text in some colors"),
41
+ "table": parser("table", "Write a Rich table"),
42
+ "ansi": parser("ansi", "Write text with ANSI escape codes"),
43
+ "flood": parser("flood", "Write many lines to check the scrollback"),
44
+ "clear": parser("clear", "Clear the output"),
45
+ "echo": parser("echo", "Write the text to the output"),
46
+ "quit": parser("quit", "Leave the demo"),
47
+ }
48
+ parsers["colors"].add_argument(
49
+ "--only", nargs="+", choices=COLORS, metavar="COLOR", help="show only these"
50
+ )
51
+ parsers["flood"].add_argument(
52
+ "count",
53
+ nargs="?",
54
+ type=int,
55
+ default=200,
56
+ metavar="COUNT",
57
+ help="how many lines",
58
+ )
59
+ parsers["flood"].add_argument(
60
+ "--style",
61
+ choices=("plain", "red", "green"),
62
+ default="plain",
63
+ help="the style of the lines",
64
+ )
65
+ parsers["echo"].add_argument("text", nargs="*", metavar="TEXT", help="the words")
66
+ return parsers
67
+
68
+
69
+ PARSERS = _build_parsers()
70
+ """The parsers of the commands of the demo, by command name."""
71
+
72
+ COMMANDS = tuple(PARSERS)
73
+ """The commands of the demo."""
74
+
75
+ HINTS = """\
76
+ Try: drag / double / triple-click to select, ctrl+c copy, middle-click paste,
77
+ Tab complete, shift+enter newline, Esc Esc clear.
78
+ Commands: {commands}; add -h for help."""
79
+ """The hints that are shown under the output."""
80
+
81
+
82
+ class ConsoleDemo(App[None]):
83
+ """A small REPL that shows all widgets of the package."""
84
+
85
+ CSS = """
86
+ ConsoleOutput {
87
+ border: round $primary;
88
+ height: 1fr;
89
+ }
90
+ #hints {
91
+ height: auto;
92
+ padding: 0 1;
93
+ color: $text-muted;
94
+ }
95
+ """
96
+
97
+ def __init__(
98
+ self, log_path: Path | None = None, max_lines: int | None = None
99
+ ) -> None:
100
+ """Create the demo.
101
+
102
+ Args:
103
+ log_path: A file that receives a plain-text transcript, or ``None``.
104
+ max_lines: The scrollback limit of the output, or ``None``.
105
+ """
106
+ super().__init__()
107
+ self.log_path = log_path
108
+ self.max_lines = max_lines
109
+ self._transcript = ConsoleLog(log_path) if log_path is not None else None
110
+
111
+ def compose(self) -> ComposeResult:
112
+ """Create the output, the hints, the prompt and the footer."""
113
+ yield ConsoleOutput(
114
+ id="output",
115
+ max_lines=self.max_lines,
116
+ log=self._transcript,
117
+ markup=False,
118
+ )
119
+ yield Static(HINTS.format(commands=", ".join(COMMANDS)), id="hints")
120
+ yield ConsoleInput(
121
+ prompt="> ", placeholder="type a command, then press enter", id="prompt"
122
+ )
123
+ yield CommandAutoComplete("#prompt", ArgparseCompleter(PARSERS))
124
+ yield Footer()
125
+
126
+ def on_mount(self) -> None:
127
+ """Write the welcome text and focus the prompt."""
128
+ output = self.query_one(ConsoleOutput)
129
+ output.write(Text("textual-console demo", style="bold magenta"))
130
+ output.write("Type 'help' to see the commands.")
131
+ if self.log_path is not None:
132
+ output.write(f"Transcript: {self.log_path}")
133
+ if self.max_lines is not None:
134
+ output.write(f"Scrollback is limited to {self.max_lines} lines.")
135
+ self.query_one(ConsoleInput).focus()
136
+
137
+ def on_unmount(self) -> None:
138
+ """Close the transcript."""
139
+ if self._transcript is not None:
140
+ self._transcript.close()
141
+
142
+ def copy_to_clipboard(self, text: str) -> None:
143
+ """Copy the text, and show a notification.
144
+
145
+ ``ctrl+c`` copies through the screen, which does not post
146
+ ``SelectionCopied``, so the notification is made here.
147
+
148
+ Args:
149
+ text: The text to copy.
150
+ """
151
+ super().copy_to_clipboard(text)
152
+ self.notify(f"Copied {len(text)} characters")
153
+
154
+ @on(ConsoleOutput.LogFailed)
155
+ def _notify_log_failure(self, event: ConsoleOutput.LogFailed) -> None:
156
+ self.notify(f"Transcript failed: {event.error}", severity="error")
157
+
158
+ @on(ConsoleInput.Submitted)
159
+ def _run_submit(self, event: ConsoleInput.Submitted) -> None:
160
+ # A submit can hold several lines. This app runs each non-blank line as a
161
+ # command: that is the app's choice, ConsoleInput only gives the text.
162
+ for line in event.value.splitlines():
163
+ if line.strip():
164
+ self._run_line(line)
165
+
166
+ def _run_line(self, line: str) -> None:
167
+ output = self.query_one(ConsoleOutput)
168
+ # The command is echoed into the output, so it also reaches the transcript.
169
+ output.write(Text(f"> {line}", style="bold green"))
170
+ try:
171
+ words = shlex.split(line)
172
+ except ValueError as error:
173
+ output.write(Text(f"Error: {error}", style="red"))
174
+ return
175
+ name, args = words[0], words[1:]
176
+ parser = PARSERS.get(name)
177
+ if parser is None:
178
+ output.write(Text(f"Unknown command: {name}", style="red"))
179
+ return
180
+ # parse_args prints usage and calls sys.exit for -h and for errors. Catch
181
+ # that, and show the text in the output.
182
+ buffer = io.StringIO()
183
+ try:
184
+ with (
185
+ contextlib.redirect_stdout(buffer),
186
+ contextlib.redirect_stderr(buffer),
187
+ ):
188
+ namespace = parser.parse_args(args)
189
+ except SystemExit as exit_:
190
+ style = "red" if exit_.code else ""
191
+ output.write(Text(buffer.getvalue().rstrip(), style=style))
192
+ return
193
+ self._execute(name, namespace)
194
+
195
+ def _execute(self, name: str, args: argparse.Namespace) -> None:
196
+ output = self.query_one(ConsoleOutput)
197
+ if name == "help":
198
+ output.write(f"Commands: {', '.join(COMMANDS)}")
199
+ elif name == "colors":
200
+ for colour in args.only or COLORS:
201
+ output.write(Text(f"{colour} text", style=colour))
202
+ elif name == "table":
203
+ output.write(self._table())
204
+ elif name == "ansi":
205
+ output.write_ansi(
206
+ "\x1b[1;31mbold red\x1b[0m \x1b[4;32munderlined green\x1b[0m"
207
+ )
208
+ elif name == "flood":
209
+ style = "" if args.style == "plain" else args.style
210
+ for number in range(1, args.count + 1):
211
+ output.write(Text(f"line {number}", style=style))
212
+ elif name == "clear":
213
+ output.clear()
214
+ elif name == "echo":
215
+ output.write(" ".join(args.text))
216
+ elif name == "quit":
217
+ self.exit()
218
+
219
+ @staticmethod
220
+ def _table() -> Table:
221
+ table = Table(title="Example", box=box.SIMPLE_HEAVY)
222
+ table.add_column("Name", style="cyan")
223
+ table.add_column("Value", justify="right", style="yellow")
224
+ for name, value in (("alpha", 1), ("beta", 22), ("gamma", 333)):
225
+ table.add_row(name, str(value))
226
+ return table
227
+
228
+
229
+ def main(argv: Sequence[str] | None = None) -> None:
230
+ """Run the demo.
231
+
232
+ Args:
233
+ argv: The command-line arguments. The default is ``sys.argv[1:]``.
234
+ """
235
+ parser = argparse.ArgumentParser(
236
+ prog="python -m textual_console", description="Demo of textual-console."
237
+ )
238
+ parser.add_argument(
239
+ "--log", type=Path, metavar="PATH", help="write a plain-text transcript"
240
+ )
241
+ parser.add_argument(
242
+ "--max-lines",
243
+ type=int,
244
+ metavar="N",
245
+ help="limit the scrollback of the output to N lines",
246
+ )
247
+ args = parser.parse_args(argv)
248
+ if args.max_lines is not None and args.max_lines < 1:
249
+ parser.error("--max-lines must be at least 1")
250
+ ConsoleDemo(log_path=args.log, max_lines=args.max_lines).run()
251
+
252
+
253
+ if __name__ == "__main__":
254
+ main()
@@ -0,0 +1,36 @@
1
+ """Remove terminal escape sequences from text."""
2
+
3
+ import re
4
+
5
+ _ESCAPES = re.compile(
6
+ r"""
7
+ \x1b\[[0-?]*[ -/]*[@-~] # CSI: colors, cursor moves
8
+ | \x1b\].*?(?:\x07|\x1b\\|\Z) # OSC: hyperlinks, titles (BEL or ST)
9
+ | \x1b[ -/]*[0-~] # other ESC sequences
10
+ | [\x00-\x08\x0a-\x1f\x7f] # C0 controls and DEL, except tab
11
+ """,
12
+ re.VERBOSE | re.DOTALL,
13
+ )
14
+
15
+
16
+ def strip_ansi(text: str) -> str:
17
+ r"""Remove escape sequences and control characters from text.
18
+
19
+ This removes CSI sequences (which include SGR color codes), OSC sequences
20
+ ended by BEL or ``ESC \``, other ``ESC`` sequences, and the C0 control
21
+ characters except tab. That includes ``\r`` and ``\n``, so pass one line
22
+ at a time.
23
+
24
+ Args:
25
+ text: Text that may hold raw escape codes.
26
+
27
+ Returns:
28
+ The plain text.
29
+
30
+ Example:
31
+ >>> strip_ansi("\x1b[1;31merror\x1b[0m:\tfailed")
32
+ 'error:\tfailed'
33
+ >>> strip_ansi("\x1b]8;;http://x.io\x07link\x1b]8;;\x07")
34
+ 'link'
35
+ """
36
+ return _ESCAPES.sub("", text)