Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
203: Progress Bars with indicatif
I've spent way too many hours staring at a blinking cursor in a terminal, wondering if my program had actually crashed or if it was just working through a massive dataset. We've all been there. When you're building a CLI tool in Rust, you can't just leave the user guessing. You need a progress bar. The industry standard for this is the indicatif crate.
Let's try to build a quick simulation of a batch file processor—something that "processes" 100 files. I'll start with the simplest implementation possible.
The basic flick of a switch
First, I'll add indicatif = "0.17" to my Cargo.toml. Now, I'll just create a bar and increment it in a loop. Here is my first attempt:
use indicatif::ProgressBar;
use std::thread;
use std::time::Duration;
fn main() {
let pb = ProgressBar::new(100);
for _ in 0..100 {
pb.inc(1);
thread::sleep(Duration::from_millis(20));
}
pb.finish();
}
If you run this, you'll see a bar. It works, but it's... boring. It's just a bunch of hashes. It doesn't tell me the percentage, how many items are left, or how much longer I have to wait. It's barely better than a loading spinner. I want something that feels like a professional tool.
Wait, where did my logs go?
Usually, when I'm processing files, I want to log which file is currently being handled. I'll try to add a println! inside that loop to see what's happening:
for i in 0..100 {
println!("Processing file_{}.txt", i);
pb.inc(1);
thread::sleep(Duration::from_millis(20));
}
And here is the problem: the progress bar is getting pushed up the screen every time I print. The terminal is just appending the bar over and over again, creating a vertical trail of garbage. This is because println! writes to stdout without knowing that indicatif is trying to manage a specific line on the screen.
To fix this, I have to stop using println! and use the progress bar's own printing method. indicatif handles the "clearing" of the bar, printing the message, and then redrawing the bar underneath it.
for i in 0..100 {
pb.println(format!("Processing file_{}.txt", i));
pb.inc(1);
thread::sleep(Duration::from_millis(20));
}
Now the output stays clean. The bar stays pinned to the bottom, and the logs scroll past it. Much better.
Making it look like a real tool
I'm still not happy with the look of the bar. I want a percentage, a timer, and maybe some custom characters. This is where ProgressStyle comes in. It uses a template system that looks a bit like a formatting string.
I'll try to define a style and apply it to my bar:
use indicatif::{ProgressBar, ProgressStyle};
let pb = ProgressBar::new(100);
pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({eta})")
.unwrap()
.progress_chars("#>-"));
I had to call .unwrap() because the template string is parsed at runtime; if I typo a placeholder like {elapsed_precis}, it would return an error. The {wide_bar} is the star here—it expands to fill the available terminal width. The progress_chars call lets me change the actual look of the bar (the filled part, the current head, and the empty part).
Tracking multiple things at once
What if I'm downloading three files simultaneously? A single bar won't cut it. I need a MultiProgress container. I'll set up a few bars and run them in separate threads to simulate concurrency.
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use std::thread;
use std::time::Duration;
fn main() {
let mp = MultiProgress::new();
let style = ProgressStyle::with_template("{prefix}: {bar:40.cyan/blue} {pos}/{len}")
.unwrap();
let files = vec!["dataset_1.bin", "dataset_2.bin", "dataset_3.bin"];
let mut handles = vec![];
for file in files {
let pb = mp.add(ProgressBar::new(100));
pb.set_style(style.clone());
pb.set_prefix(file);
let handle = thread::spawn(move || {
for _ in 0..100 {
pb.inc(1);
thread::sleep(Duration::from_millis(30));
}
pb.finish_with_message("done");
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}
Notice that I used mp.add(). This registers the individual progress bars with the manager so they don't fight for the same line of the terminal. Now we have a stacked set of bars, each tracking its own progress independently.
📋 Practical Task
Exercise: Concurrent Database Migration Simulator
Build a CLI tool that simulates migrating five different database tables. Each table should have a different number of "records" to migrate (e.g., 100, 500, 1000, 200, 800).
- Use
MultiProgressto show a separate bar for each table. - Each bar must use a custom
ProgressStylethat includes:- The table name as a prefix.
- A percentage indicator.
- An ETA (Estimated Time of Arrival) timer.
- Simulate the work using
thread::spawnandthread::sleep. - When a table finishes, use
finish_with_messageto mark it as "Complete".
There are no comments for now.