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/x/ansi"
12)
13
14// Spinner wraps the bubbles spinner for non-interactive mode
15type Spinner struct {
16	done chan struct{}
17	prog *tea.Program
18}
19
20type model struct {
21	cancel context.CancelFunc
22	anim   *anim.Anim
23}
24
25func (m model) Init() tea.Cmd  { return m.anim.Init() }
26func (m model) View() tea.View { return tea.NewView(m.anim.View()) }
27
28// Update implements tea.Model.
29func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
30	switch msg := msg.(type) {
31	case tea.KeyPressMsg:
32		switch msg.String() {
33		case "ctrl+c", "esc":
34			m.cancel()
35			return m, tea.Quit
36		}
37	}
38	mm, cmd := m.anim.Update(msg)
39	m.anim = mm.(*anim.Anim)
40	return m, cmd
41}
42
43// NewSpinner creates a new spinner with the given message
44func NewSpinner(ctx context.Context, cancel context.CancelFunc, animSettings anim.Settings) *Spinner {
45	m := model{
46		anim:   anim.New(animSettings),
47		cancel: cancel,
48	}
49
50	p := tea.NewProgram(m, tea.WithOutput(os.Stderr), tea.WithContext(ctx))
51
52	return &Spinner{
53		prog: p,
54		done: make(chan struct{}, 1),
55	}
56}
57
58// Start begins the spinner animation
59func (s *Spinner) Start() {
60	go func() {
61		defer close(s.done)
62		_, err := s.prog.Run()
63		// ensures line is cleared
64		fmt.Fprint(os.Stderr, ansi.EraseEntireLine)
65		if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, tea.ErrInterrupted) {
66			fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
67		}
68	}()
69}
70
71// Stop ends the spinner animation
72func (s *Spinner) Stop() {
73	s.prog.Quit()
74	<-s.done
75}