aboutsummaryrefslogtreecommitdiff
path: root/internal/progress/meter.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/progress/meter.go')
-rw-r--r--internal/progress/meter.go130
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
+}