Loaders

A fully-featured, customisable progress bar and loading indicator library for Rust CLI applications, implemented with only the standard library.

Crates.io Docs.rs License: MIT CI

Features

  • Progress bars with known or unknown totals.
  • Spinners with built-in frame presets.
  • Multi-progress rendering for concurrent work.
  • Iterator integration with automatic length detection.
  • Human-readable ETA, elapsed time, and rate values.
  • Byte formatting and byte throughput templates.
  • Custom templates parsed once and reused.
  • Custom template keys for application-specific labels.
  • ANSI colors with NO_COLOR support.
  • Built-in themes for common terminal styles.
  • Draw throttling by position delta and redraw rate.
  • Hidden draw targets for tests and quiet mode.
  • Custom Write targets for captures and logs.
  • Thread-safe cloned progress bars.
  • Background steady ticking for spinners.
  • Reusable ProgressChars for custom progress bar shapes.
  • Status-aware templates with {status}, {remaining}, {ratio}, and compact counts.
  • Spinner helpers for success, failure, warning, and informational endings.
  • 27 built-in spinner and loading frame presets.

Install

cargo add loaders
[dependencies]
loaders = "0.0.0"

Four-Line Progress Bar

#![allow(unused)]
fn main() {
use loaders::ProgressBar;

let pb = ProgressBar::new(100);
pb.inc(50);
pb.finish_with_message("done");
}

Guides

Getting Started

loaders gives you three main building blocks: ProgressBar for measured work, Spinner for indeterminate work, and MultiProgress for several bars rendered as one terminal block.

Add the Dependency

[dependencies]
loaders = "0.0.0"

First Progress Bar

use loaders::ProgressBar;

fn main() {
    let pb = ProgressBar::new(3);
    for _ in 0..3 {
        pb.inc(1);
    }
    pb.finish_with_message("done");
}

First Spinner

use loaders::{Spinner, spinner::frames::DOTS};
use std::time::Duration;

fn main() {
    let spinner = Spinner::new_with_interval(&DOTS, Duration::from_millis(80));
    spinner.start_with_message("connecting");
    spinner.stop_with_message("connected");
}

Iterator Usage

use loaders::ProgressIterator;

fn main() {
    let sum: u64 = (0..100u64).progress().sum();
    println!("{sum}");
}

Which Type Should I Use?

Use ProgressBar when there is a count, byte total, or step count. Use Spinner when work is happening but a total is unknown. Use MultiProgress when multiple threads or phases should be visible together.

Draw Targets and Hidden Bars

Progress output defaults to stderr. DrawTarget::hidden() suppresses terminal output and is the recommended target in tests. Non-TTY streams receive newline snapshots instead of ANSI cursor movement, which keeps pipes and logs readable. CI and NO_COLOR environments avoid color escape sequences.

API Reference

