go/pkg/cli/spinner.go
Snider 50afecea6d feat(cli): add Spinner with async handle (Update, Done, Fail)
Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-21 18:09:40 +00:00

107 lines
2 KiB
Go

package cli
import (
"fmt"
"sync"
"time"
)
// SpinnerHandle controls a running spinner.
type SpinnerHandle struct {
mu sync.Mutex
message string
done bool
ticker *time.Ticker
stopCh chan struct{}
}
// NewSpinner starts an async spinner with the given message.
// Call Stop(), Done(), or Fail() to stop it.
func NewSpinner(message string) *SpinnerHandle {
s := &SpinnerHandle{
message: message,
ticker: time.NewTicker(100 * time.Millisecond),
stopCh: make(chan struct{}),
}
frames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
if !ColorEnabled() {
frames = []string{"|", "/", "-", "\\"}
}
go func() {
i := 0
for {
select {
case <-s.stopCh:
return
case <-s.ticker.C:
s.mu.Lock()
if !s.done {
fmt.Printf("\033[2K\r%s %s", DimStyle.Render(frames[i%len(frames)]), s.message)
}
s.mu.Unlock()
i++
}
}
}()
return s
}
// Message returns the current spinner message.
func (s *SpinnerHandle) Message() string {
s.mu.Lock()
defer s.mu.Unlock()
return s.message
}
// Update changes the spinner message.
func (s *SpinnerHandle) Update(message string) {
s.mu.Lock()
defer s.mu.Unlock()
s.message = message
}
// Stop stops the spinner silently (clears the line).
func (s *SpinnerHandle) Stop() {
s.mu.Lock()
defer s.mu.Unlock()
if s.done {
return
}
s.done = true
s.ticker.Stop()
close(s.stopCh)
fmt.Print("\033[2K\r")
}
// Done stops the spinner with a success message.
func (s *SpinnerHandle) Done(message string) {
s.mu.Lock()
alreadyDone := s.done
s.done = true
s.mu.Unlock()
if alreadyDone {
return
}
s.ticker.Stop()
close(s.stopCh)
fmt.Printf("\033[2K\r%s\n", SuccessStyle.Render(Glyph(":check:")+" "+message))
}
// Fail stops the spinner with an error message.
func (s *SpinnerHandle) Fail(message string) {
s.mu.Lock()
alreadyDone := s.done
s.done = true
s.mu.Unlock()
if alreadyDone {
return
}
s.ticker.Stop()
close(s.stopCh)
fmt.Printf("\033[2K\r%s\n", ErrorStyle.Render(Glyph(":cross:")+" "+message))
}