⠋ nanospinner

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

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.
nanospinner solves this by providing the essentials and nothing more:
- Zero external dependencies (only
std) - Simple, ergonomic API
- Thread-safe with clean shutdown
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:
use Spinner;
use thread;
use Duration;
Usage
Single Spinner
Create and start a spinner
let handle = new.start;
Finalize with success or failure
handle.success; // ✔ Downloading files...
handle.fail; // ✖ Downloading files...
Finalize with a replacement message
handle.success_with; // ✔ Done!
handle.fail_with; // ✖ Connection timed out
Update the message mid-spin
let handle = new.start;
sleep;
handle.update;
sleep;
handle.success_with;
Write to a custom destination
use io;
let handle = with_writer.start;
sleep;
handle.success;
Stop without a symbol
let handle = new.start;
sleep;
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:
|
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 = with_writer_tty.start;
Multi-Spinner
For concurrent tasks, MultiSpinner manages multiple spinners on separate terminal lines with a single background render thread.
Basic usage
use MultiSpinner;
use thread;
use Duration;
let handle = new.start;
let line1 = handle.add;
let line2 = handle.add;
sleep;
line1.success;
line2.success_with;
handle.stop;
Update and finalize individual spinners
let line = handle.add;
line.update;
// Finalize with success or failure
line.success; // ✔ Processing (50%)...
line.success_with; // ✔ Done!
line.fail; // ✖ Processing (50%)...
line.fail_with; // ✖ 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 MultiSpinner;
use thread;
use Duration;
let handle = new.start;
let line1 = handle.add;
let line2 = handle.add;
let line3 = handle.add;
sleep;
line1.clear; // cache check done — dismiss silently
sleep;
line2.success_with;
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 MultiSpinner;
use thread;
use Duration;
let handle = new.start;
let workers: =
.map
.collect;
for w in workers
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:
|
Contributing
Contributions are welcome. To get started:
- Fork the repository
- Create a feature branch (
git checkout -b my-feature) - Make your changes
- Run the tests:
cargo test - 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.