Files
Ed Zynda ad07086900 cleanup
2026-02-28 01:01:12 +03:00

69 lines
1.7 KiB
Go

package ui
import (
"fmt"
"os"
"sync"
"time"
)
// Spinner provides an animated loading indicator that displays while
// long-running operations are in progress. It writes directly to stderr
// using a goroutine-based animation loop, avoiding Bubble Tea's terminal
// capability queries that can leak escape sequences (mode 2026 DECRPM).
//
// The KITT-style frames are generated by knightRiderFrames() in stream.go
// (same package) and use the active theme colors.
type Spinner struct {
frames []string
fps time.Duration
done chan struct{}
finished chan struct{} // closed by run() after cleanup
once sync.Once
}
// NewSpinner creates a new animated KITT-style spinner using theme colors.
func NewSpinner() *Spinner {
return &Spinner{
frames: knightRiderFrames(),
fps: time.Second / 14,
done: make(chan struct{}),
finished: make(chan struct{}),
}
}
// Start begins the spinner animation in a separate goroutine. The spinner
// will continue animating until Stop is called.
func (s *Spinner) Start() {
go s.run()
}
// Stop halts the spinner animation and blocks until the animation goroutine
// has exited and the line is cleared. Safe to call multiple times.
func (s *Spinner) Stop() {
s.once.Do(func() { close(s.done) })
<-s.finished
}
// run is the animation loop that renders spinner frames to stderr.
func (s *Spinner) run() {
defer close(s.finished) // unblock Stop()
ticker := time.NewTicker(s.fps)
defer ticker.Stop()
var frame int
for {
select {
case <-s.done:
// Clear the spinner line and return.
fmt.Fprint(os.Stderr, "\r\033[K")
return
case <-ticker.C:
f := s.frames[frame%len(s.frames)]
fmt.Fprintf(os.Stderr, "\r %s", f)
frame++
}
}
}