blob: 76440e1727a0dae7b333e74720ad4be81ed1486a [file] [log] [blame]
Abhay Kumara61c5222025-11-10 07:32:50 +00001package pb
2
3import (
4 "bytes"
5 "fmt"
6 "io"
7 "os"
8 "strconv"
9 "strings"
10 "sync"
11 "sync/atomic"
12 "text/template"
13 "time"
14
15 "github.com/fatih/color"
16
17 "github.com/mattn/go-colorable"
18 "github.com/mattn/go-isatty"
19
20 "github.com/cheggaaa/pb/v3/termutil"
21)
22
23// Version of ProgressBar library
24const Version = "3.0.8"
25
26type key int
27
28const (
29 // Bytes means we're working with byte sizes. Numbers will print as Kb, Mb, etc
30 // bar.Set(pb.Bytes, true)
31 Bytes key = 1 << iota
32
33 // Use SI bytes prefix names (kB, MB, etc) instead of IEC prefix names (KiB, MiB, etc)
34 SIBytesPrefix
35
36 // Terminal means we're will print to terminal and can use ascii sequences
37 // Also we're will try to use terminal width
38 Terminal
39
40 // Static means progress bar will not update automaticly
41 Static
42
43 // ReturnSymbol - by default in terminal mode it's '\r'
44 ReturnSymbol
45
46 // Color by default is true when output is tty, but you can set to false for disabling colors
47 Color
48
49 // Hide the progress bar when finished, rather than leaving it up. By default it's false.
50 CleanOnFinish
51
52 // Round elapsed time to this precision. Defaults to time.Second.
53 TimeRound
54)
55
56const (
57 defaultBarWidth = 100
58 defaultRefreshRate = time.Millisecond * 200
59)
60
61// New creates new ProgressBar object
62func New(total int) *ProgressBar {
63 return New64(int64(total))
64}
65
66// New64 creates new ProgressBar object using int64 as total
67func New64(total int64) *ProgressBar {
68 pb := new(ProgressBar)
69 return pb.SetTotal(total)
70}
71
72// StartNew starts new ProgressBar with Default template
73func StartNew(total int) *ProgressBar {
74 return New(total).Start()
75}
76
77// Start64 starts new ProgressBar with Default template. Using int64 as total.
78func Start64(total int64) *ProgressBar {
79 return New64(total).Start()
80}
81
82var (
83 terminalWidth = termutil.TerminalWidth
84 isTerminal = isatty.IsTerminal
85 isCygwinTerminal = isatty.IsCygwinTerminal
86)
87
88// ProgressBar is the main object of bar
89type ProgressBar struct {
90 current, total int64
91 width int
92 maxWidth int
93 mu sync.RWMutex
94 rm sync.Mutex
95 vars map[interface{}]interface{}
96 elements map[string]Element
97 output io.Writer
98 coutput io.Writer
99 nocoutput io.Writer
100 startTime time.Time
101 refreshRate time.Duration
102 tmpl *template.Template
103 state *State
104 buf *bytes.Buffer
105 ticker *time.Ticker
106 finish chan struct{}
107 finished bool
108 configured bool
109 err error
110}
111
112func (pb *ProgressBar) configure() {
113 if pb.configured {
114 return
115 }
116 pb.configured = true
117
118 if pb.vars == nil {
119 pb.vars = make(map[interface{}]interface{})
120 }
121 if pb.output == nil {
122 pb.output = os.Stderr
123 }
124
125 if pb.tmpl == nil {
126 pb.tmpl, pb.err = getTemplate(string(Default))
127 if pb.err != nil {
128 return
129 }
130 }
131 if pb.vars[Terminal] == nil {
132 if f, ok := pb.output.(*os.File); ok {
133 if isTerminal(f.Fd()) || isCygwinTerminal(f.Fd()) {
134 pb.vars[Terminal] = true
135 }
136 }
137 }
138 if pb.vars[ReturnSymbol] == nil {
139 if tm, ok := pb.vars[Terminal].(bool); ok && tm {
140 pb.vars[ReturnSymbol] = "\r"
141 }
142 }
143 if pb.vars[Color] == nil {
144 if tm, ok := pb.vars[Terminal].(bool); ok && tm {
145 pb.vars[Color] = true
146 }
147 }
148 if pb.refreshRate == 0 {
149 pb.refreshRate = defaultRefreshRate
150 }
151 if pb.vars[CleanOnFinish] == nil {
152 pb.vars[CleanOnFinish] = false
153 }
154 if f, ok := pb.output.(*os.File); ok {
155 pb.coutput = colorable.NewColorable(f)
156 } else {
157 pb.coutput = pb.output
158 }
159 pb.nocoutput = colorable.NewNonColorable(pb.output)
160}
161
162// Start starts the bar
163func (pb *ProgressBar) Start() *ProgressBar {
164 pb.mu.Lock()
165 defer pb.mu.Unlock()
166 if pb.finish != nil {
167 return pb
168 }
169 pb.configure()
170 pb.finished = false
171 pb.state = nil
172 pb.startTime = time.Now()
173 if st, ok := pb.vars[Static].(bool); ok && st {
174 return pb
175 }
176 pb.finish = make(chan struct{})
177 pb.ticker = time.NewTicker(pb.refreshRate)
178 go pb.writer(pb.finish)
179 return pb
180}
181
182func (pb *ProgressBar) writer(finish chan struct{}) {
183 for {
184 select {
185 case <-pb.ticker.C:
186 pb.write(false)
187 case <-finish:
188 pb.ticker.Stop()
189 pb.write(true)
190 finish <- struct{}{}
191 return
192 }
193 }
194}
195
196// Write performs write to the output
197func (pb *ProgressBar) Write() *ProgressBar {
198 pb.mu.RLock()
199 finished := pb.finished
200 pb.mu.RUnlock()
201 pb.write(finished)
202 return pb
203}
204
205func (pb *ProgressBar) write(finish bool) {
206 result, width := pb.render()
207 if pb.Err() != nil {
208 return
209 }
210 if pb.GetBool(Terminal) {
211 if r := (width - CellCount(result)); r > 0 {
212 result += strings.Repeat(" ", r)
213 }
214 }
215 if ret, ok := pb.Get(ReturnSymbol).(string); ok {
216 result = ret + result
217 if finish && ret == "\r" {
218 if pb.GetBool(CleanOnFinish) {
219 // "Wipe out" progress bar by overwriting one line with blanks
220 result = "\r" + color.New(color.Reset).Sprintf(strings.Repeat(" ", width)) + "\r"
221 } else {
222 result += "\n"
223 }
224 }
225 }
226 if pb.GetBool(Color) {
227 pb.coutput.Write([]byte(result))
228 } else {
229 pb.nocoutput.Write([]byte(result))
230 }
231}
232
233// Total return current total bar value
234func (pb *ProgressBar) Total() int64 {
235 return atomic.LoadInt64(&pb.total)
236}
237
238// SetTotal sets the total bar value
239func (pb *ProgressBar) SetTotal(value int64) *ProgressBar {
240 atomic.StoreInt64(&pb.total, value)
241 return pb
242}
243
244// AddTotal adds to the total bar value
245func (pb *ProgressBar) AddTotal(value int64) *ProgressBar {
246 atomic.AddInt64(&pb.total, value)
247 return pb
248}
249
250// SetCurrent sets the current bar value
251func (pb *ProgressBar) SetCurrent(value int64) *ProgressBar {
252 atomic.StoreInt64(&pb.current, value)
253 return pb
254}
255
256// Current return current bar value
257func (pb *ProgressBar) Current() int64 {
258 return atomic.LoadInt64(&pb.current)
259}
260
261// Add adding given int64 value to bar value
262func (pb *ProgressBar) Add64(value int64) *ProgressBar {
263 atomic.AddInt64(&pb.current, value)
264 return pb
265}
266
267// Add adding given int value to bar value
268func (pb *ProgressBar) Add(value int) *ProgressBar {
269 return pb.Add64(int64(value))
270}
271
272// Increment atomically increments the progress
273func (pb *ProgressBar) Increment() *ProgressBar {
274 return pb.Add64(1)
275}
276
277// Set sets any value by any key
278func (pb *ProgressBar) Set(key, value interface{}) *ProgressBar {
279 pb.mu.Lock()
280 defer pb.mu.Unlock()
281 if pb.vars == nil {
282 pb.vars = make(map[interface{}]interface{})
283 }
284 pb.vars[key] = value
285 return pb
286}
287
288// Get return value by key
289func (pb *ProgressBar) Get(key interface{}) interface{} {
290 pb.mu.RLock()
291 defer pb.mu.RUnlock()
292 if pb.vars == nil {
293 return nil
294 }
295 return pb.vars[key]
296}
297
298// GetBool return value by key and try to convert there to boolean
299// If value doesn't set or not boolean - return false
300func (pb *ProgressBar) GetBool(key interface{}) bool {
301 if v, ok := pb.Get(key).(bool); ok {
302 return v
303 }
304 return false
305}
306
307// SetWidth sets the bar width
308// When given value <= 0 would be using the terminal width (if possible) or default value.
309func (pb *ProgressBar) SetWidth(width int) *ProgressBar {
310 pb.mu.Lock()
311 pb.width = width
312 pb.mu.Unlock()
313 return pb
314}
315
316// SetMaxWidth sets the bar maximum width
317// When given value <= 0 would be using the terminal width (if possible) or default value.
318func (pb *ProgressBar) SetMaxWidth(maxWidth int) *ProgressBar {
319 pb.mu.Lock()
320 pb.maxWidth = maxWidth
321 pb.mu.Unlock()
322 return pb
323}
324
325// Width return the bar width
326// It's current terminal width or settled over 'SetWidth' value.
327func (pb *ProgressBar) Width() (width int) {
328 defer func() {
329 if r := recover(); r != nil {
330 width = defaultBarWidth
331 }
332 }()
333 pb.mu.RLock()
334 width = pb.width
335 maxWidth := pb.maxWidth
336 pb.mu.RUnlock()
337 if width <= 0 {
338 var err error
339 if width, err = terminalWidth(); err != nil {
340 return defaultBarWidth
341 }
342 }
343 if maxWidth > 0 && width > maxWidth {
344 width = maxWidth
345 }
346 return
347}
348
349func (pb *ProgressBar) SetRefreshRate(dur time.Duration) *ProgressBar {
350 pb.mu.Lock()
351 if dur > 0 {
352 pb.refreshRate = dur
353 }
354 pb.mu.Unlock()
355 return pb
356}
357
358// SetWriter sets the io.Writer. Bar will write in this writer
359// By default this is os.Stderr
360func (pb *ProgressBar) SetWriter(w io.Writer) *ProgressBar {
361 pb.mu.Lock()
362 pb.output = w
363 pb.configured = false
364 pb.configure()
365 pb.mu.Unlock()
366 return pb
367}
368
369// StartTime return the time when bar started
370func (pb *ProgressBar) StartTime() time.Time {
371 pb.mu.RLock()
372 defer pb.mu.RUnlock()
373 return pb.startTime
374}
375
376// Format convert int64 to string according to the current settings
377func (pb *ProgressBar) Format(v int64) string {
378 if pb.GetBool(Bytes) {
379 return formatBytes(v, pb.GetBool(SIBytesPrefix))
380 }
381 return strconv.FormatInt(v, 10)
382}
383
384// Finish stops the bar
385func (pb *ProgressBar) Finish() *ProgressBar {
386 pb.mu.Lock()
387 if pb.finished {
388 pb.mu.Unlock()
389 return pb
390 }
391 finishChan := pb.finish
392 pb.finished = true
393 pb.mu.Unlock()
394 if finishChan != nil {
395 finishChan <- struct{}{}
396 <-finishChan
397 pb.mu.Lock()
398 pb.finish = nil
399 pb.mu.Unlock()
400 }
401 return pb
402}
403
404// IsStarted indicates progress bar state
405func (pb *ProgressBar) IsStarted() bool {
406 pb.mu.RLock()
407 defer pb.mu.RUnlock()
408 return pb.finish != nil
409}
410
411// IsFinished indicates progress bar is finished
412func (pb *ProgressBar) IsFinished() bool {
413 pb.mu.RLock()
414 defer pb.mu.RUnlock()
415 return pb.finished
416}
417
418// SetTemplateString sets ProgressBar tempate string and parse it
419func (pb *ProgressBar) SetTemplateString(tmpl string) *ProgressBar {
420 pb.mu.Lock()
421 defer pb.mu.Unlock()
422 pb.tmpl, pb.err = getTemplate(tmpl)
423 return pb
424}
425
426// SetTemplateString sets ProgressBarTempate and parse it
427func (pb *ProgressBar) SetTemplate(tmpl ProgressBarTemplate) *ProgressBar {
428 return pb.SetTemplateString(string(tmpl))
429}
430
431// NewProxyReader creates a wrapper for given reader, but with progress handle
432// Takes io.Reader or io.ReadCloser
433// Also, it automatically switches progress bar to handle units as bytes
434func (pb *ProgressBar) NewProxyReader(r io.Reader) *Reader {
435 pb.Set(Bytes, true)
436 return &Reader{r, pb}
437}
438
439// NewProxyWriter creates a wrapper for given writer, but with progress handle
440// Takes io.Writer or io.WriteCloser
441// Also, it automatically switches progress bar to handle units as bytes
442func (pb *ProgressBar) NewProxyWriter(r io.Writer) *Writer {
443 pb.Set(Bytes, true)
444 return &Writer{r, pb}
445}
446
447func (pb *ProgressBar) render() (result string, width int) {
448 defer func() {
449 if r := recover(); r != nil {
450 pb.SetErr(fmt.Errorf("render panic: %v", r))
451 }
452 }()
453 pb.rm.Lock()
454 defer pb.rm.Unlock()
455 pb.mu.Lock()
456 pb.configure()
457 if pb.state == nil {
458 pb.state = &State{ProgressBar: pb}
459 pb.buf = bytes.NewBuffer(nil)
460 }
461 if pb.startTime.IsZero() {
462 pb.startTime = time.Now()
463 }
464 pb.state.id++
465 pb.state.finished = pb.finished
466 pb.state.time = time.Now()
467 pb.mu.Unlock()
468
469 pb.state.width = pb.Width()
470 width = pb.state.width
471 pb.state.total = pb.Total()
472 pb.state.current = pb.Current()
473 pb.buf.Reset()
474
475 if e := pb.tmpl.Execute(pb.buf, pb.state); e != nil {
476 pb.SetErr(e)
477 return "", 0
478 }
479
480 result = pb.buf.String()
481
482 aec := len(pb.state.recalc)
483 if aec == 0 {
484 // no adaptive elements
485 return
486 }
487
488 staticWidth := CellCount(result) - (aec * adElPlaceholderLen)
489
490 if pb.state.Width()-staticWidth <= 0 {
491 result = strings.Replace(result, adElPlaceholder, "", -1)
492 result = StripString(result, pb.state.Width())
493 } else {
494 pb.state.adaptiveElWidth = (width - staticWidth) / aec
495 for _, el := range pb.state.recalc {
496 result = strings.Replace(result, adElPlaceholder, el.ProgressElement(pb.state), 1)
497 }
498 }
499 pb.state.recalc = pb.state.recalc[:0]
500 return
501}
502
503// SetErr sets error to the ProgressBar
504// Error will be available over Err()
505func (pb *ProgressBar) SetErr(err error) *ProgressBar {
506 pb.mu.Lock()
507 pb.err = err
508 pb.mu.Unlock()
509 return pb
510}
511
512// Err return possible error
513// When all ok - will be nil
514// May contain template.Execute errors
515func (pb *ProgressBar) Err() error {
516 pb.mu.RLock()
517 defer pb.mu.RUnlock()
518 return pb.err
519}
520
521// String return currrent string representation of ProgressBar
522func (pb *ProgressBar) String() string {
523 res, _ := pb.render()
524 return res
525}
526
527// ProgressElement implements Element interface
528func (pb *ProgressBar) ProgressElement(s *State, args ...string) string {
529 if s.IsAdaptiveWidth() {
530 pb.SetWidth(s.AdaptiveElWidth())
531 }
532 return pb.String()
533}
534
535// State represents the current state of bar
536// Need for bar elements
537type State struct {
538 *ProgressBar
539
540 id uint64
541 total, current int64
542 width, adaptiveElWidth int
543 finished, adaptive bool
544 time time.Time
545
546 recalc []Element
547}
548
549// Id it's the current state identifier
550// - incremental
551// - starts with 1
552// - resets after finish/start
553func (s *State) Id() uint64 {
554 return s.id
555}
556
557// Total it's bar int64 total
558func (s *State) Total() int64 {
559 return s.total
560}
561
562// Value it's current value
563func (s *State) Value() int64 {
564 return s.current
565}
566
567// Width of bar
568func (s *State) Width() int {
569 return s.width
570}
571
572// AdaptiveElWidth - adaptive elements must return string with given cell count (when AdaptiveElWidth > 0)
573func (s *State) AdaptiveElWidth() int {
574 return s.adaptiveElWidth
575}
576
577// IsAdaptiveWidth returns true when element must be shown as adaptive
578func (s *State) IsAdaptiveWidth() bool {
579 return s.adaptive
580}
581
582// IsFinished return true when bar is finished
583func (s *State) IsFinished() bool {
584 return s.finished
585}
586
587// IsFirst return true only in first render
588func (s *State) IsFirst() bool {
589 return s.id == 1
590}
591
592// Time when state was created
593func (s *State) Time() time.Time {
594 return s.time
595}