standout 7.6.4

Styled CLI template rendering with automatic terminal detection
docs.rs failed to build standout-7.6.4
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Visit the last successful build: standout-7.6.2

Standout

A CLI framework for Rust that enforces separation between logic and presentation.

Test your data. Render your view.

use standout::cli::{CommandContext, Output};
use standout::handler;
use serde::Serialize;

#[derive(Serialize)]
struct ListResult { items: Vec<String>, total: usize }

#[handler]
fn list(#[ctx] _ctx: &CommandContext) -> Result<Output<ListResult>, anyhow::Error> {
    let items = storage::list()?;
    Ok(Output::Render(ListResult { total: items.len(), items }))
}

// Test the handler directly—no stdout capture needed
#[test]
fn test_list() {
    let Output::Render(result) = list(&ctx).unwrap() else {
        panic!("expected rendered data");
    };
    assert_eq!(result.total, 3);
}

In a full application, storage and application behavior belong in a CLI-free library. The handler belongs in the binary package and adapts library results into CLI-owned serializable view models. See the production-shaped example.

What is Standout?

Standout combines two standalone libraries into a cohesive framework:

  • standout-dispatch — Execution pattern where handlers return data, renderers produce output
  • standout-render — Terminal rendering with templates, themes, and adaptive styles

The framework provides the glue: clap integration, --output flag handling, auto-dispatch from derive macros, and the AppBuilder configuration API.

Why Standout?

CLI code that mixes logic with println! is impossible to unit test. With Standout:

  • Handlers return structs, not strings—test them like any other function
  • Multiple output modes from the same handler: rich terminal, JSON, YAML, CSV
  • MiniJinja templates with hot reload during development
  • CSS/YAML themes with automatic light/dark mode support
  • Incremental adoption—migrate one command at a time

Quick Start

[dependencies]
standout = "7"
standout-dispatch = "7" # required by #[handler] and #[derive(Dispatch)]
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
anyhow = "1"
use standout::cli::{App, Dispatch, CommandContext, HandlerResult, Output};
use standout::{embed_templates, embed_styles};
use clap::{ArgMatches, CommandFactory, Parser, Subcommand};

#[derive(Parser)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand, Dispatch)]
#[dispatch(handlers = handlers)]
pub enum Commands {
    List,
}

mod handlers {
    use super::*;

    pub fn list(_m: &ArgMatches, _ctx: &CommandContext) -> HandlerResult<Vec<String>> {
        Ok(Output::Render(vec!["item-1".into(), "item-2".into()]))
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let app = App::builder()
        .commands(Commands::dispatch_config())?
        .templates(embed_templates!("src/templates"))
        .styles(embed_styles!("src/styles"))
        .build()?;

    app.run(Cli::command(), std::env::args());
    Ok(())
}
myapp list                  # Rich terminal output
myapp list --output json    # JSON for scripting

Documentation

Framework Topics

Crate Documentation

API Reference

Standalone Crates

Each component can be used independently:

License

MIT