11package components
22
33import (
4+ "context"
45 "fmt"
6+ "sync"
57 "time"
68
79 "github.com/rivo/tview"
@@ -15,9 +17,10 @@ const appName = "pvetui"
1517type Header struct {
1618 * tview.TextView
1719
20+ mu sync.Mutex
1821 isLoading bool
1922 loadingText string
20- stopLoading chan bool
23+ loadingCancel context. CancelFunc
2124 app * tview.Application
2225 currentProfile string // Track the current active profile
2326}
@@ -34,8 +37,7 @@ func NewHeader() *Header {
3437 header .SetTextColor (theme .Colors .HeaderText )
3538
3639 return & Header {
37- TextView : header ,
38- stopLoading : make (chan bool , 1 ),
40+ TextView : header ,
3941 }
4042}
4143
@@ -56,43 +58,57 @@ func (h *Header) SetText(text string) {
5658
5759// ShowLoading displays an animated loading indicator.
5860func (h * Header ) ShowLoading (message string ) {
59- // Stop any existing loading first to avoid overlapping animations
60- if h .isLoading {
61- h .isLoading = false
62- select {
63- case h .stopLoading <- true :
64- default :
65- }
61+ h .mu .Lock ()
62+ if h .isLoading && h .loadingText == message {
63+ h .mu .Unlock ()
64+ return
65+ }
66+
67+ if h .isLoading && h .loadingCancel != nil {
68+ h .loadingCancel ()
6669 }
6770
6871 h .isLoading = true
6972 h .loadingText = message
70- h .stopLoading = make (chan bool , 1 )
73+ ctx , cancel := context .WithCancel (context .Background ())
74+ h .loadingCancel = cancel
75+ h .mu .Unlock ()
7176
72- // Start the loading animation
73- go h .animateLoading ()
77+ go h .animateLoading (ctx )
7478}
7579
7680// StopLoading stops the loading animation.
7781func (h * Header ) StopLoading () {
78- if h .isLoading {
79- h .isLoading = false
80- select {
81- case h .stopLoading <- true :
82- default :
83- }
82+ h .mu .Lock ()
83+ if ! h .isLoading {
84+ h .mu .Unlock ()
85+ return
86+ }
87+ h .isLoading = false
88+ if h .loadingCancel != nil {
89+ h .loadingCancel ()
90+ h .loadingCancel = nil
8491 }
92+ h .mu .Unlock ()
8593}
8694
8795// IsLoading reports whether the header is currently showing a loading state.
8896func (h * Header ) IsLoading () bool {
97+ h .mu .Lock ()
98+ defer h .mu .Unlock ()
8999 return h .isLoading
90100}
91101
92102// ShowSuccess displays a success message temporarily.
93103func (h * Header ) ShowSuccess (message string ) {
94104 // Mark not loading before changing text to prevent race with animateLoading
105+ h .mu .Lock ()
95106 h .isLoading = false
107+ if h .loadingCancel != nil {
108+ h .loadingCancel ()
109+ h .loadingCancel = nil
110+ }
111+ h .mu .Unlock ()
96112 h .StopLoading ()
97113 h .SetText (theme .ReplaceSemanticTags ("[success]✓ " + message + "[-]" ))
98114
@@ -102,7 +118,13 @@ func (h *Header) ShowSuccess(message string) {
102118
103119// ShowError displays an error message temporarily.
104120func (h * Header ) ShowError (message string ) {
121+ h .mu .Lock ()
105122 h .isLoading = false
123+ if h .loadingCancel != nil {
124+ h .loadingCancel ()
125+ h .loadingCancel = nil
126+ }
127+ h .mu .Unlock ()
106128 h .StopLoading ()
107129 h .SetText (theme .ReplaceSemanticTags ("[error]✗ " + message + "[-]" ))
108130
@@ -112,7 +134,13 @@ func (h *Header) ShowError(message string) {
112134
113135// ShowWarning displays a warning message temporarily.
114136func (h * Header ) ShowWarning (message string ) {
137+ h .mu .Lock ()
115138 h .isLoading = false
139+ if h .loadingCancel != nil {
140+ h .loadingCancel ()
141+ h .loadingCancel = nil
142+ }
143+ h .mu .Unlock ()
116144 h .StopLoading ()
117145 h .SetText (theme .ReplaceSemanticTags ("[warning]⚠ " + message + "[-]" ))
118146
@@ -131,7 +159,13 @@ func (h *Header) formatProfileText(profileName string) string {
131159
132160// ShowActiveProfile displays the active profile in the header.
133161func (h * Header ) ShowActiveProfile (profileName string ) {
162+ h .mu .Lock ()
134163 h .isLoading = false
164+ if h .loadingCancel != nil {
165+ h .loadingCancel ()
166+ h .loadingCancel = nil
167+ }
168+ h .mu .Unlock ()
135169 h .StopLoading ()
136170 h .currentProfile = profileName // Store the profile name
137171 h .SetText (h .formatProfileText (profileName ))
@@ -155,7 +189,10 @@ func (h *Header) clearMessageAfterDelay(delay time.Duration) {
155189 if h .app != nil {
156190 h .app .QueueUpdateDraw (func () {
157191 // Avoid overriding an active loading indicator that may have started after ShowSuccess/ShowError
158- if h .isLoading {
192+ h .mu .Lock ()
193+ loading := h .isLoading
194+ h .mu .Unlock ()
195+ if loading {
159196 return
160197 }
161198 // Restore the current profile if it exists, otherwise reset to default
@@ -170,28 +207,40 @@ func (h *Header) clearMessageAfterDelay(delay time.Duration) {
170207}
171208
172209// animateLoading displays an animated loading indicator.
173- func (h * Header ) animateLoading () {
210+ func (h * Header ) animateLoading (ctx context. Context ) {
174211 spinner := []string {"⠋" , "⠙" , "⠹" , "⠸" , "⠼" , "⠴" , "⠦" , "⠧" , "⠇" , "⠏" }
175212 index := 0
213+ ticker := time .NewTicker (100 * time .Millisecond )
214+ defer ticker .Stop ()
176215
177- for h . isLoading {
216+ for {
178217 select {
179- case <- h . stopLoading :
218+ case <- ctx . Done () :
180219 return
181- default :
220+ case <- ticker .C :
221+ h .mu .Lock ()
222+ loading := h .isLoading
223+ h .mu .Unlock ()
224+
225+ if ! loading {
226+ return
227+ }
228+
182229 if h .app != nil {
230+ spinnerChar := spinner [index ]
183231 h .app .QueueUpdateDraw (func () {
232+ h .mu .Lock ()
184233 if ! h .isLoading {
234+ h .mu .Unlock ()
185235 return
186236 }
187- spinnerChar := spinner [index ]
188- h .SetText (theme .ReplaceSemanticTags (fmt .Sprintf ("[info]%s %s[-]" , spinnerChar , h .loadingText )))
237+ currentMessage := h .loadingText
238+ h .mu .Unlock ()
239+ h .SetText (theme .ReplaceSemanticTags (fmt .Sprintf ("[info]%s %s[-]" , spinnerChar , currentMessage )))
189240 })
190241 }
191242
192243 index = (index + 1 ) % len (spinner )
193-
194- time .Sleep (100 * time .Millisecond )
195244 }
196245 }
197246}
0 commit comments