@@ -7,13 +7,10 @@ import (
"runtime"
"time"
- "charm.land/lipgloss/v2"
"github.com/charmbracelet/crush/internal/format"
- "github.com/charmbracelet/crush/internal/tui/components/anim"
- "github.com/charmbracelet/crush/internal/tui/styles"
"github.com/charmbracelet/crush/internal/update"
"github.com/charmbracelet/crush/internal/version"
- "github.com/charmbracelet/x/term"
+ "github.com/charmbracelet/x/exp/charmtone"
"github.com/spf13/cobra"
)
@@ -169,24 +166,8 @@ crush update apply --force
}
// newUpdateSpinner creates a spinner for update operations.
-func newUpdateSpinner(ctx context.Context, cancel context.CancelFunc, label string) *format.Spinner {
- t := styles.CurrentTheme()
-
- // Detect background color for appropriate text color.
- hasDarkBG := true
- if term.IsTerminal(os.Stderr.Fd()) {
- hasDarkBG = lipgloss.HasDarkBackground(os.Stdin, os.Stderr)
- }
- defaultFG := lipgloss.LightDark(hasDarkBG)(lipgloss.Color("#fafafa"), t.FgBase)
-
- return format.NewSpinner(ctx, cancel, anim.Settings{
- Size: 10,
- Label: label,
- LabelColor: defaultFG,
- GradColorA: t.Primary,
- GradColorB: t.Secondary,
- CycleColors: true,
- })
+func newUpdateSpinner(ctx context.Context, cancel context.CancelFunc, label string) *format.SimpleSpinner {
+ return format.NewSimpleSpinner(ctx, cancel, label, charmtone.Dolly)
}
func init() {
@@ -4,14 +4,18 @@ import (
"context"
"errors"
"fmt"
+ "image/color"
"os"
+ "charm.land/bubbles/v2/spinner"
tea "charm.land/bubbletea/v2"
+ "charm.land/lipgloss/v2"
"github.com/charmbracelet/crush/internal/tui/components/anim"
"github.com/charmbracelet/x/ansi"
)
-// Spinner wraps the bubbles spinner for non-interactive mode
+// Spinner wraps the anim spinner for non-interactive mode (used for LLM
+// generation).
type Spinner struct {
done chan struct{}
prog *tea.Program
@@ -40,7 +44,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, cmd
}
-// NewSpinner creates a new spinner with the given message
+// NewSpinner creates a new spinner with the given message.
func NewSpinner(ctx context.Context, cancel context.CancelFunc, animSettings anim.Settings) *Spinner {
m := model{
anim: anim.New(animSettings),
@@ -55,12 +59,11 @@ func NewSpinner(ctx context.Context, cancel context.CancelFunc, animSettings ani
}
}
-// Start begins the spinner animation
+// Start begins the spinner animation.
func (s *Spinner) Start() {
go func() {
defer close(s.done)
_, err := s.prog.Run()
- // ensures line is cleared
fmt.Fprint(os.Stderr, ansi.EraseEntireLine)
if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, tea.ErrInterrupted) {
fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
@@ -68,8 +71,68 @@ func (s *Spinner) Start() {
}()
}
-// Stop ends the spinner animation
+// Stop ends the spinner animation.
func (s *Spinner) Stop() {
s.prog.Quit()
<-s.done
}
+
+// SimpleSpinner wraps a dot spinner for non-interactive CLI operations.
+type SimpleSpinner struct {
+ done chan struct{}
+ prog *tea.Program
+}
+
+type simpleModel struct {
+ cancel context.CancelFunc
+ spinner spinner.Model
+ label string
+}
+
+func (m simpleModel) Init() tea.Cmd { return m.spinner.Tick }
+func (m simpleModel) View() tea.View { return tea.NewView(m.spinner.View() + " " + m.label) }
+
+func (m simpleModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.KeyPressMsg:
+ switch msg.String() {
+ case "ctrl+c", "esc":
+ m.cancel()
+ return m, tea.Quit
+ }
+ case spinner.TickMsg:
+ var cmd tea.Cmd
+ m.spinner, cmd = m.spinner.Update(msg)
+ return m, cmd
+ }
+ return m, nil
+}
+
+// NewSimpleSpinner creates a simple dot spinner for CLI operations.
+func NewSimpleSpinner(ctx context.Context, cancel context.CancelFunc, label string, c color.Color) *SimpleSpinner {
+ s := spinner.New(spinner.WithSpinner(spinner.Dot))
+ if c != nil {
+ s.Style = lipgloss.NewStyle().Foreground(c)
+ }
+ m := simpleModel{cancel: cancel, spinner: s, label: label}
+ p := tea.NewProgram(m, tea.WithOutput(os.Stderr), tea.WithContext(ctx))
+ return &SimpleSpinner{prog: p, done: make(chan struct{}, 1)}
+}
+
+// Start begins the spinner animation.
+func (s *SimpleSpinner) Start() {
+ go func() {
+ defer close(s.done)
+ _, err := s.prog.Run()
+ fmt.Fprint(os.Stderr, ansi.EraseEntireLine)
+ if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, tea.ErrInterrupted) {
+ fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
+ }
+ }()
+}
+
+// Stop ends the spinner animation.
+func (s *SimpleSpinner) Stop() {
+ s.prog.Quit()
+ <-s.done
+}