From 27d6453ee3916ffdbd3de7cde9d53bceb8b46537 Mon Sep 17 00:00:00 2001 From: Amolith Date: Thu, 11 Dec 2025 21:43:32 -0700 Subject: [PATCH] refactor(update): use pink highlight for spinner Assisted-by: Kimi K2 Thinking via Crush --- internal/cmd/update.go | 25 ++----------- internal/format/spinner.go | 73 +++++++++++++++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 27 deletions(-) diff --git a/internal/cmd/update.go b/internal/cmd/update.go index 2a9e420f23272d8e758fbe99477fcc1c31096489..6106179a6b0962cb041eb3c05f800d5ef185395a 100644 --- a/internal/cmd/update.go +++ b/internal/cmd/update.go @@ -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() { diff --git a/internal/format/spinner.go b/internal/format/spinner.go index 53d48dbb2831df8b6145f762884ee506c2f4ce0a..17ebe907d20f90145f87aaf7191057cb89c1b0c1 100644 --- a/internal/format/spinner.go +++ b/internal/format/spinner.go @@ -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 +}