pulsebar 0.2.1

Elegant progress reporting for Rust CLIs
Documentation

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 tokio feature)
  • Thread-safe -- progress handles are Clone + Send + Sync

Installation

Add to your Cargo.toml:

[dependencies]
pulsebar = "0.2"

For async/tokio support:

[dependencies]
pulsebar = { version = "0.2", features = ["tokio"] }

Examples

Basic Progress Bar

A simple progress bar using advance() and finish_success().

use pulsebar::ProgressBar;

let bar = ProgressBar::new(100).with_message("Processing");
for _ in 0..100 {
    bar.advance(1);
    std::thread::sleep(std::time::Duration::from_millis(30));
}
bar.finish_success("Processing complete");

Basic Progress

Download Simulation

Track byte-level progress with set_position() for a simulated file download.

use pulsebar::ProgressBar;

let total_bytes: u64 = 48_000_000;
let bar = ProgressBar::new(total_bytes).with_message("Downloading archive.tar.gz");

let mut downloaded: u64 = 0;
while downloaded < total_bytes {
    downloaded += 256_000;
    bar.set_position(downloaded);
}
bar.finish_success("Download complete");

Download Simulation

Spinner

An animated spinner with dynamic message updates for indeterminate tasks.

use pulsebar::Spinner;

let spinner = Spinner::new().with_message("Connecting to server");
// ... do work ...
spinner.set_message("Authenticating");
// ... do more work ...
spinner.finish_success("Connected");

Spinner

Multiple Concurrent Tasks

Run bars and spinners together with MultiProgress. Each task runs in its own thread.

use pulsebar::MultiProgress;
use std::thread;

let multi = MultiProgress::new();
let compile = multi.add_bar(120).with_message("Compile");
let lint = multi.add_bar(80).with_message("Lint");
let test = multi.add_bar(200).with_message("Test");
let docs = multi.add_spinner().with_message("Generate docs");

// Spawn threads to drive each task...
// ...then wait for all:
multi.join();

Multi-Task Pipeline

Custom Format Strings

Full control over bar layout with template placeholders.

use pulsebar::{ProgressBar, Theme};

let bar = ProgressBar::new(500)
    .with_message("Syncing")
    .with_theme(Theme::Default)
    .with_format("{msg} [{bar:30}] {pos}/{total} {rate}/s ETA {eta} ({elapsed})");

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

Custom Format

Color Themes

Built-in themes for different aesthetics. Applies to bars and spinners.

use pulsebar::{ProgressBar, Spinner, Theme};

let bar = ProgressBar::new(100).with_theme(Theme::Default); // green + cyan
let bar = ProgressBar::new(100).with_theme(Theme::Ocean);   // blue + cyan
let bar = ProgressBar::new(100).with_theme(Theme::Sunset);  // orange + yellow
let bar = ProgressBar::new(100).with_theme(Theme::None);    // no colors

let spinner = Spinner::new()
    .with_message("Themed spinner")
    .with_theme(Theme::Default);

Color Themes

Visual Styles

Three built-in styles for bars and spinners: Default (Unicode), Compact, and ASCII.

use pulsebar::{ProgressBar, Spinner, Style};

let bar = ProgressBar::new(80).with_style(Style::Default); // Unicode blocks
let bar = ProgressBar::new(80).with_style(Style::Compact); // Shorter bar
let bar = ProgressBar::new(80).with_style(Style::Ascii);   // ASCII-safe

let spinner = Spinner::new().with_style(Style::Ascii);

Styles

Error Handling and Finish States

Demonstrate finish_error(), plain finish(), double-finish safety, and drop-based cleanup.

use pulsebar::{ProgressBar, Spinner};

// Task that fails partway
let bar = ProgressBar::new(100).with_message("Uploading");
bar.advance(43);
bar.finish_error("Connection lost at 43%");

// Plain finish (no message)
let bar = ProgressBar::new(50).with_message("Quick task");
bar.finish();

// Spinner with error
let spinner = Spinner::new().with_message("Connecting to database");
spinner.finish_error("Connection refused");

// Double finish is safe — second call is ignored
let bar = ProgressBar::new(20).with_message("Double finish");
bar.finish_success("First finish wins");
bar.finish_error("This is ignored");

// Drop cleanup — spinner stops automatically when scope ends
{
    let _spinner = Spinner::new().with_message("Auto-cleanup on drop");
    // _spinner dropped here, animation stops cleanly
}

Error Handling

Querying State and Dynamic Messages

Use position(), total(), is_finished(), rate(), eta() at runtime. Update messages mid-progress with set_message().

use pulsebar::ProgressBar;

let bar = ProgressBar::new(200).with_message("Processing items");
println!("Total: {}", bar.total());

bar.advance(50);
println!("Position: {}, Rate: {:.1}/s, ETA: {:.1}s",
    bar.position(), bar.rate(), bar.eta().unwrap_or(0.0));

// Update message mid-progress
bar.set_message("Phase 2: Analyzing");
bar.advance(50);

bar.finish_success("All items processed");
println!("Is finished: {}", bar.is_finished());

Query State

Shared Progress Bar Across Threads

Clone a ProgressBar and share it across multiple worker threads.

use pulsebar::ProgressBar;
use std::thread;

let bar = ProgressBar::new(200).with_message("Parallel work");

let handles: Vec<_> = (0..4).map(|_| {
    let bar = bar.clone();
    thread::spawn(move || {
        for _ in 0..50 {
            bar.advance(1);
        }
    })
}).collect();

for h in handles { h.join().unwrap(); }
bar.finish_success("All workers done");

Shared Across Threads

Async Usage (Tokio)

With the tokio feature enabled, pulsebar works naturally with async code.

use pulsebar::{MultiProgress, Theme};

#[tokio::main]
async fn main() {
    let multi = MultiProgress::new();
    let download = multi.add_bar(1000).with_message("Download").with_theme(Theme::Default);
    let process = multi.add_bar(500).with_message("Process").with_theme(Theme::Default);

    let d = download.clone();
    tokio::spawn(async move {
        for _ in 0..1000 {
            d.advance(1);
            tokio::time::sleep(std::time::Duration::from_millis(3)).await;
        }
        d.finish_success("Downloaded");
    });

    // ... similar for process ...
    multi.join();
}

Async Usage

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 pulsebar::ProgressBar;

let bar = ProgressBar::new(1000);
// ... 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

  1. Defaults should be excellent. You should never need to configure anything to get good-looking output.
  2. The API should be obvious. Method names are predictable. Misuse is difficult.
  3. 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.
  4. 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:

  1. Open an issue to discuss significant changes before submitting a PR
  2. Ensure cargo test, cargo clippy -- -D warnings, and cargo fmt --check all pass
  3. Add tests for new functionality

License

MIT