Merge pull request #35 from charmbracelet/spinner-improvements

Raphael Amorim created

Spinner improvements

Change summary

internal/tui/components/anim/anim.go | 65 ++++++++++++++++-------------
todos.md                             |  2 
2 files changed, 37 insertions(+), 30 deletions(-)

Detailed changes

internal/tui/components/anim/anim.go 🔗

@@ -17,12 +17,23 @@ import (
 )
 
 const (
-	charCyclingFPS  = time.Second / 22
-	colorCycleFPS   = time.Second / 5
-	maxCyclingChars = 120
+	charCyclingFPS  = time.Second / 8 // 8 FPS for better CPU efficiency
+	colorCycleFPS   = time.Second / 3 // 3 FPS
+	maxCyclingChars = 60              // 60 characters
 )
 
-var charRunes = []rune("0123456789abcdefABCDEF~!@#$£€%^&*()+=_")
+var (
+	charRunes    = []rune("0123456789abcdefABCDEF~!@#$£€%^&*()+=_")
+	charRunePool = make([]rune, 1000) // Pre-generated pool of random characters
+	poolIndex    = 0
+)
+
+func init() {
+	// Pre-populate the character pool to avoid runtime random generation
+	for i := range charRunePool {
+		charRunePool[i] = charRunes[rand.IntN(len(charRunes))]
+	}
+}
 
 type charState int
 
@@ -41,7 +52,9 @@ type cyclingChar struct {
 }
 
 func (c cyclingChar) randomRune() rune {
-	return (charRunes)[rand.IntN(len(charRunes))] //nolint:gosec
+	// Use pre-generated pool instead of runtime random generation
+	poolIndex = (poolIndex + 1) % len(charRunePool)
+	return charRunePool[poolIndex]
 }
 
 func (c cyclingChar) state(start time.Time) charState {
@@ -126,14 +139,18 @@ func New(cyclingCharsSize uint, label string, opts ...animOption) Animation {
 	// color the cycling characters with a gradient ramp.
 	const minRampSize = 3
 	if n >= minRampSize {
-		// Note: double capacity for color cycling as we'll need to reverse and
-		// append the ramp for seamless transitions.
-		c.ramp = make([]lipgloss.Style, n, n*2) //nolint:mnd
+		// Optimized: single capacity allocation for color cycling
+		c.ramp = make([]lipgloss.Style, 0, n*2)
 		ramp := makeGradientRamp(n)
-		for i, color := range ramp {
-			c.ramp[i] = lipgloss.NewStyle().Foreground(color)
+		for _, color := range ramp {
+			c.ramp = append(c.ramp, lipgloss.NewStyle().Foreground(color))
 		}
-		c.ramp = append(c.ramp, reverse(c.ramp)...) // reverse and append for color cycling
+		// Create reversed copy for seamless color cycling
+		reversed := make([]lipgloss.Style, len(c.ramp))
+		for i, style := range c.ramp {
+			reversed[len(c.ramp)-1-i] = style
+		}
+		c.ramp = append(c.ramp, reversed...)
 	}
 
 	makeDelay := func(a int32, b time.Duration) time.Duration {
@@ -246,29 +263,28 @@ func (a anim) View() tea.View {
 		b strings.Builder
 	)
 
-	// Pre-allocate builder capacity to avoid reallocations.
-	// Estimate: cycling chars + label chars + ellipsis + style overhead.
+	// Optimized capacity calculation to reduce allocations
 	const (
-		bytesPerChar = 20 // ANSI styling
-		bufferSize   = 50 // ellipsis and safety margin
+		bytesPerChar = 15 // Reduced estimate for ANSI styling
+		bufferSize   = 30 // Reduced safety margin
 	)
 	estimatedCap := len(a.cyclingChars)*bytesPerChar + len(a.labelChars)*bytesPerChar + bufferSize
 	b.Grow(estimatedCap)
 
+	// Render cycling characters with gradient (if available)
 	for i, c := range a.cyclingChars {
 		if len(a.ramp) > i {
 			b.WriteString(a.ramp[i].Render(string(c.currentValue)))
-			continue
+		} else {
+			b.WriteRune(c.currentValue)
 		}
-		b.WriteRune(c.currentValue)
 	}
 
+	// Render label characters and ellipsis
 	if len(a.labelChars) > 1 {
 		textStyle := t.S().Text
 		for _, c := range a.labelChars {
-			b.WriteString(
-				textStyle.Render(string(c.currentValue)),
-			)
+			b.WriteString(textStyle.Render(string(c.currentValue)))
 		}
 		b.WriteString(textStyle.Render(a.ellipsis.View()))
 	}
@@ -296,12 +312,3 @@ func makeGradientRamp(length int) []color.Color {
 	}
 	return c
 }
-
-func reverse[T any](in []T) []T {
-	out := make([]T, len(in))
-	copy(out, in[:])
-	for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
-		out[i], out[j] = out[j], out[i]
-	}
-	return out
-}

todos.md 🔗

@@ -30,7 +30,7 @@
 - [ ] Revisit the core list component
   - [ ] This component has become super complex we might need to fix this.
 - [ ] Handle correct LSP and MCP status icon
-- [ ] Investigate ways to make the spinner less CPU intensive
+- [x] Investigate ways to make the spinner less CPU intensive
 - [ ] General cleanup and documentation
 - [ ] Update the readme