spinner.go

 1package format
 2
 3import (
 4	"context"
 5	"errors"
 6	"fmt"
 7	"os"
 8
 9	tea "charm.land/bubbletea/v2"
10	"github.com/charmbracelet/crush/internal/ui/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.Start() }
26func (m model) View() tea.View { return tea.NewView(m.anim.Render()) }
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	case anim.StepMsg:
38		cmd := m.anim.Animate(msg)
39		return m, cmd
40	}
41	return m, nil
42}
43
44// NewSpinner creates a new spinner with the given message
45func NewSpinner(ctx context.Context, cancel context.CancelFunc, animSettings anim.Settings) *Spinner {
46	m := model{
47		anim:   anim.New(animSettings),
48		cancel: cancel,
49	}
50
51	p := tea.NewProgram(m, tea.WithOutput(os.Stderr), tea.WithContext(ctx))
52
53	return &Spinner{
54		prog: p,
55		done: make(chan struct{}, 1),
56	}
57}
58
59// Start begins the spinner animation
60func (s *Spinner) Start() {
61	go func() {
62		defer close(s.done)
63		_, err := s.prog.Run()
64		// ensures line is cleared
65		fmt.Fprint(os.Stderr, ansi.EraseEntireLine)
66		if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, tea.ErrInterrupted) {
67			fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
68		}
69	}()
70}
71
72// Stop ends the spinner animation
73func (s *Spinner) Stop() {
74	s.prog.Quit()
75	<-s.done
76}