Trending-skills rattles-terminal-spinners
Minimal terminal spinner library for Rust with preset collection and no-std support
install
source · Clone the upstream repo
git clone https://github.com/Aradotso/trending-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Aradotso/trending-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/rattles-terminal-spinners" ~/.claude/skills/aradotso-trending-skills-rattles-terminal-spinners && rm -rf "$T"
manifest:
skills/rattles-terminal-spinners/SKILL.mdsource content
Rattles Terminal Spinners
Skill by ara.so — Daily 2026 Skills collection.
Rattles is a minimal, zero-dependency Rust library for terminal spinners. It has no runtime or lifecycle — spinners are constructed directly in render loops with negligible cost. Supports
no_std environments.
Installation
# Cargo.toml [dependencies] rattles = "0.1" # with std (default) # no_std rattles = { version = "0.1", default-features = false }
Or via CLI:
cargo add rattles # no_std variant cargo add rattles --no-default-features
Core Concepts
- Rattler: a spinner definition (frames + interval). Stateless and cheap to construct.
- TickedRattler: stateful wrapper for tick-based driving (required in
).no_std - Presets: built-in spinners organized by category.
macro: define custom spinners at compile time.rattle!
Basic Usage (std)
use std::{io::Write, time::Duration}; use rattles::presets::prelude as presets; fn main() { let rattle = presets::dots(); loop { print!("\r{}", rattle.current_frame()); std::io::stdout().flush().unwrap(); std::thread::sleep(Duration::from_millis(80)); } }
current_frame() uses the system clock internally — no state needed.
Preset Categories
use rattles::presets::{arrows, ascii, braille, emoji}; use rattles::presets::prelude as presets; // re-exports all presets // Arrows let s = arrows::arrow(); let s = arrows::arrow2(); // ASCII let s = ascii::line(); let s = ascii::pipe(); // Braille let s = braille::dots(); let s = braille::dots2(); // Emoji let s = emoji::earth(); let s = emoji::clock(); // Prelude examples let s = presets::waverows(); let s = presets::dots();
Rattler API
use rattles::presets::prelude as presets; use std::time::Duration; let rattle = presets::dots(); // Get frame based on system clock (std only) let frame: &str = rattle.current_frame(); // Get frame at specific elapsed duration (std + no_std) let frame = rattle.frame_at(Duration::from_millis(500)); // Get frame by index let frame = rattle.frame(3); // Change animation interval let rattle = presets::dots().set_interval(Duration::from_millis(50)); // Reverse direction let rattle = presets::waverows().reverse(); // Convert to tick-based (stateful) let mut ticked = presets::dots().into_ticked();
TickedRattler (Stateful / no_std-friendly)
use rattles::presets::prelude as presets; let mut rattle = presets::dots().into_ticked(); loop { rattle.tick(); let frame = rattle.current_frame(); // render frame... }
TickedRattler must be stored (it holds state). Suitable for no_std contexts where the global clock is unavailable.
Index-Based Animation (no_std)
use rattles::presets::prelude as presets; let rattle = presets::dots(); let mut i = 0usize; loop { let frame = rattle.frame(i); i = i.wrapping_add(1); // render frame... }
Time-Based Animation with External Clock (no_std)
use rattles::presets::prelude as presets; use core::time::Duration; let rattle = presets::dots(); // elapsed comes from your platform's timer let elapsed: Duration = get_elapsed(); // your implementation let frame = rattle.frame_at(elapsed);
Custom Spinners with rattle!
Macro
rattle!use rattles::rattle; rattle!( MySpinner, // generated struct name my_spinner, // generated constructor function name 1, // row count (width of spinner) 120, // interval in milliseconds ["⣾", "⣷", "⣯", "⣟", "⣻", "⣽"] // keyframes ); // Use it like any preset let s = my_spinner(); println!("{}", s.current_frame());
Multi-row custom spinner:
rattle!( Wide, wide_spinner, 3, // 3 characters wide 80, ["[ ]", "[= ]", "[== ]", "[===]", "[ ==]", "[ =]"] );
Ratatui Integration
// examples/ratatui.rs pattern use rattles::presets::prelude as presets; use ratatui::{ backend::CrosstermBackend, widgets::Paragraph, Terminal, }; fn ui(frame: &mut ratatui::Frame, rattle: &rattles::Rattler) { let spinner_text = rattle.current_frame(); let paragraph = Paragraph::new(format!("{} Loading...", spinner_text)); frame.render_widget(paragraph, frame.size()); } fn main() -> std::io::Result<()> { let rattle = presets::dots(); // standard ratatui event loop loop { terminal.draw(|f| ui(f, &rattle))?; std::thread::sleep(std::time::Duration::from_millis(16)); // break on user input... } Ok(()) }
Since
Rattler is stateless, pass it by reference anywhere — no Arc<Mutex<>> needed.
no_std Setup
[dependencies] rattles = { version = "0.1", default-features = false }
#![no_std] use rattles::presets::prelude as presets; // Option 1: tick-based let mut rattle = presets::dots().into_ticked(); rattle.tick(); let frame = rattle.current_frame(); // Option 2: index-based let rattle = presets::dots(); let frame = rattle.frame(42); // Option 3: duration-based (external clock) let rattle = presets::dots(); let frame = rattle.frame_at(core::time::Duration::from_millis(840));
Common Patterns
Spinner with message
use rattles::presets::prelude as presets; use std::{io::Write, time::Duration}; fn main() { let rattle = presets::dots(); let message = "Fetching data..."; loop { print!("\r{} {}", rattle.current_frame(), message); std::io::stdout().flush().unwrap(); std::thread::sleep(Duration::from_millis(80)); } }
Async-compatible (tokio)
use rattles::presets::prelude as presets; use tokio::time::{sleep, Duration}; #[tokio::main] async fn main() { let rattle = presets::dots(); let spinner = tokio::spawn(async move { loop { print!("\r{}", rattle.current_frame()); std::io::stdout().flush().unwrap(); sleep(Duration::from_millis(80)).await; } }); // do your async work do_work().await; spinner.abort(); println!("\rDone! "); }
Collecting all frames
let rattle = presets::dots(); let frames: Vec<&str> = (0..rattle.frame_count()) .map(|i| rattle.frame(i)) .collect();
Troubleshooting
Spinner not animating (stuck on first frame)
- Ensure you're flushing stdout:
std::io::stdout().flush().unwrap() - Use
to overwrite the line, not\r\n - The sleep interval should match or be shorter than the spinner's interval
not available in no_stdcurrent_frame()
- Use
,frame_at(duration)
, orframe(index)
insteadinto_ticked() - Disable default features:
rattles = { version = "...", default-features = false }
Custom spinner not compiling
- Keyframes must be string literals in the
macro arrayrattle! - Row count must match the visual width of each keyframe string
Spinner looks garbled in terminal
- Some braille/emoji frames require a terminal with Unicode support
- Test with ASCII presets (
) to verify basic functionality firstascii::line()