Wizard¶
A multi-step interactive form. Each step runs one or more
inquire prompts against a state
value the wizard owns, and returns a StepOutcome that
says whether to go forwards or backwards.
Wizard<S>¶
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,runreturnsOk(state).Ok(StepOutcome::Back)decrements the index. On index 0 it returnsWizardError::Cancelledinstead.Err(InquireError::OperationCanceled)— whatinquirereturns when the user presses Esc — is handled by the same match arm asBack. Escape and an explicitBackare indistinguishable to the wizard.Err(InquireError::OperationInterrupted)— Ctrl+C — returnsWizardError::Interruptedfrom any position, including the first step.- Every other
InquireErrorvariant is wrapped asWizardError::Step { step, message }, wherestepis the failing step'sname()andmessageis the underlying error'sDisplayoutput.
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.
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 aVecis 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>¶
#[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:
That exists so a WizardStep implementation can ?-propagate an
inquire prompt without adding inquire to its own Cargo.toml. If
you do add inquire directly, keep the version aligned with the one
rtb-tui depends on — a mismatch produces two distinct
InquireError types and a confusing type error at the ?.
StepOutcome¶
Copy, so returning it never moves anything. There is no Finish
variant: a wizard ends when its last step returns Next.
WizardBuilder<S>¶
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¶
This is a runtime panic, not a compile error. The builder is a plain
accumulator, not a typestate builder — 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:
The crate's rustdoc claims this contract is "enforced via a test". It is not — see Known documentation defects.