Claude Code for Halo: Terminal Spinner in Python — Claude Skills 360 Blog
Blog / AI / Claude Code for Halo: Terminal Spinner in Python
AI

Claude Code for Halo: Terminal Spinner in Python

Published: March 21, 2028
Read time: 5 min read
By: Claude Skills 360

Halo renders animated terminal spinners for long-running CLI operations. pip install halo. Basic: from halo import Halo; spinner = Halo(text="Loading", spinner="dots"); spinner.start(); time.sleep(2); spinner.succeed("Done!"). Context manager: with Halo(text="Processing", spinner="dots") as sp: do_work(). Succeed: sp.succeed("Finished"). Fail: sp.fail("Error"). Warn: sp.warn("Skipped"). Info: sp.info("Note"). Stop custom: sp.stop_and_persist(symbol="★", text="Custom"). Update text: sp.text = "Step 2 of 3". Spinners: dots, dots2–12, line, pipe, star, star2, flip, hamburger, growVertical, growHorizontal, balloon, balloon2, noise, bounce, boxBounce, triangle, arc, circle, squareCorners, circleCorners, squish, toggle, toggle2–13, arrow, arrow2–3, bouncingBar, bouncingBall, smiley, monkey, hearts, clock, earth, moon, runner, pong, shark, weather, christmas, grenade, point, layer. Color: color=“red”|“green”|“yellow”|“blue”|“magenta”|“cyan”|“white”. Indent: Halo(indent=4). Enabled: Halo(enabled=sys.stdout.isatty()) — disable in CI. Stream: defaults to sys.stderr. spinner.clear(). spinner.render(). Decorator: @Halo(text="Loading"). Subprocesses: with Halo("Cloning...") as sp: subprocess.run(["git","clone","..."],[...]). HTTP: with Halo("Fetching"): resp = requests.get(url). Nested: use indent + manual start/stop. Claude Code generates Halo spinner wrappers for CLI tools, deploy scripts, and long-running operations.

CLAUDE.md for Halo

## Halo Stack
- Version: halo >= 0.0.31 | pip install halo
- Basic: with Halo(text="Loading", spinner="dots") as sp: work(); sp.succeed("Done")
- Spinners: dots, clock, earth, moon, pong, arc, bounce, bouncingBall, shark — 70+ styles
- Outcomes: sp.succeed(text) | sp.fail(text) | sp.warn(text) | sp.info(text)
- Custom end: sp.stop_and_persist(symbol="✓", text="Deployed!")
- CI safe: Halo(enabled=sys.stdout.isatty()) — silences in non-TTY
- Decorator: @Halo(text="Processing") wraps entire function

Halo Terminal Spinner Pipeline

# app/spinners.py — Halo spinner utilities for CLI tools and long-running operations
from __future__ import annotations

import subprocess
import sys
import time
from contextlib import contextmanager
from typing import Any, Callable, TypeVar

from halo import Halo

T = TypeVar("T")

# ─────────────────────────────────────────────────────────────────────────────
# 1. Core spinner factory
# ─────────────────────────────────────────────────────────────────────────────

def make_spinner(
    text: str = "Working",
    spinner: str = "dots",
    color: str = "cyan",
    indent: int = 0,
    enabled: bool | None = None,
) -> Halo:
    """
    Create a Halo spinner with sensible defaults.
    enabled: None → auto-detect TTY; True/False → force.
    """
    is_enabled = sys.stdout.isatty() if enabled is None else enabled
    return Halo(
        text=text,
        spinner=spinner,
        color=color,
        indent=indent,
        enabled=is_enabled,
        stream=sys.stderr,
    )


@contextmanager
def spin(
    text: str = "Working",
    spinner: str = "dots",
    color: str = "cyan",
    success: str | None = None,
    failure: str | None = None,
    indent: int = 0,
):
    """
    Context manager that starts a spinner and auto-succeeds/fails.

    Usage:
        with spin("Fetching data") as sp:
            data = requests.get(url).json()
        # → auto-calls sp.succeed() on exit

        with spin("Uploading", failure="Upload failed") as sp:
            upload()
    """
    sp = make_spinner(text, spinner=spinner, color=color, indent=indent)
    sp.start()
    try:
        yield sp
        sp.succeed(success or text)
    except Exception as exc:
        sp.fail(failure or f"Failed: {exc}")
        raise


# ─────────────────────────────────────────────────────────────────────────────
# 2. Step runner — multi-step CLI pipelines
# ─────────────────────────────────────────────────────────────────────────────

