1package format
2
3import (
4 "context"
5 "fmt"
6 "os"
7
8 "github.com/charmbracelet/bubbles/v2/spinner"
9 tea "github.com/charmbracelet/bubbletea/v2"
10)
11
12// Spinner wraps the bubbles spinner for non-interactive mode
13type Spinner struct {
14 model spinner.Model
15 done chan struct{}
16 prog *tea.Program
17 ctx context.Context
18 cancel context.CancelFunc
19}
20
21// spinnerModel is the tea.Model for the spinner
22type spinnerModel struct {
23 spinner spinner.Model
24 message string
25 quitting bool
26}
27
28func (m spinnerModel) Init() tea.Cmd {
29 return m.spinner.Tick
30}
31
32func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
33 switch msg := msg.(type) {
34 case tea.KeyPressMsg:
35 m.quitting = true
36 return m, tea.Quit
37 case spinner.TickMsg:
38 var cmd tea.Cmd
39 m.spinner, cmd = m.spinner.Update(msg)
40 return m, cmd
41 case quitMsg:
42 m.quitting = true
43 return m, tea.Quit
44 default:
45 return m, nil
46 }
47}
48
49func (m spinnerModel) View() string {
50 if m.quitting {
51 return ""
52 }
53 return fmt.Sprintf("%s %s", m.spinner.View(), m.message)
54}
55
56// quitMsg is sent when we want to quit the spinner
57type quitMsg struct{}
58
59// NewSpinner creates a new spinner with the given message
60func NewSpinner(message string) *Spinner {
61 s := spinner.New()
62 s.Spinner = spinner.Dot
63 s.Style = s.Style.Foreground(s.Style.GetForeground())
64
65 ctx, cancel := context.WithCancel(context.Background())
66
67 model := spinnerModel{
68 spinner: s,
69 message: message,
70 }
71
72 prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics())
73
74 return &Spinner{
75 model: s,
76 done: make(chan struct{}),
77 prog: prog,
78 ctx: ctx,
79 cancel: cancel,
80 }
81}
82
83// Start begins the spinner animation
84func (s *Spinner) Start() {
85 go func() {
86 defer close(s.done)
87 go func() {
88 <-s.ctx.Done()
89 s.prog.Send(quitMsg{})
90 }()
91 _, err := s.prog.Run()
92 if err != nil {
93 fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
94 }
95 }()
96}
97
98// Stop ends the spinner animation
99func (s *Spinner) Stop() {
100 s.cancel()
101 <-s.done
102}