package ui import ( "fmt" "os" "sync" "time" ) var ( spinnerChan chan bool spinnerMu sync.Mutex ) func StartSpinner(msg string) { StartSpinnerWithColor(msg, Colors.Normal) } func StartSpinnerWithColor(msg string, c ColorFn) { if c == nil { c = Colors.Normal } style := `⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏` frames := []rune(style) length := len(frames) spinnerMu.Lock() // If a previous spinner exists, stop it cleanly before starting a new one if spinnerChan != nil { close(spinnerChan) spinnerChan = nil } spinnerChan = make(chan bool) spinnerMu.Unlock() ticker := time.NewTicker(100 * time.Millisecond) go func() { pos := 0 for { select { case <-spinnerChan: ticker.Stop() return case <-ticker.C: fmt.Fprintf(os.Stderr, "\r%s ... %s", c("PMG: "+msg), string(frames[pos%length])) pos += 1 } } }() } func StopSpinner() { spinnerMu.Lock() defer spinnerMu.Unlock() if spinnerChan == nil { return } // Gracefully handle the case where the spinner is already stopped // and the channel is closed, yet client code calls StopSpinner() again. defer func() { _ = recover() }() close(spinnerChan) spinnerChan = nil fmt.Fprint(os.Stderr, "\r") fmt.Fprintln(os.Stderr) }