aboutsummaryrefslogtreecommitdiff
path: root/internal/progress/meter.go
blob: 0e4138defc592a8e5c0e5cba8bb5203f7f0fa371 (about) (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
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
}