Files
sif/internal/output/spinner.go
T
TigahandGitHub d13622dc1c feat(scan): add -concurrency flag to scan targets in parallel (#367)
* refactor(cli): extract per-target scan into scanTarget

pull the target loop body into scanTarget, which returns a targetScan
holding that target's findings, report rows, scan labels and log files
instead of appending onto run-wide slices. the run loop merges those in
target order and finishRun handles the post-loop notify/silent/report/
summary steps. behavior is identical for the sequential run today; the
isolation is the seam a bounded worker pool needs next.

* feat(cli): scan targets in parallel with -concurrency

add -concurrency N (default 1) to run the per-target scan pool over
several targets at once. scanTarget already returns isolated
accumulators, so workers share only the console: output.SetConcurrent
serializes sink writes and de-animates spinners/progress so parallel
lines stay whole. results merge in input order, and concurrency 1 keeps
the sequential path unchanged.
2026-07-12 14:23:35 -07:00

122 lines
3.0 KiB
Go

/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package output
import (
"fmt"
"sync"
"time"
)
// Spinner frames using simple ASCII
var spinnerFrames = []string{"|", "/", "-", "\\"}
// Spinner displays an animated spinner for indeterminate operations
type Spinner struct {
message string
running bool
done chan struct{}
mu sync.Mutex
interval time.Duration
}
// NewSpinner creates a new spinner with the given message
func NewSpinner(message string) *Spinner {
return &Spinner{
message: message,
interval: 100 * time.Millisecond,
done: make(chan struct{}),
}
}
// Start begins the spinner animation
func (s *Spinner) Start() {
if apiMode || silent {
return
}
s.mu.Lock()
if s.running {
s.mu.Unlock()
return
}
s.running = true
s.done = make(chan struct{})
s.mu.Unlock()
// In non-TTY mode, or when targets scan in parallel, just print the message
// once: a shared animated line can't be driven from concurrent goroutines.
if !IsTTY || concurrent {
fmt.Fprintf(sink, " %s...\n", s.message)
return
}
go s.animate()
}
// Stop halts the spinner and clears the line
func (s *Spinner) Stop() {
if apiMode || silent {
return
}
s.mu.Lock()
if !s.running {
s.mu.Unlock()
return
}
s.running = false
close(s.done)
s.mu.Unlock()
// Give animation goroutine time to exit
time.Sleep(s.interval)
// Clear the spinner line
if IsTTY {
ClearLine()
}
}
// Update changes the spinner message while running
func (s *Spinner) Update(message string) {
s.mu.Lock()
s.message = message
s.mu.Unlock()
}
func (s *Spinner) animate() {
frame := 0
ticker := time.NewTicker(s.interval)
defer ticker.Stop()
for {
select {
case <-s.done:
return
case <-ticker.C:
s.mu.Lock()
msg := s.message
s.mu.Unlock()
spinnerChar := prefixInfo.Render(spinnerFrames[frame])
line := fmt.Sprintf("\r %s %s", spinnerChar, msg)
fmt.Fprint(sink, "\033[2K") // Clear line
fmt.Fprint(sink, line)
frame = (frame + 1) % len(spinnerFrames)
}
}
}