Skip to content
rusthacks.dev
CLI

Progress Bars and Spinners in Rust With Indicatif Explained

Show progress for long operations with indicatif: bars with known length, spinners for unknown work, wrapping iterators and keeping log lines from garbling the bar.

2 min read
Summarize in AI
Flat illustration for the post “Progress Bars and Spinners in Rust With Indicatif Explained”, composed around iterators.

A tool that goes silent for thirty seconds looks hung. A progress bar answers the question "is it working" and "how long" without a log line per item. indicatif is the standard crate for it, and most of the work is picking a template and remembering where output goes. This post covers the three cases: known total, unknown total and parallel work. It is part of the command line tool series.

indicatif 0.17, Rust 1.90.

A bar with a known total

Cargo.tomlTOML
[dependencies]
indicatif = "0.17"
rust
use indicatif::{ProgressBar, ProgressStyle};
use std::{thread, time::Duration};

fn main() {
    let pb = ProgressBar::new(100);
    pb.set_style(
        ProgressStyle::with_template("{spinner} [{bar:40}] {pos}/{len} ({eta}) {msg}")
            .unwrap()
            .progress_chars("=> "),
    );
    for i in 0..100 {
        pb.set_message(format!("item {i}"));
        pb.inc(1);
        thread::sleep(Duration::from_millis(20));
    }
    pb.finish_with_message("done");
}

On a terminal this draws a moving bar with position, length and an ETA computed from the recent rate. ProgressStyle::with_template returns a Result because the template is parsed at runtime; a typo is an error, and unwrap() is acceptable for a literal template you control.

Wrap an iterator

rust
use indicatif::ProgressIterator;

for path in files.iter().progress() {
    process(path);
}

.progress() reads the length from ExactSizeIterator, so there is nothing to count by hand. For streams, progress_count(n) supplies the total.

A spinner for unknown work

rust
let sp = ProgressBar::new_spinner();
sp.enable_steady_tick(Duration::from_millis(100));
sp.set_message("waiting for the server");
// ... blocking work ...
sp.finish_and_clear();

enable_steady_tick spawns a thread that redraws, so the spinner moves even while your code blocks.

Output while the bar is up

Writing with println! while a bar draws garbles both. Use pb.println("warning: skipped file"), which prints above the bar and redraws it. With the log crate, indicatif-log-bridge does the same for log records.

Parallel work

rust
let multi = indicatif::MultiProgress::new();
let bar = multi.add(ProgressBar::new(50));

Give each worker its own bar from the same MultiProgress and let them tick from their threads. Clones of a ProgressBar share state and are Send, so they cross into closures freely, including those in Rayon loops.

Pipes and CI

When stderr is not a terminal, the default draw target is hidden, so a CI log gets no escape codes and no spam. If you want periodic plain lines in CI, print them yourself when !std::io::stderr().is_terminal(). For color decisions in general, see colored output.

When a bar is wrong

Short tasks under a second do not need one. Tasks whose progress cannot be estimated mislead with a bar, so use a spinner. The crate is documented at docs.rs for indicatif.

Taggedindicatif
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.