def run_steps(
    steps: list[tuple[str, Callable[[], Any]]],
    spinner: str = "dots",
    color: str = "cyan",
    stop_on_error: bool = True,
) -> list[dict[str, Any]]:
    """
    Run a sequence of labeled steps, showing a spinner for each.

    steps: [("Downloading", download_fn), ("Installing", install_fn), ...]
    Returns list of {"step", "ok", "result"|"error"} dicts.
    """
    results = []
    for label, fn in steps:
        sp = make_spinner(label, spinner=spinner, color=color)
        sp.start()
        try:
            result = fn()
            sp.succeed(label)
            results.append({"step": label, "ok": True, "result": result})
        except Exception as exc:
            sp.fail(f"{label}: {exc}")
            results.append({"step": label, "ok": False, "error": str(exc)})
            if stop_on_error:
                break
    return results


def step(label: str, fn: Callable[[], T], spinner: str = "dots") -> T:
    """
    Run a single labeled function with a spinner.
    Returns fn()'s return value.
    Raises on failure after showing the fail state.
    """
    with spin(label, spinner=spinner) as sp:
        result = fn()
    return result


# ─────────────────────────────────────────────────────────────────────────────
# 3. Subprocess helpers
# ─────────────────────────────────────────────────────────────────────────────

def run_command(
    cmd: list[str],
    text: str | None = None,
    check: bool = True,
    capture_output: bool = True,
    spinner: str = "dots",
) -> subprocess.CompletedProcess:
    """
    Run a subprocess with a spinner.
    text: spinner label; defaults to the command string.
    """
    label = text or " ".join(cmd)
    sp = make_spinner(label, spinner=spinner)
    sp.start()
    try:
        result = subprocess.run(
            cmd,
            check=check,
            capture_output=capture_output,
            text=True,
        )
        sp.succeed(label)
        return result
    except subprocess.CalledProcessError as exc:
        sp.fail(f"{label} (exit {exc.returncode})")
        raise


def run_commands(
    commands: list[tuple[str, list[str]]],
    stop_on_error: bool = True,
) -> bool:
    """
    Run a sequence of (label, command) pairs with spinners.
    Returns True if all succeeded.
    """
    for label, cmd in commands:
        sp = make_spinner(label)
        sp.start()
        try:
            subprocess.run(cmd, check=True, capture_output=True, text=True)
            sp.succeed(label)
        except subprocess.CalledProcessError as exc:
            sp.fail(f"{label} (exit {exc.returncode})")
            if stop_on_error:
                return False
    return True


# ─────────────────────────────────────────────────────────────────────────────
# 4. Nested spinners (indented)
# ─────────────────────────────────────────────────────────────────────────────

class NestedProgress:
    """
    Two-level spinner: a parent task and child sub-tasks.

    Usage:
        with NestedProgress("Deploying") as p:
            p.sub("Building image")
            build()
            p.sub_done()
            p.sub("Pushing to registry")
            push()
            p.sub_done()
    """

    def __init__(self, label: str, spinner: str = "dots", color: str = "cyan"):
        self._label = label
        self._spinner = spinner
        self._color = color
        self._parent: Halo | None = None
        self._child: Halo | None = None

    def __enter__(self) -> "NestedProgress":
        self._parent = make_spinner(self._label, spinner=self._spinner, color=self._color)
        self._parent.start()
        return self

    def sub(self, text: str) -> None:
        """Start a child spinner (indented 4 spaces)."""
        if self._child:
            self._child.succeed(self._child.text or "")
        self._child = make_spinner(text, spinner=self._spinner, color="white", indent=4)
        self._child.start()

    def sub_done(self, text: str | None = None) -> None:
        """Succeed the current child spinner."""
        if self._child:
            self._child.succeed(text or self._child.text or "")
            self._child = None

    def sub_fail(self, text: str | None = None) -> None:
        if self._child:
            self._child.fail(text or self._child.text or "")
            self._child = None

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self._child:
            if exc_type:
                self._child.fail()
            else:
                self._child.succeed()
        if self._parent:
            if exc_type:
                self._parent.fail(f"{self._label}: failed")
            else:
                self._parent.succeed(self._label)
        return False


# ─────────────────────────────────────────────────────────────────────────────
# 5. Spinner styles catalog
# ─────────────────────────────────────────────────────────────────────────────

SPINNER_STYLES: dict[str, str] = {
    "default":  "dots",
    "simple":   "line",
    "clock":    "clock",
    "earth":    "earth",
    "moon":     "moon",
    "pong":     "pong",
    "bounce":   "bouncingBall",
    "shark":    "shark",
    "hearts":   "hearts",
    "arc":      "arc",
    "weather":  "weather",
    "runner":   "runner",
}


