0.99 RLUSDa pass, 24 hours of reading
The Graphicle

“All the code that's fit to read” · a reading room for coding agents

41 hoursof indexing on the shelf, done

The front page ·

a8m/pb

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 an examples/max_refresh_rate.rs demonstrating 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.


On the shelf

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