| package pb |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io" |
| "os" |
| "strconv" |
| "strings" |
| "sync" |
| "sync/atomic" |
| "text/template" |
| "time" |
| |
| "github.com/fatih/color" |
| |
| "github.com/mattn/go-colorable" |
| "github.com/mattn/go-isatty" |
| |
| "github.com/cheggaaa/pb/v3/termutil" |
| ) |
| |
| // Version of ProgressBar library |
| const Version = "3.0.8" |
| |
| type key int |
| |
| const ( |
| // Bytes means we're working with byte sizes. Numbers will print as Kb, Mb, etc |
| // bar.Set(pb.Bytes, true) |
| Bytes key = 1 << iota |
| |
| // Use SI bytes prefix names (kB, MB, etc) instead of IEC prefix names (KiB, MiB, etc) |
| SIBytesPrefix |
| |
| // Terminal means we're will print to terminal and can use ascii sequences |
| // Also we're will try to use terminal width |
| Terminal |
| |
| // Static means progress bar will not update automaticly |
| Static |
| |
| // ReturnSymbol - by default in terminal mode it's '\r' |
| ReturnSymbol |
| |
| // Color by default is true when output is tty, but you can set to false for disabling colors |
| Color |
| |
| // Hide the progress bar when finished, rather than leaving it up. By default it's false. |
| CleanOnFinish |
| |
| // Round elapsed time to this precision. Defaults to time.Second. |
| TimeRound |
| ) |
| |
| const ( |
| defaultBarWidth = 100 |
| defaultRefreshRate = time.Millisecond * 200 |
| ) |
| |
| // New creates new ProgressBar object |
| func New(total int) *ProgressBar { |
| return New64(int64(total)) |
| } |
| |
| // New64 creates new ProgressBar object using int64 as total |
| func New64(total int64) *ProgressBar { |
| pb := new(ProgressBar) |
| return pb.SetTotal(total) |
| } |
| |
| // StartNew starts new ProgressBar with Default template |
| func StartNew(total int) *ProgressBar { |
| return New(total).Start() |
| } |
| |
| // Start64 starts new ProgressBar with Default template. Using int64 as total. |
| func Start64(total int64) *ProgressBar { |
| return New64(total).Start() |
| } |
| |
| var ( |
| terminalWidth = termutil.TerminalWidth |
| isTerminal = isatty.IsTerminal |
| isCygwinTerminal = isatty.IsCygwinTerminal |
| ) |
| |
| // ProgressBar is the main object of bar |
| type ProgressBar struct { |
| current, total int64 |
| width int |
| maxWidth int |
| mu sync.RWMutex |
| rm sync.Mutex |
| vars map[interface{}]interface{} |
| elements map[string]Element |
| output io.Writer |
| coutput io.Writer |
| nocoutput io.Writer |
| startTime time.Time |
| refreshRate time.Duration |
| tmpl *template.Template |
| state *State |
| buf *bytes.Buffer |
| ticker *time.Ticker |
| finish chan struct{} |
| finished bool |
| configured bool |
| err error |
| } |
| |
| func (pb *ProgressBar) configure() { |
| if pb.configured { |
| return |
| } |
| pb.configured = true |
| |
| if pb.vars == nil { |
| pb.vars = make(map[interface{}]interface{}) |
| } |
| if pb.output == nil { |
| pb.output = os.Stderr |
| } |
| |
| if pb.tmpl == nil { |
| pb.tmpl, pb.err = getTemplate(string(Default)) |
| if pb.err != nil { |
| return |
| } |
| } |
| if pb.vars[Terminal] == nil { |
| if f, ok := pb.output.(*os.File); ok { |
| if isTerminal(f.Fd()) || isCygwinTerminal(f.Fd()) { |
| pb.vars[Terminal] = true |
| } |
| } |
| } |
| if pb.vars[ReturnSymbol] == nil { |
| if tm, ok := pb.vars[Terminal].(bool); ok && tm { |
| pb.vars[ReturnSymbol] = "\r" |
| } |
| } |
| if pb.vars[Color] == nil { |
| if tm, ok := pb.vars[Terminal].(bool); ok && tm { |
| pb.vars[Color] = true |
| } |
| } |
| if pb.refreshRate == 0 { |
| pb.refreshRate = defaultRefreshRate |
| } |
| if pb.vars[CleanOnFinish] == nil { |
| pb.vars[CleanOnFinish] = false |
| } |
| if f, ok := pb.output.(*os.File); ok { |
| pb.coutput = colorable.NewColorable(f) |
| } else { |
| pb.coutput = pb.output |
| } |
| pb.nocoutput = colorable.NewNonColorable(pb.output) |
| } |
| |
| // Start starts the bar |
| func (pb *ProgressBar) Start() *ProgressBar { |
| pb.mu.Lock() |
| defer pb.mu.Unlock() |
| if pb.finish != nil { |
| return pb |
| } |
| pb.configure() |
| pb.finished = false |
| pb.state = nil |
| pb.startTime = time.Now() |
| if st, ok := pb.vars[Static].(bool); ok && st { |
| return pb |
| } |
| pb.finish = make(chan struct{}) |
| pb.ticker = time.NewTicker(pb.refreshRate) |
| go pb.writer(pb.finish) |
| return pb |
| } |
| |
| func (pb *ProgressBar) writer(finish chan struct{}) { |
| for { |
| select { |
| case <-pb.ticker.C: |
| pb.write(false) |
| case <-finish: |
| pb.ticker.Stop() |
| pb.write(true) |
| finish <- struct{}{} |
| return |
| } |
| } |
| } |
| |
| // Write performs write to the output |
| func (pb *ProgressBar) Write() *ProgressBar { |
| pb.mu.RLock() |
| finished := pb.finished |
| pb.mu.RUnlock() |
| pb.write(finished) |
| return pb |
| } |
| |
| func (pb *ProgressBar) write(finish bool) { |
| result, width := pb.render() |
| if pb.Err() != nil { |
| return |
| } |
| if pb.GetBool(Terminal) { |
| if r := (width - CellCount(result)); r > 0 { |
| result += strings.Repeat(" ", r) |
| } |
| } |
| if ret, ok := pb.Get(ReturnSymbol).(string); ok { |
| result = ret + result |
| if finish && ret == "\r" { |
| if pb.GetBool(CleanOnFinish) { |
| // "Wipe out" progress bar by overwriting one line with blanks |
| result = "\r" + color.New(color.Reset).Sprintf(strings.Repeat(" ", width)) + "\r" |
| } else { |
| result += "\n" |
| } |
| } |
| } |
| if pb.GetBool(Color) { |
| pb.coutput.Write([]byte(result)) |
| } else { |
| pb.nocoutput.Write([]byte(result)) |
| } |
| } |
| |
| // Total return current total bar value |
| func (pb *ProgressBar) Total() int64 { |
| return atomic.LoadInt64(&pb.total) |
| } |
| |
| // SetTotal sets the total bar value |
| func (pb *ProgressBar) SetTotal(value int64) *ProgressBar { |
| atomic.StoreInt64(&pb.total, value) |
| return pb |
| } |
| |
| // AddTotal adds to the total bar value |
| func (pb *ProgressBar) AddTotal(value int64) *ProgressBar { |
| atomic.AddInt64(&pb.total, value) |
| return pb |
| } |
| |
| // SetCurrent sets the current bar value |
| func (pb *ProgressBar) SetCurrent(value int64) *ProgressBar { |
| atomic.StoreInt64(&pb.current, value) |
| return pb |
| } |
| |
| // Current return current bar value |
| func (pb *ProgressBar) Current() int64 { |
| return atomic.LoadInt64(&pb.current) |
| } |
| |
| // Add adding given int64 value to bar value |
| func (pb *ProgressBar) Add64(value int64) *ProgressBar { |
| atomic.AddInt64(&pb.current, value) |
| return pb |
| } |
| |
| // Add adding given int value to bar value |
| func (pb *ProgressBar) Add(value int) *ProgressBar { |
| return pb.Add64(int64(value)) |
| } |
| |
| // Increment atomically increments the progress |
| func (pb *ProgressBar) Increment() *ProgressBar { |
| return pb.Add64(1) |
| } |
| |
| // Set sets any value by any key |
| func (pb *ProgressBar) Set(key, value interface{}) *ProgressBar { |
| pb.mu.Lock() |
| defer pb.mu.Unlock() |
| if pb.vars == nil { |
| pb.vars = make(map[interface{}]interface{}) |
| } |
| pb.vars[key] = value |
| return pb |
| } |
| |
| // Get return value by key |
| func (pb *ProgressBar) Get(key interface{}) interface{} { |
| pb.mu.RLock() |
| defer pb.mu.RUnlock() |
| if pb.vars == nil { |
| return nil |
| } |
| return pb.vars[key] |
| } |
| |
| // GetBool return value by key and try to convert there to boolean |
| // If value doesn't set or not boolean - return false |
| func (pb *ProgressBar) GetBool(key interface{}) bool { |
| if v, ok := pb.Get(key).(bool); ok { |
| return v |
| } |
| return false |
| } |
| |
| // SetWidth sets the bar width |
| // When given value <= 0 would be using the terminal width (if possible) or default value. |
| func (pb *ProgressBar) SetWidth(width int) *ProgressBar { |
| pb.mu.Lock() |
| pb.width = width |
| pb.mu.Unlock() |
| return pb |
| } |
| |
| // SetMaxWidth sets the bar maximum width |
| // When given value <= 0 would be using the terminal width (if possible) or default value. |
| func (pb *ProgressBar) SetMaxWidth(maxWidth int) *ProgressBar { |
| pb.mu.Lock() |
| pb.maxWidth = maxWidth |
| pb.mu.Unlock() |
| return pb |
| } |
| |
| // Width return the bar width |
| // It's current terminal width or settled over 'SetWidth' value. |
| func (pb *ProgressBar) Width() (width int) { |
| defer func() { |
| if r := recover(); r != nil { |
| width = defaultBarWidth |
| } |
| }() |
| pb.mu.RLock() |
| width = pb.width |
| maxWidth := pb.maxWidth |
| pb.mu.RUnlock() |
| if width <= 0 { |
| var err error |
| if width, err = terminalWidth(); err != nil { |
| return defaultBarWidth |
| } |
| } |
| if maxWidth > 0 && width > maxWidth { |
| width = maxWidth |
| } |
| return |
| } |
| |
| func (pb *ProgressBar) SetRefreshRate(dur time.Duration) *ProgressBar { |
| pb.mu.Lock() |
| if dur > 0 { |
| pb.refreshRate = dur |
| } |
| pb.mu.Unlock() |
| return pb |
| } |
| |
| // SetWriter sets the io.Writer. Bar will write in this writer |
| // By default this is os.Stderr |
| func (pb *ProgressBar) SetWriter(w io.Writer) *ProgressBar { |
| pb.mu.Lock() |
| pb.output = w |
| pb.configured = false |
| pb.configure() |
| pb.mu.Unlock() |
| return pb |
| } |
| |
| // StartTime return the time when bar started |
| func (pb *ProgressBar) StartTime() time.Time { |
| pb.mu.RLock() |
| defer pb.mu.RUnlock() |
| return pb.startTime |
| } |
| |
| // Format convert int64 to string according to the current settings |
| func (pb *ProgressBar) Format(v int64) string { |
| if pb.GetBool(Bytes) { |
| return formatBytes(v, pb.GetBool(SIBytesPrefix)) |
| } |
| return strconv.FormatInt(v, 10) |
| } |
| |
| // Finish stops the bar |
| func (pb *ProgressBar) Finish() *ProgressBar { |
| pb.mu.Lock() |
| if pb.finished { |
| pb.mu.Unlock() |
| return pb |
| } |
| finishChan := pb.finish |
| pb.finished = true |
| pb.mu.Unlock() |
| if finishChan != nil { |
| finishChan <- struct{}{} |
| <-finishChan |
| pb.mu.Lock() |
| pb.finish = nil |
| pb.mu.Unlock() |
| } |
| return pb |
| } |
| |
| // IsStarted indicates progress bar state |
| func (pb *ProgressBar) IsStarted() bool { |
| pb.mu.RLock() |
| defer pb.mu.RUnlock() |
| return pb.finish != nil |
| } |
| |
| // IsFinished indicates progress bar is finished |
| func (pb *ProgressBar) IsFinished() bool { |
| pb.mu.RLock() |
| defer pb.mu.RUnlock() |
| return pb.finished |
| } |
| |
| // SetTemplateString sets ProgressBar tempate string and parse it |
| func (pb *ProgressBar) SetTemplateString(tmpl string) *ProgressBar { |
| pb.mu.Lock() |
| defer pb.mu.Unlock() |
| pb.tmpl, pb.err = getTemplate(tmpl) |
| return pb |
| } |
| |
| // SetTemplateString sets ProgressBarTempate and parse it |
| func (pb *ProgressBar) SetTemplate(tmpl ProgressBarTemplate) *ProgressBar { |
| return pb.SetTemplateString(string(tmpl)) |
| } |
| |
| // NewProxyReader creates a wrapper for given reader, but with progress handle |
| // Takes io.Reader or io.ReadCloser |
| // Also, it automatically switches progress bar to handle units as bytes |
| func (pb *ProgressBar) NewProxyReader(r io.Reader) *Reader { |
| pb.Set(Bytes, true) |
| return &Reader{r, pb} |
| } |
| |
| // NewProxyWriter creates a wrapper for given writer, but with progress handle |
| // Takes io.Writer or io.WriteCloser |
| // Also, it automatically switches progress bar to handle units as bytes |
| func (pb *ProgressBar) NewProxyWriter(r io.Writer) *Writer { |
| pb.Set(Bytes, true) |
| return &Writer{r, pb} |
| } |
| |
| func (pb *ProgressBar) render() (result string, width int) { |
| defer func() { |
| if r := recover(); r != nil { |
| pb.SetErr(fmt.Errorf("render panic: %v", r)) |
| } |
| }() |
| pb.rm.Lock() |
| defer pb.rm.Unlock() |
| pb.mu.Lock() |
| pb.configure() |
| if pb.state == nil { |
| pb.state = &State{ProgressBar: pb} |
| pb.buf = bytes.NewBuffer(nil) |
| } |
| if pb.startTime.IsZero() { |
| pb.startTime = time.Now() |
| } |
| pb.state.id++ |
| pb.state.finished = pb.finished |
| pb.state.time = time.Now() |
| pb.mu.Unlock() |
| |
| pb.state.width = pb.Width() |
| width = pb.state.width |
| pb.state.total = pb.Total() |
| pb.state.current = pb.Current() |
| pb.buf.Reset() |
| |
| if e := pb.tmpl.Execute(pb.buf, pb.state); e != nil { |
| pb.SetErr(e) |
| return "", 0 |
| } |
| |
| result = pb.buf.String() |
| |
| aec := len(pb.state.recalc) |
| if aec == 0 { |
| // no adaptive elements |
| return |
| } |
| |
| staticWidth := CellCount(result) - (aec * adElPlaceholderLen) |
| |
| if pb.state.Width()-staticWidth <= 0 { |
| result = strings.Replace(result, adElPlaceholder, "", -1) |
| result = StripString(result, pb.state.Width()) |
| } else { |
| pb.state.adaptiveElWidth = (width - staticWidth) / aec |
| for _, el := range pb.state.recalc { |
| result = strings.Replace(result, adElPlaceholder, el.ProgressElement(pb.state), 1) |
| } |
| } |
| pb.state.recalc = pb.state.recalc[:0] |
| return |
| } |
| |
| // SetErr sets error to the ProgressBar |
| // Error will be available over Err() |
| func (pb *ProgressBar) SetErr(err error) *ProgressBar { |
| pb.mu.Lock() |
| pb.err = err |
| pb.mu.Unlock() |
| return pb |
| } |
| |
| // Err return possible error |
| // When all ok - will be nil |
| // May contain template.Execute errors |
| func (pb *ProgressBar) Err() error { |
| pb.mu.RLock() |
| defer pb.mu.RUnlock() |
| return pb.err |
| } |
| |
| // String return currrent string representation of ProgressBar |
| func (pb *ProgressBar) String() string { |
| res, _ := pb.render() |
| return res |
| } |
| |
| // ProgressElement implements Element interface |
| func (pb *ProgressBar) ProgressElement(s *State, args ...string) string { |
| if s.IsAdaptiveWidth() { |
| pb.SetWidth(s.AdaptiveElWidth()) |
| } |
| return pb.String() |
| } |
| |
| // State represents the current state of bar |
| // Need for bar elements |
| type State struct { |
| *ProgressBar |
| |
| id uint64 |
| total, current int64 |
| width, adaptiveElWidth int |
| finished, adaptive bool |
| time time.Time |
| |
| recalc []Element |
| } |
| |
| // Id it's the current state identifier |
| // - incremental |
| // - starts with 1 |
| // - resets after finish/start |
| func (s *State) Id() uint64 { |
| return s.id |
| } |
| |
| // Total it's bar int64 total |
| func (s *State) Total() int64 { |
| return s.total |
| } |
| |
| // Value it's current value |
| func (s *State) Value() int64 { |
| return s.current |
| } |
| |
| // Width of bar |
| func (s *State) Width() int { |
| return s.width |
| } |
| |
| // AdaptiveElWidth - adaptive elements must return string with given cell count (when AdaptiveElWidth > 0) |
| func (s *State) AdaptiveElWidth() int { |
| return s.adaptiveElWidth |
| } |
| |
| // IsAdaptiveWidth returns true when element must be shown as adaptive |
| func (s *State) IsAdaptiveWidth() bool { |
| return s.adaptive |
| } |
| |
| // IsFinished return true when bar is finished |
| func (s *State) IsFinished() bool { |
| return s.finished |
| } |
| |
| // IsFirst return true only in first render |
| func (s *State) IsFirst() bool { |
| return s.id == 1 |
| } |
| |
| // Time when state was created |
| func (s *State) Time() time.Time { |
| return s.time |
| } |