Documentation: https://docs.rs/cli-ui
Source Code: https://github.com/NameOfShadow/cli-ui
No clap. No colored. The runtime tree is anstream + anstyle for
output, and crossterm for the interactive prompts.
use *;
┌ Set up a new project
│
◆ What's your name?
│ Alice
│
◆ Continue?
│ ● Yes / ○ No
└ Hi Alice, going.

Themed --help — sections, alignment, dimmed placeholders, all automatic:

Features
#[derive(CliOptions)]— typed argument parsing with zero boilerplate- Themes — colour your badge and accents in one line
--help— generated automatically, fully styled, sections and alignment included--version— themed badge--completions— bash, zsh, fish out of the box- Progress —
(n/total)counter, URL truncation, remote/local source labels summary!— per-section key alignment,DONE/WARNbadges- Interactive prompts — text, select, multiselect, confirm, password, autocomplete, date, path, spinner, tasks, log lines — clack-style framing with full color/theme customisation. See Prompts below.
- Pipe-safe — ANSI stripped automatically when output is redirected
- Cross-platform — Linux, macOS, Windows 10+
Installation
[]
= { = "../cli-ui" }
Requires Rust 1.70+.
Quick start
use PathBuf;
use ;
use ;
use format_bytes;
Reference
#[cli(...)] — struct attributes
| Key | Description |
|---|---|
name = "str" |
App name. Default: struct name in kebab-case (MyTool → my-tool) |
about = "str" |
One-line description |
tagline = "str" |
Secondary line under the header badge |
theme = "str" |
Colour theme — see Themes |
example = "str" |
Example command — repeat for multiple |
hint = "str" |
Note shown below examples in help |
url = "str" |
Project URL |
#[arg(...)] — field attributes
| Key | Description |
|---|---|
positional |
Required positional, consumed in declaration order |
short = 'x' |
Short flag -x |
long = "name" |
Long flag --name, also accepts --name=value |
section = "Name" |
Groups the flag under a named section in help |
default = val |
Default when flag is absent |
negatable |
Generates --flag / --no-flag pair. Field must be bool |
multi |
Allows repeating the flag. Field must be Vec<T> |
conflicts_with = "field" |
Error if both are provided. Generates resolved_*() |
Generated methods
| Method | Description |
|---|---|
Opt::parse() |
Parse args. Handles --help, --version, --completions, unknown flags, missing positionals |
Opt::help() |
Print styled help to stderr |
Opt::completions(shell) |
Print completion script: "bash", "zsh", "fish" |
opt.resolved_<field>() |
For each Option<T> with conflicts_with — returns &T, flag or positional fallback |
Macros
| Macro | Output |
|---|---|
header!(name, ver, about, tagline) |
Themed badge + version + tagline |
phase!("tag", "fmt {}", val) |
▶ tag message |
step!("message") |
▸ message with blank line before |
substep!(url, local) |
└─ url → local |
ok!(path) |
✓ path |
bail!("fmt {}", val) |
Styled error + exit 1 |
summary! { ... } |
Aligned summary block |
summary! syntax
summary!
Progress
let pb = new;
pb.ok; // ✓ line
pb.fail; // ✘ line
pb.finish; // blank line
kind is padded to 6 characters. Long URLs are truncated to fit the terminal width.
Themes
| Value | Badge |
|---|---|
"cyan" (default) |
Cyan |
"magenta" / "purple" |
Magenta |
"green" |
Green |
"blue" |
Blue |
"yellow" |
Yellow, black text |
"red" |
Red |
Affects: version badge, ◆ in help, ▶ phase markers.
Prompts

Interactive prompts (text, select, password, …) are on by default — no
feature flag, no cfg(...) ceremony. Just import the prelude:
[]
= "*"
Hello, prompt
use *;
Run cargo run --example hello_prompt for the full walkthrough.
What you get
| Category | API |
|---|---|
| Text input | text, secret, multiline |
| Choice | confirm, select, multiselect, groupmultiselect, select_key |
| Filter | autocomplete |
| Specialty | date::date, path::path |
| Framing | intro, outro, note, cancel, boxed::boxed, log::*, stream::* |
| Live work | spinner, progress, tasks, task_log |
| Composition | group::group |
Validation
Every rule lives at cli_ui::prompt::* — no submodule prefix:
use ;
let password = secret
.rule
.run;
Library of rules: required, min_chars / max_chars / exact_chars,
word_count, words_between, has_upper / has_lower / has_digit /
has_special, alpha_only, alphanumeric, only_chars, forbid_chars,
int_between, float_between, email, starts_with, ends_with,
one_of. Compose with .and(), .or(), customise the message with .msg().
For ad-hoc closures use .validate(|s: &str| ...) instead of .rule(...);
both methods accept their natural form thanks to Rust's type inference.
Theming
Every visual style is one struct field away:
use update_colors;
update_colors;
Slots: accent, active, input, success, error, cancel, dim,
header, header_plain, title, intro_badge. Also configurable via
cli_ui::prompt::settings::update: vim keybindings (h/j/k/l), bold
headers, hint visibility, default cancel / error messages.
Cancellation
Ctrl-C and Esc come through as PromptError::Interrupted. The
OnCancel extension trait turns that into a clean exit:
use ;
let name = text.run.or_cancel;
Extending
Implement prompt::core::Prompt — handle(key) -> Step<T>, render(ctx) -> Frame, render_answered(&T) -> Frame. The
generic runner owns raw mode, the key loop, the validation transition,
the answered redraw, and cleanup.
Examples
| File | Shows |
|---|---|
examples/hello_prompt.rs |
smallest possible flow |
examples/prompt.rs |
every prompt type |
examples/prompt_validate.rs |
composable validators + Ctrl-C handling |
Shell completions
Pipe and colour safety
| Context | Behaviour |
|---|---|
| Interactive terminal | Full ANSI colours |
cmd 2>file |
Plain text in file |
NO_COLOR=1 |
Plain text |
TERM=dumb |
Plain text |
| Windows 10+ | Full ANSI via VT mode |
| Windows <10 | Plain text |
Dependencies
Runtime (in your binary):
| Crate | Purpose |
|---|---|
anstream |
Pipe detection, ANSI stripping, NO_COLOR |
anstyle |
Zero-alloc style constants |
windows-sys |
Terminal width on Windows (Windows only) |
Compile-time only (not in binary):
| Crate | Purpose |
|---|---|
syn + quote + proc-macro2 |
Derive macro |
heck |
Struct name → kebab-case |
Subcommands
For tools with multiple commands (mytool download, mytool upload, etc.)
use #[derive(CliCommand)] on an enum instead of #[derive(CliOptions)].
use ;
What you get for free
mytool --help— lists all subcommands with their descriptionsmytool download --help— shows options for that specific commandmytool net --help— shows nested subcommandsmytool help download≡mytool download --helpmytool --version— themed badge, root level onlymytool --completions zsh— full command tree including nested commandsmytool tyop→✘ unknown command: tyop+did you mean: top?mytool -v download <url>≡mytool download -v <url>— global flags anywhere
#[cli(...)] on enum variants
| Key | Description |
|---|---|
alias = "str" |
Short alias — repeat for multiple: #[cli(alias = "dl", alias = "fetch")] |
about = "str" |
Override the doc comment description in help |
Notes on Cmd::global()
Global options are stored in a OnceLock set during parse() — safe for
CLI utilities. For libraries or code that calls parse() multiple times in
the same process, pass global options explicitly:
// instead of Cmd::global()
let = parse_with_global?; // alternative API
License
MIT — see LICENSE.
Changelog
See CHANGELOG.md.