Skip to content

Progress and live displays

These types show work in progress, redrawing in place instead of scrolling:

Type Use it for
track a progress bar over an iterator, in one line
Progress several tasks with bars, percentages, speeds and times
Live redrawing any renderable in place
Status and Spinner "working on it" with an animation
ProgressBar a bare bar to embed in your own layout

The screenshots on this page are single frames rendered with a fixed clock; the code runs the real animation in a terminal.

The examples use these imports:

use rich::progress::TaskUpdate;
use rich::{
    BarColumn, ColumnOptions, Console, Justify, Live, Progress, ProgressBar, ProgressColumn,
    Spinner, SpinnerColumn, Status, Text, TextColumn, TimeRemainingColumn,
};

The one-liner: track

rich::track wraps an iterator and draws a live bar on stdout while you consume it. The total comes from the iterator's exact size, if it has one.

fn track_iterator() {
    // The one-liner: a live bar on stdout for any iterator.
    let mut total = 0;
    for n in rich::track(0..40, "Summing") {
        std::thread::sleep(Duration::from_millis(10));
        total += n;
    }
    assert_eq!(total, 780);

    // The same through a running Progress, sharing its display with other tasks.
    let live = Progress::new().start(Console::new(), std::io::stdout(), 10.0);
    for _item in live.track(vec!["a", "b", "c"], None, "Items") {
        std::thread::sleep(Duration::from_millis(50));
    }
    live.stop();
}

The display stops when the iterator is exhausted or dropped. The second half shows LiveProgress::track, which adds a task to a display you already have running.

Progress

A Progress holds a list of tasks and a list of columns. It is a renderable: printing it draws one frame. To animate it, start it as a live display.

Running it live

fn live_progress() {
    let progress = Progress::new().transient(false).expand(false);
    // `start` moves the progress onto a refresh thread: 10 redraws a second.
    let live = progress.start(Console::new(), std::io::stdout(), 10.0);

    let task = live.add_task("Working", 50.0, 0.0);
    for _ in 0..50 {
        std::thread::sleep(Duration::from_millis(20));
        live.advance(task, 1.0);
    }
    // Draw the final frame, join the thread, get the Progress back.
    let (progress, _stdout) = live.stop();
    assert!(progress.finished());
}
  • start(console, writer, refresh_per_second) moves the progress to a background thread that redraws it on a timer, and returns a LiveProgress handle. writer is usually std::io::stdout(); any Write + Send works.
  • Through the handle: add_task, advance, update, reset, refresh, track, wrap_read (count bytes as they are read), open (a file, sized from its metadata) and with(|progress| …) for anything else.
  • stop() draws the final frame, joins the thread and gives back the Progress and the writer. Dropping the handle stops it too.
  • .transient(true) erases the display when it stops; .disable(true) shows nothing while tasks still update.

Tasks

fn default_columns(console: &Console) {
    let (now, clock) = fixed_clock();
    // Description, bar, percentage and time remaining — upstream's defaults.
    let mut progress = Progress::new().clock(clock);

    let download = progress.add_task("Downloading", 100.0, 0.0);
    let extract = progress.add_task("Extracting", 100.0, 0.0);
    let verify = progress.add_task("Verifying", None, 0.0); // indeterminate

    *now.lock().unwrap() = 5.0;
    progress.advance(download, 30.0);
    *now.lock().unwrap() = 10.0;
    progress.advance(download, 34.0);
    progress.update(extract, TaskUpdate::default().completed(100.0));
    progress.advance(verify, 1.0);

    console.print(&progress);
}

Default columns: description, bar, percentage and time remaining; one task complete and one indeterminate

Method Does
add_task(description, total, completed) a started task; returns its TaskId. total is f64 or None (indeterminate: the bar pulses)
add_unstarted_task(…) a task whose clock has not started (start_task starts it)
add_task_with(…, start, fields) with custom fields for format strings
advance(id, amount) add to completed and record a speed sample
update(id, TaskUpdate) change total, completed, advance, description, visible, fields
reset(id, start, total, completed) start over
start_task, stop_task, remove_task lifecycle
task(id), tasks(), finished() read back: each Task has percentage(), speed(), elapsed(), time_remaining()…

TaskUpdate::default().completed(100.0).description("done") builds an update; .refresh(true) makes a running display redraw immediately.

Columns

Progress::new() uses upstream's default columns. Replace them with .columns(vec![…]):

