Skip to content

Show progress without corrupting CI logs

Wrap the slow part in a spinner

use rtb_tui::Spinner;

let mut spinner = Spinner::new("resolving dependencies…");
let manifest = resolve().await?;

spinner.set_message("downloading…");
let archive = download(&manifest).await?;

spinner.finish();
println!("installed {} packages", manifest.len());

Spinner::new draws the first frame immediately, so the user sees the message as soon as the work starts rather than after the first stage finishes.

Do nothing special for CI

There is no --quiet to thread through and no is_ci() check to write. Spinner::new asks whether stderr is a terminal, and when it is not — a CI job log, a redirect to a file, a tool speaking a protocol over stdio — every method becomes a no-op that writes nothing at all.

That check happens once, in the constructor. Constructing the spinner before you decide whether the operation is long enough to need one is still cheap.

Give the spinner something to say

Nothing advances the spinner on a timer. The glyph is a single and it is redrawn only when you call set_message, so a spinner that goes thirty seconds between messages shows a frozen line for thirty seconds.

The fix is more stages, not a faster animation:

spinner.set_message(format!("downloading {name} ({i} of {total})…"));

If you genuinely need a moving indicator during one indivisible await, this crate will not give you one — see Why the spinner does not animate.

Clear the line before printing anything else

finish clears the spinner's line and leaves the cursor at column 0. Print your result after that, or the spinner text and your output share a line.

Dropping the spinner does the same cleanup, so an early return or a ? in the middle of the work still leaves a clean terminal — the Drop impl runs on the way out.

What finish does not do is print a completion message. If you want "done", print it yourself.

Keep the spinner off stdout

The spinner writes to stderr only. That is what lets a caller pipe your stdout into jq while a human still sees progress, and it is why the spinner and a --output json flag can be on at the same time without corrupting the JSON.

Do not mirror progress to stdout to make it more visible. That is the one change that breaks every consumer of the command.

Next