Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
- Bug Fixes
- Fixed `@with_annotated` command `--help` output bypassing the application's output stream,
leaving redirected help files empty and preventing help from being captured by embedded apps.
- Enhancements
- Added the
[textual_tty_embed.py](https://github.com/python-cmd2/cmd2/blob/main/examples/textual_tty_embed.py)
example, which embeds a `cmd2` application in a [Textual](https://textual.textualize.io/)
application using [textual-tty](https://github.com/ttygroup/textual-tty). The Textual
application shows a realtime status panel beside the `cmd2` application, which can display
much richer information than a bottom toolbar. See the
[Embedding in Textual](docs/examples/embedding_in_textual.md) documentation for details.

## 4.3.0 (October 13, 2026)

Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ completion, command history, scripting, output styling, and much more.
You can use `cmd2` for a small internal tool and grow the same application into an extensible,
scriptable interface without replacing the framework or writing the usual CLI boilerplate.

Want a richer visual display than a prompt and toolbar can offer? Your `cmd2` application can run
inside a terminal pane of a [Textual](https://textual.textualize.io/) application, right next to
dashboards and panels that show realtime status and other information. See
[Embedding in Textual](https://cmd2.readthedocs.io/en/latest/examples/embedding_in_textual/) for an
example.

## Quick start

Install `cmd2`:
Expand Down
120 changes: 120 additions & 0 deletions docs/examples/embedding_in_textual.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Embedding in Textual

[Textual](https://textual.textualize.io/) builds full-screen terminal user interfaces with widgets,
layouts, and mouse support, while `cmd2` builds interactive command interpreters. You don't have to
choose between them. [textual-tty](https://github.com/ttygroup/textual-tty) provides a `Terminal`
widget for Textual applications, which is a terminal emulator that runs a program attached to a
pseudo-terminal. That program can be a `cmd2` application, so a Textual application can show a
`cmd2` application in one pane and, beside it, other widgets which display realtime status and
situational awareness information. This can be much richer than what fits in a
[bottom toolbar](../features/prompt.md#bottom-toolbar).

The
[textual_tty_embed.py](https://github.com/python-cmd2/cmd2/blob/main/examples/textual_tty_embed.py)
example shows how to do this. It has a `cmd2` application in a terminal pane on the left. On the
right, a status panel shows whether the `cmd2` application is idle or running a command, how many
commands it has run, and how long the last one took, and a clock is updated by Textual on its own.

!!! warning

textual-tty is still pre-alpha software, so its API may change. The example was written for
textual-tty 0.4. Also, on Windows textual-tty can only start a program by name and can't pass
it arguments, so the example's Textual application requires a POSIX system such as Linux or
macOS.

## Running the Example

Neither Textual nor textual-tty are dependencies of `cmd2`. In a `cmd2` checkout, textual-tty is
part of the development environment on POSIX systems, so you can run:

```shell
uv run examples/textual_tty_embed.py
```

Otherwise, install textual-tty, which also installs Textual, into your own environment:

```shell
pip install textual-tty
python examples/textual_tty_embed.py
```

Use the `quit` command or press ++ctrl+q++ to exit. Pass `--cmd2-only` to run just the `cmd2`
application in your current terminal, without Textual.

## How It Works

The Textual application runs the `cmd2` application as a child process in a `Terminal` widget. The
child process is the example script itself, run with `--cmd2-only --report-status`:

```py
def compose(self) -> ComposeResult:
cmd2_command = [sys.executable, __file__, "--cmd2-only", "--report-status"]
yield Header()
with Horizontal():
with Vertical(id="cmd2-pane"):
yield Terminal(command=cmd2_command, id="cmd2")
with Vertical(id="sidebar"):
yield StatusPanel(id="status")
yield ClockPanel(id="clock")
yield Footer()
```

Because it is attached to a pseudo-terminal, the `cmd2` application is a completely normal `cmd2`
application which uses `prompt-toolkit` and doesn't know that it is embedded. Everything typed into
the focused `Terminal` goes to the `cmd2` application.

To report its state, the `cmd2` application registers a
[postparsing hook](../features/hooks.md#postparsing-hooks), which runs before each command, and a
[command finalization hook](../features/hooks.md#command-finalization-hooks), which runs after it.
Both of them call [cmd2.Cmd.set_window_title][] with a JSON document. The document is prefixed with
`cmd2-status:` so that the Textual application can tell it apart from other window titles. The
`Terminal` widget turns each title change into a `Terminal.TitleChanged` message, which the Textual
application handles to update the status panel:

```py
def on_terminal_title_changed(self, event: Terminal.TitleChanged) -> None:
if not event.title.startswith(STATUS_PREFIX):
return
with contextlib.suppress(TypeError, ValueError, OverflowError, RecursionError):
status = json.loads(event.title.removeprefix(STATUS_PREFIX))
if isinstance(status, dict):
self.query_one(StatusPanel).apply(status)
```

Since the status travels through the pseudo-terminal, there are no sockets, files, or threads to
manage.

## Things to Watch Out For

The example handles several details which are easy to miss when building something similar:

- Command finalization hooks run for every line of input, including blank lines, comments,
++ctrl+c++ at the prompt, and lines with syntax errors. The other hooks only run for lines which
were parsed into a statement, so check whether `data.statement` is `None` in a finalization hook.
- Commands run by other commands, like `run_script`, `history -r`, and `run_pyscript`, go through
the hooks too. The example counts the running commands so that only the outermost one is reported.
- The postparsing hooks stop running as soon as one of them sets `stop` or raises an exception, but
the finalization hooks still run. If your application has other postparsing hooks, register the
one which counts commands first so that it is never skipped.
- `prompt-toolkit` buffers its output, so flush it after setting the window title when a command
starts. Otherwise the "running" state only reaches the Textual application after the command is
done.
- Any program run in the terminal, for example by the `shell` command, can set the window title.
Validate a status document before using it. Besides `json.JSONDecodeError`, `json.loads` raises
`ValueError` for integers with too many digits and `RecursionError` for documents which are nested
too deeply. Also escape the document's strings with `textual.markup.escape` before putting them in
Textual markup.
- Only report status through the window title when running inside the Textual application. In a
normal terminal, it would overwrite the title of the user's window.
- If the Textual application exits while a command is running, the pseudo-terminal is closed before
the finalization hook reports the end of that command, so setting the window title raises
`OSError`. The example ignores that error.
- When the child process exits, `Terminal.ProcessExited.exit_code` holds its exit code, which is the
negated signal number if a signal killed it. The example passes the exit code back to the shell,
using 128 plus the signal number like a shell does, and when it isn't 0 the example keeps the
terminal on the screen so that any traceback can still be read.
- A `Terminal` sizes its pseudo-terminal from its outer size, so put a border on a container around
the `Terminal` rather than on the `Terminal` itself. Otherwise the `cmd2` application draws more
rows than are visible and its prompt can end up hidden.
- The focused `Terminal` sends almost every key press to the `cmd2` application. Textual's built-in
++ctrl+q++ binding to quit is a priority binding, so it still works.
1 change: 1 addition & 0 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- [Getting Started](getting_started.md)
- [Alternate Event Loops](alternate_event_loops.md)
- [Embedding in Textual](embedding_in_textual.md)
- [List of cmd2 examples](examples.md)

<!--intro-end-->
7 changes: 7 additions & 0 deletions docs/features/prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,10 @@ class App(cmd2.Cmd):
See the
[getting_started.py](https://github.com/python-cmd2/cmd2/blob/main/examples/getting_started.py)
example for a demonstration of this technique.

!!! tip

If you want to show more realtime status information than fits in a toolbar, you can run your
`cmd2` application inside a [Textual](https://textual.textualize.io/) application, next to
widgets which display that information. See
[Embedding in Textual](../examples/embedding_in_textual.md).
6 changes: 6 additions & 0 deletions docs/overview/alternatives.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,9 @@ If you are seeking a visually richer end-user experience and don't mind investin
time, we would recommend checking out [Textual](https://github.com/Textualize/textual) as this can
be used to build very sophisticated user interfaces in a terminal that are more akin to feature-rich
web GUIs.

You don't have to choose between `cmd2` and Textual, though. The
[textual-tty](https://github.com/ttygroup/textual-tty) package provides a terminal emulator widget
for Textual, so a `cmd2` application can run inside a Textual application, next to widgets which
display realtime information about it. See
[Embedding in Textual](../examples/embedding_in_textual.md) for an example of how to do this.
5 changes: 5 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ each:
- Example of using Rich Tables within a cmd2 application for displaying tabular data
- [rich_theme.py](https://github.com/python-cmd2/cmd2/blob/main/examples/rich_theme.py)
- Demonstrates how to create a custom theme for a cmd2 application
- [textual_tty_embed.py](https://github.com/python-cmd2/cmd2/blob/main/examples/textual_tty_embed.py)
- Shows how to embed a `cmd2` application in a [Textual](https://textual.textualize.io/)
application using [textual-tty](https://github.com/ttygroup/textual-tty), with a realtime
status panel next to the terminal. Requires a POSIX system such as Linux or macOS and
`textual-tty`, which is part of the `cmd2` development environment but not a `cmd2` dependency
- [tmux_launch.sh](https://github.com/python-cmd2/cmd2/blob/main/examples/tmux_launch.sh)
- Shell script that launches two applications using tmux in different windows/tabs
- [tmux_split.sh](https://github.com/python-cmd2/cmd2/blob/main/examples/tmux_split.sh)
Expand Down
Loading
Loading