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.
success,
error, warning, info)SpinnerStreamAdd Spinner to your Package.swift dependencies:
.package(url: "https://github.com/dominicegginton/Spinner", from: "1.0.0")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")Spinner supports custom render formats where:
{S} renders the animated spinner frame{T} renders the text message{D} renders elapsed duration since startThis makes it straightforward to display elapsed time in CLI
workflows, for example by using a format like
{D} {T} - {S}.
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.
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.