/* ©AngelaMos | 2026 spinner.go Goroutine-backed terminal spinner with graceful stop Renders a braille-frame animation while scanners run. Hides the cursor on start, clears the line on stop, and uses a WaitGroup to block until the background goroutine exits cleanly. */ package ui import ( "fmt" "strings" "sync" "time" ) var frames = []string{ "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏", } type Spinner struct { msg string done chan struct{} wg sync.WaitGroup mu sync.Mutex active bool } func NewSpinner(msg string) *Spinner { return &Spinner{msg: msg} } func (s *Spinner) Start() { s.mu.Lock() if s.active { s.mu.Unlock() return } s.active = true s.done = make(chan struct{}) s.wg.Add(1) s.mu.Unlock() go s.run() } func (s *Spinner) Stop() { s.mu.Lock() if !s.active { s.mu.Unlock() return } s.active = false close(s.done) s.mu.Unlock() s.wg.Wait() } func (s *Spinner) run() { defer s.wg.Done() fmt.Print("\033[?25l") ticker := time.NewTicker(80 * time.Millisecond) defer ticker.Stop() idx := 0 for { select { case <-s.done: clearLine() fmt.Print("\033[?25h") return case <-ticker.C: frame := frames[idx%len(frames)] fmt.Printf( "\r %s %s", CyanBold(frame), Magenta(s.msg), ) idx++ } } } func clearLine() { fmt.Print("\r" + strings.Repeat(" ", 80) + "\r") }