/* ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· : : : █▀ █ █▀▀ · Blazing-fast pentesting suite : : ▄█ █ █▀ · BSD 3-Clause License : : : : (c) 2022-2026 vmfunc, xyzeva, : : lunchcat alumni & contributors : : : ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· */ package output import ( "fmt" "sync" "sync/atomic" ) // Progress bar configuration const ( progressWidth = 30 progressFilled = "=" progressCurrent = ">" progressEmpty = " " ) // Progress displays a progress bar for operations with known counts type Progress struct { total int64 current int64 message string lastItem string mu sync.Mutex paused bool lastShown int // last printed milestone bucket in non-tty mode } // NewProgress creates a new progress bar func NewProgress(total int, message string) *Progress { return &Progress{ total: int64(total), message: message, } } // Increment advances the progress by 1 and optionally updates the current item func (p *Progress) Increment(item string) { atomic.AddInt64(&p.current, 1) p.mu.Lock() p.lastItem = item paused := p.paused p.mu.Unlock() if !paused { p.render() } } // Set sets the progress to a specific value func (p *Progress) Set(current int, item string) { atomic.StoreInt64(&p.current, int64(current)) p.mu.Lock() p.lastItem = item paused := p.paused p.mu.Unlock() if !paused { p.render() } } // Pause temporarily stops rendering (use before printing other output) func (p *Progress) Pause() { p.mu.Lock() p.paused = true p.mu.Unlock() ClearLine() } // Resume resumes rendering after a pause func (p *Progress) Resume() { p.mu.Lock() p.paused = false p.mu.Unlock() p.render() } // Done clears the progress bar line func (p *Progress) Done() { // under -concurrency there is no live line to clear (render printed milestones), // and clearing would wipe another target's output. if apiMode || !IsTTY || concurrent { return } ClearLine() } func (p *Progress) render() { if apiMode || silent { return } // In non-TTY mode, or under -concurrency, print progress at milestones only // rather than redrawing a live line that parallel targets would corrupt. if !IsTTY || concurrent { current := atomic.LoadInt64(&p.current) total := p.total if total <= 0 { return } percent := int(current * 100 / total) // map current to a milestone bucket (0=none,1..5). concurrent workers // hammer the same bucket, so only print when the bucket advances. bucket := 0 switch { case current >= total: bucket = 5 case percent >= 75: bucket = 4 case percent >= 50: bucket = 3 case percent >= 25: bucket = 2 case current >= 1: bucket = 1 } p.mu.Lock() advanced := bucket > p.lastShown if advanced { p.lastShown = bucket } p.mu.Unlock() if advanced { fmt.Fprintf(sink, " [%d%%] %d/%d\n", percent, current, total) } return } current := atomic.LoadInt64(&p.current) total := p.total p.mu.Lock() lastItem := p.lastItem p.mu.Unlock() // Calculate percentage percent := 0 if total > 0 { percent = int(current * 100 / total) } // Build progress bar filled := 0 if total > 0 { filled = int(progressWidth * current / total) } if filled > progressWidth { filled = progressWidth } bar := "" for i := 0; i < progressWidth; i++ { switch { case i < filled: bar += progressFilled case i == filled && current < total: bar += progressCurrent default: bar += progressEmpty } } // Truncate item if too long maxItemLen := 30 if len(lastItem) > maxItemLen { lastItem = lastItem[:maxItemLen-3] + "..." } // Format: [========> ] 45% (4500/10000) /admin line := fmt.Sprintf(" [%s] %3d%% (%d/%d) %s", prefixInfo.Render(bar), percent, current, total, Muted.Render(lastItem), ) ClearLine() fmt.Fprint(sink, line) }