on main help sign in
fix: use the same spinner in non-interactive mode (#131)

* fix: use the same spinner in non-interactive mode * fix: simplify * fix: move things around a bit

commitdd509684fceef6c0371668736bcc53f0bf62eefd authorCarlos Alexandro Becker <caarlos0@users.noreply.github.com> dateThu 10 Jul 2025, 16:05 (1 year ago) parente77b537
2 files changed+37−74
internal/app/app.go+7−6 internal/format/spinner.go+30−68
internal/app/app.go+7−6
@@ -89,7 +89,7 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
8989 // Start spinner if not in quiet mode
9090 var spinner *format.Spinner
9191 if !quiet {
92− spinner = format.NewSpinner("Thinking...")
92+ spinner = format.NewSpinner(ctx, "Generating")
9393 spinner.Start()
9494 defer spinner.Stop()
9595 }
@@ -120,6 +120,12 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
120120 }
121121
122122 result := <-done
123+
124+ // Stop spinner before printing output
125+ if !quiet && spinner != nil {
126+ spinner.Stop()
127+ }
128+
123129 if result.Error != nil {
124130 if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) {
125131 slog.Info("Agent processing cancelled", "session_id", sess.ID)
@@ -128,11 +134,6 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
128134 return fmt.Errorf("agent processing failed: %w", result.Error)
129135 }
130136
131− // Stop spinner before printing output
132− if !quiet && spinner != nil {
133− spinner.Stop()
134− }
135−
136137 // Get the text content from the response
137138 content := "No content available"
138139 if result.Message.Content().String() != "" {
internal/format/spinner.go+30−68
@@ -2,101 +2,63 @@ package format
22
33import (
44 "context"
5+ "errors"
56 "fmt"
67 "os"
78
8− "github.com/charmbracelet/bubbles/v2/spinner"
99 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"
1013)
1114
1215// Spinner wraps the bubbles spinner for non-interactive mode
1316type Spinner struct {
14− model spinner.Model
15− done chan struct{}
16− prog *tea.Program
17− ctx context.Context
18− cancel context.CancelFunc
17+ done chan struct{}
18+ prog *tea.Program
1919}
2020
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−
5921// NewSpinner creates a new spinner with the given message
60−func 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− }
22+func NewSpinner(ctx context.Context, message string) *Spinner {
23+ t := styles.CurrentTheme()
24+ model := anim.New(anim.Settings{
25+ Size: 10,
26+ Label: message,
27+ LabelColor: t.FgBase,
28+ GradColorA: t.Primary,
29+ GradColorB: t.Secondary,
30+ CycleColors: true,
31+ })
7132
72− prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics())
33+ prog := tea.NewProgram(
34+ model,
35+ tea.WithInput(nil),
36+ tea.WithOutput(os.Stderr),
37+ tea.WithContext(ctx),
38+ tea.WithoutCatchPanics(),
39+ )
7340
7441 return &Spinner{
75− model: s,
76− done: make(chan struct{}),
77− prog: prog,
78− ctx: ctx,
79− cancel: cancel,
42+ prog: prog,
43+ done: make(chan struct{}, 1),
8044 }
8145}
8246
8347// Start begins the spinner animation
8448func (s *Spinner) Start() {
8549 go func() {
86− defer close(s.done)
87− go func() {
88− <-s.ctx.Done()
89− s.prog.Send(quitMsg{})
90− }()
9150 _, 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) {
9354 fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
9455 }
56+ close(s.done)
9557 }()
9658}
9759
9860// Stop ends the spinner animation
9961func (s *Spinner) Stop() {
100− s.cancel()
62+ s.prog.Quit()
10163 <-s.done
10264}