nanospinner 0.2.4

A minimal, zero-dependency terminal spinner for Rust CLI applications
Documentation

⠋ nanospinner Build Status Crates.io Docs.rs License Coverage Status

A minimal, zero-dependency terminal spinner for Rust applications. Supports single and multi-spinner modes.

demo

Inspired by the Node.js nanospinner npm package, nanospinner gives you a lightweight animated spinner using only the Rust standard library — no heavy crates, no transitive dependencies, builds in .2 seconds.

Part of the nano crate family — zero-dependency building blocks for Rust.

Motivation

Most Rust spinner crates sit at two extremes: lightweight but limited (spinoff), or feature-rich but heavy (indicatif). nanospinner sits in the middle: thread-safe handles, multi-spinner support, custom writers, and automatic TTY detection, all with zero dependencies and builds in under .2 seconds. If you need a spinner (not a progress bar), you probably don't need anything else.

Comparison

nanospinner spinoff indicatif
Dependencies 0 4 6
Clean Build Time ~0.2s ~1.2s ~1.4s
Customizable Frames Default Braille set Yes (80+ sets) Yes
Multiple Spinners Yes No Yes
Auto TTY Detection Yes No Yes
Custom Writer Yes (io::Write) Stderr only Yes (custom trait)
Thread-Safe Handles Yes (Send) No Yes (Send + Sync)
Progress Bars No No Yes
Async Support No No Optional (tokio feature)

Build times measured from a clean cargo build --release on macOS aarch64 (Apple Silicon). Your numbers may vary by platform.

nanospinner is for when you want a spinner and nothing else.

Features

  • Animated Braille dot spinner (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏)
  • Colored finalization: green for success, red for failure
  • Update the message while the spinner is running
  • Custom writer support (stdout, stderr, or any io::Write + Send)
  • Automatic cleanup via Drop — no thread leaks if you forget to stop
  • Automatic TTY detection — ANSI codes and animation are skipped when output is piped or redirected
  • Multi-spinner support — manage multiple concurrent spinners on separate terminal lines
  • Thread-safe SpinnerLineHandle — move individual spinner controls to worker threads

Quick Start

Add nanospinner to your project:

cargo add nanospinner
use nanospinner::Spinner;
use std::thread;
use std::time::Duration;

fn main() {
    let handle = Spinner::new("Loading...").start();
    thread::sleep(Duration::from_secs(2));
    handle.success();
}

Usage

Single Spinner

Create and start a spinner

let handle = Spinner::new("Downloading files...").start();

Finalize with success or failure

handle.success();           // ✔ Downloading files...
handle.fail();              // ✖ Downloading files...

Finalize with a replacement message

handle.success_with("Done!");              // ✔ Done!
handle.fail_with("Connection timed out");  // ✖ Connection timed out

Update the message mid-spin

let handle = Spinner::new("Step 1...").start();
thread::sleep(Duration::from_secs(1));
handle.update("Step 2...");
thread::sleep(Duration::from_secs(1));
handle.success_with("All steps complete");

Write to a custom destination

use std::io;

let handle = Spinner::with_writer("Processing...", io::stderr()).start();
thread::sleep(Duration::from_secs(1));
handle.success();

Stop without a symbol

let handle = Spinner::new("Working...").start();
thread::sleep(Duration::from_secs(1));
handle.stop(); // clears the line, no symbol printed

Piped / non-TTY output

When stdout isn't a terminal (e.g. piped to a file or another program), nanospinner automatically skips the animation and ANSI color codes. The final result is printed as plain text:

$ my_tool | cat
 Done!

No configuration needed — Spinner::new() detects this automatically. If you're using a custom writer and want to force TTY behavior, use with_writer_tty:

let handle = Spinner::with_writer_tty("Building...", my_writer, true).start();

Multi-Spinner

For concurrent tasks, MultiSpinner manages multiple spinners on separate terminal lines with a single background render thread.

Basic usage

use nanospinner::MultiSpinner;
use std::thread;
use std::time::Duration;

let handle = MultiSpinner::new().start();

let line1 = handle.add("Downloading...");
let line2 = handle.add("Compiling...");

thread::sleep(Duration::from_secs(2));
line1.success();
line2.success_with("Compiled successfully!");

handle.stop();

Update and finalize individual spinners

let line = handle.add("Processing...");
line.update("Processing (50%)...");

// Finalize with success or failure
line.success();              // ✔ Processing (50%)...
line.success_with("Done!");  // ✔ Done!
line.fail();                 // ✖ Processing (50%)...
line.fail_with("Error");     // ✖ Error

// Or silently dismiss the line
line.clear();                // (line disappears, no output)

Dismiss a line with clear

Use clear() to silently remove a spinner line without printing any symbol or message. Remaining lines collapse together with no gap.

use nanospinner::MultiSpinner;
use std::thread;
use std::time::Duration;

let handle = MultiSpinner::new().start();

let line1 = handle.add("Checking cache...");
let line2 = handle.add("Downloading...");
let line3 = handle.add("Compiling...");

thread::sleep(Duration::from_secs(1));
line1.clear(); // cache check done — dismiss silently

thread::sleep(Duration::from_secs(1));
line2.success_with("Downloaded!");
line3.success();

handle.stop();
// Only the downloaded/compiled lines appear in the final output

Thread-based usage

SpinnerLineHandle is Send, so you can move it to worker threads:

use nanospinner::MultiSpinner;
use std::thread;
use std::time::Duration;

let handle = MultiSpinner::new().start();

let workers: Vec<_> = (1..=3)
    .map(|i| {
        let line = handle.add(format!("Worker {i} processing..."));
        thread::spawn(move || {
            thread::sleep(Duration::from_secs(i));
            line.success_with(format!("Worker {i} done"));
        })
    })
    .collect();

for w in workers {
    w.join().unwrap();
}

handle.stop();

Piped / non-TTY output

When stdout isn't a terminal, MultiSpinner skips animation and the render thread entirely. Each spinner prints a single plain-text result line when finalized:

$ my_tool | cat
 Task 1 complete
 Task 2 complete
 Task 3 failed

Contributing

Contributions are welcome. To get started:

  1. Fork the repository
  2. Create a feature branch (git checkout -b my-feature)
  3. Make your changes
  4. Run the tests: cargo test
  5. Submit a pull request

Please keep changes minimal and focused. This crate's goal is to stay small and as dependency-free as possible.

License

This project is licensed under the MIT License.