rvy/spin
Terminal spinners with dynamic text updates and CI support.
spin
Terminal spinners for the Lune Runtime with dynamic text updates and CI support.
Features
- Dynamic text updates - Change spinner text while running
- CI-aware - Automatically detects CI environments and adapts output
- Custom formatters - Style your spinner output however you want
- 89 pre-built animations - From minimal dots to elaborate emoji sequences
- Type-safe - Full Luau type definitions included
Installation
pesde add revvy02/spin
Quick Start
local spin = require('@pkg/spin')
-- Simple: auto-managed with callback
spin.wrap(spin.animations.dots, function(setText)
setText("Loading...")
task.wait(1)
setText("Almost done...")
task.wait(1)
end)
print("Complete!")
API Reference
spin.animations
Table of 89 pre-built animations. Default is animations.line.
Examples: dots, line, arc, arrow, circle, bounce, earth, moon, hearts
spin.render(animation, formatter?)
Creates a reusable renderer with optional custom formatting.
Parameters:
animation: spinner- The spinner animation to useformatter?: (frame: string, text: string) -> string- Custom output formatter
Returns: Renderer
Example:
local renderer = spin.render(spin.animations.dots, function(frame, text)
return "[" .. frame .. "] " .. text
end)
spin.start(renderer, initialText?, ci?)
Starts a spinner with manual control. Returns functions to update text and stop.
Parameters:
renderer: Renderer | spinner- Renderer object or raw animationinitialText?: string- Initial text to displayci?: boolean- Override CI detection (defaults toprocess.env.CI)
Returns: (setText, stop) where:
setText: (text: string) -> ()- Update the displayed textstop: () -> ()- Stop and clear the spinner
Example:
local setText, stop = spin.start(spin.animations.dots, "Starting...")
task.wait(1)
setText("Loading...")
task.wait(1)
setText("Processing...")
task.wait(1)
stop()
spin.wrap(renderer, callback, ci?)
Auto-managed spinner that runs a callback with text control, then automatically stops.
Parameters:
renderer: Renderer | spinner- Renderer object or raw animationcallback: (setText: (text: string) -> ()) -> R...- Function that receivessetTextci?: boolean- Override CI detection
Returns: All values returned by the callback
Example:
local result = spin.wrap(spin.animations.arc, function(setText)
setText("Step 1/3")
task.wait(0.5)
setText("Step 2/3")
task.wait(0.5)
setText("Step 3/3")
task.wait(0.5)
return "Done!"
end)
print(result) -- "Done!"
Usage Examples
Basic Usage
local spin = require('@pkg/spin')
-- Simplest form: just show a spinner during work
spin.wrap(spin.animations.dots, function(setText)
setText("Working...")
-- do your work here
task.wait(2)
end)
Multi-Step Process
local spin = require('@pkg/spin')
spin.wrap(spin.animations.dots, function(setText)
setText("Downloading dependencies...")
-- download code
task.wait(1)
setText("Installing packages...")
-- install code
task.wait(1)
setText("Building project...")
-- build code
task.wait(1)
end)
print("Build complete!")
Manual Control
local spin = require('@pkg/spin')
local setText, stop = spin.start(spin.animations.line)
-- Update text dynamically as your code runs
for i = 1, 10 do
setText(`Processing item {i}/10...`)
-- process item
task.wait(0.5)
end
stop()
print("All items processed!")
Custom Formatter
local spin = require('@pkg/spin')
local renderer = spin.render(spin.animations.dots, function(frame, text)
return `[{frame}] {text} [{os.clock()}s]`
end)
spin.wrap(renderer, function(setText)
setText("Running...")
task.wait(2)
end)
With Return Values
local spin = require('@pkg/spin')
local data = spin.wrap(spin.animations.arc, function(setText)
setText("Fetching data...")
task.wait(1)
return { users = 42, active = 17 }
end)
print(`Found {data.users} users, {data.active} active`)
Error Handling
local spin = require('@pkg/spin')
local success, result = pcall(function()
return spin.wrap(spin.animations.dots, function(setText)
setText("Attempting operation...")
if math.random() > 0.5 then
error("Something went wrong!")
end
return "Success!"
end)
end)
if success then
print("Result:", result)
else
print("Error:", result)
end
Custom Animation
local spin = require('@pkg/spin')
local customSpinner = {
interval = 100, -- milliseconds
frames = { "▹▹▹▹▹", "▸▹▹▹▹", "▸▸▹▹▹", "▸▸▸▹▹", "▸▸▸▸▹", "▸▸▸▸▸" }
}
spin.wrap(customSpinner, function(setText)
setText("Loading with custom animation...")
task.wait(2)
end)
Available Animations
The library includes 89 pre-built animations from cli-spinners.
Character-based:
dots, dots2, dots3, line, line2, arc, arrow, circle, circleQuarters, circleHalves, squareCorners, triangle, bounce, bouncingBar, bouncingBall, toggle, star, star2, flip, hamburger, growVertical, growHorizontal, pipe, simpleDots, simpleDotsScrolling, aesthetic, dwarfFortress, material, and many more
Emoji-based:
earth, moon, runner, pong, shark, hearts, clock, monkey, smiley, christmas, grenade, point, layer
Default: spin.animations.line
Types
type spinner = {
frames: { string }, -- Array of animation frames
interval: number, -- Frame duration in milliseconds
}
type Renderer = {
animation: spinner,
formatter: ((frame: string, text: string) -> string)?,
}
CI/CD Support
The start() and wrap() functions automatically detect CI environments by checking process.env.CI:
- In CI mode: Animations are disabled, only text updates are printed
- In terminal mode: Full animations are shown
- Override: Pass
ciparameter to force a specific mode
-- Force CI mode even in terminal
spin.wrap(spin.animations.dots, function(setText)
setText("Running tests...")
end, true)
How It Works
This library uses ANSI escape sequences (\x1b[2K\r) to clear and rewrite the current line. This approach:
- Works reliably across all modern terminals
- Handles Unicode/emoji correctly
- Resistant to cursor position changes
- Properly cleans up on completion
Contributing
Issues and pull requests are welcome! Terminal tools can be tricky, so any improvements to compatibility or features are appreciated.
Credits
All default animations adapted from cli-spinners by Sindre Sorhus.
License
MIT