1package format
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "image/color"
8 "os"
9
10 "charm.land/bubbles/v2/spinner"
11 tea "charm.land/bubbletea/v2"
12 "charm.land/lipgloss/v2"
13 "github.com/charmbracelet/crush/internal/tui/components/anim"
14 "github.com/charmbracelet/x/ansi"
15)
16
17// Spinner wraps the anim spinner for non-interactive mode (used for LLM
18// generation).
19type Spinner struct {
20 done chan struct{}
21 prog *tea.Program
22}
23
24type model struct {
25 cancel context.CancelFunc
26 anim *anim.Anim
27}
28
29func (m model) Init() tea.Cmd { return m.anim.Init() }
30func (m model) View() tea.View { return tea.NewView(m.anim.View()) }
31
32// Update implements tea.Model.
33func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
34 switch msg := msg.(type) {
35 case tea.KeyPressMsg:
36 switch msg.String() {
37 case "ctrl+c", "esc":
38 m.cancel()
39 return m, tea.Quit
40 }
41 }
42 mm, cmd := m.anim.Update(msg)
43 m.anim = mm.(*anim.Anim)
44 return m, cmd
45}
46
47// NewSpinner creates a new spinner with the given message.
48func NewSpinner(ctx context.Context, cancel context.CancelFunc, animSettings anim.Settings) *Spinner {
49 m := model{
50 anim: anim.New(animSettings),
51 cancel: cancel,
52 }
53
54 p := tea.NewProgram(m, tea.WithOutput(os.Stderr), tea.WithContext(ctx))
55
56 return &Spinner{
57 prog: p,
58 done: make(chan struct{}, 1),
59 }
60}
61
62// Start begins the spinner animation.
63func (s *Spinner) Start() {
64 go func() {
65 defer close(s.done)
66 _, err := s.prog.Run()
67 fmt.Fprint(os.Stderr, ansi.EraseEntireLine)
68 if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, tea.ErrInterrupted) {
69 fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
70 }
71 }()
72}
73
74// Stop ends the spinner animation.
75func (s *Spinner) Stop() {
76 s.prog.Quit()
77 <-s.done
78}
79
80// SimpleSpinner wraps a dot spinner for non-interactive CLI operations.
81type SimpleSpinner struct {
82 done chan struct{}
83 prog *tea.Program
84}
85
86type simpleModel struct {
87 cancel context.CancelFunc
88 spinner spinner.Model
89 label string
90}
91
92func (m simpleModel) Init() tea.Cmd { return m.spinner.Tick }
93func (m simpleModel) View() tea.View { return tea.NewView(m.spinner.View() + " " + m.label) }
94
95func (m simpleModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
96 switch msg := msg.(type) {
97 case tea.KeyPressMsg:
98 switch msg.String() {
99 case "ctrl+c", "esc":
100 m.cancel()
101 return m, tea.Quit
102 }
103 case spinner.TickMsg:
104 var cmd tea.Cmd
105 m.spinner, cmd = m.spinner.Update(msg)
106 return m, cmd
107 }
108 return m, nil
109}
110
111// NewSimpleSpinner creates a simple dot spinner for CLI operations.
112func NewSimpleSpinner(ctx context.Context, cancel context.CancelFunc, label string, c color.Color) *SimpleSpinner {
113 s := spinner.New(spinner.WithSpinner(spinner.Dot))
114 if c != nil {
115 s.Style = lipgloss.NewStyle().Foreground(c)
116 }
117 m := simpleModel{cancel: cancel, spinner: s, label: label}
118 p := tea.NewProgram(m, tea.WithOutput(os.Stderr), tea.WithContext(ctx))
119 return &SimpleSpinner{prog: p, done: make(chan struct{}, 1)}
120}
121
122// Start begins the spinner animation.
123func (s *SimpleSpinner) Start() {
124 go func() {
125 defer close(s.done)
126 _, err := s.prog.Run()
127 fmt.Fprint(os.Stderr, ansi.EraseEntireLine)
128 if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, tea.ErrInterrupted) {
129 fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
130 }
131 }()
132}
133
134// Stop ends the spinner animation.
135func (s *SimpleSpinner) Stop() {
136 s.prog.Quit()
137 <-s.done
138}