ItemSignatureDescriptionGuide
ProgressBarstruct ProgressBarThread-safe progress bar handle.Progress Bars
ProgressBarBuilderstruct ProgressBarBuilderBuilder for length, style, target, template, and throttling.Progress Bars
ProgressBar::newfn new(len: u64) -> ProgressBarCreates a measured bar.Progress Bars
ProgressBar::new_spinnerfn new_spinner() -> ProgressBarCreates an indeterminate bar.Spinners
ProgressBar::hiddenfn hidden() -> ProgressBarCreates a silent bar for tests.Advanced
ProgressBar::with_draw_targetfn with_draw_target(len, target) -> ProgressBarUses a custom draw target.Advanced
ProgressBar::with_stylefn with_style(len, style) -> ProgressBarCreates a bar with a custom style.Styling
ProgressBar::with_templatefn with_template(len, template) -> Result<ProgressBar, TemplateError>Creates a bar from a custom template.Styling
ProgressBar::builderfn builder() -> ProgressBarBuilderStarts builder configuration.Progress Bars
ProgressBarBuilder::templatefn template(self, template) -> Result<Self, TemplateError>Sets a custom template while building.Styling
inc / inc_byfn inc(&self, delta: u64)Advances position.Progress Bars
set_positionfn set_position(&self, pos: u64)Sets absolute position.Progress Bars
set_lengthfn set_length(&self, len: u64)Sets total length.Progress Bars
set_length_unknownfn set_length_unknown(&self)Clears the known total.Progress Bars
set_messagefn set_message(&self, msg)Sets {msg}.Styling
set_prefixfn set_prefix(&self, prefix)Sets {prefix}.Styling
set_postfixfn set_postfix(&self, postfix)Sets {postfix}.Styling
tickfn tick(&self)Advances spinner frame.Spinners
finishfn finish(&self)Completes the bar.Progress Bars
finish_with_messagefn finish_with_message(&self, msg)Completes with final message.Progress Bars
finish_with_symbolfn finish_with_symbol(&self, symbol, msg)Completes with a fixed prefix marker.Progress Bars
finish_and_clearfn finish_and_clear(&self)Completes and clears output.Progress Bars
abandonfn abandon(&self)Stops without finished state.Progress Bars
resetfn reset(&self)Reuses the bar from zero.Progress Bars
set_stylefn set_style(&self, style)Replaces visual style.Styling
set_templatefn set_template(&self, template) -> Result<(), TemplateError>Replaces style from a template.Styling
set_draw_deltafn set_draw_delta(&self, delta)Redraws after enough progress.Advanced
set_draw_ratefn set_draw_rate(&self, rate)Caps redraws per second.Advanced
enable_steady_tickfn enable_steady_tick(&self, interval)Starts background ticking.Spinners
printlnfn println(&self, msg: &str)Prints above the bar.Progress Bars
wrap_iterfn wrap_iter<I>(&self, iter: I)Uses a bar for an iterator.Examples
fractionfn fraction(&self) -> f64Returns completion ratio.Progress Bars
percentfn percent(&self) -> f64Returns completion percent.Progress Bars
remainingfn remaining(&self) -> Option<u64>Returns remaining work.Progress Bars
is_finishedfn is_finished(&self) -> boolReturns completion state.Progress Bars
is_abandonedfn is_abandoned(&self) -> boolReturns abandonment state.Progress Bars
Spinnerstruct SpinnerErgonomic spinner wrapper.Spinners
Spinner::successfn success(&self, msg)Stops with a success marker.Spinners
Spinner::failurefn failure(&self, msg)Stops with a failure marker.Spinners
Spinner::warningfn warning(&self, msg)Stops with a warning marker.Spinners
Spinner::infofn info(&self, msg)Stops with an informational marker.Spinners
MultiProgressstruct MultiProgressShared block for many bars.Multi Progress
ProgressStylestruct ProgressStyleTemplate, chars, colors, keys.Styling
ProgressCharsstruct ProgressCharsReusable fill, head, and empty characters.Styling
Themeenum ThemeBuilt-in style presets.Themes
Colorenum ColorANSI color value.Styling
ColorSpecstruct ColorSpecANSI text attributes.Styling
DrawTargetenum DrawTargetStdout, stderr, hidden, or writer.Advanced
terminal::detect::terminal_width_for_fdfn terminal_width_for_fd(fd: i32) -> usizeDetects width for stdout/stderr with fallback.Advanced
terminal::detect::terminal_height_for_fdfn terminal_height_for_fd(fd: i32) -> usizeDetects height for stdout/stderr with fallback.Advanced
terminal::detect::supports_interactive_outputfn supports_interactive_output(fd: i32) -> boolChecks interactive cursor-update support for a stream.Advanced
ProgressIteratortrait ProgressIteratorAdds .progress() methods.Examples
format_bytesfn format_bytes(u64) -> StringHuman byte count.Styling
format_durationfn format_duration(Duration) -> StringHuman duration.Styling

Progress Bars

