-
Notifications
You must be signed in to change notification settings - Fork 0
/
progressbar.go
257 lines (220 loc) · 6.75 KB
/
progressbar.go
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
package pb
import (
"fmt"
"strings"
"sync"
"github.com/fatih/color"
"github.com/sirupsen/logrus"
)
//nolint:gochecknoglobals
var (
colorFaint = color.New(color.Faint)
statusColors = map[Status]*color.Color{
Interrupted: color.New(color.FgRed),
Done: color.New(color.FgGreen),
Waiting: colorFaint,
}
)
const (
// DefaultWidth of the progress bar
DefaultWidth = 40
// threshold below which progress should be rendered as
// percentages instead of filling bars
minWidth = 8
)
// Status of the progress bar
type Status rune
// Progress bar status symbols
const (
Running Status = ' '
Waiting Status = '•'
Stopping Status = '↓'
Interrupted Status = '✗'
Done Status = '✓'
)
// ProgressBar is a simple thread-safe progressbar implementation with
// callbacks.
type ProgressBar struct {
mutex sync.RWMutex
width int
logger *logrus.Entry
status Status
left func() string
progress func() (progress float64, right []string)
hijack func() string
}
// ProgressBarOption is used for helper functions that modify the progressbar
// parameters, either in the constructor or via the Modify() method.
type ProgressBarOption func(*ProgressBar)
// WithLeft modifies the function that returns the left progressbar value.
func WithLeft(left func() string) ProgressBarOption {
return func(pb *ProgressBar) { pb.left = left }
}
// WithConstLeft sets the left progressbar value to the supplied const.
func WithConstLeft(left string) ProgressBarOption {
return func(pb *ProgressBar) {
pb.left = func() string { return left }
}
}
// WithLogger modifies the logger instance
func WithLogger(logger *logrus.Entry) ProgressBarOption {
return func(pb *ProgressBar) { pb.logger = logger }
}
// WithProgress modifies the progress calculation function.
func WithProgress(progress func() (float64, []string)) ProgressBarOption {
return func(pb *ProgressBar) { pb.progress = progress }
}
// WithStatus modifies the progressbar status
func WithStatus(status Status) ProgressBarOption {
return func(pb *ProgressBar) { pb.status = status }
}
// WithConstProgress sets the progress and right values to the supplied consts.
func WithConstProgress(progress float64, right ...string) ProgressBarOption {
return func(pb *ProgressBar) {
pb.progress = func() (float64, []string) { return progress, right }
}
}
// WithHijack replaces the progressbar Render function with the argument.
func WithHijack(hijack func() string) ProgressBarOption {
return func(pb *ProgressBar) { pb.hijack = hijack }
}
// New creates and initializes a new ProgressBar struct, calling all of the
// supplied options
func New(options ...ProgressBarOption) *ProgressBar {
pb := &ProgressBar{
mutex: sync.RWMutex{},
width: DefaultWidth,
}
pb.Modify(options...)
return pb
}
// Left returns the left part of the progressbar in a thread-safe way.
func (pb *ProgressBar) Left() string {
pb.mutex.RLock()
defer pb.mutex.RUnlock()
return pb.renderLeft(0)
}
// renderLeft renders the left part of the progressbar, replacing text
// exceeding maxLen with an ellipsis.
func (pb *ProgressBar) renderLeft(maxLen int) string {
var left string
if pb.left != nil {
l := pb.left()
if maxLen > 0 && len(l) > maxLen {
l = l[:maxLen-3] + "..."
}
left = l
}
return left
}
// Modify changes the progressbar options in a thread-safe way.
func (pb *ProgressBar) Modify(options ...ProgressBarOption) {
pb.mutex.Lock()
defer pb.mutex.Unlock()
for _, option := range options {
option(pb)
}
}
// ProgressBarRender stores the different rendered parts of the
// progress bar UI to allow dynamic positioning and padding of
// elements in the terminal output (e.g. for responsive progress
// bars).
type ProgressBarRender struct {
Right []string
progress, progressFill, progressPadding string
Left, Hijack string
status Status
Color bool
}
// Status returns an optionally colorized status string
func (pbr *ProgressBarRender) Status() string {
status := " "
if pbr.status > 0 {
status = string(pbr.status)
if c, ok := statusColors[pbr.status]; pbr.Color && ok {
status = c.Sprint(status)
}
}
return status
}
// Progress returns an assembled and optionally colorized progress string
func (pbr *ProgressBarRender) Progress() string {
var body string
if pbr.progress != "" {
body = fmt.Sprintf(" %s ", pbr.progress)
} else {
padding := pbr.progressPadding
if pbr.Color {
padding = colorFaint.Sprint(pbr.progressPadding)
}
body = pbr.progressFill + padding
}
return fmt.Sprintf("[%s]", body)
}
func (pbr ProgressBarRender) String() string {
if pbr.Hijack != "" {
return pbr.Hijack
}
var right string
if len(pbr.Right) > 0 {
right = " " + strings.Join(pbr.Right, " ")
}
return pbr.Left + " " + pbr.Status() + " " + pbr.Progress() + right
}
// Render locks the progressbar struct for reading and calls all of
// its methods to return the final output. A struct is returned over a
// plain string to allow dynamic padding and positioning of elements
// depending on other elements on the screen.
// - maxLeft defines the maximum character length of the left-side
// text. Characters exceeding this length will be replaced with a
// single ellipsis. Passing <=0 disables this.
// - widthDelta changes the progress bar width the specified amount of
// characters. E.g. passing -2 would shorten the width by 2 chars.
// If the resulting width is lower than minWidth, progress will be
// rendered as a percentage instead of a filling bar.
func (pb *ProgressBar) Render(maxLeft, widthDelta int) ProgressBarRender {
pb.mutex.RLock()
defer pb.mutex.RUnlock()
var out ProgressBarRender
if pb.hijack != nil {
out.Hijack = pb.hijack()
return out
}
var progress float64
if pb.progress != nil {
progress, out.Right = pb.progress()
progressClamped := Clampf(progress, 0, 1)
if progress != progressClamped {
progress = progressClamped
if pb.logger != nil {
pb.logger.Warnf("progress value %.2f exceeds valid range, clamped between 0 and 1", progress)
}
}
}
width := Clampf(float64(pb.width+widthDelta), minWidth, DefaultWidth)
pb.width = int(width)
if pb.width > minWidth { //nolint:nestif
space := pb.width - 2
filled := int(float64(space) * progress)
filling := ""
caret := ""
if filled > 0 {
if filled < space {
filling = strings.Repeat("=", filled-1)
caret = ">"
} else {
filling = strings.Repeat("=", filled)
}
}
out.progressPadding = ""
if space > filled {
out.progressPadding = strings.Repeat("-", space-filled)
}
out.progressFill = filling + caret
} else {
out.progress = fmt.Sprintf("%3.f%%", progress*100)
}
out.Left = pb.renderLeft(maxLeft)
out.status = pb.status
return out
}