rtb-tui 0.6.5

Reusable TUI widgets: forms, wizards, prompts, tables. Part of the phpboyscout Rust toolkit.
Documentation
---
title: Spinner
description: Spinner's constructor and methods, what it writes, and what it does when stderr is not a terminal.
tags: [reference, spinner]
---

# `Spinner`

A one-line progress indicator on stderr that turns itself off when
stderr is not a terminal.

```rust
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](../explanation/02-why-the-spinner-does-not-animate.md).

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 `finish` and then letting the value drop clears the line once,
  not twice;
- dropping a spinner without calling `finish` still 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.

## 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:

```rust
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.