standout-input 7.6.2

Declarative input collection for CLI applications
Documentation
# Framework Integration

This page describes how `standout-input` plugs into the [`standout`](https://crates.io/crates/standout) CLI framework so that input chains become a declarative part of your command configuration. If you only want to use `standout-input` standalone, see [Introduction to Input](../guides/intro-to-input.md) — the framework integration is purely additive.

---

## The Picture

Without framework integration, a handler resolves chains imperatively:

```rust
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(matches)?;          // <-- handler does this itself

    /* business logic ... */
}
```

That works, but the chain becomes invisible to anyone reading the command's registration: input rules are mixed in with logic, and you can't see at a glance "this command takes a body that may come from arg / stdin / editor".

With the integration, the chain is part of `CommandConfig`, just like `template`, `hooks`, and `pipe_through`:

```rust
use standout::cli::{App, CommandContextInput, Output};
use standout::input::{ArgSource, EditorSource, InputChain, StdinSource};

App::builder()
    .command_with("create", create, |cfg| {
        cfg.template("create.jinja")
            .input("body", InputChain::<String>::new()
                .try_source(ArgSource::new("body"))
                .try_source(StdinSource::new())
                .try_source(EditorSource::new()))
    })?
    .build()?;

fn create(_m: &ArgMatches, ctx: &CommandContext) -> HandlerResult<Value> {
    let body: &String = ctx.input("body")?;     // <-- already resolved
    /* business logic ... */
}
```

The chain runs in the **pre-dispatch** phase — before the handler is called — so handlers always see fully-resolved input. Errors during resolution (validation failure, editor cancelled, …) abort the request before any business logic runs.

---

## Where Resolution Happens

`standout`'s execution pipeline runs hooks in three phases:

```
parsed CLI args → PRE-DISPATCH → handler → POST-DISPATCH → render → POST-OUTPUT
```

`.input(name, chain)` is sugar over `.pre_dispatch(...)` — the same hook used for auth checks, request-scoped state, etc. Each `.input(...)` call adds one pre-dispatch hook that:

1. Walks to the deepest subcommand's `ArgMatches` (so chains see the same args the handler does).
2. Calls `chain.resolve_with_source(matches)`.
3. Stashes the result in an [`Inputs`]https://docs.rs/standout-input/latest/standout_input/struct.Inputs.html bag on `ctx.extensions` under `name`.

If resolution returns an error, dispatch stops and the framework reports `` Hook error: input `body`: <error message> ``. The handler does not run.

---

## Reading Inputs in the Handler

Bring the [`CommandContextInput`](https://docs.rs/standout/latest/standout/cli/trait.CommandContextInput.html) extension trait into scope and call `.input::<T>(name)`:

```rust
use standout::cli::{CommandContextInput, Output};

fn create(_m: &ArgMatches, ctx: &CommandContext) -> HandlerResult<Value> {
    let body: &String = ctx.input("body")?;
    let force: &bool = ctx.input("force")?;

    /* ... */
}
```

The lookup is by `(name, T)`. If the name was never registered, you get a [`MissingInput::NotRegistered`](https://docs.rs/standout-input/latest/standout_input/enum.MissingInput.html) error. If the registered type doesn't match `T`, you get `MissingInput::TypeMismatch`. The error type implements `std::error::Error` and converts cleanly with `?`.

### Inspecting the source

Sometimes you want to know *where* an input came from — for instance, to log "title was read from clipboard" or to alter behavior when input is piped vs. interactive:

```rust
match ctx.input_source("body") {
    Some(InputSourceKind::Editor) => log::info!("body composed in editor"),
    Some(InputSourceKind::Stdin) => log::info!("body piped from stdin"),
    Some(other) => log::debug!("body came from {other}"),
    None => unreachable!("body is registered, so it was resolved"),
}
```

### Iterating all inputs

For diagnostic output (like `--explain` flags) you can grab the whole bag:

```rust
if let Some(bag) = ctx.inputs() {
    for (name, source) in bag.iter_sources() {
        eprintln!("  {name}: {source}");
    }
}
```

---

## Multiple Inputs

`.input(...)` accumulates. A command can declare any number of named inputs of any types — including multiple inputs of the same type, which the `TypeId`-keyed `ctx.app_state` / raw `ctx.extensions` cannot disambiguate:

```rust
.command_with("create", create, |cfg| {
    cfg.template("create.jinja")
        .input("title", InputChain::<String>::new()
            .try_source(ArgSource::new("title"))
            .default("untitled".to_string()))
        .input("body", InputChain::<String>::new()
            .try_source(ArgSource::new("body"))
            .try_source(StdinSource::new())
            .try_source(EditorSource::new()))
        .input("force", InputChain::<bool>::new()
            .try_source(FlagSource::new("force"))
            .default(false))
})
```

Each chain runs in registration order during pre-dispatch. They share the same `Inputs` bag on `ctx.extensions`, so two `String` inputs (`title`, `body`) coexist without colliding.

---

## Validation

Chain-level validation runs as part of `resolve_with_source`. If validation fails on a non-interactive source, the pre-dispatch hook returns an error and dispatch aborts:

```rust
.input("body", InputChain::<String>::new()
    .try_source(ArgSource::new("body"))
    .validate(|s| !s.trim().is_empty(), "body must not be empty"))
```

If the user runs `mycli create --body "   "`, the framework reports:

```
Hook error: input `body`: validation failed: body must not be empty
```

For interactive sources (prompts, editor), validation failure re-prompts instead of aborting — the chain decides the loop. See [Backends](backends.md) for the full validation/retry semantics.

---

## Testing

The framework path composes naturally with `standout-test`:

```rust
use standout_test::TestHarness;

#[test]
fn create_uses_arg_when_provided() {
    let app = build_app();
    let cmd = my_clap_command();

    let result = TestHarness::new()
        .text_output()
        .run(&app, cmd, ["mycli", "create", "--body", "hello"]);

    result.assert_stdout_contains("hello");
}

#[test]
fn create_falls_back_to_stdin() {
    let app = build_app();
    let cmd = my_clap_command();

    let result = TestHarness::new()
        .piped_stdin("from pipe\n")
        .text_output()
        .run(&app, cmd, ["mycli", "create"]);

    result.assert_stdout_contains("from pipe");
}
```

The harness installs `MockStdin` / `MockClipboard` via `standout-input`'s process-global default readers, so `StdinSource::new()` and `ClipboardSource::new()` inside the chain transparently see the mocks. No source code changes are needed to make the chain testable.

For lower-level tests that don't need the harness, you can manipulate the readers directly with [`set_default_stdin_reader`](https://docs.rs/standout-input/latest/standout_input/fn.set_default_stdin_reader.html) and friends; serialize tests that touch them with `#[serial]` from `serial_test`.

---

## Re-exports and Feature Flags

`standout` re-exports `standout-input` as `standout::input`, so a single dependency on `standout` is enough:

```toml
[dependencies]
standout = "7"
```

```rust
use standout::input::{ArgSource, InputChain, StdinSource};
```

A default `standout` dependency only enables `standout-input`'s `simple-prompts` backend, which has no extra deps. The heavier backends are opt-in via these `standout` features:

| Feature | Enables | Adds deps |
|---------|---------|-----------|
| `input-editor` | `EditorSource` (opens `$VISUAL` / `$EDITOR`) | `tempfile`, `which`, `shell-words` |
| `input-inquire` | The `Inquire*` rich TUI prompt sources | `inquire` (~29 transitive) |

```toml
[dependencies]
standout = { version = "7", features = ["input-editor"] }
```

You can still depend on `standout-input` directly if you want to bypass the `standout` re-export and pick features there.

---

## When NOT to Use the Builder Integration

The standalone `chain.resolve(matches)?` form is still the right tool when:

- Input shape depends on already-resolved values. If `--mode` decides which other inputs to ask for, you can't precompute a static chain.
- You're adopting `standout` incrementally and your handler isn't yet on the framework path.
- You're using `standout-input` outside the `standout` framework altogether.

In every other case, `.input(...)` keeps the command's input contract visible at registration time, alongside its template and hooks.