Spinner

Animated CLI spinners for visual feedback during long-running operations. The Spinner class provides configurable animations with Rich styling, context manager support, and decorator patterns.

Tip

Use spinners to indicate progress when exact completion time is unknown. For determinate progress, consider Rich’s built-in progress bars instead.

Quick overview

  • Multiple animation styles: BRAILLE, DOTS, LINE, ARROW, BLOCKS, CIRCLE, MOON, CLOCK

  • Configurable via presets (minimal, fancy, blocks, bounce, color_wave)

  • Context manager and decorator support

  • Thread-safe animation loop

  • Success/failure indicators on completion

Usage patterns

Basic context manager

from kstlib.ui import Spinner

with Spinner("Loading data...") as spinner:
    # Long operation here
    data = fetch_data()
    spinner.update("Processing...")
    process(data)
# Spinner shows success checkmark on exit

As a decorator

from kstlib.ui import Spinner

@Spinner.wrap("Fetching results")
def fetch_results():
    # Long operation
    return api.get_results()

results = fetch_results()  # Spinner runs during execution

With presets

from kstlib.ui import Spinner

# Use a preset style
with Spinner("Working...", preset="fancy"):
    do_work()

# Or specify style directly
with Spinner("Computing...", style="MOON", interval=0.1):
    compute()

Manual control

from kstlib.ui import Spinner

spinner = Spinner("Starting...")
spinner.start()
try:
    for item in items:
        spinner.update(f"Processing {item}...")
        process(item)
    spinner.succeed("All done!")
except Exception as e:
    spinner.fail(f"Error: {e}")
finally:
    spinner.stop()

Module reference

Animated spinner utilities for CLI feedback during long operations.

class kstlib.ui.spinner.SpinnerStyle(value)[source]

Bases: Enum

Predefined spinner animation families.

Each style defines a sequence of frames that cycle during animation.

BRAILLE = ('⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷')
DOTS = ('⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏')
LINE = ('|', '/', '-', '\\')
ARROW = ('←', '↖', '↑', '↗', '→', '↘', '↓', '↙')
BLOCKS = ('▁', '▂', '▃', '▄', '▅', '▆', '▇', '█', '▇', '▆', '▅', '▄', '▃', '▂')
CIRCLE = ('◐', '◓', '◑', '◒')
SQUARE = ('◰', '◳', '◲', '◱')
MOON = ('🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘')
CLOCK = ('🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚', '🕛')
class kstlib.ui.spinner.SpinnerPosition(value)[source]

Bases: Enum

Position of the spinner relative to the message text.

BEFORE = 'before'
AFTER = 'after'
class kstlib.ui.spinner.SpinnerAnimationType(value)[source]

Bases: Enum

Type of animation to display.

SPIN = 'spin'
BOUNCE = 'bounce'
COLOR_WAVE = 'color_wave'
class kstlib.ui.spinner.Spinner(message='', *, style=SpinnerStyle.BRAILLE, position=SpinnerPosition.BEFORE, animation_type=SpinnerAnimationType.SPIN, interval=0.08, spinner_style='cyan', text_style=None, done_character='✓', done_style='green', fail_character='✗', fail_style='red', console=None, file=None)[source]

Bases: object

Animated spinner for CLI feedback during long operations.

Supports multiple animation styles including character spinners, bouncing bars, and color wave effects. Can be used as a context manager or controlled manually.

Parameters:
  • message (str) – Text to display alongside the spinner.

  • style (SpinnerStyle | str) – Spinner animation style (SpinnerStyle enum or string name).

  • position (SpinnerPosition | str) – Where to place spinner relative to text (before/after).

  • animation_type (SpinnerAnimationType | str) – Type of animation (spin/bounce/color_wave).

  • interval (float) – Seconds between animation frames.

  • spinner_style (str | Style | None) – Rich style for the spinner character.

  • text_style (str | Style | None) – Rich style for the message text.

  • console (Console | None) – Optional Rich console instance.

  • file (IO[str] | None) – Output stream (defaults to sys.stderr).

Examples

Create a spinner with default settings:

>>> spinner = Spinner("Loading...")
>>> spinner.message
'Loading...'

Create with custom style:

>>> spinner = Spinner("Working", style=SpinnerStyle.DOTS)
>>> spinner = Spinner("Building", style="BLOCKS", interval=0.1)

Using as a context manager (terminal I/O):

>>> with Spinner("Processing...") as s:  
...     do_long_operation()
...     s.update("Almost done...")

Manual control:

>>> spinner = Spinner("Working...")  
>>> spinner.start()  
>>> spinner.stop(success=True)  
__init__(self, message: 'str' = '', *, style: 'SpinnerStyle | str' = <SpinnerStyle.BRAILLE: ('⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷')>, position: 'SpinnerPosition | str' = <SpinnerPosition.BEFORE: 'before'>, animation_type: 'SpinnerAnimationType | str' = <SpinnerAnimationType.SPIN: 'spin'>, interval: 'float' = 0.08, spinner_style: 'str | Style | None' = 'cyan', text_style: 'str | Style | None' = None, done_character: 'str' = '✓', done_style: 'str | Style | None' = 'green', fail_character: 'str' = '✗', fail_style: 'str | Style | None' = 'red', console: 'Console | None' = None, file: 'IO[str] | None' = None) 'None' -> None[source]

Initialize spinner with configuration.

classmethod from_preset(preset: 'str', message: 'str' = '', *, console: 'Console | None' = None, **overrides: 'Any') 'Spinner' -> Spinner[source]

Create a spinner from a named preset.

Parameters:
  • preset (str) – Name of the preset (e.g., “minimal”, “fancy”, “bounce”).

  • message (str) – Text to display alongside the spinner.

  • console (Console | None) – Optional Rich console instance.

  • **overrides (Any) – Additional parameters to override preset values.

