Skip to content

5. Progress and live output

Progress bars

use rich::{Console, Progress, ProgressColumn};

let mut progress = Progress::new().columns(vec![
    ProgressColumn::Description,
    ProgressColumn::Bar,
    ProgressColumn::Percentage,
]);

progress.add_task("Downloading", 100.0, 60.0);   // description, total, completed
progress.add_task("Extracting", 100.0, 30.0);

console.print(&progress);

Progress

Progress renders a snapshot — it draws the bars at the values you give it. To animate, update the values and redraw, which is what Live is for.

Progress::new() uses upstream's default columns: description, bar, percentage and time remaining.

Time, rate and spinner columns

add_task returns a TaskId; advance and update move a task along and record speed samples, exactly as upstream's Progress does. Time-based columns read the progress clock, which you can replace — handy in tests, where a fixed clock makes every frame reproducible:

use rich::{Progress, ProgressColumn, TaskUpdate};
use std::sync::{Arc, Mutex};

let now = Arc::new(Mutex::new(0.0_f64));
let clock = now.clone();
let mut progress = Progress::new()
    .clock(move || *clock.lock().unwrap())
    .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);
*now.lock().unwrap() = 4.0;
progress.advance(iso, 400_000_000.0);
*now.lock().unwrap() = 8.0;
progress.update(iso, TaskUpdate::default().advance(500_000_000.0));
console.print(&progress);   // ⠋ ubuntu.iso ━━━━╸━━━ 0.9/4.7 GB 125.0 MB/s 0:00:08 0:00:31

Columns: Description, Bar, Percentage, TaskProgress { show_speed }, MofN, Download, BinaryDownload, TimeElapsed, TimeRemaining (ProgressColumn::time_remaining() or TimeRemainingColumn::new(compact, elapsed_when_finished)), TransferSpeed, FileSize, TotalFileSize, Spinner (ProgressColumn::spinner() or SpinnerColumn::new(name, finished_text)), Text(String, Style) for a fixed cell, TextFormat for a cell formatted from the task, Renderable (upstream's RenderableColumn) for the same renderable in every row, BarWith(BarColumn) for a bar with its own width and styles, and WithTableColumn (via ProgressColumn::with_table_column) for a column with explicit table options. A task with a total of None is indeterminate and its bar pulses; add_unstarted_task adds one whose clock has not started.

To redraw on its own, progress.start(console, writer, refresh_per_second) starts a Live-driven display on a background thread (upstream's with progress:); .transient(true) clears it when stopped and .disable(true) draws nothing. For the common case, rich::track(iter, "Working") wraps an iterator with a live bar on stdout, like upstream's track(). The progress guide covers these.

Spinners

use rich::Spinner;

let spinner = Spinner::new("dots");
let frame = spinner.render(elapsed_seconds);
console.print(&frame);

Spinner

render takes a time in seconds and returns the frame for that moment. As in upstream rich, the first render marks the start of the animation, so keep one spinner for the whole animation rather than building a new one per frame (a new spinner always starts on its first frame). spinner.update(text, style, speed) changes the text, style or speed mid-animation; a new speed continues from the current frame. Spinner::text is console markup.

Live displays

Live redraws a renderable in place, moving the cursor back over its previous output rather than scrolling:

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
}

Live::new takes the renderable to show, the console and the writer; start begins drawing, update swaps in a new renderable, and stop draws the last frame. Progress and live displays covers the automatic-refresh mode and Progress on top of it.

One live display at a time

Live owns the cursor while it runs. Printing to the same console from elsewhere — including from another thread — will interleave with its redraws and corrupt the display. A second display started on a clone of the same console is nested instead, and drawn inside the first one, as in Python rich.

Status

Status is a Live with a spinner and a message, for the common "working on it" case:

use rich::Status;

let mut status = Status::new("Fetching [b]index[/]…");
let frame = status.renderable().render(elapsed_seconds);
status.update(Some("Unpacking…"), None, None, None);

Status::update follows upstream: a new spinner name replaces the spinner and restarts its animation, while a new message, style or speed updates it in place.

Next: The CLI →