@@ -2,6 +2,7 @@ package spinner
22
33import (
44 "math/rand/v2"
5+ "strings"
56 "sync/atomic"
67 "time"
78
@@ -17,32 +18,26 @@ type Mode int
1718const (
1819 ModeBoth Mode = iota
1920 ModeSpinnerOnly
20- ModeMessageOnly
2121)
2222
23- var lastID int64
24-
25- func nextID () int {
26- return int (atomic .AddInt64 (& lastID , 1 ))
27- }
23+ var lastID atomic.Int64
2824
2925type tickMsg struct {
30- Time time.Time
31- tag int
32- ID int
26+ tag int
27+ id int
3328}
3429
3530type Spinner struct {
36- dotsStyle lipgloss.Style
37- messages [] string
38- mode Mode
39- currentMessage string
40- lightPosition int
41- frame int
42- id int
43- tag int
44- direction int // 1 for forward, -1 for backward
45- pauseFrames int
31+ dotsStyle lipgloss.Style
32+ styledSpinnerFrames [] string // pre-rendered spinner frames
33+ mode Mode
34+ currentMessage string
35+ lightPosition int
36+ frame int
37+ id int
38+ tag int
39+ direction int // 1 for forward, -1 for backward
40+ pauseFrames int
4641}
4742
4843// Default messages for the spinner
@@ -70,21 +65,21 @@ var defaultMessages = []string{
7065}
7166
7267func New (mode Mode , dotsStyle lipgloss.Style ) Spinner {
73- return Spinner {
74- dotsStyle : dotsStyle ,
75- messages : defaultMessages ,
76- mode : mode ,
77- currentMessage : defaultMessages [rand .IntN (len (defaultMessages ))],
78- lightPosition : - 3 ,
79- frame : 0 ,
80- id : nextID (),
81- direction : 1 ,
82- pauseFrames : 0 ,
68+ // Pre-render all spinner frames for fast lookup during render
69+ styledFrames := make ([]string , len (spinnerChars ))
70+ for i , char := range spinnerChars {
71+ styledFrames [i ] = dotsStyle .Render (char )
8372 }
84- }
8573
86- func (s Spinner ) Init () tea.Cmd {
87- return s .Tick ()
74+ return Spinner {
75+ dotsStyle : dotsStyle ,
76+ styledSpinnerFrames : styledFrames ,
77+ mode : mode ,
78+ currentMessage : defaultMessages [rand .IntN (len (defaultMessages ))],
79+ lightPosition : - 3 ,
80+ id : int (lastID .Add (1 )),
81+ direction : 1 ,
82+ }
8883}
8984
9085func (s Spinner ) Reset () Spinner {
@@ -93,107 +88,64 @@ func (s Spinner) Reset() Spinner {
9388
9489func (s Spinner ) Update (message tea.Msg ) (layout.Model , tea.Cmd ) {
9590 msg , ok := message .(tickMsg )
96- if ! ok {
97- return s , nil
98- }
99-
100- if msg .ID > 0 && msg .ID != s .id {
101- return s , nil
102- }
103- if msg .tag > 0 && msg .tag != s .tag {
91+ if ! ok || (msg .id > 0 && msg .id != s .id ) || (msg .tag > 0 && msg .tag != s .tag ) {
10492 return s , nil
10593 }
10694
10795 s .tag ++
10896 s .frame ++
10997
110- if s .pauseFrames > 0 {
111- s .pauseFrames --
112- if s .pauseFrames == 0 {
113- s .direction = - 1
114- }
115- } else {
116- s .lightPosition += s .direction
117-
118- // Use rune count for proper Unicode character handling in light animation
119- messageRuneCount := len ([]rune (s .currentMessage ))
120- if s .direction == 1 && s .lightPosition > messageRuneCount + 2 {
121- s .pauseFrames = 6
122- }
123-
124- if s .direction == - 1 && s .lightPosition < - 3 {
125- s .direction = 1
98+ // Light animation only needed for ModeBoth
99+ if s .mode == ModeBoth {
100+ if s .pauseFrames > 0 {
101+ s .pauseFrames --
102+ if s .pauseFrames == 0 {
103+ s .direction = - 1
104+ }
105+ } else {
106+ s .lightPosition += s .direction
107+ if s .direction == 1 && s .lightPosition > len ([]rune (s .currentMessage ))+ 2 {
108+ s .pauseFrames = 6
109+ } else if s .direction == - 1 && s .lightPosition < - 3 {
110+ s .direction = 1
111+ }
126112 }
127113 }
128114
129115 return s , s .Tick ()
130116}
131117
132118func (s Spinner ) View () string {
133- return s . render ()
134- }
135-
136- func ( s Spinner ) SetSize ( _ , _ int ) tea. Cmd {
137- return nil
119+ spinner := s . styledSpinnerFrames [ s . frame % len ( s . styledSpinnerFrames )]
120+ if s . mode == ModeSpinnerOnly {
121+ return spinner
122+ }
123+ return spinner + " " + s . renderMessage ()
138124}
139125
126+ func (s Spinner ) SetSize (_ , _ int ) tea.Cmd { return nil }
127+ func (s Spinner ) Init () tea.Cmd { return s .Tick () }
140128func (s Spinner ) Tick () tea.Cmd {
141- return tea .Tick (50 * time .Millisecond , func (t time.Time ) tea.Msg {
142- return tickMsg {
143- Time : t ,
144- ID : s .id ,
145- tag : s .tag ,
146- }
129+ return tea .Tick (50 * time .Millisecond , func (time.Time ) tea.Msg {
130+ return tickMsg {id : s .id , tag : s .tag }
147131 })
148132}
149133
150- func (s Spinner ) render () string {
151- message := s .currentMessage
152- output := make ([]rune , 0 , len (message ))
153-
154- for i , char := range message {
155- distance := abs (i - s .lightPosition )
156-
157- var style lipgloss.Style
158- switch distance {
159- case 0 :
160- style = styles .SpinnerTextBrightestStyle
161- case 1 :
162- style = styles .SpinnerTextBrightStyle
163- case 2 :
164- style = styles .SpinnerTextDimStyle
165- default :
166- style = styles .SpinnerTextDimmestStyle
167- }
168-
169- output = append (output , []rune (style .Render (string (char )))... )
170- }
171-
172- spinnerChars := []string {"⠋" , "⠙" , "⠹" , "⠸" , "⠼" , "⠴" , "⠦" , "⠧" , "⠇" , "⠏" }
173- spinnerChar := spinnerChars [s .frame % len (spinnerChars )]
174- spinnerStyled := s .dotsStyle .Render (spinnerChar )
175-
176- switch s .mode {
177- case ModeSpinnerOnly :
178- return spinnerStyled
179- case ModeMessageOnly :
180- return string (output )
181- }
182-
183- return spinnerStyled + " " + string (output )
184- }
185-
186- func (s * Spinner ) Render () string {
187- return s .render ()
188- }
134+ var spinnerChars = []string {"⠋" , "⠙" , "⠹" , "⠸" , "⠼" , "⠴" , "⠦" , "⠧" , "⠇" , "⠏" }
189135
190- func (s * Spinner ) SetMessage (message string ) {
191- s .currentMessage = message
136+ // lightStyles maps distance from light position to style (0=brightest, 1=bright, 2=dim, 3+=dimmest).
137+ var lightStyles = []lipgloss.Style {
138+ styles .SpinnerTextBrightestStyle ,
139+ styles .SpinnerTextBrightStyle ,
140+ styles .SpinnerTextDimStyle ,
141+ styles .SpinnerTextDimmestStyle ,
192142}
193143
194- func abs (x int ) int {
195- if x < 0 {
196- return - x
144+ func (s Spinner ) renderMessage () string {
145+ var out strings.Builder
146+ for i , char := range s .currentMessage {
147+ dist := min (max (i - s .lightPosition , s .lightPosition - i ), len (lightStyles )- 1 )
148+ out .WriteString (lightStyles [dist ].Render (string (char )))
197149 }
198- return x
150+ return out . String ()
199151}
0 commit comments