Terminal Output
Format human-facing CLI output with progress bars, spinners, and color while keeping stdout pipeable.
Search across all documentation pages
Format human-facing CLI output with progress bars, spinners, and color while keeping stdout pipeable.
use indicatif::{ProgressBar, ProgressStyle};
use owo_colors::OwoColorize;
fn run(items: &[String]) -> anyhow::Result<()> {
let bar = ProgressBar::new(items.len() as u64);
bar.set_style(ProgressStyle::with_template(
"{spinner:.green} [{bar:30}] {pos}/{len} {msg}",
)?);
for item in items {
bar.set_message(item.clone());
// work...
bar.inc(1);
}
bar.finish_with_message("done".green().to_string());
Ok(())
}When to reach for this: Long-running batch operations where users need feedback without polluting machine-readable stdout.
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use std::time::Duration;
fn main() -> anyhow::Result<()> {
let multi = MultiProgress::new();
let overall = multi.add(ProgressBar::new(3));
overall.set_style(ProgressStyle::with_template(
"[{bar:40.cyan/blue}] {pos}/{len} batches",
)?);
for batch in 0..3 {
let sub = multi.add(ProgressBar::new(50));
sub.set_style(ProgressStyle::default_bar());
for _ in 0..50 {
sub.inc(1);
std::thread::sleep(Duration::from_millis(10));
}
sub.finish_and_clear();
overall.inc(1);
}
overall.finish_with_message("all batches complete");
Ok(())
}What this demonstrates:
MultiProgress renders nested bars without garbled outputfinish_and_clear removes completed bars cleanly| Stream | Use for |
|---|---|
| stdout | Data users pipe to other commands |
| stderr | Logs, progress, warnings, errors |
| Crate | Notes |
|---|---|
owo-colors | Lightweight, respects NO_COLOR |
colored | Simple API, widely used |
console | Used by indicatif internally |
use std::io::IsTerminal;
let use_color = std::io::stderr().is_terminal();Disable color and progress animations when output is not a terminal.
mytool | jq. Fix: Always attach progress to stderr.NO_COLOR - Breaks CI and user preference. Fix: Check env var before styling.--plain mode.indicatif-log-bridge or suspend bars while logging.finish() in a guard or Drop impl.| Alternative | Use When | Don't Use When |
|---|---|---|
Plain eprintln! | Scripts and CI-only tools | Interactive long jobs |
tracing subscriber | Server apps with structured logs | Simple one-shot CLIs |
ratatui full TUI | Interactive dashboards | Batch scripts in pipelines |
indicatif handles progress rendering. console provides lower-level terminal control. They complement each other.
ProgressStyle templates support {eta} when total length is known.
Skip progress bar creation when --quiet is set or stderr is not a TTY.
Modern Windows Terminal works well. Test on older cmd.exe if you support it.
When --json is active, suppress all styling and progress. Emit only JSON on stdout.
Use comfy-table or tabled for aligned column output on stdout.
Use ProgressBar::new_spinner() instead of a sized bar.
MultiProgress coordinates drawing. One parent bar plus per-task children is the usual pattern.
Map -v / -vv to tracing levels. Keep progress separate from log lines.
Factor rendering behind functions and assert on strings in unit tests. Full TTY tests are rarely worth it.
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.
Reviewed by Chris St. John·Last updated Jul 16, 2026