diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..63bd916
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+# Test binary, build with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..a7fd776
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,14 @@
+language: go
+sudo: false
+go:
+ - 1.8.x
+ - 1.9.x
+
+before_install:
+ - go get -t -v ./...
+
+script:
+ - go test -race -coverprofile=coverage.txt -covermode=atomic
+
+after_success:
+ - bash <(curl -s https://codecov.io/bash)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5e68ed2
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (C) 2016-2018 Vladimir Bauer
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9b76064
--- /dev/null
+++ b/README.md
@@ -0,0 +1,117 @@
+# Multi Progress Bar
+
+[](https://godoc.org/github.com/vbauerster/mpb)
+[](https://travis-ci.org/vbauerster/mpb)
+[](https://goreportcard.com/report/github.com/vbauerster/mpb)
+[](https://codecov.io/gh/vbauerster/mpb)
+
+**mpb** is a Go lib for rendering progress bars in terminal applications.
+
+## Features
+
+* __Multiple Bars__: Multiple progress bars are supported
+* __Dynamic Total__: [Set total](https://github.com/vbauerster/mpb/issues/9#issuecomment-344448984) while bar is running
+* __Dynamic Add/Remove__: Dynamically add or remove bars
+* __Cancellation__: Cancel whole rendering process
+* __Predefined Decorators__: Elapsed time, [ewma](https://github.com/VividCortex/ewma) based ETA, Percentage, Bytes counter
+* __Decorator's width sync__: Synchronized decorator's width among multiple bars
+
+## Installation
+
+```sh
+go get github.com/vbauerster/mpb
+```
+
+_Note:_ it is preferable to go get from github.com, rather than gopkg.in. See issue [#11](https://github.com/vbauerster/mpb/issues/11).
+
+## Usage
+
+#### [Rendering single bar](examples/singleBar/main.go)
+```go
+ p := mpb.New(
+ // override default (80) width
+ mpb.WithWidth(64),
+ // override default "[=>-]" format
+ mpb.WithFormat("╢▌▌░╟"),
+ // override default 120ms refresh rate
+ mpb.WithRefreshRate(180*time.Millisecond),
+ )
+
+ total := 100
+ name := "Single Bar:"
+ // adding a single bar
+ bar := p.AddBar(int64(total),
+ mpb.PrependDecorators(
+ // display our name with one space on the right
+ decor.Name(name, decor.WC{W: len(name) + 1, C: decor.DidentRight}),
+ // replace ETA decorator with "done" message, OnComplete event
+ decor.OnComplete(
+ // ETA decorator with ewma age of 60, and width reservation of 4
+ decor.EwmaETA(decor.ET_STYLE_GO, 60, decor.WC{W: 4}), "done",
+ ),
+ ),
+ mpb.AppendDecorators(decor.Percentage()),
+ )
+ // simulating some work
+ max := 100 * time.Millisecond
+ for i := 0; i < total; i++ {
+ start := time.Now()
+ time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10)
+ // ewma based decorators require work duration measurement
+ bar.IncrBy(1, time.Since(start))
+ }
+ // wait for our bar to complete and flush
+ p.Wait()
+```
+
+#### [Rendering multiple bars](examples/simple/main.go)
+```go
+ var wg sync.WaitGroup
+ p := mpb.New(mpb.WithWaitGroup(&wg))
+ total, numBars := 100, 3
+ wg.Add(numBars)
+
+ for i := 0; i < numBars; i++ {
+ name := fmt.Sprintf("Bar#%d:", i)
+ bar := p.AddBar(int64(total),
+ mpb.PrependDecorators(
+ // simple name decorator
+ decor.Name(name),
+ // decor.DSyncWidth bit enables column width synchronization
+ decor.Percentage(decor.WCSyncSpace),
+ ),
+ mpb.AppendDecorators(
+ // replace ETA decorator with "done" message, OnComplete event
+ decor.OnComplete(
+ // ETA decorator with ewma age of 60
+ decor.EwmaETA(decor.ET_STYLE_GO, 60), "done",
+ ),
+ ),
+ )
+ // simulating some work
+ go func() {
+ defer wg.Done()
+ max := 100 * time.Millisecond
+ for i := 0; i < total; i++ {
+ start := time.Now()
+ time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10)
+ // ewma based decorators require work duration measurement
+ bar.IncrBy(1, time.Since(start))
+ }
+ }()
+ }
+ // wait for all bars to complete and flush
+ p.Wait()
+```
+
+#### [Dynamic total](examples/dynTotal/main.go)
+
+
+
+#### [Complex example](examples/complex/main.go)
+
+
+
+#### [Bytes counters](examples/io/single/main.go)
+
+
diff --git a/bar.go b/bar.go
new file mode 100644
index 0000000..5a506fc
--- /dev/null
+++ b/bar.go
@@ -0,0 +1,455 @@
+package mpb
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "strings"
+ "sync"
+ "time"
+ "unicode/utf8"
+
+ "github.com/vbauerster/mpb/decor"
+ "github.com/vbauerster/mpb/internal"
+)
+
+const (
+ rLeft = iota
+ rFill
+ rTip
+ rEmpty
+ rRight
+)
+
+const formatLen = 5
+
+type barRunes [formatLen]rune
+
+// Bar represents a progress Bar
+type Bar struct {
+ priority int
+ index int
+
+ runningBar *Bar
+ cacheState *bState
+ operateState chan func(*bState)
+ int64Ch chan int64
+ boolCh chan bool
+ frameReaderCh chan *frameReader
+ syncTableCh chan [][]chan int
+
+ // done is closed by Bar's goroutine, after cacheState is written
+ done chan struct{}
+ // shutdown is closed from master Progress goroutine only
+ shutdown chan struct{}
+}
+
+type (
+ bState struct {
+ id int
+ width int
+ total int64
+ current int64
+ runes barRunes
+ trimLeftSpace bool
+ trimRightSpace bool
+ toComplete bool
+ removeOnComplete bool
+ barClearOnComplete bool
+ completeFlushed bool
+ aDecorators []decor.Decorator
+ pDecorators []decor.Decorator
+ amountReceivers []decor.AmountReceiver
+ shutdownListeners []decor.ShutdownListener
+ refill *refill
+ bufP, bufB, bufA *bytes.Buffer
+ bufNL *bytes.Buffer
+ panicMsg string
+ newLineExtendFn func(io.Writer, *decor.Statistics)
+
+ // following options are assigned to the *Bar
+ priority int
+ runningBar *Bar
+ }
+ refill struct {
+ char rune
+ till int64
+ }
+ frameReader struct {
+ io.Reader
+ extendedLines int
+ toShutdown bool
+ removeOnComplete bool
+ }
+)
+
+func newBar(wg *sync.WaitGroup, id int, total int64, cancel <-chan struct{}, options ...BarOption) *Bar {
+ if total <= 0 {
+ total = time.Now().Unix()
+ }
+
+ s := &bState{
+ id: id,
+ priority: id,
+ total: total,
+ }
+
+ for _, opt := range options {
+ if opt != nil {
+ opt(s)
+ }
+ }
+
+ s.bufP = bytes.NewBuffer(make([]byte, 0, s.width))
+ s.bufB = bytes.NewBuffer(make([]byte, 0, s.width))
+ s.bufA = bytes.NewBuffer(make([]byte, 0, s.width))
+
+ b := &Bar{
+ priority: s.priority,
+ runningBar: s.runningBar,
+ operateState: make(chan func(*bState)),
+ int64Ch: make(chan int64),
+ boolCh: make(chan bool),
+ frameReaderCh: make(chan *frameReader, 1),
+ syncTableCh: make(chan [][]chan int),
+ done: make(chan struct{}),
+ shutdown: make(chan struct{}),
+ }
+
+ if b.runningBar != nil {
+ b.priority = b.runningBar.priority
+ }
+
+ if s.newLineExtendFn != nil {
+ s.bufNL = bytes.NewBuffer(make([]byte, 0, s.width))
+ }
+
+ go b.serve(wg, s, cancel)
+ return b
+}
+
+// RemoveAllPrependers removes all prepend functions.
+func (b *Bar) RemoveAllPrependers() {
+ select {
+ case b.operateState <- func(s *bState) { s.pDecorators = nil }:
+ case <-b.done:
+ }
+}
+
+// RemoveAllAppenders removes all append functions.
+func (b *Bar) RemoveAllAppenders() {
+ select {
+ case b.operateState <- func(s *bState) { s.aDecorators = nil }:
+ case <-b.done:
+ }
+}
+
+// ProxyReader wraps r with metrics required for progress tracking.
+func (b *Bar) ProxyReader(r io.Reader) io.ReadCloser {
+ if r == nil {
+ panic("expect io.Reader, got nil")
+ }
+ rc, ok := r.(io.ReadCloser)
+ if !ok {
+ rc = ioutil.NopCloser(r)
+ }
+ return &proxyReader{rc, b, time.Now()}
+}
+
+// ID returs id of the bar.
+func (b *Bar) ID() int {
+ select {
+ case b.operateState <- func(s *bState) { b.int64Ch <- int64(s.id) }:
+ return int(<-b.int64Ch)
+ case <-b.done:
+ return b.cacheState.id
+ }
+}
+
+// Current returns bar's current number, in other words sum of all increments.
+func (b *Bar) Current() int64 {
+ select {
+ case b.operateState <- func(s *bState) { b.int64Ch <- s.current }:
+ return <-b.int64Ch
+ case <-b.done:
+ return b.cacheState.current
+ }
+}
+
+// SetTotal sets total dynamically.
+// Set final to true, when total is known, it will trigger bar complete event.
+func (b *Bar) SetTotal(total int64, final bool) bool {
+ select {
+ case b.operateState <- func(s *bState) {
+ if total > 0 {
+ s.total = total
+ }
+ if final {
+ s.current = s.total
+ s.toComplete = true
+ }
+ }:
+ return true
+ case <-b.done:
+ return false
+ }
+}
+
+// SetRefill sets fill rune to r, up until n.
+func (b *Bar) SetRefill(n int, r rune) {
+ if n <= 0 {
+ return
+ }
+ b.operateState <- func(s *bState) {
+ s.refill = &refill{r, int64(n)}
+ }
+}
+
+// RefillBy is deprecated, use SetRefill
+func (b *Bar) RefillBy(n int, r rune) {
+ b.SetRefill(n, r)
+}
+
+// Increment is a shorthand for b.IncrBy(1).
+func (b *Bar) Increment() {
+ b.IncrBy(1)
+}
+
+// IncrBy increments progress bar by amount of n.
+// wdd is optional work duration i.e. time.Since(start),
+// which expected to be provided, if any ewma based decorator is used.
+func (b *Bar) IncrBy(n int, wdd ...time.Duration) {
+ select {
+ case b.operateState <- func(s *bState) {
+ s.current += int64(n)
+ if s.current >= s.total {
+ s.current = s.total
+ s.toComplete = true
+ }
+ for _, ar := range s.amountReceivers {
+ ar.NextAmount(n, wdd...)
+ }
+ }:
+ case <-b.done:
+ }
+}
+
+// Completed reports whether the bar is in completed state.
+func (b *Bar) Completed() bool {
+ // omit select here, because primary usage of the method is for loop
+ // condition, like for !bar.Completed() {...}
+ // so when toComplete=true it is called once (at which time, the bar is still alive),
+ // then quits the loop and never suppose to be called afterwards.
+ return <-b.boolCh
+}
+
+func (b *Bar) wSyncTable() [][]chan int {
+ select {
+ case b.operateState <- func(s *bState) { b.syncTableCh <- s.wSyncTable() }:
+ return <-b.syncTableCh
+ case <-b.done:
+ return b.cacheState.wSyncTable()
+ }
+}
+
+func (b *Bar) serve(wg *sync.WaitGroup, s *bState, cancel <-chan struct{}) {
+ defer wg.Done()
+ for {
+ select {
+ case op := <-b.operateState:
+ op(s)
+ case b.boolCh <- s.toComplete:
+ case <-cancel:
+ s.toComplete = true
+ cancel = nil
+ case <-b.shutdown:
+ b.cacheState = s
+ close(b.done)
+ for _, sl := range s.shutdownListeners {
+ sl.Shutdown()
+ }
+ return
+ }
+ }
+}
+
+func (b *Bar) render(debugOut io.Writer, tw int) {
+ select {
+ case b.operateState <- func(s *bState) {
+ defer func() {
+ // recovering if user defined decorator panics for example
+ if p := recover(); p != nil {
+ s.panicMsg = fmt.Sprintf("panic: %v", p)
+ fmt.Fprintf(debugOut, "%s %s bar id %02d %v\n", "[mpb]", time.Now(), s.id, s.panicMsg)
+ b.frameReaderCh <- &frameReader{
+ Reader: strings.NewReader(fmt.Sprintf(fmt.Sprintf("%%.%ds\n", tw), s.panicMsg)),
+ toShutdown: true,
+ }
+ }
+ }()
+ r := s.draw(tw)
+ var extendedLines int
+ if s.newLineExtendFn != nil {
+ s.bufNL.Reset()
+ s.newLineExtendFn(s.bufNL, newStatistics(s))
+ extendedLines = countLines(s.bufNL.Bytes())
+ r = io.MultiReader(r, s.bufNL)
+ }
+ b.frameReaderCh <- &frameReader{
+ Reader: r,
+ extendedLines: extendedLines,
+ toShutdown: s.toComplete && !s.completeFlushed,
+ removeOnComplete: s.removeOnComplete,
+ }
+ s.completeFlushed = s.toComplete
+ }:
+ case <-b.done:
+ s := b.cacheState
+ r := s.draw(tw)
+ var extendedLines int
+ if s.newLineExtendFn != nil {
+ s.bufNL.Reset()
+ s.newLineExtendFn(s.bufNL, newStatistics(s))
+ extendedLines = countLines(s.bufNL.Bytes())
+ r = io.MultiReader(r, s.bufNL)
+ }
+ b.frameReaderCh <- &frameReader{
+ Reader: r,
+ extendedLines: extendedLines,
+ }
+ }
+}
+
+func (s *bState) draw(termWidth int) io.Reader {
+ defer s.bufA.WriteByte('\n')
+
+ if s.panicMsg != "" {
+ return strings.NewReader(fmt.Sprintf(fmt.Sprintf("%%.%ds\n", termWidth), s.panicMsg))
+ }
+
+ stat := newStatistics(s)
+
+ for _, d := range s.pDecorators {
+ s.bufP.WriteString(d.Decor(stat))
+ }
+
+ for _, d := range s.aDecorators {
+ s.bufA.WriteString(d.Decor(stat))
+ }
+
+ prependCount := utf8.RuneCount(s.bufP.Bytes())
+ appendCount := utf8.RuneCount(s.bufA.Bytes())
+
+ if s.barClearOnComplete && s.completeFlushed {
+ return io.MultiReader(s.bufP, s.bufA)
+ }
+
+ s.fillBar(s.width)
+ barCount := utf8.RuneCount(s.bufB.Bytes())
+ totalCount := prependCount + barCount + appendCount
+ if spaceCount := 0; totalCount > termWidth {
+ if !s.trimLeftSpace {
+ spaceCount++
+ }
+ if !s.trimRightSpace {
+ spaceCount++
+ }
+ s.fillBar(termWidth - prependCount - appendCount - spaceCount)
+ }
+
+ return io.MultiReader(s.bufP, s.bufB, s.bufA)
+}
+
+func (s *bState) fillBar(width int) {
+ defer func() {
+ s.bufB.WriteRune(s.runes[rRight])
+ if !s.trimRightSpace {
+ s.bufB.WriteByte(' ')
+ }
+ }()
+
+ s.bufB.Reset()
+ if !s.trimLeftSpace {
+ s.bufB.WriteByte(' ')
+ }
+ s.bufB.WriteRune(s.runes[rLeft])
+ if width <= 2 {
+ return
+ }
+
+ // bar s.width without leftEnd and rightEnd runes
+ barWidth := width - 2
+
+ completedWidth := internal.Percentage(s.total, s.current, int64(barWidth))
+
+ if s.refill != nil {
+ till := internal.Percentage(s.total, s.refill.till, int64(barWidth))
+ // append refill rune
+ var i int64
+ for i = 0; i < till; i++ {
+ s.bufB.WriteRune(s.refill.char)
+ }
+ for i = till; i < completedWidth; i++ {
+ s.bufB.WriteRune(s.runes[rFill])
+ }
+ } else {
+ var i int64
+ for i = 0; i < completedWidth; i++ {
+ s.bufB.WriteRune(s.runes[rFill])
+ }
+ }
+
+ if completedWidth < int64(barWidth) && completedWidth > 0 {
+ _, size := utf8.DecodeLastRune(s.bufB.Bytes())
+ s.bufB.Truncate(s.bufB.Len() - size)
+ s.bufB.WriteRune(s.runes[rTip])
+ }
+
+ for i := completedWidth; i < int64(barWidth); i++ {
+ s.bufB.WriteRune(s.runes[rEmpty])
+ }
+}
+
+func (s *bState) wSyncTable() [][]chan int {
+ columns := make([]chan int, 0, len(s.pDecorators)+len(s.aDecorators))
+ var pCount int
+ for _, d := range s.pDecorators {
+ if ok, ch := d.Syncable(); ok {
+ columns = append(columns, ch)
+ pCount++
+ }
+ }
+ var aCount int
+ for _, d := range s.aDecorators {
+ if ok, ch := d.Syncable(); ok {
+ columns = append(columns, ch)
+ aCount++
+ }
+ }
+ table := make([][]chan int, 2)
+ table[0] = columns[0:pCount]
+ table[1] = columns[pCount : pCount+aCount : pCount+aCount]
+ return table
+}
+
+func newStatistics(s *bState) *decor.Statistics {
+ return &decor.Statistics{
+ ID: s.id,
+ Completed: s.completeFlushed,
+ Total: s.total,
+ Current: s.current,
+ }
+}
+
+func strToBarRunes(format string) (array barRunes) {
+ for i, n := 0, 0; len(format) > 0; i++ {
+ array[i], n = utf8.DecodeRuneInString(format)
+ format = format[n:]
+ }
+ return
+}
+
+func countLines(b []byte) int {
+ return bytes.Count(b, []byte("\n"))
+}
diff --git a/bar_option.go b/bar_option.go
new file mode 100644
index 0000000..e33bce4
--- /dev/null
+++ b/bar_option.go
@@ -0,0 +1,124 @@
+package mpb
+
+import (
+ "io"
+
+ "github.com/vbauerster/mpb/decor"
+)
+
+// BarOption is a function option which changes the default behavior of a bar,
+// if passed to p.AddBar(int64, ...BarOption)
+type BarOption func(*bState)
+
+// AppendDecorators let you inject decorators to the bar's right side
+func AppendDecorators(appenders ...decor.Decorator) BarOption {
+ return func(s *bState) {
+ for _, decorator := range appenders {
+ if ar, ok := decorator.(decor.AmountReceiver); ok {
+ s.amountReceivers = append(s.amountReceivers, ar)
+ }
+ if sl, ok := decorator.(decor.ShutdownListener); ok {
+ s.shutdownListeners = append(s.shutdownListeners, sl)
+ }
+ s.aDecorators = append(s.aDecorators, decorator)
+ }
+ }
+}
+
+// PrependDecorators let you inject decorators to the bar's left side
+func PrependDecorators(prependers ...decor.Decorator) BarOption {
+ return func(s *bState) {
+ for _, decorator := range prependers {
+ if ar, ok := decorator.(decor.AmountReceiver); ok {
+ s.amountReceivers = append(s.amountReceivers, ar)
+ }
+ if sl, ok := decorator.(decor.ShutdownListener); ok {
+ s.shutdownListeners = append(s.shutdownListeners, sl)
+ }
+ s.pDecorators = append(s.pDecorators, decorator)
+ }
+ }
+}
+
+// BarTrimLeft trims left side space of the bar
+func BarTrimLeft() BarOption {
+ return func(s *bState) {
+ s.trimLeftSpace = true
+ }
+}
+
+// BarTrimRight trims right space of the bar
+func BarTrimRight() BarOption {
+ return func(s *bState) {
+ s.trimRightSpace = true
+ }
+}
+
+// BarTrim trims both left and right spaces of the bar
+func BarTrim() BarOption {
+ return func(s *bState) {
+ s.trimLeftSpace = true
+ s.trimRightSpace = true
+ }
+}
+
+// BarID overwrites internal bar id
+func BarID(id int) BarOption {
+ return func(s *bState) {
+ s.id = id
+ }
+}
+
+// BarRemoveOnComplete is a flag, if set whole bar line will be removed on complete event.
+// If both BarRemoveOnComplete and BarClearOnComplete are set, first bar section gets cleared
+// and then whole bar line gets removed completely.
+func BarRemoveOnComplete() BarOption {
+ return func(s *bState) {
+ s.removeOnComplete = true
+ }
+}
+
+// BarReplaceOnComplete is indicator for delayed bar start, after the `runningBar` is complete.
+// To achieve bar replacement effect, `runningBar` should has its `BarRemoveOnComplete` option set.
+func BarReplaceOnComplete(runningBar *Bar) BarOption {
+ return func(s *bState) {
+ s.runningBar = runningBar
+ }
+}
+
+// BarClearOnComplete is a flag, if set will clear bar section on complete event.
+// If you need to remove a whole bar line, refer to BarRemoveOnComplete.
+func BarClearOnComplete() BarOption {
+ return func(s *bState) {
+ s.barClearOnComplete = true
+ }
+}
+
+// BarPriority sets bar's priority.
+// Zero is highest priority, i.e. bar will be on top.
+// If `BarReplaceOnComplete` option is supplied, this option is ignored.
+func BarPriority(priority int) BarOption {
+ return func(s *bState) {
+ s.priority = priority
+ }
+}
+
+// BarNewLineExtend takes user defined efn, which gets called each render cycle.
+// Any write to provided writer of efn, will appear on new line of respective bar.
+func BarNewLineExtend(efn func(io.Writer, *decor.Statistics)) BarOption {
+ return func(s *bState) {
+ s.newLineExtendFn = efn
+ }
+}
+
+func barWidth(w int) BarOption {
+ return func(s *bState) {
+ s.width = w
+ }
+}
+
+func barFormat(format string) BarOption {
+ return func(s *bState) {
+ s.runes = strToBarRunes(format)
+ }
+}
diff --git a/bar_test.go b/bar_test.go
new file mode 100644
index 0000000..f97d5c7
--- /dev/null
+++ b/bar_test.go
@@ -0,0 +1,129 @@
+package mpb_test
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "strings"
+ "testing"
+ "time"
+
+ . "github.com/vbauerster/mpb"
+ "github.com/vbauerster/mpb/decor"
+)
+
+func TestBarCompleted(t *testing.T) {
+ p := New(WithOutput(ioutil.Discard))
+ total := 80
+ bar := p.AddBar(int64(total))
+
+ var count int
+ for !bar.Completed() {
+ time.Sleep(10 * time.Millisecond)
+ bar.Increment()
+ count++
+ }
+
+ p.Wait()
+ if count != total {
+ t.Errorf("got count: %d, expected %d\n", count, total)
+ }
+}
+
+func TestBarID(t *testing.T) {
+ p := New(WithOutput(ioutil.Discard))
+ total := 80
+ wantID := 11
+ bar := p.AddBar(int64(total), BarID(wantID))
+
+ go func() {
+ for i := 0; i < total; i++ {
+ time.Sleep(50 * time.Millisecond)
+ bar.Increment()
+ }
+ }()
+
+ gotID := bar.ID()
+ if gotID != wantID {
+ t.Errorf("Expected bar id: %d, got %d\n", wantID, gotID)
+ }
+
+ p.Abort(bar, true)
+ p.Wait()
+}
+
+func TestBarSetRefill(t *testing.T) {
+ var buf bytes.Buffer
+
+ width := 100
+ p := New(WithOutput(&buf), WithWidth(width))
+
+ total := 100
+ till := 30
+ refillRune := '+'
+
+ bar := p.AddBar(int64(total), BarTrim())
+
+ bar.SetRefill(till, refillRune)
+ bar.IncrBy(till)
+
+ for i := 0; i < total-till; i++ {
+ bar.Increment()
+ time.Sleep(10 * time.Millisecond)
+ }
+
+ p.Wait()
+
+ wantBar := fmt.Sprintf("[%s%s]",
+ strings.Repeat(string(refillRune), till-1),
+ strings.Repeat("=", total-till-1))
+
+ if !strings.Contains(buf.String(), wantBar) {
+ t.Errorf("Want bar: %s, got bar: %s\n", wantBar, buf.String())
+ }
+}
+
+func TestBarPanics(t *testing.T) {
+ var buf bytes.Buffer
+ p := New(WithDebugOutput(&buf), WithOutput(ioutil.Discard))
+
+ wantPanic := "Upps!!!"
+ total := 100
+
+ bar := p.AddBar(int64(total), PrependDecorators(panicDecorator(wantPanic)))
+
+ go func() {
+ for i := 0; i < 100; i++ {
+ time.Sleep(10 * time.Millisecond)
+ bar.Increment()
+ }
+ }()
+
+ p.Wait()
+
+ wantPanic = fmt.Sprintf("panic: %s", wantPanic)
+ debugStr := buf.String()
+ if !strings.Contains(debugStr, wantPanic) {
+ t.Errorf("%q doesn't contain %q\n", debugStr, wantPanic)
+ }
+}
+
+func panicDecorator(panicMsg string) decor.Decorator {
+ d := &decorator{
+ panicMsg: panicMsg,
+ }
+ d.Init()
+ return d
+}
+
+type decorator struct {
+ decor.WC
+ panicMsg string
+}
+
+func (d *decorator) Decor(st *decor.Statistics) string {
+ if st.Current >= 42 {
+ panic(d.panicMsg)
+ }
+ return d.FormatMsg("")
+}
diff --git a/barbench_test.go b/barbench_test.go
new file mode 100644
index 0000000..d5d904c
--- /dev/null
+++ b/barbench_test.go
@@ -0,0 +1,43 @@
+package mpb
+
+import (
+ "io/ioutil"
+ "testing"
+
+ "github.com/vbauerster/mpb/decor"
+)
+
+func BenchmarkIncrSingleBar(b *testing.B) {
+ p := New(WithOutput(ioutil.Discard))
+ bar := p.AddBar(int64(b.N))
+ for i := 0; i < b.N; i++ {
+ bar.Increment()
+ }
+}
+
+func BenchmarkIncrSingleBarWhileIsNotCompleted(b *testing.B) {
+ p := New(WithOutput(ioutil.Discard))
+ bar := p.AddBar(int64(b.N))
+ for !bar.Completed() {
+ bar.Increment()
+ }
+}
+
+func BenchmarkIncrSingleBarWithNameDecorator(b *testing.B) {
+ p := New(WithOutput(ioutil.Discard))
+ bar := p.AddBar(int64(b.N), PrependDecorators(decor.Name("test")))
+ for i := 0; i < b.N; i++ {
+ bar.Increment()
+ }
+}
+
+func BenchmarkIncrSingleBarWithNameAndEwmaETADecorator(b *testing.B) {
+ p := New(WithOutput(ioutil.Discard))
+ bar := p.AddBar(int64(b.N),
+ PrependDecorators(decor.Name("test")),
+ AppendDecorators(decor.EwmaETA(decor.ET_STYLE_GO, 60)),
+ )
+ for i := 0; i < b.N; i++ {
+ bar.Increment()
+ }
+}
diff --git a/cwriter/export_test.go b/cwriter/export_test.go
new file mode 100644
index 0000000..1219508
--- /dev/null
+++ b/cwriter/export_test.go
@@ -0,0 +1,3 @@
+package cwriter
+
+var ClearCursorAndLine = clearCursorAndLine
diff --git a/cwriter/writer.go b/cwriter/writer.go
new file mode 100644
index 0000000..0b1470d
--- /dev/null
+++ b/cwriter/writer.go
@@ -0,0 +1,78 @@
+package cwriter
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+
+ isatty "github.com/mattn/go-isatty"
+ "golang.org/x/crypto/ssh/terminal"
+)
+
+// ESC is the ASCII code for escape character
+const ESC = 27
+
+var NotATTY = errors.New("not a terminal")
+
+var (
+ cursorUp = fmt.Sprintf("%c[%dA", ESC, 1)
+ clearLine = fmt.Sprintf("%c[2K\r", ESC)
+ clearCursorAndLine = cursorUp + clearLine
+)
+
+// Writer is a buffered the writer that updates the terminal.
+// The contents of writer will be flushed when Flush is called.
+type Writer struct {
+ out io.Writer
+ buf bytes.Buffer
+ isTerminal bool
+ fd int
+ lineCount int
+}
+
+// New returns a new Writer with defaults
+func New(out io.Writer) *Writer {
+ w := &Writer{out: out}
+ if f, ok := out.(*os.File); ok {
+ fd := f.Fd()
+ w.isTerminal = isatty.IsTerminal(fd)
+ w.fd = int(fd)
+ }
+ return w
+}
+
+// Flush flushes the underlying buffer
+func (w *Writer) Flush(lineCount int) error {
+ err := w.clearLines()
+ w.lineCount = lineCount
+ // WriteTo takes care of w.buf.Reset
+ if _, e := w.buf.WriteTo(w.out); err == nil {
+ err = e
+ }
+ return err
+}
+
+// Write appends the contents of p to the underlying buffer
+func (w *Writer) Write(p []byte) (n int, err error) {
+ return w.buf.Write(p)
+}
+
+// WriteString writes string to the underlying buffer
+func (w *Writer) WriteString(s string) (n int, err error) {
+ return w.buf.WriteString(s)
+}
+
+// ReadFrom reads from the provided io.Reader and writes to the underlying buffer.
+func (w *Writer) ReadFrom(r io.Reader) (n int64, err error) {
+ return w.buf.ReadFrom(r)
+}
+
+func (w *Writer) GetWidth() (int, error) {
+ if w.isTerminal {
+ tw, _, err := terminal.GetSize(w.fd)
+ return tw, err
+ }
+ return -1, NotATTY
+}
diff --git a/cwriter/writer_posix.go b/cwriter/writer_posix.go
new file mode 100644
index 0000000..05e31c4
--- /dev/null
+++ b/cwriter/writer_posix.go
@@ -0,0 +1,13 @@
+// +build !windows
+
+package cwriter
+
+import (
+ "io"
+ "strings"
+)
+
+func (w *Writer) clearLines() error {
+ _, err := io.WriteString(w.out, strings.Repeat(clearCursorAndLine, w.lineCount))
+ return err
+}
diff --git a/cwriter/writer_posix_test.go b/cwriter/writer_posix_test.go
new file mode 100644
index 0000000..e2a48a4
--- /dev/null
+++ b/cwriter/writer_posix_test.go
@@ -0,0 +1,36 @@
+// +build !windows
+
+package cwriter_test
+
+import (
+ "bytes"
+ "testing"
+
+ . "github.com/vbauerster/mpb/cwriter"
+)
+
+// TestWriterPosix by writing and flushing many times. The output buffer
+// must contain the clearCursor and clearLine sequences.
+func TestWriterPosix(t *testing.T) {
+ out := new(bytes.Buffer)
+ w := New(out)
+
+ for _, tcase := range []struct {
+ input, expectedOutput string
+ }{
+ {input: "foo\n", expectedOutput: "foo\n"},
+ {input: "bar\n", expectedOutput: "foo\n" + ClearCursorAndLine + "bar\n"},
+ {input: "fizz\n", expectedOutput: "foo\n" + ClearCursorAndLine + "bar\n" + ClearCursorAndLine + "fizz\n"},
+ } {
+ t.Run(tcase.input, func(t *testing.T) {
+ s := []byte(tcase.input)
+ lc := bytes.Count(s, []byte("\n"))
+ w.Write(s)
+ w.Flush(lc)
+ output := out.String()
+ if output != tcase.expectedOutput {
+ t.Fatalf("want %q, got %q", tcase.expectedOutput, output)
+ }
+ })
+ }
+}
diff --git a/cwriter/writer_windows.go b/cwriter/writer_windows.go
new file mode 100644
index 0000000..dad7f50
--- /dev/null
+++ b/cwriter/writer_windows.go
@@ -0,0 +1,77 @@
+// +build windows
+
+package cwriter
+
+import (
+ "io"
+ "strings"
+ "syscall"
+ "unsafe"
+
+ "github.com/mattn/go-isatty"
+)
+
+var kernel32 = syscall.NewLazyDLL("kernel32.dll")
+
+var (
+ procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
+ procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
+ procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW")
+ procFillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute")
+)
+
+type (
+ short int16
+ word uint16
+ dword uint32
+
+ coord struct {
+ x short
+ y short
+ }
+ smallRect struct {
+ left short
+ top short
+ right short
+ bottom short
+ }
+ consoleScreenBufferInfo struct {
+ size coord
+ cursorPosition coord
+ attributes word
+ window smallRect
+ maximumWindowSize coord
+ }
+)
+
+// FdWriter is a writer with a file descriptor.
+type FdWriter interface {
+ io.Writer
+ Fd() uintptr
+}
+
+func (w *Writer) clearLines() error {
+ f, ok := w.out.(FdWriter)
+ if ok && !isatty.IsTerminal(f.Fd()) {
+ _, err := io.WriteString(w.out, strings.Repeat(clearCursorAndLine, w.lineCount))
+ return err
+ }
+ fd := f.Fd()
+ var info consoleScreenBufferInfo
+ procGetConsoleScreenBufferInfo.Call(fd, uintptr(unsafe.Pointer(&info)))
+
+ for i := 0; i < w.lineCount; i++ {
+ // move the cursor up
+ info.cursorPosition.y--
+ procSetConsoleCursorPosition.Call(fd, uintptr(*(*int32)(unsafe.Pointer(&info.cursorPosition))))
+ // clear the line
+ cursor := coord{
+ x: info.window.left,
+ y: info.window.top + info.cursorPosition.y,
+ }
+ var count, w dword
+ count = dword(info.size.x)
+ procFillConsoleOutputCharacter.Call(fd, uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&w)))
+ }
+ return nil
+}
diff --git a/decor/counters.go b/decor/counters.go
new file mode 100644
index 0000000..e4161dc
--- /dev/null
+++ b/decor/counters.go
@@ -0,0 +1,206 @@
+package decor
+
+import (
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+)
+
+const (
+ _ = iota
+ KiB = 1 << (iota * 10)
+ MiB
+ GiB
+ TiB
+)
+
+const (
+ KB = 1000
+ MB = KB * 1000
+ GB = MB * 1000
+ TB = GB * 1000
+)
+
+const (
+ _ = iota
+ UnitKiB
+ UnitKB
+)
+
+type CounterKiB int64
+
+func (c CounterKiB) Format(st fmt.State, verb rune) {
+ prec, ok := st.Precision()
+
+ if verb == 'd' || !ok {
+ prec = 0
+ }
+ if verb == 'f' && !ok {
+ prec = 6
+ }
+ // retain old beahavior if s verb used
+ if verb == 's' {
+ prec = 1
+ }
+
+ var res, unit string
+ switch {
+ case c >= TiB:
+ unit = "TiB"
+ res = strconv.FormatFloat(float64(c)/TiB, 'f', prec, 64)
+ case c >= GiB:
+ unit = "GiB"
+ res = strconv.FormatFloat(float64(c)/GiB, 'f', prec, 64)
+ case c >= MiB:
+ unit = "MiB"
+ res = strconv.FormatFloat(float64(c)/MiB, 'f', prec, 64)
+ case c >= KiB:
+ unit = "KiB"
+ res = strconv.FormatFloat(float64(c)/KiB, 'f', prec, 64)
+ default:
+ unit = "b"
+ res = strconv.FormatInt(int64(c), 10)
+ }
+
+ if st.Flag(' ') {
+ res += " "
+ }
+ res += unit
+
+ if w, ok := st.Width(); ok {
+ if len(res) < w {
+ pad := strings.Repeat(" ", w-len(res))
+ if st.Flag(int('-')) {
+ res += pad
+ } else {
+ res = pad + res
+ }
+ }
+ }
+
+ io.WriteString(st, res)
+}
+
+type CounterKB int64
+
+func (c CounterKB) Format(st fmt.State, verb rune) {
+ prec, ok := st.Precision()
+
+ if verb == 'd' || !ok {
+ prec = 0
+ }
+ if verb == 'f' && !ok {
+ prec = 6
+ }
+ // retain old beahavior if s verb used
+ if verb == 's' {
+ prec = 1
+ }
+
+ var res, unit string
+ switch {
+ case c >= TB:
+ unit = "TB"
+ res = strconv.FormatFloat(float64(c)/TB, 'f', prec, 64)
+ case c >= GB:
+ unit = "GB"
+ res = strconv.FormatFloat(float64(c)/GB, 'f', prec, 64)
+ case c >= MB:
+ unit = "MB"
+ res = strconv.FormatFloat(float64(c)/MB, 'f', prec, 64)
+ case c >= KB:
+ unit = "kB"
+ res = strconv.FormatFloat(float64(c)/KB, 'f', prec, 64)
+ default:
+ unit = "b"
+ res = strconv.FormatInt(int64(c), 10)
+ }
+
+ if st.Flag(' ') {
+ res += " "
+ }
+ res += unit
+
+ if w, ok := st.Width(); ok {
+ if len(res) < w {
+ pad := strings.Repeat(" ", w-len(res))
+ if st.Flag(int('-')) {
+ res += pad
+ } else {
+ res = pad + res
+ }
+ }
+ }
+
+ io.WriteString(st, res)
+}
+
+// CountersNoUnit is a wrapper around Counters with no unit param.
+func CountersNoUnit(pairFormat string, wcc ...WC) Decorator {
+ return Counters(0, pairFormat, wcc...)
+}
+
+// CountersKibiByte is a wrapper around Counters with predefined unit UnitKiB (bytes/1024).
+func CountersKibiByte(pairFormat string, wcc ...WC) Decorator {
+ return Counters(UnitKiB, pairFormat, wcc...)
+}
+
+// CountersKiloByte is a wrapper around Counters with predefined unit UnitKB (bytes/1000).
+func CountersKiloByte(pairFormat string, wcc ...WC) Decorator {
+ return Counters(UnitKB, pairFormat, wcc...)
+}
+
+// Counters decorator with dynamic unit measure adjustment.
+//
+// `unit` one of [0|UnitKiB|UnitKB] zero for no unit
+//
+// `pairFormat` printf compatible verbs for current and total, like "%f" or "%d"
+//
+// `wcc` optional WC config
+//
+// pairFormat example if UnitKB is chosen:
+//
+// "%.1f / %.1f" = "1.0MB / 12.0MB" or "% .1f / % .1f" = "1.0 MB / 12.0 MB"
+func Counters(unit int, pairFormat string, wcc ...WC) Decorator {
+ var wc WC
+ for _, widthConf := range wcc {
+ wc = widthConf
+ }
+ wc.Init()
+ d := &countersDecorator{
+ WC: wc,
+ unit: unit,
+ pairFormat: pairFormat,
+ }
+ return d
+}
+
+type countersDecorator struct {
+ WC
+ unit int
+ pairFormat string
+ completeMsg *string
+}
+
+func (d *countersDecorator) Decor(st *Statistics) string {
+ if st.Completed && d.completeMsg != nil {
+ return d.FormatMsg(*d.completeMsg)
+ }
+
+ var str string
+ switch d.unit {
+ case UnitKiB:
+ str = fmt.Sprintf(d.pairFormat, CounterKiB(st.Current), CounterKiB(st.Total))
+ case UnitKB:
+ str = fmt.Sprintf(d.pairFormat, CounterKB(st.Current), CounterKB(st.Total))
+ default:
+ str = fmt.Sprintf(d.pairFormat, st.Current, st.Total)
+ }
+
+ return d.FormatMsg(str)
+}
+
+func (d *countersDecorator) OnCompleteMessage(msg string) {
+ d.completeMsg = &msg
+}
diff --git a/decor/counters_test.go b/decor/counters_test.go
new file mode 100644
index 0000000..9988d50
--- /dev/null
+++ b/decor/counters_test.go
@@ -0,0 +1,142 @@
+package decor
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestCounterKiB(t *testing.T) {
+ cases := map[string]struct {
+ value int64
+ verb string
+ expected string
+ }{
+ "verb %f": {12345678, "%f", "11.773756MiB"},
+ "verb %.0f": {12345678, "%.0f", "12MiB"},
+ "verb %.1f": {12345678, "%.1f", "11.8MiB"},
+ "verb %.2f": {12345678, "%.2f", "11.77MiB"},
+ "verb %.3f": {12345678, "%.3f", "11.774MiB"},
+
+ "verb % f": {12345678, "% f", "11.773756 MiB"},
+ "verb % .0f": {12345678, "% .0f", "12 MiB"},
+ "verb % .1f": {12345678, "% .1f", "11.8 MiB"},
+ "verb % .2f": {12345678, "% .2f", "11.77 MiB"},
+ "verb % .3f": {12345678, "% .3f", "11.774 MiB"},
+
+ "verb %8.f": {12345678, "%8.f", " 12MiB"},
+ "verb %8.0f": {12345678, "%8.0f", " 12MiB"},
+ "verb %8.1f": {12345678, "%8.1f", " 11.8MiB"},
+ "verb %8.2f": {12345678, "%8.2f", "11.77MiB"},
+ "verb %8.3f": {12345678, "%8.3f", "11.774MiB"},
+
+ "verb % 8.f": {12345678, "% 8.f", " 12 MiB"},
+ "verb % 8.0f": {12345678, "% 8.0f", " 12 MiB"},
+ "verb % 8.1f": {12345678, "% 8.1f", "11.8 MiB"},
+
+ "verb %-8.f": {12345678, "%-8.f", "12MiB "},
+ "verb %-8.0f": {12345678, "%-8.0f", "12MiB "},
+ "verb %-8.1f": {12345678, "%-8.1f", "11.8MiB "},
+ "verb %-8.2f": {12345678, "%8.2f", "11.77MiB"},
+ "verb %-8.3f": {12345678, "%8.3f", "11.774MiB"},
+
+ "verb % -8.f": {12345678, "% -8.f", "12 MiB "},
+ "verb % -8.0f": {12345678, "% -8.0f", "12 MiB "},
+ "verb % -8.1f": {12345678, "% -8.1f", "11.8 MiB"},
+
+ "1000 %f": {1000, "%f", "1000b"},
+ "1000 %d": {1000, "%d", "1000b"},
+ "1000 %s": {1000, "%s", "1000b"},
+ "1024 %f": {1024, "%f", "1.000000KiB"},
+ "1024 %d": {1024, "%d", "1KiB"},
+ "1024 %.1f": {1024, "%.1f", "1.0KiB"},
+ "1024 %s": {1024, "%s", "1.0KiB"},
+ "3*MiB+140KiB %f": {3*MiB + 140*KiB, "%f", "3.136719MiB"},
+ "3*MiB+140KiB %d": {3*MiB + 140*KiB, "%d", "3MiB"},
+ "3*MiB+140KiB %.1f": {3*MiB + 140*KiB, "%.1f", "3.1MiB"},
+ "3*MiB+140KiB %s": {3*MiB + 140*KiB, "%s", "3.1MiB"},
+ "2*GiB %f": {2 * GiB, "%f", "2.000000GiB"},
+ "2*GiB %d": {2 * GiB, "%d", "2GiB"},
+ "2*GiB %.1f": {2 * GiB, "%.1f", "2.0GiB"},
+ "2*GiB %s": {2 * GiB, "%s", "2.0GiB"},
+ "4*TiB %f": {4 * TiB, "%f", "4.000000TiB"},
+ "4*TiB %d": {4 * TiB, "%d", "4TiB"},
+ "4*TiB %.1f": {4 * TiB, "%.1f", "4.0TiB"},
+ "4*TiB %s": {4 * TiB, "%s", "4.0TiB"},
+ }
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ got := fmt.Sprintf(tc.verb, CounterKiB(tc.value))
+ if got != tc.expected {
+ t.Fatalf("expected: %q, got: %q\n", tc.expected, got)
+ }
+ })
+ }
+}
+
+func TestCounterKB(t *testing.T) {
+ cases := map[string]struct {
+ value int64
+ verb string
+ expected string
+ }{
+ "verb %f": {12345678, "%f", "12.345678MB"},
+ "verb %.0f": {12345678, "%.0f", "12MB"},
+ "verb %.1f": {12345678, "%.1f", "12.3MB"},
+ "verb %.2f": {12345678, "%.2f", "12.35MB"},
+ "verb %.3f": {12345678, "%.3f", "12.346MB"},
+
+ "verb % f": {12345678, "% f", "12.345678 MB"},
+ "verb % .0f": {12345678, "% .0f", "12 MB"},
+ "verb % .1f": {12345678, "% .1f", "12.3 MB"},
+ "verb % .2f": {12345678, "% .2f", "12.35 MB"},
+ "verb % .3f": {12345678, "% .3f", "12.346 MB"},
+
+ "verb %8.f": {12345678, "%8.f", " 12MB"},
+ "verb %8.0f": {12345678, "%8.0f", " 12MB"},
+ "verb %8.1f": {12345678, "%8.1f", " 12.3MB"},
+ "verb %8.2f": {12345678, "%8.2f", " 12.35MB"},
+ "verb %8.3f": {12345678, "%8.3f", "12.346MB"},
+
+ "verb % 8.f": {12345678, "% 8.f", " 12 MB"},
+ "verb % 8.0f": {12345678, "% 8.0f", " 12 MB"},
+ "verb % 8.1f": {12345678, "% 8.1f", " 12.3 MB"},
+
+ "verb %-8.f": {12345678, "%-8.f", "12MB "},
+ "verb %-8.0f": {12345678, "%-8.0f", "12MB "},
+ "verb %-8.1f": {12345678, "%-8.1f", "12.3MB "},
+ "verb %-8.2f": {12345678, "%8.2f", " 12.35MB"},
+ "verb %-8.3f": {12345678, "%8.3f", "12.346MB"},
+
+ "verb % -8.f": {12345678, "% -8.f", "12 MB "},
+ "verb % -8.0f": {12345678, "% -8.0f", "12 MB "},
+ "verb % -8.1f": {12345678, "% -8.1f", "12.3 MB "},
+
+ "1000 %f": {1000, "%f", "1.000000kB"},
+ "1000 %d": {1000, "%d", "1kB"},
+ "1000 %s": {1000, "%s", "1.0kB"},
+ "1024 %f": {1024, "%f", "1.024000kB"},
+ "1024 %d": {1024, "%d", "1kB"},
+ "1024 %.1f": {1024, "%.1f", "1.0kB"},
+ "1024 %s": {1024, "%s", "1.0kB"},
+ "3*MB+140*KB %f": {3*MB + 140*KB, "%f", "3.140000MB"},
+ "3*MB+140*KB %d": {3*MB + 140*KB, "%d", "3MB"},
+ "3*MB+140*KB %.1f": {3*MB + 140*KB, "%.1f", "3.1MB"},
+ "3*MB+140*KB %s": {3*MB + 140*KB, "%s", "3.1MB"},
+ "2*GB %f": {2 * GB, "%f", "2.000000GB"},
+ "2*GB %d": {2 * GB, "%d", "2GB"},
+ "2*GB %.1f": {2 * GB, "%.1f", "2.0GB"},
+ "2*GB %s": {2 * GB, "%s", "2.0GB"},
+ "4*TB %f": {4 * TB, "%f", "4.000000TB"},
+ "4*TB %d": {4 * TB, "%d", "4TB"},
+ "4*TB %.1f": {4 * TB, "%.1f", "4.0TB"},
+ "4*TB %s": {4 * TB, "%s", "4.0TB"},
+ }
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ got := fmt.Sprintf(tc.verb, CounterKB(tc.value))
+ if got != tc.expected {
+ t.Fatalf("expected: %q, got: %q\n", tc.expected, got)
+ }
+ })
+ }
+}
diff --git a/decor/decorator.go b/decor/decorator.go
new file mode 100644
index 0000000..6aaf6c8
--- /dev/null
+++ b/decor/decorator.go
@@ -0,0 +1,144 @@
+package decor
+
+import (
+ "fmt"
+ "time"
+ "unicode/utf8"
+)
+
+const (
+ // DidentRight bit specifies identation direction.
+ // |foo |b | With DidentRight
+ // | foo| b| Without DidentRight
+ DidentRight = 1 << iota
+
+ // DextraSpace bit adds extra space, makes sense with DSyncWidth only.
+ // When DidentRight bit set, the space will be added to the right,
+ // otherwise to the left.
+ DextraSpace
+
+ // DSyncWidth bit enables same column width synchronization.
+ // Effective with multiple bars only.
+ DSyncWidth
+
+ // DSyncWidthR is shortcut for DSyncWidth|DidentRight
+ DSyncWidthR = DSyncWidth | DidentRight
+
+ // DSyncSpace is shortcut for DSyncWidth|DextraSpace
+ DSyncSpace = DSyncWidth | DextraSpace
+
+ // DSyncSpaceR is shortcut for DSyncWidth|DextraSpace|DidentRight
+ DSyncSpaceR = DSyncWidth | DextraSpace | DidentRight
+)
+
+const (
+ ET_STYLE_GO = iota
+ ET_STYLE_HHMMSS
+ ET_STYLE_HHMM
+ ET_STYLE_MMSS
+)
+
+// Statistics is a struct, which gets passed to a Decorator.
+type Statistics struct {
+ ID int
+ Completed bool
+ Total int64
+ Current int64
+}
+
+// Decorator interface.
+// A decorator must implement this interface, in order to be used with mpb library.
+type Decorator interface {
+ Decor(*Statistics) string
+ Syncable
+}
+
+// Syncable interface.
+// All decorators implement this interface implicitly.
+// Its Syncable method exposes width sync channel, if sync is enabled.
+type Syncable interface {
+ Syncable() (bool, chan int)
+}
+
+// OnCompleteMessenger interface.
+// Decorators implementing this interface suppose to return provided string on complete event.
+type OnCompleteMessenger interface {
+ OnCompleteMessage(string)
+}
+
+// AmountReceiver interface.
+// If decorator needs to receive increment amount,
+// so this is the right interface to implement.
+type AmountReceiver interface {
+ NextAmount(int, ...time.Duration)
+}
+
+// ShutdownListener interface.
+// If decorator needs to be notified once upon bar shutdown event,
+// so this is the right interface to implement.
+type ShutdownListener interface {
+ Shutdown()
+}
+
+// Global convenience shortcuts
+var (
+ WCSyncWidth = WC{C: DSyncWidth}
+ WCSyncWidthR = WC{C: DSyncWidthR}
+ WCSyncSpace = WC{C: DSyncSpace}
+ WCSyncSpaceR = WC{C: DSyncSpaceR}
+)
+
+// WC is a struct with two public fields W and C, both of int type.
+// W represents width and C represents bit set of width related config.
+type WC struct {
+ W int
+ C int
+ format string
+ wsync chan int
+}
+
+// FormatMsg formats final message according to WC.W and WC.C.
+// Should be called by any Decorator implementation.
+func (wc WC) FormatMsg(msg string) string {
+ if (wc.C & DSyncWidth) != 0 {
+ wc.wsync <- utf8.RuneCountInString(msg)
+ max := <-wc.wsync
+ if max == 0 {
+ max = wc.W
+ }
+ if (wc.C & DextraSpace) != 0 {
+ max++
+ }
+ return fmt.Sprintf(fmt.Sprintf(wc.format, max), msg)
+ }
+ return fmt.Sprintf(fmt.Sprintf(wc.format, wc.W), msg)
+}
+
+// Init initializes width related config.
+func (wc *WC) Init() {
+ wc.format = "%%"
+ if (wc.C & DidentRight) != 0 {
+ wc.format += "-"
+ }
+ wc.format += "%ds"
+ if (wc.C & DSyncWidth) != 0 {
+ wc.wsync = make(chan int)
+ }
+}
+
+func (wc *WC) Syncable() (bool, chan int) {
+ return (wc.C & DSyncWidth) != 0, wc.wsync
+}
+
+// OnComplete returns decorator, which wraps provided decorator, with sole
+// purpose to display provided message on complete event.
+//
+// `decorator` Decorator to wrap
+//
+// `message` message to display on complete event
+func OnComplete(decorator Decorator, message string) Decorator {
+ if d, ok := decorator.(OnCompleteMessenger); ok {
+ d.OnCompleteMessage(message)
+ }
+ return decorator
+}
diff --git a/decor/doc.go b/decor/doc.go
new file mode 100644
index 0000000..561a867
--- /dev/null
+++ b/decor/doc.go
@@ -0,0 +1,25 @@
+// Copyright (C) 2016-2018 Vladimir Bauer
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+/*
+ Package decor contains common decorators used by "github.com/vbauerster/mpb" package.
+
+ Some decorators returned by this package might have a closure state. It is ok to use
+ decorators concurrently, unless you share the same decorator among multiple
+ *mpb.Bar instances. To avoid data races, create new decorator per *mpb.Bar instance.
+
+ Don't:
+
+ p := mpb.New()
+ name := decor.Name("bar")
+ p.AddBar(100, mpb.AppendDecorators(name))
+ p.AddBar(100, mpb.AppendDecorators(name))
+
+ Do:
+
+ p := mpb.New()
+ p.AddBar(100, mpb.AppendDecorators(decor.Name("bar1")))
+ p.AddBar(100, mpb.AppendDecorators(decor.Name("bar2")))
+*/
+package decor
diff --git a/decor/elapsed.go b/decor/elapsed.go
new file mode 100644
index 0000000..649d40a
--- /dev/null
+++ b/decor/elapsed.go
@@ -0,0 +1,68 @@
+package decor
+
+import (
+ "fmt"
+ "time"
+)
+
+// Elapsed returns elapsed time decorator.
+//
+// `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS]
+//
+// `wcc` optional WC config
+func Elapsed(style int, wcc ...WC) Decorator {
+ var wc WC
+ for _, widthConf := range wcc {
+ wc = widthConf
+ }
+ wc.Init()
+ d := &elapsedDecorator{
+ WC: wc,
+ style: style,
+ startTime: time.Now(),
+ }
+ return d
+}
+
+type elapsedDecorator struct {
+ WC
+ style int
+ startTime time.Time
+ msg string
+ completeMsg *string
+}
+
+func (d *elapsedDecorator) Decor(st *Statistics) string {
+ if st.Completed {
+ if d.completeMsg != nil {
+ return d.FormatMsg(*d.completeMsg)
+ }
+ return d.FormatMsg(d.msg)
+ }
+
+ timeElapsed := time.Since(d.startTime)
+ hours := int64((timeElapsed / time.Hour) % 60)
+ minutes := int64((timeElapsed / time.Minute) % 60)
+ seconds := int64((timeElapsed / time.Second) % 60)
+
+ switch d.style {
+ case ET_STYLE_GO:
+ d.msg = fmt.Sprint(time.Duration(timeElapsed.Seconds()) * time.Second)
+ case ET_STYLE_HHMMSS:
+ d.msg = fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
+ case ET_STYLE_HHMM:
+ d.msg = fmt.Sprintf("%02d:%02d", hours, minutes)
+ case ET_STYLE_MMSS:
+ if hours > 0 {
+ d.msg = fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
+ } else {
+ d.msg = fmt.Sprintf("%02d:%02d", minutes, seconds)
+ }
+ }
+
+ return d.FormatMsg(d.msg)
+}
+
+func (d *elapsedDecorator) OnCompleteMessage(msg string) {
+ d.completeMsg = &msg
+}
diff --git a/decor/eta.go b/decor/eta.go
new file mode 100644
index 0000000..44a1f03
--- /dev/null
+++ b/decor/eta.go
@@ -0,0 +1,207 @@
+package decor
+
+import (
+ "fmt"
+ "math"
+ "time"
+
+ "github.com/VividCortex/ewma"
+ "github.com/vbauerster/mpb/internal"
+)
+
+type TimeNormalizer func(time.Duration) time.Duration
+
+// EwmaETA exponential-weighted-moving-average based ETA decorator.
+//
+// `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS]
+//
+// `age` is the previous N samples to average over.
+//
+// `wcc` optional WC config
+func EwmaETA(style int, age float64, wcc ...WC) Decorator {
+ return MovingAverageETA(style, ewma.NewMovingAverage(age), NopNormalizer(), wcc...)
+}
+
+// MovingAverageETA decorator relies on MovingAverage implementation to calculate its average.
+//
+// `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS]
+//
+// `average` available implementations of MovingAverage [ewma.MovingAverage|NewMedian|NewMedianEwma]
+//
+// `normalizer` available implementations are [NopNormalizer|FixedIntervalTimeNormalizer|MaxTolerateTimeNormalizer]
+//
+// `wcc` optional WC config
+func MovingAverageETA(style int, average MovingAverage, normalizer TimeNormalizer, wcc ...WC) Decorator {
+ var wc WC
+ for _, widthConf := range wcc {
+ wc = widthConf
+ }
+ wc.Init()
+ d := &movingAverageETA{
+ WC: wc,
+ style: style,
+ average: average,
+ normalizer: normalizer,
+ }
+ return d
+}
+
+type movingAverageETA struct {
+ WC
+ style int
+ average ewma.MovingAverage
+ completeMsg *string
+ normalizer TimeNormalizer
+}
+
+func (d *movingAverageETA) Decor(st *Statistics) string {
+ if st.Completed && d.completeMsg != nil {
+ return d.FormatMsg(*d.completeMsg)
+ }
+
+ v := internal.Round(d.average.Value())
+ remaining := d.normalizer(time.Duration((st.Total - st.Current) * int64(v)))
+ hours := int64((remaining / time.Hour) % 60)
+ minutes := int64((remaining / time.Minute) % 60)
+ seconds := int64((remaining / time.Second) % 60)
+
+ var str string
+ switch d.style {
+ case ET_STYLE_GO:
+ str = fmt.Sprint(time.Duration(remaining.Seconds()) * time.Second)
+ case ET_STYLE_HHMMSS:
+ str = fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
+ case ET_STYLE_HHMM:
+ str = fmt.Sprintf("%02d:%02d", hours, minutes)
+ case ET_STYLE_MMSS:
+ if hours > 0 {
+ str = fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
+ } else {
+ str = fmt.Sprintf("%02d:%02d", minutes, seconds)
+ }
+ }
+
+ return d.FormatMsg(str)
+}
+
+func (d *movingAverageETA) NextAmount(n int, wdd ...time.Duration) {
+ var workDuration time.Duration
+ for _, wd := range wdd {
+ workDuration = wd
+ }
+ lastItemEstimate := float64(workDuration) / float64(n)
+ if math.IsInf(lastItemEstimate, 0) || math.IsNaN(lastItemEstimate) {
+ return
+ }
+ d.average.Add(lastItemEstimate)
+}
+
+func (d *movingAverageETA) OnCompleteMessage(msg string) {
+ d.completeMsg = &msg
+}
+
+// AverageETA decorator.
+//
+// `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS]
+//
+// `wcc` optional WC config
+func AverageETA(style int, wcc ...WC) Decorator {
+ var wc WC
+ for _, widthConf := range wcc {
+ wc = widthConf
+ }
+ wc.Init()
+ d := &averageETA{
+ WC: wc,
+ style: style,
+ startTime: time.Now(),
+ }
+ return d
+}
+
+type averageETA struct {
+ WC
+ style int
+ startTime time.Time
+ completeMsg *string
+}
+
+func (d *averageETA) Decor(st *Statistics) string {
+ if st.Completed && d.completeMsg != nil {
+ return d.FormatMsg(*d.completeMsg)
+ }
+
+ var str string
+ timeElapsed := time.Since(d.startTime)
+ v := internal.Round(float64(timeElapsed) / float64(st.Current))
+ if math.IsInf(v, 0) || math.IsNaN(v) {
+ v = 0
+ }
+ remaining := time.Duration((st.Total - st.Current) * int64(v))
+ hours := int64((remaining / time.Hour) % 60)
+ minutes := int64((remaining / time.Minute) % 60)
+ seconds := int64((remaining / time.Second) % 60)
+
+ switch d.style {
+ case ET_STYLE_GO:
+ str = fmt.Sprint(time.Duration(remaining.Seconds()) * time.Second)
+ case ET_STYLE_HHMMSS:
+ str = fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
+ case ET_STYLE_HHMM:
+ str = fmt.Sprintf("%02d:%02d", hours, minutes)
+ case ET_STYLE_MMSS:
+ if hours > 0 {
+ str = fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
+ } else {
+ str = fmt.Sprintf("%02d:%02d", minutes, seconds)
+ }
+ }
+
+ return d.FormatMsg(str)
+}
+
+func (d *averageETA) OnCompleteMessage(msg string) {
+ d.completeMsg = &msg
+}
+
+func MaxTolerateTimeNormalizer(maxTolerate time.Duration) TimeNormalizer {
+ var normalized time.Duration
+ var lastCall time.Time
+ return func(remaining time.Duration) time.Duration {
+ if diff := normalized - remaining; diff <= 0 || diff > maxTolerate || remaining < maxTolerate/2 {
+ normalized = remaining
+ lastCall = time.Now()
+ return remaining
+ }
+ normalized -= time.Since(lastCall)
+ lastCall = time.Now()
+ return normalized
+ }
+}
+
+func FixedIntervalTimeNormalizer(updInterval int) TimeNormalizer {
+ var normalized time.Duration
+ var lastCall time.Time
+ var count int
+ return func(remaining time.Duration) time.Duration {
+ if count == 0 || remaining <= time.Duration(15*time.Second) {
+ count = updInterval
+ normalized = remaining
+ lastCall = time.Now()
+ return remaining
+ }
+ count--
+ normalized -= time.Since(lastCall)
+ lastCall = time.Now()
+ if normalized > 0 {
+ return normalized
+ }
+ return remaining
+ }
+}
+
+func NopNormalizer() TimeNormalizer {
+ return func(remaining time.Duration) time.Duration {
+ return remaining
+ }
+}
diff --git a/decor/moving-average.go b/decor/moving-average.go
new file mode 100644
index 0000000..f9596a2
--- /dev/null
+++ b/decor/moving-average.go
@@ -0,0 +1,66 @@
+package decor
+
+import (
+ "sort"
+
+ "github.com/VividCortex/ewma"
+)
+
+// MovingAverage is the interface that computes a moving average over a time-
+// series stream of numbers. The average may be over a window or exponentially
+// decaying.
+type MovingAverage interface {
+ Add(float64)
+ Value() float64
+ Set(float64)
+}
+
+type medianWindow [3]float64
+
+func (s *medianWindow) Len() int { return len(s) }
+func (s *medianWindow) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s *medianWindow) Less(i, j int) bool { return s[i] < s[j] }
+
+func (s *medianWindow) Add(value float64) {
+ s[0], s[1] = s[1], s[2]
+ s[2] = value
+}
+
+func (s *medianWindow) Value() float64 {
+ tmp := *s
+ sort.Sort(&tmp)
+ return tmp[1]
+}
+
+func (s *medianWindow) Set(value float64) {
+ for i := 0; i < len(s); i++ {
+ s[i] = value
+ }
+}
+
+// NewMedian is fixed last 3 samples median MovingAverage.
+func NewMedian() MovingAverage {
+ return new(medianWindow)
+}
+
+type medianEwma struct {
+ count uint
+ median MovingAverage
+ MovingAverage
+}
+
+func (s *medianEwma) Add(v float64) {
+ s.median.Add(v)
+ if s.count >= 2 {
+ s.MovingAverage.Add(s.median.Value())
+ }
+ s.count++
+}
+
+// NewMedianEwma is ewma based MovingAverage, which gets its values from median MovingAverage.
+func NewMedianEwma(age ...float64) MovingAverage {
+ return &medianEwma{
+ MovingAverage: ewma.NewMovingAverage(age...),
+ median: NewMedian(),
+ }
+}
diff --git a/decor/name.go b/decor/name.go
new file mode 100644
index 0000000..a5a5d14
--- /dev/null
+++ b/decor/name.go
@@ -0,0 +1,45 @@
+package decor
+
+// StaticName returns name decorator.
+//
+// `name` string to display
+//
+// `wcc` optional WC config
+func StaticName(name string, wcc ...WC) Decorator {
+ return Name(name, wcc...)
+}
+
+// Name returns name decorator.
+//
+// `name` string to display
+//
+// `wcc` optional WC config
+func Name(name string, wcc ...WC) Decorator {
+ var wc WC
+ for _, widthConf := range wcc {
+ wc = widthConf
+ }
+ wc.Init()
+ d := &nameDecorator{
+ WC: wc,
+ msg: name,
+ }
+ return d
+}
+
+type nameDecorator struct {
+ WC
+ msg string
+ complete *string
+}
+
+func (d *nameDecorator) Decor(st *Statistics) string {
+ if st.Completed && d.complete != nil {
+ return d.FormatMsg(*d.complete)
+ }
+ return d.FormatMsg(d.msg)
+}
+
+func (d *nameDecorator) OnCompleteMessage(msg string) {
+ d.complete = &msg
+}
diff --git a/decor/percentage.go b/decor/percentage.go
new file mode 100644
index 0000000..078fbcf
--- /dev/null
+++ b/decor/percentage.go
@@ -0,0 +1,39 @@
+package decor
+
+import (
+ "fmt"
+
+ "github.com/vbauerster/mpb/internal"
+)
+
+// Percentage returns percentage decorator.
+//
+// `wcc` optional WC config
+func Percentage(wcc ...WC) Decorator {
+ var wc WC
+ for _, widthConf := range wcc {
+ wc = widthConf
+ }
+ wc.Init()
+ d := &percentageDecorator{
+ WC: wc,
+ }
+ return d
+}
+
+type percentageDecorator struct {
+ WC
+ completeMsg *string
+}
+
+func (d *percentageDecorator) Decor(st *Statistics) string {
+ if st.Completed && d.completeMsg != nil {
+ return d.FormatMsg(*d.completeMsg)
+ }
+ str := fmt.Sprintf("%d %%", internal.Percentage(st.Total, st.Current, 100))
+ return d.FormatMsg(str)
+}
+
+func (d *percentageDecorator) OnCompleteMessage(msg string) {
+ d.completeMsg = &msg
+}
diff --git a/decor/speed.go b/decor/speed.go
new file mode 100644
index 0000000..395e5d0
--- /dev/null
+++ b/decor/speed.go
@@ -0,0 +1,270 @@
+package decor
+
+import (
+ "fmt"
+ "io"
+ "math"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/VividCortex/ewma"
+)
+
+type SpeedKiB float64
+
+func (s SpeedKiB) Format(st fmt.State, verb rune) {
+ prec, ok := st.Precision()
+
+ if verb == 'd' || !ok {
+ prec = 0
+ }
+ if verb == 'f' && !ok {
+ prec = 6
+ }
+ // retain old beahavior if s verb used
+ if verb == 's' {
+ prec = 1
+ }
+
+ var res, unit string
+ switch {
+ case s >= TiB:
+ unit = "TiB/s"
+ res = strconv.FormatFloat(float64(s)/TiB, 'f', prec, 64)
+ case s >= GiB:
+ unit = "GiB/s"
+ res = strconv.FormatFloat(float64(s)/GiB, 'f', prec, 64)
+ case s >= MiB:
+ unit = "MiB/s"
+ res = strconv.FormatFloat(float64(s)/MiB, 'f', prec, 64)
+ case s >= KiB:
+ unit = "KiB/s"
+ res = strconv.FormatFloat(float64(s)/KiB, 'f', prec, 64)
+ default:
+ unit = "b/s"
+ res = strconv.FormatInt(int64(s), 10)
+ }
+
+ if st.Flag(' ') {
+ res += " "
+ }
+ res += unit
+
+ if w, ok := st.Width(); ok {
+ if len(res) < w {
+ pad := strings.Repeat(" ", w-len(res))
+ if st.Flag(int('-')) {
+ res += pad
+ } else {
+ res = pad + res
+ }
+ }
+ }
+
+ io.WriteString(st, res)
+}
+
+type SpeedKB float64
+
+func (s SpeedKB) Format(st fmt.State, verb rune) {
+ prec, ok := st.Precision()
+
+ if verb == 'd' || !ok {
+ prec = 0
+ }
+ if verb == 'f' && !ok {
+ prec = 6
+ }
+ // retain old beahavior if s verb used
+ if verb == 's' {
+ prec = 1
+ }
+
+ var res, unit string
+ switch {
+ case s >= TB:
+ unit = "TB/s"
+ res = strconv.FormatFloat(float64(s)/TB, 'f', prec, 64)
+ case s >= GB:
+ unit = "GB/s"
+ res = strconv.FormatFloat(float64(s)/GB, 'f', prec, 64)
+ case s >= MB:
+ unit = "MB/s"
+ res = strconv.FormatFloat(float64(s)/MB, 'f', prec, 64)
+ case s >= KB:
+ unit = "kB/s"
+ res = strconv.FormatFloat(float64(s)/KB, 'f', prec, 64)
+ default:
+ unit = "b/s"
+ res = strconv.FormatInt(int64(s), 10)
+ }
+
+ if st.Flag(' ') {
+ res += " "
+ }
+ res += unit
+
+ if w, ok := st.Width(); ok {
+ if len(res) < w {
+ pad := strings.Repeat(" ", w-len(res))
+ if st.Flag(int('-')) {
+ res += pad
+ } else {
+ res = pad + res
+ }
+ }
+ }
+
+ io.WriteString(st, res)
+}
+
+// EwmaSpeed exponential-weighted-moving-average based speed decorator,
+// with dynamic unit measure adjustment.
+//
+// `unit` one of [0|UnitKiB|UnitKB] zero for no unit
+//
+// `unitFormat` printf compatible verb for value, like "%f" or "%d"
+//
+// `average` MovingAverage implementation
+//
+// `wcc` optional WC config
+//
+// unitFormat example if UnitKiB is chosen:
+//
+// "%.1f" = "1.0MiB/s" or "% .1f" = "1.0 MiB/s"
+func EwmaSpeed(unit int, unitFormat string, age float64, wcc ...WC) Decorator {
+ return MovingAverageSpeed(unit, unitFormat, ewma.NewMovingAverage(age), wcc...)
+}
+
+// MovingAverageSpeed decorator relies on MovingAverage implementation to calculate its average.
+//
+// `unit` one of [0|UnitKiB|UnitKB] zero for no unit
+//
+// `unitFormat` printf compatible verb for value, like "%f" or "%d"
+//
+// `average` MovingAverage implementation
+//
+// `wcc` optional WC config
+func MovingAverageSpeed(unit int, unitFormat string, average MovingAverage, wcc ...WC) Decorator {
+ var wc WC
+ for _, widthConf := range wcc {
+ wc = widthConf
+ }
+ wc.Init()
+ d := &movingAverageSpeed{
+ WC: wc,
+ unit: unit,
+ unitFormat: unitFormat,
+ average: average,
+ }
+ return d
+}
+
+type movingAverageSpeed struct {
+ WC
+ unit int
+ unitFormat string
+ average ewma.MovingAverage
+ msg string
+ completeMsg *string
+}
+
+func (d *movingAverageSpeed) Decor(st *Statistics) string {
+ if st.Completed {
+ if d.completeMsg != nil {
+ return d.FormatMsg(*d.completeMsg)
+ }
+ return d.FormatMsg(d.msg)
+ }
+
+ speed := d.average.Value()
+ switch d.unit {
+ case UnitKiB:
+ d.msg = fmt.Sprintf(d.unitFormat, SpeedKiB(speed))
+ case UnitKB:
+ d.msg = fmt.Sprintf(d.unitFormat, SpeedKB(speed))
+ default:
+ d.msg = fmt.Sprintf(d.unitFormat, speed)
+ }
+
+ return d.FormatMsg(d.msg)
+}
+
+func (s *movingAverageSpeed) NextAmount(n int, wdd ...time.Duration) {
+ var workDuration time.Duration
+ for _, wd := range wdd {
+ workDuration = wd
+ }
+ speed := float64(n) / workDuration.Seconds() / 1000
+ if math.IsInf(speed, 0) || math.IsNaN(speed) {
+ return
+ }
+ s.average.Add(speed)
+}
+
+func (d *movingAverageSpeed) OnCompleteMessage(msg string) {
+ d.completeMsg = &msg
+}
+
+// AverageSpeed decorator with dynamic unit measure adjustment.
+//
+// `unit` one of [0|UnitKiB|UnitKB] zero for no unit
+//
+// `unitFormat` printf compatible verb for value, like "%f" or "%d"
+//
+// `wcc` optional WC config
+//
+// unitFormat example if UnitKiB is chosen:
+//
+// "%.1f" = "1.0MiB/s" or "% .1f" = "1.0 MiB/s"
+func AverageSpeed(unit int, unitFormat string, wcc ...WC) Decorator {
+ var wc WC
+ for _, widthConf := range wcc {
+ wc = widthConf
+ }
+ wc.Init()
+ d := &averageSpeed{
+ WC: wc,
+ unit: unit,
+ unitFormat: unitFormat,
+ startTime: time.Now(),
+ }
+ return d
+}
+
+type averageSpeed struct {
+ WC
+ unit int
+ unitFormat string
+ startTime time.Time
+ msg string
+ completeMsg *string
+}
+
+func (d *averageSpeed) Decor(st *Statistics) string {
+ if st.Completed {
+ if d.completeMsg != nil {
+ return d.FormatMsg(*d.completeMsg)
+ }
+ return d.FormatMsg(d.msg)
+ }
+
+ timeElapsed := time.Since(d.startTime)
+ speed := float64(st.Current) / timeElapsed.Seconds()
+
+ switch d.unit {
+ case UnitKiB:
+ d.msg = fmt.Sprintf(d.unitFormat, SpeedKiB(speed))
+ case UnitKB:
+ d.msg = fmt.Sprintf(d.unitFormat, SpeedKB(speed))
+ default:
+ d.msg = fmt.Sprintf(d.unitFormat, speed)
+ }
+
+ return d.FormatMsg(d.msg)
+}
+
+func (d *averageSpeed) OnCompleteMessage(msg string) {
+ d.completeMsg = &msg
+}
diff --git a/decor/speed_test.go b/decor/speed_test.go
new file mode 100644
index 0000000..a95252d
--- /dev/null
+++ b/decor/speed_test.go
@@ -0,0 +1,142 @@
+package decor
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestSpeedKiB(t *testing.T) {
+ cases := map[string]struct {
+ value int64
+ verb string
+ expected string
+ }{
+ "verb %f": {12345678, "%f", "11.773756MiB/s"},
+ "verb %.0f": {12345678, "%.0f", "12MiB/s"},
+ "verb %.1f": {12345678, "%.1f", "11.8MiB/s"},
+ "verb %.2f": {12345678, "%.2f", "11.77MiB/s"},
+ "verb %.3f": {12345678, "%.3f", "11.774MiB/s"},
+
+ "verb % f": {12345678, "% f", "11.773756 MiB/s"},
+ "verb % .0f": {12345678, "% .0f", "12 MiB/s"},
+ "verb % .1f": {12345678, "% .1f", "11.8 MiB/s"},
+ "verb % .2f": {12345678, "% .2f", "11.77 MiB/s"},
+ "verb % .3f": {12345678, "% .3f", "11.774 MiB/s"},
+
+ "verb %10.f": {12345678, "%10.f", " 12MiB/s"},
+ "verb %10.0f": {12345678, "%10.0f", " 12MiB/s"},
+ "verb %10.1f": {12345678, "%10.1f", " 11.8MiB/s"},
+ "verb %10.2f": {12345678, "%10.2f", "11.77MiB/s"},
+ "verb %10.3f": {12345678, "%10.3f", "11.774MiB/s"},
+
+ "verb % 10.f": {12345678, "% 10.f", " 12 MiB/s"},
+ "verb % 10.0f": {12345678, "% 10.0f", " 12 MiB/s"},
+ "verb % 10.1f": {12345678, "% 10.1f", "11.8 MiB/s"},
+
+ "verb %-10.f": {12345678, "%-10.f", "12MiB/s "},
+ "verb %-10.0f": {12345678, "%-10.0f", "12MiB/s "},
+ "verb %-10.1f": {12345678, "%-10.1f", "11.8MiB/s "},
+ "verb %-10.2f": {12345678, "%10.2f", "11.77MiB/s"},
+ "verb %-10.3f": {12345678, "%10.3f", "11.774MiB/s"},
+
+ "verb % -10.f": {12345678, "% -10.f", "12 MiB/s "},
+ "verb % -10.0f": {12345678, "% -10.0f", "12 MiB/s "},
+ "verb % -10.1f": {12345678, "% -10.1f", "11.8 MiB/s"},
+
+ "1000 %f": {1000, "%f", "1000b/s"},
+ "1000 %d": {1000, "%d", "1000b/s"},
+ "1000 %s": {1000, "%s", "1000b/s"},
+ "1024 %f": {1024, "%f", "1.000000KiB/s"},
+ "1024 %d": {1024, "%d", "1KiB/s"},
+ "1024 %.1f": {1024, "%.1f", "1.0KiB/s"},
+ "1024 %s": {1024, "%s", "1.0KiB/s"},
+ "3*MiB/s+140KiB/s %f": {3*MiB + 140*KiB, "%f", "3.136719MiB/s"},
+ "3*MiB/s+140KiB/s %d": {3*MiB + 140*KiB, "%d", "3MiB/s"},
+ "3*MiB/s+140KiB/s %.1f": {3*MiB + 140*KiB, "%.1f", "3.1MiB/s"},
+ "3*MiB/s+140KiB/s %s": {3*MiB + 140*KiB, "%s", "3.1MiB/s"},
+ "2*GiB/s %f": {2 * GiB, "%f", "2.000000GiB/s"},
+ "2*GiB/s %d": {2 * GiB, "%d", "2GiB/s"},
+ "2*GiB/s %.1f": {2 * GiB, "%.1f", "2.0GiB/s"},
+ "2*GiB/s %s": {2 * GiB, "%s", "2.0GiB/s"},
+ "4*TiB/s %f": {4 * TiB, "%f", "4.000000TiB/s"},
+ "4*TiB/s %d": {4 * TiB, "%d", "4TiB/s"},
+ "4*TiB/s %.1f": {4 * TiB, "%.1f", "4.0TiB/s"},
+ "4*TiB/s %s": {4 * TiB, "%s", "4.0TiB/s"},
+ }
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ got := fmt.Sprintf(tc.verb, SpeedKiB(tc.value))
+ if got != tc.expected {
+ t.Fatalf("expected: %q, got: %q\n", tc.expected, got)
+ }
+ })
+ }
+}
+
+func TestSpeedKB(t *testing.T) {
+ cases := map[string]struct {
+ value int64
+ verb string
+ expected string
+ }{
+ "verb %f": {12345678, "%f", "12.345678MB/s"},
+ "verb %.0f": {12345678, "%.0f", "12MB/s"},
+ "verb %.1f": {12345678, "%.1f", "12.3MB/s"},
+ "verb %.2f": {12345678, "%.2f", "12.35MB/s"},
+ "verb %.3f": {12345678, "%.3f", "12.346MB/s"},
+
+ "verb % f": {12345678, "% f", "12.345678 MB/s"},
+ "verb % .0f": {12345678, "% .0f", "12 MB/s"},
+ "verb % .1f": {12345678, "% .1f", "12.3 MB/s"},
+ "verb % .2f": {12345678, "% .2f", "12.35 MB/s"},
+ "verb % .3f": {12345678, "% .3f", "12.346 MB/s"},
+
+ "verb %10.f": {12345678, "%10.f", " 12MB/s"},
+ "verb %10.0f": {12345678, "%10.0f", " 12MB/s"},
+ "verb %10.1f": {12345678, "%10.1f", " 12.3MB/s"},
+ "verb %10.2f": {12345678, "%10.2f", " 12.35MB/s"},
+ "verb %10.3f": {12345678, "%10.3f", "12.346MB/s"},
+
+ "verb % 10.f": {12345678, "% 10.f", " 12 MB/s"},
+ "verb % 10.0f": {12345678, "% 10.0f", " 12 MB/s"},
+ "verb % 10.1f": {12345678, "% 10.1f", " 12.3 MB/s"},
+
+ "verb %-10.f": {12345678, "%-10.f", "12MB/s "},
+ "verb %-10.0f": {12345678, "%-10.0f", "12MB/s "},
+ "verb %-10.1f": {12345678, "%-10.1f", "12.3MB/s "},
+ "verb %-10.2f": {12345678, "%10.2f", " 12.35MB/s"},
+ "verb %-10.3f": {12345678, "%10.3f", "12.346MB/s"},
+
+ "verb % -10.f": {12345678, "% -10.f", "12 MB/s "},
+ "verb % -10.0f": {12345678, "% -10.0f", "12 MB/s "},
+ "verb % -10.1f": {12345678, "% -10.1f", "12.3 MB/s "},
+
+ "1000 %f": {1000, "%f", "1.000000kB/s"},
+ "1000 %d": {1000, "%d", "1kB/s"},
+ "1000 %s": {1000, "%s", "1.0kB/s"},
+ "1024 %f": {1024, "%f", "1.024000kB/s"},
+ "1024 %d": {1024, "%d", "1kB/s"},
+ "1024 %.1f": {1024, "%.1f", "1.0kB/s"},
+ "1024 %s": {1024, "%s", "1.0kB/s"},
+ "3*MB/s+140*KB/s %f": {3*MB + 140*KB, "%f", "3.140000MB/s"},
+ "3*MB/s+140*KB/s %d": {3*MB + 140*KB, "%d", "3MB/s"},
+ "3*MB/s+140*KB/s %.1f": {3*MB + 140*KB, "%.1f", "3.1MB/s"},
+ "3*MB/s+140*KB/s %s": {3*MB + 140*KB, "%s", "3.1MB/s"},
+ "2*GB/s %f": {2 * GB, "%f", "2.000000GB/s"},
+ "2*GB/s %d": {2 * GB, "%d", "2GB/s"},
+ "2*GB/s %.1f": {2 * GB, "%.1f", "2.0GB/s"},
+ "2*GB/s %s": {2 * GB, "%s", "2.0GB/s"},
+ "4*TB/s %f": {4 * TB, "%f", "4.000000TB/s"},
+ "4*TB/s %d": {4 * TB, "%d", "4TB/s"},
+ "4*TB/s %.1f": {4 * TB, "%.1f", "4.0TB/s"},
+ "4*TB/s %s": {4 * TB, "%s", "4.0TB/s"},
+ }
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ got := fmt.Sprintf(tc.verb, SpeedKB(tc.value))
+ if got != tc.expected {
+ t.Fatalf("expected: %q, got: %q\n", tc.expected, got)
+ }
+ })
+ }
+}
diff --git a/decorators_test.go b/decorators_test.go
new file mode 100644
index 0000000..a057197
--- /dev/null
+++ b/decorators_test.go
@@ -0,0 +1,219 @@
+package mpb_test
+
+import (
+ "sync"
+ "testing"
+
+ . "github.com/vbauerster/mpb"
+ "github.com/vbauerster/mpb/decor"
+)
+
+func TestNameDecorator(t *testing.T) {
+ tests := []struct {
+ decorator decor.Decorator
+ want string
+ }{
+ {
+ decorator: decor.Name("Test"),
+ want: "Test",
+ },
+ {
+ decorator: decor.Name("Test", decor.WC{W: len("Test")}),
+ want: "Test",
+ },
+ {
+ decorator: decor.Name("Test", decor.WC{W: 10}),
+ want: " Test",
+ },
+ {
+ decorator: decor.Name("Test", decor.WC{W: 10, C: decor.DidentRight}),
+ want: "Test ",
+ },
+ }
+
+ for _, test := range tests {
+ got := test.decorator.Decor(new(decor.Statistics))
+ if got != test.want {
+ t.Errorf("Want: %q, Got: %q\n", test.want, got)
+ }
+ }
+}
+
+type step struct {
+ stat *decor.Statistics
+ decorator decor.Decorator
+ want string
+}
+
+func TestPercentageDwidthSync(t *testing.T) {
+
+ testCases := [][]step{
+ []step{
+ {
+ &decor.Statistics{Total: 100, Current: 8},
+ decor.Percentage(decor.WCSyncWidth),
+ "8 %",
+ },
+ {
+ &decor.Statistics{Total: 100, Current: 9},
+ decor.Percentage(decor.WCSyncWidth),
+ "9 %",
+ },
+ },
+ []step{
+ {
+ &decor.Statistics{Total: 100, Current: 9},
+ decor.Percentage(decor.WCSyncWidth),
+ " 9 %",
+ },
+ {
+ &decor.Statistics{Total: 100, Current: 10},
+ decor.Percentage(decor.WCSyncWidth),
+ "10 %",
+ },
+ },
+ []step{
+ {
+ &decor.Statistics{Total: 100, Current: 9},
+ decor.Percentage(decor.WCSyncWidth),
+ " 9 %",
+ },
+ {
+ &decor.Statistics{Total: 100, Current: 100},
+ decor.Percentage(decor.WCSyncWidth),
+ "100 %",
+ },
+ },
+ }
+
+ testDecoratorConcurrently(t, testCases)
+}
+
+func TestPercentageDwidthSyncDidentRight(t *testing.T) {
+
+ testCases := [][]step{
+ []step{
+ {
+ &decor.Statistics{Total: 100, Current: 8},
+ decor.Percentage(decor.WCSyncWidthR),
+ "8 %",
+ },
+ {
+ &decor.Statistics{Total: 100, Current: 9},
+ decor.Percentage(decor.WCSyncWidthR),
+ "9 %",
+ },
+ },
+ []step{
+ {
+ &decor.Statistics{Total: 100, Current: 9},
+ decor.Percentage(decor.WCSyncWidthR),
+ "9 % ",
+ },
+ {
+ &decor.Statistics{Total: 100, Current: 10},
+ decor.Percentage(decor.WCSyncWidthR),
+ "10 %",
+ },
+ },
+ []step{
+ {
+ &decor.Statistics{Total: 100, Current: 9},
+ decor.Percentage(decor.WCSyncWidthR),
+ "9 % ",
+ },
+ {
+ &decor.Statistics{Total: 100, Current: 100},
+ decor.Percentage(decor.WCSyncWidthR),
+ "100 %",
+ },
+ },
+ }
+
+ testDecoratorConcurrently(t, testCases)
+}
+
+func TestPercentageDSyncSpace(t *testing.T) {
+
+ testCases := [][]step{
+ []step{
+ {
+ &decor.Statistics{Total: 100, Current: 8},
+ decor.Percentage(decor.WCSyncSpace),
+ " 8 %",
+ },
+ {
+ &decor.Statistics{Total: 100, Current: 9},
+ decor.Percentage(decor.WCSyncSpace),
+ " 9 %",
+ },
+ },
+ []step{
+ {
+ &decor.Statistics{Total: 100, Current: 9},
+ decor.Percentage(decor.WCSyncSpace),
+ " 9 %",
+ },
+ {
+ &decor.Statistics{Total: 100, Current: 10},
+ decor.Percentage(decor.WCSyncSpace),
+ " 10 %",
+ },
+ },
+ []step{
+ {
+ &decor.Statistics{Total: 100, Current: 9},
+ decor.Percentage(decor.WCSyncSpace),
+ " 9 %",
+ },
+ {
+ &decor.Statistics{Total: 100, Current: 100},
+ decor.Percentage(decor.WCSyncSpace),
+ " 100 %",
+ },
+ },
+ }
+
+ testDecoratorConcurrently(t, testCases)
+}
+
+func testDecoratorConcurrently(t *testing.T, testCases [][]step) {
+ if len(testCases) == 0 {
+ t.Fail()
+ }
+
+ numBars := len(testCases[0])
+ var wg sync.WaitGroup
+ for _, columnCase := range testCases {
+ wg.Add(numBars)
+ SyncWidth(toSyncMatrix(columnCase))
+ gott := make([]chan string, numBars)
+ for i := 0; i < numBars; i++ {
+ gott[i] = make(chan string, 1)
+ go func(s step, ch chan string) {
+ defer wg.Done()
+ ch <- s.decorator.Decor(s.stat)
+ }(columnCase[i], gott[i])
+ }
+ wg.Wait()
+
+ for i, ch := range gott {
+ got := <-ch
+ want := columnCase[i].want
+ if got != want {
+ t.Errorf("Want: %q, Got: %q\n", want, got)
+ }
+ }
+
+ }
+}
+
+func toSyncMatrix(ss []step) map[int][]chan int {
+ var column []chan int
+ for _, s := range ss {
+ if ok, ch := s.decorator.Syncable(); ok {
+ column = append(column, ch)
+ }
+ }
+ return map[int][]chan int{0: column}
+}
diff --git a/doc.go b/doc.go
new file mode 100644
index 0000000..1624595
--- /dev/null
+++ b/doc.go
@@ -0,0 +1,6 @@
+// Copyright (C) 2016-2018 Vladimir Bauer
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package mpb is a library for rendering progress bars in terminal applications.
+package mpb
diff --git a/draw_test.go b/draw_test.go
new file mode 100644
index 0000000..77bf654
--- /dev/null
+++ b/draw_test.go
@@ -0,0 +1,212 @@
+package mpb
+
+import (
+ "bytes"
+ "testing"
+)
+
+func TestDraw(t *testing.T) {
+ // key is termWidth
+ testSuite := map[int]map[string]struct {
+ total, current int64
+ barWidth int
+ barRefill *refill
+ want string
+ }{
+ 100: {
+ "t,c,bw{100,100,0}": {
+ total: 100,
+ current: 0,
+ barWidth: 100,
+ want: "[--------------------------------------------------------------------------------------------------]",
+ },
+ "t,c,bw{100,1,100}": {
+ total: 100,
+ current: 1,
+ barWidth: 100,
+ want: "[>-------------------------------------------------------------------------------------------------]",
+ },
+ "t,c,bw{100,40,100}": {
+ total: 100,
+ current: 40,
+ barWidth: 100,
+ want: "[======================================>-----------------------------------------------------------]",
+ },
+ "t,c,bw{100,40,100}refill{'+', 32}": {
+ total: 100,
+ current: 40,
+ barWidth: 100,
+ barRefill: &refill{'+', 32},
+ want: "[+++++++++++++++++++++++++++++++=======>-----------------------------------------------------------]",
+ },
+ "t,c,bw{100,99,100}": {
+ total: 100,
+ current: 99,
+ barWidth: 100,
+ want: "[================================================================================================>-]",
+ },
+ "t,c,bw{100,100,100}": {
+ total: 100,
+ current: 100,
+ barWidth: 100,
+ want: "[==================================================================================================]",
+ },
+ },
+ 2: {
+ "t,c,bw{0,0,100}": {
+ barWidth: 100,
+ want: "[]",
+ },
+ "t,c,bw{60,20,80}": {
+ total: 60,
+ current: 20,
+ barWidth: 80,
+ want: "[]",
+ },
+ },
+ 3: {
+ "t,c,bw{100,20,100}": {
+ total: 100,
+ current: 20,
+ barWidth: 100,
+ want: "[-]",
+ },
+ "t,c,bw{100,98,100}": {
+ total: 100,
+ current: 98,
+ barWidth: 100,
+ want: "[=]",
+ },
+ "t,c,bw{100,100,100}": {
+ total: 100,
+ current: 100,
+ barWidth: 100,
+ want: "[=]",
+ },
+ },
+ 5: {
+ "t,c,bw{100,20,100}": {
+ total: 100,
+ current: 20,
+ barWidth: 100,
+ want: "[>--]",
+ },
+ "t,c,bw{100,98,100}": {
+ total: 100,
+ current: 98,
+ barWidth: 100,
+ want: "[===]",
+ },
+ "t,c,bw{100,100,100}": {
+ total: 100,
+ current: 100,
+ barWidth: 100,
+ want: "[===]",
+ },
+ },
+ 6: {
+ "t,c,bw{100,20,100}": {
+ total: 100,
+ current: 20,
+ barWidth: 100,
+ want: "[>---]",
+ },
+ "t,c,bw{100,98,100}": {
+ total: 100,
+ current: 98,
+ barWidth: 100,
+ want: "[====]",
+ },
+ "t,c,bw{100,100,100}": {
+ total: 100,
+ current: 100,
+ barWidth: 100,
+ want: "[====]",
+ },
+ },
+ 20: {
+ "t,c,bw{100,20,100}": {
+ total: 100,
+ current: 20,
+ barWidth: 100,
+ want: "[===>--------------]",
+ },
+ "t,c,bw{100,60,100}": {
+ total: 100,
+ current: 60,
+ barWidth: 100,
+ want: "[==========>-------]",
+ },
+ "t,c,bw{100,98,100}": {
+ total: 100,
+ current: 98,
+ barWidth: 100,
+ want: "[==================]",
+ },
+ "t,c,bw{100,100,100}": {
+ total: 100,
+ current: 100,
+ barWidth: 100,
+ want: "[==================]",
+ },
+ },
+ 50: {
+ "t,c,bw{100,20,100}": {
+ total: 100,
+ current: 20,
+ barWidth: 100,
+ want: "[=========>--------------------------------------]",
+ },
+ "t,c,bw{100,60,100}": {
+ total: 100,
+ current: 60,
+ barWidth: 100,
+ want: "[============================>-------------------]",
+ },
+ "t,c,bw{100,98,100}": {
+ total: 100,
+ current: 98,
+ barWidth: 100,
+ want: "[==============================================>-]",
+ },
+ "t,c,bw{100,100,100}": {
+ total: 100,
+ current: 100,
+ barWidth: 100,
+ want: "[================================================]",
+ },
+ },
+ }
+
+ var tmpBuf bytes.Buffer
+ for termWidth, cases := range testSuite {
+ for name, tc := range cases {
+ s := newTestState()
+ s.width = tc.barWidth
+ s.total = tc.total
+ s.current = tc.current
+ if tc.barRefill != nil {
+ s.refill = tc.barRefill
+ }
+ tmpBuf.Reset()
+ tmpBuf.ReadFrom(s.draw(termWidth))
+ got := tmpBuf.String()
+ want := tc.want + "\n"
+ if got != want {
+ t.Errorf("termWidth %d; %s: want: %s %d, got: %s %d\n", termWidth, name, want, len(want), got, len(got))
+ }
+ }
+ }
+}
+
+func newTestState() *bState {
+ s := &bState{
+ trimLeftSpace: true,
+ trimRightSpace: true,
+ bufP: new(bytes.Buffer),
+ bufB: new(bytes.Buffer),
+ bufA: new(bytes.Buffer),
+ }
+ s.runes = strToBarRunes(pformat)
+ return s
+}
diff --git a/example_test.go b/example_test.go
new file mode 100644
index 0000000..97f1ded
--- /dev/null
+++ b/example_test.go
@@ -0,0 +1,84 @@
+package mpb_test
+
+import (
+ "io"
+ "io/ioutil"
+ "math/rand"
+ "net/http"
+ "time"
+
+ "github.com/vbauerster/mpb"
+ "github.com/vbauerster/mpb/decor"
+)
+
+func Example() {
+ p := mpb.New(
+ // override default (80) width
+ mpb.WithWidth(64),
+ // override default "[=>-]" format
+ mpb.WithFormat("╢▌▌░╟"),
+ // override default 120ms refresh rate
+ mpb.WithRefreshRate(180*time.Millisecond),
+ )
+
+ total := 100
+ name := "Single Bar:"
+ // adding a single bar
+ bar := p.AddBar(int64(total),
+ mpb.PrependDecorators(
+ // display our name with one space on the right
+ decor.Name(name, decor.WC{W: len(name) + 1, C: decor.DidentRight}),
+ // replace ETA decorator with "done" message, OnComplete event
+ decor.OnComplete(
+ // ETA decorator with ewma age of 60, and width reservation of 4
+ decor.EwmaETA(decor.ET_STYLE_GO, 60, decor.WC{W: 4}), "done",
+ ),
+ ),
+ mpb.AppendDecorators(decor.Percentage()),
+ )
+ // simulating some work
+ max := 100 * time.Millisecond
+ for i := 0; i < total; i++ {
+ start := time.Now()
+ time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10)
+ // ewma based decorators require work duration measurement
+ bar.IncrBy(1, time.Since(start))
+ }
+ // wait for our bar to complete and flush
+ p.Wait()
+}
+
+func ExampleBar_Completed() {
+ p := mpb.New()
+ bar := p.AddBar(100)
+
+ max := 100 * time.Millisecond
+ for !bar.Completed() {
+ time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10)
+ bar.Increment()
+ }
+
+ p.Wait()
+}
+
+func ExampleBar_ProxyReader() {
+ p := mpb.New()
+ // make http get request, ignoring errors
+ resp, _ := http.Get("https://homebrew.bintray.com/bottles/libtiff-4.0.7.sierra.bottle.tar.gz")
+ defer resp.Body.Close()
+
+ // Assuming ContentLength > 0
+ bar := p.AddBar(resp.ContentLength,
+ mpb.AppendDecorators(
+ decor.CountersKibiByte("%6.1f / %6.1f"),
+ ),
+ )
+
+ // create proxy reader
+ reader := bar.ProxyReader(resp.Body)
+
+ // and copy from reader, ignoring errors
+ io.Copy(ioutil.Discard, reader)
+
+ p.Wait()
+}
diff --git a/examples/barNewLineExtend/main.go b/examples/barNewLineExtend/main.go
new file mode 100644
index 0000000..08a6515
--- /dev/null
+++ b/examples/barNewLineExtend/main.go
@@ -0,0 +1,60 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "math/rand"
+ "sync"
+ "time"
+
+ "github.com/vbauerster/mpb"
+ "github.com/vbauerster/mpb/decor"
+)
+
+func init() {
+ rand.Seed(time.Now().UnixNano())
+}
+
+func main() {
+ var wg sync.WaitGroup
+ p := mpb.New(mpb.WithWaitGroup(&wg))
+ total, numBars := 100, 3
+ wg.Add(numBars)
+
+ for i := 0; i < numBars; i++ {
+ name := fmt.Sprintf("Bar#%d:", i)
+ efn := func(w io.Writer, s *decor.Statistics) {
+ if s.Completed {
+ fmt.Fprintf(w, "Bar id: %d has been completed\n", s.ID)
+ }
+ }
+ bar := p.AddBar(int64(total), mpb.BarNewLineExtend(efn),
+ mpb.PrependDecorators(
+ // simple name decorator
+ decor.Name(name),
+ // decor.DSyncWidth bit enables column width synchronization
+ decor.Percentage(decor.WCSyncSpace),
+ ),
+ mpb.AppendDecorators(
+ // replace ETA decorator with "done" message, OnComplete event
+ decor.OnComplete(
+ // ETA decorator with ewma age of 60
+ decor.EwmaETA(decor.ET_STYLE_GO, 60), "done",
+ ),
+ ),
+ )
+ // simulating some work
+ go func() {
+ defer wg.Done()
+ max := 100 * time.Millisecond
+ for i := 0; i < total; i++ {
+ start := time.Now()
+ time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10)
+ // ewma based decorators require work duration measurement
+ bar.IncrBy(1, time.Since(start))
+ }
+ }()
+ }
+ // wait for all bars to complete and flush
+ p.Wait()
+}
diff --git a/examples/cancel/main.go b/examples/cancel/main.go
new file mode 100644
index 0000000..001e201
--- /dev/null
+++ b/examples/cancel/main.go
@@ -0,0 +1,58 @@
+//+build go1.7
+
+package main
+
+import (
+ "context"
+ "fmt"
+ "math/rand"
+ "sync"
+ "time"
+
+ "github.com/vbauerster/mpb"
+ "github.com/vbauerster/mpb/decor"
+)
+
+func init() {
+ rand.Seed(time.Now().UnixNano())
+}
+
+func main() {
+ ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
+ defer cancel()
+
+ var wg sync.WaitGroup
+ p := mpb.New(
+ mpb.WithWaitGroup(&wg),
+ mpb.WithContext(ctx),
+ )
+ total := 300
+ numBars := 3
+ wg.Add(numBars)
+
+ for i := 0; i < numBars; i++ {
+ name := fmt.Sprintf("Bar#%d:", i)
+ bar := p.AddBar(int64(total),
+ mpb.PrependDecorators(
+ decor.Name(name),
+ decor.EwmaETA(decor.ET_STYLE_GO, 60, decor.WCSyncSpace),
+ ),
+ mpb.AppendDecorators(
+ decor.Percentage(decor.WC{W: 5}),
+ ),
+ )
+
+ go func() {
+ defer wg.Done()
+ max := 100 * time.Millisecond
+ for !bar.Completed() {
+ start := time.Now()
+ time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10)
+ // ewma based decorators require work duration measurement
+ bar.IncrBy(1, time.Since(start))
+ }
+ }()
+ }
+
+ p.Wait()
+}
diff --git a/examples/complex/main.go b/examples/complex/main.go
new file mode 100644
index 0000000..c610e10
--- /dev/null
+++ b/examples/complex/main.go
@@ -0,0 +1,79 @@
+package main
+
+import (
+ "fmt"
+ "math/rand"
+ "sync"
+ "time"
+
+ "github.com/vbauerster/mpb"
+ "github.com/vbauerster/mpb/decor"
+)
+
+func init() {
+ rand.Seed(time.Now().UnixNano())
+}
+
+func main() {
+ doneWg := new(sync.WaitGroup)
+ p := mpb.New(mpb.WithWidth(64), mpb.WithWaitGroup(doneWg))
+ numBars := 4
+
+ var bars []*mpb.Bar
+ var downloadWgg []*sync.WaitGroup
+ for i := 0; i < numBars; i++ {
+ wg := new(sync.WaitGroup)
+ wg.Add(1)
+ downloadWgg = append(downloadWgg, wg)
+ task := fmt.Sprintf("Task#%02d:", i)
+ job := "downloading"
+ b := p.AddBar(rand.Int63n(201)+100,
+ mpb.BarRemoveOnComplete(),
+ mpb.PrependDecorators(
+ decor.Name(task, decor.WC{W: len(task) + 1, C: decor.DidentRight}),
+ decor.Name(job, decor.WCSyncSpaceR),
+ decor.CountersNoUnit("%d / %d", decor.WCSyncWidth),
+ ),
+ mpb.AppendDecorators(decor.Percentage(decor.WC{W: 5})),
+ )
+ go newTask(wg, b, i+1)
+ bars = append(bars, b)
+ }
+
+ for i := 0; i < numBars; i++ {
+ doneWg.Add(1)
+ i := i
+ go func() {
+ task := fmt.Sprintf("Task#%02d:", i)
+ job := "installing"
+ // preparing delayed bars
+ b := p.AddBar(rand.Int63n(101)+100,
+ mpb.BarReplaceOnComplete(bars[i]),
+ mpb.BarClearOnComplete(),
+ mpb.PrependDecorators(
+ decor.Name(task, decor.WC{W: len(task) + 1, C: decor.DidentRight}),
+ decor.OnComplete(decor.Name(job, decor.WCSyncSpaceR), "done!"),
+ decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_MMSS, 60, decor.WCSyncWidth), "")),
+ mpb.AppendDecorators(
+ decor.OnComplete(decor.Percentage(decor.WC{W: 5}), ""),
+ ),
+ )
+ // waiting for download to complete, before starting install job
+ downloadWgg[i].Wait()
+ go newTask(doneWg, b, numBars-i)
+ }()
+ }
+
+ p.Wait()
+}
+
+func newTask(wg *sync.WaitGroup, b *mpb.Bar, incrBy int) {
+ defer wg.Done()
+ max := 100 * time.Millisecond
+ for !b.Completed() {
+ start := time.Now()
+ time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10)
+ // ewma based decorators require work duration measurement
+ b.IncrBy(incrBy, time.Since(start))
+ }
+}
diff --git a/examples/dynTotal/main.go b/examples/dynTotal/main.go
new file mode 100644
index 0000000..18313c9
--- /dev/null
+++ b/examples/dynTotal/main.go
@@ -0,0 +1,52 @@
+package main
+
+import (
+ "io"
+ "math/rand"
+ "time"
+
+ "github.com/vbauerster/mpb"
+ "github.com/vbauerster/mpb/decor"
+)
+
+func init() {
+ rand.Seed(time.Now().UnixNano())
+}
+
+func main() {
+ p := mpb.New(mpb.WithWidth(64))
+
+ var total int64
+ bar := p.AddBar(total,
+ mpb.PrependDecorators(decor.Counters(decor.UnitKiB, "% .1f / % .1f")),
+ mpb.AppendDecorators(decor.Percentage()),
+ )
+
+ maxSleep := 100 * time.Millisecond
+ read := makeStream(200)
+ for {
+ n, err := read()
+ total += int64(n)
+ time.Sleep(time.Duration(rand.Intn(10)+1) * maxSleep / 10)
+ bar.IncrBy(n)
+ if err == io.EOF {
+ // total is known, final=true
+ bar.SetTotal(total, true)
+ break
+ }
+ // total is unknown, final=false
+ bar.SetTotal(total+2048, false)
+ }
+
+ p.Wait()
+}
+
+func makeStream(limit int) func() (int, error) {
+ return func() (int, error) {
+ if limit <= 0 {
+ return 0, io.EOF
+ }
+ limit--
+ return rand.Intn(1024) + 1, nil
+ }
+}
diff --git a/examples/gifs/godEMrCZmJkHYH1X9dN4Nm0U7.svg b/examples/gifs/godEMrCZmJkHYH1X9dN4Nm0U7.svg
new file mode 100644
index 0000000..f794408
--- /dev/null
+++ b/examples/gifs/godEMrCZmJkHYH1X9dN4Nm0U7.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/gifs/hIpTa3A5rQz65ssiVuRJu87X6.svg b/examples/gifs/hIpTa3A5rQz65ssiVuRJu87X6.svg
new file mode 100644
index 0000000..163921c
--- /dev/null
+++ b/examples/gifs/hIpTa3A5rQz65ssiVuRJu87X6.svg
@@ -0,0 +1 @@
+~/go/src/github.com/vbauerster/mpb/examples/io/single❯~/go/src/github.com/vbauerster/mpb/examples/io/singlemaster*❯gorun-racemain.go❯gorun-racemain.go❯gorun-racemain.go40.6MiB/40.6MiB[==========================================================|00:00]7.54MiB/s~/go/src/github.com/vbauerster/mpb/examples/io/singlemaster*46s~/go/src/github.com/vbauerster/mpb/examples/io/singlemaster❯gitclean-fdx❯gorun-racemain.go❯gorun-racemain.go❯gorun-racemain.go16.6KiB/40.6MiB[----------------------------------------------------------|00:00]0b/s66.5KiB/40.6MiB[----------------------------------------------------------|00:00]0b/s134.5KiB/40.6MiB[----------------------------------------------------------|09:01]6.18MiB/s339.6KiB/40.6MiB[----------------------------------------------------------|08:18]6.41MiB/s577.6KiB/40.6MiB[>---------------------------------------------------------|07:32]7.05MiB/s866.6KiB/40.6MiB[>---------------------------------------------------------|06:43]7.36MiB/s1.5MiB/40.6MiB[=>--------------------------------------------------------|05:12]8.12MiB/s1.8MiB/40.6MiB[==>-------------------------------------------------------|04:28]8.48MiB/s2.4MiB/40.6MiB[==>-------------------------------------------------------|03:29]16.62MiB/s2.9MiB/40.6MiB[===>------------------------------------------------------|02:52]15.37MiB/s3.5MiB/40.6MiB[====>-----------------------------------------------------|02:17]14.22MiB/s3.9MiB/40.6MiB[=====>----------------------------------------------------|01:59]13.18MiB/s4.3MiB/40.6MiB[=====>----------------------------------------------------|01:40]12.42MiB/s4.9MiB/40.6MiB[======>---------------------------------------------------|01:19]18.55MiB/s5.2MiB/40.6MiB[======>---------------------------------------------------|01:11]17.39MiB/s5.5MiB/40.6MiB[=======>--------------------------------------------------|01:03]16.23MiB/s5.6MiB/40.6MiB[=======>--------------------------------------------------|01:02]16.04MiB/s6.2MiB/40.6MiB[========>-------------------------------------------------|00:50]17.91MiB/s6.5MiB/40.6MiB[========>-------------------------------------------------|00:47]16.80MiB/s6.7MiB/40.6MiB[=========>------------------------------------------------|00:45]15.85MiB/s7.0MiB/40.6MiB[=========>------------------------------------------------|00:41]14.66MiB/s7.3MiB/40.6MiB[=========>------------------------------------------------|00:38]17.03MiB/s7.5MiB/40.6MiB[==========>-----------------------------------------------|00:39]16.26MiB/s7.7MiB/40.6MiB[==========>-----------------------------------------------|00:38]15.16MiB/s8.0MiB/40.6MiB[==========>-----------------------------------------------|00:36]14.16MiB/s8.1MiB/40.6MiB[===========>----------------------------------------------|00:35]13.56MiB/s8.4MiB/40.6MiB[===========>----------------------------------------------|00:36]13.08MiB/s8.6MiB/40.6MiB[===========>----------------------------------------------|00:34]12.51MiB/s8.8MiB/40.6MiB[============>---------------------------------------------|00:34]12.05MiB/s9.0MiB/40.6MiB[============>---------------------------------------------|00:35]11.56MiB/s9.3MiB/40.6MiB[============>---------------------------------------------|00:32]11.12MiB/s9.5MiB/40.6MiB[=============>--------------------------------------------|00:31]10.75MiB/s9.7MiB/40.6MiB[=============>--------------------------------------------|00:30]10.23MiB/s10.0MiB/40.6MiB[=============>--------------------------------------------|00:29]9.89MiB/s10.2MiB/40.6MiB[==============>-------------------------------------------|00:30]9.54MiB/s10.4MiB/40.6MiB[==============>-------------------------------------------|00:28]9.34MiB/s10.7MiB/40.6MiB[==============>-------------------------------------------|00:27]9.04MiB/s10.9MiB/40.6MiB[===============>------------------------------------------|00:26]8.79MiB/s11.0MiB/40.6MiB[===============>------------------------------------------|00:26]8.75MiB/s11.3MiB/40.6MiB[===============>------------------------------------------|00:25]11.13MiB/s11.5MiB/40.6MiB[===============>------------------------------------------|00:24]10.81MiB/s11.7MiB/40.6MiB[================>-----------------------------------------|00:24]10.45MiB/s11.9MiB/40.6MiB[================>-----------------------------------------|00:25]10.00MiB/s12.1MiB/40.6MiB[================>-----------------------------------------|00:25]9.63MiB/s12.3MiB/40.6MiB[=================>----------------------------------------|00:24]9.26MiB/s12.4MiB/40.6MiB[=================>----------------------------------------|00:24]9.14MiB/s12.6MiB/40.6MiB[=================>----------------------------------------|00:23]8.79MiB/s12.8MiB/40.6MiB[=================>----------------------------------------|00:23]10.07MiB/s12.9MiB/40.6MiB[=================>----------------------------------------|00:23]9.96MiB/s13.1MiB/40.6MiB[==================>---------------------------------------|00:22]9.80MiB/s13.2MiB/40.6MiB[==================>---------------------------------------|00:22]9.71MiB/s13.4MiB/40.6MiB[==================>---------------------------------------|00:22]9.61MiB/s13.6MiB/40.6MiB[==================>---------------------------------------|00:25]9.36MiB/s13.7MiB/40.6MiB[===================>--------------------------------------|00:24]9.19MiB/s13.9MiB/40.6MiB[===================>--------------------------------------|00:24]8.93MiB/s14.0MiB/40.6MiB[===================>--------------------------------------|00:24]8.65MiB/s14.2MiB/40.6MiB[===================>--------------------------------------|00:24]8.48MiB/s14.4MiB/40.6MiB[====================>-------------------------------------|00:23]8.32MiB/s14.5MiB/40.6MiB[====================>-------------------------------------|00:23]8.20MiB/s14.7MiB/40.6MiB[====================>-------------------------------------|00:22]8.12MiB/s14.8MiB/40.6MiB[====================>-------------------------------------|00:22]7.97MiB/s15.0MiB/40.6MiB[====================>-------------------------------------|00:22]7.88MiB/s15.2MiB/40.6MiB[=====================>------------------------------------|00:21]7.74MiB/s15.3MiB/40.6MiB[=====================>------------------------------------|00:21]7.73MiB/s15.4MiB/40.6MiB[=====================>------------------------------------|00:20]9.07MiB/s15.6MiB/40.6MiB[=====================>------------------------------------|00:20]8.84MiB/s15.7MiB/40.6MiB[=====================>------------------------------------|00:20]8.72MiB/s15.9MiB/40.6MiB[======================>-----------------------------------|00:20]8.45MiB/s16.0MiB/40.6MiB[======================>-----------------------------------|00:20]8.27MiB/s16.1MiB/40.6MiB[======================>-----------------------------------|00:22]8.05MiB/s16.3MiB/40.6MiB[======================>-----------------------------------|00:21]7.88MiB/s16.4MiB/40.6MiB[======================>-----------------------------------|00:21]7.69MiB/s16.6MiB/40.6MiB[=======================>----------------------------------|00:21]7.60MiB/s16.7MiB/40.6MiB[=======================>----------------------------------|00:22]7.49MiB/s16.9MiB/40.6MiB[=======================>----------------------------------|00:22]7.41MiB/s17.0MiB/40.6MiB[=======================>----------------------------------|00:21]7.43MiB/s17.2MiB/40.6MiB[========================>---------------------------------|00:21]7.38MiB/s17.4MiB/40.6MiB[========================>---------------------------------|00:23]7.23MiB/s17.5MiB/40.6MiB[========================>---------------------------------|00:23]7.15MiB/s17.7MiB/40.6MiB[========================>---------------------------------|00:24]6.99MiB/s17.9MiB/40.6MiB[=========================>--------------------------------|00:25]6.90MiB/s18.0MiB/40.6MiB[=========================>--------------------------------|00:24]6.83MiB/s18.2MiB/40.6MiB[=========================>--------------------------------|00:23]8.15MiB/s18.3MiB/40.6MiB[=========================>--------------------------------|00:24]7.99MiB/s18.4MiB/40.6MiB[=========================>--------------------------------|00:24]7.88MiB/s18.6MiB/40.6MiB[==========================>-------------------------------|00:24]7.62MiB/s18.7MiB/40.6MiB[==========================>-------------------------------|00:23]7.58MiB/s18.8MiB/40.6MiB[==========================>-------------------------------|00:23]7.47MiB/s19.0MiB/40.6MiB[==========================>-------------------------------|00:22]7.39MiB/s19.1MiB/40.6MiB[==========================>-------------------------------|00:22]7.33MiB/s19.2MiB/40.6MiB[===========================>------------------------------|00:22]8.48MiB/s19.4MiB/40.6MiB[===========================>------------------------------|00:25]8.27MiB/s19.5MiB/40.6MiB[===========================>------------------------------|00:25]8.13MiB/s19.6MiB/40.6MiB[===========================>------------------------------|00:25]7.89MiB/s19.7MiB/40.6MiB[===========================>------------------------------|00:25]7.67MiB/s19.8MiB/40.6MiB[===========================>------------------------------|00:24]7.52MiB/s20.0MiB/40.6MiB[============================>-----------------------------|00:24]7.42MiB/s20.1MiB/40.6MiB[============================>-----------------------------|00:24]7.25MiB/s20.2MiB/40.6MiB[============================>-----------------------------|00:23]7.12MiB/s20.3MiB/40.6MiB[============================>-----------------------------|00:23]6.94MiB/s20.5MiB/40.6MiB[============================>-----------------------------|00:24]6.75MiB/s20.6MiB/40.6MiB[============================>-----------------------------|00:24]6.59MiB/s20.7MiB/40.6MiB[=============================>----------------------------|00:27]6.57MiB/s20.8MiB/40.6MiB[=============================>----------------------------|00:27]6.45MiB/s21.0MiB/40.6MiB[=============================>----------------------------|00:28]6.40MiB/s21.1MiB/40.6MiB[=============================>----------------------------|00:30]6.36MiB/s21.2MiB/40.6MiB[=============================>----------------------------|00:29]6.28MiB/s21.3MiB/40.6MiB[=============================>----------------------------|00:28]6.22MiB/s21.4MiB/40.6MiB[==============================>---------------------------|00:27]6.19MiB/s21.5MiB/40.6MiB[==============================>---------------------------|00:29]6.10MiB/s21.7MiB/40.6MiB[==============================>---------------------------|00:28]6.09MiB/s21.8MiB/40.6MiB[==============================>---------------------------|00:27]6.04MiB/s21.9MiB/40.6MiB[==============================>---------------------------|00:27]5.88MiB/s22.0MiB/40.6MiB[===============================>--------------------------|00:27]5.83MiB/s22.2MiB/40.6MiB[===============================>--------------------------|00:26]5.78MiB/s22.3MiB/40.6MiB[===============================>--------------------------|00:25]5.80MiB/s22.4MiB/40.6MiB[===============================>--------------------------|00:24]5.76MiB/s22.5MiB/40.6MiB[===============================>--------------------------|00:24]5.72MiB/s22.7MiB/40.6MiB[===============================>--------------------------|00:23]5.70MiB/s22.8MiB/40.6MiB[================================>-------------------------|00:24]5.60MiB/s22.8MiB/40.6MiB[================================>-------------------------|00:24]5.58MiB/s23.0MiB/40.6MiB[================================>-------------------------|00:25]6.35MiB/s23.1MiB/40.6MiB[================================>-------------------------|00:25]6.33MiB/s23.2MiB/40.6MiB[================================>-------------------------|00:24]6.33MiB/s23.4MiB/40.6MiB[================================>-------------------------|00:24]6.31MiB/s23.5MiB/40.6MiB[=================================>------------------------|00:23]6.28MiB/s23.6MiB/40.6MiB[=================================>------------------------|00:23]6.20MiB/s23.7MiB/40.6MiB[=================================>------------------------|00:22]6.26MiB/s23.8MiB/40.6MiB[=================================>------------------------|00:22]6.26MiB/s23.9MiB/40.6MiB[=================================>------------------------|00:21]6.31MiB/s24.0MiB/40.6MiB[=================================>------------------------|00:21]6.26MiB/s24.2MiB/40.6MiB[==================================>-----------------------|00:22]6.25MiB/s24.3MiB/40.6MiB[==================================>-----------------------|00:25]6.20MiB/s24.4MiB/40.6MiB[==================================>-----------------------|00:24]6.20MiB/s24.5MiB/40.6MiB[==================================>-----------------------|00:23]6.14MiB/s24.7MiB/40.6MiB[==================================>-----------------------|00:24]6.16MiB/s24.8MiB/40.6MiB[==================================>-----------------------|00:24]6.13MiB/s24.9MiB/40.6MiB[===================================>----------------------|00:23]6.14MiB/s25.0MiB/40.6MiB[===================================>----------------------|00:23]6.17MiB/s25.1MiB/40.6MiB[===================================>----------------------|00:23]6.16MiB/s25.3MiB/40.6MiB[===================================>----------------------|00:22]6.05MiB/s25.4MiB/40.6MiB[===================================>----------------------|00:22]6.14MiB/s25.5MiB/40.6MiB[===================================>----------------------|00:21]6.06MiB/s25.6MiB/40.6MiB[====================================>---------------------|00:22]6.04MiB/s25.8MiB/40.6MiB[====================================>---------------------|00:21]6.02MiB/s25.9MiB/40.6MiB[====================================>---------------------|00:22]5.96MiB/s26.0MiB/40.6MiB[====================================>---------------------|00:24]6.05MiB/s26.1MiB/40.6MiB[====================================>---------------------|00:23]6.03MiB/s26.2MiB/40.6MiB[=====================================>--------------------|00:22]7.18MiB/s26.3MiB/40.6MiB[=====================================>--------------------|00:22]7.10MiB/s26.4MiB/40.6MiB[=====================================>--------------------|00:21]7.09MiB/s26.5MiB/40.6MiB[=====================================>--------------------|00:21]7.09MiB/s26.7MiB/40.6MiB[=====================================>--------------------|00:21]7.07MiB/s26.8MiB/40.6MiB[=====================================>--------------------|00:20]7.02MiB/s26.8MiB/40.6MiB[=====================================>--------------------|00:20]6.92MiB/s27.0MiB/40.6MiB[======================================>-------------------|00:19]6.89MiB/s27.1MiB/40.6MiB[======================================>-------------------|00:19]6.99MiB/s27.2MiB/40.6MiB[======================================>-------------------|00:18]6.98MiB/s27.3MiB/40.6MiB[======================================>-------------------|00:18]6.99MiB/s27.5MiB/40.6MiB[======================================>-------------------|00:20]6.98MiB/s27.5MiB/40.6MiB[======================================>-------------------|00:20]6.94MiB/s27.7MiB/40.6MiB[=======================================>------------------|00:20]7.98MiB/s27.8MiB/40.6MiB[=======================================>------------------|00:19]7.88MiB/s27.8MiB/40.6MiB[=======================================>------------------|00:19]7.81MiB/s28.0MiB/40.6MiB[=======================================>------------------|00:18]7.71MiB/s28.0MiB/40.6MiB[=======================================>------------------|00:18]7.58MiB/s28.1MiB/40.6MiB[=======================================>------------------|00:18]7.44MiB/s28.2MiB/40.6MiB[=======================================>------------------|00:18]7.38MiB/s28.3MiB/40.6MiB[========================================>-----------------|00:17]7.33MiB/s28.4MiB/40.6MiB[========================================>-----------------|00:17]7.19MiB/s28.5MiB/40.6MiB[========================================>-----------------|00:18]7.17MiB/s28.6MiB/40.6MiB[========================================>-----------------|00:17]7.17MiB/s28.7MiB/40.6MiB[========================================>-----------------|00:20]7.11MiB/s28.8MiB/40.6MiB[========================================>-----------------|00:19]7.03MiB/s28.9MiB/40.6MiB[========================================>-----------------|00:20]6.89MiB/s29.0MiB/40.6MiB[=========================================>----------------|00:20]6.79MiB/s29.1MiB/40.6MiB[=========================================>----------------|00:19]6.72MiB/s29.2MiB/40.6MiB[=========================================>----------------|00:21]6.60MiB/s29.4MiB/40.6MiB[=========================================>----------------|00:20]6.50MiB/s29.4MiB/40.6MiB[=========================================>----------------|00:20]6.48MiB/s29.5MiB/40.6MiB[=========================================>----------------|00:20]6.44MiB/s29.7MiB/40.6MiB[=========================================>----------------|00:19]6.37MiB/s29.7MiB/40.6MiB[==========================================>---------------|00:19]6.31MiB/s29.8MiB/40.6MiB[==========================================>---------------|00:21]6.31MiB/s29.9MiB/40.6MiB[==========================================>---------------|00:20]6.34MiB/s30.0MiB/40.6MiB[==========================================>---------------|00:20]6.32MiB/s30.2MiB/40.6MiB[==========================================>---------------|00:19]6.22MiB/s30.2MiB/40.6MiB[==========================================>---------------|00:19]6.18MiB/s30.3MiB/40.6MiB[==========================================>---------------|00:19]6.16MiB/s30.5MiB/40.6MiB[===========================================>--------------|00:19]6.21MiB/s30.6MiB/40.6MiB[===========================================>--------------|00:18]6.19MiB/s30.7MiB/40.6MiB[===========================================>--------------|00:17]6.18MiB/s30.8MiB/40.6MiB[===========================================>--------------|00:17]6.19MiB/s30.9MiB/40.6MiB[===========================================>--------------|00:17]5.99MiB/s31.0MiB/40.6MiB[===========================================>--------------|00:17]5.89MiB/s31.1MiB/40.6MiB[============================================>-------------|00:16]5.92MiB/s31.3MiB/40.6MiB[============================================>-------------|00:15]5.80MiB/s31.4MiB/40.6MiB[============================================>-------------|00:15]5.85MiB/s31.5MiB/40.6MiB[============================================>-------------|00:14]5.87MiB/s31.7MiB/40.6MiB[============================================>-------------|00:13]5.93MiB/s31.9MiB/40.6MiB[=============================================>------------|00:13]5.95MiB/s32.0MiB/40.6MiB[=============================================>------------|00:12]5.98MiB/s32.2MiB/40.6MiB[=============================================>------------|00:11]5.99MiB/s32.4MiB/40.6MiB[=============================================>------------|00:11]6.04MiB/s32.5MiB/40.6MiB[=============================================>------------|00:10]6.08MiB/s32.7MiB/40.6MiB[==============================================>-----------|00:10]6.03MiB/s32.9MiB/40.6MiB[==============================================>-----------|00:09]6.05MiB/s33.1MiB/40.6MiB[==============================================>-----------|00:09]6.08MiB/s33.3MiB/40.6MiB[===============================================>----------|00:08]6.13MiB/s33.6MiB/40.6MiB[===============================================>----------|00:07]6.30MiB/s33.8MiB/40.6MiB[===============================================>----------|00:07]6.33MiB/s34.0MiB/40.6MiB[================================================>---------|00:07]6.44MiB/s34.3MiB/40.6MiB[================================================>---------|00:06]6.88MiB/s34.6MiB/40.6MiB[================================================>---------|00:05]6.88MiB/s34.8MiB/40.6MiB[=================================================>--------|00:05]6.88MiB/s35.2MiB/40.6MiB[=================================================>--------|00:04]10.50MiB/s35.4MiB/40.6MiB[==================================================>-------|00:04]10.17MiB/s35.6MiB/40.6MiB[==================================================>-------|00:03]9.88MiB/s35.9MiB/40.6MiB[==================================================>-------|00:03]9.36MiB/s36.2MiB/40.6MiB[===================================================>------|00:03]9.27MiB/s36.4MiB/40.6MiB[===================================================>------|00:02]9.08MiB/s36.7MiB/40.6MiB[===================================================>------|00:02]8.83MiB/s37.0MiB/40.6MiB[====================================================>-----|00:02]8.64MiB/s37.2MiB/40.6MiB[====================================================>-----|00:02]8.48MiB/s37.5MiB/40.6MiB[=====================================================>----|00:02]8.26MiB/s37.9MiB/40.6MiB[=====================================================>----|00:02]8.16MiB/s38.1MiB/40.6MiB[=====================================================>----|00:01]8.21MiB/s38.4MiB/40.6MiB[======================================================>---|00:01]8.38MiB/s38.7MiB/40.6MiB[======================================================>---|00:01]8.31MiB/s39.0MiB/40.6MiB[=======================================================>--|00:01]8.40MiB/s39.3MiB/40.6MiB[=======================================================>--|00:01]8.21MiB/s39.6MiB/40.6MiB[========================================================>-|00:00]8.14MiB/s40.0MiB/40.6MiB[========================================================>-|00:00]7.97MiB/s40.2MiB/40.6MiB[==========================================================|00:00]7.75MiB/s
\ No newline at end of file
diff --git a/examples/gifs/wHzf1M7sd7B3zVa2scBMnjqRf.svg b/examples/gifs/wHzf1M7sd7B3zVa2scBMnjqRf.svg
new file mode 100644
index 0000000..9e3b871
--- /dev/null
+++ b/examples/gifs/wHzf1M7sd7B3zVa2scBMnjqRf.svg
@@ -0,0 +1 @@
+~/go/src/github.com/vbauerster/mpb/examples/complex❯~/go/src/github.com/vbauerster/mpb/examples/complexmaster*❯gorun-racemain.go❯gorun-racemain.go❯gorun-racemain.goTask#03:installing00:08[======>-------------------------------------------------------]11%Task#03:installing00:07[==============>-----------------------------------------------]24%Task#02:done!Task#03:done!Task#01:done!Task#00:done!~/go/src/github.com/vbauerster/mpb/examples/complexmaster*19s~/go/src/github.com/vbauerster/mpb/examples/complexmaster❯gorun-racemain.go❯gorun-racemain.go❯gorun-racemain.go❯gorun-racemain.goTask#00:downloading4/268[>-------------------------------------------------------------]1%Task#01:downloading2/274[--------------------------------------------------------------]1%Task#02:downloading3/114[=>------------------------------------------------------------]3%Task#03:downloading4/114[=>------------------------------------------------------------]4%Task#00:downloading6/268[>-------------------------------------------------------------]2%Task#01:downloading8/274[=>------------------------------------------------------------]3%Task#02:downloading9/114[====>---------------------------------------------------------]8%Task#03:downloading12/114[======>-------------------------------------------------------]11%Task#00:downloading9/268[=>------------------------------------------------------------]3%Task#01:downloading12/274[==>-----------------------------------------------------------]4%Task#02:downloading12/114[======>-------------------------------------------------------]11%Task#03:downloading24/114[============>-------------------------------------------------]21%Task#00:downloading11/268[==>-----------------------------------------------------------]4%Task#01:downloading16/274[===>----------------------------------------------------------]6%Task#02:downloading18/114[=========>----------------------------------------------------]16%Task#03:downloading36/114[===================>------------------------------------------]32%Task#00:downloading13/268[==>-----------------------------------------------------------]5%Task#01:downloading22/274[====>---------------------------------------------------------]8%Task#02:downloading27/114[==============>-----------------------------------------------]24%Task#03:downloading40/114[=====================>----------------------------------------]35%Task#00:downloading15/268[==>-----------------------------------------------------------]6%Task#01:downloading24/274[====>---------------------------------------------------------]9%Task#02:downloading30/114[===============>----------------------------------------------]26%Task#03:downloading48/114[=========================>------------------------------------]42%Task#00:downloading16/268[===>----------------------------------------------------------]6%Task#01:downloading30/274[======>-------------------------------------------------------]11%Task#02:downloading39/114[====================>-----------------------------------------]34%Task#03:downloading56/114[=============================>--------------------------------]49%Task#00:downloading18/268[===>----------------------------------------------------------]7%Task#01:downloading34/274[=======>------------------------------------------------------]12%Task#02:downloading42/114[======================>---------------------------------------]37%Task#03:downloading64/114[==================================>---------------------------]56%Task#00:downloading19/268[===>----------------------------------------------------------]7%Task#01:downloading40/274[========>-----------------------------------------------------]15%Task#02:downloading45/114[=======================>--------------------------------------]39%Task#03:downloading68/114[====================================>-------------------------]60%Task#00:downloading21/268[====>---------------------------------------------------------]8%Task#01:downloading44/274[=========>----------------------------------------------------]16%Task#02:downloading54/114[============================>---------------------------------]47%Task#03:downloading76/114[========================================>---------------------]67%Task#00:downloading25/268[=====>--------------------------------------------------------]9%Task#01:downloading52/274[===========>--------------------------------------------------]19%Task#02:downloading60/114[================================>-----------------------------]53%Task#03:downloading80/114[===========================================>------------------]70%Task#00:downloading27/268[=====>--------------------------------------------------------]10%Task#01:downloading54/274[===========>--------------------------------------------------]20%Task#02:downloading63/114[=================================>----------------------------]55%Task#03:downloading88/114[===============================================>--------------]77%Task#00:downloading29/268[======>-------------------------------------------------------]11%Task#01:downloading58/274[============>-------------------------------------------------]21%Task#02:downloading69/114[=====================================>------------------------]61%Task#03:downloading92/114[=================================================>------------]81%Task#00:downloading30/268[======>-------------------------------------------------------]11%Task#01:downloading60/274[=============>------------------------------------------------]22%Task#02:downloading75/114[========================================>---------------------]66%Task#03:downloading100/114[=====================================================>--------]88%Task#00:downloading32/268[======>-------------------------------------------------------]12%Task#01:downloading66/274[==============>-----------------------------------------------]24%Task#02:downloading78/114[=========================================>--------------------]68%Task#03:downloading108/114[==========================================================>---]95%Task#00:downloading34/268[=======>------------------------------------------------------]13%Task#01:downloading70/274[===============>----------------------------------------------]26%Task#02:downloading84/114[=============================================>----------------]74%Task#03:downloading114/114[==============================================================]100%Task#00:downloading35/268[=======>------------------------------------------------------]13%Task#01:downloading74/274[================>---------------------------------------------]27%Task#02:downloading90/114[================================================>-------------]79%Task#03:installing00:00[--------------------------------------------------------------]1%Task#00:downloading37/268[========>-----------------------------------------------------]14%Task#01:downloading76/274[================>---------------------------------------------]28%Task#02:downloading96/114[===================================================>----------]84%Task#03:installing00:00[=>------------------------------------------------------------]2%Task#00:downloading40/268[========>-----------------------------------------------------]15%Task#01:downloading78/274[=================>--------------------------------------------]28%Task#02:downloading102/114[======================================================>-------]89%Task#03:installing00:00[=>------------------------------------------------------------]4%Task#00:downloading42/268[=========>----------------------------------------------------]16%Task#01:downloading82/274[==================>-------------------------------------------]30%Task#02:downloading108/114[==========================================================>---]95%Task#03:installing00:00[==>-----------------------------------------------------------]4%Task#00:downloading44/268[=========>----------------------------------------------------]16%Task#01:downloading86/274[==================>-------------------------------------------]31%Task#02:downloading114/114[==============================================================]100%Task#03:installing00:00[==>-----------------------------------------------------------]6%Task#00:downloading47/268[==========>---------------------------------------------------]18%Task#01:downloading88/274[===================>------------------------------------------]32%Task#02:installing00:00[=>------------------------------------------------------------]3%Task#03:installing00:09[====>---------------------------------------------------------]7%Task#00:downloading50/268[===========>--------------------------------------------------]19%Task#01:downloading92/274[====================>-----------------------------------------]34%Task#02:installing00:00[===>----------------------------------------------------------]6%Task#03:installing00:09[====>---------------------------------------------------------]9%Task#00:downloading52/268[===========>--------------------------------------------------]19%Task#01:downloading96/274[=====================>----------------------------------------]35%Task#02:installing00:00[=====>--------------------------------------------------------]9%Task#00:downloading54/268[===========>--------------------------------------------------]20%Task#01:downloading98/274[=====================>----------------------------------------]36%Task#02:installing00:03[========>-----------------------------------------------------]14%Task#00:downloading56/268[============>-------------------------------------------------]21%Task#01:downloading102/274[======================>---------------------------------------]37%Task#02:installing00:03[=========>----------------------------------------------------]17%Task#03:installing00:08[=======>------------------------------------------------------]12%Task#00:downloading57/268[============>-------------------------------------------------]21%Task#01:downloading106/274[=======================>--------------------------------------]39%Task#02:installing00:03[===========>--------------------------------------------------]19%Task#03:installing00:08[=======>------------------------------------------------------]14%Task#00:downloading59/268[=============>------------------------------------------------]22%Task#01:downloading114/274[=========================>------------------------------------]42%Task#02:installing00:03[=============>------------------------------------------------]23%Task#03:installing00:08[========>-----------------------------------------------------]14%Task#00:downloading61/268[=============>------------------------------------------------]23%Task#01:downloading120/274[==========================>-----------------------------------]44%Task#02:installing00:03[===============>----------------------------------------------]25%Task#03:installing00:08[=========>----------------------------------------------------]16%Task#00:downloading63/268[==============>-----------------------------------------------]24%Task#01:downloading126/274[============================>---------------------------------]46%Task#02:installing00:02[=================>--------------------------------------------]29%Task#03:installing00:08[=========>----------------------------------------------------]17%Task#00:downloading67/268[===============>----------------------------------------------]25%Task#01:downloading130/274[============================>---------------------------------]47%Task#02:installing00:02[==================>-------------------------------------------]31%Task#03:installing00:08[==========>---------------------------------------------------]17%Task#00:downloading68/268[===============>----------------------------------------------]25%Task#01:downloading132/274[=============================>--------------------------------]48%Task#02:installing00:02[=====================>----------------------------------------]36%Task#03:installing00:08[===========>--------------------------------------------------]19%Task#00:downloading69/268[===============>----------------------------------------------]26%Task#01:downloading136/274[==============================>-------------------------------]50%Task#02:installing00:02[======================>---------------------------------------]37%Task#03:installing00:08[============>-------------------------------------------------]20%Task#00:downloading71/268[===============>----------------------------------------------]26%Task#01:downloading140/274[===============================>------------------------------]51%Task#02:installing00:02[========================>-------------------------------------]41%Task#03:installing00:07[============>-------------------------------------------------]22%Task#00:downloading73/268[================>---------------------------------------------]27%Task#01:downloading144/274[================================>-----------------------------]53%Task#02:installing00:02[=========================>------------------------------------]42%Task#00:downloading75/268[================>---------------------------------------------]28%Task#01:downloading150/274[=================================>----------------------------]55%Task#02:installing00:02[===========================>----------------------------------]45%Task#00:downloading78/268[=================>--------------------------------------------]29%Task#01:downloading154/274[==================================>---------------------------]56%Task#02:installing00:02[============================>---------------------------------]47%Task#03:installing00:07[===============>----------------------------------------------]26%Task#00:downloading80/268[==================>-------------------------------------------]30%Task#01:downloading158/274[===================================>--------------------------]58%Task#02:installing00:02[==============================>-------------------------------]50%Task#03:installing00:07[================>---------------------------------------------]27%Task#00:downloading82/268[==================>-------------------------------------------]31%Task#01:downloading162/274[====================================>-------------------------]59%Task#02:installing00:02[===============================>------------------------------]51%Task#03:installing00:06[=================>--------------------------------------------]29%Task#00:downloading87/268[===================>------------------------------------------]32%Task#01:downloading164/274[====================================>-------------------------]60%Task#02:installing00:01[==================================>---------------------------]56%Task#03:installing00:06[==================>-------------------------------------------]31%Task#00:downloading90/268[====================>-----------------------------------------]34%Task#01:downloading166/274[=====================================>------------------------]61%Task#02:installing00:01[====================================>-------------------------]60%Task#03:installing00:06[===================>------------------------------------------]32%Task#00:downloading92/268[====================>-----------------------------------------]34%Task#01:downloading172/274[======================================>-----------------------]63%Task#02:installing00:01[======================================>-----------------------]62%Task#03:installing00:05[====================>-----------------------------------------]34%Task#00:downloading93/268[=====================>----------------------------------------]35%Task#01:downloading176/274[=======================================>----------------------]64%Task#02:installing00:01[=======================================>----------------------]65%Task#03:installing00:05[=====================>----------------------------------------]35%Task#00:downloading96/268[=====================>----------------------------------------]36%Task#01:downloading178/274[=======================================>----------------------]65%Task#02:installing00:01[==========================================>-------------------]69%Task#03:installing00:05[=====================>----------------------------------------]36%Task#00:downloading99/268[======================>---------------------------------------]37%Task#01:downloading182/274[========================================>---------------------]66%Task#02:installing00:01[===========================================>------------------]71%Task#03:installing00:05[======================>---------------------------------------]37%Task#00:downloading102/268[=======================>--------------------------------------]38%Task#01:downloading186/274[=========================================>--------------------]68%Task#02:installing00:00[==============================================>---------------]75%Task#03:installing00:06[======================>---------------------------------------]37%Task#00:downloading105/268[=======================>--------------------------------------]39%Task#01:downloading188/274[==========================================>-------------------]69%Task#02:installing00:00[===============================================>--------------]78%Task#03:installing00:06[=======================>--------------------------------------]39%Task#00:downloading107/268[========================>-------------------------------------]40%Task#01:downloading192/274[==========================================>-------------------]70%Task#02:installing00:00[=================================================>------------]80%Task#03:installing00:05[========================>-------------------------------------]40%Task#00:downloading109/268[========================>-------------------------------------]41%Task#01:downloading194/274[===========================================>------------------]71%Task#02:installing00:00[==================================================>-----------]82%Task#03:installing00:05[========================>-------------------------------------]41%Task#00:downloading112/268[=========================>------------------------------------]42%Task#01:downloading198/274[============================================>-----------------]72%Task#02:installing00:00[===================================================>----------]84%Task#03:installing00:05[=========================>------------------------------------]42%Task#00:downloading114/268[=========================>------------------------------------]43%Task#01:downloading202/274[=============================================>----------------]74%Task#02:installing00:00[=====================================================>--------]88%Task#03:installing00:05[==========================>-----------------------------------]44%Task#00:downloading116/268[==========================>-----------------------------------]43%Task#01:downloading206/274[==============================================>---------------]75%Task#02:installing00:00[========================================================>-----]92%Task#03:installing00:04[===========================>----------------------------------]46%Task#00:downloading119/268[===========================>----------------------------------]44%Task#01:downloading210/274[===============================================>--------------]77%Task#02:installing00:00[=========================================================>----]94%Task#03:installing00:04[=============================>--------------------------------]48%Task#00:downloading122/268[===========================>----------------------------------]46%Task#01:downloading212/274[===============================================>--------------]77%Task#02:installing00:00[===========================================================>--]97%Task#03:installing00:04[=============================>--------------------------------]49%Task#00:downloading124/268[============================>---------------------------------]46%Task#01:downloading214/274[===============================================>--------------]78%Task#02:installing00:00[==============================================================]99%Task#03:installing00:04[==============================>-------------------------------]50%Task#00:downloading126/268[============================>---------------------------------]47%Task#01:downloading218/274[================================================>-------------]80%Task#02:installing00:00[==============================================================]100%Task#03:installing00:04[===============================>------------------------------]52%Task#00:downloading127/268[============================>---------------------------------]47%Task#01:downloading220/274[=================================================>------------]80%Task#03:installing00:04[================================>-----------------------------]53%Task#00:downloading130/268[=============================>--------------------------------]49%Task#01:downloading224/274[==================================================>-----------]82%Task#03:installing00:03[=================================>----------------------------]55%Task#00:downloading132/268[==============================>-------------------------------]49%Task#01:downloading230/274[===================================================>----------]84%Task#03:installing00:03[==================================>---------------------------]57%Task#00:downloading134/268[==============================>-------------------------------]50%Task#01:downloading234/274[====================================================>---------]85%Task#03:installing00:03[====================================>-------------------------]59%Task#00:downloading136/268[==============================>-------------------------------]51%Task#01:downloading238/274[=====================================================>--------]87%Task#03:installing00:03[====================================>-------------------------]60%Task#00:downloading139/268[===============================>------------------------------]52%Task#01:downloading242/274[======================================================>-------]88%Task#03:installing00:03[=====================================>------------------------]61%Task#00:downloading141/268[================================>-----------------------------]53%Task#01:downloading246/274[=======================================================>------]90%Task#03:installing00:03[======================================>-----------------------]63%Task#00:downloading143/268[================================>-----------------------------]53%Task#01:downloading254/274[========================================================>-----]93%Task#03:installing00:02[=======================================>----------------------]65%Task#00:downloading147/268[=================================>----------------------------]55%Task#01:downloading258/274[=========================================================>----]94%Task#03:installing00:02[========================================>---------------------]66%Task#00:downloading149/268[=================================>----------------------------]56%Task#01:downloading262/274[==========================================================>---]96%Task#03:installing00:02[=========================================>--------------------]68%Task#00:downloading150/268[==================================>---------------------------]56%Task#01:downloading268/274[============================================================>-]98%Task#03:installing00:02[==========================================>-------------------]69%Task#00:downloading152/268[==================================>---------------------------]57%Task#01:downloading272/274[==============================================================]99%Task#03:installing00:02[===========================================>------------------]70%Task#00:downloading156/268[===================================>--------------------------]58%Task#01:downloading274/274[==============================================================]100%Task#03:installing00:02[===========================================>------------------]71%Task#00:downloading160/268[====================================>-------------------------]60%Task#01:installing00:00[==>-----------------------------------------------------------]5%Task#03:installing00:02[============================================>-----------------]73%Task#00:downloading162/268[====================================>-------------------------]60%Task#01:installing00:00[====>---------------------------------------------------------]8%Task#03:installing00:02[=============================================>----------------]74%Task#00:downloading163/268[=====================================>------------------------]61%Task#01:installing00:00[=======>------------------------------------------------------]14%Task#03:installing00:02[=============================================>----------------]75%Task#00:downloading168/268[======================================>-----------------------]63%Task#01:installing00:00[==========>---------------------------------------------------]17%Task#03:installing00:02[==============================================>---------------]76%Task#00:downloading170/268[======================================>-----------------------]63%Task#01:installing00:02[============>-------------------------------------------------]20%Task#03:installing00:02[===============================================>--------------]77%Task#00:downloading173/268[=======================================>----------------------]65%Task#01:installing00:02[===============>----------------------------------------------]25%Task#03:installing00:01[================================================>-------------]78%Task#00:downloading176/268[========================================>---------------------]66%Task#01:installing00:02[==================>-------------------------------------------]31%Task#03:installing00:01[================================================>-------------]79%Task#00:downloading178/268[========================================>---------------------]66%Task#01:installing00:01[=====================>----------------------------------------]36%Task#03:installing00:01[=================================================>------------]81%Task#00:downloading180/268[=========================================>--------------------]67%Task#01:installing00:01[======================>---------------------------------------]37%Task#03:installing00:01[==================================================>-----------]83%Task#00:downloading182/268[=========================================>--------------------]68%Task#01:installing00:01[========================>-------------------------------------]41%Task#03:installing00:01[===================================================>----------]83%Task#00:downloading185/268[==========================================>-------------------]69%Task#01:installing00:01[=========================>------------------------------------]42%Task#03:installing00:01[===================================================>----------]84%Task#00:downloading188/268[==========================================>-------------------]70%Task#01:installing00:01[===========================>----------------------------------]46%Task#03:installing00:01[====================================================>---------]85%Task#00:downloading190/268[===========================================>------------------]71%Task#01:installing00:01[=============================>--------------------------------]49%Task#03:installing00:01[=====================================================>--------]87%Task#00:downloading192/268[===========================================>------------------]72%Task#01:installing00:01[================================>-----------------------------]53%Task#03:installing00:01[======================================================>-------]88%Task#00:downloading194/268[============================================>-----------------]72%Task#01:installing00:01[=================================>----------------------------]54%Task#03:installing00:00[======================================================>-------]89%Task#00:downloading197/268[=============================================>----------------]74%Task#01:installing00:01[===================================>--------------------------]58%Task#03:installing00:00[=======================================================>------]91%Task#00:downloading198/268[=============================================>----------------]74%Task#01:installing00:01[=====================================>------------------------]61%Task#03:installing00:00[========================================================>-----]93%Task#00:downloading202/268[==============================================>---------------]75%Task#01:installing00:01[======================================>-----------------------]63%Task#03:installing00:00[=========================================================>----]93%Task#00:downloading204/268[==============================================>---------------]76%Task#01:installing00:01[========================================>---------------------]66%Task#03:installing00:00[==========================================================>---]94%Task#00:downloading206/268[===============================================>--------------]77%Task#01:installing00:01[==========================================>-------------------]69%Task#03:installing00:00[===========================================================>--]97%Task#00:downloading209/268[===============================================>--------------]78%Task#01:installing00:00[============================================>-----------------]73%Task#03:installing00:00[===========================================================>--]98%Task#00:downloading212/268[================================================>-------------]79%Task#01:installing00:00[==============================================>---------------]76%Task#03:installing00:00[============================================================>-]99%Task#00:downloading213/268[================================================>-------------]79%Task#01:installing00:00[================================================>-------------]80%Task#03:installing00:00[==============================================================]100%Task#00:downloading215/268[=================================================>------------]80%Task#01:installing00:00[=================================================>------------]81%Task#00:downloading217/268[=================================================>------------]81%Task#01:installing00:00[=====================================================>--------]86%Task#00:downloading219/268[==================================================>-----------]82%Task#01:installing00:00[======================================================>-------]88%Task#00:downloading220/268[==================================================>-----------]82%Task#01:installing00:00[========================================================>-----]92%Task#00:downloading221/268[==================================================>-----------]82%Task#01:installing00:00[=========================================================>----]93%Task#00:downloading225/268[===================================================>----------]84%Task#01:installing00:00[===========================================================>--]97%Task#00:downloading227/268[====================================================>---------]85%Task#01:installing00:00[============================================================>-]98%Task#00:downloading229/268[====================================================>---------]85%Task#01:installing00:00[==============================================================]100%Task#00:downloading230/268[====================================================>---------]86%Task#00:downloading232/268[=====================================================>--------]87%Task#00:downloading233/268[=====================================================>--------]87%Task#00:downloading235/268[=====================================================>--------]88%Task#00:downloading236/268[======================================================>-------]88%Task#00:downloading237/268[======================================================>-------]88%Task#00:downloading240/268[=======================================================>------]90%Task#00:downloading241/268[=======================================================>------]90%Task#00:downloading244/268[=======================================================>------]91%Task#00:downloading246/268[========================================================>-----]92%Task#00:downloading249/268[=========================================================>----]93%Task#00:downloading251/268[=========================================================>----]94%Task#00:downloading253/268[==========================================================>---]94%Task#00:downloading256/268[==========================================================>---]96%Task#00:downloading257/268[==========================================================>---]96%Task#00:downloading259/268[===========================================================>--]97%Task#00:downloading261/268[===========================================================>--]97%Task#00:downloading262/268[============================================================>-]98%Task#00:downloading263/268[============================================================>-]98%Task#00:downloading266/268[==============================================================]99%Task#00:downloading268/268[==============================================================]100%Task#00:installing00:00[=>------------------------------------------------------------]3%Task#00:installing00:00[======>-------------------------------------------------------]12%Task#00:installing00:00[========>-----------------------------------------------------]14%Task#00:installing00:00[============>-------------------------------------------------]20%Task#00:installing00:00[=================>--------------------------------------------]29%Task#00:installing00:01[===================>------------------------------------------]32%Task#00:installing00:01[==========================>-----------------------------------]43%Task#00:installing00:01[==============================>-------------------------------]49%Task#00:installing00:00[=================================>----------------------------]55%Task#00:installing00:00[=====================================>------------------------]61%Task#00:installing00:00[========================================>---------------------]67%Task#00:installing00:00[============================================>-----------------]72%Task#00:installing00:00[=================================================>------------]81%Task#00:installing00:00[=====================================================>--------]87%Task#00:installing00:00[=========================================================>----]93%Task#00:installing00:00[============================================================>-]99%Task#00:installing00:00[==============================================================]100%
\ No newline at end of file
diff --git a/examples/io/multiple/main.go b/examples/io/multiple/main.go
new file mode 100644
index 0000000..8a02e16
--- /dev/null
+++ b/examples/io/multiple/main.go
@@ -0,0 +1,83 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+ "sync"
+
+ "github.com/vbauerster/mpb"
+ "github.com/vbauerster/mpb/decor"
+)
+
+func main() {
+ log.SetOutput(os.Stderr)
+
+ url1 := "https://homebrew.bintray.com/bottles/youtube-dl-2016.12.12.sierra.bottle.tar.gz"
+ url2 := "https://homebrew.bintray.com/bottles/libtiff-4.0.7.sierra.bottle.tar.gz"
+
+ var wg sync.WaitGroup
+ p := mpb.New(mpb.WithWidth(64), mpb.WithWaitGroup(&wg))
+
+ for i, url := range [...]string{url1, url2} {
+ wg.Add(1)
+ name := fmt.Sprintf("url%d:", i+1)
+ go download(&wg, p, name, url, i)
+ }
+
+ p.Wait()
+}
+
+func download(wg *sync.WaitGroup, p *mpb.Progress, name, url string, n int) {
+ defer wg.Done()
+ resp, err := http.Get(url)
+ if err != nil {
+ log.Printf("%s: %v", name, err)
+ return
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ err = fmt.Errorf("non-200 status: %s", resp.Status)
+ log.Printf("%s: %v", name, err)
+ return
+ }
+
+ size := resp.ContentLength
+
+ // create dest
+ destName := filepath.Base(url)
+ dest, err := os.Create(destName)
+ if err != nil {
+ err = fmt.Errorf("Can't create %s: %v", destName, err)
+ log.Printf("%s: %v", name, err)
+ return
+ }
+
+ // create bar with appropriate decorators
+ bar := p.AddBar(size, mpb.BarPriority(n),
+ mpb.PrependDecorators(
+ decor.Name(name, decor.WC{W: len(name) + 1, C: decor.DidentRight}),
+ decor.CountersKibiByte("%6.1f / %6.1f", decor.WCSyncWidth),
+ ),
+ mpb.AppendDecorators(
+ decor.EwmaETA(decor.ET_STYLE_HHMMSS, 1024*4, decor.WCSyncWidth),
+ decor.AverageSpeed(decor.UnitKiB, "% .2f"),
+ ),
+ )
+
+ // create proxy reader
+ reader := bar.ProxyReader(resp.Body)
+ // and copy from reader
+ _, err = io.Copy(dest, reader)
+
+ if e := dest.Close(); err == nil {
+ err = e
+ }
+ if err != nil {
+ log.Printf("%s: %v", name, err)
+ }
+}
diff --git a/examples/io/single/main.go b/examples/io/single/main.go
new file mode 100644
index 0000000..a5eb2e4
--- /dev/null
+++ b/examples/io/single/main.go
@@ -0,0 +1,64 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/vbauerster/mpb"
+ "github.com/vbauerster/mpb/decor"
+)
+
+func main() {
+ url := "https://github.com/onivim/oni/releases/download/v0.3.4/Oni-0.3.4-amd64-linux.deb"
+
+ resp, err := http.Get(url)
+ if err != nil {
+ panic(err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ fmt.Printf("Server return non-200 status: %s\n", resp.Status)
+ return
+ }
+
+ size := resp.ContentLength
+
+ // create dest
+ destName := filepath.Base(url)
+ dest, err := os.Create(destName)
+ if err != nil {
+ fmt.Printf("Can't create %s: %v\n", destName, err)
+ return
+ }
+ defer dest.Close()
+
+ p := mpb.New(
+ mpb.WithWidth(60),
+ mpb.WithFormat("[=>-|"),
+ mpb.WithRefreshRate(180*time.Millisecond),
+ )
+
+ bar := p.AddBar(size,
+ mpb.PrependDecorators(
+ decor.CountersKibiByte("% 6.1f / % 6.1f"),
+ ),
+ mpb.AppendDecorators(
+ decor.EwmaETA(decor.ET_STYLE_MMSS, float64(size)/2048),
+ decor.Name(" ] "),
+ decor.AverageSpeed(decor.UnitKiB, "% .2f"),
+ ),
+ )
+
+ // create proxy reader
+ reader := bar.ProxyReader(resp.Body)
+
+ // and copy from reader, ignoring errors
+ io.Copy(dest, reader)
+
+ p.Wait()
+}
diff --git a/examples/panic/main.go b/examples/panic/main.go
new file mode 100644
index 0000000..b951fe6
--- /dev/null
+++ b/examples/panic/main.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "sync"
+ "time"
+
+ "github.com/vbauerster/mpb"
+ "github.com/vbauerster/mpb/decor"
+)
+
+func main() {
+ var wg sync.WaitGroup
+ p := mpb.New(mpb.WithWaitGroup(&wg), mpb.WithDebugOutput(os.Stderr))
+
+ wantPanic := "Some really long panic panic panic panic panic panic panic, really it is very long"
+ numBars := 3
+ wg.Add(numBars)
+
+ for i := 0; i < numBars; i++ {
+ name := fmt.Sprintf("b#%02d:", i)
+ bar := p.AddBar(100, mpb.BarID(i), mpb.PrependDecorators(panicDecorator(name, wantPanic)))
+
+ go func() {
+ defer wg.Done()
+ for i := 0; i < 100; i++ {
+ time.Sleep(50 * time.Millisecond)
+ bar.Increment()
+ }
+ }()
+ }
+
+ p.Wait()
+}
+
+func panicDecorator(name, panicMsg string) decor.Decorator {
+ d := &decorator{
+ msg: name,
+ panicMsg: panicMsg,
+ }
+ d.Init()
+ return d
+}
+
+type decorator struct {
+ decor.WC
+ msg string
+ panicMsg string
+}
+
+func (d *decorator) Decor(st *decor.Statistics) string {
+ if st.ID == 1 && st.Current >= 42 {
+ panic(d.panicMsg)
+ }
+ return d.FormatMsg(d.msg)
+}
diff --git a/examples/remove/main.go b/examples/remove/main.go
new file mode 100644
index 0000000..9bfd4f3
--- /dev/null
+++ b/examples/remove/main.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+ "fmt"
+ "math/rand"
+ "sync"
+ "time"
+
+ "github.com/vbauerster/mpb"
+ "github.com/vbauerster/mpb/decor"
+)
+
+func init() {
+ rand.Seed(time.Now().UnixNano())
+}
+
+func main() {
+ var wg sync.WaitGroup
+ p := mpb.New(mpb.WithWaitGroup(&wg))
+ total := 100
+ numBars := 3
+ wg.Add(numBars)
+
+ for i := 0; i < numBars; i++ {
+ name := fmt.Sprintf("Bar#%d:", i)
+
+ var bOption mpb.BarOption
+ if i == 0 {
+ bOption = mpb.BarRemoveOnComplete()
+ }
+
+ b := p.AddBar(int64(total), mpb.BarID(i),
+ bOption,
+ mpb.PrependDecorators(
+ decor.Name(name),
+ decor.EwmaETA(decor.ET_STYLE_GO, 60, decor.WCSyncSpace),
+ ),
+ mpb.AppendDecorators(decor.Percentage()),
+ )
+ go func() {
+ defer wg.Done()
+ max := 100 * time.Millisecond
+ for i := 0; i < total; i++ {
+ start := time.Now()
+ if b.ID() == 2 && i == 42 {
+ p.Abort(b, true)
+ return
+ }
+ time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10)
+ // ewma based decorators require work duration measurement
+ b.IncrBy(1, time.Since(start))
+ }
+ }()
+ }
+
+ p.Wait()
+}
diff --git a/examples/simple/main.go b/examples/simple/main.go
new file mode 100644
index 0000000..afc2050
--- /dev/null
+++ b/examples/simple/main.go
@@ -0,0 +1,54 @@
+package main
+
+import (
+ "fmt"
+ "math/rand"
+ "sync"
+ "time"
+
+ "github.com/vbauerster/mpb"
+ "github.com/vbauerster/mpb/decor"
+)
+
+func init() {
+ rand.Seed(time.Now().UnixNano())
+}
+
+func main() {
+ var wg sync.WaitGroup
+ p := mpb.New(mpb.WithWaitGroup(&wg))
+ total, numBars := 100, 3
+ wg.Add(numBars)
+
+ for i := 0; i < numBars; i++ {
+ name := fmt.Sprintf("Bar#%d:", i)
+ bar := p.AddBar(int64(total),
+ mpb.PrependDecorators(
+ // simple name decorator
+ decor.Name(name),
+ // decor.DSyncWidth bit enables column width synchronization
+ decor.Percentage(decor.WCSyncSpace),
+ ),
+ mpb.AppendDecorators(
+ // replace ETA decorator with "done" message, OnComplete event
+ decor.OnComplete(
+ // ETA decorator with ewma age of 60
+ decor.EwmaETA(decor.ET_STYLE_GO, 60), "done",
+ ),
+ ),
+ )
+ // simulating some work
+ go func() {
+ defer wg.Done()
+ max := 100 * time.Millisecond
+ for i := 0; i < total; i++ {
+ start := time.Now()
+ time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10)
+ // ewma based decorators require work duration measurement
+ bar.IncrBy(1, time.Since(start))
+ }
+ }()
+ }
+ // wait for all bars to complete and flush
+ p.Wait()
+}
diff --git a/examples/singleBar/main.go b/examples/singleBar/main.go
new file mode 100644
index 0000000..7fe249f
--- /dev/null
+++ b/examples/singleBar/main.go
@@ -0,0 +1,46 @@
+package main
+
+import (
+ "math/rand"
+ "time"
+
+ "github.com/vbauerster/mpb"
+ "github.com/vbauerster/mpb/decor"
+)
+
+func main() {
+ p := mpb.New(
+ // override default (80) width
+ mpb.WithWidth(64),
+ // override default "[=>-]" format
+ mpb.WithFormat("╢▌▌░╟"),
+ // override default 120ms refresh rate
+ mpb.WithRefreshRate(180*time.Millisecond),
+ )
+
+ total := 100
+ name := "Single Bar:"
+ // adding a single bar
+ bar := p.AddBar(int64(total),
+ mpb.PrependDecorators(
+ // display our name with one space on the right
+ decor.Name(name, decor.WC{W: len(name) + 1, C: decor.DidentRight}),
+ // replace ETA decorator with "done" message, OnComplete event
+ decor.OnComplete(
+ // ETA decorator with ewma age of 60, and width reservation of 4
+ decor.EwmaETA(decor.ET_STYLE_GO, 60, decor.WC{W: 4}), "done",
+ ),
+ ),
+ mpb.AppendDecorators(decor.Percentage()),
+ )
+ // simulating some work
+ max := 100 * time.Millisecond
+ for i := 0; i < total; i++ {
+ start := time.Now()
+ time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10)
+ // ewma based decorators require work duration measurement
+ bar.IncrBy(1, time.Since(start))
+ }
+ // wait for our bar to complete and flush
+ p.Wait()
+}
diff --git a/examples/sort/main.go b/examples/sort/main.go
new file mode 100644
index 0000000..3dd979c
--- /dev/null
+++ b/examples/sort/main.go
@@ -0,0 +1,55 @@
+package main
+
+import (
+ "fmt"
+ "math/rand"
+ "sync"
+ "time"
+
+ "github.com/vbauerster/mpb"
+ "github.com/vbauerster/mpb/decor"
+)
+
+func init() {
+ rand.Seed(time.Now().UnixNano())
+}
+
+func main() {
+ var wg sync.WaitGroup
+ p := mpb.New(mpb.WithWaitGroup(&wg))
+ total := 100
+ numBars := 3
+ wg.Add(numBars)
+
+ for i := 0; i < numBars; i++ {
+ var name string
+ if i != 1 {
+ name = fmt.Sprintf("Bar#%d:", i)
+ }
+ b := p.AddBar(int64(total),
+ mpb.PrependDecorators(
+ decor.Name(name, decor.WCSyncWidth),
+ decor.CountersNoUnit("%d / %d", decor.WCSyncSpace),
+ ),
+ mpb.AppendDecorators(
+ decor.EwmaETA(decor.ET_STYLE_GO, 60, decor.WC{W: 3}),
+ ),
+ )
+ go func() {
+ defer wg.Done()
+ max := 100 * time.Millisecond
+ for i := 0; i < total; i++ {
+ start := time.Now()
+ time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10)
+ if i&1 == 1 {
+ priority := total - int(b.Current())
+ p.UpdateBarPriority(b, priority)
+ }
+ // ewma based decorators require work duration measurement
+ b.IncrBy(1, time.Since(start))
+ }
+ }()
+ }
+
+ p.Wait()
+}
diff --git a/examples/stress/main.go b/examples/stress/main.go
new file mode 100644
index 0000000..b1e99fb
--- /dev/null
+++ b/examples/stress/main.go
@@ -0,0 +1,52 @@
+package main
+
+import (
+ "fmt"
+ "math/rand"
+ "sync"
+ "time"
+
+ "github.com/vbauerster/mpb"
+ "github.com/vbauerster/mpb/decor"
+)
+
+const (
+ totalBars = 32
+)
+
+func init() {
+ rand.Seed(time.Now().UnixNano())
+}
+
+func main() {
+ var wg sync.WaitGroup
+ p := mpb.New(mpb.WithWaitGroup(&wg))
+ wg.Add(totalBars)
+
+ for i := 0; i < totalBars; i++ {
+ name := fmt.Sprintf("Bar#%02d: ", i)
+ total := rand.Intn(320) + 10
+ bar := p.AddBar(int64(total),
+ mpb.PrependDecorators(
+ decor.Name(name),
+ decor.EwmaETA(decor.ET_STYLE_GO, 60, decor.WCSyncSpace),
+ ),
+ mpb.AppendDecorators(
+ decor.Percentage(decor.WC{W: 5}),
+ ),
+ )
+
+ go func() {
+ defer wg.Done()
+ max := 100 * time.Millisecond
+ for !bar.Completed() {
+ start := time.Now()
+ time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10)
+ // ewma based decorators require work duration measurement
+ bar.IncrBy(1, time.Since(start))
+ }
+ }()
+ }
+
+ p.Wait()
+}
diff --git a/export_test.go b/export_test.go
new file mode 100644
index 0000000..0bc28fe
--- /dev/null
+++ b/export_test.go
@@ -0,0 +1,3 @@
+package mpb
+
+var SyncWidth = syncWidth
diff --git a/go.test.sh b/go.test.sh
new file mode 100755
index 0000000..34dbbfb
--- /dev/null
+++ b/go.test.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+set -e
+echo "" > coverage.txt
+
+for d in $(go list ./... | grep -v vendor); do
+ go test -race -coverprofile=profile.out -covermode=atomic $d
+ if [ -f profile.out ]; then
+ cat profile.out >> coverage.txt
+ rm profile.out
+ fi
+done
diff --git a/internal/percentage.go b/internal/percentage.go
new file mode 100644
index 0000000..3c8defb
--- /dev/null
+++ b/internal/percentage.go
@@ -0,0 +1,10 @@
+package internal
+
+// Percentage is a helper function, to calculate percentage.
+func Percentage(total, current, width int64) int64 {
+ if total <= 0 {
+ return 0
+ }
+ p := float64(width*current) / float64(total)
+ return int64(Round(p))
+}
diff --git a/internal/percentage_test.go b/internal/percentage_test.go
new file mode 100644
index 0000000..ce5a25e
--- /dev/null
+++ b/internal/percentage_test.go
@@ -0,0 +1,74 @@
+package internal
+
+import (
+ "testing"
+)
+
+func TestPercentage(t *testing.T) {
+ // key is barWidth
+ testSuite := map[int64]map[string]struct {
+ total, current, expected int64
+ }{
+ 100: {
+ "t,c,e{-1,-1,0}": {-1, -1, 0},
+ "t,c,e{0,-1,0}": {0, -1, 0},
+ "t,c,e{0,0,0}": {0, 0, 0},
+ "t,c,e{0,1,0}": {0, 1, 0},
+ "t,c,e{100,0,0}": {100, 0, 0},
+ "t,c,e{100,10,10}": {100, 10, 10},
+ "t,c,e{100,15,15}": {100, 15, 15},
+ "t,c,e{100,50,50}": {100, 50, 50},
+ "t,c,e{100,99,99}": {100, 99, 99},
+ "t,c,e{100,100,100}": {100, 100, 100},
+ "t,c,e{100,101,101}": {100, 101, 101},
+ "t,c,e{100,102,101}": {100, 102, 102},
+ "t,c,e{120,0,0}": {120, 0, 0},
+ "t,c,e{120,10,8}": {120, 10, 8},
+ "t,c,e{120,15,13}": {120, 15, 13},
+ "t,c,e{120,50,42}": {120, 50, 42},
+ "t,c,e{120,60,50}": {120, 60, 50},
+ "t,c,e{120,99,83}": {120, 99, 83},
+ "t,c,e{120,101,84}": {120, 101, 84},
+ "t,c,e{120,118,98}": {120, 118, 98},
+ "t,c,e{120,119,99}": {120, 119, 99},
+ "t,c,e{120,120,100}": {120, 120, 100},
+ "t,c,e{120,121,101}": {120, 121, 101},
+ "t,c,e{120,122,101}": {120, 122, 102},
+ },
+ 80: {
+ "t,c,e{-1,-1,0}": {-1, -1, 0},
+ "t,c,e{0,-1,0}": {0, -1, 0},
+ "t,c,e{0,0,0}": {0, 0, 0},
+ "t,c,e{0,1,0}": {0, 1, 0},
+ "t,c,e{100,0,0}": {100, 0, 0},
+ "t,c,e{100,10,8}": {100, 10, 8},
+ "t,c,e{100,15,12}": {100, 15, 12},
+ "t,c,e{100,50,40}": {100, 50, 40},
+ "t,c,e{100,99,79}": {100, 99, 79},
+ "t,c,e{100,100,80}": {100, 100, 80},
+ "t,c,e{100,101,81}": {100, 101, 81},
+ "t,c,e{100,102,82}": {100, 102, 82},
+ "t,c,e{120,0,0}": {120, 0, 0},
+ "t,c,e{120,10,7}": {120, 10, 7},
+ "t,c,e{120,15,10}": {120, 15, 10},
+ "t,c,e{120,50,33}": {120, 50, 33},
+ "t,c,e{120,60,40}": {120, 60, 40},
+ "t,c,e{120,99,66}": {120, 99, 66},
+ "t,c,e{120,101,67}": {120, 101, 67},
+ "t,c,e{120,118,79}": {120, 118, 79},
+ "t,c,e{120,119,79}": {120, 119, 79},
+ "t,c,e{120,120,80}": {120, 120, 80},
+ "t,c,e{120,121,81}": {120, 121, 81},
+ "t,c,e{120,122,81}": {120, 122, 81},
+ },
+ }
+
+ for width, cases := range testSuite {
+ for name, tc := range cases {
+ got := Percentage(tc.total, tc.current, width)
+ if got != tc.expected {
+ t.Errorf("width %d; %s: Expected: %d, got: %d\n", width, name, tc.expected, got)
+ }
+ }
+ }
+}
diff --git a/internal/round.go b/internal/round.go
new file mode 100644
index 0000000..c54a789
--- /dev/null
+++ b/internal/round.go
@@ -0,0 +1,49 @@
+package internal
+
+import "math"
+
+const (
+ uvone = 0x3FF0000000000000
+ mask = 0x7FF
+ shift = 64 - 11 - 1
+ bias = 1023
+ signMask = 1 << 63
+ fracMask = 1<= 0.5 {
+ // return t + Copysign(1, x)
+ // }
+ // return t
+ // }
+ bits := math.Float64bits(x)
+ e := uint(bits>>shift) & mask
+ if e < bias {
+ // Round abs(x) < 1 including denormals.
+ bits &= signMask // +-0
+ if e == bias-1 {
+ bits |= uvone // +-1
+ }
+ } else if e < bias+shift {
+ // Round any abs(x) >= 1 containing a fractional component [0,1).
+ //
+ // Numbers with larger exponents are returned unchanged since they
+ // must be either an integer, infinity, or NaN.
+ const half = 1 << (shift - 1)
+ e -= bias
+ bits += half >> e
+ bits &^= fracMask >> e
+ }
+ return math.Float64frombits(bits)
+}
diff --git a/options.go b/options.go
new file mode 100644
index 0000000..05d2ecf
--- /dev/null
+++ b/options.go
@@ -0,0 +1,95 @@
+package mpb
+
+import (
+ "io"
+ "sync"
+ "time"
+ "unicode/utf8"
+
+ "github.com/vbauerster/mpb/cwriter"
+)
+
+// ProgressOption is a function option which changes the default behavior of
+// progress pool, if passed to mpb.New(...ProgressOption)
+type ProgressOption func(*pState)
+
+// WithWaitGroup provides means to have a single joint point.
+// If *sync.WaitGroup is provided, you can safely call just p.Wait()
+// without calling Wait() on provided *sync.WaitGroup.
+// Makes sense when there are more than one bar to render.
+func WithWaitGroup(wg *sync.WaitGroup) ProgressOption {
+ return func(s *pState) {
+ s.uwg = wg
+ }
+}
+
+// WithWidth overrides default width 80
+func WithWidth(w int) ProgressOption {
+ return func(s *pState) {
+ if w >= 0 {
+ s.width = w
+ }
+ }
+}
+
+// WithFormat overrides default bar format "[=>-]"
+func WithFormat(format string) ProgressOption {
+ return func(s *pState) {
+ if utf8.RuneCountInString(format) == formatLen {
+ s.format = format
+ }
+ }
+}
+
+// WithRefreshRate overrides default 120ms refresh rate
+func WithRefreshRate(d time.Duration) ProgressOption {
+ return func(s *pState) {
+ if d < 10*time.Millisecond {
+ return
+ }
+ s.rr = d
+ }
+}
+
+// WithManualRefresh disables internal auto refresh time.Ticker.
+// Refresh will occur upon receive value from provided ch.
+func WithManualRefresh(ch <-chan time.Time) ProgressOption {
+ return func(s *pState) {
+ s.manualRefreshCh = ch
+ }
+}
+
+// WithCancel provide your cancel channel,
+// which you plan to close at some point.
+func WithCancel(ch <-chan struct{}) ProgressOption {
+ return func(s *pState) {
+ s.cancel = ch
+ }
+}
+
+// WithShutdownNotifier provided chanel will be closed, after all bars have been rendered.
+func WithShutdownNotifier(ch chan struct{}) ProgressOption {
+ return func(s *pState) {
+ s.shutdownNotifier = ch
+ }
+}
+
+// WithOutput overrides default output os.Stdout
+func WithOutput(w io.Writer) ProgressOption {
+ return func(s *pState) {
+ if w == nil {
+ return
+ }
+ s.cw = cwriter.New(w)
+ }
+}
+
+// WithDebugOutput sets debug output.
+func WithDebugOutput(w io.Writer) ProgressOption {
+ return func(s *pState) {
+ if w == nil {
+ return
+ }
+ s.debugOut = w
+ }
+}
diff --git a/options_go1.7.go b/options_go1.7.go
new file mode 100644
index 0000000..ca9a5ba
--- /dev/null
+++ b/options_go1.7.go
@@ -0,0 +1,15 @@
+//+build go1.7
+
+package mpb
+
+import "context"
+
+// WithContext provided context will be used for cancellation purposes
+func WithContext(ctx context.Context) ProgressOption {
+ return func(s *pState) {
+ if ctx == nil {
+ panic("ctx must not be nil")
+ }
+ s.cancel = ctx.Done()
+ }
+}
diff --git a/priority_queue.go b/priority_queue.go
new file mode 100644
index 0000000..7bc588c
--- /dev/null
+++ b/priority_queue.go
@@ -0,0 +1,40 @@
+package mpb
+
+import "container/heap"
+
+// A priorityQueue implements heap.Interface
+type priorityQueue []*Bar
+
+func (pq priorityQueue) Len() int { return len(pq) }
+
+func (pq priorityQueue) Less(i, j int) bool {
+ return pq[i].priority < pq[j].priority
+}
+
+func (pq priorityQueue) Swap(i, j int) {
+ pq[i], pq[j] = pq[j], pq[i]
+ pq[i].index = i
+ pq[j].index = j
+}
+
+func (pq *priorityQueue) Push(x interface{}) {
+ n := len(*pq)
+ bar := x.(*Bar)
+ bar.index = n
+ *pq = append(*pq, bar)
+}
+
+func (pq *priorityQueue) Pop() interface{} {
+ old := *pq
+ n := len(old)
+ bar := old[n-1]
+ bar.index = -1 // for safety
+ *pq = old[0 : n-1]
+ return bar
+}
+
+// update modifies the priority of a Bar in the queue.
+func (pq *priorityQueue) update(bar *Bar, priority int) {
+ bar.priority = priority
+ heap.Fix(pq, bar.index)
+}
diff --git a/progress.go b/progress.go
new file mode 100644
index 0000000..d95fe45
--- /dev/null
+++ b/progress.go
@@ -0,0 +1,251 @@
+package mpb
+
+import (
+ "container/heap"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "sync"
+ "time"
+
+ "github.com/vbauerster/mpb/cwriter"
+)
+
+const (
+ // default RefreshRate
+ prr = 120 * time.Millisecond
+ // default width
+ pwidth = 80
+ // default format
+ pformat = "[=>-]"
+)
+
+// Progress represents the container that renders Progress bars
+type Progress struct {
+ wg *sync.WaitGroup
+ uwg *sync.WaitGroup
+ operateState chan func(*pState)
+ done chan struct{}
+}
+
+type pState struct {
+ bHeap *priorityQueue
+ shutdownPending []*Bar
+ heapUpdated bool
+ zeroWait bool
+ idCounter int
+ width int
+ format string
+ rr time.Duration
+ cw *cwriter.Writer
+ pMatrix map[int][]chan int
+ aMatrix map[int][]chan int
+
+ // following are provided by user
+ uwg *sync.WaitGroup
+ manualRefreshCh <-chan time.Time
+ cancel <-chan struct{}
+ shutdownNotifier chan struct{}
+ waitBars map[*Bar]*Bar
+ debugOut io.Writer
+}
+
+// New creates new Progress instance, which orchestrates bars rendering process.
+// Accepts mpb.ProgressOption funcs for customization.
+func New(options ...ProgressOption) *Progress {
+ pq := make(priorityQueue, 0)
+ heap.Init(&pq)
+ s := &pState{
+ bHeap: &pq,
+ width: pwidth,
+ format: pformat,
+ cw: cwriter.New(os.Stdout),
+ rr: prr,
+ waitBars: make(map[*Bar]*Bar),
+ debugOut: ioutil.Discard,
+ }
+
+ for _, opt := range options {
+ if opt != nil {
+ opt(s)
+ }
+ }
+
+ p := &Progress{
+ uwg: s.uwg,
+ wg: new(sync.WaitGroup),
+ operateState: make(chan func(*pState)),
+ done: make(chan struct{}),
+ }
+ go p.serve(s)
+ return p
+}
+
+// AddBar creates a new progress bar and adds to the container.
+func (p *Progress) AddBar(total int64, options ...BarOption) *Bar {
+ p.wg.Add(1)
+ result := make(chan *Bar)
+ select {
+ case p.operateState <- func(s *pState) {
+ options = append(options, barWidth(s.width), barFormat(s.format))
+ b := newBar(p.wg, s.idCounter, total, s.cancel, options...)
+ if b.runningBar != nil {
+ s.waitBars[b.runningBar] = b
+ } else {
+ heap.Push(s.bHeap, b)
+ s.heapUpdated = true
+ }
+ s.idCounter++
+ result <- b
+ }:
+ return <-result
+ case <-p.done:
+ p.wg.Done()
+ return nil
+ }
+}
+
+// Abort is only effective while bar progress is running,
+// it means remove bar now without waiting for its completion.
+// If bar is already completed, there is nothing to abort.
+// If you need to remove bar after completion, use BarRemoveOnComplete BarOption.
+func (p *Progress) Abort(b *Bar, remove bool) {
+ select {
+ case p.operateState <- func(s *pState) {
+ if b.index < 0 {
+ return
+ }
+ if remove {
+ s.heapUpdated = heap.Remove(s.bHeap, b.index) != nil
+ }
+ s.shutdownPending = append(s.shutdownPending, b)
+ }:
+ case <-p.done:
+ }
+}
+
+// UpdateBarPriority provides a way to change bar's order position.
+// Zero is highest priority, i.e. bar will be on top.
+func (p *Progress) UpdateBarPriority(b *Bar, priority int) {
+ select {
+ case p.operateState <- func(s *pState) { s.bHeap.update(b, priority) }:
+ case <-p.done:
+ }
+}
+
+// BarCount returns bars count
+func (p *Progress) BarCount() int {
+ result := make(chan int, 1)
+ select {
+ case p.operateState <- func(s *pState) { result <- s.bHeap.Len() }:
+ return <-result
+ case <-p.done:
+ return 0
+ }
+}
+
+// Wait first waits for user provided *sync.WaitGroup, if any,
+// then waits far all bars to complete and finally shutdowns master goroutine.
+// After this method has been called, there is no way to reuse *Progress instance.
+func (p *Progress) Wait() {
+ if p.uwg != nil {
+ p.uwg.Wait()
+ }
+
+ p.wg.Wait()
+
+ select {
+ case p.operateState <- func(s *pState) { s.zeroWait = true }:
+ <-p.done
+ case <-p.done:
+ }
+}
+
+func (s *pState) updateSyncMatrix() {
+ s.pMatrix = make(map[int][]chan int)
+ s.aMatrix = make(map[int][]chan int)
+ for i := 0; i < s.bHeap.Len(); i++ {
+ bar := (*s.bHeap)[i]
+ table := bar.wSyncTable()
+ pRow, aRow := table[0], table[1]
+
+ for i, ch := range pRow {
+ s.pMatrix[i] = append(s.pMatrix[i], ch)
+ }
+
+ for i, ch := range aRow {
+ s.aMatrix[i] = append(s.aMatrix[i], ch)
+ }
+ }
+}
+
+func (s *pState) render(tw int) {
+ if s.heapUpdated {
+ s.updateSyncMatrix()
+ s.heapUpdated = false
+ }
+ syncWidth(s.pMatrix)
+ syncWidth(s.aMatrix)
+
+ for i := 0; i < s.bHeap.Len(); i++ {
+ bar := (*s.bHeap)[i]
+ go bar.render(s.debugOut, tw)
+ }
+
+ if err := s.flush(s.bHeap.Len()); err != nil {
+ fmt.Fprintf(s.debugOut, "%s %s %v\n", "[mpb]", time.Now(), err)
+ }
+}
+
+func (s *pState) flush(lineCount int) error {
+ for s.bHeap.Len() > 0 {
+ bar := heap.Pop(s.bHeap).(*Bar)
+ frameReader := <-bar.frameReaderCh
+ defer func() {
+ if frameReader.toShutdown {
+ // shutdown at next flush, in other words decrement underlying WaitGroup
+ // only after the bar with completed state has been flushed.
+ // this ensures no bar ends up with less than 100% rendered.
+ s.shutdownPending = append(s.shutdownPending, bar)
+ if replacementBar, ok := s.waitBars[bar]; ok {
+ heap.Push(s.bHeap, replacementBar)
+ s.heapUpdated = true
+ delete(s.waitBars, bar)
+ }
+ if frameReader.removeOnComplete {
+ s.heapUpdated = true
+ return
+ }
+ }
+ heap.Push(s.bHeap, bar)
+ }()
+ s.cw.ReadFrom(frameReader)
+ lineCount += frameReader.extendedLines
+ }
+
+ for i := len(s.shutdownPending) - 1; i >= 0; i-- {
+ close(s.shutdownPending[i].shutdown)
+ s.shutdownPending = s.shutdownPending[:i]
+ }
+
+ return s.cw.Flush(lineCount)
+}
+
+func syncWidth(matrix map[int][]chan int) {
+ for _, column := range matrix {
+ column := column
+ go func() {
+ var maxWidth int
+ for _, ch := range column {
+ w := <-ch
+ if w > maxWidth {
+ maxWidth = w
+ }
+ }
+ for _, ch := range column {
+ ch <- maxWidth
+ }
+ }()
+ }
+}
diff --git a/progress_go1.7_test.go b/progress_go1.7_test.go
new file mode 100644
index 0000000..6b4adda
--- /dev/null
+++ b/progress_go1.7_test.go
@@ -0,0 +1,50 @@
+//+build go1.7
+
+package mpb_test
+
+import (
+ "context"
+ "io/ioutil"
+ "testing"
+ "time"
+
+ "github.com/vbauerster/mpb"
+)
+
+func TestWithContext(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ shutdown := make(chan struct{})
+ p := mpb.New(
+ mpb.WithOutput(ioutil.Discard),
+ mpb.WithContext(ctx),
+ mpb.WithShutdownNotifier(shutdown),
+ )
+
+ total := 1000
+ numBars := 3
+ bars := make([]*mpb.Bar, 0, numBars)
+ for i := 0; i < numBars; i++ {
+ bar := p.AddBar(int64(total))
+ bars = append(bars, bar)
+ go func() {
+ for !bar.Completed() {
+ time.Sleep(randomDuration(40 * time.Millisecond))
+ bar.Increment()
+ }
+ }()
+ }
+
+ time.AfterFunc(100*time.Millisecond, cancel)
+
+ p.Wait()
+ for _, bar := range bars {
+ if bar.Current() >= int64(total) {
+ t.Errorf("bar %d: total = %d, current = %d\n", bar.ID(), total, bar.Current())
+ }
+ }
+ select {
+ case <-shutdown:
+ case <-time.After(100 * time.Millisecond):
+ t.Error("Progress didn't stop")
+ }
+}
diff --git a/progress_posix.go b/progress_posix.go
new file mode 100644
index 0000000..545245a
--- /dev/null
+++ b/progress_posix.go
@@ -0,0 +1,70 @@
+// +build !windows
+
+package mpb
+
+import (
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+)
+
+func (p *Progress) serve(s *pState) {
+
+ var ticker *time.Ticker
+ var refreshCh <-chan time.Time
+ var winch chan os.Signal
+ var resumeTimer *time.Timer
+ var resumeEvent <-chan time.Time
+ winchIdleDur := s.rr * 2
+
+ if s.manualRefreshCh == nil {
+ ticker = time.NewTicker(s.rr)
+ refreshCh = ticker.C
+ winch = make(chan os.Signal, 2)
+ signal.Notify(winch, syscall.SIGWINCH)
+ } else {
+ refreshCh = s.manualRefreshCh
+ }
+
+ for {
+ select {
+ case op := <-p.operateState:
+ op(s)
+ case <-refreshCh:
+ if s.zeroWait {
+ if s.manualRefreshCh == nil {
+ signal.Stop(winch)
+ ticker.Stop()
+ }
+ if s.shutdownNotifier != nil {
+ close(s.shutdownNotifier)
+ }
+ close(p.done)
+ return
+ }
+ tw, err := s.cw.GetWidth()
+ if err != nil {
+ tw = s.width
+ }
+ s.render(tw)
+ case <-winch:
+ tw, err := s.cw.GetWidth()
+ if err != nil {
+ tw = s.width
+ }
+ s.render(tw - tw/8)
+ if resumeTimer != nil && resumeTimer.Reset(winchIdleDur) {
+ break
+ }
+ ticker.Stop()
+ resumeTimer = time.NewTimer(winchIdleDur)
+ resumeEvent = resumeTimer.C
+ case <-resumeEvent:
+ ticker = time.NewTicker(s.rr)
+ refreshCh = ticker.C
+ resumeEvent = nil
+ resumeTimer = nil
+ }
+ }
+}
diff --git a/progress_test.go b/progress_test.go
new file mode 100644
index 0000000..0c4388c
--- /dev/null
+++ b/progress_test.go
@@ -0,0 +1,149 @@
+package mpb_test
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "math/rand"
+ "sync"
+ "testing"
+ "time"
+
+ . "github.com/vbauerster/mpb"
+ "github.com/vbauerster/mpb/cwriter"
+)
+
+var (
+ cursorUp = fmt.Sprintf("%c[%dA", cwriter.ESC, 1)
+ clearLine = fmt.Sprintf("%c[2K\r", cwriter.ESC)
+ clearCursorAndLine = cursorUp + clearLine
+)
+
+func init() {
+ rand.Seed(time.Now().UnixNano())
+}
+
+func TestBarCount(t *testing.T) {
+ p := New(WithOutput(ioutil.Discard))
+
+ var wg sync.WaitGroup
+ wg.Add(1)
+ b := p.AddBar(100)
+ go func() {
+ for i := 0; i < 100; i++ {
+ if i == 33 {
+ wg.Done()
+ }
+ b.Increment()
+ time.Sleep(randomDuration(100 * time.Millisecond))
+ }
+ }()
+
+ wg.Wait()
+ count := p.BarCount()
+ if count != 1 {
+ t.Errorf("BarCount want: %q, got: %q\n", 1, count)
+ }
+
+ p.Abort(b, true)
+ p.Wait()
+}
+
+func TestBarAbort(t *testing.T) {
+ p := New(WithOutput(ioutil.Discard))
+
+ var wg sync.WaitGroup
+ wg.Add(1)
+ bars := make([]*Bar, 3)
+ for i := 0; i < 3; i++ {
+ b := p.AddBar(100)
+ bars[i] = b
+ go func(n int) {
+ for i := 0; i < 100; i++ {
+ if n == 0 && i == 33 {
+ p.Abort(b, true)
+ wg.Done()
+ }
+ b.Increment()
+ time.Sleep(randomDuration(100 * time.Millisecond))
+ }
+ }(i)
+ }
+
+ wg.Wait()
+ count := p.BarCount()
+ if count != 2 {
+ t.Errorf("BarCount want: %q, got: %q\n", 2, count)
+ }
+ p.Abort(bars[1], true)
+ p.Abort(bars[2], true)
+ p.Wait()
+}
+
+func TestWithCancel(t *testing.T) {
+ cancel := make(chan struct{})
+ shutdown := make(chan struct{})
+ p := New(
+ WithOutput(ioutil.Discard),
+ WithCancel(cancel),
+ WithShutdownNotifier(shutdown),
+ )
+
+ for i := 0; i < 2; i++ {
+ bar := p.AddBar(int64(1000), BarID(i))
+ go func() {
+ for !bar.Completed() {
+ time.Sleep(randomDuration(100 * time.Millisecond))
+ bar.Increment()
+ }
+ }()
+ }
+
+ time.AfterFunc(100*time.Millisecond, func() {
+ close(cancel)
+ })
+
+ p.Wait()
+
+ select {
+ case <-shutdown:
+ case <-time.After(200 * time.Millisecond):
+ t.FailNow()
+ }
+}
+
+func TestWithFormat(t *testing.T) {
+ var buf bytes.Buffer
+ customFormat := "╢▌▌░╟"
+ p := New(WithOutput(&buf), WithFormat(customFormat))
+ bar := p.AddBar(100, BarTrim())
+
+ for i := 0; i < 100; i++ {
+ if i == 33 {
+ p.Abort(bar, true)
+ break
+ }
+ time.Sleep(randomDuration(100 * time.Millisecond))
+ bar.Increment()
+ }
+
+ p.Wait()
+
+ lastLine := getLastLine(buf.Bytes())
+
+ for _, r := range customFormat {
+ if !bytes.ContainsRune(lastLine, r) {
+ t.Errorf("Rune %#U not found in bar\n", r)
+ }
+ }
+}
+
+func getLastLine(bb []byte) []byte {
+ split := bytes.Split(bb, []byte("\n"))
+ lastLine := split[len(split)-2]
+ return lastLine[len(clearCursorAndLine):]
+}
+
+func randomDuration(max time.Duration) time.Duration {
+ return time.Duration(rand.Intn(10)+1) * max / 10
+}
diff --git a/progress_windows.go b/progress_windows.go
new file mode 100644
index 0000000..cab03d3
--- /dev/null
+++ b/progress_windows.go
@@ -0,0 +1,43 @@
+// +build windows
+
+package mpb
+
+import (
+ "time"
+)
+
+func (p *Progress) serve(s *pState) {
+
+ var ticker *time.Ticker
+ var refreshCh <-chan time.Time
+
+ if s.manualRefreshCh == nil {
+ ticker = time.NewTicker(s.rr)
+ refreshCh = ticker.C
+ } else {
+ refreshCh = s.manualRefreshCh
+ }
+
+ for {
+ select {
+ case op := <-p.operateState:
+ op(s)
+ case <-refreshCh:
+ if s.zeroWait {
+ if s.manualRefreshCh == nil {
+ ticker.Stop()
+ }
+ if s.shutdownNotifier != nil {
+ close(s.shutdownNotifier)
+ }
+ close(p.done)
+ return
+ }
+ tw, err := s.cw.GetWidth()
+ if err != nil {
+ tw = s.width
+ }
+ s.render(tw)
+ }
+ }
+}
diff --git a/proxyreader.go b/proxyreader.go
new file mode 100644
index 0000000..d2692cc
--- /dev/null
+++ b/proxyreader.go
@@ -0,0 +1,22 @@
+package mpb
+
+import (
+ "io"
+ "time"
+)
+
+// proxyReader is io.Reader wrapper, for proxy read bytes
+type proxyReader struct {
+ io.ReadCloser
+ bar *Bar
+ iT time.Time
+}
+
+func (pr *proxyReader) Read(p []byte) (n int, err error) {
+ n, err = pr.ReadCloser.Read(p)
+ if n > 0 {
+ pr.bar.IncrBy(n, time.Since(pr.iT))
+ pr.iT = time.Now()
+ }
+ return
+}
diff --git a/proxyreader_test.go b/proxyreader_test.go
new file mode 100644
index 0000000..d1c0862
--- /dev/null
+++ b/proxyreader_test.go
@@ -0,0 +1,45 @@
+package mpb_test
+
+import (
+ "bytes"
+ "io"
+ "io/ioutil"
+ "strings"
+ "testing"
+
+ "github.com/vbauerster/mpb"
+)
+
+const content = `Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do
+ eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
+ veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+ commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit
+ esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
+ cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
+ est laborum.`
+
+func TestProxyReader(t *testing.T) {
+ var buf bytes.Buffer
+ p := mpb.New(mpb.WithOutput(&buf))
+
+ reader := strings.NewReader(content)
+
+ total := len(content)
+ bar := p.AddBar(100, mpb.BarTrim())
+ preader := bar.ProxyReader(reader)
+
+ if _, ok := preader.(io.Closer); !ok {
+ t.Error("type assertion to io.Closer is not ok")
+ }
+
+ written, err := io.Copy(ioutil.Discard, preader)
+ if err != nil {
+ t.Errorf("Error copying from reader: %+v\n", err)
+ }
+
+ p.Wait()
+
+ if written != int64(total) {
+ t.Errorf("Expected written: %d, got: %d\n", total, written)
+ }
+}