Spinner¶
A one-line progress indicator on stderr that turns itself off when stderr is not a terminal.
pub struct Spinner { /* private fields */ }
impl Spinner {
#[must_use]
pub fn new(msg: impl Into<String>) -> Self;
pub fn set_message(&mut self, msg: impl Into<String>);
pub fn finish(self);
}
impl Drop for Spinner { /* calls the same cleanup as finish */ }
What Spinner writes to the terminal¶
Every draw is a clear-line followed by ⠋ and the current message.
The frame character is the single Braille glyph ⠋ and it never
changes — there is no frame sequence and no animation. See
Why the spinner does not animate.
Everything goes to stderr. Nothing is written to stdout, which is what makes it safe to pipe a command's stdout into another program while the spinner is running.
finish clears the line and writes nothing in its place. The cursor
ends at column 0 of a now-empty line, so whatever you print next starts
cleanly. If you want a "done" message, print it yourself after
finishing.
Write failures are ignored. Every terminal call is discarded with
let _ = ..., so a closed or failing stderr silently produces no
output rather than returning an error or panicking.
What happens when stderr is not a terminal¶
Spinner::new asks console::Term::stderr() whether it is a terminal.
If it is not, the spinner stores no terminal handle and every method
becomes a no-op: no escape sequences, no frame characters, no
whitespace. That covers CI job logs, output redirected to a file, and
tools speaking a protocol over stdio such as MCP.
This is decided once, in the constructor. Changing the terminal state afterwards has no effect on a spinner that already exists.
The check is also why the crate's own tests can construct and drive a
spinner: cargo test captures stderr, so is_term() is false and the
spinner is inert.
Calling finish and the Drop impl¶
finish takes self by value. It can therefore be called at most
once on a given spinner — a second call will not compile, because the
first one moved it.
What is idempotent is the cleanup itself. Internally a finished flag
guards it, and Drop runs the same cleanup, so:
- calling
finishand then letting the value drop clears the line once, not twice; - dropping a spinner without calling
finishstill clears the line.
finish exists to let you control when the line is cleared rather
than leaving it to the end of the enclosing scope.
The rustdoc on finish describes it as "safe to call multiple times",
which the signature does not permit — see
Known documentation defects.
Making the spinner move¶
Nothing in the crate advances the spinner on a timer. The glyph is
redrawn only when you call set_message, and it is the same glyph each
time, so the visible change is the text:
let mut spinner = Spinner::new("resolving dependencies…");
let manifest = fetch_manifest().await?;
spinner.set_message("downloading…");
let archive = download(&manifest).await?;
spinner.set_message("verifying signature…");
verify(&archive)?;
spinner.finish();
A long await between two set_message calls shows a static line for
its whole duration. If that matters, break the work into more stages
and call set_message more often.
Threading and concurrency¶
set_message takes &mut self, so the spinner is driven from one
place at a time. There is no background task, no thread and no internal
locking; if two async tasks need to report progress, give them one
owner between them rather than two spinners.