Skip to content

feat(stloader): add terminal spinner package with shining text effect#189

Merged
AnnatarHe merged 3 commits intomainfrom
feat/add-stloader-package
Jan 3, 2026
Merged

feat(stloader): add terminal spinner package with shining text effect#189
AnnatarHe merged 3 commits intomainfrom
feat/add-stloader-package

Conversation

@AnnatarHe
Copy link
Copy Markdown
Contributor

Summary

  • Add new stloader package for displaying loading spinners with animated text effects
  • Includes Braille dots spinner animation for smooth visual feedback
  • Features optional "shining" text effect with left-to-right color sweep (20% lighter highlight)
  • Thread-safe implementation with Start(), Stop(), and UpdateText() methods
  • Uses only Go standard library (no third-party dependencies)

Features

  • Spinner: Configurable symbols with Braille dots as default (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏)
  • Shining Effect: Left-to-right color sweep at configurable intervals (default 32ms)
  • Colors: ANSI 24-bit RGB color support for modern terminals
  • Thread Safety: Mutex-protected state with proper goroutine coordination

Usage

import "github.com/malamtime/cli/stloader"

// Simple usage
l := stloader.NewLoaderWithText("Loading...")
l.Start()
defer l.Stop()

// With shining effect
l := stloader.NewLoader(stloader.LoaderConfig{
    Text:          "Processing",
    EnableShining: true,
    BaseColor:     stloader.RGB{R: 100, G: 180, B: 255},
})
l.Start()
defer l.Stop()

Test plan

  • Unit tests for Start/Stop lifecycle
  • Unit tests for double Start/Stop safety
  • Unit tests for UpdateText during animation
  • Unit tests for shining effect color codes
  • Manual testing with cmd/loadertest demo

🤖 Generated with Claude Code

Add new stloader package for displaying loading spinners with optional
animated text effects. Features include:

- Braille dots spinner animation (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏)
- Optional "shining" text effect with left-to-right color sweep
- Configurable base color with 20% lighter highlight
- Thread-safe Start/Stop/UpdateText methods
- Standard library only (no third-party dependencies)
- Uses ANSI 24-bit color escape codes for terminal colors

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @AnnatarHe, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new stloader Go package, providing a sophisticated terminal loading spinner with customizable text effects. The primary goal is to enhance the user experience in command-line applications by offering visually engaging feedback during background processes. It includes features like Braille dot animations and a unique "shining" text effect, all implemented with thread safety and without relying on any third-party libraries.

Highlights

  • New stloader package: Introduces a new Go package, stloader, designed to display animated loading spinners in terminal applications.
  • Advanced Visual Effects: Features a Braille dots spinner animation and an optional "shining" text effect with a left-to-right color sweep, highlighting text with a 20% lighter color.
  • Thread-Safe Implementation: Provides thread-safe Start(), Stop(), and UpdateText() methods for robust control over the loader's lifecycle.
  • Zero External Dependencies: The entire package is built using only the Go standard library, ensuring a lightweight and self-contained solution.
  • Demo and Tests: Includes a cmd/loadertest demo application and comprehensive unit tests covering various functionalities and edge cases.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 3, 2026

Codecov Report

❌ Patch coverage is 75.32468% with 38 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cmd/loadertest/main.go 0.00% 15 Missing ⚠️
commands/auth.go 0.00% 9 Missing ⚠️
commands/grep.go 0.00% 7 Missing ⚠️
stloader/loader.go 93.85% 4 Missing and 3 partials ⚠️
Flag Coverage Δ
unittests 20.47% <75.32%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
commands/query.go 86.53% <100.00%> (+0.53%) ⬆️
commands/grep.go 0.00% <0.00%> (ø)
stloader/loader.go 93.85% <93.85%> (ø)
commands/auth.go 0.00% <0.00%> (ø)
cmd/loadertest/main.go 0.00% <0.00%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new stloader package for terminal spinners, which is a great addition. The implementation is thread-safe and includes good test coverage. My review focuses on a few areas for improvement, including fixing a configuration bug, enhancing performance in the animation loop, and addressing an unused configuration option. Overall, this is a solid contribution.

Comment thread stloader/loader.go Outdated
Comment on lines +74 to +77
// HideCursor defaults to true (we check if explicitly set to false)
// Since bool zero value is false, we need a different approach
// For simplicity, we'll always hide cursor by default
cfg.HideCursor = true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation for HideCursor is buggy because it unconditionally sets the value to true, overriding any user-provided configuration. This prevents users from disabling the cursor-hiding feature.

