Output Modes

Standout supports multiple output formats through a single handler because modern CLI tools serve two masters: human operators and machine automation.

The same handler logic produces styled terminal output for eyes, plain text for logs, or structured JSON for jq pipelines—controlled entirely by the user's --output flag. This frees you from writing separate "API" and "CLI" logic.

The OutputMode Enum

#![allow(unused)]
fn main() {
pub enum OutputMode {
    Auto,       // Auto-detect terminal capabilities
    Term,       // Always use ANSI escape codes
    Text,       // Never use ANSI codes (plain text)
    TermDebug,  // Keep style tags as [name]...[/name]
    Json,       // Serialize as JSON (skip template)
    Yaml,       // Serialize as YAML (skip template)
    Xml,        // Serialize as XML (skip template)
    Csv,        // Serialize as CSV (skip template)
}
}

Three categories:

Templated modes (Auto, Term, Text): Render the template, vary ANSI handling.

Debug mode (TermDebug): Render the template, keep tags as literals for inspection.

Structured modes (Json, Yaml, Xml, Csv): Skip the template entirely, serialize handler data directly.

Auto Mode

Auto is the default when --output is absent, and an application can change that default with output_mode_fallback(mode) — an explicit --output still outranks it. Auto queries the terminal for color support:

#![allow(unused)]
fn main() {
Term::stdout().features().colors_supported()
}

If colors are supported, Auto behaves like Term (ANSI codes applied). If not, Auto behaves like Text (tags stripped).

This detection happens at render time, not startup. Piping output to a file or another process typically disables color support, so:

myapp list              # Colors (if terminal supports)
myapp list > file.txt   # No colors (not a TTY)
myapp list | less       # No colors (pipe)

The --output Flag

Standout adds a global --output flag accepting these values:

myapp list --output=auto        # Default
myapp list --output=term        # Force ANSI codes
myapp list --output=text        # Force plain text
myapp list --output=term-debug  # Show style tags
myapp list --output=json        # JSON serialization
myapp list --output=yaml        # YAML serialization
myapp list --output=xml         # XML serialization
myapp list --output=csv         # CSV serialization

The flag is global—it applies to all subcommands.

Term vs Text

Term: turns every resolved style tag into ANSI escape codes, including when the destination is a pipe rather than a terminal:

myapp list --output=term > colored.txt

Useful when you want to preserve colors for later display (e.g., less -R).

A term request is unconditional, and the environment's color conventions do not override it: NO_COLOR=1 myapp list --output=term still emits ANSI, the same way CLICOLOR_FORCE=1 myapp list --output=text still emits none. auto is the only mode the environment reaches, and it reaches it through one value: the destination's reported color capability. auto resolves to term when that capability is reported and to text when it is not. NO_COLOR and TERM=dumb suppress the capability, so they turn auto plain; CLICOLOR_FORCE is not part of that capability probe, so it never turns auto into term.

Text: removes Standout's own style tags and adds no ANSI of its own:

myapp list --output=text

Useful for clean output regardless of terminal capabilities, or when processing output with other tools.

Neither term nor text touches ANSI bytes that a handler or template writes literally into the rendered text — the framework does not sanitize those bytes and does not promise to. A caller that needs them gone strips them itself.

term-debug (which shows tags as [name]...[/name] rather than resolving them) is internal: its tag vocabulary and exact spelling may change in any release, so don't build automation against its output the way you might against term or text.

TermDebug Mode

TermDebug preserves style tags instead of converting them:

Template: [title]Hello[/title]
Output:   [title]Hello[/title]

Use cases:

  • Debugging template issues
  • Verifying style tag placement
  • Automated testing of template output

Unlike Term mode, unknown tags don't get the ? marker in TermDebug. TermDebug shows tag placement; it does not check whether a tag has a matching style definition. Use validate_template when validation is required.

Structured Modes

Structured modes bypass the template entirely. Handler data is serialized directly:

#![allow(unused)]
fn main() {
#[derive(Serialize)]
struct ListOutput {
    items: Vec<Item>,
    total: usize,
}

fn list_handler(...) -> HandlerResult<ListOutput> {
    Ok(Output::Render(ListOutput { items, total: items.len() }))
}
}
myapp list --output=json
{
  "items": [...],
  "total": 42
}

Same handler, same types—different output format. This enables:

  • Machine-readable output for scripts
  • Integration with other tools (jq, etc.)
  • API-like behavior from CLI apps

