on main help sign in
Merge pull request #35 from charmbracelet/spinner-improvements
1ba2164Raphael Amorim
internal/tui/components/anim/anim.go+36−29
@@ -17,12 +17,23 @@ import (
17)
17)
18
18
19const (
19const (
20− charCyclingFPS = time.Second / 22
20+ charCyclingFPS = time.Second / 8 // 8 FPS for better CPU efficiency
21− colorCycleFPS = time.Second / 5
21+ colorCycleFPS = time.Second / 3 // 3 FPS
22− maxCyclingChars = 120
22+ maxCyclingChars = 60 // 60 characters
23)
23)
24
24
25+var (
25−var charRunes = []rune("0123456789abcdefABCDEF~!@#$£€%^&*()+=_")
26+ charRunes = []rune("0123456789abcdefABCDEF~!@#$£€%^&*()+=_")
27+ charRunePool = make([]rune, 1000) // Pre-generated pool of random characters
28+ poolIndex = 0
29+)
30+
31+func init() {
32+ // Pre-populate the character pool to avoid runtime random generation
33+ for i := range charRunePool {
34+ charRunePool[i] = charRunes[rand.IntN(len(charRunes))]
35+ }
36+}
26
37
27type charState int
38type charState int
28
39
@@ -41,7 +52,9 @@ type cyclingChar struct {
41}
52}
42
53
43func (c cyclingChar) randomRune() rune {
54func (c cyclingChar) randomRune() rune {
44− return (charRunes)[rand.IntN(len(charRunes))] //nolint:gosec
55+ // Use pre-generated pool instead of runtime random generation
56+ poolIndex = (poolIndex + 1) % len(charRunePool)
57+ return charRunePool[poolIndex]
45}
58}
46
59
47func (c cyclingChar) state(start time.Time) charState {
60func (c cyclingChar) state(start time.Time) charState {
@@ -126,14 +139,18 @@ func New(cyclingCharsSize uint, label string, opts ...animOption) Animation {
126 // color the cycling characters with a gradient ramp.
139 // color the cycling characters with a gradient ramp.
127 const minRampSize = 3
140 const minRampSize = 3
128 if n >= minRampSize {
141 if n >= minRampSize {
129− // Note: double capacity for color cycling as we'll need to reverse and
142+ // Optimized: single capacity allocation for color cycling
130− // append the ramp for seamless transitions.
131− c.ramp = make([]lipgloss.Style, n, n*2) //nolint:mnd
143+ c.ramp = make([]lipgloss.Style, 0, n*2)
132 ramp := makeGradientRamp(n)
144 ramp := makeGradientRamp(n)
133− for i, color := range ramp {
145+ for _, color := range ramp {
134− c.ramp[i] = lipgloss.NewStyle().Foreground(color)
146+ c.ramp = append(c.ramp, lipgloss.NewStyle().Foreground(color))
135 }
147 }
136− c.ramp = append(c.ramp, reverse(c.ramp)...) // reverse and append for color cycling
148+ // Create reversed copy for seamless color cycling
149+ reversed := make([]lipgloss.Style, len(c.ramp))
150+ for i, style := range c.ramp {
151+ reversed[len(c.ramp)-1-i] = style
152+ }
153+ c.ramp = append(c.ramp, reversed...)
137 }
154 }
138
155
139 makeDelay := func(a int32, b time.Duration) time.Duration {
156 makeDelay := func(a int32, b time.Duration) time.Duration {
@@ -246,29 +263,28 @@ func (a anim) View() tea.View {
246 b strings.Builder
263 b strings.Builder
247 )
264 )
248
265
249− // Pre-allocate builder capacity to avoid reallocations.
266+ // Optimized capacity calculation to reduce allocations
250− // Estimate: cycling chars + label chars + ellipsis + style overhead.
251 const (
267 const (
252− bytesPerChar = 20 // ANSI styling
268+ bytesPerChar = 15 // Reduced estimate for ANSI styling
253− bufferSize = 50 // ellipsis and safety margin
269+ bufferSize = 30 // Reduced safety margin
254 )
270 )
255 estimatedCap := len(a.cyclingChars)*bytesPerChar + len(a.labelChars)*bytesPerChar + bufferSize
271 estimatedCap := len(a.cyclingChars)*bytesPerChar + len(a.labelChars)*bytesPerChar + bufferSize
256 b.Grow(estimatedCap)
272 b.Grow(estimatedCap)
257
273
274+ // Render cycling characters with gradient (if available)
258 for i, c := range a.cyclingChars {
275 for i, c := range a.cyclingChars {
259 if len(a.ramp) > i {
276 if len(a.ramp) > i {
260 b.WriteString(a.ramp[i].Render(string(c.currentValue)))
277 b.WriteString(a.ramp[i].Render(string(c.currentValue)))
261− continue
278+ } else {
279+ b.WriteRune(c.currentValue)
262 }
280 }
263− b.WriteRune(c.currentValue)
264 }
281 }
265
282
283+ // Render label characters and ellipsis
266 if len(a.labelChars) > 1 {
284 if len(a.labelChars) > 1 {
267 textStyle := t.S().Text
285 textStyle := t.S().Text
268 for _, c := range a.labelChars {
286 for _, c := range a.labelChars {
269− b.WriteString(
270− textStyle.Render(string(c.currentValue)),
287+ b.WriteString(textStyle.Render(string(c.currentValue)))
271− )
272 }
288 }
273 b.WriteString(textStyle.Render(a.ellipsis.View()))
289 b.WriteString(textStyle.Render(a.ellipsis.View()))
274 }
290 }
@@ -296,12 +312,3 @@ func makeGradientRamp(length int) []color.Color {
296 }
312 }
297 return c
313 return c
298}
314}
299−
300−func reverse[T any](in []T) []T {
301− out := make([]T, len(in))
302− copy(out, in[:])
303− for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
304− out[i], out[j] = out[j], out[i]
305− }
306− return out
307−}
todos.md+1−1
@@ -30,7 +30,7 @@
30- [ ] Revisit the core list component
30- [ ] Revisit the core list component
31 - [ ] This component has become super complex we might need to fix this.
31 - [ ] This component has become super complex we might need to fix this.
32- [ ] Handle correct LSP and MCP status icon
32- [ ] Handle correct LSP and MCP status icon
33−- [ ] Investigate ways to make the spinner less CPU intensive
33+- [x] Investigate ways to make the spinner less CPU intensive
34- [ ] General cleanup and documentation
34- [ ] General cleanup and documentation
35- [ ] Update the readme
35- [ ] Update the readme
36
36