To correctly handle a boolean option that should default to true, the idiomatic approach in Go is to use a pointer (*bool). This allows you to distinguish between the field being unset (nil) and being explicitly set to false.

Here's how you could implement this fix:

  1. Change LoaderConfig:
    Modify the HideCursor field to be a pointer.

    // In LoaderConfig struct
    HideCursor *bool
  2. Update NewLoader:
    Set the default value if the field is nil.

    // In NewLoader function
    if cfg.HideCursor == nil {
        v := true
        cfg.HideCursor = &v
    }
  3. Update usage sites:
    Dereference the pointer when accessing the value.

    // e.g., in Start() and Stop()
    if *l.config.HideCursor {
        // ...
    }

Comment thread stloader/loader.go Outdated
// BaseColor is the base text color (user-defined)
BaseColor RGB
// DarkTheme indicates if the terminal is in dark theme (highlighted char is 20% lighter)
DarkTheme bool
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The DarkTheme field is declared in LoaderConfig but is not used anywhere in the package. This could be confusing for users of the API. It should be removed to keep the configuration clean and intentional.

Comment thread stloader/loader.go Outdated
Comment on lines +170 to +189
l.render()

// Update highlight index for shining effect
if l.config.EnableShining {
l.mu.Lock()
textLen := len([]rune(l.config.Text))
if textLen > 0 {
l.highlightIndex = (l.highlightIndex + 1) % textLen
}
l.mu.Unlock()
}

// Update spinner symbol at appropriate interval
spinCounter++
if spinCounter >= spinThreshold {
l.mu.Lock()
l.symbolIdx = (l.symbolIdx + 1) % len(l.config.Symbols)
l.mu.Unlock()
spinCounter = 0
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The animate function's main loop acquires and releases the mutex multiple times per tick: once in render(), and then up to twice more for updating highlightIndex and symbolIdx. This can be optimized by using a single lock for all shared state access within a single tick.

To implement this, you can modify render() to no longer acquire the lock itself, and then wrap the call to render() and the state updates in animate within a single lock-unlock block as suggested. This improves both readability and performance by reducing lock contention.

l.mu.Lock()
			l.render()

			// Update highlight index for shining effect
			if l.config.EnableShining {
				textLen := len([]rune(l.config.Text))
				if textLen > 0 {
					l.highlightIndex = (l.highlightIndex + 1) % textLen
				}
			}

			// Update spinner symbol at appropriate interval
			spinCounter++
			if spinCounter >= spinThreshold {
				l.symbolIdx = (l.symbolIdx + 1) % len(l.config.Symbols)
				spinCounter = 0
			}
			l.mu.Unlock()

Comment thread stloader/loader.go
Comment on lines +235 to +239
if i == highlightIdx {
result.WriteString(colorize(string(r), highlightColor))
} else {
result.WriteString(colorize(string(r), baseColor))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Inside the loop in renderShiningText, string(r) creates a new string from a rune in each iteration. This causes unnecessary memory allocations, which can be inefficient for long text strings. You can optimize this by avoiding the intermediate string conversion and writing the color codes and the character directly to the strings.Builder using fmt.Fprintf.

if i == highlightIdx {
			fmt.Fprintf(&result, "\033[38;2;%d;%d;%dm%c", highlightColor.R, highlightColor.G, highlightColor.B, r)
		} else {
			fmt.Fprintf(&result, "\033[38;2;%d;%d;%dm%c", baseColor.R, baseColor.G, baseColor.B, r)
		}

AnnatarHe and others added 2 commits January 4, 2026 00:58
Replace all usages of github.com/briandowns/spinner with the new
stloader package. All commands now have consistent visual feedback
with the shining text effect.

Changes:
- grep.go: Use stloader with "Searching commands..." text
- auth.go: Use stloader with "Waiting for authentication..." text
- query.go: Use stloader with "Querying AI..." text
- Remove unused briandowns/spinner dependency

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 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>
@AnnatarHe AnnatarHe merged commit a9f7747 into main Jan 3, 2026
3 checks passed
@AnnatarHe AnnatarHe deleted the feat/add-stloader-package branch January 3, 2026 17:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant