How To: Adopt Standout Alongside Existing Clap Code
Adopting a new framework shouldn't require rewriting your entire application. Standout is designed for gradual adoption, allowing you to migrate one command at a time without breaking existing functionality.
This guide shows how to run Standout alongside your existing manual dispatch or Clap loop.
The Core Pattern
When Standout handles a command, it prints output and returns None. When no handler matches, it returns Some(ArgMatches) for your fallback:
#![allow(unused)] fn main() { if let Some(matches) = app.run(cli, std::env::args()) { // Standout didn't handle this command, fall back to legacy your_existing_dispatch(matches); } }
Pattern 1: Standout First, Fallback Second
Try Standout dispatch first. If no match, use your existing code:
use standout::cli::App; use clap::Command; fn main() { let cli = Command::new("myapp") .subcommand(Command::new("list")) // Standout handles .subcommand(Command::new("status")) // Your existing code .subcommand(Command::new("config")); // Your existing code // Build Standout for just the commands you want let app = App::builder() .command("list", list_handler, "list.j2") .build() .expect("Failed to build app"); // Try Standout first if let Some(matches) = app.run(cli, std::env::args()) { // Fall back to your existing dispatch match matches.subcommand() { Some(("status", sub)) => handle_status(sub), Some(("config", sub)) => handle_config(sub), _ => eprintln!("Unknown command"), } } }
Pattern 2: Existing Code First, Standout Fallback
Your dispatch handles known commands, Standout handles new ones:
fn main() { let cli = build_cli(); let matches = cli.clone().get_matches(); // Your existing dispatch first match matches.subcommand() { Some(("legacy-cmd", sub)) => { handle_legacy(sub); return; } Some(("old-feature", sub)) => { handle_old_feature(sub); return; } _ => { // Not handled by existing code } } // Standout handles everything else let app = build_standout_app(); app.run(cli, std::env::args()); }
Pattern 3: Standout Inside Your Match
Call Standout for specific commands within your existing match:
fn main() { let cli = build_cli(); let matches = cli.clone().get_matches(); match matches.subcommand() { Some(("status", sub)) => handle_status(sub), Some(("config", sub)) => handle_config(sub), // Use Standout just for these Some(("list", _)) | Some(("show", _)) => { let app = build_standout_app(); app.run(cli, std::env::args()); } _ => eprintln!("Unknown command"), } }
Adding Standout to One Command
Minimal setup for a single command:
#![allow(unused)] fn main() { let app = App::builder() .command("list", |matches, ctx| { let items = fetch_items()?; Ok(Output::Render(ListOutput { items })) }, "{% for item in items %}- {{ item }}\n{% endfor %}") .build()?; }
No embedded files required. The template is inline. No theme means style tags show ? markers, but rendering still works.
Sharing Clap Command Definition
Standout augments your clap::Command with --output and help. You can share the definition:
fn build_cli() -> Command { Command::new("myapp") .subcommand(Command::new("list").about("List items")) .subcommand(Command::new("status").about("Show status")) } fn main() { let cli = build_cli(); let app = App::builder() .command("list", list_handler, "list.j2") .build()?; // Standout augments the command, then dispatches if let Some(matches) = app.run(cli, std::env::args()) { // matches from the augmented command (has --output, etc.) match matches.subcommand() { Some(("status", sub)) => handle_status(sub), _ => {} } } }
Gradual Migration Strategy
-
Start with one command: Pick a command with complex output. Add Standout for just that command.
-
Keep existing tests passing: Your dispatch logic stays the same for unhandled commands.
-
Add more commands over time: Register additional handlers as you refactor.
-
Add themes when ready: Start with inline templates, add YAML stylesheets later.
-
Eventually remove legacy dispatch: Once all commands are migrated, simplify to just
app.run().
Using run() vs run_to_string()
run() prints output directly and returns Option<ArgMatches>:
#![allow(unused)] fn main() { if let Some(matches) = app.run(cli, args) { // Standout didn't handle, use matches for fallback legacy_dispatch(matches); } }
run_to_string() captures output instead of printing, returning RunResult:
#![allow(unused)] fn main() { match app.run_to_string(cli, args) { RunResult::Handled(output) => { /* process output string */ } RunResult::Binary(bytes, filename) => { /* handle binary */ } RunResult::NoMatch(matches) => { /* access matches */ } } }
Use run_to_string() when you need to:
- Capture output for testing
- Post-process the output string before printing
- Log or record what was generated
For normal partial adoption, run() is simpler and preferred.
Accessing the --output Flag in Fallback
Standout adds --output globally. In fallback code, you can still access it:
#![allow(unused)] fn main() { if let Some(matches) = app.run(cli, std::env::args()) { // Get the output mode Standout parsed let mode = matches.get_one::<String>("_output_mode") .map(|s| s.as_str()) .unwrap_or("auto"); match matches.subcommand() { Some(("status", sub)) => { if mode == "json" { println!("{}", serde_json::to_string(&status_data)?); } else { print_status_text(&status_data); } } _ => {} } } }
Disabling Standout's Flags
If --output conflicts with your existing flags:
#![allow(unused)] fn main() { App::builder() .no_output_flag() // Don't add --output .no_output_file_flag() // Don't add --output-file-path .command("list", handler, template) .build()? }
Example: Hybrid Application
Complete example with both Standout and manual handlers:
use standout::cli::{App, HandlerResult, Output, CommandContext}; use clap::{Command, ArgMatches}; use serde::Serialize; #[derive(Serialize)] struct ListOutput { items: Vec<String> } fn list_handler(_m: &ArgMatches, _ctx: &CommandContext) -> HandlerResult<ListOutput> { Ok(Output::Render(ListOutput { items: vec!["one".into(), "two".into()], })) } fn handle_status(_matches: &ArgMatches) { println!("Status: OK"); } fn main() -> Result<(), Box<dyn std::error::Error>> { let cli = Command::new("myapp") .subcommand(Command::new("list").about("List items (Standout)")) .subcommand(Command::new("status").about("Show status (legacy)")); let app = App::builder() .command("list", list_handler, "{% for i in items %}- {{ i }}\n{% endfor %}") .build()?; if let Some(matches) = app.run(cli, std::env::args()) { match matches.subcommand() { Some(("status", sub)) => handle_status(sub), _ => eprintln!("Unknown command"), } } Ok(()) }
Run it:
myapp list # Standout handles, renders template
myapp list --output=json # Standout handles, JSON output
myapp status # Fallback to handle_status()