CSV Output

Normal App dispatch flattens the serializable handler data automatically for CSV. That is the same handler data used by the other structured modes; handlers should not inspect the requested mode or return a CSV-specific shape.

The standalone rendering API also supports direct FlatDataSpec rendering when a caller needs explicit columns and headers:

#![allow(unused)]
fn main() {
use standout::tabular::{Column, FlatDataSpec, Width};
use standout::OutputMode;
use standout_render::render_auto_with_spec;

let spec = FlatDataSpec::builder()
    .column(Column::new(Width::Fixed(10)).key("name").header("Name"))
    .column(Column::new(Width::Fixed(10)).key("meta.role").header("Role"))
    .build();

render_auto_with_spec(template, &data, &theme, OutputMode::Csv, Some(&spec))?
}

The key field uses dot notation for nested paths ("meta.role" extracts data["meta"]["role"]).

When a command's canonical response is an object containing the CSV rows, attach a presentation-layer projection through CommandConfig:

#![allow(unused)]
fn main() {
use serde_json::json;
use standout::cli::FnHandler;
use standout::tabular::{Column, Width};
use standout::{CsvProjection, StructuredOutputProjection};

let projection = StructuredOutputProjection::csv(
    CsvProjection::builder("items")
        .column(Column::new(Width::default()).key("language").header("LANGUAGE"))
        .column(Column::new(Width::default()).key("code").header("CODE"))
        .derived_column(
            Column::new(Width::default()).header("NET"),
            |row, _root| json!(
                row["code"].as_i64().unwrap_or(0)
                    - row["comments"].as_i64().unwrap_or(0)
            ),
        )
        .synthetic_row(|root| json!({
            "language": "TOTAL",
            "code": root["totals"]["code"],
            "comments": root["totals"]["comments"]
        }))
        .conditional_row(|root| {
            (root["skipped"].as_u64().unwrap_or(0) > 0)
                .then(|| json!({ "language": "SKIPPED" }))
        })
        .build(),
);

App::builder().command_with("summary", FnHandler::new(summary_handler), |config| {
    config.structured_output_projection(projection)
})?;
}

Direct-column dot paths are resolved against each selected row. Derived columns receive both the current row and the root response. Synthetic-row callbacks receive the root response and run in registration order. Column ordering, headers, and null_repr use the existing FlatDataSpec behavior.

The projection applies only to CSV. Text and terminal modes still use the template, while JSON, YAML, and XML serialize the canonical response. In the pipeline, post-dispatch hooks run before projection and post-output hooks run after it; run, run_with, output-file handling, and final emission therefore all observe the same projected CSV.

See Introduction to Tabular for tabular specifications and layout.

File Output

The --output-file-path flag redirects output to a file:

myapp list --output-file-path=results.txt
myapp list --output=json --output-file-path=data.json

Behavior:

  • Text output: written to file, nothing printed to stdout
  • Binary output: written to the requested file instead of stdout
  • Silent output: no-op

After writing to file, stdout output is suppressed to prevent double-printing.

Customizing Flags

Rename or disable the flags via AppBuilder:

#![allow(unused)]
fn main() {
App::builder()
    .output_flag(Some("format"))       // --format instead of --output
    .output_file_flag(Some("out"))     // --out instead of --output-file-path
    .build()?
}
#![allow(unused)]
fn main() {
App::builder()
    .no_output_flag()                  // Disable --output entirely
    .no_output_file_flag()             // Disable file output
    .build()?
}

Keep Output Mode Out of Handlers

Output mode is a rendering concern and is deliberately absent from CommandContext. A handler should return the same serializable data regardless of whether the caller selected terminal, text, or structured output. If a command's behavior genuinely differs, model that as an explicit command or argument rather than an implicit presentation-mode branch.

Rendering Without CLI

For standalone rendering with explicit mode:

#![allow(unused)]
fn main() {
use standout::{render_auto, OutputMode};

// Renders template for Term/Text, serializes for Json/Yaml
let output = render_auto(template, &data, &theme, OutputMode::Json)?;
}

The "auto" in render_auto refers to template-vs-serialize dispatch, not color detection.

For full control over both output mode and color mode:

#![allow(unused)]
fn main() {
use standout::{render_with_mode, ColorMode};

let output = render_with_mode(
    template,
    &data,
    &theme,
    OutputMode::Term,
    ColorMode::Dark,
)?;
}