diff --git a/internal/app/app.go b/internal/app/app.go index da014df81367665caf1df793760e3d832c223648..aba2dd255d076566ec0b7412df654209d21350d5 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -89,7 +89,7 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat // Start spinner if not in quiet mode var spinner *format.Spinner if !quiet { - spinner = format.NewSpinner("Thinking...") + spinner = format.NewSpinner(ctx, "Generating") spinner.Start() defer spinner.Stop() } @@ -120,6 +120,12 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat } result := <-done + + // Stop spinner before printing output + if !quiet && spinner != nil { + spinner.Stop() + } + if result.Error != nil { if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) { slog.Info("Agent processing cancelled", "session_id", sess.ID) @@ -128,11 +134,6 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat return fmt.Errorf("agent processing failed: %w", result.Error) } - // Stop spinner before printing output - if !quiet && spinner != nil { - spinner.Stop() - } - // Get the text content from the response content := "No content available" if result.Message.Content().String() != "" { diff --git a/internal/format/spinner.go b/internal/format/spinner.go index 739fac1b27c99b9c4c4da030943500eda79f957c..9377bd3b4c145fc6866ac1e0f4e63dff8ab51619 100644 --- a/internal/format/spinner.go +++ b/internal/format/spinner.go @@ -2,101 +2,63 @@ package format import ( "context" + "errors" "fmt" "os" - "github.com/charmbracelet/bubbles/v2/spinner" tea "github.com/charmbracelet/bubbletea/v2" + "github.com/charmbracelet/crush/internal/tui/components/anim" + "github.com/charmbracelet/crush/internal/tui/styles" + "github.com/charmbracelet/x/ansi" ) // Spinner wraps the bubbles spinner for non-interactive mode type Spinner struct { - model spinner.Model - done chan struct{} - prog *tea.Program - ctx context.Context - cancel context.CancelFunc + done chan struct{} + prog *tea.Program } -// spinnerModel is the tea.Model for the spinner -type spinnerModel struct { - spinner spinner.Model - message string - quitting bool -} - -func (m spinnerModel) Init() tea.Cmd { - return m.spinner.Tick -} - -func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyPressMsg: - m.quitting = true - return m, tea.Quit - case spinner.TickMsg: - var cmd tea.Cmd - m.spinner, cmd = m.spinner.Update(msg) - return m, cmd - case quitMsg: - m.quitting = true - return m, tea.Quit - default: - return m, nil - } -} - -func (m spinnerModel) View() string { - if m.quitting { - return "" - } - return fmt.Sprintf("%s %s", m.spinner.View(), m.message) -} - -// quitMsg is sent when we want to quit the spinner -type quitMsg struct{} - // NewSpinner creates a new spinner with the given message -func NewSpinner(message string) *Spinner { - s := spinner.New() - s.Spinner = spinner.Dot - s.Style = s.Style.Foreground(s.Style.GetForeground()) - - ctx, cancel := context.WithCancel(context.Background()) - - model := spinnerModel{ - spinner: s, - message: message, - } +func NewSpinner(ctx context.Context, message string) *Spinner { + t := styles.CurrentTheme() + model := anim.New(anim.Settings{ + Size: 10, + Label: message, + LabelColor: t.FgBase, + GradColorA: t.Primary, + GradColorB: t.Secondary, + CycleColors: true, + }) - prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics()) + prog := tea.NewProgram( + model, + tea.WithInput(nil), + tea.WithOutput(os.Stderr), + tea.WithContext(ctx), + tea.WithoutCatchPanics(), + ) return &Spinner{ - model: s, - done: make(chan struct{}), - prog: prog, - ctx: ctx, - cancel: cancel, + prog: prog, + done: make(chan struct{}, 1), } } // Start begins the spinner animation func (s *Spinner) Start() { go func() { - defer close(s.done) - go func() { - <-s.ctx.Done() - s.prog.Send(quitMsg{}) - }() _, err := s.prog.Run() - if err != nil { + // ensures line is cleared + fmt.Fprint(os.Stderr, ansi.EraseEntireLine) + if err != nil && !errors.Is(err, context.Canceled) { fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err) } + close(s.done) }() } // Stop ends the spinner animation func (s *Spinner) Stop() { - s.cancel() + s.prog.Quit() <-s.done }