ProgressBar is cloneable and all mutation methods take &self, so a bar can be shared between threads or moved into worker closures.

Construction

#![allow(unused)]
fn main() {
use loaders::{DrawTarget, ProgressBar, Theme};

let plain = ProgressBar::new(100);
let hidden = ProgressBar::hidden();
let targeted = ProgressBar::with_draw_target(100, DrawTarget::stderr());
let custom = ProgressBar::with_style(100, Theme::Unicode.style());
let templated = ProgressBar::with_template(100, "{prefix} [{bar:20}] {percent}%")?;

let built = ProgressBar::builder()
    .length(100)
    .template("{prefix} [{bar:20}] {remaining} left")?
    .message("loading")
    .prefix("job")
    .draw_delta(2)
    .draw_rate(30)
    .build();
Ok::<(), loaders::bar::template::TemplateError>(())
}

Mutating Progress

Use inc or inc_by for relative progress and set_position for absolute progress. set_length can be called later when a total becomes known. set_length_unknown clears the total and makes the bar indeterminate again.

#![allow(unused)]
fn main() {
let pb = loaders::ProgressBar::new(10);
pb.set_prefix("compile");
pb.set_message("parsing");
pb.inc(3);
pb.set_position(7);
pb.set_postfix("fast path");
pb.set_length_unknown();
}

Width Control

Use {bar:N} for a fixed bar width and {wide_bar} for terminal-adaptive width. {wide_bar} recalculates on each draw and fills the remaining width in the full rendered template.

#![allow(unused)]
fn main() {
let fixed = loaders::ProgressBar::with_template(100, "[{bar:20}] {percent}%")?;
let adaptive = loaders::ProgressBar::with_template(100, "[{wide_bar}] {percent}%")?;
Ok::<(), loaders::bar::template::TemplateError>(())
}

Reading Progress State

position, length, remaining, fraction, and percent make progress state visible for summaries and application logic.

#![allow(unused)]
fn main() {
let pb = loaders::ProgressBar::hidden();
pb.set_length(10);
pb.inc(4);
assert_eq!(pb.remaining(), Some(6));
assert_eq!(pb.fraction(), 0.4);
assert_eq!(pb.percent(), 40.0);
}

Draw Delta and Draw Rate

draw_delta skips redraws until enough position changes have accumulated. It is useful for hot loops. draw_rate limits redraws per second and is useful when progress changes very frequently. Set draw_rate(0) for unlimited redraws.

Interactive terminals redraw in place on the same line. Non-interactive outputs (pipes, files, CI logs, custom non-TTY writers) fall back to newline snapshots.

Steady Tick

enable_steady_tick spawns a background thread that calls tick() until the bar finishes or disable_steady_tick() is called.

#![allow(unused)]
fn main() {
use std::time::Duration;
let pb = loaders::ProgressBar::new_spinner();
pb.enable_steady_tick(Duration::from_millis(80));
pb.set_message("waiting");
pb.disable_steady_tick();
pb.finish();
}

Finishing

finish completes the bar. finish_with_message sets a final message first. finish_with_symbol sets a fixed marker prefix and final message. finish_and_clear removes the line. abandon leaves the bar unfinished, which is useful for cancelled work and can be inspected with is_abandoned.

Reset and Reuse

reset clears position, completion state, spinner frame index, and timing. It keeps the same style, length, target, prefix, and message so repeated phases can reuse one handle.

Spinners

A spinner is a progress bar without a length. You can use ProgressBar::new_spinner() or the Spinner wrapper when you prefer spinner-specific method names.

Presets

loaders ships with 27 built-in loading frame presets:

