Skip to content

Show progress and spinners

output has three progress-feedback helpers, all obtained from a Renderer so they share its writer, theme and interactivity. Each animates in a terminal and prints plain lines under CI — the same code, no branching.

Point the renderer at os.Stderr for progress feedback so it does not pollute a piped stdout:

r := output.New(output.WithWriter(os.Stderr))

Spinner — indeterminate work

err := r.Spin(ctx, "Fetching release", func(ctx context.Context) error {
    return download(ctx)
})

Interactive: an animated spinner next to the message that clears on completion. Non-interactive: Fetching release... then Fetching release... done (or ... failed). A Ctrl-C while the spinner is active cancels fn's context and returns context.Canceled — never a false success.

Need a return value? Use the free generic function:

release, err := output.SpinWithResult(r, ctx, "Fetching", func(ctx context.Context) (Release, error) {
    return fetch(ctx)
})

Progress bar — known total

p := r.Progress(len(files), "Uploading")
for range files {
    upload()
    p.Increment() // or p.IncrementBy(n)
}
p.Done()

Interactive: a live bar Uploading [=====> ] 12/40 (30%). Non-interactive: a line at each 10% boundary, so logs are informative without flooding.

Status — multi-step operations

st := r.Status()
st.Update("Resolving dependencies")
st.Success("Dependencies resolved")
st.Update("Building")
st.Fail("Build failed")
st.Done()

Update replaces the current line in place (interactive) with a spinner-framed message; Success/Warn/Fail finalise the current step onto its own line with the themed icon; Done clears any pending in-place line.

Change the icons, colours, spinner frames and bar runes via theming.

Run one at a time

Status and Progress each hold their own mutex, so a single instance is safe to drive from several goroutines. Nothing coordinates between helpers: two Status values, or a Status and a Progress, writing to the same destination will overwrite each other's in-place redraws. Spin is stricter still — it takes over the terminal with a Bubble Tea program and must not overlap with another spinner.

Show one piece of live feedback at a time, and finish it (Done, or the return of Spin) before starting the next.

What is not configurable

The bar is a fixed 40 characters wide and does not adapt to terminal width; on a narrow terminal the line wraps. The non-interactive form logs at fixed 10% boundaries. Only the glyphs are themeable. Full list in what this does not do.

Why the spinner stops animating when you pipe the command

Interactivity is detected from stdout, even for a renderer writing to stderr — so mytool | jq prints plain lines rather than animating, on both streams. That is deliberate; interactive vs plain rendering explains why, and how to override it.