Files
fleet/tools/github-manage/pkg/util/progress.go
T

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)
}