⠋ 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.
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— renders final state and joins the background thread, even if you never callstop() - 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
Spinner::new(msg).start() spawns a background thread that animates the spinner. It returns a SpinnerHandle you use to update or finalize the spinner. Calling success() or fail() stops the thread and prints the final line — no separate stop() needed. If you drop the handle without finalizing, the thread is joined and the line is cleared automatically.
SpinnerHandle methods
| Method | Effect |
|---|---|
update(msg) |
Change the message while spinning |
success() |
Stop and print ✔ with the current message |
success_with(msg) |
Stop and print ✔ with a replacement message |
fail() |
Stop and print ✖ with the current message |
fail_with(msg) |
Stop and print ✖ with a replacement message |
stop() |
Stop and clear the line (no symbol) |
| drop | Same as stop() — joins the thread, clears the line |
Examples
use Spinner;
use thread;
use Duration;
// Basic: start, wait, finalize
let handle = new.start;
sleep;
handle.success; // ✔ Downloading...
// Update mid-spin, finalize with a replacement message
let handle = new.start;
sleep;
handle.update;
sleep;
handle.success_with; // ✔ All steps complete
Multi-Spinner
MultiSpinner manages multiple spinner lines with a single background render thread. The key difference from a single spinner: finalizing a line (success, fail, clear) only updates that line's status — the render thread keeps running and redraws all lines each frame. You must call stop() on the group handle (or let it drop) to shut down the render thread.
MultiSpinnerHandle methods
| Method | Effect |
|---|---|
add(msg) |
Add a spinner line, returns a SpinnerLineHandle |
stop() |
Stop the render thread and print final state |
| drop | Same as stop() |
SpinnerLineHandle methods
Each SpinnerLineHandle controls one line in the group. Finalizing consumes the handle, preventing double-finalization. Handles are Send so they can be moved to worker threads.
| Method | Effect |
|---|---|
update(msg) |
Change this line's message |
success() |
Finalize with ✔ and the current message |
success_with(msg) |
Finalize with ✔ and a replacement message |
fail() |
Finalize with ✖ and the current message |
fail_with(msg) |
Finalize with ✖ and a replacement message |
clear() |
Silently dismiss — line disappears, no output |
Examples
use MultiSpinner;
use thread;
use Duration;
// Basic: add lines, finalize, stop the group
let handle = new.start;
let line1 = handle.add;
let line2 = handle.add;
sleep;
line1.success;
line2.fail_with;
handle.stop; // shuts down the render thread
// Clear: silently dismiss lines you no longer need
let handle = new.start;
let check = handle.add;
let lint = handle.add;
let build = handle.add;
sleep;
lint.clear; // line disappears, remaining lines collapse
sleep;
check.success;
build.success;
handle.stop;
// Thread-based: move line handles to worker threads
let handle = new.start;
let workers: =
.map
.collect;
for w in workers
handle.stop;
Custom Writers and TTY Detection
Both Spinner and MultiSpinner auto-detect whether stdout is a terminal. When it isn't (piped, redirected), animation and ANSI codes are skipped — only plain text is printed:
$ my_tool | cat
✔ Done!
For custom output targets, both offer with_writer and with_writer_tty constructors:
// Custom writer (defaults to non-TTY — no ANSI codes)
let handle = with_writer.start;
let handle = with_writer.start;
// Custom writer with explicit TTY control
let handle = with_writer_tty.start;
let handle = with_writer_tty.start;
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.