Input Sources
standout-input provides a unified way to acquire input before your handler runs. This enables interactive workflows like:
- Opening an editor for commit messages
- Prompting for confirmation ("Delete 5 items?")
- Selecting from a list of options
- Reading piped stdin for scripting
- Pre-filling from clipboard
All without polluting your handler logic.
Why Input Sources?
CLI commands often need content that doesn't fit in command-line arguments. The gh pr create pattern is common:
# Option 1: Inline (awkward for long text)
gh pr create --body "Long description..."
# Option 2: Editor (interactive)
gh pr create --editor
# Option 3: Piped (scriptable)
echo "Description" | gh pr create --body-file -
Your CLI should support these patterns, but the logic doesn't belong in handlers:
- Separation of concerns: Handlers produce results, input acquisition is a setup concern
- Testability: Handler adapters receive already-resolved data through an explicit seam
- Composability: Different commands can mix input sources
An InputChain runs as a pre-dispatch phase, before your handler executes. The handler receives the resolved value; input acquisition is transparent.
Source Types
Every source implements InputCollector<T> and composes into an InputChain<T>. See Backends for the full constructor and feature-flag reference for each one.
Non-Interactive Sources at a Glance
These work in scripts and CI pipelines:
| Source | Type | Use Case |
|---|---|---|
ArgSource | String | Short content as a CLI argument |
FlagSource | bool | A CLI flag, with an optional .inverted() |
StdinSource | String | Piped content (cat file | cmd) |
EnvSource | String | Environment variable |
ClipboardSource | String | Pre-filled content from the clipboard |
DefaultSource<T> | T | Hardcoded fallback |
Interactive Sources at a Glance
These require a terminal and are grouped by feature flag:
| Source | Feature | Type | Use Case |
|---|---|---|---|
TextPromptSource | simple-prompts (default) | String | Short text input |
ConfirmPromptSource | simple-prompts (default) | bool | Yes/no questions |
EditorSource | editor (default) | String | Long-form text (commit messages) |
InquireText | inquire | String | Rich text input with autocomplete |
InquireConfirm | inquire | bool | Polished yes/no prompt |
InquireSelect<T> | inquire | T | Pick one from a list |
InquireMultiSelect<T> | inquire | Vec<T> | Pick many from a list |
InquirePassword | inquire | String | Hidden text input |
InquireEditor | inquire | String | Editor with an inquire preview |
Building a Chain
Chain sources in priority order with InputChain:
#![allow(unused)] fn main() { use standout_input::{InputChain, ArgSource, StdinSource, EditorSource}; let body = InputChain::<String>::new() .try_source(ArgSource::new("body")) // First: try the CLI argument .try_source(StdinSource::new()) // Second: try piped stdin .try_source(EditorSource::new() // Third: open the editor .extension(".md")) .resolve(&matches)?; }
The chain stops at the first source whose is_available() returns true and whose collect() returns Some(_). This is the gh pr create pattern:
gh pr create --body "text"→ uses the argumentecho "text" | gh pr create→ uses stdingh pr create→ opens the editor
Add .default(value) to fall back to a literal value instead of erroring with InputError::NoInput when every source is skipped, and .validate(f, "message") to apply a rule regardless of which source produced the value. See Introduction to Input for the full walkthrough.
Wiring a Chain to a Command
Outside the framework, a handler resolves a chain itself, passing the run's InputSources so stdin/clipboard/prompt mocks are honored in tests:
#![allow(unused)] fn main() { fn create(matches: &ArgMatches, ctx: &CommandContext) -> HandlerResult<Pad> { let body = InputChain::<String>::new() .try_source(ArgSource::new("body")) .try_source(StdinSource::new()) .try_source(EditorSource::new()) .resolve_from(matches, ctx.input_sources())?; /* business logic ... */ } }
With the standout framework, CommandConfig::input(name, chain) registers the same chain to run in pre-dispatch, and the handler reads the resolved value with ctx.input::<T>(name) instead of resolving it itself. See Framework Integration for the full wiring and the CommandContextInput trait.
Skipping Interactive Sources
Some commands want a flag like --no-editor to skip interactive input entirely. Since chain construction is ordinary Rust, build the chain conditionally instead of adding sources that would prompt:
#![allow(unused)] fn main() { let no_editor = matches.get_flag("no-editor"); let mut chain = InputChain::<String>::new() .try_source(ArgSource::new("body")) .try_source(StdinSource::new()); if !no_editor { chain = chain.try_source(EditorSource::new()); } let body = chain.default(String::new()).resolve(&matches)?; }
Direct Use Without a Chain
For commands with input logic too specific for a declarative chain, call the primitives directly. Every interactive source also has a .prompt() shortcut that skips the chain and the &ArgMatches plumbing (see Standalone Prompts):
#![allow(unused)] fn main() { use standout_input::{read_if_piped, EditorSource}; fn create(matches: &ArgMatches, _ctx: &CommandContext) -> HandlerResult<Pad> { let no_editor = matches.get_flag("no-editor"); let title_arg = matches.get_one::<String>("title"); let content = if let Some(piped) = read_if_piped()? { // Piped input takes precedence piped } else if let Some(title) = title_arg { if no_editor { title.clone() } else { let body = EditorSource::new() .initial_content(format!("# {}\n\n", title)) .extension(".md") .prompt()?; format!("{}\n\n{}", title, body) } } else if no_editor { return Err(anyhow!("No content provided. Use --title or pipe input.")); } else { EditorSource::new().prompt()? }; // ... rest of handler } }
Editor Detection
Editor detection follows established conventions:
| Priority | Source | Example |
|---|---|---|
| 1 | VISUAL env var | VISUAL=code |
| 2 | EDITOR env var | EDITOR=vim |
| 3 | Platform default | vim, vi, nano (Unix), notepad (Windows) |
EditorSource::is_available() also requires stdin to be a terminal, so a piped invocation never blocks on an editor.
Clipboard Integration
#![allow(unused)] fn main() { use standout_input::ClipboardSource; let content = InputChain::<String>::new() .try_source(ArgSource::new("content")) .try_source(ClipboardSource::new()) .try_source(EditorSource::new()) .resolve(&matches)?; }
Platform support:
| Platform | Read Command |
|---|---|
| macOS | pbpaste |
| Linux | xclip -selection clipboard -o |
| Other | InputError::ClipboardFailed — not supported |
Comparison with Output Piping
Input sources and output piping are symmetric but opposite:
| Aspect | Input Sources | Output Piping |
|---|---|---|
| Direction | External → Handler | Handler → External |
| Pipeline position | Pre-dispatch | Post-output |
| Interactive | Can be (editor, prompts) | Never |
| Purpose | Acquire content | Transform/route output |
INPUT SOURCES OUTPUT PIPING
↓ ↓
[Arg/Stdin/Editor] → Handler → Render → [jq/tee/clipboard]
Error Handling
InputError carries the failure reason so a chain-level ? produces an actionable message:
No editor found. Set VISUAL or EDITOR environment variable. // InputError::NoEditor
Editor cancelled without saving. // InputError::EditorCancelled
Failed to read stdin: <io error> // InputError::StdinFailed
Validation failed: Message cannot be empty // InputError::ValidationFailed
No input provided and no default available. // InputError::NoInput
For interactive sources, a validation failure re-prompts instead of returning an error — see Backends for the retry semantics.
Security Considerations
Editor execution: The editor command is resolved from environment variables. Ensure VISUAL/EDITOR are set by the user, not from untrusted sources.
Temp file handling: EditorSource writes the initial content to a named temp file and hands it to the editor process; the file is removed when the collector drops it. Content may briefly exist on disk in the system temp directory.
Summary
| Feature | Method |
|---|---|
| From a CLI argument | ArgSource::new("name") |
| From a CLI flag | FlagSource::new("name") |
| From piped stdin | StdinSource::new() |
| From an environment variable | EnvSource::new("VAR") |
| From the clipboard | ClipboardSource::new() |
| From the editor | EditorSource::new() |
| Fallback value | .default(value) |
| Validation | .validate(f, "error message") |
| Chain multiple sources | InputChain::new().try_source(...).try_source(...) |
For the full constructor reference and feature flags, see Backends. For wiring a chain into a standout command, see Framework Integration.