pulsebar
Elegant progress bars, spinners, and task tracking for Rust terminal applications.
Pulsebar gives your CLI tools polished progress reporting with minimal code. It focuses on beautiful defaults, stable rendering, and an API that's hard to misuse.
Why pulsebar?
- Beautiful defaults -- looks great without any configuration
- Minimal API -- a handful of types and methods cover all common use cases
- Stable rendering -- no flickering, no corrupted output, even with concurrent tasks
- Native terminal width -- auto-detects terminal size via ioctl (Unix) and Console API (Windows)
- Color themes -- built-in color themes (Default, Ocean, Sunset) with clean ANSI output
- Rate and ETA -- automatic throughput measurement and time estimation
- Custom formats -- template strings for full control over bar layout
- Async ready -- works with tokio and other async runtimes (optional
tokiofeature) - Thread-safe -- progress handles are
Clone + Send + Sync
Installation
Add to your Cargo.toml:
[]
= "0.2"
For async/tokio support:
[]
= { = "0.2", = ["tokio"] }
Examples
Basic Progress Bar
A simple progress bar using advance() and finish_success().
use ProgressBar;
let bar = new.with_message;
for _ in 0..100
bar.finish_success;

Download Simulation
Track byte-level progress with set_position() for a simulated file download.
use ProgressBar;
let total_bytes: u64 = 48_000_000;
let bar = new.with_message;
let mut downloaded: u64 = 0;
while downloaded < total_bytes
bar.finish_success;

Spinner
An animated spinner with dynamic message updates for indeterminate tasks.
use Spinner;
let spinner = new.with_message;
// ... do work ...
spinner.set_message;
// ... do more work ...
spinner.finish_success;

Multiple Concurrent Tasks
Run bars and spinners together with MultiProgress. Each task runs in its own thread.
use MultiProgress;
use thread;
let multi = new;
let compile = multi.add_bar.with_message;
let lint = multi.add_bar.with_message;
let test = multi.add_bar.with_message;
let docs = multi.add_spinner.with_message;
// Spawn threads to drive each task...
// ...then wait for all:
multi.join;

Custom Format Strings
Full control over bar layout with template placeholders.
use ;
let bar = new
.with_message
.with_theme
.with_format;
Supported placeholders:
| Placeholder | Description |
|---|---|
{msg} |
The message |
{bar} |
Progress bar (auto-sized to fill remaining space) |
{bar:N} |
Progress bar with fixed width N |
{pos} |
Current position |
{total} |
Total |
{pct} |
Percentage (e.g., 42%) |
{elapsed} |
Elapsed time |
{rate} |
Throughput (items/sec, auto-formatted) |
{eta} |
Estimated time remaining |

Color Themes
Built-in themes for different aesthetics. Applies to bars and spinners.
use ;
let bar = new.with_theme; // green + cyan
let bar = new.with_theme; // blue + cyan
let bar = new.with_theme; // orange + yellow
let bar = new.with_theme; // no colors
let spinner = new
.with_message
.with_theme;

Visual Styles
Three built-in styles for bars and spinners: Default (Unicode), Compact, and ASCII.
use ;
let bar = new.with_style; // Unicode blocks
let bar = new.with_style; // Shorter bar
let bar = new.with_style; // ASCII-safe
let spinner = new.with_style;

Error Handling and Finish States
Demonstrate finish_error(), plain finish(), double-finish safety, and drop-based cleanup.
use ;
// Task that fails partway
let bar = new.with_message;
bar.advance;
bar.finish_error;
// Plain finish (no message)
let bar = new.with_message;
bar.finish;
// Spinner with error
let spinner = new.with_message;
spinner.finish_error;
// Double finish is safe — second call is ignored
let bar = new.with_message;
bar.finish_success;
bar.finish_error;
// Drop cleanup — spinner stops automatically when scope ends

Querying State and Dynamic Messages
Use position(), total(), is_finished(), rate(), eta() at runtime.
Update messages mid-progress with set_message().
use ProgressBar;
let bar = new.with_message;
println!;
bar.advance;
println!;
// Update message mid-progress
bar.set_message;
bar.advance;
bar.finish_success;
println!;

Shared Progress Bar Across Threads
Clone a ProgressBar and share it across multiple worker threads.
use ProgressBar;
use thread;
let bar = new.with_message;
let handles: = .map.collect;
for h in handles
bar.finish_success;

Async Usage (Tokio)
With the tokio feature enabled, pulsebar works naturally with async code.
use ;
async

Rate and ETA
Pulsebar automatically tracks throughput using an exponential moving average. The rate is displayed in the default bar format and available programmatically:
use ProgressBar;
let bar = new;
// ... after some progress ...
let items_per_sec = bar.rate;
let seconds_remaining = bar.eta; // Option<f64>
Rate formatting adapts to magnitude: 42, 1.5K, 2.5M.
API Overview
ProgressBar
| Method | Description |
|---|---|
ProgressBar::new(total) |
Create a bar with the given total |
.with_message(msg) |
Set initial message (builder) |
.with_style(style) |
Set visual style (builder) |
.with_theme(theme) |
Set color theme (builder) |
.with_format(fmt) |
Set custom format string (builder) |
.advance(n) |
Increment position by n |
.set_position(n) |
Set absolute position |
.set_message(msg) |
Update the message |
.position() |
Get current position |
.total() |
Get total |
.rate() |
Get throughput (items/sec) |
.eta() |
Get estimated time remaining |
.finish() |
Mark as complete |
.finish_success(msg) |
Mark as successful with message |
.finish_error(msg) |
Mark as failed with message |
Spinner
| Method | Description |
|---|---|
Spinner::new() |
Create and start a spinner |
.with_message(msg) |
Set initial message (builder) |
.with_style(style) |
Set visual style (builder) |
.with_theme(theme) |
Set color theme (builder) |
.set_message(msg) |
Update the message |
.tick() |
Manually advance one frame |
.finish() |
Stop the spinner |
.finish_success(msg) |
Stop with success indicator |
.finish_error(msg) |
Stop with error indicator |
MultiProgress
| Method | Description |
|---|---|
MultiProgress::new() |
Create a multi-progress container |
.add_bar(total) |
Add a progress bar |
.add_spinner() |
Add a spinner |
.join() |
Wait for all tasks to complete |
.is_finished() |
Check if all tasks are done |
Design Philosophy
- Defaults should be excellent. You should never need to configure anything to get good-looking output.
- The API should be obvious. Method names are predictable. Misuse is difficult.
- Progress reporting must never crash your program. All I/O errors are silently handled. Setting invalid positions is clamped. Double-finishing is a no-op.
- Rendering must be stable. No flickering, no garbled output, even under rapid concurrent updates.
Non-TTY Behavior
When stderr is not a terminal (e.g., piped to a file), pulsebar automatically falls back to simple line-based output without ANSI escape codes or cursor manipulation.
Contributing
Contributions are welcome. Please:
- Open an issue to discuss significant changes before submitting a PR
- Ensure
cargo test,cargo clippy -- -D warnings, andcargo fmt --checkall pass - Add tests for new functionality
License
MIT