pbr: a terminal progress bar library for Rust CLIs
Single and multi-bar console progress rendering, with byte units, custom formats and an iterator wrapper.
What it is
pbr is a console progress-bar library for Rust, published to crates.io under the name pbr. The README says the crate is inspired from pb. It targets command-line tools that want to show how much of a long-running job is done — copying files or iterating over many items.
The repository lives at a8m/pb, is MIT-licensed, and last saw a commit on 2023-08-08. The crate version in Cargo.toml is 1.1.1; the latest GitHub release tag is v1.0.3. Its runtime dependencies are libc and crossbeam-channel, plus winapi when built for Windows; it also has a dev-dependency on rand.
The public surface is small: ProgressBar, MultiBar, a Units enum, a Pipe writer used internally by the multi-bar machinery, and an iterator wrapper called PbIter. Everything else lives behind module boundaries in src/pb.rs, src/multi.rs, and src/tty/.
A single bar
The core type is ProgressBar<T: Write>, generic over its output writer. The README example constructs a bar directly:
use pbr::ProgressBar;
let count = 1000;
let mut pb = ProgressBar::new(count);
pb.format("╢▌▌░╟");
for _ in 0..count {
pb.inc();
}
pb.finish_print("done");
ProgressBar::on(handle, total) creates a bar over any Write. The bar tracks a current count against a fixed total, and normally redraws itself on each advance by writing a carriage-return-prefixed line to its writer, unless throttled by set_max_refresh_rate.
The API is deliberately imperative. You move it forward with inc(), add(n), or set(n); you can call tick() to request a redraw without advancing progress, as the tests and examples do for spinner-like output. When work finishes you must close out explicitly — finish() sets current to total and draws one last time, while finish_print(s) replaces the bar with a message.
Because ProgressBar itself implements Write, it can sit at the end of an I/O chain. The README's file-copy example adds a broadcast writer around the destination file that also writes to the progress bar, so bytes written through std::io::copy advance the counter:
let mut pb = ProgressBar::new(n_bytes);
pb.set_units(Units::Bytes);
let mut handle = File::create("copy-words").unwrap().broadcast(&mut pb);
copy(&mut file, &mut handle).unwrap();
What you can configure
A set of public boolean fields controls which boxes appear: show_bar, show_speed, show_percent, show_counter, show_time_left, show_tick, and show_message. The unit tests in src/pb.rs exercise combinations of these, asserting the exact rendered string for a given width.
The bar's characters come from format(), which takes a five-character template such as "[~> ]" — start bracket, filled segment, current head, remaining fill, end bracket. The README example uses Unicode block glyphs (╢▌▌░╟) for a denser look.
Other knobs:
set_units(Units::Bytes)switches speed and counters to human-readable byte sizes (KB/MB/GB/TB) instead of plain numbers.tick_format()sets the rotating animation characters; examples use\|/-.message(s)sets the prefix label shown before the bar.set_width(Some(n))fixes the bar width; otherwise it reads the terminal size when available and falls back to 80 columns.set_max_refresh_rate(Some(dur))throttles redraws so fast loops don't spam the terminal. The repository includes anexamples/max_refresh_rate.rsdemonstrating the setting.
Multiple bars at once
For concurrent work, MultiBar<T: Write> keeps several bars on screen and refreshes them together. You create a bar with create_bar(total), run it from its own thread, and call finish() when that thread is done so the manager knows to drop it.
The plumbing is channel-based. Each created bar writes through an internal Pipe writer rather than directly to the terminal; every write becomes a message on an unbounded crossbeam channel carrying the string plus the bar's row level. The listen() method runs a blocking loop that receives those messages, rewrites all live rows in place, moving the cursor up via move_cursor_up (an escape sequence on Unix/WASI, a console API call on Windows), and returns only when every bar has sent its done marker.
let mut mb = MultiBar::new();
mb.println("Application header:");
let mut p1 = mb.create_bar(count);
// ... run p1 on a thread ...
mb.listen(); // blocks until all bars finish
Because listen() is blocking, the README suggests running it in its own thread if you don't want to stall your main loop. The examples/multi_bg.rs example uses a similar pattern — a background thread spawns task threads that create bars and advance them, while the main thread repeatedly calls listen() until they finish.
The docs carry a caveat: any bar inside a MultiBar should use finish_print, not finish_println, because printing a raw newline would break the synchronized multi-row output. finish_println detects this case and falls back to finish_print.
Terminal handling
The src/tty/ module isolates platform differences behind two functions: terminal_size() returns the terminal dimensions, and move_cursor_up(n) repositions the cursor — returning an escape sequence on Unix and WASI, and calling a Windows console API function.
On Unix it calls ioctl(TIOCGWINSZ) on stdout's file descriptor and checks isatty first — if stdout is not a tty, it returns None. On Windows it uses the winapi console functions to read the window size. A WASI implementation returns None for size but keeps the cursor-up sequence, with comments pointing at WebAssembly/WASI issue 42 as background.
The iterator wrapper
For simple counted loops there is PbIter, which wraps any Iterator and advances a progress bar once per yielded item. It sizes the bar from the iterator's size_hint(). A test in tests/lib.rs runs it over 0..2000.
The integration tests under tests/ are mostly runnable demonstrations — they sleep through real loops so you can watch them render, including a "npm-style" spinner and a timeout example that flips between waiting and connected messages. The GitHub Actions workflow builds on Ubuntu, Windows and macOS against Rust stable and 1.58, installing the faketty tool on non-Windows stable runners before running tests.
Should you use it
For a small CLI that needs an honest progress indicator with no heavy dependencies, pbr is straightforward: three source modules, two base runtime dependencies plus winapi on Windows, and a documented API. The multi-bar path is a distinctive part of this crate's API. Its age shows in places (the edition = "2018" manifest, examples using rand::thread_rng().gen_range), so treat it as stable-but-quiet rather than actively developed.
a8m/pb: 4 MB in 21 files, indexed Nov 9, 2026 after 0.00 hours of indexing. License: MIT. Source: https://github.com/a8m/pb.
Ask your agent to name it in its tool calls (the repo argument of query_graph, recall_project and read_project_source).
Written by AI models on the operator's own hardware, checked by a second model, from the repository's README and its code. Back to the shelf