rtb-tui 0.6.5

Reusable TUI widgets: forms, wizards, prompts, tables. Part of the phpboyscout Rust toolkit.
Documentation
---
title: rtb-tui
description: Three building blocks for line-oriented CLI tools, a multi-step Wizard, text-or-JSON render helpers and a TTY-aware Spinner.
---

# `rtb-tui`

Three small building blocks for a line-oriented CLI tool: a multi-step
interactive `Wizard` where <kbd>Esc</kbd> goes back a step, a pair of
render helpers behind one `--output text|json` flag, and a `Spinner`
that writes nothing when stderr is not a terminal.

Part of the [phpboyscout Rust toolkit](https://rust.phpboyscout.uk),
extracted from [rust-tool-base](https://gitlab.com/phpboyscout/rust-tool-base),
which carried it as an internal module before it was published on its
own.

```toml
[dependencies]
rtb-tui = "0.6"
```

## Where to start

| You want to | Go to |
|---|---|
| Build something with it, from nothing | [Build a three-step wizard]tutorials/01-build-a-three-step-wizard.md |
| Add one thing to a CLI you already have | [How-to]how-to/index.md |
| Know exactly what a function does or returns | [Reference]reference/index.md |
| Know why it behaves that way, or what it will not do | [Explanation]explanation/index.md |

The shortest useful answer to "should I use this?" is on
[What rtb-tui does not do](explanation/04-what-rtb-tui-does-not-do.md).
It is not a full-screen terminal UI library, it has no Cargo features,
and it renders exactly two output formats.

## Public API

```rust
use rtb_tui::{Wizard, WizardStep, StepOutcome, render_table, render_json, Spinner};
```

| Item | Purpose |
|---|---|
| `Wizard<S>` / `WizardBuilder<S>` | Multi-step interactive form with escape-to-back navigation, backed by `inquire`. |
| `WizardStep<S>` | Async trait a step implements; receives `&mut S`. |
| `StepOutcome` | `Next` advances, `Back` re-runs the previous step. |
| `WizardError` | `Cancelled`, `Interrupted`, or `Step { step, message }`. |
| `render_table<R: Tabled>(rows)` | Infallible psql-style text table. |
| `render_json<R: Serialize>(rows)` | Pretty-printed JSON array; `RenderError::Json` on a failing `Serialize` impl. |
| `Spinner` | TTY-aware progress indicator; every method no-ops when stderr isn't a terminal. |
| `InquireError` | Re-export of the error type in the `WizardStep::prompt` signature. Steps that issue real prompts still depend on `inquire` directly. |

Everything is exported from the crate root; there are no public
submodules. Signatures, defaults and failure modes are in
[Reference](reference/index.md); the generated listing is on
[docs.rs/rtb-tui](https://docs.rs/rtb-tui).

## `Wizard`

Multi-step interactive form backed by
[`inquire`](https://crates.io/crates/inquire). Each step mutates a
state value the wizard owns, and `run` returns that state once the last
step advances past the end.

```rust
let profile = Wizard::<Profile>::builder()
    .initial(Profile::default())
    .step(AskName)
    .step(AskLanguage)
    .build()
    .run()
    .await?;
```

`.initial(...)` is required, and `build()` panics without it. Full
behaviour: [Wizard reference](reference/01-wizard.md).

### Navigation rules

`Next` advances and finishes after the last step. `Back` re-runs the
previous step, and on the first step returns
`WizardError::Cancelled`. <kbd>Esc</kbd> arrives as
`InquireError::OperationCanceled` and is treated exactly like `Back`,
so steps just `?`-propagate. <kbd>Ctrl</kbd>+<kbd>C</kbd> short-circuits
to `WizardError::Interrupted` from anywhere; any other `InquireError`
becomes `WizardError::Step { step, message }`.

Back navigation moves exactly one step. There is no jump-to-step and
no branching. The full table is in
[Wizard reference](reference/01-wizard.md#navigation-rules), and the
reasoning is in
[Why escape means back](explanation/01-why-escape-means-back.md).

### State threading

`Wizard<S>` owns its state and hands each step a `&mut S`, so step
*N+1* sees what step *N* wrote. Going back does not roll anything back:
the earlier step re-runs against the state as it stands now, which
means **a step must be safe to run more than once**. Assign to fields
rather than appending to collections, and pre-fill prompts from the
current state so the user sees their previous answer.

## Render helpers

```rust
print!("{}", render_table(&rows));               // psql-style text table
print!("{}", render_json(&rows).unwrap());       // pretty-printed JSON array
```

Both take a slice and return a string ending in a newline, so `print!`
is the right macro. `render_table` is infallible and its style is fixed;
`render_json` always produces a top-level array and returns
`RenderError::Json(_)` when a row's `Serialize` impl fails.

Exact output for empty input, the failure cases, and the derive
attributes that apply:
[Render helpers reference](reference/02-render-helpers.md). Wiring them
to a flag:
[Add a `--output text|json` flag](how-to/01-add-a-text-or-json-output-flag.md).

## `Spinner`

```rust
let mut spinner = Spinner::new("downloading…");
spinner.set_message("verifying signature…");
spinner.finish();
```

A one-line status indicator on stderr. When stderr isn't a terminal
(CI logs, redirected output, MCP-stdio transports) every method is a
no-op and nothing is written.

It does not animate. The glyph is static and redraws only when you call
`set_message`, because there is no background task advancing frames.
[Spinner reference](reference/03-spinner.md) has the details;
[Why the spinner does not animate](explanation/02-why-the-spinner-does-not-animate.md)
has the reasoning.

## Design record

The reasoning behind each of these decisions is written out in
[Explanation](explanation/index.md), in prose, checked against the
code:

- [Why escape means back]explanation/01-why-escape-means-back.md
- [Why the spinner does not animate]explanation/02-why-the-spinner-does-not-animate.md
- [Why the output helpers are a pair]explanation/03-why-the-output-helpers-are-a-pair.md
- [What rtb-tui does not do]explanation/04-what-rtb-tui-does-not-do.md

The original design record is
[spec 0020](https://gitlab.com/phpboyscout/rust-tool-base/-/wikis/specs/0020-rtb-tui-v0.1)
on the rust-tool-base wiki, and the rustdoc cites it by item number.

## Further reading

The blog carries a curated route through this subject: **[Rust, and what survived the port](https://phpboyscout.uk/topics/rust/)** collects
everything written about it, ordered so you can start at the beginning rather
than newest-first.

!!! tip "Ask phpbotscout"

    ![phpbotscout](https://phpboyscout.uk/images/projects/logo-phpbotscout.png){ width="84" align=left style="border-radius:10px;margin-right:1rem" }

    He answers questions about the projects over on the Discord, citing the docs
    where they already cover it, and offering to raise an issue where they don't.
    Bring a bug, an idea, or a questionable engineering decision.

    [Join the Discord](https://discord.gg/mQzGbmGyzZ){ .md-button .md-button--primary }