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
21type model struct {
22 cancel context.CancelFunc
23 anim anim.Spinner
24}
25
26func (m model) Init() tea.Cmd { return m.anim.Init() }
27func (m model) View() string { return m.anim.View() }
28
29// Update implements tea.Model.
30func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
31 switch msg := msg.(type) {
32 case tea.KeyPressMsg:
33 switch msg.String() {
34 case "ctrl+c", "esc":
35 m.cancel()
36 return m, tea.Quit
37 }
38 }
39 var cmd tea.Cmd
40 m.anim, cmd = m.anim.Update(msg)
41 return m, cmd
42}
43
44// NewSpinner creates a new spinner with the given message
45func NewSpinner(ctx context.Context, cancel context.CancelFunc, message string) *Spinner {
46 t := styles.CurrentTheme()
47 model := model{
48 anim: anim.New(anim.Settings{
49 Size: 10,
50 Label: message,
51 LabelColor: t.FgBase,
52 GradColorA: t.Primary,
53 GradColorB: t.Secondary,
54 CycleColors: true,
55 }),
56 cancel: cancel,
57 }
58
59 prog := tea.NewProgram(
60 model,
61 tea.WithOutput(os.Stderr),
62 tea.WithContext(ctx),
63 )
64
65 return &Spinner{
66 prog: prog,
67 done: make(chan struct{}, 1),
68 }
69}
70
71// Start begins the spinner animation
72func (s *Spinner) Start() {
73 go func() {
74 defer close(s.done)
75 _, err := s.prog.Run()
76 // ensures line is cleared
77 fmt.Fprint(os.Stderr, ansi.EraseEntireLine)
78 if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, tea.ErrInterrupted) {
79 fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
80 }
81 }()
82}
83
84// Stop ends the spinner animation
85func (s *Spinner) Stop() {
86 s.prog.Quit()
87 <-s.done
88}