diff options
Diffstat (limited to 'internal/progress/meter.go')
| -rw-r--r-- | internal/progress/meter.go | 130 |
1 files changed, 130 insertions, 0 deletions
diff --git a/internal/progress/meter.go b/internal/progress/meter.go new file mode 100644 index 00000000..0e4138de --- /dev/null +++ b/internal/progress/meter.go @@ -0,0 +1,130 @@ +package progress + +import ( + "time" + + "lindenii.org/go/lgo/intconv" + "lindenii.org/go/lgo/iowrap" +) + +const ( + updateInterval = time.Second + throughputInterval = 500 * time.Millisecond +) + +// Meter renders one in-place progress line. +type Meter struct { + writer iowrap.WriteFlusher + + title string + total uint64 + delay time.Duration + sparse bool + throughput bool + + startedAt time.Time + nextUpdateAt time.Time + nextThroughput time.Time + + lastDone uint64 + lastBytes uint64 + lastPercent int + lastCounterW int + sawValue bool + + throughputSuffix string +} + +// New creates one progress meter. +func New(opts Options) *Meter { + now := time.Now() + + return &Meter{ + writer: opts.Writer, + title: opts.Title, + total: opts.Total, + delay: max(opts.Delay, time.Duration(0)), + sparse: opts.Sparse, + throughput: opts.Throughput, + startedAt: now, + nextUpdateAt: now.Add(updateInterval), + nextThroughput: now.Add(throughputInterval), + lastPercent: -1, + } +} + +// Options configures one progress meter. +type Options struct { + Writer iowrap.WriteFlusher + + Title string + Total uint64 + + // Delay suppresses progress output until Delay has elapsed since Start. + Delay time.Duration + // Sparse forces one final 100% line at Stop when the caller sampled updates. + Sparse bool + // Throughput appends ", <total> | <rate>/s" and refreshes rate every 500ms. + Throughput bool +} + +// Set records current progress +// and renders when percent changed or the 1s tick elapsed. +func (meter *Meter) Set(done uint64, bytes uint64) { + meter.lastDone = done + meter.lastBytes = bytes + meter.sawValue = true + + if meter.writer == nil { + return + } + + now := time.Now() + forced := meter.consumeUpdateTick(now) + + percentChanged := false + + if meter.total > 0 { + percent, err := intconv.Uint64ToInt(done * 100 / meter.total) + if err != nil { + return // TODO + } + + percentChanged = percent != meter.lastPercent + } + + if !percentChanged && !forced { + return + } + + meter.render(now, "\r") +} + +// Stop forces the final progress line and appends ", <msg>.". +func (meter *Meter) Stop(msg string) { + if !meter.sawValue || meter.writer == nil { + return + } + + if msg == "" { + msg = "done" + } + + if meter.sparse && meter.total > 0 && meter.lastDone != meter.total { + meter.lastDone = meter.total + } + + meter.render(time.Now(), ", "+msg+".\n") +} + +func (meter *Meter) consumeUpdateTick(now time.Time) bool { + if now.Before(meter.nextUpdateAt) { + return false + } + + for !now.Before(meter.nextUpdateAt) { + meter.nextUpdateAt = meter.nextUpdateAt.Add(updateInterval) + } + + return true +} |