NameFrames
dotsbraille dot cycle
dots2heavy braille dot cycle
dots3smooth braille dot cycle
line`- \
pipebox pipe cycle
starstar pulse
bounceminimal bounce
arrowsdirectional arrows
clockclock faces
earthglobe rotation
moonmoon phases
runnerwalking/running frames
pongmoving paddle frames
sharkswimming text frames
weatherweather icons
christmastree frames
togglefilled/empty square
square_cornersrotating square corners
binary0 1 10 11
meter▱▱▱ ▰▱▱ ▰▰▱ ▰▰▰
quadrants◐ ◓ ◑ ◒
triangles◢ ◣ ◤ ◥
circle○ ◔ ◐ ◕ ●
box_bouncemoving block
pulse. .. ... ..
heart♡ ♥
progress_blocks▁ ▂ ▃ ▄ ▅ ▆ ▇ █

Custom Frames

#![allow(unused)]
fn main() {
use loaders::{ProgressBar, ProgressStyle};

let pb = ProgressBar::new_spinner();
pb.set_style(ProgressStyle::default_spinner().tick_strings(&[".", "o", "O", "o"]));
pb.tick();
}

Success and Failure Symbols

#![allow(unused)]
fn main() {
use loaders::{Spinner, spinner::frames::DOTS};

let spinner = Spinner::with_message(&DOTS, "uploading");
spinner.stop_with_symbol("ok", "uploaded");
}

Convenience helpers provide portable text markers:

#![allow(unused)]
fn main() {
use loaders::{Spinner, spinner::frames::METER};

let spinner = Spinner::with_message(&METER, "checking");
spinner.success("ready");
}

Use failure, warning, and info for the other common terminal outcomes.

Custom Message and Padding

Spinners use the same template engine as progress bars, so message fields can be padded or truncated with width specs such as {msg:>20} and {prefix:^10}.

Tick Intervals

Fast terminal spinners usually feel good at 60-120ms. Longer operations with expensive rendered templates can use 120-250ms. Background ticking is optional; manual tick() is better when each loop iteration naturally represents progress.

Multi Progress

MultiProgress owns a single renderer and stores bars in draw order. Child bars are updated through their normal ProgressBar handles, and join refreshes the block until every bar is finished or abandoned.

Thread Safety

ProgressBar and MultiProgress use Arc<Mutex<_>> internally. Cloning either handle shares the same state, which makes them suitable for worker threads.

#![allow(unused)]
fn main() {
use loaders::{MultiProgress, ProgressBar};
use std::thread;

let multi = MultiProgress::new();
let pb = multi.add(ProgressBar::new(10));
let handle = thread::spawn(move || {
    for _ in 0..10 {
        pb.inc(1);
    }
    pb.finish();
});
multi.join();
let _ = handle.join();
}

Ordering

Use add to append, insert for an index, insert_before and insert_after for relative placement, and remove when a bar should leave the block.

Messages Above the Block

multi.println("message") prints without corrupting the progress area. It is useful for phase names, warnings, and completed file names.

Best Practices

Create the MultiProgress on the main thread, add bars before spawning workers, clone or move each returned ProgressBar into the worker, and call join from the thread that owns the terminal.

Styling

ProgressStyle combines a parsed template, progress characters, spinner frames, optional line color, and custom keys.

Template Variables

VariableFormat SpecDescriptionExample Output
{bar}:40, :40.#/-Progress bar########>-------
{wide_bar}noneWidth-adaptive progress barfills remaining columns
{pos}noneCurrent position42
{count}noneCompact current position1.2k
{human_pos}noneAlias for compact position1.2k
{len}noneTotal length or ?100
{total_count}noneCompact total length1.5M
{human_len}noneAlias for compact total length1.5M
{remaining}noneRemaining units58
{human_remaining}noneCompact remaining units58.0k
{ratio}:.3Completion ratio0.420
{percent}:.2Percentage without %42 or 42.50
{elapsed}noneHuman elapsed time3m 14s
{elapsed_precise}nonePrecise elapsed time00:03:14
{elapsed_millis}noneMillisecond elapsed time00:03:14.123
{eta}noneEstimated remaining time~12s
{eta_precise}nonePrecise ETA00:00:12
{eta_millis}noneMillisecond ETA00:00:12.345
{per_sec}:.2Average rate123.45
{rate}noneFormatted item rate1.23k items/s
{bytes}nonePosition as bytes1.00 MB
{total_bytes}noneLength as bytes50.00 MB
{bytes_per_sec}noneByte throughput1.23 MB/s
{spinner}noneCurrent spinner frame-
{msg}:20, :>20, :^20Message with optional padding/truncationdownloading
{prefix}:10, :>10, :^10Prefix with optional padding/truncationtask
{postfix}:10, :>10, :^10Postfix with optional padding/truncationok
{status}:10, :>10, :^10running, spinning, finished, or abandonedrunning
{wide_msg}none, :20, :>20, :^20Terminal-width or explicit-width message fieldterminal-width text

Bar Format

{bar:N} sets the width. {bar:N.fill/empty} sets fill and empty characters. Color words inside a template are accepted for compatibility with terminal-style templates; whole-line color is configured with ColorSpec.

{wide_bar} uses the active draw target width. Width detection uses terminal APIs and falls back to COLUMNS/LINES or safe defaults when a TTY is not available.

#![allow(unused)]
fn main() {
let style = loaders::ProgressStyle::with_template(
    "{prefix} [{bar:30.#/-}] {percent:.1}% {msg}",
)?;
Ok::<(), loaders::bar::template::TemplateError>(())
}

Reusable Progress Characters

ProgressChars stores fill, head, and empty characters as a reusable value.

#![allow(unused)]
fn main() {
use loaders::{ProgressChars, ProgressStyle};

let style = ProgressStyle::default_bar()
    .progress_chars_set(ProgressChars::blocks());
}

Use try_progress_chars when parsing user configuration and invalid inputs should return an error.

Text Padding and Truncation

String variables support width specs for alignment and fixed width:

  • :20 left-aligned width 20
  • :>20 right-aligned width 20
  • :^20 centered width 20

When the content is longer than the width it is truncated to fit.

Colors

#![allow(unused)]
fn main() {
use loaders::{Color, ColorSpec, ProgressStyle};

let style = ProgressStyle::default_bar()
    .color(ColorSpec::new().set_fg(Color::Cyan).set_bold(true));
}

Custom Keys

#![allow(unused)]
fn main() {
let style = loaders::ProgressStyle::with_template("{phase} {bar:20}")?
    .with_key("phase", |state| format!("phase {}", state.pos / 10 + 1));
Ok::<(), loaders::bar::template::TemplateError>(())
}

Themes

Every Theme returns a fully configured ProgressStyle.

ThemeDescription50% PreviewCode
DefaultPortable ASCII[###############>--------------]Theme::Default.style()
UnicodeHeavy line style[━━━━━━━━━━━━━━━╸░░░░░░░░░░░░░░]Theme::Unicode.style()
BrailleDense braille feel[⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣦⣦⣦]Theme::Braille.style()
BlockFull block style[███████████████▉▒▒▒▒▒▒▒▒▒▒▒▒▒▒]Theme::Block.style()
MinimalNo box=============== >..............Theme::Minimal.style()
ColorfulBright colored output[###############>--------------]Theme::Colorful.style()
GradientShaded blocks[███████████████▓░░░░░░░░░░░░░░]Theme::Gradient.style()
RetroClassic equals arrow[=============== >-------------]Theme::Retro.style()
DotsDot progress[•••••••••••••••∘ ]Theme::Dots.style()
RoundedRound markers[●●●●●●●●●●●●●●●○──────────────]Theme::Rounded.style()
ShadedSmall square shading[▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▫ ]Theme::Shaded.style()
ArrowsDirectional fill[>>>>>>>>>>>>>>>--------------]Theme::Arrows.style()
#![allow(unused)]
fn main() {
use loaders::{ProgressBar, Theme};

let pb = ProgressBar::new(100);
pb.set_style(Theme::Block.style());
}

Advanced Usage

Custom Draw Targets

DrawTarget::to_writer accepts any Write + Send + 'static. This is useful for tests, logs, or embedding progress output into a larger terminal abstraction.

#![allow(unused)]
fn main() {
let target = loaders::DrawTarget::to_writer(Vec::<u8>::new());
let pb = loaders::ProgressBar::with_draw_target(10, target);
pb.inc(1);
}

CI, Pipes, and NO_COLOR

Terminal detection checks common CI variables and stream capabilities. Non-TTY targets use newline snapshots instead of cursor movement. The NO_COLOR environment variable disables ANSI color output everywhere the crate emits color.

On Windows consoles, interactive targets enable virtual terminal mode so progress bars and spinners update on the same line when the terminal supports ANSI cursor controls.

Platform Coverage

loaders is dependency-free and uses std + platform APIs for terminal detection:

  • Windows consoles use Win32 handles, console mode checks, and console width/height queries.
  • Unix-family terminals (Linux, macOS, BSD, and similar) use isatty and ioctl.

The crate supports both 32-bit and 64-bit targets as long as Rust supports the target.

Hidden Bars in Tests

#![allow(unused)]
fn main() {
let pb = loaders::ProgressBar::hidden();
pb.inc(1);
assert_eq!(pb.position(), 1);
}

Custom Template Keys

Custom keys let applications render domain terms without forking the renderer.

#![allow(unused)]
fn main() {
let style = loaders::ProgressStyle::with_template("{phase} {bar}")?
    .with_key("phase", |state| format!("phase {}", state.pos / 10 + 1));
Ok::<(), loaders::bar::template::TemplateError>(())
}

Thread Safety Model

ProgressBar is a cloneable handle around shared state. Clone it into worker threads and call inc, set_message, or finish from those threads. Each method locks briefly around state changes and rendering.

Draw Rate Limiting

For high-throughput loops, set draw_delta to redraw every N items and draw_rate to cap terminal writes per second.

#![allow(unused)]
fn main() {
let pb = loaders::ProgressBar::builder()
    .length(1_000_000)
    .draw_delta(1_000)
    .draw_rate(20)
    .build();
}

Capturing Output

Use a custom writer when assertions need rendered text. Use ProgressBar::hidden() when the test only needs state.

Examples

The examples/ directory contains runnable demonstrations for common CLI situations.

basic_bar.rs

Shows a default bar and a Unicode themed bar.

default [##########>-----------------------------] 25/100 (25%) working
unicode [━━━━━━━━━━╸░░░░░░░░░░░░░░░░░░░░] 25/100 working

spinner.rs

Runs the dots, moon, and clock presets with automatic steady ticks and a final success symbol.

multi_progress.rs

Creates three bars, moves each into a worker thread, and calls multi.join() on the main thread so the terminal block refreshes while work continues.

custom_style.rs

Demonstrates a custom template, progress_chars("=>-"), custom spinner frames, and a bold cyan ColorSpec.

download_simulation.rs

Uses byte-aware variables:

download [########>-------------------------------] 12.00 MB/50.00 MB 1.34 MB/s ETA ~28s output.bin

nested_bars.rs

Shows an outer batch bar and an inner item bar using MultiProgress, with phase messages printed above the block.

eta_and_rate.rs

Displays {per_sec:.2}, {eta}, and {elapsed} while simulated processing speed changes.

themed_bar.rs

Loops through every theme and renders a short visual gallery.

iterator_wrap.rs

Demonstrates .progress(), .progress_with_style(style), .progress_count(500), summing, and chained iterators.

custom_template.rs

Registers custom_label with with_key and renders phase labels directly in the template.

advanced_customization.rs

Combines reusable ProgressChars, ANSI color, status-aware templates, compact remaining counts, draw throttling, progress state reads, and finish_with_symbol.

custom_loader.rs

Shows custom loading animations using the meter, progress block, and pulse presets plus spinner status helpers.