83 lines
2.2 KiB
Go
83 lines
2.2 KiB
Go
package util
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mattn/go-isatty"
|
|
)
|
|
|
|
// ProgressBar renders an inline progress bar to stderr for long per-item loops
|
|
// (e.g. fetching metadata for each issue in a milestone report). It writes only
|
|
// to stderr, so it never corrupts report output sent to stdout or a file. When
|
|
// stderr is not a terminal the bar stays silent to avoid polluting piped or
|
|
// redirected output.
|
|
type ProgressBar struct {
|
|
label string
|
|
total int
|
|
width int
|
|
start time.Time
|
|
enabled bool
|
|
}
|
|
|
|
// NewProgressBar creates a progress bar for total steps with the given label.
|
|
func NewProgressBar(label string, total int) *ProgressBar {
|
|
fd := os.Stderr.Fd()
|
|
return &ProgressBar{
|
|
label: label,
|
|
total: total,
|
|
width: 30,
|
|
start: time.Now(),
|
|
enabled: isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd),
|
|
}
|
|
}
|
|
|
|
// Update redraws the bar at the given 1-based step. detail is shown after the
|
|
// counter, e.g. the issue number currently being fetched ("#46029").
|
|
func (p *ProgressBar) Update(current int, detail string) {
|
|
if p == nil || !p.enabled || p.total <= 0 {
|
|
return
|
|
}
|
|
if current < 0 {
|
|
current = 0
|
|
}
|
|
if current > p.total {
|
|
current = p.total
|
|
}
|
|
frac := float64(current) / float64(p.total)
|
|
filled := int(frac * float64(p.width))
|
|
bar := strings.Repeat("█", filled) + strings.Repeat("░", p.width-filled)
|
|
|
|
line := fmt.Sprintf("%s [%s] %d/%d (%.0f%%)", p.label, bar, current, p.total, frac*100)
|
|
if detail != "" {
|
|
line += " " + detail
|
|
}
|
|
if eta := p.eta(current); eta != "" {
|
|
line += " " + eta
|
|
}
|
|
|
|
// \r returns to column 0; \033[K clears leftovers from a longer prior line.
|
|
fmt.Fprintf(os.Stderr, "\r\033[K%s", line)
|
|
}
|
|
|
|
// eta estimates remaining time from the average duration per completed step.
|
|
func (p *ProgressBar) eta(current int) string {
|
|
if current <= 0 || current >= p.total {
|
|
return ""
|
|
}
|
|
per := time.Since(p.start) / time.Duration(current)
|
|
remaining := (per * time.Duration(p.total-current)).Round(time.Second)
|
|
return fmt.Sprintf("ETA %s", remaining)
|
|
}
|
|
|
|
// Done terminates the bar's line so subsequent output starts cleanly. It is a
|
|
// no-op when the bar is disabled.
|
|
func (p *ProgressBar) Done() {
|
|
if p == nil || !p.enabled {
|
|
return
|
|
}
|
|
fmt.Fprintln(os.Stderr)
|
|
}
|