@@ -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() != "" {
@@ -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
}