byob-go-cli

Progress writes to ErrOut, never Out

byob-progress.4 iostreamsprogress

Problem: a spinner or bar written to stdout destroys pipeability. mytool fetch | jq . gets \r-animated ANSI codes in its input stream, and the jq parse fails — or worse, silently succeeds on garbage.

Idea: progress writes only to IO.ErrOut. This reinforces byob-iostreams.3 (data to Out, chatter to ErrOut) and byob-output.1 (TTY-adaptive output goes to the adaptive side). Data — the thing a user might pipe — stays on Out, uncluttered. Chatter + progress share ErrOut; the byob-logging.4 "quiet by default" posture keeps logs out of the way unless asked.

Rule of thumb: if removing a print would change what a pipe consumer sees, that print belongs to Out. Progress indicators never qualify — their entire job is ephemeral status.

Tradeoffs: none worth noting. This is a corollary of byob-iostreams.3 more than a new decision.

Design

// Factory method always wires to ErrOut:
func (f *Factory) Progress(ctx context.Context, label string) progress.Progress {
    if f.IOStreams.IsStderrTTY() {
        return progress.NewSpinner(ctx, f.IOStreams.ErrOut, label) // NOT Out
    }
    return progress.NewLogging(ctx, f.IOStreams.ErrOut, label)
}

// Consumer code stays pipe-safe:
p := f.Progress(ctx, "fetching")
p.Start()
items := fetch()
p.Stop()
for _, item := range items {
    fmt.Fprintln(f.IOStreams.Out, item.Name) // data to Out
}