Returns:

Configured Spinner instance.

Raises:

SpinnerError – If preset name is not found.

Return type:

Spinner

Examples

Create from a built-in preset:

>>> spinner = Spinner.from_preset("minimal", "Loading...")
>>> spinner = Spinner.from_preset("fancy", "Processing data")

Override preset values:

>>> spinner = Spinner.from_preset("bounce", "Building", interval=0.05)

Invalid preset raises error:

>>> Spinner.from_preset("nonexistent")  
Traceback (most recent call last):
...
kstlib.ui.exceptions.SpinnerError: Unknown preset 'nonexistent'. ...
start(self) 'None' -> None[source]

Start the spinner animation in a background thread.

stop(self, *, success: 'bool' = True, final_message: 'str | None' = None) 'None' -> None[source]

Stop the spinner animation.

Parameters:
  • success (bool) – If True, show done character; if False, show fail character.

  • final_message (str | None) – Optional message to display after stopping.

update(self, message: 'str') 'None' -> None[source]

Update the spinner message while running.

Parameters:

message (str) – New message to display.

log(self, message: 'str', style: 'str | None' = None) 'None' -> None[source]

Print a message above the spinner without disrupting animation.

Use this to display logs, progress info, or any output while the spinner continues running on the bottom line.

Parameters:
  • message (str) – Text to print above the spinner.

  • style (str | None) – Optional Rich style for the message.

property message: str

Current spinner message.

__enter__(self) 'Self' -> Self[source]

Start spinner when entering context.

__exit__(self, exc_type: 'type[BaseException] | None', exc_val: 'BaseException | None', exc_tb: 'types.TracebackType | None') 'None' -> None[source]

Stop spinner when exiting context.

kstlib.ui.spinner.with_spinner(message: 'str' = 'Processing...', *, style: 'SpinnerStyle | str' = <SpinnerStyle.BRAILLE: ('⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷')>, log_style: 'str | None' = 'dim', capture_prints: 'bool' = True, log_zone_height: 'int | None' = None, **spinner_kwargs: 'Any') 'Callable[[Callable[P, R]], Callable[P, R]]' -> Callable[[Callable[P, R]], Callable[P, R]][source]

Wrap a function with a spinner, capturing its prints.

Parameters:
  • message (str) – Spinner message to display.

  • style (SpinnerStyle | str) – Spinner animation style.

  • log_style (str | None) – Style for captured print output (None for no style).

  • capture_prints (bool) – If True, redirect stdout to spinner.log().

  • log_zone_height (int | None) – If set, use SpinnerWithLogZone with fixed height. The spinner stays at top, logs scroll in bounded zone below.

  • **spinner_kwargs (Any) – Additional arguments passed to Spinner.

Returns:

Decorated function.

Return type:

Callable[[Callable[P, R]], Callable[P, R]]

Examples

Basic decorator usage (terminal I/O):

>>> @with_spinner("Loading data...")  
... def load_data():
...     return {"data": [1, 2, 3]}
>>> result = load_data()  

With log capture (prints appear above spinner):

>>> @with_spinner("Processing...", log_style="cyan")  
... def process():
...     print("Step 1 complete")  # Appears above spinner
...     print("Step 2 complete")
...     return True

Fixed log zone with bounded scrolling:

>>> @with_spinner("Building...", log_zone_height=5)  
... def build():
...     for i in range(10):
...         print(f"Step {i}")  # Scrolls in 5-line zone
...     return True
class kstlib.ui.spinner.SpinnerWithLogZone(message='', *, log_zone_height=10, style=SpinnerStyle.BRAILLE, spinner_style='cyan', console=None, file=None, interval=0.08)[source]

Bases: object

Spinner with a fixed position and a scrollable log zone.

The spinner stays fixed at the top while logs scroll in a zone below. When the zone is full, old logs are pushed out automatically.

Parameters:
  • message (str) – Spinner message.

  • log_zone_height (int) – Number of lines for the log zone (default 10).

  • style (SpinnerStyle | str) – Spinner animation style.

  • spinner_style (str | None) – Rich style for spinner character.

  • console (Console | None) – Optional Rich console.

  • **kwargs – Additional Spinner arguments.

Examples

Create with custom log zone height:

>>> sz = SpinnerWithLogZone("Building...", log_zone_height=5)
>>> sz._log_zone_height
5

Usage as context manager (terminal I/O):

>>> with SpinnerWithLogZone("Processing", log_zone_height=3) as sz:  
...     sz.log("Step 1 done")
...     sz.log("Step 2 done")
...     sz.update("Almost finished...")
__init__(self, message: 'str' = '', *, log_zone_height: 'int' = 10, style: 'SpinnerStyle | str' = <SpinnerStyle.BRAILLE: ('⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷')>, spinner_style: 'str | None' = 'cyan', console: 'Console | None' = None, file: 'IO[str] | None' = None, interval: 'float' = 0.08) 'None' -> None[source]

Initialize the spinner with a fixed-height log zone and the usual Spinner options.

start(self) 'None' -> None[source]

Start the spinner animation.

stop(self, *, success: 'bool' = True, final_message: 'str | None' = None) 'None' -> None[source]

Stop the spinner and clean up the display.

update(self, message: 'str') 'None' -> None[source]

Update the spinner message.

log(self, message: 'str', style: 'str | None' = None) 'None' -> None[source]

Add a log entry to the scrolling zone.

__enter__(self) 'Self' -> Self[source]

Start spinner when entering context.

__exit__(self, exc_type: 'type[BaseException] | None', exc_val: 'BaseException | None', exc_tb: 'types.TracebackType | None') 'None' -> None[source]

Stop spinner when exiting context.