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 + +[![GoDoc](https://godoc.org/github.com/vbauerster/mpb?status.svg)](https://godoc.org/github.com/vbauerster/mpb) +[![Build Status](https://travis-ci.org/vbauerster/mpb.svg?branch=master)](https://travis-ci.org/vbauerster/mpb) +[![Go Report Card](https://goreportcard.com/badge/github.com/vbauerster/mpb)](https://goreportcard.com/report/github.com/vbauerster/mpb) +[![codecov](https://codecov.io/gh/vbauerster/mpb/branch/master/graph/badge.svg)](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) + +![dynamic total](examples/gifs/godEMrCZmJkHYH1X9dN4Nm0U7.svg) + +#### [Complex example](examples/complex/main.go) + +![complex](examples/gifs/wHzf1M7sd7B3zVa2scBMnjqRf.svg) + +#### [Bytes counters](examples/io/single/main.go) + +![byte counters](examples/gifs/hIpTa3A5rQz65ssiVuRJu87X6.svg) 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 @@ +~/go/src/github.com/vbauerster/mpb/examples/dynTotal~/go/src/github.com/vbauerster/mpb/examples/dynTotalmaster*gorun-racemain.gogorun-racemain.gogorun-racemain.go55.7KiB/56.7KiB[============================================================>-]98%100.7KiB/100.7KiB[==============================================================]100%~/go/src/github.com/vbauerster/mpb/examples/dynTotalmaster*13s~/go/src/github.com/vbauerster/mpb/examples/dynTotalmaster~/go/src/github.com/vbauerster/mpb/examples/dynTotalmastergorun-racemain.gogorun-racemain.gogorun-racemain.gogorun-racemain.go519b/1.5KiB[====================>-----------------------------------------]34%2.2KiB/3.2KiB[==========================================>-------------------]69%2.5KiB/3.5KiB[===========================================>------------------]72%3.2KiB/4.2KiB[==============================================>---------------]76%3.6KiB/4.6KiB[================================================>-------------]78%6.5KiB/7.5KiB[=====================================================>--------]87%8.0KiB/9.0KiB[======================================================>-------]89%10.6KiB/11.6KiB[========================================================>-----]91%12.1KiB/13.1KiB[========================================================>-----]92%13.1KiB/14.1KiB[=========================================================>----]93%13.2KiB/14.2KiB[=========================================================>----]93%13.9KiB/14.9KiB[=========================================================>----]93%15.5KiB/16.5KiB[=========================================================>----]94%15.8KiB/16.8KiB[=========================================================>----]94%17.6KiB/18.6KiB[==========================================================>---]95%18.9KiB/19.9KiB[==========================================================>---]95%20.0KiB/21.0KiB[==========================================================>---]95%21.0KiB/22.0KiB[==========================================================>---]95%21.3KiB/22.3KiB[==========================================================>---]96%22.6KiB/23.6KiB[==========================================================>---]96%23.9KiB/24.9KiB[===========================================================>--]96%25.7KiB/26.7KiB[===========================================================>--]96%26.1KiB/27.1KiB[===========================================================>--]96%26.8KiB/27.8KiB[===========================================================>--]96%28.0KiB/29.0KiB[===========================================================>--]97%28.4KiB/29.4KiB[===========================================================>--]97%29.6KiB/30.6KiB[===========================================================>--]97%30.0KiB/31.0KiB[===========================================================>--]97%30.4KiB/31.4KiB[===========================================================>--]97%31.8KiB/32.8KiB[===========================================================>--]97%34.4KiB/35.4KiB[===========================================================>--]97%34.5KiB/35.5KiB[===========================================================>--]97%37.0KiB/38.0KiB[===========================================================>--]97%38.5KiB/39.5KiB[===========================================================>--]97%40.4KiB/41.4KiB[============================================================>-]98%41.1KiB/42.1KiB[============================================================>-]98%42.2KiB/43.2KiB[============================================================>-]98%43.9KiB/44.9KiB[============================================================>-]98%44.9KiB/45.9KiB[============================================================>-]98%46.2KiB/47.2KiB[============================================================>-]98%46.9KiB/47.9KiB[============================================================>-]98%48.4KiB/49.4KiB[============================================================>-]98%48.7KiB/49.7KiB[============================================================>-]98%49.3KiB/50.3KiB[============================================================>-]98%50.1KiB/51.1KiB[============================================================>-]98%50.5KiB/51.5KiB[============================================================>-]98%50.6KiB/51.6KiB[============================================================>-]98%50.8KiB/51.8KiB[============================================================>-]98%51.7KiB/52.7KiB[============================================================>-]98%52.7KiB/53.7KiB[============================================================>-]98%53.6KiB/54.6KiB[============================================================>-]98%57.5KiB/58.5KiB[============================================================>-]98%58.3KiB/59.3KiB[============================================================>-]98%58.7KiB/59.7KiB[============================================================>-]98%60.1KiB/61.1KiB[============================================================>-]98%62.0KiB/63.0KiB[============================================================>-]98%63.7KiB/64.7KiB[============================================================>-]98%64.7KiB/65.7KiB[============================================================>-]98%65.2KiB/66.2KiB[============================================================>-]98%65.8KiB/66.8KiB[============================================================>-]99%66.4KiB/67.4KiB[============================================================>-]99%67.6KiB/68.6KiB[============================================================>-]99%68.5KiB/69.5KiB[============================================================>-]99%70.0KiB/71.0KiB[============================================================>-]99%70.4KiB/71.4KiB[============================================================>-]99%70.8KiB/71.8KiB[============================================================>-]99%72.3KiB/73.3KiB[============================================================>-]99%73.1KiB/74.1KiB[============================================================>-]99%74.4KiB/75.4KiB[============================================================>-]99%75.7KiB/76.7KiB[============================================================>-]99%78.2KiB/79.2KiB[============================================================>-]99%79.3KiB/80.3KiB[============================================================>-]99%80.1KiB/81.1KiB[============================================================>-]99%81.3KiB/82.3KiB[============================================================>-]99%82.3KiB/83.3KiB[============================================================>-]99%82.6KiB/83.6KiB[============================================================>-]99%84.0KiB/85.0KiB[============================================================>-]99%84.7KiB/85.7KiB[============================================================>-]99%85.9KiB/86.9KiB[============================================================>-]99%87.7KiB/88.7KiB[============================================================>-]99%88.8KiB/89.8KiB[============================================================>-]99%90.3KiB/91.3KiB[============================================================>-]99%91.6KiB/92.6KiB[============================================================>-]99%92.8KiB/93.8KiB[============================================================>-]99%93.5KiB/94.5KiB[============================================================>-]99%93.6KiB/94.6KiB[============================================================>-]99%95.1KiB/96.1KiB[============================================================>-]99%96.4KiB/97.4KiB[============================================================>-]99%97.6KiB/98.6KiB[============================================================>-]99%98.8KiB/99.8KiB[============================================================>-]99%99.7KiB/100.7KiB[============================================================>-]99%100.6KiB/101.6KiB[============================================================>-]99%100.7KiB/101.7KiB[============================================================>-]99% \ 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.gogorun-racemain.gogorun-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/singlemastergitclean-fdxgorun-racemain.gogorun-racemain.gogorun-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.gogorun-racemain.gogorun-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/complexmastergorun-racemain.gogorun-racemain.gogorun-racemain.gogorun-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) + } +}