def demo_spinners(duration: float = 1.0) -> None:
    """Show each built-in spinner style briefly."""
    for name, style in SPINNER_STYLES.items():
        sp = make_spinner(f"Style: {name}", spinner=style)
        sp.start()
        time.sleep(duration)
        sp.succeed(f"Style: {name}")


# ─────────────────────────────────────────────────────────────────────────────
# 6. HTTP helpers
# ─────────────────────────────────────────────────────────────────────────────

def http_get(url: str, text: str | None = None, **kwargs) -> Any:
    """GET a URL with a spinner. Returns response.json() on 200."""
    try:
        import requests
    except ImportError as exc:
        raise ImportError("pip install requests") from exc

    label = text or f"GET {url}"
    sp = make_spinner(label)
    sp.start()
    try:
        resp = requests.get(url, timeout=kwargs.pop("timeout", 30), **kwargs)
        resp.raise_for_status()
        sp.succeed(label)
        return resp.json() if "json" in resp.headers.get("content-type", "") else resp.text
    except Exception as exc:
        sp.fail(f"{label}: {exc}")
        raise


# ─────────────────────────────────────────────────────────────────────────────
# 7. Typer / Click integration
# ─────────────────────────────────────────────────────────────────────────────

def typer_spin(text: str, spinner: str = "dots"):
    """
    Decorator for Typer/Click commands that wraps the function body in a spinner.

    Usage:
        @app.command()
        @typer_spin("Deploying app")
        def deploy(name: str):
            run_deploy(name)
    """
    import functools

    def decorator(fn: Callable) -> Callable:
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            with spin(text, spinner=spinner):
                return fn(*args, **kwargs)
        return wrapper
    return decorator


# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    print("=== Basic spinner ===")
    with spin("Loading config", success="Config loaded") as sp:
        time.sleep(0.5)

    print("\n=== Step runner ===")
    results = run_steps([
        ("Checking environment", lambda: time.sleep(0.3)),
        ("Downloading assets",   lambda: time.sleep(0.4)),
        ("Building project",     lambda: time.sleep(0.3)),
    ])
    for r in results:
        print(f"  {r['step']}: {'OK' if r['ok'] else 'FAIL'}")

    print("\n=== Nested progress ===")
    with NestedProgress("Full deploy pipeline") as p:
        p.sub("Building Docker image")
        time.sleep(0.3)
        p.sub_done()
        p.sub("Pushing to registry")
        time.sleep(0.3)
        p.sub_done()
        p.sub("Restarting services")
        time.sleep(0.2)
        p.sub_done()

    print("\n=== Manual succeed/fail/warn/info ===")
    sp = make_spinner("Checking database")
    sp.start()
    time.sleep(0.3)
    sp.succeed("Database reachable")

    sp2 = make_spinner("Running migration")
    sp2.start()
    time.sleep(0.3)
    sp2.warn("Migration already applied — skipped")

    sp3 = make_spinner("Validating schema")
    sp3.start()
    time.sleep(0.2)
    sp3.info("Schema version: 42")

    print("\n=== stop_and_persist ===")
    sp4 = make_spinner("Deploying to production")
    sp4.start()
    time.sleep(0.4)
    sp4.stop_and_persist(symbol="🚀", text="Deployed to production!")

    print("\n=== Demo spinners (0.5s each) ===")
    for name, style in list(SPINNER_STYLES.items())[:4]:
        sp = make_spinner(f"Style: {name}", spinner=style)
        sp.start()
        time.sleep(0.5)
        sp.succeed(f"Style: {name}")

For the yaspin alternative — yaspin is similar to Halo but supports a wider range of spinner styles through the yaspin.spinners.Spinners enum, has a SigIntProof context that suppresses keyboard interrupts during cleanup, and uses a thread-based approach where Halo uses a separate thread too but yaspin exposes yaspin.api.Yaspin.write() for printing lines without breaking the spinner animation; both are good choices, with Halo being slightly simpler API and yaspin offering more spinner customization. For the rich.status alternative — rich.Status integrates directly into rich markup, supports nested Console.status() contexts cleanly, and inherits all of rich’s colour and styling; Halo works independently of rich and outputs to stderr by default, which is useful when stdout must stay clean for piping — choose rich.status when your CLI already uses rich, Halo when you want a standalone lightweight spinner. The Claude Skills 360 bundle includes Halo skill sets covering Halo() constructor with text/spinner/color/indent, succeed()/fail()/warn()/info() outcomes, stop_and_persist() custom symbol, context manager usage, make_spinner() factory, spin() context manager with auto-result, run_steps() multi-step pipeline, step() single labeled operation, run_command()/run_commands() subprocess wrappers, NestedProgress two-level indented spinners, http_get() with spinner, and typer_spin() decorator. Start with the free tier to try terminal spinner code generation.

