Skip to content

Commit 5d37a52

Browse files
AnnatarHeclaude
andcommitted
fix(stloader): address PR review comments
- Use *bool for HideCursor to distinguish unset (nil, defaults to true) from explicitly false - Remove unused DarkTheme field from LoaderConfig - Consolidate mutex locks in animate() to single lock-unlock per tick - Use fmt.Fprintf with %c in renderShiningText to avoid string(r) allocations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5f336b8 commit 5d37a52

2 files changed

Lines changed: 44 additions & 35 deletions

File tree

stloader/loader.go

Lines changed: 29 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,11 @@ type LoaderConfig struct {
3737
ShineInterval time.Duration
3838
// BaseColor is the base text color (user-defined)
3939
BaseColor RGB
40-
// DarkTheme indicates if the terminal is in dark theme (highlighted char is 20% lighter)
41-
DarkTheme bool
4240
// Writer is the output writer (default: os.Stdout)
4341
Writer io.Writer
4442
// HideCursor hides the cursor while loading (default: true)
45-
HideCursor bool
43+
// Use pointer to distinguish between unset (nil, defaults to true) and explicitly false
44+
HideCursor *bool
4645
}
4746

4847
// Loader represents a terminal spinner with optional shining text effect
@@ -71,10 +70,11 @@ func NewLoader(cfg LoaderConfig) *Loader {
7170
if cfg.Writer == nil {
7271
cfg.Writer = os.Stdout
7372
}
74-
// HideCursor defaults to true (we check if explicitly set to false)
75-
// Since bool zero value is false, we need a different approach
76-
// For simplicity, we'll always hide cursor by default
77-
cfg.HideCursor = true
73+
// HideCursor defaults to true if not explicitly set
74+
if cfg.HideCursor == nil {
75+
hideCursor := true
76+
cfg.HideCursor = &hideCursor
77+
}
7878

7979
return &Loader{
8080
config: cfg,
@@ -102,7 +102,7 @@ func (l *Loader) Start() {
102102
l.highlightIndex = 0
103103
l.mu.Unlock()
104104

105-
if l.config.HideCursor {
105+
if *l.config.HideCursor {
106106
fmt.Fprint(l.config.Writer, "\033[?25l") // Hide cursor
107107
}
108108

@@ -125,7 +125,7 @@ func (l *Loader) Stop() {
125125
// Clear the line
126126
fmt.Fprint(l.config.Writer, "\r\033[K")
127127

128-
if l.config.HideCursor {
128+
if *l.config.HideCursor {
129129
fmt.Fprint(l.config.Writer, "\033[?25h") // Show cursor
130130
}
131131
}
@@ -167,46 +167,44 @@ func (l *Loader) animate() {
167167
case <-l.stopChan:
168168
return
169169
case <-ticker.C:
170-
l.render()
170+
// Consolidate all shared state access within a single mutex lock
171+
l.mu.Lock()
172+
symbol := l.config.Symbols[l.symbolIdx]
173+
text := l.config.Text
174+
highlightIdx := l.highlightIndex
171175

172176
// Update highlight index for shining effect
173177
if l.config.EnableShining {
174-
l.mu.Lock()
175-
textLen := len([]rune(l.config.Text))
178+
textLen := len([]rune(text))
176179
if textLen > 0 {
177180
l.highlightIndex = (l.highlightIndex + 1) % textLen
178181
}
179-
l.mu.Unlock()
180182
}
181183

182184
// Update spinner symbol at appropriate interval
183185
spinCounter++
184186
if spinCounter >= spinThreshold {
185-
l.mu.Lock()
186187
l.symbolIdx = (l.symbolIdx + 1) % len(l.config.Symbols)
187-
l.mu.Unlock()
188188
spinCounter = 0
189189
}
190+
l.mu.Unlock()
191+
192+
// Render outside the lock since it only reads local copies
193+
l.renderWithValues(symbol, text, highlightIdx)
190194
}
191195
}
192196
}
193197

194-
// render draws the current state to the terminal
195-
func (l *Loader) render() {
196-
l.mu.Lock()
197-
symbol := l.config.Symbols[l.symbolIdx]
198-
text := l.config.Text
199-
highlightIdx := l.highlightIndex
200-
l.mu.Unlock()
201-
198+
// renderWithValues draws the current state to the terminal with pre-fetched values
199+
func (l *Loader) renderWithValues(symbol, text string, highlightIdx int) {
202200
var output strings.Builder
203201
output.WriteString("\r\033[K") // Clear line first
204202
output.WriteString(symbol)
205203

206204
if text != "" {
207205
output.WriteString(" ")
208206
if l.config.EnableShining {
209-
output.WriteString(l.renderShiningText(text, highlightIdx))
207+
l.renderShiningText(&output, text, highlightIdx)
210208
} else {
211209
output.WriteString(text)
212210
}
@@ -215,33 +213,30 @@ func (l *Loader) render() {
215213
fmt.Fprint(l.config.Writer, output.String())
216214
}
217215

218-
// renderShiningText renders text with the shining effect
219-
func (l *Loader) renderShiningText(text string, highlightIdx int) string {
216+
// renderShiningText renders text with the shining effect directly to the builder
217+
func (l *Loader) renderShiningText(w *strings.Builder, text string, highlightIdx int) {
220218
runes := []rune(text)
221219
if len(runes) == 0 {
222-
return ""
220+
return
223221
}
224222

225223
baseColor := l.config.BaseColor
226224
highlightColor := lightenColor(baseColor)
227225

228-
var result strings.Builder
229226
for i, r := range runes {
230227
if r == ' ' {
231-
result.WriteRune(r)
228+
w.WriteRune(r)
232229
continue
233230
}
234231

235232
if i == highlightIdx {
236-
result.WriteString(colorize(string(r), highlightColor))
233+
fmt.Fprintf(w, "\033[38;2;%d;%d;%dm%c", highlightColor.R, highlightColor.G, highlightColor.B, r)
237234
} else {
238-
result.WriteString(colorize(string(r), baseColor))
235+
fmt.Fprintf(w, "\033[38;2;%d;%d;%dm%c", baseColor.R, baseColor.G, baseColor.B, r)
239236
}
240237
}
241238
// Reset color at the end
242-
result.WriteString("\033[0m")
243-
244-
return result.String()
239+
w.WriteString("\033[0m")
245240
}
246241

247242
// lightenColor returns a color 20% lighter

stloader/loader_test.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,25 @@ func TestNewLoader(t *testing.T) {
2727
if l.config.Writer == nil {
2828
t.Error("Expected Writer to be set")
2929
}
30-
if !l.config.HideCursor {
30+
if l.config.HideCursor == nil || !*l.config.HideCursor {
3131
t.Error("Expected HideCursor to be true by default")
3232
}
3333
}
3434

35+
func TestNewLoaderWithHideCursorFalse(t *testing.T) {
36+
hideCursor := false
37+
l := NewLoader(LoaderConfig{
38+
HideCursor: &hideCursor,
39+
})
40+
41+
if l.config.HideCursor == nil {
42+
t.Fatal("Expected HideCursor to be set")
43+
}
44+
if *l.config.HideCursor != false {
45+
t.Error("Expected HideCursor to be false when explicitly set")
46+
}
47+
}
48+
3549
func TestNewLoaderWithText(t *testing.T) {
3650
text := "Loading..."
3751
l := NewLoaderWithText(text)

0 commit comments

Comments
 (0)