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 ", | /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 ", .". 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 }