on main help sign in
fix: use the same spinner in non-interactive mode (#131)
dd50968Carlos Alexandro Becker
internal/app/app.go+7−6
@@ -89,7 +89,7 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
89 // Start spinner if not in quiet mode
89 // Start spinner if not in quiet mode
90 var spinner *format.Spinner
90 var spinner *format.Spinner
91 if !quiet {
91 if !quiet {
92− spinner = format.NewSpinner("Thinking...")
92+ spinner = format.NewSpinner(ctx, "Generating")
93 spinner.Start()
93 spinner.Start()
94 defer spinner.Stop()
94 defer spinner.Stop()
95 }
95 }
@@ -120,6 +120,12 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
120 }
120 }
121
121
122 result := <-done
122 result := <-done
123+
124+ // Stop spinner before printing output
125+ if !quiet && spinner != nil {
126+ spinner.Stop()
127+ }
128+
123 if result.Error != nil {
129 if result.Error != nil {
124 if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) {
130 if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) {
125 slog.Info("Agent processing cancelled", "session_id", sess.ID)
131 slog.Info("Agent processing cancelled", "session_id", sess.ID)
@@ -128,11 +134,6 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
128 return fmt.Errorf("agent processing failed: %w", result.Error)
134 return fmt.Errorf("agent processing failed: %w", result.Error)
129 }
135 }
130
136
131− // Stop spinner before printing output
132− if !quiet && spinner != nil {
133− spinner.Stop()
134− }
135−
136 // Get the text content from the response
137 // Get the text content from the response
137 content := "No content available"
138 content := "No content available"
138 if result.Message.Content().String() != "" {
139 if result.Message.Content().String() != "" {
internal/format/spinner.go+30−68
@@ -2,101 +2,63 @@ package format
2
2
3import (
3import (
4 "context"
4 "context"
5+ "errors"
5 "fmt"
6 "fmt"
6 "os"
7 "os"
7
8
8− "github.com/charmbracelet/bubbles/v2/spinner"
9 tea "github.com/charmbracelet/bubbletea/v2"
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"
10)
13)
11
14
12// Spinner wraps the bubbles spinner for non-interactive mode
15// Spinner wraps the bubbles spinner for non-interactive mode
13type Spinner struct {
16type Spinner struct {
14− model spinner.Model
15− done chan struct{}
17+ done chan struct{}
16− prog *tea.Program
18+ prog *tea.Program
17− ctx context.Context
18− cancel context.CancelFunc
19}
19}
20
20
21−// spinnerModel is the tea.Model for the spinner
22−type spinnerModel struct {
23− spinner spinner.Model
24− message string
25− quitting bool
26−}
27−
28−func (m spinnerModel) Init() tea.Cmd {
29− return m.spinner.Tick
30−}
31−
32−func (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−
49−func (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
57−type quitMsg struct{}
58−
59// NewSpinner creates a new spinner with the given message
21// NewSpinner creates a new spinner with the given message
60−func NewSpinner(message string) *Spinner {
22+func NewSpinner(ctx context.Context, message string) *Spinner {
61− s := spinner.New()
23+ t := styles.CurrentTheme()
62− s.Spinner = spinner.Dot
24+ model := anim.New(anim.Settings{
63− s.Style = s.Style.Foreground(s.Style.GetForeground())
25+ Size: 10,
64−
26+ Label: message,
65− ctx, cancel := context.WithCancel(context.Background())
27+ LabelColor: t.FgBase,
66−
28+ GradColorA: t.Primary,
67− model := spinnerModel{
29+ GradColorB: t.Secondary,
68− spinner: s,
30+ CycleColors: true,
69− message: message,
70− }
31+ })
71
32
33+ prog := tea.NewProgram(
34+ model,
35+ tea.WithInput(nil),
72− prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics())
36+ tea.WithOutput(os.Stderr),
37+ tea.WithContext(ctx),
38+ tea.WithoutCatchPanics(),
39+ )
73
40
74 return &Spinner{
41 return &Spinner{
75− model: s,
42+ prog: prog,
76− done: make(chan struct{}),
43+ done: make(chan struct{}, 1),
77− prog: prog,
78− ctx: ctx,
79− cancel: cancel,
80 }
44 }
81}
45}
82
46
83// Start begins the spinner animation
47// Start begins the spinner animation
84func (s *Spinner) Start() {
48func (s *Spinner) Start() {
85 go func() {
49 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()
50 _, err := s.prog.Run()
92− if err != nil {
51+ // ensures line is cleared
52+ fmt.Fprint(os.Stderr, ansi.EraseEntireLine)
53+ if err != nil && !errors.Is(err, context.Canceled) {
93 fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
54 fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
94 }
55 }
56+ close(s.done)
95 }()
57 }()
96}
58}
97
59
98// Stop ends the spinner animation
60// Stop ends the spinner animation
99func (s *Spinner) Stop() {
61func (s *Spinner) Stop() {
100− s.cancel()
62+ s.prog.Quit()
101 <-s.done
63 <-s.done
102}
64}