Skip to content

The Renderer façade

output deliberately exposes one public entry point — output.New returning a *Renderer — rather than a scatter of independent constructors (NewWriter, NewTableWriter, NewStatus, NewProgress, Spin). This page explains why.

Configure once, read everywhere

A CLI's output concerns share four decisions: where output goes, which format it is in, how it is styled, and whether the destination is interactive. Those belong together and rarely change within a command. The Renderer fixes them at construction, and every method reads them:

r := output.New(
    output.WithWriter(os.Stdout),
    output.WithFormat(output.FormatJSON),
)
r.Emit(resp)   // knows the writer and that we are in JSON mode
r.Table(rows)  // same writer, same format — no re-passing

The alternative — threading (writer, format) through every call — is what the predecessor package did, and it meant every call site re-derived the same two values, usually from a *cobra.Command. The façade removes that repetition and the coupling behind it.

No hidden global state

The predecessor reached for os.Stderr, os.Stdout, os.Stdin and the CI environment variable inside its render calls. That made behaviour depend on process globals and forced tests to swap os.Stdout or set env vars — hostile to parallelism.

The Renderer takes its writer and interactivity as injected values. Tests construct output.New(output.WithWriter(&buf), output.WithInteractive(true)) and assert on the buffer, fully in parallel, with no global mutation. IsInteractive() remains exported as the default detector the renderer falls back to when WithInteractive is not supplied — so production behaviour is unchanged.

One format matrix

Because the format lives on the renderer, Write can honour the whole matrix from a single method: text via a caller function, JSON/YAML by marshalling, and CSV/TSV/Markdown by tabulating. Table is the specialised entry point for tabular data with column options; Write is the general envelope. Both funnel structured formats through the same rendering core, so a value renders identically whichever door it comes through.

The progress family shares the renderer

Status(), Progress() and Spin() inherit the renderer's writer, theme and interactivity. A tool configures the destination and palette once and every piece of feedback is consistent — and every piece degrades to plain lines together when output is not a terminal.