rtb-tui¶
Three small building blocks every CLI tool needs and would otherwise
have to roll itself: a multi-step interactive Wizard, uniform
structured-output render helpers, and a TTY-aware Spinner.
Part of the phpboyscout Rust toolkit; extracted from — and battle-tested by — rust-tool-base.
Where to start¶
| You want to | Go to |
|---|---|
| Build something with it, from nothing | Build a three-step wizard |
| Add one thing to a CLI you already have | How-to |
| Know exactly what a function does or returns | Reference |
| Know why it behaves that way, or what it will not do | Explanation |
The shortest useful answer to "should I use this?" is on What rtb-tui does not do. It is not a full-screen terminal UI library, it has no Cargo features, and it renders exactly two output formats.
Public API¶
| 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 so WizardStep impls can ?-propagate without a direct inquire dependency. |
Everything is exported from the crate root; there are no public submodules. Signatures, defaults and failure modes are in Reference; the generated listing is on docs.rs/rtb-tui.
Wizard¶
Multi-step interactive form backed by
inquire. Each step mutates a
state value the wizard owns, and run returns that state once the last
step advances past the end.
let profile = Wizard::<Profile>::builder()
.initial(Profile::default())
.step(AskName)
.step(AskLanguage)
.build()
.run()
.await?;
.initial(...) is required — build() panics without it. Full
behaviour: Wizard reference.
Navigation rules¶
Next advances and finishes after the last step. Back re-runs the
previous step, and on the first step returns
WizardError::Cancelled. Esc arrives as
InquireError::OperationCanceled and is treated exactly like Back,
so steps just ?-propagate. Ctrl+C 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, and the reasoning is in Why escape means back.
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¶
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. Wiring them
to a flag:
Add a --output text|json flag.
Spinner¶
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 has the details;
Why the spinner does not animate
has the reasoning.
Design record¶
The reasoning behind each of these decisions is written out in Explanation, in prose, checked against the code:
- Why escape means back
- Why the spinner does not animate
- Why the output helpers are a pair
- What rtb-tui does not do
Several rustdoc comments in the source still defer to a design spec that is not in this repository, and a few of them describe behaviour the code does not have. Those are catalogued in Known documentation defects.