App Configuration
AppBuilder is the unified entry point for configuring your application. Instead of scattering configuration across multiple structs (Standout, RenderSetup, Theme), everything from command registration to theme selection happens in one fluent interface.
This design ensures that your application defines its entire environment—commands, styles, templates, and hooks—before the runtime starts, preventing configuration race conditions and simplifying testing.
This guide covers the full setup: embedding resources, registering commands, configuring themes, and customizing behavior.
See also:
- Rendering System for details on templates and styles.
- Topics System for help topics.
Basic Setup
#![allow(unused)] fn main() { use standout::cli::App; use standout_macros::{embed_templates, embed_styles}; let app = App::builder() .templates(embed_templates!("src/templates")) .styles(embed_styles!("src/styles")) .default_theme("default") .command("list", list_handler, "list.j2") .build()?; app.run(Cli::command(), std::env::args()); }
Embedding Resources
Templates
embed_templates! embeds template files at compile time:
#![allow(unused)] fn main() { .templates(embed_templates!("src/templates")) }
Collects files matching: .jinja, .jinja2, .j2, .txt (in priority order).
Directory structure:
src/templates/
list.j2
add.j2
db/
migrate.j2
status.j2
Templates are referenced by path without extension: "list", "db/migrate".
Styles
embed_styles! embeds stylesheet files:
#![allow(unused)] fn main() { .styles(embed_styles!("src/styles")) }
Collects files matching: .yaml, .yml.
src/styles/
default.yaml
dark.yaml
light.yaml
Themes are referenced by filename without extension: "default", "dark".
Hot Reloading
In debug builds, embedded resources are re-read from disk on each render—edit without recompiling. In release builds, embedded content is used directly.
This is automatic when the source path exists on disk.
Runtime Overrides
Users can override embedded resources with local files:
#![allow(unused)] fn main() { App::builder() .templates(embed_templates!("src/templates")) .templates_dir("~/.myapp/templates") // Overrides embedded .styles(embed_styles!("src/styles")) .styles_dir("~/.myapp/themes") // Overrides embedded }
Local directories take precedence. This enables user customization without recompiling.
Theme Selection
From Stylesheet Registry
#![allow(unused)] fn main() { .styles(embed_styles!("src/styles")) // Optional: set explicit default name // If omitted, tries "default", "theme", then "base" .default_theme("dark") }
If .default_theme() is not called, AppBuilder attempts to load a theme from the registry in this order:
defaultthemebase
This allows you to provide a standard base.yaml or theme.yaml without requiring explicit configuration code. If the explicit theme isn't found, build() returns SetupError::ThemeNotFound.
Explicit Theme
#![allow(unused)] fn main() { let theme = Theme::new() .add("title", Style::new().bold().cyan()) .add("muted", Style::new().dim()); App::builder() .theme(theme) // Overrides stylesheet registry }
Explicit .theme() takes precedence over .default_theme().
Command Registration
Simple Commands
#![allow(unused)] fn main() { App::builder() .command("list", list_handler, "list.j2") .command("add", add_handler, "add.j2") }
Arguments: command name, handler function, template path.
With Configuration
#![allow(unused)] fn main() { App::builder() .command_with("delete", delete_handler, |cfg| cfg .template("delete.j2") .pre_dispatch(require_confirmation) .post_dispatch(log_deletion)) }
Inline hook attachment without separate .hooks() call.
Nested Groups
#![allow(unused)] fn main() { App::builder() .group("db", |g| g .command("migrate", migrate_handler, "db/migrate.j2") .command("status", status_handler, "db/status.j2") .group("backup", |b| b .command("create", backup_create, "db/backup/create.j2") .command("restore", backup_restore, "db/backup/restore.j2"))) }
Creates command paths: db.migrate, db.status, db.backup.create, db.backup.restore.
From Dispatch Macro
#![allow(unused)] fn main() { #[derive(Dispatch)] enum Commands { List, Add, #[dispatch(nested)] Db(DbCommands), } App::builder() .commands(Commands::dispatch_config()) }
The macro generates registration for all variants.
Default Command
When a CLI is invoked without a subcommand (a "naked" invocation like myapp or myapp --verbose), you can specify a default command to run:
#![allow(unused)] fn main() { App::builder() .default_command("list") .command("list", list_handler, "list.j2") .command("add", add_handler, "add.j2") }
With this configuration:
myappbecomesmyapp listmyapp --output=jsonbecomesmyapp list --output=jsonmyapp add foostays asmyapp add foo(explicit command takes precedence)
With Dispatch Macro
Use the #[dispatch(default)] attribute to mark a variant as the default:
#![allow(unused)] fn main() { #[derive(Dispatch)] #[dispatch(handlers = handlers)] enum Commands { #[dispatch(default)] List, Add, } App::builder() .commands(Commands::dispatch_config()) }
Only one command can be marked as default. Multiple #[dispatch(default)] attributes will cause a compile error.
Hooks
Attach hooks to specific command paths:
#![allow(unused)] fn main() { App::builder() .command("migrate", migrate_handler, "migrate.j2") .hooks("db.migrate", Hooks::new() .pre_dispatch(require_admin) .post_dispatch(add_timestamp) .post_output(log_result)) }
The path uses dot notation matching the command hierarchy.
Context Injection
Add values available in all templates:
Static Context
#![allow(unused)] fn main() { App::builder() .context("version", "1.0.0") .context("app_name", "MyApp") }
Dynamic Context
#![allow(unused)] fn main() { App::builder() .context_fn("terminal_width", |ctx| { Value::from(ctx.terminal_width.unwrap_or(80)) }) .context_fn("timestamp", |_ctx| { Value::from(chrono::Utc::now().to_rfc3339()) }) }
Dynamic providers receive RenderContext with output mode, terminal width, and handler data.
Topics
Add help topics:
#![allow(unused)] fn main() { App::builder() .topics_dir("docs/topics") .add_topic(Topic::new("auth", "Authentication...", TopicType::Text, None)) }
See Topics System for details.
Flag Customization
Output Flag
#![allow(unused)] fn main() { App::builder() .output_flag(Some("format")) // --format instead of --output }
#![allow(unused)] fn main() { App::builder() .no_output_flag() // Disable entirely }
File Output Flag
#![allow(unused)] fn main() { App::builder() .output_file_flag(Some("out")) // --out instead of --output-file-path }
#![allow(unused)] fn main() { App::builder() .no_output_file_flag() // Disable entirely }
The App Struct
build() produces an App:
#![allow(unused)] fn main() { pub struct App { registry: TopicRegistry, output_flag: Option<String>, output_file_flag: Option<String>, output_mode: OutputMode, theme: Option<Theme>, command_hooks: HashMap<String, Hooks>, template_registry: Option<TemplateRegistry>, stylesheet_registry: Option<StylesheetRegistry>, } }
Running the App
Standard Execution
#![allow(unused)] fn main() { if let Some(matches) = app.run(Cli::command(), std::env::args()) { // Standout didn't handle this command, fall back to legacy legacy_dispatch(matches); } }
Parses args, dispatches to handler, prints output. Returns Option<ArgMatches>—None if handled, Some(matches) for fallback.
Capture Output
For testing, post-processing, or when you need the output string:
#![allow(unused)] fn main() { match app.run_to_string(cmd, args) { RunResult::Handled(output) => { /* use output string */ } RunResult::Binary(bytes, filename) => { /* handle binary */ } RunResult::NoMatch(matches) => { /* fallback dispatch */ } } }
Returns RunResult instead of printing.
Parse Only
#![allow(unused)] fn main() { let matches = app.parse_with(cmd); // Use matches for manual dispatch }
Parses with Standout's augmented command but doesn't dispatch.
Build Validation
build() validates:
- Theme exists if
.default_theme()was called - Returns
SetupError::ThemeNotFoundif not found
What's NOT validated at build time:
- Templates (resolved lazily at render time)
- Command handlers
- Hook signatures (verified at registration)
Complete Example
use standout::cli::{App, HandlerResult, Output}; use standout_macros::{embed_templates, embed_styles}; use clap::{Command, Arg}; use serde::Serialize; #[derive(Serialize)] struct ListOutput { items: Vec<String>, } fn list_handler(matches: &ArgMatches, _ctx: &CommandContext) -> HandlerResult<ListOutput> { let items = vec!["one".into(), "two".into()]; Ok(Output::Render(ListOutput { items })) } fn main() -> Result<(), Box<dyn std::error::Error>> { let cli = Command::new("myapp") .subcommand(Command::new("list").about("List items")); let app = App::builder() .templates(embed_templates!("src/templates")) .styles(embed_styles!("src/styles")) .default_theme("default") .context("version", env!("CARGO_PKG_VERSION")) .command("list", list_handler, "list.j2") .topics_dir("docs/topics") .build()?; app.run(cli, std::env::args()); Ok(()) }
Template src/templates/list.j2:
[header]Items[/header] ({{ items | length }} total)
{% for item in items %}
- {{ item }}
{% endfor %}
[muted]v{{ version }}[/muted]
Style src/styles/default.yaml:
header:
fg: cyan
bold: true
muted:
dim: true