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}