spinner.go

 1package format
 2
 3import (
 4	"context"
 5	"errors"
 6	"fmt"
 7	"os"
 8
 9	tea "github.com/charmbracelet/bubbletea/v2"
10	"github.com/charmbracelet/crush/internal/tui/components/anim"
11	"github.com/charmbracelet/crush/internal/tui/styles"
12	"github.com/charmbracelet/x/ansi"
13)
14
15// Spinner wraps the bubbles spinner for non-interactive mode
16type Spinner struct {
17	done chan struct{}
18	prog *tea.Program
19}
20
21// NewSpinner creates a new spinner with the given message
22func NewSpinner(ctx context.Context, message string) *Spinner {
23	t := styles.CurrentTheme()
24	model := anim.New(anim.Settings{
25		Size:        10,
26		Label:       message,
27		LabelColor:  t.FgBase,
28		GradColorA:  t.Primary,
29		GradColorB:  t.Secondary,
30		CycleColors: true,
31	})
32
33	prog := tea.NewProgram(
34		model,
35		tea.WithInput(nil),
36		tea.WithOutput(os.Stderr),
37		tea.WithContext(ctx),
38		tea.WithoutCatchPanics(),
39	)
40
41	return &Spinner{
42		prog: prog,
43		done: make(chan struct{}, 1),
44	}
45}
46
47// Start begins the spinner animation
48func (s *Spinner) Start() {
49	go func() {
50		_, err := s.prog.Run()
51		// ensures line is cleared
52		fmt.Fprint(os.Stderr, ansi.EraseEntireLine)
53		if err != nil && !errors.Is(err, context.Canceled) {
54			fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
55		}
56		close(s.done)
57	}()
58}
59
60// Stop ends the spinner animation
61func (s *Spinner) Stop() {
62	s.prog.Quit()
63	<-s.done
64}