Skip to content
UrushiDocumentation

Fields and validation

All prompt content is plain text. Use the form Theme for styling rather than embedding ANSI sequences in questions, descriptions, labels, or messages.

use urushi_prompt::{FieldKey, Input, ValidationError};
let project_key = FieldKey::new("project");
let project = Input::new(project_key, "Project name", "")?
.description("Used as the generated directory name.")
.placeholder("my-project")
.required()
.required_message("Enter a project name.")
.validate(Box::new(|value| {
if value.contains(' ') {
Err(ValidationError::new("Use a name without spaces."))
} else {
Ok(())
}
}));

The configured field appears as one focused region. The placeholder and description use muted theme roles:

┃ Project name
┃ Used as the generated directory name.
┃ › my-project

Submitting the empty value runs the required check before custom validators:

┃ Project name
┃ ›
┃ ! Enter a project name.

Validators are synchronous and run against the String value. Multiple validators run in insertion order.

If the user enters my project, the validator adds the error below the control without replacing the entered value:

┃ Project name
┃ Used as the generated directory name.
┃ › my project
┃ ! Use a name without spaces.

Input editing follows terminal grapheme boundaries, so a combined emoji or a base character plus combining mark moves and deletes as one unit.

Action Keys
Move one grapheme Left / Right, Ctrl-B / Ctrl-F
Move to an edge Home / End, Ctrl-A / Ctrl-E
Delete one grapheme Backspace / Delete
Delete to an edge Ctrl-U / Ctrl-K
Validate and continue Enter / Tab
Return to the previous field Shift-Tab
Cancel the form Esc

Typing or pasting inserts at the cursor and clears a displayed validation error. Because Input is one line, paste converts CR, LF, and tab boundaries to spaces and removes other control characters. For example, pasting api\tserver\r\nrelease at the cursor produces this observable state:

┃ Project name
┃ › api server release 

Moving left twice and pressing Delete removes the grapheme under the cursor; Ctrl-U then removes everything before it. Validation runs only when Enter or Tab attempts to advance.

Select<T> stores values independently from their visible labels.

use urushi_prompt::{FieldKey, Select, SelectOption};
#[derive(Debug)]
enum Profile {
Debug,
Release,
Test,
Bench,
Minimal,
Production,
}
let profile_key = FieldKey::new("profile");
let profile = Select::new(
profile_key,
"Build profile",
vec![
SelectOption::new("Debug", Profile::Debug),
SelectOption::new("Release", Profile::Release),
SelectOption::new("Test", Profile::Test),
SelectOption::new("Bench", Profile::Bench),
SelectOption::new("Minimal", Profile::Minimal),
SelectOption::new("Production", Profile::Production),
],
)?.visible_rows(3)
.filter_help("type filter • enter apply", "enter choose • / edit")
.no_matches_message("No build profiles match");

The first option starts selected. Three rows are visible; moving farther scrolls the window instead of increasing the field height:

┃ Build profile
┃   Test
┃ › Bench
┃   Minimal
┃   ↑ 2 • ↓ 1
┃ ↑/↓ select • enter continue • shift+tab back • esc cancel

Press / to edit a case-insensitive substring filter. Typing pro narrows the list while showing the editing help configured above:

┃ Build profile  / pro 
┃ › Production
┃
┃
┃   = 1
┃ type filter • enter apply

When no label contains the filter, the configured message replaces the option window without inventing a submitted value:

┃ Build profile  / xyz 
┃ No build profiles match
┃
┃
┃   = 0
┃ type filter • enter apply
use urushi_prompt::{
Confirm, ConfirmAnswer, FieldKey,
urushi::Align,
};
let overwrite_key = FieldKey::<ConfirmAnswer>::new("overwrite");
let overwrite = Confirm::new(
overwrite_key,
"Overwrite the existing file?",
None,
)?.labels("Overwrite", "Keep")
.button_alignment(Align::Left)
.unanswered_message("Choose an action.");

The two choices share one left-aligned control row. With no default, neither is selected initially:

┃ Overwrite the existing file?
┃
┃   Overwrite     Keep

Pressing Enter before choosing keeps the field active and displays the configured error:

┃ Overwrite the existing file?
┃   Overwrite     Keep
┃ ! Choose an action.

Pressing Right or n explicitly chooses Keep; submission then returns ConfirmAnswer { value: false, source: ConfirmSource::Explicit }:

┃ Overwrite the existing file?
┃   Overwrite     Keep

ConfirmAnswer::value is the yes-or-no result. ConfirmAnswer::source tells whether the user made an explicit choice or submitted the configured default. When no default exists, the field remains active until the user chooses.

Every field supports a description and configurable navigation help. Select fields provide separate help for editing and applying a filter:

let input = Input::new(FieldKey::new("name"), "Name", "")?
.description("Used in the greeting.")
.help("enter next • esc cancel");
let select = Select::new(
FieldKey::new("region"),
"Region",
vec![SelectOption::new("Tokyo", "ap-northeast-1")],
)?
.help("↑/↓ choose • enter next")
.filter_help("type filter • enter apply", "enter next • / edit");
┃ Name
┃ Used in the greeting.
┃ ›
┃ enter next • esc cancel

Keep help short enough to remain useful at narrow widths. See the prompt reference for all defaults and builders.