diff options
| -rw-r--r-- | internal/progress/constants.go | 11 | ||||
| -rw-r--r-- | internal/progress/consume.go | 15 | ||||
| -rw-r--r-- | internal/progress/counters.go | 14 | ||||
| -rw-r--r-- | internal/progress/humanize.go | 21 | ||||
| -rw-r--r-- | internal/progress/meter.go | 30 | ||||
| -rw-r--r-- | internal/progress/new.go | 22 | ||||
| -rw-r--r-- | internal/progress/options.go | 22 | ||||
| -rw-r--r-- | internal/progress/refresh.go | 25 | ||||
| -rw-r--r-- | internal/progress/render.go | 34 | ||||
| -rw-r--r-- | internal/progress/set.go | 30 | ||||
| -rw-r--r-- | internal/progress/stop.go | 20 |
11 files changed, 244 insertions, 0 deletions
diff --git a/internal/progress/constants.go b/internal/progress/constants.go new file mode 100644 index 00000000..c73adb2e --- /dev/null +++ b/internal/progress/constants.go @@ -0,0 +1,11 @@ +package progress + +import "time" + +const ( + // DefaultDelay is the default delayed-progress interval. + DefaultDelay = time.Second + + updateInterval = time.Second + throughputInterval = 500 * time.Millisecond +) diff --git a/internal/progress/consume.go b/internal/progress/consume.go new file mode 100644 index 00000000..fa142f49 --- /dev/null +++ b/internal/progress/consume.go @@ -0,0 +1,15 @@ +package progress + +import "time" + +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 +} diff --git a/internal/progress/counters.go b/internal/progress/counters.go new file mode 100644 index 00000000..25d81878 --- /dev/null +++ b/internal/progress/counters.go @@ -0,0 +1,14 @@ +package progress + +import "fmt" + +func (meter *Meter) renderCounters() string { + if meter.total > 0 { + percent := int(meter.lastDone * 100 / meter.total) + meter.lastPercent = percent + + return fmt.Sprintf("%3d%% (%d/%d)%s", percent, meter.lastDone, meter.total, meter.throughputSuffix) + } + + return fmt.Sprintf("%d%s", meter.lastDone, meter.throughputSuffix) +} diff --git a/internal/progress/humanize.go b/internal/progress/humanize.go new file mode 100644 index 00000000..8363d6b1 --- /dev/null +++ b/internal/progress/humanize.go @@ -0,0 +1,21 @@ +package progress + +import "fmt" + +func humanizeBytes(n uint64) string { + const unit = 1024 + if n < unit { + return fmt.Sprintf("%d B", n) + } + + value := float64(n) + units := []string{"KiB", "MiB", "GiB", "TiB", "PiB"} + for i := 0; i < len(units); i++ { + value /= unit + if value < unit || i == len(units)-1 { + return fmt.Sprintf("%.2f %s", value, units[i]) + } + } + + return fmt.Sprintf("%d B", n) +} diff --git a/internal/progress/meter.go b/internal/progress/meter.go new file mode 100644 index 00000000..3c0079dd --- /dev/null +++ b/internal/progress/meter.go @@ -0,0 +1,30 @@ +package progress + +import ( + "io" + "time" +) + +// Meter renders one in-place progress line. +type Meter struct { + writer io.Writer + flush func() error + + 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 +} diff --git a/internal/progress/new.go b/internal/progress/new.go new file mode 100644 index 00000000..a86a0660 --- /dev/null +++ b/internal/progress/new.go @@ -0,0 +1,22 @@ +package progress + +import "time" + +// New creates one progress meter. +func New(opts Options) *Meter { + now := time.Now() + + return &Meter{ + writer: opts.Writer, + flush: opts.Flush, + 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, + } +} diff --git a/internal/progress/options.go b/internal/progress/options.go new file mode 100644 index 00000000..d7c08894 --- /dev/null +++ b/internal/progress/options.go @@ -0,0 +1,22 @@ +package progress + +import ( + "io" + "time" +) + +// Options configures one progress meter. +type Options struct { + Writer io.Writer + Flush func() error + + 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 +} diff --git a/internal/progress/refresh.go b/internal/progress/refresh.go new file mode 100644 index 00000000..ed1782db --- /dev/null +++ b/internal/progress/refresh.go @@ -0,0 +1,25 @@ +package progress + +import "time" + +func (meter *Meter) refreshThroughput(now time.Time) { + if !meter.throughput { + return + } + + if meter.nextThroughput.After(now) && meter.throughputSuffix != "" { + return + } + + for !now.Before(meter.nextThroughput) { + meter.nextThroughput = meter.nextThroughput.Add(throughputInterval) + } + + elapsed := now.Sub(meter.startedAt) + if elapsed <= 0 { + return + } + + rate := uint64(float64(meter.lastBytes) / elapsed.Seconds()) + meter.throughputSuffix = ", " + humanizeBytes(meter.lastBytes) + " | " + humanizeBytes(rate) + "/s" +} diff --git a/internal/progress/render.go b/internal/progress/render.go new file mode 100644 index 00000000..a2395d98 --- /dev/null +++ b/internal/progress/render.go @@ -0,0 +1,34 @@ +package progress + +import ( + "strings" + "time" + + "codeberg.org/lindenii/furgit/internal/utils" +) + +func (meter *Meter) render(now time.Time, eol string) { + if meter.delay > 0 && now.Sub(meter.startedAt) < meter.delay && eol == "\r" { + return + } + + meter.refreshThroughput(now) + + counters := meter.renderCounters() + clear := 0 + if len(counters) < meter.lastCounterW { + clear = meter.lastCounterW - len(counters) + 1 + } + meter.lastCounterW = len(counters) + + line := meter.title + ": " + counters + if clear > 0 { + line += strings.Repeat(" ", clear) + } + line += eol + + utils.BestEffortFprintf(meter.writer, "%s", line) + if meter.flush != nil { + _ = meter.flush() + } +} diff --git a/internal/progress/set.go b/internal/progress/set.go new file mode 100644 index 00000000..3a580a63 --- /dev/null +++ b/internal/progress/set.go @@ -0,0 +1,30 @@ +package progress + +import "time" + +// 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 := int(done * 100 / meter.total) + percentChanged = percent != meter.lastPercent + } + + if !percentChanged && !forced { + return + } + + meter.render(now, "\r") +} diff --git a/internal/progress/stop.go b/internal/progress/stop.go new file mode 100644 index 00000000..fdc3f9af --- /dev/null +++ b/internal/progress/stop.go @@ -0,0 +1,20 @@ +package progress + +import "time" + +// 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") +} |
