rtb-tui 0.6.5

Reusable TUI widgets: forms, wizards, prompts, tables. Part of the phpboyscout Rust toolkit.
Documentation
---
title: Wizard
description: Signatures, navigation rules and panics for Wizard, WizardBuilder, WizardStep and StepOutcome.
tags: [reference, wizard]
---

# `Wizard`

A multi-step interactive form. Each step runs one or more
[`inquire`](https://crates.io/crates/inquire) prompts against a state
value the wizard owns, and returns a [`StepOutcome`](#stepoutcome) that
says whether to go forwards or backwards.

## `Wizard<S>`

```rust
pub struct Wizard<S> { /* private fields */ }

impl<S: Send + 'static> Wizard<S> {
    #[must_use]
    pub fn builder() -> WizardBuilder<S>;

    pub async fn run(self) -> Result<S, WizardError>;
}
```

`S` is your own state type. The wizard owns it: `builder().initial(s)`
moves it in, and `run` moves it back out on success. There is no way to
borrow the state while the wizard is running.

`S: Send + 'static` is required. A state type holding a non-`Send` value
(an `Rc`, a raw pointer) will not compile.

### `Wizard::run` return values

`run` is `async` and needs an executor. The crate does not start one;
`tokio` is a dev-dependency only, used by the test suite.

| Situation | Result |
|---|---|
| Every step returned `Next` | `Ok(state)` with all mutations applied |
| The wizard has zero steps | `Ok(state)`, the initial state unmodified, without prompting |
| A step returned `Back` while on the first step | `Err(WizardError::Cancelled)` |
| A step returned `Err(InquireError::OperationCanceled)` while on the first step | `Err(WizardError::Cancelled)` |
| Any step returned `Err(InquireError::OperationInterrupted)` | `Err(WizardError::Interrupted)` |
| Any step returned any other `InquireError` | `Err(WizardError::Step { step, message })` |

`run` never panics.

## Navigation rules

The driver loop holds an index into the step list and moves it by one.

- `Ok(StepOutcome::Next)` advances the index. If that takes it past the
  last step, `run` returns `Ok(state)`.
- `Ok(StepOutcome::Back)` decrements the index. On index 0 it returns
  `WizardError::Cancelled` instead.
- `Err(InquireError::OperationCanceled)`, which is what `inquire` returns
  when the user presses <kbd>Esc</kbd>, is handled by the *same* match arm
  as `Back`. Escape and an explicit `Back` are indistinguishable to the
  wizard.
- `Err(InquireError::OperationInterrupted)` (<kbd>Ctrl</kbd>+<kbd>C</kbd>)
  returns `WizardError::Interrupted` from any position, including the
  first step.
- Every other `InquireError` variant is wrapped as
  `WizardError::Step { step, message }`, where `step` is the failing
  step's `name()` and `message` is the underlying error's `Display`
  output.

There is no jump-to-step, no skip, and no conditional branching. Back
navigation moves exactly one step. See
[What rtb-tui does not do](../explanation/04-what-rtb-tui-does-not-do.md#the-wizard-only-moves-one-step-at-a-time).

## How state threads through the steps

Each step receives `&mut S`, so step *N+1* observes everything step *N*
wrote. Backing up does **not** roll anything back: the earlier step
re-runs against the state as it stands now, including values written by
later steps.

Two consequences worth planning for:

- A step must be safe to run more than once. Writing `state.name =
  Some(prompt()?)` is fine; pushing onto a `Vec` is not, because a back
  navigation will push a second time.
- Using the current state to pre-fill the prompt is the intended
  pattern. That is how a user sees their previous answer when they
  come back to change it.

## `WizardStep<S>`

```rust
#[async_trait]
pub trait WizardStep<S>: Send + Sync {
    fn name(&self) -> &'static str;

    async fn prompt(&self, state: &mut S) -> Result<StepOutcome, InquireError>;
}
```

`name` must return a `&'static str`. It appears in
`WizardError::Step { step, .. }` and nowhere else. It is not shown to
the user during the wizard, so make it something you would want in an
error message and in a log line.

`prompt` takes `&self`, not `&mut self`. Per-step mutable data has to
live in the wizard state or behind interior mutability.

The `Send + Sync` supertrait bound is on the trait itself, so a step
type holding a `Cell` or an `Rc` will not compile.

Implementations are boxed: `WizardBuilder::step` requires
`W: WizardStep<S> + 'static`.

### `InquireError` re-export

The crate re-exports `inquire::InquireError` from its root:

```rust
pub use inquire::InquireError;
```

It lets code name `InquireError` without depending on `inquire`: the
`WizardStep::prompt` signature, and a scripted test step like the one in
[Test a wizard without a terminal](../how-to/04-test-a-wizard-without-a-terminal.md).
A step that issues a real prompt calls `inquire::Text` or
`inquire::Select`, so it needs `inquire` in its own `Cargo.toml` anyway.
Keep that version aligned with the one `rtb-tui` depends on. A mismatch
produces two distinct `InquireError` types and a confusing type error at
the `?`.

## `StepOutcome`

```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepOutcome {
    Next,
    Back,
}
```

`Copy`, so returning it never moves anything. There is no `Finish`
variant: a wizard ends when its last step returns `Next`.

## `WizardBuilder<S>`

```rust
pub struct WizardBuilder<S> { /* private fields */ }

impl<S: Send + 'static> WizardBuilder<S> {
    #[must_use] pub fn initial(self, state: S) -> Self;
    #[must_use] pub fn step<W>(self, step: W) -> Self where W: WizardStep<S> + 'static;
    #[must_use] pub fn build(self) -> Wizard<S>;
}
```

| Method | Default if not called | Notes |
|---|---|---|
| `initial` | none; `build` panics | Calling it twice keeps the last value |
| `step` | no steps; `run` returns the initial state | Steps run in call order |
| `build` | — | Consumes the builder |

### `WizardBuilder::build` panics without `initial`

```
thread 'main' panicked at src/wizard.rs:
Wizard::builder requires .initial(...)
```

This is a runtime panic, not a compile error. The builder is a plain
accumulator, not a typestate builder, so nothing in the type system stops
you calling `build()` on a builder that has no initial state. Construct
the wizard in one chained expression and the mistake is hard to make:

```rust
let wizard = Wizard::<Profile>::builder()
    .initial(Profile::default())
    .step(AskName)
    .build();
```