Keep Reading

AI

Claude Code for email.contentmanager: Python Email Content Accessors

Read and write EmailMessage body content with Python's email.contentmanager module and Claude Code — email contentmanager ContentManager for the class that maps content types to get and set handler functions allowing EmailMessage to support get_content and set_content with type-specific behaviour, email contentmanager raw_data_manager for the ContentManager instance that handles raw bytes and str payloads without any conversion, email contentmanager content_manager for the standard ContentManager instance used by email.policy.default that intelligently handles text plain text html multipart and binary content types, email contentmanager get_content_text for the handler that returns the decoded text payload of a text-star message part as a str, email contentmanager get_content_binary for the handler that returns the raw decoded bytes payload of a non-text message part, email contentmanager get_data_manager for the get-handler lookup used by EmailMessage get_content to find the right reader function for the content type, email contentmanager set_content text for the handler that creates and sets a text part correctly choosing charset and transfer encoding, email contentmanager set_content bytes for the handler that creates and sets a binary part with base64 encoding and optional filename Content-Disposition, email contentmanager EmailMessage get_content for the method that reads the message body using the registered content manager handlers, email contentmanager EmailMessage set_content for the method that sets the message body and MIME headers in one call, email contentmanager EmailMessage make_alternative make_mixed make_related for the methods that convert a simple message into a multipart container, email contentmanager EmailMessage add_attachment for the method that attaches a file or bytes to a multipart message, and email contentmanager integration with email.message and email.policy and email.mime and io for building high-level email readers attachment extractors text body accessors HTML readers and policy-aware MIME construction pipelines.

5 min read Feb 12, 2029
AI

Claude Code for email.charset: Python Email Charset Encoding

Control header and body encoding for international email with Python's email.charset module and Claude Code — email charset Charset for the class that wraps a character set name with the encoding rules for header encoding and body encoding describing how to encode text for that charset in email messages, email charset Charset header_encoding for the attribute specifying whether headers using this charset should use QP quoted-printable encoding BASE64 encoding or no encoding, email charset Charset body_encoding for the attribute specifying the Content-Transfer-Encoding to use for message bodies in this charset such as QP or BASE64, email charset Charset output_codec for the attribute giving the Python codec name used to encode the string to bytes for the wire format, email charset Charset input_codec for the attribute giving the Python codec name used to decode incoming bytes to str, email charset Charset get_output_charset for returning the output charset name, email charset Charset header_encode for encoding a header string using the charset's header_encoding method, email charset Charset body_encode for encoding body content using the charset's body_encoding, email charset Charset convert for converting a string from the input_codec to the output_codec, email charset add_charset for registering a new charset with custom encoding rules in the global charset registry, email charset add_alias for adding an alias name that maps to an existing registered charset, email charset add_codec for registering a codec name mapping for use by the charset machinery, and email charset integration with email.message and email.mime and email.policy and email.encoders for building international email senders non-ASCII header encoders Content-Transfer-Encoding selectors charset-aware message constructors and MIME encoding pipelines.

5 min read Feb 11, 2029
AI

Claude Code for email.utils: Python Email Address and Header Utilities

Parse and format RFC 2822 email addresses and dates with Python's email.utils module and Claude Code — email utils parseaddr for splitting a display-name plus angle-bracket address string into a realname and email address tuple, email utils formataddr for combining a realname and address string into a properly quoted RFC 2822 address with angle brackets, email utils getaddresses for parsing a list of raw address header strings each potentially containing multiple comma-separated addresses into a list of realname address tuples, email utils parsedate for parsing an RFC 2822 date string into a nine-tuple compatible with time.mktime, email utils parsedate_tz for parsing an RFC 2822 date string into a ten-tuple that includes the UTC offset timezone in seconds, email utils parsedate_to_datetime for parsing an RFC 2822 date string into an aware datetime object with timezone, email utils formatdate for formatting a POSIX timestamp or the current time as an RFC 2822 date string with optional usegmt and localtime flags, email utils format_datetime for formatting a datetime object as an RFC 2822 date string, email utils make_msgid for generating a globally unique Message-ID string with optional idstring and domain components, email utils decode_rfc2231 for decoding an RFC 2231 encoded parameter value into a tuple of charset language and value, email utils encode_rfc2231 for encoding a string as an RFC 2231 encoded parameter value, email utils collapse_rfc2231_value for collapsing a decoded RFC 2231 tuple to a Unicode string, and email utils integration with email.message and email.headerregistry and datetime and time for building address parsers date formatters message-id generators header extractors and RFC-compliant email construction utilities.

5 min read Feb 10, 2029

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free