fn all_columns(console: &Console) {
    let (now, clock) = fixed_clock();
    let mut progress = Progress::new().clock(clock).columns(vec![
        ProgressColumn::spinner(),
        ProgressColumn::Description,
        ProgressColumn::Bar,
        ProgressColumn::Download,
        ProgressColumn::TransferSpeed,
        ProgressColumn::TimeElapsed,
        ProgressColumn::time_remaining(),
    ]);
    let iso = progress.add_task("ubuntu.iso", 4_700_000_000.0, 0.0);
    let deb = progress.add_task("rich.deb", 18_000_000.0, 0.0);

    *now.lock().unwrap() = 4.0;
    progress.advance(iso, 400_000_000.0);
    progress.advance(deb, 18_000_000.0);
    *now.lock().unwrap() = 8.0;
    progress.advance(iso, 500_000_000.0);

    console.print(&progress);
}

Spinner, description, bar, download size, transfer speed, elapsed and remaining columns

ProgressColumn Shows
Description the task description (markup)
Bar, BarWith(BarColumn) the bar; BarColumn sets width and styles
Percentage 64%
MofN 9/40
TaskProgress { show_speed } percentage, or speed for indeterminate tasks
Download, BinaryDownload 0.9/4.7 GB (decimal or binary units)
TransferSpeed 125.0 MB/s
FileSize, TotalFileSize completed or total as a size
TimeElapsed 0:00:08
TimeRemaining(..), time_remaining() estimated time left; TimeRemainingColumn::new(compact, elapsed_when_finished)
Spinner(..), spinner() a spinner, replaced by finished_text when the task completes
Text(String, Style) fixed text
TextFormat(TextColumn) a format string over the task
Renderable(Arc<…>) any renderable, the same on every row
WithTableColumn(..) / .with_table_column(ColumnOptions) set the grid column's width, ratio or justify

Format strings and custom fields

TextColumn expands a Python-style format string against the task, as upstream does: {task.description}, {task.completed}, {task.percentage:>3.0f}, {task.fields[name]}. The result is markup unless you call .markup(false).

fn custom_columns(console: &Console) {
    let (now, clock) = fixed_clock();
    let mut progress = Progress::new().clock(clock).columns(vec![
        // Format strings see the task, like upstream's "{task.description}".
        ProgressColumn::TextFormat(TextColumn::new("[bold blue]{task.fields[stage]}")),
        ProgressColumn::Description,
        ProgressColumn::BarWith(
            BarColumn::new()
                .bar_width(Some(20))
                .complete_style("magenta")
                .finished_style("bold green"),
        ),
        ProgressColumn::MofN,
        ProgressColumn::TextFormat(
            TextColumn::new("{task.percentage:>3.0f}%").justify(Justify::Right),
        ),
        ProgressColumn::TimeRemaining(TimeRemainingColumn::new(true, true)),
        ProgressColumn::Spinner(SpinnerColumn::new("line", "✓")).with_table_column(ColumnOptions {
            width: Some(2),
            ..ColumnOptions::default()
        }),
    ]);

    let fields = |stage: &str| [("stage", stage.to_string())];
    let build = progress.add_task_with("compile", 120.0, 0.0, true, fields("1/2"));
    let test = progress.add_task_with("test", 40.0, 0.0, true, fields("2/2"));

    *now.lock().unwrap() = 30.0;
    progress.advance(build, 120.0);
    progress.update(test, TaskUpdate::default().advance(9.0));

    console.print(&progress);
}

Custom columns: a stage field, a styled bar, M of N, a formatted percentage, compact time and a line spinner

Deterministic time

Time-based columns (speed, elapsed, remaining, spinners) read the progress clock. Replace it with .clock(f) — a function returning seconds — to get the same frame every run, which is how the screenshots here are made and how you should test progress output:

/// A clock you control. Time-based columns read it, so frames are reproducible.
fn fixed_clock() -> (Arc<Mutex<f64>>, impl Fn() -> f64 + Send + Sync + 'static) {
    let now = Arc::new(Mutex::new(0.0_f64));
    let reader = now.clone();
    (now, move || *reader.lock().unwrap())
}

Live

Live redraws a renderable in place: each update moves the cursor back over the previous frame and draws the new one. It has two modes.

Manual: you call update (or refresh) and each call redraws.

fn live_display() {
    // Live redraws a renderable in place. Manual mode: you call update/refresh.
    let spinner = Spinner::new("dots").text("counting…");
    let mut live = Live::new(Box::new(Text::new("")), Console::new(), std::io::stdout());
    live.start();
    let started = Instant::now();
    for n in 0..20 {
        let frame = spinner.render(started.elapsed().as_secs_f64());
        live.update(Box::new(frame.append_text(&Text::new(format!(" {n}")))));
        std::thread::sleep(Duration::from_millis(50));
    }
    live.stop(); // leaves the last frame on screen
}

