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}