Interactive prompts
The prompts package is a complete question toolkit: seven primary prompt types that render as live, repainting lines at a terminal — and fall back to plain line input automatically whenever stdin or stdout isn’t a terminal, so the same command works in a pipe, a script, or CI without a non-interactive flag.
In a command, context.prompts() returns an instance pre-wired to the command’s streams, arena allocator, and theme:
// Pre-wired to the command's streams, allocator, and theme.
const p = context.prompts();
const name = try p.text(.{
.message = "Project name:", .default = "my-project",
});
const features = try p.multiSelect(.{
.message = "Features:",
.choices = &.{ "typescript", "eslint", "prettier" },
.defaults = &.{ true, true, false },
.search = true,
});
const pw = try p.password(.{
.message = "Token:",
});
The seven types
| Prompt | Asks | Returns |
|---|---|---|
text | free-form input, optional default and live preview hint | the entered string |
confirm | yes/no with a default (Y/n hint, single keypress) | bool |
select | one choice from a list — ↑/↓ to move, Enter or Space to pick | the chosen index |
multiSelect | several choices — Space toggles, per-item defaults | the chosen indices |
password | masked input, never echoed | the entered string |
number | numeric input with optional min/max, validation inline | the parsed number |
editor | opens $EDITOR on a seeded temp file | the edited text |
Explicit editing
For an edit command, open immediately with the reusable editor operation:
const p = context.prompts();
var result = try p.edit(.{
.io = context.io,
.environ = context.environ,
.default = existing_document,
.extension = ".md",
});
defer result.deinit(context.allocator);
switch (result) {
.edited => |content| try save(content),
.failed => |failure| {
if (failure.recovery_path) |path|
try context.stderr().print("Content preserved at {s}\n", .{path});
return error.EditCancelled;
},
}
The editor opens on a controlling terminal even when stdout is redirected. Its UI never goes into the command’s redirected output. Missing terminals, launch failures, unsuccessful termination and read-back failures are distinct structured failures. Successful editing returns exact bytes: trailing newlines, unchanged text and empty documents are valid.
Supply .argv = &.{ "code", "--wait" } for explicit program configuration. Otherwise an explicit editor_cmd takes precedence over nonempty $VISUAL, then $EDITOR, then vi (notepad on Windows), from the supplied environment. Command strings support quotes and backslash escaping, without shell expansion or pipelines. Prefer .argv for Windows paths containing backslashes. Explicit shells work: EDITOR="sh -c 'echo edited > \"\$1\"' --". Lookup uses the supplied PATH and the child receives the supplied environment.
Scratch files are exclusive, randomized and private. They are removed on success. Changed or unreadable content is preserved on failure, with an owned recovery_path; calling result.deinit frees result memory and does not delete that file. .recovery = .discard deliberately disables recovery. .max_bytes bounds the saved document read (128 MiB default); oversized saved content remains recoverable. .attachment = .inherit is available explicitly for noninteractive tools or tests.
The existing p.editor prompt still invites Enter and falls back to reading stdin when noninteractive. Set .immediate = true to bypass the invitation at an interactive prompt; its noninteractive stdin fallback remains unchanged. The p.editor prompt returns EditorFailed, EditorLaunchFailed or EditorReadFailed rather than silently returning the initial text; it displays a recovery path when available. Use p.edit for editing with redirected stdout or structured failure details. Its expected failures are .failed result values; allocation failures remain Zig errors.
Searchable choices
Set .search = true on select or multiSelect for a case-insensitive substring filter. Printable characters other than ASCII Space filter, Backspace edits the query, Up/Down navigate, and the Space key selects or toggles the highlighted choice; Enter selects or commits. ASCII Space is not query text; hidden multi-select choices remain selected.
Terminal behavior, handled
- Wrapping & resize — long messages word-wrap at the terminal width (grapheme- and ANSI-aware), and every prompt re-lays-out live on window resize.
- Unicode-correct editing — backspace deletes one visual character; wide characters and combining sequences count correctly.
- Answers persist — a completed prompt emits its styled question-and-answer as a static line into scrollback, so the transcript reads cleanly afterward.
- Interrupts are yours — pass
interrupt_keysand the prompt returnserror.Interruptedwhen one is pressed; the caller decides whether that means cancel, back, or help.
Off a TTY
Redirect either stream — myapp setup < answers.txt, myapp setup > log.txt, a pipeline, CI — and every prompt degrades to a line-based equivalent with the same return type and value. There is no second code path to write:
| at a terminal | piped / CI | |
|---|---|---|
text / password | live editing, masked for password | reads a line |
confirm | single keypress | reads y/n from a line |
select | arrow-key highlighted list; Enter or Space selects | numbered list, reads a number |
multiSelect | Space-toggle list | prints the list, reads selections |
editor | opens $EDITOR | reads remaining stdin |
Fallback, or interactive-only
The fallback above is the default, and for most commands it is the right answer: one code path serves a terminal, a pipe, and CI.
Some commands have nothing sensible to do off a terminal — a wizard whose whole job is the conversation, or a prompt whose line fallback would silently accept something destructive. Those guard once, before the first question:
const p = context.prompts();
// One documented call, up front — nothing is asked if this fails.
try p.requireInteractive(); // error.NotInteractive when piped
const name = try p.text(.{ .message = "Project name:" });
const ok = try p.confirm(.{ .message = "Create it?" });
requireInteractive returns error.NotInteractive unless both stdin and stdout are terminals: stdin so keystrokes can be read in raw mode, stdout so the rendered frame lands on the screen instead of a redirected file. It is the same check each prompt makes, so the guard’s verdict is exactly what the next prompt would have done — no second detection to keep in sync.
To branch rather than fail, ask p.isInteractive() — that is how zcli add command scaffolds a plain skeleton off a terminal and runs the wizard at one.
Setting .interactive on the instance overrides the detection for that instance, guard and prompts alike. Be precise about what false means: it forces line mode, so the prompts still print and still read stdin — at a terminal that waits for a typed line, on a closed stream it is error.EndOfStream. It is not a --no-input switch; a flag that means “ask me nothing” has to skip the prompt sequence itself and take defaults or required options instead.
One case sets .interactive for you: an instance from context.prompts() reports non-interactive whenever the Context’s stdout is captured or its stdin injected — which is what a runCommand unit test does. The streams the prompt was handed decide, not the process’s descriptors, so a test can’t enter raw mode and read the real keyboard just because the test binary was started from a terminal. A command that guards therefore reports error.NotInteractive under runCommand, and belongs in the PTY-backed E2E tier.
Pick one per command: no guard means “this works piped”, the guard means “this needs a terminal, and says so before asking”.
Prompts or widgets?
Prompts are one-shot questions: they block, return a value, and move on — right for a wizard, a scaffold, a few sequential questions. For a persistent, laid-out, keyboard-driven surface — a live form, a dashboard — build a full-screen App from the interactive widgets instead: see prompts vs widgets.
Standalone use
Like every zcli package, prompts works without the framework — construct a Prompts value with your own writer, reader, allocator, and theme context, and every prompt type behaves identically. The package’s examples/ directory has a runnable example per prompt type.