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.

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
[dependencies]
indicatif = "0.17"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
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
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
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.


