Cybersecurity-Projects/PROJECTS/intermediate/secrets-scanner/internal/ui/spinner.go

104 lines
1.8 KiB
Go

/*
©AngelaMos | 2026
spinner.go
Braille-frame terminal spinner with goroutine lifecycle management
Animates a 10-frame Braille spinner at 80ms intervals using a background
goroutine. Start() and Stop() are safe to call concurrently. The cursor is
hidden during animation and restored on Stop(). Used to indicate scan progress
when verbose mode is off.
Key exports:
Spinner - spinner with Start() and Stop() methods
NewSpinner - creates a Spinner with a display message
Connects to:
cli/scan.go - creates and controls spinners for scan and HIBP check phases
ui/color.go - uses CyanBold and HiMagenta for frame and message styling
*/
package ui
import (
"fmt"
"strings"
"sync"
"time"
)
var frames = []string{
"\u280b", "\u2819", "\u2839", "\u2838",
"\u283c", "\u2834", "\u2826", "\u2827",
"\u2807", "\u280f",
}
type Spinner struct {
msg string
done chan struct{}
wg sync.WaitGroup
mu sync.Mutex
active bool
}
func NewSpinner(msg string) *Spinner {
return &Spinner{msg: msg}
}
func (s *Spinner) Start() {
s.mu.Lock()
if s.active {
s.mu.Unlock()
return
}
s.active = true
s.done = make(chan struct{})
s.wg.Add(1)
s.mu.Unlock()
go s.run()
}
func (s *Spinner) Stop() {
s.mu.Lock()
if !s.active {
s.mu.Unlock()
return
}
s.active = false
close(s.done)
s.mu.Unlock()
s.wg.Wait()
}
func (s *Spinner) run() {
defer s.wg.Done()
defer fmt.Print("\033[?25h")
fmt.Print("\033[?25l")
ticker := time.NewTicker(80 * time.Millisecond)
defer ticker.Stop()
idx := 0
for {
select {
case <-s.done:
clearLine()
fmt.Print("\033[?25h")
return
case <-ticker.C:
frame := frames[idx%len(frames)]
fmt.Printf(
"\r %s %s",
CyanBold(frame),
HiMagenta(s.msg),
)
idx++
}
}
}
func clearLine() {
fmt.Print("\r" + strings.Repeat(" ", 80) + "\r")
}