qubit-progress

Generic progress reporting abstractions for Qubit Rust libraries

Rust CI Coverage Crates.io Rust License

Generic progress reporting abstractions for the Rust ecosystem.

When to use this crate

Use qubit-progress when an operation needs to report progress without tying the reporting API to one domain:

This crate is not a scheduler, task executor, or UI framework. It only defines the event model and basic reporter implementations.

Overview

Qubit Progress models progress as immutable events. A progress event itself carries:

ProgressReporter is intentionally small: when the stock reporters are not enough, your application can provide its own implementation—for example, to drive a graphical progress bar, refresh a status region in a desktop UI, or forward updates to a web client. This crate stays UI-agnostic; wiring events to a particular toolkit or transport is your integration layer.

Domain crates should keep their own domain state and expose progress by converting their state into ProgressEvent values. Domain-specific errors, logs, metrics, and traces should stay in their own mechanisms instead of being attached to progress events.

Installation

[dependencies]
qubit-progress = "0.4"

Quick Start

use std::time::Duration;

use qubit_progress::{
    ProgressEvent,
    ProgressReporter,
    StdoutProgressReporter,
};

let reporter = StdoutProgressReporter::default();
let event = ProgressEvent::builder()
    .running()
    .total(4)
    .completed(2)
    .active(1)
    .stage_named("copy", "Copy files")
    .elapsed(Duration::from_secs(2))
    .build();

reporter.report(&event);

The lower-level constructors remain available when a caller already has prebuilt counters or stage metadata, but the builder is the preferred entry point for ordinary reporting code.

Reporting an operation lifecycle

Use Progress when one operation needs a started event, periodic running events, and a terminal event. The run tracks elapsed time and throttles running callbacks, while your domain code owns the counters.

use std::time::Duration;

use qubit_progress::{
    ProgressCounters,
    Progress,
    StdoutProgressReporter,
};

let reporter = StdoutProgressReporter::default();
let mut progress = Progress::new(&reporter, Duration::from_secs(5));

let started = progress.report_started(ProgressCounters::new(Some(3)));
assert!(started.elapsed().is_zero());

let mut completed = 0;
for _task in 0..3 {
    // ... execute one unit of work ...
    completed += 1;
    let counters = ProgressCounters::new(Some(3)).with_completed_count(completed);
    let _running_event = progress.report_running_if_due(counters);
}

let final_counters = ProgressCounters::new(Some(3))
    .with_completed_count(3)
    .with_succeeded_count(3);
let finished = progress.report_finished(final_counters);
assert!(finished.elapsed() >= started.elapsed());

report_running_if_due returns Some(event) only when it emitted an event. The method does not block waiting for the next interval: it returns None immediately when not due, and when due it synchronously calls the reporter and returns the emitted event (so blocking behavior depends on the reporter implementation). A practical pattern is calling it once after each completed unit of work: reporting happens automatically when the interval is due, and otherwise the call is effectively a no-op. Use report_running when an external scheduler or background thread already controls the reporting interval.

Reporting from a background thread

Use Progress::spawn_running_reporter when work happens on one or more worker threads but running events should be reported from a separate background thread. Workers keep updating domain state. The background reporter waits for either a timeout or a RunningProgressPointHandle::report signal, then calls your snapshot closure to build fresh ProgressCounters.

This is useful for parallel executors: reporter callbacks stay out of worker hot paths for positive intervals, while Duration::ZERO can still report after each worker progress point without busy waiting. Keep the RunningProgressGuard on the coordinating thread, pass RunningProgressPointHandle clones to workers, and call stop_and_join before terminal finished, failed, or canceled events are reported.

The example below uses qubit-atomic’s AtomicCount for the shared completion counter. Add it to your manifest only when you adopt this pattern:

qubit-atomic = "0.10"
use std::{
    sync::Arc,
    thread,
    time::Duration,
};

use qubit_atomic::AtomicCount;
use qubit_progress::{
    Progress,
    ProgressCounters,
    StdoutProgressReporter,
};

let reporter = StdoutProgressReporter::default();
let completed = Arc::new(AtomicCount::zero());

thread::scope(|scope| {
    let loop_completed = Arc::clone(&completed);
    let progress = Progress::new(&reporter, Duration::ZERO);
    let running_progress =
        progress.spawn_running_reporter(scope, move || {
            ProgressCounters::new(Some(3))
                .with_completed_count(loop_completed.get())
        });
    let progress_point_handle = running_progress.point_handle();

    let mut handles = Vec::new();
    for _ in 0..3 {
        let completed_worker = Arc::clone(&completed);
        let point = progress_point_handle.clone();
        handles.push(scope.spawn(move || {
            completed_worker.inc();
            point.report();
        }));
    }
    for handle in handles {
        handle.join().unwrap();
    }

    running_progress.stop_and_join();
});

For positive intervals, RunningProgressPointHandle::report is a no-op; the background reporter wakes itself with a timeout. This lets worker code call it unconditionally while the guard keeps stop/join ownership on the coordinating thread.

Multi-stage progress

Stages describe where an operation is inside a larger workflow. They are separate from lifecycle phases: a copy stage can be running, finished, failed, or canceled depending on the event.

use std::time::Duration;

use qubit_progress::{
    ProgressEvent,
    ProgressPhase,
    ProgressStage,
};

let stage = ProgressStage::new("verify", "Verify installation")
    .with_index(2)
    .with_total_stages(5)
    .with_weight(0.2);

let event = ProgressEvent::builder()
    .phase(ProgressPhase::Running)
    .total(10)
    .completed(7)
    .elapsed(Duration::from_secs(12))
    .stage(stage)
    .build();

assert_eq!(event.phase(), ProgressPhase::Running);
assert_eq!(event.counters().completed_count(), 7);

Counter semantics

ProgressCounters supports known-total and unknown-total progress.

For zero-sized known-total work, progress is treated as complete:

use qubit_progress::ProgressCounters;

let counters = ProgressCounters::new(Some(0));
assert_eq!(counters.progress_percent(), Some(100.0));

Reporter behavior

Reporter callbacks are intentionally side-effecting. A reporter may write to a terminal, append to a file, emit logs, update a UI bridge, or record events for tests. If a reporter panics, the caller decides whether to propagate or isolate that panic.

WriterProgressReporter writes a compact human-readable line.

StdoutProgressReporter and StderrProgressReporter are convenience reporters built on top of WriterProgressReporter for standard output and standard error.

LoggerProgressReporter emits through the log crate and can be configured with a target and level.

Public API Cheat Sheet

Documentation

Testing and CI

Run the fast local checks from the crate root:

cargo test
cargo clippy --all-targets -- -D warnings

To match the repository CI environment, run:

./align-ci.sh
./ci-check.sh
./coverage.sh json

./align-ci.sh aligns the local toolchain and CI-related configuration before ./ci-check.sh runs the same checks used by the pipeline. Use ./coverage.sh when changing behavior that should be reflected in coverage reports.

Contributing

Issues and pull requests are welcome. Please keep changes focused, add or update tests when behavior changes, and update this README or rustdoc when public API or user-visible behavior changes.

By contributing, you agree that your contribution is licensed under the same Apache License, Version 2.0 as this project.

Copyright © 2026 Haixing Hu, Qubit Co. Ltd.

This software is licensed under the Apache License, Version 2.0.

Author and Maintenance

Haixing Hu — Qubit Co. Ltd.

Repositorygithub.com/qubit-ltd/rs-progress
API documentationdocs.rs/qubit-progress
Cratecrates.io/crates/qubit-progress