Why escape means back¶
The rule¶
Wizard::run handles Ok(StepOutcome::Back) and
Err(InquireError::OperationCanceled) in the same match arm. inquire
returns OperationCanceled when the user presses Esc, so
escaping a prompt and a step deciding to go back are literally
indistinguishable to the driver.
On the first step, that arm returns WizardError::Cancelled rather
than moving the index to −1.
Why escape is not a cancel¶
The alternative — Esc aborts the whole wizard — is what most prompt libraries do on their own, and it is wrong for a multi-step form.
A wizard exists because there are several things to ask. By the fourth question the user has invested four answers, and the most common reason they reach for Esc is that they got the third one wrong. Under an abort-on-escape rule that keystroke throws away all four and starts them again, and the only way to change an answer is to finish the wizard and re-run it.
Mapping Esc to "back" makes the obvious keystroke do the thing people are actually trying to do. It costs one behaviour: there is no single key that abandons the whole wizard from the middle. Ctrl+C covers that, and it arrives as a distinct error variant so the caller can treat it differently.
Why steps do not have to know about it¶
A step implementation never sees OperationCanceled. It calls an
inquire prompt, ?-propagates whatever comes back, and the driver
does the interpretation:
If each step had to match on the error and decide what escape meant, one of them would eventually decide differently, and the wizard would behave inconsistently from step to step. Putting the interpretation in one place in the driver means the rule cannot drift.
The cost is that a step cannot opt out. There is no way to write a step
where Esc means something else, because the driver has
already consumed the signal by the time the step's caller sees it. A
step that needs an escape-like affordance has to offer it as a prompt
option instead — an explicit "go back" choice in a Select, say, which
returns StepOutcome::Back.
Why the first step cancels instead of ignoring the key¶
There is no step before the first one, so the driver has three options: ignore the keystroke, re-run the first step, or end the wizard.
Ignoring it is the worst of the three. The user presses a key, nothing happens, and they cannot tell whether the program is stuck. Re-running the first step is nearly as bad — it looks like the wizard reset itself, and it discards whatever they had typed into it.
Ending the wizard is the one that matches the mental model. The user is
at the start; pressing Esc at the start means "actually, no".
WizardError::Cancelled says exactly that, and it is a different
variant from Interrupted so a caller can print "nothing was written"
for one and stay quiet for the other.
What this costs you as an implementer¶
Back navigation re-runs the previous step against the current state, not against a snapshot. The wizard keeps no history and rolls nothing back.
That makes steps that append rather than assign dangerous. A step doing
state.tags.push(tag) will push a second tag when the user comes back
to it; a step doing state.tag = Some(tag) will not. The rule is that
a step must be safe to run more than once, and the crate cannot enforce
it — it is a contract on your implementations.
The upside of the same design is that pre-filling a prompt from the current state gives the user their previous answer to edit, for free, because the value is still sitting in the state when the step re-runs.