44 "fmt"
55 "image/color"
66 "os"
7+ "strings"
78 "sync"
89 "time"
910
@@ -12,13 +13,54 @@ import (
1213
1314// spinnerFrames defines available spinner animation styles.
1415var (
15- pointsFrames = []string {"∙∙∙" , "●∙∙" , "∙●∙" , "∙∙●" }
16- pointsFPS = time .Second / 7
17-
1816 dotFrames = []string {"⣾ " , "⣽ " , "⣻ " , "⢿ " , "⡿ " , "⣟ " , "⣯ " , "⣷ " }
1917 dotFPS = time .Second / 10
2018)
2119
20+ // knightRiderFrames generates a KITT-style scanning animation where a bright
21+ // red light bounces back and forth across a row of dots with a trailing glow.
22+ func knightRiderFrames () []string {
23+ const numDots = 8
24+ const dot = "▪"
25+
26+ bright := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#FF0000" ))
27+ med := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#990000" ))
28+ dim := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#440000" ))
29+ off := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#222222" ))
30+
31+ // Scanner bounces: 0→7→0
32+ positions := make ([]int , 0 , 2 * numDots - 2 )
33+ for i := 0 ; i < numDots ; i ++ {
34+ positions = append (positions , i )
35+ }
36+ for i := numDots - 2 ; i > 0 ; i -- {
37+ positions = append (positions , i )
38+ }
39+
40+ frames := make ([]string , len (positions ))
41+ for f , pos := range positions {
42+ var b strings.Builder
43+ for i := 0 ; i < numDots ; i ++ {
44+ d := pos - i
45+ if d < 0 {
46+ d = - d
47+ }
48+ switch {
49+ case d == 0 :
50+ b .WriteString (bright .Render (dot ))
51+ case d == 1 :
52+ b .WriteString (med .Render (dot ))
53+ case d == 2 :
54+ b .WriteString (dim .Render (dot ))
55+ default :
56+ b .WriteString (off .Render (dot ))
57+ }
58+ }
59+ frames [f ] = b .String ()
60+ }
61+ return frames
62+ }
63+
2264// Spinner provides an animated loading indicator that displays while
2365// long-running operations are in progress. It writes directly to stderr
2466// using a goroutine-based animation loop, avoiding Bubble Tea's terminal
@@ -27,19 +69,19 @@ type Spinner struct {
2769 message string
2870 frames []string
2971 fps time.Duration
30- color color.Color
72+ color color.Color // nil when frames are pre-rendered with embedded colors
3173 done chan struct {}
3274 once sync.Once
3375}
3476
3577// NewSpinner creates a new animated spinner with the specified message.
36- // The spinner uses the theme's primary color and a points animation style .
78+ // Uses a KITT-style red scanning animation.
3779func NewSpinner (message string ) * Spinner {
3880 return & Spinner {
3981 message : message ,
40- frames : pointsFrames ,
41- fps : pointsFPS ,
42- color : GetTheme (). Primary ,
82+ frames : knightRiderFrames () ,
83+ fps : time . Second / 14 ,
84+ color : nil , // frames are pre-rendered
4385 done : make (chan struct {}),
4486 }
4587}
@@ -72,14 +114,17 @@ func (s *Spinner) Stop() {
72114func (s * Spinner ) run () {
73115 theme := GetTheme ()
74116
75- spinnerStyle := lipgloss .NewStyle ().
76- Foreground (s .color ).
77- Bold (true )
78-
79117 messageStyle := lipgloss .NewStyle ().
80118 Foreground (theme .Text ).
81119 Italic (true )
82120
121+ var spinnerStyle lipgloss.Style
122+ if s .color != nil {
123+ spinnerStyle = lipgloss .NewStyle ().
124+ Foreground (s .color ).
125+ Bold (true )
126+ }
127+
83128 ticker := time .NewTicker (s .fps )
84129 defer ticker .Stop ()
85130
@@ -92,8 +137,12 @@ func (s *Spinner) run() {
92137 return
93138 case <- ticker .C :
94139 f := s .frames [frame % len (s .frames )]
140+ rendered := f
141+ if s .color != nil {
142+ rendered = spinnerStyle .Render (f )
143+ }
95144 fmt .Fprintf (os .Stderr , "\r %s %s" ,
96- spinnerStyle . Render ( f ) ,
145+ rendered ,
97146 messageStyle .Render (s .message ))
98147 frame ++
99148 }
0 commit comments