1package format
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8
9 tea "charm.land/bubbletea/v2"
10 "git.secluded.site/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}