strides 1.0.0-rc.2

Async-first terminal UI spinners and progress bars
# strides

[![Cargo](https://img.shields.io/crates/v/strides.svg)](https://crates.io/crates/strides)
[![Documentation](https://docs.rs/strides/badge.svg)](https://docs.rs/strides)
[![MSRV](https://img.shields.io/badge/MSRV-1.85-blue)](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0.html)

A command-line UI library to enhance async programs with progress bars and
spinners.

The crate is built around two extension traits and two containers:

- [`FutureExt`] adds `.progress(theme)` (sugar for
  `.progressive().with_theme(theme)`), `.progressive()` (unconfigured;
  inherits the parent Group's theme or renders with `Theme::default()` if
  awaited directly), and the `.with_label(...)` / `.with_messages(...)` /
  `.with_progress(...)` / `.with_elapsed_time()` setters that implicitly lift a
  bare [`Future`] into a `ProgressFuture`.
- [`StreamExt`] adds `.progress(theme, ...)` / `.progress_bytes(theme, ...)`
  (sugar) and `.progressive()` / `.progressive_bytes()` (unconfigured) to
  any [`Stream`].
- [`future::Group`] runs many [`Future`]s concurrently, rendering one line
  per task.
- [`stream::Group`] runs many [`Stream`]s concurrently, rendering one line
  per stream.
- For byte-counted file copies and downloads, convert an `AsyncRead` to a
  byte stream (e.g. via [`tokio_util::io::ReaderStream`]) and use
  `.progress_bytes(...)`.

Each `.progress(...)` call animates automatically and returns a builder for
further customization. A [`Theme`] bundles a [`Spinner`], a [`Bar`] and a
[`Layout`], and is accepted everywhere a theme is expected. A [`Layout`] is an
ordered list of [`Segment`]s controlling the order, spacing and formatting of
each progress line. See [`spinner::styles`] and [`bar::styles`] for predefined
variants.


Pick a mode by how many tasks you have and how many terminal rows you want to
spend on them:

| Tasks | Rows  | Futures                                          | Streams                                                   |
|-------|-------|--------------------------------------------------|-----------------------------------------------------------|
| 1     | 1     | `fut.progress(theme).await`                      | `s.progress(theme, ...)` / `s.progress_bytes(theme, ...)` |
| N     | N     | `Group::new(theme)` + `group.push(fut)` per task | `stream::Group::new(theme)` + `group.push(s.progressive(...))` per stream |
| N     | 1     | `join(futs).with_theme(theme).await`             | n/a                                                       |
| N     | 1-of-many | `group.push(join(futs).with_label(...))`     | n/a                                                       |

The last row uses [`future::join`] inside a [`future::Group`]: many futures
collapse into a single progress line that sits alongside other rows in the
Group. Streams do not have a `join` collapse, s— push each stream as its own
row.

[`Future`]: https://doc.rust-lang.org/std/future/trait.Future.html
[`Stream`]: https://docs.rs/futures-lite/latest/futures_lite/stream/trait.Stream.html
[`FutureExt`]: https://docs.rs/strides/latest/strides/future/trait.FutureExt.html
[`StreamExt`]: https://docs.rs/strides/latest/strides/stream/trait.StreamExt.html
[`future::Group`]: https://docs.rs/strides/latest/strides/future/struct.Group.html
[`stream::Group`]: https://docs.rs/strides/latest/strides/stream/struct.Group.html
[`tokio_util::io::ReaderStream`]: https://docs.rs/tokio-util/latest/tokio_util/io/struct.ReaderStream.html
[`Theme`]: https://docs.rs/strides/latest/strides/struct.Theme.html
[`Spinner`]: https://docs.rs/strides/latest/strides/spinner/struct.Spinner.html
[`Bar`]: https://docs.rs/strides/latest/strides/bar/struct.Bar.html
[`Layout`]: https://docs.rs/strides/latest/strides/layout/struct.Layout.html
[`Segment`]: https://docs.rs/strides/latest/strides/layout/enum.Segment.html
[`spinner::styles`]: https://docs.rs/strides/latest/strides/spinner/styles/index.html
[`bar::styles`]: https://docs.rs/strides/latest/strides/bar/styles/index.html
[`future::join`]: https://docs.rs/strides/latest/strides/future/fn.join.html


## Example

This example demonstrates how to animate single futures, a group of futures and
a stream. Run it with `cargo run --example readme`.

```rust
use std::time::Duration;

use async_io::Timer;
use futures_lite::StreamExt;
use strides::spinner::styles::{DOTS_3, SAND};

async fn animate_simple_future() {
    use strides::future::FutureExt as _;

    Timer::after(Duration::from_secs(2))
        .progress(DOTS_3)
        .with_label("some work going on for two seconds")
        .await;
}

async fn animate_two_futures_concurrently() {
    use strides::future::{FutureExt as _, Group};

    let mut group = Group::new(SAND).with_elapsed_time();
    group.push(Timer::after(Duration::from_secs(2)).with_label("two seconds"));
    group.push(Timer::after(Duration::from_secs(3)).with_label("three seconds"));

    group.for_each(|_| {}).await;
}

async fn animate_stream() {
    use strides::stream::StreamExt as _;

    let theme = strides::Theme::default()
        .with_spinner(DOTS_3)
        .with_bar(strides::bar::styles::THIN_LINE);

    futures_lite::stream::iter(0..100)
        .progress(theme, |_, item| *item as f64 / 100.0)
        .with_label("streaming 100 items")
        .then(|item| async move {
            Timer::after(Duration::from_millis(20)).await;
            item
        })
        .for_each(|_| {})
        .await;
}

fn main() {
    futures_lite::future::block_on(async {
        animate_simple_future().await;
        animate_two_futures_concurrently().await;
        animate_stream().await;
    })
}
```

See the [examples](./examples/) directory for more elaborate uses including
downloads, dynamic messages, per-task progress bars, and custom layouts.


## Minimum supported Rust version

strides requires Rust 1.85 or newer.


## License

[MIT](./LICENSE)