~/ ~/documents ~/software ~/pictures ~/harmful.txt github (opens in new tab)

Spinner

github:dominicegginton/spinner

A terminal spinner library for Swift command-line tools. Spinner gives clear progress feedback during long-running tasks, with flexible patterns, formatting, and output control.

Features

Install

Add Spinner to your Package.swift dependencies:

.package(url: "https://github.com/dominicegginton/Spinner", from: "1.0.0")

Usage

Create, start, and stop a spinner:

import Foundation
import Spinner

let spinner = Spinner(.dots, "Building release artifacts")
spinner.start()

sleep(2) // do work

spinner.stop()

Use helper completion states:

spinner.success("Build complete")
spinner.error("Build failed")
spinner.warning("Build completed with warnings")
spinner.info("Build cache reused")

Custom Formats

Spinner supports custom render formats where:

This makes it straightforward to display elapsed time in CLI workflows, for example by using a format like {D} {T} - {S}.

CustomStreams

Spinner wraps output in the SpinnerStream protocol, so you can direct spinner rendering to your own output implementation instead of the default STDOUT stream.

This is useful when integrating Spinner with other CLI frameworks that expose their own writable stream types.

import Spinner

struct SwiftCLISpinnerStream: SpinnerStream {
    private let stdout: WritableStream

    init(stdout: WritableStream) {
        self.stdout = stdout
    }

    func write(string: String, terminator: String) {
        stdout.write(string, terminator: terminator)
    }

    func hideCursor() {
        stdout.write("\u{001B}[?25l", terminator: "")
    }

    func showCursor() {
        stdout.write("\u{001B}[?25h", terminator: "")
    }
}

let spinner = Spinner(.dots, "Deploying", stream: SwiftCLISpinnerStream(stdout: stdout))

The key requirement is to implement write, hideCursor, and showCursor so Spinner can render frames correctly and restore terminal cursor state when finished.

Caveat

Spinner uses a signal trap by default so that if the process is interrupted (for example with Ctrl+C), it can restore cursor visibility before exit.

If your CLI already manages signals, this can conflict with existing handlers. In that case, provide your own SpinnerSignal implementation and pass it to the spinner.

import Spinner
import Signals

struct CustomSpinnerSignal: SpinnerSignal {
    func trap() {
        Signals.trap(signal: .int) { _ in
            print("\u{001B}[?25h", terminator: "") // show cursor
            exit(0)
        }
    }
}

let spinner = Spinner(.dots, "Deploying", signal: CustomSpinnerSignal())

This lets Spinner integrate cleanly with projects that need strict control over process signal behavior.