Auto-refresh: Live::spawn runs the display on a background thread that redraws on a timer; update sends it a new renderable. The renderable, the console and the writer move to the thread, so they must be Send.

fn auto_live() {
    // Auto-refresh mode: a background thread redraws; you send new renderables.
    let live = Live::spawn(
        Box::new(Text::new("starting")),
        Console::new(),
        std::io::stdout(),
        20.0,
    );
    let status = Status::new("Downloading…");
    let started = Instant::now();
    while started.elapsed() < Duration::from_millis(800) {
        // Spinners animate by rendering at the current time.
        let frame = status.renderable().render(started.elapsed().as_secs_f64());
        live.update(Box::new(frame));
        std::thread::sleep(Duration::from_millis(50));
    }
    live.update(Box::new(Text::styled("✓ downloaded", "green")));
    let _stdout = live.stop();
}
  • Live::new(renderable, console, writer); .transient(true) erases the display on stop.
  • start() hides the cursor and draws the first frame; stop() draws the last frame and restores the cursor.
  • For an auto-refreshing display, Live::spawn_with(…, transient) and AutoLive's update, refresh, refresh_wait and stop.

One live display at a time

A live display owns the cursor. Anything else printed to the same terminal while it runs — including from another thread — lands in the middle of its redraws.

When stdout is not a terminal

Live draws nothing while running, and stop writes the final frame once, without a trailing newline (upstream's behaviour for files). Print a newline after stop if more output follows. LiveProgress::stop does this for you.

Status and spinners

A Spinner is an animation of frames; render(seconds) returns the frame for that moment as a Text. The first call fixes the start of the animation, so keep one spinner for the whole animation.

fn spinners(console: &Console) {
    for name in [
        "dots",
        "line",
        "arc",
        "bouncingBar",
        "moon",
        "earth",
        "clock",
    ] {
        let spinner = Spinner::new(name).style("green");
        spinner.render(0.0); // the first render starts the animation clock
                             // Six frames, 0.1 s apart, then the name.
        let mut row = Text::new("");
        for step in 0..6 {
            row = row.append_text(&spinner.render(step as f64 * 0.1));
            row.append("  ", None);
        }
        row.append(name, Some("dim".into()));
        console.print(&row);
    }
}

Six frames each of the dots, line, arc, bouncingBar, moon, earth and clock spinners

Spinner::new(name) accepts every upstream spinner name (dots, dots2 … dots12, line, arc, bouncingBar, bouncingBall, moon, earth, clock, material, aesthetic and more); an unknown name falls back to dots. .text(markup) adds text after the frame, .style(s) styles the frame, .speed(x) scales the rate.

A Status is a spinner plus a message, styled status.spinner:

fn status(console: &Console) {
    let mut status = Status::new("Fetching [bold]index[/]…").spinner("dots");
    console.print(&status.renderable().render(0.0));

    // Change the message, the spinner, its style or speed.
    status.update(
        Some("Unpacking…"),
        Some("line"),
        Some("yellow".into()),
        None,
    );
    console.print(&status.renderable().render(0.0));
}

A status line, then the same status updated with a new message and spinner

A status in a Live animates on its own. The auto-refresh example above renders status.renderable().render(elapsed) instead, to choose each frame.

Spinners read the console clock

As upstream's does, a Spinner or Status renders the frame for console.get_time(), so one placed directly in a Live animates on every refresh. For reproducible output, such as tests or screenshots, pin the clock with ConsoleBuilder::get_time, or render a given moment with render(elapsed_seconds) as the examples on this page do.

ProgressBar

ProgressBar is the bar on its own — for a table cell, a panel, or your own renderable.

fn bars(console: &Console) {
    // The bar on its own, as a renderable.
    console.print(&ProgressBar::new(100.0, 25.0).width(40));
    console.print(
        &ProgressBar::new(100.0, 70.0)
            .width(40)
            .complete_style("yellow"),
    );
    console.print(&ProgressBar::new(100.0, 100.0).width(40));
    // No total: a pulsing bar. `animation_time` picks the frame.
    console.print(&ProgressBar::indeterminate().width(40).animation_time(0.4));
}

Bars at 25%, 70% in yellow, complete, and pulsing

ProgressBar::new(total, completed), .width(n) (default: the full width), .style, .complete_style, .finished_style, .pulse_style, .pulse(true) and ProgressBar::indeterminate(); .animation_time(t) picks the pulse frame.

Not yet ported

  • Console.status() / with console.status(...): create a Status and drive it with Live as shown above.
  • Live: screen=True (alternate screen), redirect_stdout/redirect_stderr, vertical_overflow, and get_renderable callbacks.
  • Progress: console.print through the display (progress.console), and redirect_stdout.

See also