From e28e21a9397e402a78499ceeef63dd19989be29e Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Tue, 16 Apr 2024 18:12:43 +0200 Subject: Adding upstream version 8.6.1. Signed-off-by: Daniel Baumann --- .github/workflows/golangci-lint.yml | 41 + .github/workflows/test.yml | 28 + .gitignore | 5 + CONTRIBUTING | 15 + README.md | 119 +++ UNLICENSE | 24 + _examples/.gitignore | 1 + _examples/barExtender/go.mod | 13 + _examples/barExtender/main.go | 62 ++ _examples/barExtenderRev/go.mod | 13 + _examples/barExtenderRev/main.go | 137 +++ _examples/cancel/go.mod | 13 + _examples/cancel/main.go | 54 + _examples/complex/go.mod | 18 + _examples/complex/main.go | 81 ++ _examples/decoratorsOnTop/go.mod | 13 + _examples/decoratorsOnTop/main.go | 49 + _examples/differentWidth/go.mod | 13 + _examples/differentWidth/main.go | 59 ++ _examples/dynTotal/go.mod | 13 + _examples/dynTotal/main.go | 48 + _examples/gomodtidyall.sh | 8 + _examples/io/go.mod | 13 + _examples/io/main.go | 42 + _examples/mexicanBar/go.mod | 13 + _examples/mexicanBar/main.go | 46 + _examples/multiBars/go.mod | 13 + _examples/multiBars/main.go | 54 + _examples/poplog/go.mod | 13 + _examples/poplog/main.go | 43 + _examples/progressAsWriter/go.mod | 13 + _examples/progressAsWriter/main.go | 78 ++ _examples/quietMode/go.mod | 13 + _examples/quietMode/main.go | 67 ++ _examples/remove/go.mod | 13 + _examples/remove/main.go | 55 + _examples/reverseBar/go.mod | 13 + _examples/reverseBar/main.go | 62 ++ _examples/singleBar/go.mod | 13 + _examples/singleBar/main.go | 39 + _examples/spinTipBar/go.mod | 13 + _examples/spinTipBar/main.go | 32 + _examples/spinnerBar/go.mod | 13 + _examples/spinnerBar/main.go | 66 ++ _examples/spinnerDecorator/go.mod | 13 + _examples/spinnerDecorator/main.go | 49 + _examples/stress/go.mod | 18 + _examples/stress/main.go | 62 ++ _examples/suppressBar/go.mod | 15 + _examples/suppressBar/main.go | 123 +++ _examples/tipOnComplete/go.mod | 13 + _examples/tipOnComplete/main.go | 30 + _svg/godEMrCZmJkHYH1X9dN4Nm0U7.svg | 1 + _svg/hIpTa3A5rQz65ssiVuRJu87X6.svg | 1 + _svg/wHzf1M7sd7B3zVa2scBMnjqRf.svg | 1 + bar.go | 662 ++++++++++++ bar_filler.go | 31 + bar_filler_bar.go | 291 ++++++ bar_filler_nop.go | 24 + bar_filler_spinner.go | 103 ++ bar_option.go | 200 ++++ bar_test.go | 305 ++++++ barbench_test.go | 93 ++ container_option.go | 135 +++ cwriter/cuuAndEd_construction_bench_test.go | 45 + cwriter/doc.go | 2 + cwriter/util_bsd.go | 7 + cwriter/util_linux.go | 7 + cwriter/util_solaris.go | 7 + cwriter/util_zos.go | 7 + cwriter/writer.go | 59 ++ cwriter/writer_posix.go | 48 + cwriter/writer_windows.go | 101 ++ decor/any.go | 21 + decor/counters.go | 253 +++++ decor/decorator.go | 186 ++++ decor/doc.go | 19 + decor/elapsed.go | 33 + decor/eta.go | 205 ++++ decor/meta.go | 34 + decor/moving_average.go | 74 ++ decor/name.go | 11 + decor/on_abort.go | 68 ++ decor/on_compete_or_on_abort.go | 21 + decor/on_complete.go | 67 ++ decor/on_condition.go | 51 + decor/percentage.go | 68 ++ decor/percentage_test.go | 58 ++ decor/size_type.go | 120 +++ decor/size_type_test.go | 160 +++ decor/sizeb1000_string.go | 41 + decor/sizeb1024_string.go | 41 + decor/speed.go | 179 ++++ decor/speed_test.go | 278 ++++++ decor/spinner.go | 21 + decorators_test.go | 215 ++++ doc.go | 2 + draw_test.go | 1439 +++++++++++++++++++++++++++ example_test.go | 77 ++ export_test.go | 6 + go.mod | 12 + go.sum | 11 + heap_manager.go | 173 ++++ internal/percentage.go | 19 + internal/percentage_test.go | 71 ++ internal/width.go | 10 + priority_queue.go | 37 + progress.go | 456 +++++++++ progress_test.go | 282 ++++++ proxyreader.go | 96 ++ proxyreader_test.go | 117 +++ proxywriter.go | 96 ++ proxywriter_test.go | 120 +++ 113 files changed, 9255 insertions(+) create mode 100644 .github/workflows/golangci-lint.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 CONTRIBUTING create mode 100644 README.md create mode 100644 UNLICENSE create mode 100644 _examples/.gitignore create mode 100644 _examples/barExtender/go.mod create mode 100644 _examples/barExtender/main.go create mode 100644 _examples/barExtenderRev/go.mod create mode 100644 _examples/barExtenderRev/main.go create mode 100644 _examples/cancel/go.mod create mode 100644 _examples/cancel/main.go create mode 100644 _examples/complex/go.mod create mode 100644 _examples/complex/main.go create mode 100644 _examples/decoratorsOnTop/go.mod create mode 100644 _examples/decoratorsOnTop/main.go create mode 100644 _examples/differentWidth/go.mod create mode 100644 _examples/differentWidth/main.go create mode 100644 _examples/dynTotal/go.mod create mode 100644 _examples/dynTotal/main.go create mode 100755 _examples/gomodtidyall.sh create mode 100644 _examples/io/go.mod create mode 100644 _examples/io/main.go create mode 100644 _examples/mexicanBar/go.mod create mode 100644 _examples/mexicanBar/main.go create mode 100644 _examples/multiBars/go.mod create mode 100644 _examples/multiBars/main.go create mode 100644 _examples/poplog/go.mod create mode 100644 _examples/poplog/main.go create mode 100644 _examples/progressAsWriter/go.mod create mode 100644 _examples/progressAsWriter/main.go create mode 100644 _examples/quietMode/go.mod create mode 100644 _examples/quietMode/main.go create mode 100644 _examples/remove/go.mod create mode 100644 _examples/remove/main.go create mode 100644 _examples/reverseBar/go.mod create mode 100644 _examples/reverseBar/main.go create mode 100644 _examples/singleBar/go.mod create mode 100644 _examples/singleBar/main.go create mode 100644 _examples/spinTipBar/go.mod create mode 100644 _examples/spinTipBar/main.go create mode 100644 _examples/spinnerBar/go.mod create mode 100644 _examples/spinnerBar/main.go create mode 100644 _examples/spinnerDecorator/go.mod create mode 100644 _examples/spinnerDecorator/main.go create mode 100644 _examples/stress/go.mod create mode 100644 _examples/stress/main.go create mode 100644 _examples/suppressBar/go.mod create mode 100644 _examples/suppressBar/main.go create mode 100644 _examples/tipOnComplete/go.mod create mode 100644 _examples/tipOnComplete/main.go create mode 100644 _svg/godEMrCZmJkHYH1X9dN4Nm0U7.svg create mode 100644 _svg/hIpTa3A5rQz65ssiVuRJu87X6.svg create mode 100644 _svg/wHzf1M7sd7B3zVa2scBMnjqRf.svg create mode 100644 bar.go create mode 100644 bar_filler.go create mode 100644 bar_filler_bar.go create mode 100644 bar_filler_nop.go create mode 100644 bar_filler_spinner.go create mode 100644 bar_option.go create mode 100644 bar_test.go create mode 100644 barbench_test.go create mode 100644 container_option.go create mode 100644 cwriter/cuuAndEd_construction_bench_test.go create mode 100644 cwriter/doc.go create mode 100644 cwriter/util_bsd.go create mode 100644 cwriter/util_linux.go create mode 100644 cwriter/util_solaris.go create mode 100644 cwriter/util_zos.go create mode 100644 cwriter/writer.go create mode 100644 cwriter/writer_posix.go create mode 100644 cwriter/writer_windows.go create mode 100644 decor/any.go create mode 100644 decor/counters.go create mode 100644 decor/decorator.go create mode 100644 decor/doc.go create mode 100644 decor/elapsed.go create mode 100644 decor/eta.go create mode 100644 decor/meta.go create mode 100644 decor/moving_average.go create mode 100644 decor/name.go create mode 100644 decor/on_abort.go create mode 100644 decor/on_compete_or_on_abort.go create mode 100644 decor/on_complete.go create mode 100644 decor/on_condition.go create mode 100644 decor/percentage.go create mode 100644 decor/percentage_test.go create mode 100644 decor/size_type.go create mode 100644 decor/size_type_test.go create mode 100644 decor/sizeb1000_string.go create mode 100644 decor/sizeb1024_string.go create mode 100644 decor/speed.go create mode 100644 decor/speed_test.go create mode 100644 decor/spinner.go create mode 100644 decorators_test.go create mode 100644 doc.go create mode 100644 draw_test.go create mode 100644 example_test.go create mode 100644 export_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 heap_manager.go create mode 100644 internal/percentage.go create mode 100644 internal/percentage_test.go create mode 100644 internal/width.go create mode 100644 priority_queue.go create mode 100644 progress.go create mode 100644 progress_test.go create mode 100644 proxyreader.go create mode 100644 proxyreader_test.go create mode 100644 proxywriter.go create mode 100644 proxywriter_test.go diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..c42aa0e --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,41 @@ +name: golangci-lint + +on: + push: + tags: + - v* + branches: + - master + - main + pull_request: + +permissions: + contents: read + # Optional: allow read access to pull request. Use with `only-new-issues` option. + pull-requests: read + +jobs: + golangci: + strategy: + matrix: + go-version: ['stable'] + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go-version }} + cache: false + - uses: golangci/golangci-lint-action@v3 + with: + # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. + version: latest + # Optional: working directory, useful for monorepos + # working-directory: somedir + + # Optional: golangci-lint command line arguments. + # args: --issues-exit-code=0 + + # Optional: show only new issues if it's a pull request. The default value is `false`. + only-new-issues: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ca86ad9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +name: test + +on: + push: + tags: + - v* + branches: + - master + - v* + pull_request: + +permissions: + contents: read + pull-requests: read + +jobs: + test: + strategy: + matrix: + go-version: ['stable', 'oldstable'] + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go-version }} + - run: go test -race ./... 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/CONTRIBUTING b/CONTRIBUTING new file mode 100644 index 0000000..6ca5453 --- /dev/null +++ b/CONTRIBUTING @@ -0,0 +1,15 @@ +When contributing your first changes, please include an empty commit for +copyright waiver using the following message (replace 'John Doe' with +your name or nickname): + + John Doe Copyright Waiver + + I dedicate any and all copyright interest in this software to the + public domain. I make this dedication for the benefit of the public at + large and to the detriment of my heirs and successors. I intend this + dedication to be an overt act of relinquishment in perpetuity of all + present and future rights to this software under copyright law. + +The command to create an empty commit from the command-line is: + + git commit --allow-empty diff --git a/README.md b/README.md new file mode 100644 index 0000000..09825ca --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# Multi Progress Bar + +[![GoDoc](https://pkg.go.dev/badge/github.com/vbauerster/mpb)](https://pkg.go.dev/github.com/vbauerster/mpb/v8) +[![Test status](https://github.com/vbauerster/mpb/actions/workflows/test.yml/badge.svg)](https://github.com/vbauerster/mpb/actions/workflows/test.yml) +[![Lint status](https://github.com/vbauerster/mpb/actions/workflows/golangci-lint.yml/badge.svg)](https://github.com/vbauerster/mpb/actions/workflows/golangci-lint.yml) + +**mpb** is a Go lib for rendering progress bars in terminal applications. + +## Features + +- **Multiple Bars**: Multiple progress bars are supported +- **Dynamic Total**: Set total 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 + +## Usage + +#### [Rendering single bar](_examples/singleBar/main.go) + +```go +package main + +import ( + "math/rand" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + // initialize progress container, with custom width + p := mpb.New(mpb.WithWidth(64)) + + total := 100 + name := "Single Bar:" + // create a single bar, which will inherit container's width + bar := p.New(int64(total), + // BarFillerBuilder with custom style + mpb.BarStyle().Lbound("╢").Filler("▌").Tip("▌").Padding("░").Rbound("╟"), + 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( + decor.AverageETA(decor.ET_STYLE_GO, decor.WC{W: 4}), "done", + ), + ), + mpb.AppendDecorators(decor.Percentage()), + ) + // simulating some work + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) + bar.Increment() + } + // wait for our bar to complete and flush + p.Wait() +} +``` + +#### [Rendering multiple bars](_examples/multiBars/main.go) + +```go + var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call + 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 30 + decor.EwmaETA(decor.ET_STYLE_GO, 30, decor.WCSyncWidth), "done", + ), + ), + ) + // simulating some work + go func() { + defer wg.Done() + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + // start variable is solely for EWMA calculation + // EWMA's unit of measure is an iteration's duration + start := time.Now() + time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) + } + }() + } + // wait for passed wg and for all bars to complete and flush + p.Wait() +``` + +#### [Dynamic total](_examples/dynTotal/main.go) + +![dynamic total](_svg/godEMrCZmJkHYH1X9dN4Nm0U7.svg) + +#### [Complex example](_examples/complex/main.go) + +![complex](_svg/wHzf1M7sd7B3zVa2scBMnjqRf.svg) + +#### [Bytes counters](_examples/io/main.go) + +![byte counters](_svg/hIpTa3A5rQz65ssiVuRJu87X6.svg) diff --git a/UNLICENSE b/UNLICENSE new file mode 100644 index 0000000..68a49da --- /dev/null +++ b/UNLICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/_examples/.gitignore b/_examples/.gitignore new file mode 100644 index 0000000..08cb523 --- /dev/null +++ b/_examples/.gitignore @@ -0,0 +1 @@ +go.sum diff --git a/_examples/barExtender/go.mod b/_examples/barExtender/go.mod new file mode 100644 index 0000000..dcaa9aa --- /dev/null +++ b/_examples/barExtender/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/barExtender + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.6.1 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/barExtender/main.go b/_examples/barExtender/main.go new file mode 100644 index 0000000..965925b --- /dev/null +++ b/_examples/barExtender/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "fmt" + "io" + "math/rand" + "sync" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call + 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) (err error) { + if s.Completed { + _, err = fmt.Fprintf(w, "Bar id: %d has been completed\n", s.ID) + } + return err + } + bar := p.AddBar(int64(total), + mpb.BarExtender(mpb.BarFillerFunc(efn), false), + 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 30 + decor.EwmaETA(decor.ET_STYLE_GO, 30), "done", + ), + ), + ) + // simulating some work + go func() { + defer wg.Done() + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + // start variable is solely for EWMA calculation + // EWMA's unit of measure is an iteration's duration + start := time.Now() + time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) + } + }() + } + // wait for passed wg and for all bars to complete and flush + p.Wait() +} diff --git a/_examples/barExtenderRev/go.mod b/_examples/barExtenderRev/go.mod new file mode 100644 index 0000000..f8c9ae7 --- /dev/null +++ b/_examples/barExtenderRev/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/barExtenderRev + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.6.1 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/barExtenderRev/main.go b/_examples/barExtenderRev/main.go new file mode 100644 index 0000000..aa9d534 --- /dev/null +++ b/_examples/barExtenderRev/main.go @@ -0,0 +1,137 @@ +package main + +import ( + "fmt" + "io" + "math/rand" + "sync/atomic" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +var curTask uint32 +var doneTasks uint32 + +type task struct { + id uint32 + total int64 + bar *mpb.Bar +} + +func main() { + numTasks := 4 + + var total int64 + var filler mpb.BarFiller + tasks := make([]*task, numTasks) + + for i := 0; i < numTasks; i++ { + task := &task{ + id: uint32(i), + total: rand.Int63n(666) + 100, + } + total += task.total + filler = middleware(filler, task.id) + tasks[i] = task + } + + filler = newLineMiddleware(filler) + + p := mpb.New() + + for i := 0; i < numTasks; i++ { + bar := p.AddBar(tasks[i].total, + mpb.BarExtender(filler, true), // all bars share same extender filler + mpb.BarFuncOptional(func() mpb.BarOption { + return mpb.BarQueueAfter(tasks[i-1].bar) + }, i != 0), + mpb.PrependDecorators( + decor.Name("current:", decor.WCSyncWidthR), + ), + mpb.AppendDecorators( + decor.Percentage(decor.WCSyncWidth), + ), + ) + tasks[i].bar = bar + } + + tb := p.AddBar(0, + mpb.PrependDecorators( + decor.Any(func(st decor.Statistics) string { + return fmt.Sprintf("TOTAL(%d/%d)", atomic.LoadUint32(&doneTasks), len(tasks)) + }, decor.WCSyncWidthR), + ), + mpb.AppendDecorators( + decor.Percentage(decor.WCSyncWidth), + ), + ) + + tb.SetTotal(total, false) + + for _, t := range tasks { + atomic.StoreUint32(&curTask, t.id) + complete(t.bar, tb) + atomic.AddUint32(&doneTasks, 1) + } + + tb.EnableTriggerComplete() + + p.Wait() +} + +func middleware(base mpb.BarFiller, id uint32) mpb.BarFiller { + var done bool + fn := func(w io.Writer, st decor.Statistics) error { + if !done { + if atomic.LoadUint32(&curTask) != id { + _, err := fmt.Fprintf(w, " Taksk %02d\n", id) + return err + } + if !st.Completed { + _, err := fmt.Fprintf(w, "=> Taksk %02d\n", id) + return err + } + done = true + } + _, err := fmt.Fprintf(w, " Taksk %02d: Done!\n", id) + return err + } + if base == nil { + return mpb.BarFillerFunc(fn) + } + return mpb.BarFillerFunc(func(w io.Writer, st decor.Statistics) error { + err := fn(w, st) + if err != nil { + return err + } + return base.Fill(w, st) + }) +} + +func newLineMiddleware(base mpb.BarFiller) mpb.BarFiller { + return mpb.BarFillerFunc(func(w io.Writer, st decor.Statistics) error { + _, err := fmt.Fprintln(w) + if err != nil { + return err + } + return base.Fill(w, st) + }) +} + +func complete(bar, totalBar *mpb.Bar) { + max := 100 * time.Millisecond + for !bar.Completed() { + n := rand.Int63n(10) + 1 + incrementBars(n, bar, totalBar) + time.Sleep(time.Duration(n) * max / 10) + } + bar.Wait() +} + +func incrementBars(n int64, bb ...*mpb.Bar) { + for _, b := range bb { + b.IncrInt64(n) + } +} diff --git a/_examples/cancel/go.mod b/_examples/cancel/go.mod new file mode 100644 index 0000000..4814890 --- /dev/null +++ b/_examples/cancel/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/cancel + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.6.1 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/cancel/main.go b/_examples/cancel/main.go new file mode 100644 index 0000000..a60bfe5 --- /dev/null +++ b/_examples/cancel/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "context" + "fmt" + "math/rand" + "sync" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call + p := mpb.NewWithContext(ctx, mpb.WithWaitGroup(&wg)) + total := 300 + numBars := 3 + wg.Add(numBars) + + for i := 0; i < numBars; i++ { + name := fmt.Sprintf("Bar#%02d: ", i) + bar := p.AddBar(int64(total), + mpb.PrependDecorators( + decor.Name(name, decor.WCSyncWidthR), + decor.EwmaETA(decor.ET_STYLE_GO, 30, decor.WCSyncWidth), + ), + mpb.AppendDecorators( + // note that OnComplete will not be fired, because of cancel + decor.OnComplete(decor.Percentage(decor.WC{W: 5}), "done"), + ), + ) + + go func() { + defer wg.Done() + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + max := 100 * time.Millisecond + for bar.IsRunning() { + // start variable is solely for EWMA calculation + // EWMA's unit of measure is an iteration's duration + start := time.Now() + time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) + } + }() + } + // wait for passed wg and for all bars to complete and flush + p.Wait() +} diff --git a/_examples/complex/go.mod b/_examples/complex/go.mod new file mode 100644 index 0000000..03d237e --- /dev/null +++ b/_examples/complex/go.mod @@ -0,0 +1,18 @@ +module github.com/vbauerster/mpb/_examples/complex + +go 1.17 + +require ( + github.com/fatih/color v1.15.0 + github.com/vbauerster/mpb/v8 v8.6.1 +) + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/complex/main.go b/_examples/complex/main.go new file mode 100644 index 0000000..e64f18d --- /dev/null +++ b/_examples/complex/main.go @@ -0,0 +1,81 @@ +package main + +import ( + "fmt" + "math/rand" + "time" + + "github.com/fatih/color" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + numBars := 4 + // to support color in Windows following both options are required + p := mpb.New( + mpb.WithOutput(color.Output), + mpb.WithAutoRefresh(), + ) + + red, green := color.New(color.FgRed), color.New(color.FgGreen) + + for i := 0; i < numBars; i++ { + task := fmt.Sprintf("Task#%02d:", i) + queue := make([]*mpb.Bar, 2) + queue[0] = p.AddBar(rand.Int63n(201)+100, + mpb.PrependDecorators( + decor.Name(task, decor.WC{W: len(task) + 1, C: decor.DidentRight}), + decor.Name("downloading", decor.WCSyncSpaceR), + decor.CountersNoUnit("%d / %d", decor.WCSyncWidth), + ), + mpb.AppendDecorators( + decor.OnComplete(decor.Percentage(decor.WC{W: 5}), "done"), + ), + ) + queue[1] = p.AddBar(rand.Int63n(101)+100, + mpb.BarQueueAfter(queue[0]), // this bar is queued + mpb.BarFillerClearOnComplete(), + mpb.PrependDecorators( + decor.Name(task, decor.WC{W: len(task) + 1, C: decor.DidentRight}), + decor.OnCompleteMeta( + decor.OnComplete( + decor.Meta(decor.Name("installing", decor.WCSyncSpaceR), toMetaFunc(red)), + "done!", + ), + toMetaFunc(green), + ), + decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_MMSS, 0, decor.WCSyncWidth), ""), + ), + mpb.AppendDecorators( + decor.OnComplete(decor.Percentage(decor.WC{W: 5}), ""), + ), + ) + + go func() { + for _, b := range queue { + complete(b) + } + }() + } + + p.Wait() +} + +func complete(bar *mpb.Bar) { + max := 100 * time.Millisecond + for !bar.Completed() { + // start variable is solely for EWMA calculation + // EWMA's unit of measure is an iteration's duration + start := time.Now() + time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrInt64(rand.Int63n(5)+1, time.Since(start)) + } +} + +func toMetaFunc(c *color.Color) func(string) string { + return func(s string) string { + return c.Sprint(s) + } +} diff --git a/_examples/decoratorsOnTop/go.mod b/_examples/decoratorsOnTop/go.mod new file mode 100644 index 0000000..ae8e32d --- /dev/null +++ b/_examples/decoratorsOnTop/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/decoratorsOnTop + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.6.1 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/decoratorsOnTop/main.go b/_examples/decoratorsOnTop/main.go new file mode 100644 index 0000000..f35ad82 --- /dev/null +++ b/_examples/decoratorsOnTop/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "io" + "math/rand" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + p := mpb.New() + + total := 100 + bar := p.New(int64(total), + mpb.NopStyle(), // make main bar style nop, so there are just decorators + mpb.BarExtender(extended(mpb.BarStyle().Build()), false), // extend wtih normal bar on the next line + mpb.PrependDecorators( + decor.Name("Percentage: "), + decor.NewPercentage("%d"), + ), + mpb.AppendDecorators( + decor.Name("ETA: "), + decor.OnComplete( + decor.AverageETA(decor.ET_STYLE_GO), "done", + ), + ), + ) + // simulating some work + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) + bar.Increment() + } + // wait for our bar to complete and flush + p.Wait() +} + +func extended(base mpb.BarFiller) mpb.BarFiller { + return mpb.BarFillerFunc(func(w io.Writer, st decor.Statistics) error { + err := base.Fill(w, st) + if err != nil { + return err + } + _, err = io.WriteString(w, "\n") + return err + }) +} diff --git a/_examples/differentWidth/go.mod b/_examples/differentWidth/go.mod new file mode 100644 index 0000000..6ac87a9 --- /dev/null +++ b/_examples/differentWidth/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/differentWidth + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.6.1 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/differentWidth/main.go b/_examples/differentWidth/main.go new file mode 100644 index 0000000..683876d --- /dev/null +++ b/_examples/differentWidth/main.go @@ -0,0 +1,59 @@ +package main + +import ( + "fmt" + "math/rand" + "sync" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call + p := mpb.New( + mpb.WithWaitGroup(&wg), + mpb.WithWidth(60), + ) + total, numBars := 100, 3 + wg.Add(numBars) + + for i := 0; i < numBars; i++ { + name := fmt.Sprintf("Bar#%d:", i) + bar := p.AddBar(int64(total), + // set BarWidth 40 for bar 1 and 2 + mpb.BarOptional(mpb.BarWidth(40), i > 0), + 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 30 + decor.EwmaETA(decor.ET_STYLE_GO, 30), "done", + ), + ), + ) + // simulating some work + go func() { + defer wg.Done() + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + // start variable is solely for EWMA calculation + // EWMA's unit of measure is an iteration's duration + start := time.Now() + time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) + } + }() + } + // wait for passed wg and for all bars to complete and flush + p.Wait() +} diff --git a/_examples/dynTotal/go.mod b/_examples/dynTotal/go.mod new file mode 100644 index 0000000..33622b1 --- /dev/null +++ b/_examples/dynTotal/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/dynTotal + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.6.1 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/dynTotal/main.go b/_examples/dynTotal/main.go new file mode 100644 index 0000000..212ef13 --- /dev/null +++ b/_examples/dynTotal/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "io" + "math/rand" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + p := mpb.New(mpb.WithWidth(64)) + + // new bar with 'trigger complete event' disabled, because total is zero + bar := p.AddBar(0, + mpb.PrependDecorators(decor.Counters(decor.SizeB1024(0), "% .1f / % .1f")), + mpb.AppendDecorators(decor.Percentage()), + ) + + maxSleep := 100 * time.Millisecond + read := makeStream(200) + for { + n, err := read() + if err == io.EOF { + // triggering complete event now + bar.SetTotal(-1, true) + break + } + // increment methods won't trigger complete event because bar was constructed with total = 0 + bar.IncrBy(n) + // following call is not required, it's called to show some progress instead of an empty bar + bar.SetTotal(bar.Current()+2048, false) + time.Sleep(time.Duration(rand.Intn(10)+1) * maxSleep / 10) + } + + 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/gomodtidyall.sh b/_examples/gomodtidyall.sh new file mode 100755 index 0000000..a9e6100 --- /dev/null +++ b/_examples/gomodtidyall.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +for d in *; do + [ ! -d "$d" ] && continue + pushd "$d" >/dev/null 2>&1 + go mod tidy + popd >/dev/null 2>&1 +done diff --git a/_examples/io/go.mod b/_examples/io/go.mod new file mode 100644 index 0000000..8b0e0d4 --- /dev/null +++ b/_examples/io/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/io + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.6.1 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/io/main.go b/_examples/io/main.go new file mode 100644 index 0000000..252a967 --- /dev/null +++ b/_examples/io/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "crypto/rand" + "io" + "io/ioutil" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + var total int64 = 1024 * 1024 * 500 + reader := io.LimitReader(rand.Reader, total) + + p := mpb.New( + mpb.WithWidth(60), + mpb.WithRefreshRate(180*time.Millisecond), + ) + + bar := p.New(total, + mpb.BarStyle().Rbound("|"), + mpb.PrependDecorators( + decor.Counters(decor.SizeB1024(0), "% .2f / % .2f"), + ), + mpb.AppendDecorators( + decor.EwmaETA(decor.ET_STYLE_GO, 30), + decor.Name(" ] "), + decor.EwmaSpeed(decor.SizeB1024(0), "% .2f", 30), + ), + ) + + // create proxy reader + proxyReader := bar.ProxyReader(reader) + defer proxyReader.Close() + + // copy from proxyReader, ignoring errors + _, _ = io.Copy(ioutil.Discard, proxyReader) + + p.Wait() +} diff --git a/_examples/mexicanBar/go.mod b/_examples/mexicanBar/go.mod new file mode 100644 index 0000000..681c2c0 --- /dev/null +++ b/_examples/mexicanBar/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/mexicanBar + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.6.1 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/mexicanBar/main.go b/_examples/mexicanBar/main.go new file mode 100644 index 0000000..8c2ecae --- /dev/null +++ b/_examples/mexicanBar/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "math/rand" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + // initialize progress container, with custom width + p := mpb.New(mpb.WithWidth(80)) + + total := 100 + name := "Complex Filler:" + bs := mpb.BarStyle() + bs = bs.LboundMeta(func(s string) string { + return "\033[34m" + s + "\033[0m" // blue + }) + bs = bs.Filler("_").FillerMeta(func(s string) string { + return "\033[36m" + s + "\033[0m" // cyan + }) + bs = bs.Tip("⛵").TipMeta(func(s string) string { + return "\033[31m" + s + "\033[0m" // red + }) + bs = bs.TipOnComplete() // leave tip on complete + bs = bs.Padding("_").PaddingMeta(func(s string) string { + return "\033[36m" + s + "\033[0m" // cyan + }) + bs = bs.RboundMeta(func(s string) string { + return "\033[34m" + s + "\033[0m" // blue + }) + bar := p.New(int64(total), bs, + mpb.PrependDecorators(decor.Name(name)), + mpb.AppendDecorators(decor.Percentage()), + ) + // simulating some work + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) + bar.Increment() + } + // wait for our bar to complete + p.Wait() +} diff --git a/_examples/multiBars/go.mod b/_examples/multiBars/go.mod new file mode 100644 index 0000000..00a0b10 --- /dev/null +++ b/_examples/multiBars/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/multiBars + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.6.1 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/multiBars/main.go b/_examples/multiBars/main.go new file mode 100644 index 0000000..3d42d70 --- /dev/null +++ b/_examples/multiBars/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "fmt" + "math/rand" + "sync" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call + 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 30 + decor.EwmaETA(decor.ET_STYLE_GO, 30, decor.WCSyncWidth), "done", + ), + ), + ) + // simulating some work + go func() { + defer wg.Done() + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + // start variable is solely for EWMA calculation + // EWMA's unit of measure is an iteration's duration + start := time.Now() + time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) + } + }() + } + // wait for passed wg and for all bars to complete and flush + p.Wait() +} diff --git a/_examples/poplog/go.mod b/_examples/poplog/go.mod new file mode 100644 index 0000000..dbf2523 --- /dev/null +++ b/_examples/poplog/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/poplog + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.6.1 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/poplog/main.go b/_examples/poplog/main.go new file mode 100644 index 0000000..ff875c9 --- /dev/null +++ b/_examples/poplog/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + "math/rand" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + p := mpb.New(mpb.PopCompletedMode()) + total, numBars := 100, 4 + for i := 0; i < numBars; i++ { + name := fmt.Sprintf("Bar#%d:", i) + bar := p.AddBar(int64(total), + mpb.BarFillerOnComplete(fmt.Sprintf("%s has been completed", name)), + mpb.BarFillerTrim(), + mpb.PrependDecorators( + decor.OnComplete(decor.Name(name), ""), + decor.OnComplete(decor.NewPercentage(" % d "), ""), + ), + mpb.AppendDecorators( + decor.OnComplete(decor.Name(" "), ""), + decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_GO, 60), ""), + ), + ) + // simulating some work + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + // start variable is solely for EWMA calculation + // EWMA's unit of measure is an iteration's duration + start := time.Now() + time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) + } + } + + p.Wait() +} diff --git a/_examples/progressAsWriter/go.mod b/_examples/progressAsWriter/go.mod new file mode 100644 index 0000000..a7b985f --- /dev/null +++ b/_examples/progressAsWriter/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/progressAsWriter + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.6.1 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/progressAsWriter/main.go b/_examples/progressAsWriter/main.go new file mode 100644 index 0000000..61e819b --- /dev/null +++ b/_examples/progressAsWriter/main.go @@ -0,0 +1,78 @@ +package main + +import ( + "fmt" + "log" + "math/rand" + "sync" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + total, numBars := 100, 2 + var wg sync.WaitGroup + wg.Add(numBars) + done := make(chan interface{}) + p := mpb.New( + mpb.WithWidth(64), + mpb.WithWaitGroup(&wg), + mpb.WithShutdownNotifier(done), + ) + + log.SetOutput(p) + + for i := 0; i < numBars; i++ { + name := fmt.Sprintf("Bar#%d:", i) + bar := p.AddBar(int64(total), + mpb.PrependDecorators( + decor.Name(name), + decor.Percentage(decor.WCSyncSpace), + ), + mpb.AppendDecorators( + decor.OnComplete( + decor.EwmaETA(decor.ET_STYLE_GO, 30, decor.WCSyncWidth), "done", + ), + ), + ) + // simulating some work + go func() { + defer wg.Done() + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + // start variable is solely for EWMA calculation + // EWMA's unit of measure is an iteration's duration + start := time.Now() + time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) + } + log.Println(name, "done") + }() + } + + var qwg sync.WaitGroup + qwg.Add(1) + go func() { + quit: + for { + select { + case <-done: + // after done, underlying io.Writer returns mpb.DoneError + // so following isn't printed + log.Println("all done") + break quit + default: + log.Println("waiting for done") + time.Sleep(100 * time.Millisecond) + } + } + qwg.Done() + }() + + p.Wait() + qwg.Wait() +} diff --git a/_examples/quietMode/go.mod b/_examples/quietMode/go.mod new file mode 100644 index 0000000..6a42347 --- /dev/null +++ b/_examples/quietMode/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/quietMode + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.6.1 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/quietMode/main.go b/_examples/quietMode/main.go new file mode 100644 index 0000000..315fc3c --- /dev/null +++ b/_examples/quietMode/main.go @@ -0,0 +1,67 @@ +package main + +import ( + "flag" + "fmt" + "io" + "math/rand" + "sync" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +var quietMode bool + +func init() { + flag.BoolVar(&quietMode, "q", false, "quiet mode") +} + +func main() { + flag.Parse() + var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call + p := mpb.New( + mpb.WithWaitGroup(&wg), + mpb.ContainerOptional(mpb.WithOutput(io.Discard), quietMode), + ) + 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 30 + decor.EwmaETA(decor.ET_STYLE_GO, 30), "done", + ), + ), + ) + // simulating some work + go func() { + defer wg.Done() + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + // start variable is solely for EWMA calculation + // EWMA's unit of measure is an iteration's duration + start := time.Now() + time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) + } + }() + } + // wait for passed wg and for all bars to complete and flush + p.Wait() + fmt.Println("done") +} diff --git a/_examples/remove/go.mod b/_examples/remove/go.mod new file mode 100644 index 0000000..988c866 --- /dev/null +++ b/_examples/remove/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/remove + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.6.1 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/remove/main.go b/_examples/remove/main.go new file mode 100644 index 0000000..e9894ed --- /dev/null +++ b/_examples/remove/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "fmt" + "math/rand" + "sync" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call + 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) + bar := p.AddBar(int64(total), + mpb.BarID(i), + mpb.BarOptional(mpb.BarRemoveOnComplete(), i == 0), + mpb.PrependDecorators( + decor.Name(name), + ), + mpb.AppendDecorators( + decor.Any(func(s decor.Statistics) string { + return fmt.Sprintf("completed: %v", s.Completed) + }, decor.WCSyncSpaceR), + decor.Any(func(s decor.Statistics) string { + return fmt.Sprintf("aborted: %v", s.Aborted) + }, decor.WCSyncSpaceR), + decor.OnComplete(decor.NewPercentage("%d", decor.WCSyncSpace), "done"), + decor.OnAbort(decor.NewPercentage("%d", decor.WCSyncSpace), "ohno"), + ), + ) + go func() { + defer wg.Done() + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + max := 100 * time.Millisecond + for i := 0; bar.IsRunning(); i++ { + if bar.ID() == 2 && i >= 42 { + go bar.Abort(false) + } + time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) + bar.Increment() + } + }() + } + // wait for passed wg and for all bars to complete and flush + p.Wait() +} diff --git a/_examples/reverseBar/go.mod b/_examples/reverseBar/go.mod new file mode 100644 index 0000000..b062f40 --- /dev/null +++ b/_examples/reverseBar/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/reverseBar + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.6.1 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/reverseBar/main.go b/_examples/reverseBar/main.go new file mode 100644 index 0000000..9ff3c74 --- /dev/null +++ b/_examples/reverseBar/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "fmt" + "math/rand" + "sync" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call + p := mpb.New(mpb.WithWaitGroup(&wg)) + total, numBars := 100, 3 + wg.Add(numBars) + + condFillerBuilder := func(cond bool) mpb.BarFillerBuilder { + if cond { // reverse Bar on cond + return mpb.BarStyle().Tip("<").Reverse() + } + return mpb.BarStyle() + } + + for i := 0; i < numBars; i++ { + name := fmt.Sprintf("Bar#%d:", i) + bar := p.New(int64(total), + condFillerBuilder(i == 1), + 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 30 + decor.EwmaETA(decor.ET_STYLE_GO, 30), "done", + ), + ), + ) + // simulating some work + go func() { + defer wg.Done() + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + // start variable is solely for EWMA calculation + // EWMA's unit of measure is an iteration's duration + start := time.Now() + time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) + } + }() + } + // wait for passed wg and for all bars to complete and flush + p.Wait() +} diff --git a/_examples/singleBar/go.mod b/_examples/singleBar/go.mod new file mode 100644 index 0000000..e15d831 --- /dev/null +++ b/_examples/singleBar/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/singleBar + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.6.1 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/singleBar/main.go b/_examples/singleBar/main.go new file mode 100644 index 0000000..a7c2a1b --- /dev/null +++ b/_examples/singleBar/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "math/rand" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + // initialize progress container, with custom width + p := mpb.New(mpb.WithWidth(64)) + + total := 100 + name := "Single Bar:" + // create a single bar, which will inherit container's width + bar := p.New(int64(total), + // BarFillerBuilder with custom style + mpb.BarStyle().Lbound("╢").Filler("▌").Tip("▌").Padding("░").Rbound("╟"), + 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( + decor.AverageETA(decor.ET_STYLE_GO, decor.WC{W: 4}), "done", + ), + ), + mpb.AppendDecorators(decor.Percentage()), + ) + // simulating some work + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) + bar.Increment() + } + // wait for our bar to complete and flush + p.Wait() +} diff --git a/_examples/spinTipBar/go.mod b/_examples/spinTipBar/go.mod new file mode 100644 index 0000000..334d1fc --- /dev/null +++ b/_examples/spinTipBar/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/spinTipBar + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.6.1 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/spinTipBar/main.go b/_examples/spinTipBar/main.go new file mode 100644 index 0000000..62eab3f --- /dev/null +++ b/_examples/spinTipBar/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "math/rand" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + // initialize progress container, with custom width + p := mpb.New(mpb.WithWidth(80)) + + total := 100 + name := "Single Bar:" + bar := p.New(int64(total), + mpb.BarStyle().Tip(`-`, `\`, `|`, `/`).TipMeta(func(s string) string { + return "\033[31m" + s + "\033[0m" // red + }), + mpb.PrependDecorators(decor.Name(name)), + mpb.AppendDecorators(decor.Percentage()), + ) + // simulating some work + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) + bar.Increment() + } + // wait for our bar to complete and flush + p.Wait() +} diff --git a/_examples/spinnerBar/go.mod b/_examples/spinnerBar/go.mod new file mode 100644 index 0000000..b5722de --- /dev/null +++ b/_examples/spinnerBar/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/spinnerBar + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.6.1 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/spinnerBar/main.go b/_examples/spinnerBar/main.go new file mode 100644 index 0000000..c564452 --- /dev/null +++ b/_examples/spinnerBar/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "fmt" + "math/rand" + "sync" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call + p := mpb.New( + mpb.WithWaitGroup(&wg), + mpb.WithWidth(16), + ) + total, numBars := 101, 3 + wg.Add(numBars) + + condFillerBuilder := func(cond bool) mpb.BarFillerBuilder { + if cond { + s := mpb.SpinnerStyle("∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙") + return s.Meta(func(s string) string { + return "\033[31m" + s + "\033[0m" // red + }) + } + return mpb.BarStyle().Lbound("╢").Filler("▌").Tip("▌").Padding("░").Rbound("╟") + } + + for i := 0; i < numBars; i++ { + name := fmt.Sprintf("Bar#%d:", i) + bar := p.New(int64(total), + condFillerBuilder(i != 0), + mpb.PrependDecorators( + // simple name decorator + decor.Name(name), + ), + mpb.AppendDecorators( + // replace ETA decorator with "done" message, OnComplete event + decor.OnComplete( + // ETA decorator with ewma age of 30 + decor.EwmaETA(decor.ET_STYLE_GO, 30), "done", + ), + ), + ) + // simulating some work + go func() { + defer wg.Done() + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + // start variable is solely for EWMA calculation + // EWMA's unit of measure is an iteration's duration + start := time.Now() + time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) + } + }() + } + // wait for passed wg and for all bars to complete and flush + p.Wait() +} diff --git a/_examples/spinnerDecorator/go.mod b/_examples/spinnerDecorator/go.mod new file mode 100644 index 0000000..d239911 --- /dev/null +++ b/_examples/spinnerDecorator/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/spinnerDecorator + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.6.1 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/spinnerDecorator/main.go b/_examples/spinnerDecorator/main.go new file mode 100644 index 0000000..d4a1ac7 --- /dev/null +++ b/_examples/spinnerDecorator/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" + "math/rand" + "sync" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call + p := mpb.New(mpb.WithWaitGroup(&wg), mpb.WithWidth(64)) + 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.OnComplete( + // spinner decorator with default style + decor.Spinner(nil, decor.WCSyncSpace), "done", + ), + ), + mpb.AppendDecorators( + // decor.DSyncWidth bit enables column width synchronization + decor.Percentage(decor.WCSyncWidth), + ), + ) + // simulating some work + go func() { + defer wg.Done() + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) + bar.Increment() + } + }() + } + // wait for passed wg and for all bars to complete and flush + p.Wait() +} diff --git a/_examples/stress/go.mod b/_examples/stress/go.mod new file mode 100644 index 0000000..0541360 --- /dev/null +++ b/_examples/stress/go.mod @@ -0,0 +1,18 @@ +module github.com/vbauerster/mpb/_examples/stress + +go 1.17 + +require ( + github.com/pkg/profile v1.7.0 + github.com/vbauerster/mpb/v8 v8.6.1 +) + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/felixge/fgprof v0.9.3 // indirect + github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/stress/main.go b/_examples/stress/main.go new file mode 100644 index 0000000..04cf476 --- /dev/null +++ b/_examples/stress/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "flag" + "fmt" + "math/rand" + "os" + "sync" + "time" + + "github.com/pkg/profile" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +const ( + totalBars = 32 +) + +var proftype = flag.String("prof", "", "profile type (cpu, mem)") + +func main() { + flag.Parse() + switch *proftype { + case "cpu": + defer profile.Start(profile.CPUProfile, profile.ProfilePath("."), profile.NoShutdownHook).Stop() + case "mem": + defer profile.Start(profile.MemProfile, profile.ProfilePath("."), profile.NoShutdownHook).Stop() + } + var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call + p := mpb.New(mpb.WithWaitGroup(&wg), mpb.WithDebugOutput(os.Stderr)) + 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.WCSyncWidthR), + decor.Elapsed(decor.ET_STYLE_GO, decor.WCSyncWidth), + ), + mpb.AppendDecorators( + decor.OnComplete( + decor.Percentage(decor.WC{W: 5}), "done", + ), + ), + ) + + go func() { + defer wg.Done() + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + max := 100 * time.Millisecond + for !bar.Completed() { + time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) + bar.Increment() + } + }() + } + // wait for passed wg and for all bars to complete and flush + p.Wait() +} diff --git a/_examples/suppressBar/go.mod b/_examples/suppressBar/go.mod new file mode 100644 index 0000000..c7fd7ee --- /dev/null +++ b/_examples/suppressBar/go.mod @@ -0,0 +1,15 @@ +module github.com/vbauerster/mpb/_examples/suppressBar + +go 1.17 + +require ( + github.com/mattn/go-runewidth v0.0.15 + github.com/vbauerster/mpb/v8 v8.6.1 +) + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/suppressBar/main.go b/_examples/suppressBar/main.go new file mode 100644 index 0000000..6f68a37 --- /dev/null +++ b/_examples/suppressBar/main.go @@ -0,0 +1,123 @@ +package main + +import ( + "errors" + "fmt" + "io" + "math/rand" + "sync" + "time" + + "github.com/mattn/go-runewidth" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + p := mpb.New() + + total := 100 + msgCh := make(chan string) + resumeCh := make(chan struct{}) + nextCh := make(chan struct{}, 1) + ew := &errorWrapper{} + timer := time.AfterFunc(2*time.Second, func() { + ew.reset(errors.New("timeout")) + }) + defer timer.Stop() + bar := p.AddBar(int64(total), + mpb.BarFillerMiddleware(func(base mpb.BarFiller) mpb.BarFiller { + var msg *string + var times int + return mpb.BarFillerFunc(func(w io.Writer, st decor.Statistics) error { + if msg == nil { + select { + case m := <-msgCh: + msg = &m + times = 10 + nextCh <- struct{}{} + default: + } + return base.Fill(w, st) + } + switch { + case times == 0, st.Completed, st.Aborted: + defer func() { + msg = nil + }() + resumeCh <- struct{}{} + default: + times-- + } + _, err := io.WriteString(w, runewidth.Truncate(*msg, st.AvailableWidth, "…")) + nextCh <- struct{}{} + return err + }) + }), + mpb.PrependDecorators(decor.Name("my bar:")), + mpb.AppendDecorators(newCustomPercentage(nextCh)), + ) + // simulating some work + go func() { + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) + if ew.isErr() { + msgCh <- fmt.Sprintf("%s at %d, retrying...", ew.Error(), i) + i-- + bar.SetRefill(int64(i)) + ew.reset(nil) + <-resumeCh + continue + } + bar.Increment() + } + }() + + p.Wait() +} + +type errorWrapper struct { + sync.RWMutex + err error +} + +func (ew *errorWrapper) Error() string { + ew.RLock() + defer ew.RUnlock() + return ew.err.Error() +} + +func (ew *errorWrapper) isErr() bool { + ew.RLock() + defer ew.RUnlock() + return ew.err != nil +} + +func (ew *errorWrapper) reset(err error) { + ew.Lock() + ew.err = err + ew.Unlock() +} + +type percentage struct { + decor.Decorator + suspend <-chan struct{} +} + +func (d percentage) Decor(s decor.Statistics) (string, int) { + select { + case <-d.suspend: + return d.Format("") + default: + return d.Decorator.Decor(s) + } +} + +func newCustomPercentage(nextCh <-chan struct{}) decor.Decorator { + return percentage{ + Decorator: decor.Percentage(), + suspend: nextCh, + } +} diff --git a/_examples/tipOnComplete/go.mod b/_examples/tipOnComplete/go.mod new file mode 100644 index 0000000..c164b7e --- /dev/null +++ b/_examples/tipOnComplete/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/tipOnComplete + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.6.1 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/_examples/tipOnComplete/main.go b/_examples/tipOnComplete/main.go new file mode 100644 index 0000000..852240e --- /dev/null +++ b/_examples/tipOnComplete/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "math/rand" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + // initialize progress container, with custom width + p := mpb.New(mpb.WithWidth(80)) + + total := 100 + name := "Single Bar:" + bar := p.New(int64(total), + mpb.BarStyle().TipOnComplete(), + mpb.PrependDecorators(decor.Name(name)), + mpb.AppendDecorators(decor.Percentage()), + ) + // simulating some work + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) + bar.Increment() + } + // wait for our bar to complete and flush + p.Wait() +} diff --git a/_svg/godEMrCZmJkHYH1X9dN4Nm0U7.svg b/_svg/godEMrCZmJkHYH1X9dN4Nm0U7.svg new file mode 100644 index 0000000..f794408 --- /dev/null +++ b/_svg/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/_svg/hIpTa3A5rQz65ssiVuRJu87X6.svg b/_svg/hIpTa3A5rQz65ssiVuRJu87X6.svg new file mode 100644 index 0000000..163921c --- /dev/null +++ b/_svg/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/_svg/wHzf1M7sd7B3zVa2scBMnjqRf.svg b/_svg/wHzf1M7sd7B3zVa2scBMnjqRf.svg new file mode 100644 index 0000000..9e3b871 --- /dev/null +++ b/_svg/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/bar.go b/bar.go new file mode 100644 index 0000000..7d83268 --- /dev/null +++ b/bar.go @@ -0,0 +1,662 @@ +package mpb + +import ( + "bytes" + "context" + "io" + "strings" + "sync" + "time" + + "github.com/acarl005/stripansi" + "github.com/mattn/go-runewidth" + "github.com/vbauerster/mpb/v8/decor" +) + +// Bar represents a progress bar. +type Bar struct { + index int // used by heap + priority int // used by heap + frameCh chan *renderFrame + operateState chan func(*bState) + done chan struct{} + container *Progress + bs *bState + cancel func() +} + +type syncTable [2][]chan int +type extenderFunc func([]io.Reader, decor.Statistics) ([]io.Reader, error) + +// bState is actual bar's state. +type bState struct { + id int + priority int + reqWidth int + shutdown int + total int64 + current int64 + refill int64 + trimSpace bool + completed bool + aborted bool + triggerComplete bool + rmOnComplete bool + noPop bool + autoRefresh bool + aDecorators []decor.Decorator + pDecorators []decor.Decorator + averageDecorators []decor.AverageDecorator + ewmaDecorators []decor.EwmaDecorator + shutdownListeners []decor.ShutdownListener + buffers [3]*bytes.Buffer + filler BarFiller + extender extenderFunc + renderReq chan<- time.Time + waitBar *Bar // key for (*pState).queueBars +} + +type renderFrame struct { + rows []io.Reader + shutdown int + rmOnComplete bool + noPop bool + done bool + err error +} + +func newBar(ctx context.Context, container *Progress, bs *bState) *Bar { + ctx, cancel := context.WithCancel(ctx) + + bar := &Bar{ + priority: bs.priority, + frameCh: make(chan *renderFrame, 1), + operateState: make(chan func(*bState)), + done: make(chan struct{}), + container: container, + cancel: cancel, + } + + container.bwg.Add(1) + go bar.serve(ctx, bs) + return bar +} + +// ProxyReader wraps io.Reader with metrics required for progress tracking. +// If `r` is 'unknown total/size' reader it's mandatory to call +// (*Bar).SetTotal(-1, true) method after (io.Reader).Read returns io.EOF. +// If bar is already completed or aborted, returns nil. +// Panics if `r` is nil. +func (b *Bar) ProxyReader(r io.Reader) io.ReadCloser { + if r == nil { + panic("expected non nil io.Reader") + } + result := make(chan bool) + select { + case b.operateState <- func(s *bState) { result <- len(s.ewmaDecorators) != 0 }: + return newProxyReader(r, b, <-result) + case <-b.done: + return nil + } +} + +// ProxyWriter wraps io.Writer with metrics required for progress tracking. +// If bar is already completed or aborted, returns nil. +// Panics if `w` is nil. +func (b *Bar) ProxyWriter(w io.Writer) io.WriteCloser { + if w == nil { + panic("expected non nil io.Writer") + } + result := make(chan bool) + select { + case b.operateState <- func(s *bState) { result <- len(s.ewmaDecorators) != 0 }: + return newProxyWriter(w, b, <-result) + case <-b.done: + return nil + } +} + +// ID returs id of the bar. +func (b *Bar) ID() int { + result := make(chan int) + select { + case b.operateState <- func(s *bState) { result <- s.id }: + return <-result + case <-b.done: + return b.bs.id + } +} + +// Current returns bar's current value, in other words sum of all increments. +func (b *Bar) Current() int64 { + result := make(chan int64) + select { + case b.operateState <- func(s *bState) { result <- s.current }: + return <-result + case <-b.done: + return b.bs.current + } +} + +// SetRefill sets refill flag with specified amount. +// The underlying BarFiller will change its visual representation, to +// indicate refill event. Refill event may be referred to some retry +// operation for example. +func (b *Bar) SetRefill(amount int64) { + select { + case b.operateState <- func(s *bState) { + if amount < s.current { + s.refill = amount + } else { + s.refill = s.current + } + }: + case <-b.done: + } +} + +// TraverseDecorators traverses all available decorators and calls cb func on each. +func (b *Bar) TraverseDecorators(cb func(decor.Decorator)) { + iter := make(chan decor.Decorator) + select { + case b.operateState <- func(s *bState) { + for _, decorators := range [][]decor.Decorator{ + s.pDecorators, + s.aDecorators, + } { + for _, d := range decorators { + iter <- d + } + } + close(iter) + }: + for d := range iter { + cb(unwrap(d)) + } + case <-b.done: + } +} + +// EnableTriggerComplete enables triggering complete event. It's +// effective only for bars which were constructed with `total <= 0` and +// after total has been set with (*Bar).SetTotal(int64, false). If bar +// has been incremented to the total, complete event is triggered right +// away. +func (b *Bar) EnableTriggerComplete() { + select { + case b.operateState <- func(s *bState) { + if s.triggerComplete || s.total <= 0 { + return + } + if s.current >= s.total { + s.current = s.total + s.completed = true + b.triggerCompletion(s) + } else { + s.triggerComplete = true + } + }: + case <-b.done: + } +} + +// SetTotal sets total to an arbitrary value. It's effective only for +// bar which was constructed with `total <= 0`. Setting total to negative +// value is equivalent to (*Bar).SetTotal((*Bar).Current(), bool) but faster. +// If triggerCompletion is true, total value is set to current and +// complete event is triggered right away. +func (b *Bar) SetTotal(total int64, triggerCompletion bool) { + select { + case b.operateState <- func(s *bState) { + if s.triggerComplete { + return + } + if total < 0 { + s.total = s.current + } else { + s.total = total + } + if triggerCompletion { + s.current = s.total + s.completed = true + b.triggerCompletion(s) + } + }: + case <-b.done: + } +} + +// SetCurrent sets progress' current to an arbitrary value. +func (b *Bar) SetCurrent(current int64) { + if current < 0 { + return + } + select { + case b.operateState <- func(s *bState) { + s.current = current + if s.triggerComplete && s.current >= s.total { + s.current = s.total + s.completed = true + b.triggerCompletion(s) + } + }: + case <-b.done: + } +} + +// EwmaSetCurrent sets progress' current to an arbitrary value and updates +// EWMA based decorators by dur of a single iteration. +func (b *Bar) EwmaSetCurrent(current int64, iterDur time.Duration) { + if current < 0 { + return + } + select { + case b.operateState <- func(s *bState) { + if n := current - s.current; n > 0 { + s.decoratorEwmaUpdate(n, iterDur) + } + s.current = current + if s.triggerComplete && s.current >= s.total { + s.current = s.total + s.completed = true + b.triggerCompletion(s) + } + }: + case <-b.done: + } +} + +// Increment is a shorthand for b.IncrInt64(1). +func (b *Bar) Increment() { + b.IncrInt64(1) +} + +// IncrBy is a shorthand for b.IncrInt64(int64(n)). +func (b *Bar) IncrBy(n int) { + b.IncrInt64(int64(n)) +} + +// IncrInt64 increments progress by amount of n. +func (b *Bar) IncrInt64(n int64) { + if n <= 0 { + return + } + select { + case b.operateState <- func(s *bState) { + s.current += n + if s.triggerComplete && s.current >= s.total { + s.current = s.total + s.completed = true + b.triggerCompletion(s) + } + }: + case <-b.done: + } +} + +// EwmaIncrement is a shorthand for b.EwmaIncrInt64(1, iterDur). +func (b *Bar) EwmaIncrement(iterDur time.Duration) { + b.EwmaIncrInt64(1, iterDur) +} + +// EwmaIncrBy is a shorthand for b.EwmaIncrInt64(int64(n), iterDur). +func (b *Bar) EwmaIncrBy(n int, iterDur time.Duration) { + b.EwmaIncrInt64(int64(n), iterDur) +} + +// EwmaIncrInt64 increments progress by amount of n and updates EWMA based +// decorators by dur of a single iteration. +func (b *Bar) EwmaIncrInt64(n int64, iterDur time.Duration) { + if n <= 0 { + return + } + select { + case b.operateState <- func(s *bState) { + s.decoratorEwmaUpdate(n, iterDur) + s.current += n + if s.triggerComplete && s.current >= s.total { + s.current = s.total + s.completed = true + b.triggerCompletion(s) + } + }: + case <-b.done: + } +} + +// DecoratorAverageAdjust adjusts all average based decorators. Call +// if you need to adjust start time of all average based decorators +// or after progress resume. +func (b *Bar) DecoratorAverageAdjust(start time.Time) { + select { + case b.operateState <- func(s *bState) { s.decoratorAverageAdjust(start) }: + case <-b.done: + } +} + +// SetPriority changes bar's order among multiple bars. Zero is highest +// priority, i.e. bar will be on top. If you don't need to set priority +// dynamically, better use BarPriority option. +func (b *Bar) SetPriority(priority int) { + b.container.UpdateBarPriority(b, priority, false) +} + +// Abort interrupts bar's running goroutine. Abort won't be engaged +// if bar is already in complete state. If drop is true bar will be +// removed as well. To make sure that bar has been removed call +// (*Bar).Wait method. +func (b *Bar) Abort(drop bool) { + select { + case b.operateState <- func(s *bState) { + if s.completed || s.aborted { + return + } + s.aborted = true + s.rmOnComplete = drop + b.triggerCompletion(s) + }: + case <-b.done: + } +} + +// Aborted reports whether the bar is in aborted state. +func (b *Bar) Aborted() bool { + result := make(chan bool) + select { + case b.operateState <- func(s *bState) { result <- s.aborted }: + return <-result + case <-b.done: + return b.bs.aborted + } +} + +// Completed reports whether the bar is in completed state. +func (b *Bar) Completed() bool { + result := make(chan bool) + select { + case b.operateState <- func(s *bState) { result <- s.completed }: + return <-result + case <-b.done: + return b.bs.completed + } +} + +// IsRunning reports whether the bar is running, i.e. not yet completed +// and not yet aborted. +func (b *Bar) IsRunning() bool { + result := make(chan bool) + select { + case b.operateState <- func(s *bState) { result <- !s.completed && !s.aborted }: + return <-result + case <-b.done: + return false + } +} + +// Wait blocks until bar is completed or aborted. +func (b *Bar) Wait() { + <-b.done +} + +func (b *Bar) serve(ctx context.Context, bs *bState) { + defer b.container.bwg.Done() + for { + select { + case op := <-b.operateState: + op(bs) + case <-ctx.Done(): + bs.aborted = !bs.completed + bs.decoratorShutdownNotify() + b.bs = bs + close(b.done) + return + } + } +} + +func (b *Bar) render(tw int) { + var done bool + fn := func(s *bState) { + var rows []io.Reader + stat := newStatistics(tw, s) + r, err := s.draw(stat) + if err != nil { + b.frameCh <- &renderFrame{err: err} + return + } + rows = append(rows, r) + if s.extender != nil { + rows, err = s.extender(rows, stat) + if err != nil { + b.frameCh <- &renderFrame{err: err} + return + } + } + frame := &renderFrame{ + rows: rows, + shutdown: s.shutdown, + rmOnComplete: s.rmOnComplete, + noPop: s.noPop, + done: done, + } + if s.completed || s.aborted { + // post increment makes sure OnComplete decorators are rendered + s.shutdown++ + } + b.frameCh <- frame + } + select { + case b.operateState <- fn: + case <-b.done: + done = true + fn(b.bs) + } +} + +func (b *Bar) triggerCompletion(s *bState) { + if s.autoRefresh { + // Technically this call isn't required, but if refresh rate is set to + // one hour for example and bar completes within a few minutes p.Wait() + // will wait for one hour. This call helps to avoid unnecessary waiting. + go b.tryEarlyRefresh(s.renderReq) + } else { + b.cancel() + } +} + +func (b *Bar) tryEarlyRefresh(renderReq chan<- time.Time) { + var anyOtherRunning bool + b.container.traverseBars(func(bar *Bar) bool { + anyOtherRunning = b != bar && bar.IsRunning() + return anyOtherRunning + }) + if !anyOtherRunning { + for { + select { + case renderReq <- time.Now(): + case <-b.done: + return + } + } + } +} + +func (b *Bar) wSyncTable() syncTable { + result := make(chan syncTable) + select { + case b.operateState <- func(s *bState) { result <- s.wSyncTable() }: + return <-result + case <-b.done: + return b.bs.wSyncTable() + } +} + +func (s *bState) draw(stat decor.Statistics) (io.Reader, error) { + r, err := s.drawImpl(stat) + if err != nil { + for _, b := range s.buffers { + b.Reset() + } + return nil, err + } + return io.MultiReader(r, strings.NewReader("\n")), nil +} + +func (s *bState) drawImpl(stat decor.Statistics) (io.Reader, error) { + decorFiller := func(buf *bytes.Buffer, decorators []decor.Decorator) (err error) { + for _, d := range decorators { + // need to call Decor in any case becase of width synchronization + str, width := d.Decor(stat) + if err != nil { + continue + } + if w := stat.AvailableWidth - width; w >= 0 { + _, err = buf.WriteString(str) + stat.AvailableWidth = w + } else if stat.AvailableWidth > 0 { + trunc := runewidth.Truncate(stripansi.Strip(str), stat.AvailableWidth, "…") + _, err = buf.WriteString(trunc) + stat.AvailableWidth = 0 + } + } + return err + } + + bufP, bufB, bufA := s.buffers[0], s.buffers[1], s.buffers[2] + + err := eitherError(decorFiller(bufP, s.pDecorators), decorFiller(bufA, s.aDecorators)) + if err != nil { + return nil, err + } + + if !s.trimSpace && stat.AvailableWidth >= 2 { + stat.AvailableWidth -= 2 + writeFiller := func(buf *bytes.Buffer) error { + return s.filler.Fill(buf, stat) + } + for _, fn := range []func(*bytes.Buffer) error{ + writeSpace, + writeFiller, + writeSpace, + } { + if err := fn(bufB); err != nil { + return nil, err + } + } + } else { + err := s.filler.Fill(bufB, stat) + if err != nil { + return nil, err + } + } + + return io.MultiReader(bufP, bufB, bufA), nil +} + +func (s *bState) wSyncTable() (table syncTable) { + var count int + var row []chan int + + for i, decorators := range [][]decor.Decorator{ + s.pDecorators, + s.aDecorators, + } { + for _, d := range decorators { + if ch, ok := d.Sync(); ok { + row = append(row, ch) + count++ + } + } + switch i { + case 0: + table[i] = row[0:count] + default: + table[i] = row[len(table[i-1]):count] + } + } + return table +} + +func (s bState) decoratorEwmaUpdate(n int64, dur time.Duration) { + var wg sync.WaitGroup + for i := 0; i < len(s.ewmaDecorators); i++ { + switch d := s.ewmaDecorators[i]; i { + case len(s.ewmaDecorators) - 1: + d.EwmaUpdate(n, dur) + default: + wg.Add(1) + go func() { + d.EwmaUpdate(n, dur) + wg.Done() + }() + } + } + wg.Wait() +} + +func (s bState) decoratorAverageAdjust(start time.Time) { + var wg sync.WaitGroup + for i := 0; i < len(s.averageDecorators); i++ { + switch d := s.averageDecorators[i]; i { + case len(s.averageDecorators) - 1: + d.AverageAdjust(start) + default: + wg.Add(1) + go func() { + d.AverageAdjust(start) + wg.Done() + }() + } + } + wg.Wait() +} + +func (s bState) decoratorShutdownNotify() { + var wg sync.WaitGroup + for i := 0; i < len(s.shutdownListeners); i++ { + switch d := s.shutdownListeners[i]; i { + case len(s.shutdownListeners) - 1: + d.OnShutdown() + default: + wg.Add(1) + go func() { + d.OnShutdown() + wg.Done() + }() + } + } + wg.Wait() +} + +func newStatistics(tw int, s *bState) decor.Statistics { + return decor.Statistics{ + AvailableWidth: tw, + RequestedWidth: s.reqWidth, + ID: s.id, + Total: s.total, + Current: s.current, + Refill: s.refill, + Completed: s.completed, + Aborted: s.aborted, + } +} + +func unwrap(d decor.Decorator) decor.Decorator { + if d, ok := d.(decor.Wrapper); ok { + return unwrap(d.Unwrap()) + } + return d +} + +func writeSpace(buf *bytes.Buffer) error { + return buf.WriteByte(' ') +} + +func eitherError(errors ...error) error { + for _, err := range errors { + if err != nil { + return err + } + } + return nil +} diff --git a/bar_filler.go b/bar_filler.go new file mode 100644 index 0000000..379cfea --- /dev/null +++ b/bar_filler.go @@ -0,0 +1,31 @@ +package mpb + +import ( + "io" + + "github.com/vbauerster/mpb/v8/decor" +) + +// BarFiller interface. +// Bar (without decorators) renders itself by calling BarFiller's Fill method. +type BarFiller interface { + Fill(io.Writer, decor.Statistics) error +} + +// BarFillerBuilder interface. +// Default implementations are: +// +// BarStyle() +// SpinnerStyle() +// NopStyle() +type BarFillerBuilder interface { + Build() BarFiller +} + +// BarFillerFunc is function type adapter to convert compatible function +// into BarFiller interface. +type BarFillerFunc func(io.Writer, decor.Statistics) error + +func (f BarFillerFunc) Fill(w io.Writer, stat decor.Statistics) error { + return f(w, stat) +} diff --git a/bar_filler_bar.go b/bar_filler_bar.go new file mode 100644 index 0000000..5d7837a --- /dev/null +++ b/bar_filler_bar.go @@ -0,0 +1,291 @@ +package mpb + +import ( + "io" + + "github.com/mattn/go-runewidth" + "github.com/vbauerster/mpb/v8/decor" + "github.com/vbauerster/mpb/v8/internal" +) + +const ( + iLbound = iota + iRbound + iRefiller + iFiller + iTip + iPadding + components +) + +var defaultBarStyle = [components]string{"[", "]", "+", "=", ">", "-"} + +// BarStyleComposer interface. +type BarStyleComposer interface { + BarFillerBuilder + Lbound(string) BarStyleComposer + LboundMeta(func(string) string) BarStyleComposer + Rbound(string) BarStyleComposer + RboundMeta(func(string) string) BarStyleComposer + Filler(string) BarStyleComposer + FillerMeta(func(string) string) BarStyleComposer + Refiller(string) BarStyleComposer + RefillerMeta(func(string) string) BarStyleComposer + Padding(string) BarStyleComposer + PaddingMeta(func(string) string) BarStyleComposer + Tip(frames ...string) BarStyleComposer + TipMeta(func(string) string) BarStyleComposer + TipOnComplete() BarStyleComposer + Reverse() BarStyleComposer +} + +type component struct { + width int + bytes []byte +} + +type flushSection struct { + meta func(io.Writer, []byte) error + bytes []byte +} + +type bFiller struct { + components [components]component + meta [components]func(io.Writer, []byte) error + flush func(io.Writer, ...flushSection) error + tipOnComplete bool + tip struct { + frames []component + count uint + } +} + +type barStyle struct { + style [components]string + metaFuncs [components]func(io.Writer, []byte) error + tipFrames []string + tipOnComplete bool + rev bool +} + +// BarStyle constructs default bar style which can be altered via +// BarStyleComposer interface. +func BarStyle() BarStyleComposer { + bs := barStyle{ + style: defaultBarStyle, + tipFrames: []string{defaultBarStyle[iTip]}, + } + for i := range bs.metaFuncs { + bs.metaFuncs[i] = defaultMeta + } + return bs +} + +func (s barStyle) Lbound(bound string) BarStyleComposer { + s.style[iLbound] = bound + return s +} + +func (s barStyle) LboundMeta(fn func(string) string) BarStyleComposer { + s.metaFuncs[iLbound] = makeMetaFunc(fn) + return s +} + +func (s barStyle) Rbound(bound string) BarStyleComposer { + s.style[iRbound] = bound + return s +} + +func (s barStyle) RboundMeta(fn func(string) string) BarStyleComposer { + s.metaFuncs[iRbound] = makeMetaFunc(fn) + return s +} + +func (s barStyle) Filler(filler string) BarStyleComposer { + s.style[iFiller] = filler + return s +} + +func (s barStyle) FillerMeta(fn func(string) string) BarStyleComposer { + s.metaFuncs[iFiller] = makeMetaFunc(fn) + return s +} + +func (s barStyle) Refiller(refiller string) BarStyleComposer { + s.style[iRefiller] = refiller + return s +} + +func (s barStyle) RefillerMeta(fn func(string) string) BarStyleComposer { + s.metaFuncs[iRefiller] = makeMetaFunc(fn) + return s +} + +func (s barStyle) Padding(padding string) BarStyleComposer { + s.style[iPadding] = padding + return s +} + +func (s barStyle) PaddingMeta(fn func(string) string) BarStyleComposer { + s.metaFuncs[iPadding] = makeMetaFunc(fn) + return s +} + +func (s barStyle) Tip(frames ...string) BarStyleComposer { + if len(frames) != 0 { + s.tipFrames = frames + } + return s +} + +func (s barStyle) TipMeta(fn func(string) string) BarStyleComposer { + s.metaFuncs[iTip] = makeMetaFunc(fn) + return s +} + +func (s barStyle) TipOnComplete() BarStyleComposer { + s.tipOnComplete = true + return s +} + +func (s barStyle) Reverse() BarStyleComposer { + s.rev = true + return s +} + +func (s barStyle) Build() BarFiller { + bf := &bFiller{ + meta: s.metaFuncs, + tipOnComplete: s.tipOnComplete, + } + bf.components[iLbound] = component{ + width: runewidth.StringWidth(s.style[iLbound]), + bytes: []byte(s.style[iLbound]), + } + bf.components[iRbound] = component{ + width: runewidth.StringWidth(s.style[iRbound]), + bytes: []byte(s.style[iRbound]), + } + bf.components[iFiller] = component{ + width: runewidth.StringWidth(s.style[iFiller]), + bytes: []byte(s.style[iFiller]), + } + bf.components[iRefiller] = component{ + width: runewidth.StringWidth(s.style[iRefiller]), + bytes: []byte(s.style[iRefiller]), + } + bf.components[iPadding] = component{ + width: runewidth.StringWidth(s.style[iPadding]), + bytes: []byte(s.style[iPadding]), + } + bf.tip.frames = make([]component, len(s.tipFrames)) + for i, t := range s.tipFrames { + bf.tip.frames[i] = component{ + width: runewidth.StringWidth(t), + bytes: []byte(t), + } + } + if s.rev { + bf.flush = func(w io.Writer, sections ...flushSection) error { + for i := len(sections) - 1; i >= 0; i-- { + if s := sections[i]; len(s.bytes) != 0 { + err := s.meta(w, s.bytes) + if err != nil { + return err + } + } + } + return nil + } + } else { + bf.flush = func(w io.Writer, sections ...flushSection) error { + for _, s := range sections { + if len(s.bytes) != 0 { + err := s.meta(w, s.bytes) + if err != nil { + return err + } + } + } + return nil + } + } + return bf +} + +func (s *bFiller) Fill(w io.Writer, stat decor.Statistics) error { + width := internal.CheckRequestedWidth(stat.RequestedWidth, stat.AvailableWidth) + // don't count brackets as progress + width -= (s.components[iLbound].width + s.components[iRbound].width) + if width < 0 { + return nil + } + + err := s.meta[iLbound](w, s.components[iLbound].bytes) + if err != nil { + return err + } + + if width == 0 { + return s.meta[iRbound](w, s.components[iRbound].bytes) + } + + var tip component + var refilling, filling, padding []byte + var fillCount int + curWidth := int(internal.PercentageRound(stat.Total, stat.Current, uint(width))) + + if curWidth != 0 { + if !stat.Completed || s.tipOnComplete { + tip = s.tip.frames[s.tip.count%uint(len(s.tip.frames))] + s.tip.count++ + fillCount += tip.width + } + if stat.Refill != 0 { + refWidth := int(internal.PercentageRound(stat.Total, stat.Refill, uint(width))) + curWidth -= refWidth + refWidth += curWidth + for w := s.components[iFiller].width; curWidth-fillCount >= w; fillCount += w { + filling = append(filling, s.components[iFiller].bytes...) + } + for w := s.components[iRefiller].width; refWidth-fillCount >= w; fillCount += w { + refilling = append(refilling, s.components[iRefiller].bytes...) + } + } else { + for w := s.components[iFiller].width; curWidth-fillCount >= w; fillCount += w { + filling = append(filling, s.components[iFiller].bytes...) + } + } + } + + for w := s.components[iPadding].width; width-fillCount >= w; fillCount += w { + padding = append(padding, s.components[iPadding].bytes...) + } + + for w := 1; width-fillCount >= w; fillCount += w { + padding = append(padding, "…"...) + } + + err = s.flush(w, + flushSection{s.meta[iRefiller], refilling}, + flushSection{s.meta[iFiller], filling}, + flushSection{s.meta[iTip], tip.bytes}, + flushSection{s.meta[iPadding], padding}, + ) + if err != nil { + return err + } + return s.meta[iRbound](w, s.components[iRbound].bytes) +} + +func makeMetaFunc(fn func(string) string) func(io.Writer, []byte) error { + return func(w io.Writer, p []byte) (err error) { + _, err = io.WriteString(w, fn(string(p))) + return err + } +} + +func defaultMeta(w io.Writer, p []byte) (err error) { + _, err = w.Write(p) + return err +} diff --git a/bar_filler_nop.go b/bar_filler_nop.go new file mode 100644 index 0000000..a23c61b --- /dev/null +++ b/bar_filler_nop.go @@ -0,0 +1,24 @@ +package mpb + +import ( + "io" + + "github.com/vbauerster/mpb/v8/decor" +) + +// barFillerBuilderFunc is function type adapter to convert compatible +// function into BarFillerBuilder interface. +type barFillerBuilderFunc func() BarFiller + +func (f barFillerBuilderFunc) Build() BarFiller { + return f() +} + +// NopStyle provides BarFillerBuilder which builds NOP BarFiller. +func NopStyle() BarFillerBuilder { + return barFillerBuilderFunc(func() BarFiller { + return BarFillerFunc(func(io.Writer, decor.Statistics) error { + return nil + }) + }) +} diff --git a/bar_filler_spinner.go b/bar_filler_spinner.go new file mode 100644 index 0000000..c9fd463 --- /dev/null +++ b/bar_filler_spinner.go @@ -0,0 +1,103 @@ +package mpb + +import ( + "io" + "strings" + + "github.com/mattn/go-runewidth" + "github.com/vbauerster/mpb/v8/decor" + "github.com/vbauerster/mpb/v8/internal" +) + +const ( + positionLeft = 1 + iota + positionRight +) + +var defaultSpinnerStyle = [...]string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} + +// SpinnerStyleComposer interface. +type SpinnerStyleComposer interface { + BarFillerBuilder + PositionLeft() SpinnerStyleComposer + PositionRight() SpinnerStyleComposer + Meta(func(string) string) SpinnerStyleComposer +} + +type sFiller struct { + frames []string + count uint + meta func(string) string + position func(string, int) string +} + +type spinnerStyle struct { + position uint + frames []string + meta func(string) string +} + +// SpinnerStyle constructs default spinner style which can be altered via +// SpinnerStyleComposer interface. +func SpinnerStyle(frames ...string) SpinnerStyleComposer { + ss := spinnerStyle{ + meta: func(s string) string { return s }, + } + if len(frames) != 0 { + ss.frames = frames + } else { + ss.frames = defaultSpinnerStyle[:] + } + return ss +} + +func (s spinnerStyle) PositionLeft() SpinnerStyleComposer { + s.position = positionLeft + return s +} + +func (s spinnerStyle) PositionRight() SpinnerStyleComposer { + s.position = positionRight + return s +} + +func (s spinnerStyle) Meta(fn func(string) string) SpinnerStyleComposer { + s.meta = fn + return s +} + +func (s spinnerStyle) Build() BarFiller { + sf := &sFiller{ + frames: s.frames, + meta: s.meta, + } + switch s.position { + case positionLeft: + sf.position = func(frame string, padWidth int) string { + return frame + strings.Repeat(" ", padWidth) + } + case positionRight: + sf.position = func(frame string, padWidth int) string { + return strings.Repeat(" ", padWidth) + frame + } + default: + sf.position = func(frame string, padWidth int) string { + return strings.Repeat(" ", padWidth/2) + frame + strings.Repeat(" ", padWidth/2+padWidth%2) + } + } + return sf +} + +func (s *sFiller) Fill(w io.Writer, stat decor.Statistics) error { + width := internal.CheckRequestedWidth(stat.RequestedWidth, stat.AvailableWidth) + frame := s.frames[s.count%uint(len(s.frames))] + frameWidth := runewidth.StringWidth(frame) + s.count++ + + if width < frameWidth { + return nil + } + + _, err := io.WriteString(w, s.position(s.meta(frame), width-frameWidth)) + return err +} diff --git a/bar_option.go b/bar_option.go new file mode 100644 index 0000000..d3cb3e2 --- /dev/null +++ b/bar_option.go @@ -0,0 +1,200 @@ +package mpb + +import ( + "bytes" + "io" + + "github.com/vbauerster/mpb/v8/decor" +) + +// BarOption is a func option to alter default behavior of a bar. +type BarOption func(*bState) + +func inspect(decorators []decor.Decorator) (dest []decor.Decorator) { + for _, decorator := range decorators { + if decorator == nil { + continue + } + dest = append(dest, decorator) + } + return +} + +// AppendDecorators let you inject decorators to the bar's right side. +func AppendDecorators(decorators ...decor.Decorator) BarOption { + decorators = inspect(decorators) + return func(s *bState) { + s.aDecorators = decorators + } +} + +// PrependDecorators let you inject decorators to the bar's left side. +func PrependDecorators(decorators ...decor.Decorator) BarOption { + decorators = inspect(decorators) + return func(s *bState) { + s.pDecorators = decorators + } +} + +// BarID sets bar id. +func BarID(id int) BarOption { + return func(s *bState) { + s.id = id + } +} + +// BarWidth sets bar width independent of the container. +func BarWidth(width int) BarOption { + return func(s *bState) { + s.reqWidth = width + } +} + +// BarQueueAfter puts this (being constructed) bar into the queue. +// BarPriority will be inherited from the argument bar. +// When argument bar completes or aborts queued bar replaces its place. +func BarQueueAfter(bar *Bar) BarOption { + return func(s *bState) { + s.waitBar = bar + } +} + +// BarRemoveOnComplete removes both bar's filler and its decorators +// on complete event. +func BarRemoveOnComplete() BarOption { + return func(s *bState) { + s.rmOnComplete = true + } +} + +// BarFillerClearOnComplete clears bar's filler on complete event. +// It's shortcut for BarFillerOnComplete(""). +func BarFillerClearOnComplete() BarOption { + return BarFillerOnComplete("") +} + +// BarFillerOnComplete replaces bar's filler with message, on complete event. +func BarFillerOnComplete(message string) BarOption { + return BarFillerMiddleware(func(base BarFiller) BarFiller { + return BarFillerFunc(func(w io.Writer, st decor.Statistics) error { + if st.Completed { + _, err := io.WriteString(w, message) + return err + } + return base.Fill(w, st) + }) + }) +} + +// BarFillerMiddleware provides a way to augment the underlying BarFiller. +func BarFillerMiddleware(middle func(BarFiller) BarFiller) BarOption { + if middle == nil { + return nil + } + return func(s *bState) { + s.filler = middle(s.filler) + } +} + +// BarPriority sets bar's priority. Zero is highest priority, i.e. bar +// will be on top. This option isn't effective with `BarQueueAfter` option. +func BarPriority(priority int) BarOption { + return func(s *bState) { + s.priority = priority + } +} + +// BarExtender extends bar with arbitrary lines. Provided BarFiller will be +// called at each render/flush cycle. Any lines written to the underlying +// io.Writer will extend the bar either in above (rev = true) or below +// (rev = false) direction. +func BarExtender(filler BarFiller, rev bool) BarOption { + if filler == nil { + return nil + } + fn := makeExtenderFunc(filler, rev) + return func(s *bState) { + s.extender = fn + } +} + +func makeExtenderFunc(filler BarFiller, rev bool) extenderFunc { + buf := new(bytes.Buffer) + base := func(rows []io.Reader, stat decor.Statistics) ([]io.Reader, error) { + err := filler.Fill(buf, stat) + if err != nil { + buf.Reset() + return rows, err + } + for { + b, err := buf.ReadBytes('\n') + if err != nil { + break + } + rows = append(rows, bytes.NewReader(b)) + } + buf.Reset() + return rows, err + } + + if !rev { + return base + } + return func(rows []io.Reader, stat decor.Statistics) ([]io.Reader, error) { + rows, err := base(rows, stat) + if err != nil { + return rows, err + } + for left, right := 0, len(rows)-1; left < right; left, right = left+1, right-1 { + rows[left], rows[right] = rows[right], rows[left] + } + return rows, err + } +} + +// BarFillerTrim removes leading and trailing space around the underlying BarFiller. +func BarFillerTrim() BarOption { + return func(s *bState) { + s.trimSpace = true + } +} + +// BarNoPop disables bar pop out of container. Effective when +// PopCompletedMode of container is enabled. +func BarNoPop() BarOption { + return func(s *bState) { + s.noPop = true + } +} + +// BarOptional will return provided option only when cond is true. +func BarOptional(option BarOption, cond bool) BarOption { + if cond { + return option + } + return nil +} + +// BarOptOn will return provided option only when predicate evaluates to true. +func BarOptOn(option BarOption, predicate func() bool) BarOption { + if predicate() { + return option + } + return nil +} + +// BarFuncOptional will call option and return its value only when cond is true. +func BarFuncOptional(option func() BarOption, cond bool) BarOption { + if cond { + return option() + } + return nil +} + +// BarFuncOptOn will call option and return its value only when predicate evaluates to true. +func BarFuncOptOn(option func() BarOption, predicate func() bool) BarOption { + if predicate() { + return option() + } + return nil +} diff --git a/bar_test.go b/bar_test.go new file mode 100644 index 0000000..c5e0e70 --- /dev/null +++ b/bar_test.go @@ -0,0 +1,305 @@ +package mpb_test + +import ( + "bytes" + "context" + "fmt" + "io" + "strings" + "testing" + "time" + "unicode/utf8" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func TestBarCompleted(t *testing.T) { + p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) + total := 80 + bar := p.AddBar(int64(total)) + + if bar.Completed() { + t.Fail() + } + + bar.IncrBy(total) + + if !bar.Completed() { + t.Error("bar isn't completed after increment") + } + + p.Wait() +} + +func TestBarAborted(t *testing.T) { + p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) + total := 80 + bar := p.AddBar(int64(total)) + + if bar.Aborted() { + t.Fail() + } + + bar.Abort(false) + + if !bar.Aborted() { + t.Error("bar isn't aborted after abort call") + } + + p.Wait() +} + +func TestBarSetTotal(t *testing.T) { + p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) + bar := p.AddBar(0) + + bar.SetTotal(0, false) + if bar.Completed() { + t.Error("expected bar not to complete") + } + + bar.SetTotal(0, true) + if !bar.Completed() { + t.Error("expected bar to complete") + } + + p.Wait() +} + +func TestBarEnableTriggerCompleteAndIncrementBefore(t *testing.T) { + p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) + bar := p.AddBar(0) // never complete bar + + for _, f := range []func(){ + func() { bar.SetTotal(40, false) }, + func() { bar.IncrBy(60) }, + func() { bar.SetTotal(80, false) }, + func() { bar.IncrBy(20) }, + } { + f() + if bar.Completed() { + t.Fail() + } + } + + bar.EnableTriggerComplete() + + if !bar.Completed() { + t.Fail() + } + + p.Wait() +} + +func TestBarEnableTriggerCompleteAndIncrementAfter(t *testing.T) { + p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) + bar := p.AddBar(0) // never complete bar + + for _, f := range []func(){ + func() { bar.SetTotal(40, false) }, + func() { bar.IncrBy(60) }, + func() { bar.SetTotal(80, false) }, + func() { bar.EnableTriggerComplete() }, + } { + f() + if bar.Completed() { + t.Fail() + } + } + + bar.IncrBy(20) + + if !bar.Completed() { + t.Fail() + } + + p.Wait() +} + +func TestBarID(t *testing.T) { + p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) + total := 100 + wantID := 11 + bar := p.AddBar(int64(total), mpb.BarID(wantID)) + + gotID := bar.ID() + if gotID != wantID { + t.Errorf("Expected bar id: %d, got %d", wantID, gotID) + } + + bar.IncrBy(total) + + p.Wait() +} + +func TestBarSetRefill(t *testing.T) { + var buf bytes.Buffer + p := mpb.New( + mpb.WithWidth(100), + mpb.WithOutput(&buf), + mpb.WithAutoRefresh(), + ) + + total := 100 + till := 30 + refiller := "+" + + bar := p.New(int64(total), mpb.BarStyle().Refiller(refiller), mpb.BarFillerTrim()) + + bar.IncrBy(till) + bar.SetRefill(int64(till)) + bar.IncrBy(total - till) + + p.Wait() + + wantBar := fmt.Sprintf("[%s%s]", + strings.Repeat(refiller, till-1), + strings.Repeat("=", total-till-1), + ) + + got := string(bytes.Split(buf.Bytes(), []byte("\n"))[0]) + + if !strings.Contains(got, wantBar) { + t.Errorf("Want bar: %q, got bar: %q", wantBar, got) + } +} + +func TestBarHas100PercentWithBarRemoveOnComplete(t *testing.T) { + var buf bytes.Buffer + p := mpb.New( + mpb.WithWidth(80), + mpb.WithOutput(&buf), + mpb.WithAutoRefresh(), + ) + + total := 50 + + bar := p.AddBar(int64(total), + mpb.BarRemoveOnComplete(), + mpb.AppendDecorators(decor.Percentage()), + ) + + bar.IncrBy(total) + + p.Wait() + + hundred := "100 %" + if !bytes.Contains(buf.Bytes(), []byte(hundred)) { + t.Errorf("Bar's buffer does not contain: %q", hundred) + } +} + +func TestBarStyle(t *testing.T) { + var buf bytes.Buffer + customFormat := "╢▌▌░╟" + runes := []rune(customFormat) + total := 80 + p := mpb.New( + mpb.WithWidth(80), + mpb.WithOutput(&buf), + mpb.WithAutoRefresh(), + ) + bs := mpb.BarStyle() + bs = bs.Lbound(string(runes[0])) + bs = bs.Filler(string(runes[1])) + bs = bs.Tip(string(runes[2])) + bs = bs.Padding(string(runes[3])) + bs = bs.Rbound(string(runes[4])) + bar := p.New(int64(total), bs, mpb.BarFillerTrim()) + + bar.IncrBy(total) + + p.Wait() + + wantBar := fmt.Sprintf("%s%s%s%s", + string(runes[0]), + strings.Repeat(string(runes[1]), total-3), + string(runes[2]), + string(runes[4]), + ) + got := string(bytes.Split(buf.Bytes(), []byte("\n"))[0]) + + if !strings.Contains(got, wantBar) { + t.Errorf("Want bar: %q:%d, got bar: %q:%d", wantBar, utf8.RuneCountInString(wantBar), got, utf8.RuneCountInString(got)) + } +} + +func TestDecorStatisticsAvailableWidth(t *testing.T) { + ch := make(chan int, 2) + td1 := func(s decor.Statistics) string { + ch <- s.AvailableWidth + return strings.Repeat("0", 20) + } + td2 := func(s decor.Statistics) string { + ch <- s.AvailableWidth + return "" + } + ctx, cancel := context.WithCancel(context.Background()) + refresh := make(chan interface{}) + p := mpb.NewWithContext(ctx, + mpb.WithWidth(100), + mpb.WithManualRefresh(refresh), + mpb.WithOutput(io.Discard), + ) + _ = p.AddBar(0, + mpb.BarFillerTrim(), + mpb.PrependDecorators( + decor.Name(strings.Repeat("0", 20)), + decor.Meta( + decor.Any(td1), + func(s string) string { + return "\x1b[31;1m" + s + "\x1b[0m" + }, + ), + ), + mpb.AppendDecorators( + decor.Name(strings.Repeat("0", 20)), + decor.Any(td2), + ), + ) + refresh <- time.Now() + go func() { + time.Sleep(10 * time.Millisecond) + cancel() + }() + p.Wait() + + if availableWidth := <-ch; availableWidth != 80 { + t.Errorf("expected AvailableWidth %d got %d", 80, availableWidth) + } + + if availableWidth := <-ch; availableWidth != 40 { + t.Errorf("expected AvailableWidth %d got %d", 40, availableWidth) + } +} + +func TestBarQueueAfterBar(t *testing.T) { + shutdown := make(chan interface{}) + ctx, cancel := context.WithCancel(context.Background()) + p := mpb.NewWithContext(ctx, + mpb.WithOutput(io.Discard), + mpb.WithAutoRefresh(), + mpb.WithShutdownNotifier(shutdown), + ) + a := p.AddBar(100) + b := p.AddBar(100, mpb.BarQueueAfter(a)) + identity := map[*mpb.Bar]string{ + a: "a", + b: "b", + } + + a.IncrBy(100) + a.Wait() + cancel() + + bars := (<-shutdown).([]*mpb.Bar) + if l := len(bars); l != 1 { + t.Errorf("Expected len of bars: %d, got: %d", 1, l) + } + + p.Wait() + if bars[0] != b { + t.Errorf("Expected bars[0] == b, got: %s", identity[bars[0]]) + } +} diff --git a/barbench_test.go b/barbench_test.go new file mode 100644 index 0000000..47b633f --- /dev/null +++ b/barbench_test.go @@ -0,0 +1,93 @@ +package mpb_test + +import ( + "io" + "sync" + "testing" + + "github.com/vbauerster/mpb/v8" +) + +const total = 1000 + +func BenchmarkNopStyle1Bar(b *testing.B) { + bench(b, mpb.NopStyle(), false, 1) +} + +func BenchmarkNopStyle1BarWithAutoRefresh(b *testing.B) { + bench(b, mpb.NopStyle(), true, 1) +} + +func BenchmarkNopStyle2Bars(b *testing.B) { + bench(b, mpb.NopStyle(), false, 2) +} + +func BenchmarkNopStyle2BarsWithAutoRefresh(b *testing.B) { + bench(b, mpb.NopStyle(), true, 2) +} + +func BenchmarkNopStyle3Bars(b *testing.B) { + bench(b, mpb.NopStyle(), false, 3) +} + +func BenchmarkNopStyle3BarsWithAutoRefresh(b *testing.B) { + bench(b, mpb.NopStyle(), true, 3) +} + +func BenchmarkBarStyle1Bar(b *testing.B) { + bench(b, mpb.BarStyle(), false, 1) +} + +func BenchmarkBarStyle1BarWithAutoRefresh(b *testing.B) { + bench(b, mpb.BarStyle(), true, 1) +} + +func BenchmarkBarStyle2Bars(b *testing.B) { + bench(b, mpb.BarStyle(), false, 2) +} + +func BenchmarkBarStyle2BarsWithAutoRefresh(b *testing.B) { + bench(b, mpb.BarStyle(), true, 2) +} + +func BenchmarkBarStyle3Bars(b *testing.B) { + bench(b, mpb.BarStyle(), false, 3) +} + +func BenchmarkBarStyle3BarsWithAutoRefresh(b *testing.B) { + bench(b, mpb.BarStyle(), true, 3) +} + +func bench(b *testing.B, builder mpb.BarFillerBuilder, autoRefresh bool, n int) { + var wg sync.WaitGroup + p := mpb.New( + mpb.WithWidth(100), + mpb.WithOutput(io.Discard), + mpb.ContainerOptional(mpb.WithAutoRefresh(), autoRefresh), + ) + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := 0; j < n; j++ { + bar := p.New(total, builder) + switch j { + case n - 1: + complete(b, bar) + default: + wg.Add(1) + go func() { + complete(b, bar) + wg.Done() + }() + } + } + wg.Wait() + } + p.Wait() +} + +func complete(b *testing.B, bar *mpb.Bar) { + for i := 0; i < total; i++ { + bar.Increment() + } + bar.Wait() +} diff --git a/container_option.go b/container_option.go new file mode 100644 index 0000000..f2ab01e --- /dev/null +++ b/container_option.go @@ -0,0 +1,135 @@ +package mpb + +import ( + "io" + "sync" + "time" +) + +// ContainerOption is a func option to alter default behavior of a bar +// container. Container term refers to a Progress struct which can +// hold one or more Bars. +type ContainerOption 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) ContainerOption { + return func(s *pState) { + s.uwg = wg + } +} + +// WithWidth sets container width. If not set it defaults to terminal +// width. A bar added to the container will inherit its width, unless +// overridden by `func BarWidth(int) BarOption`. +func WithWidth(width int) ContainerOption { + return func(s *pState) { + s.reqWidth = width + } +} + +// WithRefreshRate overrides default 150ms refresh rate. +func WithRefreshRate(d time.Duration) ContainerOption { + return func(s *pState) { + s.refreshRate = d + } +} + +// WithManualRefresh disables internal auto refresh time.Ticker. +// Refresh will occur upon receive value from provided ch. +func WithManualRefresh(ch <-chan interface{}) ContainerOption { + return func(s *pState) { + s.manualRC = ch + } +} + +// WithRenderDelay delays rendering. By default rendering starts as +// soon as bar is added, with this option it's possible to delay +// rendering process by keeping provided chan unclosed. In other words +// rendering will start as soon as provided chan is closed. +func WithRenderDelay(ch <-chan struct{}) ContainerOption { + return func(s *pState) { + s.delayRC = ch + } +} + +// WithShutdownNotifier value of type `[]*mpb.Bar` will be send into provided +// channel upon container shutdown. +func WithShutdownNotifier(ch chan<- interface{}) ContainerOption { + return func(s *pState) { + s.shutdownNotifier = ch + } +} + +// WithOutput overrides default os.Stdout output. If underlying io.Writer +// is not a terminal then auto refresh is disabled unless WithAutoRefresh +// option is set. +func WithOutput(w io.Writer) ContainerOption { + if w == nil { + w = io.Discard + } + return func(s *pState) { + s.output = w + } +} + +// WithDebugOutput sets debug output. +func WithDebugOutput(w io.Writer) ContainerOption { + if w == nil { + w = io.Discard + } + return func(s *pState) { + s.debugOut = w + } +} + +// WithAutoRefresh force auto refresh regardless of what output is set to. +// Applicable only if not WithManualRefresh set. +func WithAutoRefresh() ContainerOption { + return func(s *pState) { + s.autoRefresh = true + } +} + +// PopCompletedMode will pop completed bars to the top. +// To stop rendering bar after it has been popped, use +// mpb.BarRemoveOnComplete() option on that bar. +func PopCompletedMode() ContainerOption { + return func(s *pState) { + s.popCompleted = true + } +} + +// ContainerOptional will return provided option only when cond is true. +func ContainerOptional(option ContainerOption, cond bool) ContainerOption { + if cond { + return option + } + return nil +} + +// ContainerOptOn will return provided option only when predicate evaluates to true. +func ContainerOptOn(option ContainerOption, predicate func() bool) ContainerOption { + if predicate() { + return option + } + return nil +} + +// ContainerFuncOptional will call option and return its value only when cond is true. +func ContainerFuncOptional(option func() ContainerOption, cond bool) ContainerOption { + if cond { + return option() + } + return nil +} + +// ContainerFuncOptOn will call option and return its value only when predicate evaluates to true. +func ContainerFuncOptOn(option func() ContainerOption, predicate func() bool) ContainerOption { + if predicate() { + return option() + } + return nil +} diff --git a/cwriter/cuuAndEd_construction_bench_test.go b/cwriter/cuuAndEd_construction_bench_test.go new file mode 100644 index 0000000..996d5a8 --- /dev/null +++ b/cwriter/cuuAndEd_construction_bench_test.go @@ -0,0 +1,45 @@ +package cwriter + +import ( + "bytes" + "fmt" + "io" + "strconv" + "testing" +) + +var ( + out = io.Discard + lines = 99 +) + +func BenchmarkWithFprintf(b *testing.B) { + verb := fmt.Sprintf("%s%%d%s", escOpen, cuuAndEd) + b.ResetTimer() + for i := 0; i < b.N; i++ { + fmt.Fprintf(out, verb, lines) + } +} + +func BenchmarkWithJoin(b *testing.B) { + bCuuAndEd := [][]byte{[]byte(escOpen), []byte(cuuAndEd)} + for i := 0; i < b.N; i++ { + _, _ = out.Write(bytes.Join(bCuuAndEd, []byte(strconv.Itoa(lines)))) + } +} + +func BenchmarkWithAppend(b *testing.B) { + escOpen := []byte(escOpen) + cuuAndEd := []byte(cuuAndEd) + for i := 0; i < b.N; i++ { + _, _ = out.Write(append(strconv.AppendInt(escOpen, int64(lines), 10), cuuAndEd...)) + } +} + +func BenchmarkWithCurrentImpl(b *testing.B) { + w := New(out) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = w.ew.ansiCuuAndEd(out, lines) + } +} diff --git a/cwriter/doc.go b/cwriter/doc.go new file mode 100644 index 0000000..93c8f82 --- /dev/null +++ b/cwriter/doc.go @@ -0,0 +1,2 @@ +// Package cwriter is a console writer abstraction for the underlying OS. +package cwriter diff --git a/cwriter/util_bsd.go b/cwriter/util_bsd.go new file mode 100644 index 0000000..215643b --- /dev/null +++ b/cwriter/util_bsd.go @@ -0,0 +1,7 @@ +//go:build darwin || dragonfly || freebsd || netbsd || openbsd + +package cwriter + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TIOCGETA diff --git a/cwriter/util_linux.go b/cwriter/util_linux.go new file mode 100644 index 0000000..7d0e761 --- /dev/null +++ b/cwriter/util_linux.go @@ -0,0 +1,7 @@ +//go:build aix || linux + +package cwriter + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TCGETS diff --git a/cwriter/util_solaris.go b/cwriter/util_solaris.go new file mode 100644 index 0000000..981f574 --- /dev/null +++ b/cwriter/util_solaris.go @@ -0,0 +1,7 @@ +//go:build solaris + +package cwriter + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TCGETA diff --git a/cwriter/util_zos.go b/cwriter/util_zos.go new file mode 100644 index 0000000..5daf003 --- /dev/null +++ b/cwriter/util_zos.go @@ -0,0 +1,7 @@ +//go:build zos + +package cwriter + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TCGETS diff --git a/cwriter/writer.go b/cwriter/writer.go new file mode 100644 index 0000000..23a72d3 --- /dev/null +++ b/cwriter/writer.go @@ -0,0 +1,59 @@ +package cwriter + +import ( + "bytes" + "errors" + "io" + "os" + "strconv" +) + +// https://github.com/dylanaraps/pure-sh-bible#cursor-movement +const ( + escOpen = "\x1b[" + cuuAndEd = "A\x1b[J" +) + +// ErrNotTTY not a TeleTYpewriter error. +var ErrNotTTY = errors.New("not a terminal") + +// New returns a new Writer with defaults. +func New(out io.Writer) *Writer { + w := &Writer{ + Buffer: new(bytes.Buffer), + out: out, + termSize: func(_ int) (int, int, error) { + return -1, -1, ErrNotTTY + }, + } + if f, ok := out.(*os.File); ok { + w.fd = int(f.Fd()) + if IsTerminal(w.fd) { + w.terminal = true + w.termSize = func(fd int) (int, int, error) { + return GetSize(fd) + } + } + } + bb := make([]byte, 16) + w.ew = escWriter(bb[:copy(bb, []byte(escOpen))]) + return w +} + +// IsTerminal tells whether underlying io.Writer is terminal. +func (w *Writer) IsTerminal() bool { + return w.terminal +} + +// GetTermSize returns WxH of underlying terminal. +func (w *Writer) GetTermSize() (width, height int, err error) { + return w.termSize(w.fd) +} + +type escWriter []byte + +func (b escWriter) ansiCuuAndEd(out io.Writer, n int) error { + b = strconv.AppendInt(b, int64(n), 10) + _, err := out.Write(append(b, []byte(cuuAndEd)...)) + return err +} diff --git a/cwriter/writer_posix.go b/cwriter/writer_posix.go new file mode 100644 index 0000000..e80d757 --- /dev/null +++ b/cwriter/writer_posix.go @@ -0,0 +1,48 @@ +//go:build !windows + +package cwriter + +import ( + "bytes" + "io" + + "golang.org/x/sys/unix" +) + +// Writer is a buffered terminal writer, which moves cursor N lines up +// on each flush except the first one, where N is a number of lines of +// a previous flush. +type Writer struct { + *bytes.Buffer + out io.Writer + ew escWriter + fd int + terminal bool + termSize func(int) (int, int, error) +} + +// Flush flushes the underlying buffer. +// It's caller's responsibility to pass correct number of lines. +func (w *Writer) Flush(lines int) error { + _, err := w.WriteTo(w.out) + // some terminals interpret 'cursor up 0' as 'cursor up 1' + if err == nil && lines > 0 { + err = w.ew.ansiCuuAndEd(w, lines) + } + return err +} + +// GetSize returns the dimensions of the given terminal. +func GetSize(fd int) (width, height int, err error) { + ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) + if err != nil { + return -1, -1, err + } + return int(ws.Col), int(ws.Row), nil +} + +// IsTerminal returns whether the given file descriptor is a terminal. +func IsTerminal(fd int) bool { + _, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + return err == nil +} diff --git a/cwriter/writer_windows.go b/cwriter/writer_windows.go new file mode 100644 index 0000000..44293f2 --- /dev/null +++ b/cwriter/writer_windows.go @@ -0,0 +1,101 @@ +//go:build windows + +package cwriter + +import ( + "bytes" + "io" + "unsafe" + + "golang.org/x/sys/windows" +) + +var kernel32 = windows.NewLazySystemDLL("kernel32.dll") + +var ( + procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") + procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") +) + +// Writer is a buffered terminal writer, which moves cursor N lines up +// on each flush except the first one, where N is a number of lines of +// a previous flush. +type Writer struct { + *bytes.Buffer + out io.Writer + ew escWriter + lines int + fd int + terminal bool + termSize func(int) (int, int, error) +} + +// Flush flushes the underlying buffer. +// It's caller's responsibility to pass correct number of lines. +func (w *Writer) Flush(lines int) error { + if w.lines > 0 { + err := w.clearLines(w.lines) + if err != nil { + return err + } + } + w.lines = lines + _, err := w.WriteTo(w.out) + return err +} + +func (w *Writer) clearLines(n int) error { + if !w.terminal { + // hope it's cygwin or similar + return w.ew.ansiCuuAndEd(w.out, n) + } + + var info windows.ConsoleScreenBufferInfo + if err := windows.GetConsoleScreenBufferInfo(windows.Handle(w.fd), &info); err != nil { + return err + } + + info.CursorPosition.Y -= int16(n) + if info.CursorPosition.Y < 0 { + info.CursorPosition.Y = 0 + } + _, _, _ = procSetConsoleCursorPosition.Call( + uintptr(w.fd), + uintptr(uint32(uint16(info.CursorPosition.Y))<<16|uint32(uint16(info.CursorPosition.X))), + ) + + // clear the lines + cursor := &windows.Coord{ + X: info.Window.Left, + Y: info.CursorPosition.Y, + } + count := uint32(info.Size.X) * uint32(n) + _, _, _ = procFillConsoleOutputCharacter.Call( + uintptr(w.fd), + uintptr(' '), + uintptr(count), + *(*uintptr)(unsafe.Pointer(cursor)), + uintptr(unsafe.Pointer(new(uint32))), + ) + return nil +} + +// GetSize returns the visible dimensions of the given terminal. +// These dimensions don't include any scrollback buffer height. +func GetSize(fd int) (width, height int, err error) { + var info windows.ConsoleScreenBufferInfo + if err := windows.GetConsoleScreenBufferInfo(windows.Handle(fd), &info); err != nil { + return 0, 0, err + } + // terminal.GetSize from crypto/ssh adds "+ 1" to both width and height: + // https://go.googlesource.com/crypto/+/refs/heads/release-branch.go1.14/ssh/terminal/util_windows.go#75 + // but looks like this is a root cause of issue #66, so removing both "+ 1" have fixed it. + return int(info.Window.Right - info.Window.Left), int(info.Window.Bottom - info.Window.Top), nil +} + +// IsTerminal returns whether the given file descriptor is a terminal. +func IsTerminal(fd int) bool { + var st uint32 + err := windows.GetConsoleMode(windows.Handle(fd), &st) + return err == nil +} diff --git a/decor/any.go b/decor/any.go new file mode 100644 index 0000000..ca208d8 --- /dev/null +++ b/decor/any.go @@ -0,0 +1,21 @@ +package decor + +var _ Decorator = any{} + +// Any decorator. +// Converts DecorFunc into Decorator. +// +// `fn` DecorFunc callback +// `wcc` optional WC config +func Any(fn DecorFunc, wcc ...WC) Decorator { + return any{initWC(wcc...), fn} +} + +type any struct { + WC + fn DecorFunc +} + +func (d any) Decor(s Statistics) (string, int) { + return d.Format(d.fn(s)) +} diff --git a/decor/counters.go b/decor/counters.go new file mode 100644 index 0000000..0420275 --- /dev/null +++ b/decor/counters.go @@ -0,0 +1,253 @@ +package decor + +import ( + "fmt" +) + +// CountersNoUnit is a wrapper around Counters with no unit param. +func CountersNoUnit(pairFmt string, wcc ...WC) Decorator { + return Counters(0, pairFmt, wcc...) +} + +// CountersKibiByte is a wrapper around Counters with predefined unit +// as SizeB1024(0). +func CountersKibiByte(pairFmt string, wcc ...WC) Decorator { + return Counters(SizeB1024(0), pairFmt, wcc...) +} + +// CountersKiloByte is a wrapper around Counters with predefined unit +// as SizeB1000(0). +func CountersKiloByte(pairFmt string, wcc ...WC) Decorator { + return Counters(SizeB1000(0), pairFmt, wcc...) +} + +// Counters decorator with dynamic unit measure adjustment. +// +// `unit` one of [0|SizeB1024(0)|SizeB1000(0)] +// +// `pairFmt` printf compatible verbs for current and total +// +// `wcc` optional WC config +// +// pairFmt example if unit=SizeB1000(0): +// +// pairFmt="%d / %d" output: "1MB / 12MB" +// pairFmt="% d / % d" output: "1 MB / 12 MB" +// pairFmt="%.1f / %.1f" output: "1.0MB / 12.0MB" +// pairFmt="% .1f / % .1f" output: "1.0 MB / 12.0 MB" +// pairFmt="%f / %f" output: "1.000000MB / 12.000000MB" +// pairFmt="% f / % f" output: "1.000000 MB / 12.000000 MB" +func Counters(unit interface{}, pairFmt string, wcc ...WC) Decorator { + producer := func() DecorFunc { + switch unit.(type) { + case SizeB1024: + if pairFmt == "" { + pairFmt = "% d / % d" + } + return func(s Statistics) string { + return fmt.Sprintf(pairFmt, SizeB1024(s.Current), SizeB1024(s.Total)) + } + case SizeB1000: + if pairFmt == "" { + pairFmt = "% d / % d" + } + return func(s Statistics) string { + return fmt.Sprintf(pairFmt, SizeB1000(s.Current), SizeB1000(s.Total)) + } + default: + if pairFmt == "" { + pairFmt = "%d / %d" + } + return func(s Statistics) string { + return fmt.Sprintf(pairFmt, s.Current, s.Total) + } + } + } + return Any(producer(), wcc...) +} + +// TotalNoUnit is a wrapper around Total with no unit param. +func TotalNoUnit(format string, wcc ...WC) Decorator { + return Total(0, format, wcc...) +} + +// TotalKibiByte is a wrapper around Total with predefined unit +// as SizeB1024(0). +func TotalKibiByte(format string, wcc ...WC) Decorator { + return Total(SizeB1024(0), format, wcc...) +} + +// TotalKiloByte is a wrapper around Total with predefined unit +// as SizeB1000(0). +func TotalKiloByte(format string, wcc ...WC) Decorator { + return Total(SizeB1000(0), format, wcc...) +} + +// Total decorator with dynamic unit measure adjustment. +// +// `unit` one of [0|SizeB1024(0)|SizeB1000(0)] +// +// `format` printf compatible verb for Total +// +// `wcc` optional WC config +// +// format example if unit=SizeB1024(0): +// +// format="%d" output: "12MiB" +// format="% d" output: "12 MiB" +// format="%.1f" output: "12.0MiB" +// format="% .1f" output: "12.0 MiB" +// format="%f" output: "12.000000MiB" +// format="% f" output: "12.000000 MiB" +func Total(unit interface{}, format string, wcc ...WC) Decorator { + producer := func() DecorFunc { + switch unit.(type) { + case SizeB1024: + if format == "" { + format = "% d" + } + return func(s Statistics) string { + return fmt.Sprintf(format, SizeB1024(s.Total)) + } + case SizeB1000: + if format == "" { + format = "% d" + } + return func(s Statistics) string { + return fmt.Sprintf(format, SizeB1000(s.Total)) + } + default: + if format == "" { + format = "%d" + } + return func(s Statistics) string { + return fmt.Sprintf(format, s.Total) + } + } + } + return Any(producer(), wcc...) +} + +// CurrentNoUnit is a wrapper around Current with no unit param. +func CurrentNoUnit(format string, wcc ...WC) Decorator { + return Current(0, format, wcc...) +} + +// CurrentKibiByte is a wrapper around Current with predefined unit +// as SizeB1024(0). +func CurrentKibiByte(format string, wcc ...WC) Decorator { + return Current(SizeB1024(0), format, wcc...) +} + +// CurrentKiloByte is a wrapper around Current with predefined unit +// as SizeB1000(0). +func CurrentKiloByte(format string, wcc ...WC) Decorator { + return Current(SizeB1000(0), format, wcc...) +} + +// Current decorator with dynamic unit measure adjustment. +// +// `unit` one of [0|SizeB1024(0)|SizeB1000(0)] +// +// `format` printf compatible verb for Current +// +// `wcc` optional WC config +// +// format example if unit=SizeB1024(0): +// +// format="%d" output: "12MiB" +// format="% d" output: "12 MiB" +// format="%.1f" output: "12.0MiB" +// format="% .1f" output: "12.0 MiB" +// format="%f" output: "12.000000MiB" +// format="% f" output: "12.000000 MiB" +func Current(unit interface{}, format string, wcc ...WC) Decorator { + producer := func() DecorFunc { + switch unit.(type) { + case SizeB1024: + if format == "" { + format = "% d" + } + return func(s Statistics) string { + return fmt.Sprintf(format, SizeB1024(s.Current)) + } + case SizeB1000: + if format == "" { + format = "% d" + } + return func(s Statistics) string { + return fmt.Sprintf(format, SizeB1000(s.Current)) + } + default: + if format == "" { + format = "%d" + } + return func(s Statistics) string { + return fmt.Sprintf(format, s.Current) + } + } + } + return Any(producer(), wcc...) +} + +// InvertedCurrentNoUnit is a wrapper around InvertedCurrent with no unit param. +func InvertedCurrentNoUnit(format string, wcc ...WC) Decorator { + return InvertedCurrent(0, format, wcc...) +} + +// InvertedCurrentKibiByte is a wrapper around InvertedCurrent with predefined unit +// as SizeB1024(0). +func InvertedCurrentKibiByte(format string, wcc ...WC) Decorator { + return InvertedCurrent(SizeB1024(0), format, wcc...) +} + +// InvertedCurrentKiloByte is a wrapper around InvertedCurrent with predefined unit +// as SizeB1000(0). +func InvertedCurrentKiloByte(format string, wcc ...WC) Decorator { + return InvertedCurrent(SizeB1000(0), format, wcc...) +} + +// InvertedCurrent decorator with dynamic unit measure adjustment. +// +// `unit` one of [0|SizeB1024(0)|SizeB1000(0)] +// +// `format` printf compatible verb for InvertedCurrent +// +// `wcc` optional WC config +// +// format example if unit=SizeB1024(0): +// +// format="%d" output: "12MiB" +// format="% d" output: "12 MiB" +// format="%.1f" output: "12.0MiB" +// format="% .1f" output: "12.0 MiB" +// format="%f" output: "12.000000MiB" +// format="% f" output: "12.000000 MiB" +func InvertedCurrent(unit interface{}, format string, wcc ...WC) Decorator { + producer := func() DecorFunc { + switch unit.(type) { + case SizeB1024: + if format == "" { + format = "% d" + } + return func(s Statistics) string { + return fmt.Sprintf(format, SizeB1024(s.Total-s.Current)) + } + case SizeB1000: + if format == "" { + format = "% d" + } + return func(s Statistics) string { + return fmt.Sprintf(format, SizeB1000(s.Total-s.Current)) + } + default: + if format == "" { + format = "%d" + } + return func(s Statistics) string { + return fmt.Sprintf(format, s.Total-s.Current) + } + } + } + return Any(producer(), wcc...) +} diff --git a/decor/decorator.go b/decor/decorator.go new file mode 100644 index 0000000..f537d3f --- /dev/null +++ b/decor/decorator.go @@ -0,0 +1,186 @@ +package decor + +import ( + "fmt" + "time" + + "github.com/mattn/go-runewidth" +) + +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 +) + +// TimeStyle enum. +type TimeStyle int + +// TimeStyle kinds. +const ( + ET_STYLE_GO TimeStyle = iota + ET_STYLE_HHMMSS + ET_STYLE_HHMM + ET_STYLE_MMSS +) + +// Statistics contains fields which are necessary for implementing +// `decor.Decorator` and `mpb.BarFiller` interfaces. +type Statistics struct { + AvailableWidth int // calculated width initially equal to terminal width + RequestedWidth int // width set by `mpb.WithWidth` + ID int + Total int64 + Current int64 + Refill int64 + Completed bool + Aborted bool +} + +// Decorator interface. +// Most of the time there is no need to implement this interface +// manually, as decor package already provides a wide range of decorators +// which implement this interface. If however built-in decorators don't +// meet your needs, you're free to implement your own one by implementing +// this particular interface. The easy way to go is to convert a +// `DecorFunc` into a `Decorator` interface by using provided +// `func Any(DecorFunc, ...WC) Decorator`. +type Decorator interface { + Synchronizer + Formatter + Decor(Statistics) (str string, viewWidth int) +} + +// DecorFunc func type. +// To be used with `func Any(DecorFunc, ...WC) Decorator`. +type DecorFunc func(Statistics) string + +// Synchronizer interface. +// All decorators implement this interface implicitly. Its Sync +// method exposes width sync channel, if DSyncWidth bit is set. +type Synchronizer interface { + Sync() (chan int, bool) +} + +// Formatter interface. +// Format method needs to be called from within Decorator.Decor method +// in order to format string according to decor.WC settings. +// No need to implement manually as long as decor.WC is embedded. +type Formatter interface { + Format(string) (str string, viewWidth int) +} + +// Wrapper interface. +// If you're implementing custom Decorator by wrapping a built-in one, +// it is necessary to implement this interface to retain functionality +// of built-in Decorator. +type Wrapper interface { + Unwrap() Decorator +} + +// EwmaDecorator interface. +// EWMA based decorators should implement this one. +type EwmaDecorator interface { + EwmaUpdate(int64, time.Duration) +} + +// AverageDecorator interface. +// Average decorators should implement this interface to provide start +// time adjustment facility, for resume-able tasks. +type AverageDecorator interface { + AverageAdjust(time.Time) +} + +// ShutdownListener interface. +// If decorator needs to be notified once upon bar shutdown event, so +// this is the right interface to implement. +type ShutdownListener interface { + OnShutdown() +} + +// Global convenience instances of WC with sync width bit set. +// To be used with multiple bars only, i.e. not effective for single bar usage. +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. +// A decorator should embed WC, to enable width synchronization. +type WC struct { + W int + C int + fill func(s string, w int) string + wsync chan int +} + +// Format should be called by any Decorator implementation. +// Returns formatted string and its view (visual) width. +func (wc WC) Format(str string) (string, int) { + viewWidth := runewidth.StringWidth(str) + if wc.W > viewWidth { + viewWidth = wc.W + } + if (wc.C & DSyncWidth) != 0 { + if (wc.C & DextraSpace) != 0 { + viewWidth++ + } + wc.wsync <- viewWidth + viewWidth = <-wc.wsync + } + return wc.fill(str, viewWidth), viewWidth +} + +// Init initializes width related config. +func (wc *WC) Init() WC { + if (wc.C & DidentRight) != 0 { + wc.fill = runewidth.FillRight + } else { + wc.fill = runewidth.FillLeft + } + if (wc.C & DSyncWidth) != 0 { + // it's deliberate choice to override wsync on each Init() call, + // this way globals like WCSyncSpace can be reused + wc.wsync = make(chan int) + } + return *wc +} + +// Sync is implementation of Synchronizer interface. +func (wc WC) Sync() (chan int, bool) { + if (wc.C&DSyncWidth) != 0 && wc.wsync == nil { + panic(fmt.Sprintf("%T is not initialized", wc)) + } + return wc.wsync, (wc.C & DSyncWidth) != 0 +} + +func initWC(wcc ...WC) WC { + var wc WC + for _, nwc := range wcc { + wc = nwc + } + return wc.Init() +} diff --git a/decor/doc.go b/decor/doc.go new file mode 100644 index 0000000..d41aa50 --- /dev/null +++ b/decor/doc.go @@ -0,0 +1,19 @@ +// Package decor provides common decorators for "github.com/vbauerster/mpb/v8" module. +// +// 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..6cee7d1 --- /dev/null +++ b/decor/elapsed.go @@ -0,0 +1,33 @@ +package decor + +import ( + "time" +) + +// Elapsed decorator. It's wrapper of NewElapsed. +// +// `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS] +// +// `wcc` optional WC config +func Elapsed(style TimeStyle, wcc ...WC) Decorator { + return NewElapsed(style, time.Now(), wcc...) +} + +// NewElapsed returns elapsed time decorator. +// +// `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS] +// +// `startTime` start time +// +// `wcc` optional WC config +func NewElapsed(style TimeStyle, startTime time.Time, wcc ...WC) Decorator { + var msg string + producer := chooseTimeProducer(style) + fn := func(s Statistics) string { + if !s.Completed { + msg = producer(time.Since(startTime)) + } + return msg + } + return Any(fn, wcc...) +} diff --git a/decor/eta.go b/decor/eta.go new file mode 100644 index 0000000..ecb6f8f --- /dev/null +++ b/decor/eta.go @@ -0,0 +1,205 @@ +package decor + +import ( + "fmt" + "math" + "time" + + "github.com/VividCortex/ewma" +) + +var ( + _ Decorator = (*movingAverageETA)(nil) + _ EwmaDecorator = (*movingAverageETA)(nil) + _ Decorator = (*averageETA)(nil) + _ AverageDecorator = (*averageETA)(nil) +) + +// TimeNormalizer interface. Implementors could be passed into +// MovingAverageETA, in order to affect i.e. normalize its output. +type TimeNormalizer interface { + Normalize(time.Duration) time.Duration +} + +// TimeNormalizerFunc is function type adapter to convert function +// into TimeNormalizer. +type TimeNormalizerFunc func(time.Duration) time.Duration + +func (f TimeNormalizerFunc) Normalize(src time.Duration) time.Duration { + return f(src) +} + +// EwmaETA exponential-weighted-moving-average based ETA decorator. For this +// decorator to work correctly you have to measure each iteration's duration +// and pass it to one of the (*Bar).EwmaIncr... family methods. +func EwmaETA(style TimeStyle, age float64, wcc ...WC) Decorator { + var average ewma.MovingAverage + if age == 0 { + average = ewma.NewMovingAverage() + } else { + average = ewma.NewMovingAverage(age) + } + return MovingAverageETA(style, NewThreadSafeMovingAverage(average), nil, 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` implementation of MovingAverage interface +// +// `normalizer` available implementations are [FixedIntervalTimeNormalizer|MaxTolerateTimeNormalizer] +// +// `wcc` optional WC config +func MovingAverageETA(style TimeStyle, average ewma.MovingAverage, normalizer TimeNormalizer, wcc ...WC) Decorator { + d := &movingAverageETA{ + WC: initWC(wcc...), + average: average, + normalizer: normalizer, + producer: chooseTimeProducer(style), + } + return d +} + +type movingAverageETA struct { + WC + average ewma.MovingAverage + normalizer TimeNormalizer + producer func(time.Duration) string +} + +func (d *movingAverageETA) Decor(s Statistics) (string, int) { + v := math.Round(d.average.Value()) + remaining := time.Duration((s.Total - s.Current) * int64(v)) + if d.normalizer != nil { + remaining = d.normalizer.Normalize(remaining) + } + return d.Format(d.producer(remaining)) +} + +func (d *movingAverageETA) EwmaUpdate(n int64, dur time.Duration) { + durPerItem := float64(dur) / float64(n) + if math.IsInf(durPerItem, 0) || math.IsNaN(durPerItem) { + return + } + d.average.Add(durPerItem) +} + +// AverageETA decorator. It's wrapper of NewAverageETA. +// +// `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS] +// +// `wcc` optional WC config +func AverageETA(style TimeStyle, wcc ...WC) Decorator { + return NewAverageETA(style, time.Now(), nil, wcc...) +} + +// NewAverageETA decorator with user provided start time. +// +// `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS] +// +// `startTime` start time +// +// `normalizer` available implementations are [FixedIntervalTimeNormalizer|MaxTolerateTimeNormalizer] +// +// `wcc` optional WC config +func NewAverageETA(style TimeStyle, startTime time.Time, normalizer TimeNormalizer, wcc ...WC) Decorator { + d := &averageETA{ + WC: initWC(wcc...), + startTime: startTime, + normalizer: normalizer, + producer: chooseTimeProducer(style), + } + return d +} + +type averageETA struct { + WC + startTime time.Time + normalizer TimeNormalizer + producer func(time.Duration) string +} + +func (d *averageETA) Decor(s Statistics) (string, int) { + var remaining time.Duration + if s.Current != 0 { + durPerItem := float64(time.Since(d.startTime)) / float64(s.Current) + durPerItem = math.Round(durPerItem) + remaining = time.Duration((s.Total - s.Current) * int64(durPerItem)) + if d.normalizer != nil { + remaining = d.normalizer.Normalize(remaining) + } + } + return d.Format(d.producer(remaining)) +} + +func (d *averageETA) AverageAdjust(startTime time.Time) { + d.startTime = startTime +} + +// MaxTolerateTimeNormalizer returns implementation of TimeNormalizer. +func MaxTolerateTimeNormalizer(maxTolerate time.Duration) TimeNormalizer { + var normalized time.Duration + var lastCall time.Time + return TimeNormalizerFunc(func(remaining time.Duration) time.Duration { + if diff := normalized - remaining; diff <= 0 || diff > maxTolerate || remaining < time.Minute { + normalized = remaining + lastCall = time.Now() + return remaining + } + normalized -= time.Since(lastCall) + lastCall = time.Now() + return normalized + }) +} + +// FixedIntervalTimeNormalizer returns implementation of TimeNormalizer. +func FixedIntervalTimeNormalizer(updInterval int) TimeNormalizer { + var normalized time.Duration + var lastCall time.Time + var count int + return TimeNormalizerFunc(func(remaining time.Duration) time.Duration { + if count == 0 || remaining < time.Minute { + count = updInterval + normalized = remaining + lastCall = time.Now() + return remaining + } + count-- + normalized -= time.Since(lastCall) + lastCall = time.Now() + return normalized + }) +} + +func chooseTimeProducer(style TimeStyle) func(time.Duration) string { + switch style { + case ET_STYLE_HHMMSS: + return func(remaining time.Duration) string { + hours := int64(remaining/time.Hour) % 60 + minutes := int64(remaining/time.Minute) % 60 + seconds := int64(remaining/time.Second) % 60 + return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds) + } + case ET_STYLE_HHMM: + return func(remaining time.Duration) string { + hours := int64(remaining/time.Hour) % 60 + minutes := int64(remaining/time.Minute) % 60 + return fmt.Sprintf("%02d:%02d", hours, minutes) + } + case ET_STYLE_MMSS: + return func(remaining time.Duration) string { + hours := int64(remaining/time.Hour) % 60 + minutes := int64(remaining/time.Minute) % 60 + seconds := int64(remaining/time.Second) % 60 + if hours > 0 { + return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds) + } + return fmt.Sprintf("%02d:%02d", minutes, seconds) + } + default: + return func(remaining time.Duration) string { + return remaining.Truncate(time.Second).String() + } + } +} diff --git a/decor/meta.go b/decor/meta.go new file mode 100644 index 0000000..0045a31 --- /dev/null +++ b/decor/meta.go @@ -0,0 +1,34 @@ +package decor + +var ( + _ Decorator = metaWrapper{} + _ Wrapper = metaWrapper{} +) + +// Meta wrap decorator. +// Provided fn is supposed to wrap output of given decorator +// with meta information like ANSI escape codes for example. +// Primary usage intention is to set SGR display attributes. +// +// `decorator` Decorator to wrap +// `fn` func to apply meta information +func Meta(decorator Decorator, fn func(string) string) Decorator { + if decorator == nil { + return nil + } + return metaWrapper{decorator, fn} +} + +type metaWrapper struct { + Decorator + fn func(string) string +} + +func (d metaWrapper) Decor(s Statistics) (string, int) { + str, width := d.Decorator.Decor(s) + return d.fn(str), width +} + +func (d metaWrapper) Unwrap() Decorator { + return d.Decorator +} diff --git a/decor/moving_average.go b/decor/moving_average.go new file mode 100644 index 0000000..a1be8ad --- /dev/null +++ b/decor/moving_average.go @@ -0,0 +1,74 @@ +package decor + +import ( + "sort" + "sync" + + "github.com/VividCortex/ewma" +) + +var ( + _ ewma.MovingAverage = (*threadSafeMovingAverage)(nil) + _ ewma.MovingAverage = (*medianWindow)(nil) + _ sort.Interface = (*medianWindow)(nil) +) + +type threadSafeMovingAverage struct { + ewma.MovingAverage + mu sync.Mutex +} + +func (s *threadSafeMovingAverage) Add(value float64) { + s.mu.Lock() + s.MovingAverage.Add(value) + s.mu.Unlock() +} + +func (s *threadSafeMovingAverage) Value() float64 { + s.mu.Lock() + defer s.mu.Unlock() + return s.MovingAverage.Value() +} + +func (s *threadSafeMovingAverage) Set(value float64) { + s.mu.Lock() + s.MovingAverage.Set(value) + s.mu.Unlock() +} + +// NewThreadSafeMovingAverage converts provided ewma.MovingAverage +// into thread safe ewma.MovingAverage. +func NewThreadSafeMovingAverage(average ewma.MovingAverage) ewma.MovingAverage { + if tsma, ok := average.(*threadSafeMovingAverage); ok { + return tsma + } + return &threadSafeMovingAverage{MovingAverage: average} +} + +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() ewma.MovingAverage { + return NewThreadSafeMovingAverage(new(medianWindow)) +} diff --git a/decor/name.go b/decor/name.go new file mode 100644 index 0000000..31ac123 --- /dev/null +++ b/decor/name.go @@ -0,0 +1,11 @@ +package decor + +// Name decorator displays text that is set once and can't be changed +// during decorator's lifetime. +// +// `str` string to display +// +// `wcc` optional WC config +func Name(str string, wcc ...WC) Decorator { + return Any(func(Statistics) string { return str }, wcc...) +} diff --git a/decor/on_abort.go b/decor/on_abort.go new file mode 100644 index 0000000..50a1dfb --- /dev/null +++ b/decor/on_abort.go @@ -0,0 +1,68 @@ +package decor + +var ( + _ Decorator = onAbortWrapper{} + _ Wrapper = onAbortWrapper{} + _ Decorator = onAbortMetaWrapper{} + _ Wrapper = onAbortMetaWrapper{} +) + +// OnAbort wrap decorator. +// Displays provided message on abort event. +// Has no effect if bar.Abort(true) is called. +// +// `decorator` Decorator to wrap +// `message` message to display +func OnAbort(decorator Decorator, message string) Decorator { + if decorator == nil { + return nil + } + return onAbortWrapper{decorator, message} +} + +type onAbortWrapper struct { + Decorator + msg string +} + +func (d onAbortWrapper) Decor(s Statistics) (string, int) { + if s.Aborted { + return d.Format(d.msg) + } + return d.Decorator.Decor(s) +} + +func (d onAbortWrapper) Unwrap() Decorator { + return d.Decorator +} + +// OnAbortMeta wrap decorator. +// Provided fn is supposed to wrap output of given decorator +// with meta information like ANSI escape codes for example. +// Primary usage intention is to set SGR display attributes. +// +// `decorator` Decorator to wrap +// `fn` func to apply meta information +func OnAbortMeta(decorator Decorator, fn func(string) string) Decorator { + if decorator == nil { + return nil + } + return onAbortMetaWrapper{decorator, fn} +} + +type onAbortMetaWrapper struct { + Decorator + fn func(string) string +} + +func (d onAbortMetaWrapper) Decor(s Statistics) (string, int) { + if s.Completed { + str, width := d.Decorator.Decor(s) + return d.fn(str), width + } + return d.Decorator.Decor(s) +} + +func (d onAbortMetaWrapper) Unwrap() Decorator { + return d.Decorator +} diff --git a/decor/on_compete_or_on_abort.go b/decor/on_compete_or_on_abort.go new file mode 100644 index 0000000..f9ca841 --- /dev/null +++ b/decor/on_compete_or_on_abort.go @@ -0,0 +1,21 @@ +package decor + +// OnCompleteOrOnAbort wrap decorator. +// Displays provided message on complete or on abort event. +// +// `decorator` Decorator to wrap +// `message` message to display +func OnCompleteOrOnAbort(decorator Decorator, message string) Decorator { + return OnComplete(OnAbort(decorator, message), message) +} + +// OnCompleteMetaOrOnAbortMeta wrap decorator. +// Provided fn is supposed to wrap output of given decorator +// with meta information like ANSI escape codes for example. +// Primary usage intention is to set SGR display attributes. +// +// `decorator` Decorator to wrap +// `fn` func to apply meta information +func OnCompleteMetaOrOnAbortMeta(decorator Decorator, fn func(string) string) Decorator { + return OnCompleteMeta(OnAbortMeta(decorator, fn), fn) +} diff --git a/decor/on_complete.go b/decor/on_complete.go new file mode 100644 index 0000000..f18b5a6 --- /dev/null +++ b/decor/on_complete.go @@ -0,0 +1,67 @@ +package decor + +var ( + _ Decorator = onCompleteWrapper{} + _ Wrapper = onCompleteWrapper{} + _ Decorator = onCompleteMetaWrapper{} + _ Wrapper = onCompleteMetaWrapper{} +) + +// OnComplete wrap decorator. +// Displays provided message on complete event. +// +// `decorator` Decorator to wrap +// `message` message to display +func OnComplete(decorator Decorator, message string) Decorator { + if decorator == nil { + return nil + } + return onCompleteWrapper{decorator, message} +} + +type onCompleteWrapper struct { + Decorator + msg string +} + +func (d onCompleteWrapper) Decor(s Statistics) (string, int) { + if s.Completed { + return d.Format(d.msg) + } + return d.Decorator.Decor(s) +} + +func (d onCompleteWrapper) Unwrap() Decorator { + return d.Decorator +} + +// OnCompleteMeta wrap decorator. +// Provided fn is supposed to wrap output of given decorator +// with meta information like ANSI escape codes for example. +// Primary usage intention is to set SGR display attributes. +// +// `decorator` Decorator to wrap +// `fn` func to apply meta information +func OnCompleteMeta(decorator Decorator, fn func(string) string) Decorator { + if decorator == nil { + return nil + } + return onCompleteMetaWrapper{decorator, fn} +} + +type onCompleteMetaWrapper struct { + Decorator + fn func(string) string +} + +func (d onCompleteMetaWrapper) Decor(s Statistics) (string, int) { + if s.Completed { + str, width := d.Decorator.Decor(s) + return d.fn(str), width + } + return d.Decorator.Decor(s) +} + +func (d onCompleteMetaWrapper) Unwrap() Decorator { + return d.Decorator +} diff --git a/decor/on_condition.go b/decor/on_condition.go new file mode 100644 index 0000000..f4626c3 --- /dev/null +++ b/decor/on_condition.go @@ -0,0 +1,51 @@ +package decor + +// OnCondition applies decorator only if a condition is true. +// +// `decorator` Decorator +// +// `cond` bool +func OnCondition(decorator Decorator, cond bool) Decorator { + return Conditional(cond, decorator, nil) +} + +// OnPredicate applies decorator only if a predicate evaluates to true. +// +// `decorator` Decorator +// +// `predicate` func() bool +func OnPredicate(decorator Decorator, predicate func() bool) Decorator { + return Predicative(predicate, decorator, nil) +} + +// Conditional returns decorator `a` if condition is true, otherwise +// decorator `b`. +// +// `cond` bool +// +// `a` Decorator +// +// `b` Decorator +func Conditional(cond bool, a, b Decorator) Decorator { + if cond { + return a + } else { + return b + } +} + +// Predicative returns decorator `a` if predicate evaluates to true, +// otherwise decorator `b`. +// +// `predicate` func() bool +// +// `a` Decorator +// +// `b` Decorator +func Predicative(predicate func() bool, a, b Decorator) Decorator { + if predicate() { + return a + } else { + return b + } +} diff --git a/decor/percentage.go b/decor/percentage.go new file mode 100644 index 0000000..9709c19 --- /dev/null +++ b/decor/percentage.go @@ -0,0 +1,68 @@ +package decor + +import ( + "fmt" + "strconv" + + "github.com/vbauerster/mpb/v8/internal" +) + +var _ fmt.Formatter = percentageType(0) + +type percentageType float64 + +func (s percentageType) Format(st fmt.State, verb rune) { + prec := -1 + switch verb { + case 'f', 'e', 'E': + prec = 6 // default prec of fmt.Printf("%f|%e|%E") + fallthrough + case 'b', 'g', 'G', 'x', 'X': + if p, ok := st.Precision(); ok { + prec = p + } + default: + verb, prec = 'f', 0 + } + + b := strconv.AppendFloat(make([]byte, 0, 16), float64(s), byte(verb), prec, 64) + if st.Flag(' ') { + b = append(b, ' ', '%') + } else { + b = append(b, '%') + } + _, err := st.Write(b) + if err != nil { + panic(err) + } +} + +// Percentage returns percentage decorator. It's a wrapper of NewPercentage. +func Percentage(wcc ...WC) Decorator { + return NewPercentage("% d", wcc...) +} + +// NewPercentage percentage decorator with custom format string. +// +// `format` printf compatible verb +// +// `wcc` optional WC config +// +// format examples: +// +// format="%d" output: "1%" +// format="% d" output: "1 %" +// format="%.1f" output: "1.0%" +// format="% .1f" output: "1.0 %" +// format="%f" output: "1.000000%" +// format="% f" output: "1.000000 %" +func NewPercentage(format string, wcc ...WC) Decorator { + if format == "" { + format = "% d" + } + f := func(s Statistics) string { + p := internal.Percentage(s.Total, s.Current, 100) + return fmt.Sprintf(format, percentageType(p)) + } + return Any(f, wcc...) +} diff --git a/decor/percentage_test.go b/decor/percentage_test.go new file mode 100644 index 0000000..bbddf5b --- /dev/null +++ b/decor/percentage_test.go @@ -0,0 +1,58 @@ +package decor + +import ( + "fmt" + "testing" +) + +func TestPercentageType(t *testing.T) { + cases := map[string]struct { + value float64 + verb string + expected string + }{ + "10 %d": {10, "%d", "10%"}, + "10 %s": {10, "%s", "10%"}, + "10 %f": {10, "%f", "10.000000%"}, + "10 %.6f": {10, "%.6f", "10.000000%"}, + "10 %.0f": {10, "%.0f", "10%"}, + "10 %.1f": {10, "%.1f", "10.0%"}, + "10 %.2f": {10, "%.2f", "10.00%"}, + "10 %.3f": {10, "%.3f", "10.000%"}, + + "10 % d": {10, "% d", "10 %"}, + "10 % s": {10, "% s", "10 %"}, + "10 % f": {10, "% f", "10.000000 %"}, + "10 % .6f": {10, "% .6f", "10.000000 %"}, + "10 % .0f": {10, "% .0f", "10 %"}, + "10 % .1f": {10, "% .1f", "10.0 %"}, + "10 % .2f": {10, "% .2f", "10.00 %"}, + "10 % .3f": {10, "% .3f", "10.000 %"}, + + "10.5 %d": {10.5, "%d", "10%"}, + "10.5 %s": {10.5, "%s", "10%"}, + "10.5 %f": {10.5, "%f", "10.500000%"}, + "10.5 %.6f": {10.5, "%.6f", "10.500000%"}, + "10.5 %.0f": {10.5, "%.0f", "10%"}, + "10.5 %.1f": {10.5, "%.1f", "10.5%"}, + "10.5 %.2f": {10.5, "%.2f", "10.50%"}, + "10.5 %.3f": {10.5, "%.3f", "10.500%"}, + + "10.5 % d": {10.5, "% d", "10 %"}, + "10.5 % s": {10.5, "% s", "10 %"}, + "10.5 % f": {10.5, "% f", "10.500000 %"}, + "10.5 % .6f": {10.5, "% .6f", "10.500000 %"}, + "10.5 % .0f": {10.5, "% .0f", "10 %"}, + "10.5 % .1f": {10.5, "% .1f", "10.5 %"}, + "10.5 % .2f": {10.5, "% .2f", "10.50 %"}, + "10.5 % .3f": {10.5, "% .3f", "10.500 %"}, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := fmt.Sprintf(tc.verb, percentageType(tc.value)) + if got != tc.expected { + t.Fatalf("expected: %q, got: %q\n", tc.expected, got) + } + }) + } +} diff --git a/decor/size_type.go b/decor/size_type.go new file mode 100644 index 0000000..d9950b6 --- /dev/null +++ b/decor/size_type.go @@ -0,0 +1,120 @@ +package decor + +import ( + "fmt" + "strconv" +) + +//go:generate stringer -type=SizeB1024 -trimprefix=_i +//go:generate stringer -type=SizeB1000 -trimprefix=_ + +var ( + _ fmt.Formatter = SizeB1024(0) + _ fmt.Stringer = SizeB1024(0) + _ fmt.Formatter = SizeB1000(0) + _ fmt.Stringer = SizeB1000(0) +) + +const ( + _ib SizeB1024 = iota + 1 + _iKiB SizeB1024 = 1 << (iota * 10) + _iMiB + _iGiB + _iTiB +) + +// SizeB1024 named type, which implements fmt.Formatter interface. It +// adjusts its value according to byte size multiple by 1024 and appends +// appropriate size marker (KiB, MiB, GiB, TiB). +type SizeB1024 int64 + +func (self SizeB1024) Format(st fmt.State, verb rune) { + prec := -1 + switch verb { + case 'f', 'e', 'E': + prec = 6 // default prec of fmt.Printf("%f|%e|%E") + fallthrough + case 'b', 'g', 'G', 'x', 'X': + if p, ok := st.Precision(); ok { + prec = p + } + default: + verb, prec = 'f', 0 + } + + var unit SizeB1024 + switch { + case self < _iKiB: + unit = _ib + case self < _iMiB: + unit = _iKiB + case self < _iGiB: + unit = _iMiB + case self < _iTiB: + unit = _iGiB + default: + unit = _iTiB + } + + b := strconv.AppendFloat(make([]byte, 0, 24), float64(self)/float64(unit), byte(verb), prec, 64) + if st.Flag(' ') { + b = append(b, ' ') + } + b = append(b, []byte(unit.String())...) + _, err := st.Write(b) + if err != nil { + panic(err) + } +} + +const ( + _b SizeB1000 = 1 + _KB SizeB1000 = _b * 1000 + _MB SizeB1000 = _KB * 1000 + _GB SizeB1000 = _MB * 1000 + _TB SizeB1000 = _GB * 1000 +) + +// SizeB1000 named type, which implements fmt.Formatter interface. It +// adjusts its value according to byte size multiple by 1000 and appends +// appropriate size marker (KB, MB, GB, TB). +type SizeB1000 int64 + +func (self SizeB1000) Format(st fmt.State, verb rune) { + prec := -1 + switch verb { + case 'f', 'e', 'E': + prec = 6 // default prec of fmt.Printf("%f|%e|%E") + fallthrough + case 'b', 'g', 'G', 'x', 'X': + if p, ok := st.Precision(); ok { + prec = p + } + default: + verb, prec = 'f', 0 + } + + var unit SizeB1000 + switch { + case self < _KB: + unit = _b + case self < _MB: + unit = _KB + case self < _GB: + unit = _MB + case self < _TB: + unit = _GB + default: + unit = _TB + } + + b := strconv.AppendFloat(make([]byte, 0, 24), float64(self)/float64(unit), byte(verb), prec, 64) + if st.Flag(' ') { + b = append(b, ' ') + } + b = append(b, []byte(unit.String())...) + _, err := st.Write(b) + if err != nil { + panic(err) + } +} diff --git a/decor/size_type_test.go b/decor/size_type_test.go new file mode 100644 index 0000000..117d9fb --- /dev/null +++ b/decor/size_type_test.go @@ -0,0 +1,160 @@ +package decor + +import ( + "fmt" + "testing" +) + +func TestB1024(t *testing.T) { + cases := map[string]struct { + value int64 + verb string + expected string + }{ + "verb %d": {12345678, "%d", "12MiB"}, + "verb %s": {12345678, "%s", "12MiB"}, + "verb %f": {12345678, "%f", "11.773756MiB"}, + "verb %.6f": {12345678, "%.6f", "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 % d": {12345678, "% d", "12 MiB"}, + "verb % s": {12345678, "% s", "12 MiB"}, + "verb % f": {12345678, "% f", "11.773756 MiB"}, + "verb % .6f": {12345678, "% .6f", "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"}, + + "1000 %d": {1000, "%d", "1000b"}, + "1000 %s": {1000, "%s", "1000b"}, + "1000 %f": {1000, "%f", "1000.000000b"}, + "1000 %.6f": {1000, "%.6f", "1000.000000b"}, + "1000 %.0f": {1000, "%.0f", "1000b"}, + "1000 %.1f": {1000, "%.1f", "1000.0b"}, + "1000 %.2f": {1000, "%.2f", "1000.00b"}, + "1000 %.3f": {1000, "%.3f", "1000.000b"}, + "1024 %d": {1024, "%d", "1KiB"}, + "1024 %s": {1024, "%s", "1KiB"}, + "1024 %f": {1024, "%f", "1.000000KiB"}, + "1024 %.6f": {1024, "%.6f", "1.000000KiB"}, + "1024 %.0f": {1024, "%.0f", "1KiB"}, + "1024 %.1f": {1024, "%.1f", "1.0KiB"}, + "1024 %.2f": {1024, "%.2f", "1.00KiB"}, + "1024 %.3f": {1024, "%.3f", "1.000KiB"}, + + "3*MiB+100KiB %d": {3*int64(_iMiB) + 100*int64(_iKiB), "%d", "3MiB"}, + "3*MiB+100KiB %s": {3*int64(_iMiB) + 100*int64(_iKiB), "%s", "3MiB"}, + "3*MiB+100KiB %f": {3*int64(_iMiB) + 100*int64(_iKiB), "%f", "3.097656MiB"}, + "3*MiB+100KiB %.6f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.6f", "3.097656MiB"}, + "3*MiB+100KiB %.0f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.0f", "3MiB"}, + "3*MiB+100KiB %.1f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.1f", "3.1MiB"}, + "3*MiB+100KiB %.2f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.2f", "3.10MiB"}, + "3*MiB+100KiB %.3f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.3f", "3.098MiB"}, + + "2*GiB %d": {2 * int64(_iGiB), "%d", "2GiB"}, + "2*GiB %s": {2 * int64(_iGiB), "%s", "2GiB"}, + "2*GiB %f": {2 * int64(_iGiB), "%f", "2.000000GiB"}, + "2*GiB %.6f": {2 * int64(_iGiB), "%.6f", "2.000000GiB"}, + "2*GiB %.0f": {2 * int64(_iGiB), "%.0f", "2GiB"}, + "2*GiB %.1f": {2 * int64(_iGiB), "%.1f", "2.0GiB"}, + "2*GiB %.2f": {2 * int64(_iGiB), "%.2f", "2.00GiB"}, + "2*GiB %.3f": {2 * int64(_iGiB), "%.3f", "2.000GiB"}, + + "4*TiB %d": {4 * int64(_iTiB), "%d", "4TiB"}, + "4*TiB %s": {4 * int64(_iTiB), "%s", "4TiB"}, + "4*TiB %f": {4 * int64(_iTiB), "%f", "4.000000TiB"}, + "4*TiB %.6f": {4 * int64(_iTiB), "%.6f", "4.000000TiB"}, + "4*TiB %.0f": {4 * int64(_iTiB), "%.0f", "4TiB"}, + "4*TiB %.1f": {4 * int64(_iTiB), "%.1f", "4.0TiB"}, + "4*TiB %.2f": {4 * int64(_iTiB), "%.2f", "4.00TiB"}, + "4*TiB %.3f": {4 * int64(_iTiB), "%.3f", "4.000TiB"}, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := fmt.Sprintf(tc.verb, SizeB1024(tc.value)) + if got != tc.expected { + t.Fatalf("expected: %q, got: %q\n", tc.expected, got) + } + }) + } +} + +func TestB1000(t *testing.T) { + cases := map[string]struct { + value int64 + verb string + expected string + }{ + "verb %d": {12345678, "%d", "12MB"}, + "verb %s": {12345678, "%s", "12MB"}, + "verb %f": {12345678, "%f", "12.345678MB"}, + "verb %.6f": {12345678, "%.6f", "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 % d": {12345678, "% d", "12 MB"}, + "verb % s": {12345678, "% s", "12 MB"}, + "verb % f": {12345678, "% f", "12.345678 MB"}, + "verb % .6f": {12345678, "% .6f", "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"}, + + "1000 %d": {1000, "%d", "1KB"}, + "1000 %s": {1000, "%s", "1KB"}, + "1000 %f": {1000, "%f", "1.000000KB"}, + "1000 %.6f": {1000, "%.6f", "1.000000KB"}, + "1000 %.0f": {1000, "%.0f", "1KB"}, + "1000 %.1f": {1000, "%.1f", "1.0KB"}, + "1000 %.2f": {1000, "%.2f", "1.00KB"}, + "1000 %.3f": {1000, "%.3f", "1.000KB"}, + "1024 %d": {1024, "%d", "1KB"}, + "1024 %s": {1024, "%s", "1KB"}, + "1024 %f": {1024, "%f", "1.024000KB"}, + "1024 %.6f": {1024, "%.6f", "1.024000KB"}, + "1024 %.0f": {1024, "%.0f", "1KB"}, + "1024 %.1f": {1024, "%.1f", "1.0KB"}, + "1024 %.2f": {1024, "%.2f", "1.02KB"}, + "1024 %.3f": {1024, "%.3f", "1.024KB"}, + + "3*MB+100*KB %d": {3*int64(_MB) + 100*int64(_KB), "%d", "3MB"}, + "3*MB+100*KB %s": {3*int64(_MB) + 100*int64(_KB), "%s", "3MB"}, + "3*MB+100*KB %f": {3*int64(_MB) + 100*int64(_KB), "%f", "3.100000MB"}, + "3*MB+100*KB %.6f": {3*int64(_MB) + 100*int64(_KB), "%.6f", "3.100000MB"}, + "3*MB+100*KB %.0f": {3*int64(_MB) + 100*int64(_KB), "%.0f", "3MB"}, + "3*MB+100*KB %.1f": {3*int64(_MB) + 100*int64(_KB), "%.1f", "3.1MB"}, + "3*MB+100*KB %.2f": {3*int64(_MB) + 100*int64(_KB), "%.2f", "3.10MB"}, + "3*MB+100*KB %.3f": {3*int64(_MB) + 100*int64(_KB), "%.3f", "3.100MB"}, + + "2*GB %d": {2 * int64(_GB), "%d", "2GB"}, + "2*GB %s": {2 * int64(_GB), "%s", "2GB"}, + "2*GB %f": {2 * int64(_GB), "%f", "2.000000GB"}, + "2*GB %.6f": {2 * int64(_GB), "%.6f", "2.000000GB"}, + "2*GB %.0f": {2 * int64(_GB), "%.0f", "2GB"}, + "2*GB %.1f": {2 * int64(_GB), "%.1f", "2.0GB"}, + "2*GB %.2f": {2 * int64(_GB), "%.2f", "2.00GB"}, + "2*GB %.3f": {2 * int64(_GB), "%.3f", "2.000GB"}, + + "4*TB %d": {4 * int64(_TB), "%d", "4TB"}, + "4*TB %s": {4 * int64(_TB), "%s", "4TB"}, + "4*TB %f": {4 * int64(_TB), "%f", "4.000000TB"}, + "4*TB %.6f": {4 * int64(_TB), "%.6f", "4.000000TB"}, + "4*TB %.0f": {4 * int64(_TB), "%.0f", "4TB"}, + "4*TB %.1f": {4 * int64(_TB), "%.1f", "4.0TB"}, + "4*TB %.2f": {4 * int64(_TB), "%.2f", "4.00TB"}, + "4*TB %.3f": {4 * int64(_TB), "%.3f", "4.000TB"}, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := fmt.Sprintf(tc.verb, SizeB1000(tc.value)) + if got != tc.expected { + t.Fatalf("expected: %q, got: %q\n", tc.expected, got) + } + }) + } +} diff --git a/decor/sizeb1000_string.go b/decor/sizeb1000_string.go new file mode 100644 index 0000000..3f32ef7 --- /dev/null +++ b/decor/sizeb1000_string.go @@ -0,0 +1,41 @@ +// Code generated by "stringer -type=SizeB1000 -trimprefix=_"; DO NOT EDIT. + +package decor + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[_b-1] + _ = x[_KB-1000] + _ = x[_MB-1000000] + _ = x[_GB-1000000000] + _ = x[_TB-1000000000000] +} + +const ( + _SizeB1000_name_0 = "b" + _SizeB1000_name_1 = "KB" + _SizeB1000_name_2 = "MB" + _SizeB1000_name_3 = "GB" + _SizeB1000_name_4 = "TB" +) + +func (i SizeB1000) String() string { + switch { + case i == 1: + return _SizeB1000_name_0 + case i == 1000: + return _SizeB1000_name_1 + case i == 1000000: + return _SizeB1000_name_2 + case i == 1000000000: + return _SizeB1000_name_3 + case i == 1000000000000: + return _SizeB1000_name_4 + default: + return "SizeB1000(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/decor/sizeb1024_string.go b/decor/sizeb1024_string.go new file mode 100644 index 0000000..9fca66c --- /dev/null +++ b/decor/sizeb1024_string.go @@ -0,0 +1,41 @@ +// Code generated by "stringer -type=SizeB1024 -trimprefix=_i"; DO NOT EDIT. + +package decor + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[_ib-1] + _ = x[_iKiB-1024] + _ = x[_iMiB-1048576] + _ = x[_iGiB-1073741824] + _ = x[_iTiB-1099511627776] +} + +const ( + _SizeB1024_name_0 = "b" + _SizeB1024_name_1 = "KiB" + _SizeB1024_name_2 = "MiB" + _SizeB1024_name_3 = "GiB" + _SizeB1024_name_4 = "TiB" +) + +func (i SizeB1024) String() string { + switch { + case i == 1: + return _SizeB1024_name_0 + case i == 1024: + return _SizeB1024_name_1 + case i == 1048576: + return _SizeB1024_name_2 + case i == 1073741824: + return _SizeB1024_name_3 + case i == 1099511627776: + return _SizeB1024_name_4 + default: + return "SizeB1024(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/decor/speed.go b/decor/speed.go new file mode 100644 index 0000000..5879d06 --- /dev/null +++ b/decor/speed.go @@ -0,0 +1,179 @@ +package decor + +import ( + "fmt" + "io" + "math" + "time" + + "github.com/VividCortex/ewma" +) + +var ( + _ Decorator = (*movingAverageSpeed)(nil) + _ EwmaDecorator = (*movingAverageSpeed)(nil) + _ Decorator = (*averageSpeed)(nil) + _ AverageDecorator = (*averageSpeed)(nil) +) + +// FmtAsSpeed adds "/s" to the end of the input formatter. To be +// used with SizeB1000 or SizeB1024 types, for example: +// +// fmt.Printf("%.1f", FmtAsSpeed(SizeB1024(2048))) +func FmtAsSpeed(input fmt.Formatter) fmt.Formatter { + return speedFormatter{input} +} + +type speedFormatter struct { + fmt.Formatter +} + +func (self speedFormatter) Format(st fmt.State, verb rune) { + self.Formatter.Format(st, verb) + _, err := io.WriteString(st, "/s") + if err != nil { + panic(err) + } +} + +// EwmaSpeed exponential-weighted-moving-average based speed decorator. +// For this decorator to work correctly you have to measure each iteration's +// duration and pass it to one of the (*Bar).EwmaIncr... family methods. +func EwmaSpeed(unit interface{}, format string, age float64, wcc ...WC) Decorator { + var average ewma.MovingAverage + if age == 0 { + average = ewma.NewMovingAverage() + } else { + average = ewma.NewMovingAverage(age) + } + return MovingAverageSpeed(unit, format, NewThreadSafeMovingAverage(average), wcc...) +} + +// MovingAverageSpeed decorator relies on MovingAverage implementation +// to calculate its average. +// +// `unit` one of [0|SizeB1024(0)|SizeB1000(0)] +// +// `format` printf compatible verb for value, like "%f" or "%d" +// +// `average` MovingAverage implementation +// +// `wcc` optional WC config +// +// format examples: +// +// unit=SizeB1024(0), format="%.1f" output: "1.0MiB/s" +// unit=SizeB1024(0), format="% .1f" output: "1.0 MiB/s" +// unit=SizeB1000(0), format="%.1f" output: "1.0MB/s" +// unit=SizeB1000(0), format="% .1f" output: "1.0 MB/s" +func MovingAverageSpeed(unit interface{}, format string, average ewma.MovingAverage, wcc ...WC) Decorator { + d := &movingAverageSpeed{ + WC: initWC(wcc...), + average: average, + producer: chooseSpeedProducer(unit, format), + } + return d +} + +type movingAverageSpeed struct { + WC + producer func(float64) string + average ewma.MovingAverage + msg string +} + +func (d *movingAverageSpeed) Decor(s Statistics) (string, int) { + if !s.Completed { + var speed float64 + if v := d.average.Value(); v > 0 { + speed = 1 / v + } + d.msg = d.producer(speed * 1e9) + } + return d.Format(d.msg) +} + +func (d *movingAverageSpeed) EwmaUpdate(n int64, dur time.Duration) { + durPerByte := float64(dur) / float64(n) + if math.IsInf(durPerByte, 0) || math.IsNaN(durPerByte) { + return + } + d.average.Add(durPerByte) +} + +// AverageSpeed decorator with dynamic unit measure adjustment. It's +// a wrapper of NewAverageSpeed. +func AverageSpeed(unit interface{}, format string, wcc ...WC) Decorator { + return NewAverageSpeed(unit, format, time.Now(), wcc...) +} + +// NewAverageSpeed decorator with dynamic unit measure adjustment and +// user provided start time. +// +// `unit` one of [0|SizeB1024(0)|SizeB1000(0)] +// +// `format` printf compatible verb for value, like "%f" or "%d" +// +// `startTime` start time +// +// `wcc` optional WC config +// +// format examples: +// +// unit=SizeB1024(0), format="%.1f" output: "1.0MiB/s" +// unit=SizeB1024(0), format="% .1f" output: "1.0 MiB/s" +// unit=SizeB1000(0), format="%.1f" output: "1.0MB/s" +// unit=SizeB1000(0), format="% .1f" output: "1.0 MB/s" +func NewAverageSpeed(unit interface{}, format string, startTime time.Time, wcc ...WC) Decorator { + d := &averageSpeed{ + WC: initWC(wcc...), + startTime: startTime, + producer: chooseSpeedProducer(unit, format), + } + return d +} + +type averageSpeed struct { + WC + startTime time.Time + producer func(float64) string + msg string +} + +func (d *averageSpeed) Decor(s Statistics) (string, int) { + if !s.Completed { + speed := float64(s.Current) / float64(time.Since(d.startTime)) + d.msg = d.producer(speed * 1e9) + } + return d.Format(d.msg) +} + +func (d *averageSpeed) AverageAdjust(startTime time.Time) { + d.startTime = startTime +} + +func chooseSpeedProducer(unit interface{}, format string) func(float64) string { + switch unit.(type) { + case SizeB1024: + if format == "" { + format = "% d" + } + return func(speed float64) string { + return fmt.Sprintf(format, FmtAsSpeed(SizeB1024(math.Round(speed)))) + } + case SizeB1000: + if format == "" { + format = "% d" + } + return func(speed float64) string { + return fmt.Sprintf(format, FmtAsSpeed(SizeB1000(math.Round(speed)))) + } + default: + if format == "" { + format = "%f" + } + return func(speed float64) string { + return fmt.Sprintf(format, speed) + } + } +} diff --git a/decor/speed_test.go b/decor/speed_test.go new file mode 100644 index 0000000..2fe770b --- /dev/null +++ b/decor/speed_test.go @@ -0,0 +1,278 @@ +package decor + +import ( + "testing" + "time" +) + +func TestAverageSpeedSizeB1024(t *testing.T) { + cases := []struct { + name string + fmt string + unit interface{} + current int64 + elapsed time.Duration + expected string + }{ + { + name: "empty fmt", + unit: SizeB1024(0), + fmt: "", + current: 0, + elapsed: time.Second, + expected: "0 b/s", + }, + { + name: "SizeB1024(0):%d:0b", + unit: SizeB1024(0), + fmt: "%d", + current: 0, + elapsed: time.Second, + expected: "0b/s", + }, + { + name: "SizeB1024(0):%f:0b", + unit: SizeB1024(0), + fmt: "%f", + current: 0, + elapsed: time.Second, + expected: "0.000000b/s", + }, + { + name: "SizeB1024(0):% .2f:0b", + unit: SizeB1024(0), + fmt: "% .2f", + current: 0, + elapsed: time.Second, + expected: "0.00 b/s", + }, + { + name: "SizeB1024(0):%d:1b", + unit: SizeB1024(0), + fmt: "%d", + current: 1, + elapsed: time.Second, + expected: "1b/s", + }, + { + name: "SizeB1024(0):% .2f:1b", + unit: SizeB1024(0), + fmt: "% .2f", + current: 1, + elapsed: time.Second, + expected: "1.00 b/s", + }, + { + name: "SizeB1024(0):%d:KiB", + unit: SizeB1024(0), + fmt: "%d", + current: 2 * int64(_iKiB), + elapsed: 1 * time.Second, + expected: "2KiB/s", + }, + { + name: "SizeB1024(0):% .f:KiB", + unit: SizeB1024(0), + fmt: "% .2f", + current: 2 * int64(_iKiB), + elapsed: 1 * time.Second, + expected: "2.00 KiB/s", + }, + { + name: "SizeB1024(0):%d:MiB", + unit: SizeB1024(0), + fmt: "%d", + current: 2 * int64(_iMiB), + elapsed: 1 * time.Second, + expected: "2MiB/s", + }, + { + name: "SizeB1024(0):% .2f:MiB", + unit: SizeB1024(0), + fmt: "% .2f", + current: 2 * int64(_iMiB), + elapsed: 1 * time.Second, + expected: "2.00 MiB/s", + }, + { + name: "SizeB1024(0):%d:GiB", + unit: SizeB1024(0), + fmt: "%d", + current: 2 * int64(_iGiB), + elapsed: 1 * time.Second, + expected: "2GiB/s", + }, + { + name: "SizeB1024(0):% .2f:GiB", + unit: SizeB1024(0), + fmt: "% .2f", + current: 2 * int64(_iGiB), + elapsed: 1 * time.Second, + expected: "2.00 GiB/s", + }, + { + name: "SizeB1024(0):%d:TiB", + unit: SizeB1024(0), + fmt: "%d", + current: 2 * int64(_iTiB), + elapsed: 1 * time.Second, + expected: "2TiB/s", + }, + { + name: "SizeB1024(0):% .2f:TiB", + unit: SizeB1024(0), + fmt: "% .2f", + current: 2 * int64(_iTiB), + elapsed: 1 * time.Second, + expected: "2.00 TiB/s", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + decor := NewAverageSpeed(tc.unit, tc.fmt, time.Now().Add(-tc.elapsed)) + stat := Statistics{ + Current: tc.current, + } + res, _ := decor.Decor(stat) + if res != tc.expected { + t.Fatalf("expected: %q, got: %q\n", tc.expected, res) + } + }) + } +} + +func TestAverageSpeedSizeB1000(t *testing.T) { + cases := []struct { + name string + fmt string + unit interface{} + current int64 + elapsed time.Duration + expected string + }{ + { + name: "empty fmt", + unit: SizeB1000(0), + fmt: "", + current: 0, + elapsed: time.Second, + expected: "0 b/s", + }, + { + name: "SizeB1000(0):%d:0b", + unit: SizeB1000(0), + fmt: "%d", + current: 0, + elapsed: time.Second, + expected: "0b/s", + }, + { + name: "SizeB1000(0):%f:0b", + unit: SizeB1000(0), + fmt: "%f", + current: 0, + elapsed: time.Second, + expected: "0.000000b/s", + }, + { + name: "SizeB1000(0):% .2f:0b", + unit: SizeB1000(0), + fmt: "% .2f", + current: 0, + elapsed: time.Second, + expected: "0.00 b/s", + }, + { + name: "SizeB1000(0):%d:1b", + unit: SizeB1000(0), + fmt: "%d", + current: 1, + elapsed: time.Second, + expected: "1b/s", + }, + { + name: "SizeB1000(0):% .2f:1b", + unit: SizeB1000(0), + fmt: "% .2f", + current: 1, + elapsed: time.Second, + expected: "1.00 b/s", + }, + { + name: "SizeB1000(0):%d:KB", + unit: SizeB1000(0), + fmt: "%d", + current: 2 * int64(_KB), + elapsed: 1 * time.Second, + expected: "2KB/s", + }, + { + name: "SizeB1000(0):% .f:KB", + unit: SizeB1000(0), + fmt: "% .2f", + current: 2 * int64(_KB), + elapsed: 1 * time.Second, + expected: "2.00 KB/s", + }, + { + name: "SizeB1000(0):%d:MB", + unit: SizeB1000(0), + fmt: "%d", + current: 2 * int64(_MB), + elapsed: 1 * time.Second, + expected: "2MB/s", + }, + { + name: "SizeB1000(0):% .2f:MB", + unit: SizeB1000(0), + fmt: "% .2f", + current: 2 * int64(_MB), + elapsed: 1 * time.Second, + expected: "2.00 MB/s", + }, + { + name: "SizeB1000(0):%d:GB", + unit: SizeB1000(0), + fmt: "%d", + current: 2 * int64(_GB), + elapsed: 1 * time.Second, + expected: "2GB/s", + }, + { + name: "SizeB1000(0):% .2f:GB", + unit: SizeB1000(0), + fmt: "% .2f", + current: 2 * int64(_GB), + elapsed: 1 * time.Second, + expected: "2.00 GB/s", + }, + { + name: "SizeB1000(0):%d:TB", + unit: SizeB1000(0), + fmt: "%d", + current: 2 * int64(_TB), + elapsed: 1 * time.Second, + expected: "2TB/s", + }, + { + name: "SizeB1000(0):% .2f:TB", + unit: SizeB1000(0), + fmt: "% .2f", + current: 2 * int64(_TB), + elapsed: 1 * time.Second, + expected: "2.00 TB/s", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + decor := NewAverageSpeed(tc.unit, tc.fmt, time.Now().Add(-tc.elapsed)) + stat := Statistics{ + Current: tc.current, + } + res, _ := decor.Decor(stat) + if res != tc.expected { + t.Fatalf("expected: %q, got: %q\n", tc.expected, res) + } + }) + } +} diff --git a/decor/spinner.go b/decor/spinner.go new file mode 100644 index 0000000..9d2f890 --- /dev/null +++ b/decor/spinner.go @@ -0,0 +1,21 @@ +package decor + +var defaultSpinnerStyle = [...]string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} + +// Spinner returns spinner decorator. +// +// `frames` spinner frames, if nil or len==0, default is used +// +// `wcc` optional WC config +func Spinner(frames []string, wcc ...WC) Decorator { + if len(frames) == 0 { + frames = defaultSpinnerStyle[:] + } + var count uint + f := func(s Statistics) string { + frame := frames[count%uint(len(frames))] + count++ + return frame + } + return Any(f, wcc...) +} diff --git a/decorators_test.go b/decorators_test.go new file mode 100644 index 0000000..56e7a4b --- /dev/null +++ b/decorators_test.go @@ -0,0 +1,215 @@ +package mpb_test + +import ( + "testing" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/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(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{ + { + { + decor.Statistics{Total: 100, Current: 8}, + decor.Percentage(decor.WCSyncWidth), + "8 %", + }, + { + decor.Statistics{Total: 100, Current: 9}, + decor.Percentage(decor.WCSyncWidth), + "9 %", + }, + }, + { + { + decor.Statistics{Total: 100, Current: 9}, + decor.Percentage(decor.WCSyncWidth), + " 9 %", + }, + { + decor.Statistics{Total: 100, Current: 10}, + decor.Percentage(decor.WCSyncWidth), + "10 %", + }, + }, + { + { + 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{ + { + { + decor.Statistics{Total: 100, Current: 8}, + decor.Percentage(decor.WCSyncWidthR), + "8 %", + }, + { + decor.Statistics{Total: 100, Current: 9}, + decor.Percentage(decor.WCSyncWidthR), + "9 %", + }, + }, + { + { + decor.Statistics{Total: 100, Current: 9}, + decor.Percentage(decor.WCSyncWidthR), + "9 % ", + }, + { + decor.Statistics{Total: 100, Current: 10}, + decor.Percentage(decor.WCSyncWidthR), + "10 %", + }, + }, + { + { + 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{ + { + { + decor.Statistics{Total: 100, Current: 8}, + decor.Percentage(decor.WCSyncSpace), + " 8 %", + }, + { + decor.Statistics{Total: 100, Current: 9}, + decor.Percentage(decor.WCSyncSpace), + " 9 %", + }, + }, + { + { + decor.Statistics{Total: 100, Current: 9}, + decor.Percentage(decor.WCSyncSpace), + " 9 %", + }, + { + decor.Statistics{Total: 100, Current: 10}, + decor.Percentage(decor.WCSyncSpace), + " 10 %", + }, + }, + { + { + 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() + } + + for _, columnCase := range testCases { + mpb.SyncWidth(toSyncMatrix(columnCase), nil) + var results []chan string + for _, step := range columnCase { + step := step + ch := make(chan string) + go func() { + str, _ := step.decorator.Decor(step.stat) + ch <- str + }() + results = append(results, ch) + } + + for i, ch := range results { + res := <-ch + want := columnCase[i].want + if res != want { + t.Errorf("Want: %q, Got: %q\n", want, res) + } + } + } +} + +func toSyncMatrix(ss []step) map[int][]chan int { + var column []chan int + for _, s := range ss { + if ch, ok := s.decorator.Sync(); 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..5ada717 --- /dev/null +++ b/doc.go @@ -0,0 +1,2 @@ +// 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..f7baeaf --- /dev/null +++ b/draw_test.go @@ -0,0 +1,1439 @@ +package mpb + +import ( + "bytes" + "testing" + "unicode/utf8" +) + +func TestDrawDefault(t *testing.T) { + // key is termWidth + testSuite := map[int][]struct { + style BarStyleComposer + name string + total int64 + current int64 + refill int64 + barWidth int + trim bool + want string + }{ + 0: { + { + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: "", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "", + }, + }, + 1: { + { + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: "", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "", + }, + }, + 2: { + { + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: " ", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "[]", + }, + }, + 3: { + { + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: " ", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "[-]", + }, + { + style: BarStyle(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " ", + }, + { + style: BarStyle(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[>]", + }, + { + style: BarStyle(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " ", + }, + { + style: BarStyle(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[=]", + }, + }, + 4: { + { + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: " [] ", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "[>-]", + }, + { + style: BarStyle(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [] ", + }, + { + style: BarStyle(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[=>]", + }, + { + style: BarStyle(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [] ", + }, + { + style: BarStyle(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[==]", + }, + }, + 5: { + { + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: " [-] ", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "[>--]", + }, + { + style: BarStyle(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [>] ", + }, + { + style: BarStyle(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[==>]", + }, + { + style: BarStyle(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [=] ", + }, + { + style: BarStyle(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[===]", + }, + }, + 6: { + { + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: " [>-] ", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "[>---]", + }, + { + style: BarStyle(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [=>] ", + }, + { + style: BarStyle(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[===>]", + }, + { + style: BarStyle(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [==] ", + }, + { + style: BarStyle(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[====]", + }, + }, + 7: { + { + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: " [>--] ", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "[=>---]", + }, + { + style: BarStyle(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [==>] ", + }, + { + style: BarStyle(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[====>]", + }, + { + style: BarStyle(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [===] ", + }, + { + style: BarStyle(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[=====]", + }, + }, + 8: { + { + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: " [>---] ", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "[=>----]", + }, + { + style: BarStyle(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [===>] ", + }, + { + style: BarStyle(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[=====>]", + }, + { + style: BarStyle(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [====] ", + }, + { + style: BarStyle(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[======]", + }, + }, + 80: { + { + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: " [========================>---------------------------------------------------] ", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "[=========================>----------------------------------------------------]", + }, + { + style: BarStyle(), + name: "t,c,bw{60,20,60}", + total: 60, + current: 20, + barWidth: 60, + want: " [==================>---------------------------------------] ", + }, + { + style: BarStyle(), + name: "t,c,bw{60,20,60}trim", + total: 60, + current: 20, + barWidth: 60, + trim: true, + want: "[==================>---------------------------------------]", + }, + { + style: BarStyle(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [==========================================================================>-] ", + }, + { + style: BarStyle(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[============================================================================>-]", + }, + { + style: BarStyle(), + name: "t,c,bw{60,59,60}", + total: 60, + current: 59, + barWidth: 60, + want: " [========================================================>-] ", + }, + { + style: BarStyle(), + name: "t,c,bw{60,59,60}trim", + total: 60, + current: 59, + barWidth: 60, + trim: true, + want: "[========================================================>-]", + }, + { + style: BarStyle(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [============================================================================] ", + }, + { + style: BarStyle(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[==============================================================================]", + }, + { + style: BarStyle(), + name: "t,c,bw{60,60,60}", + total: 60, + current: 60, + barWidth: 60, + want: " [==========================================================] ", + }, + { + style: BarStyle(), + name: "t,c,bw{60,60,60}trim", + total: 60, + current: 60, + barWidth: 60, + trim: true, + want: "[==========================================================]", + }, + }, + 99: { + { + style: BarStyle(), + name: "t,c{100,1}", + total: 100, + current: 1, + want: " [>----------------------------------------------------------------------------------------------] ", + }, + { + style: BarStyle(), + name: "t,c{100,1}trim", + total: 100, + current: 1, + trim: true, + want: "[>------------------------------------------------------------------------------------------------]", + }, + { + style: BarStyle(), + name: "t,c,r{100,40,33}", + total: 100, + current: 40, + refill: 33, + want: " [+++++++++++++++++++++++++++++++======>---------------------------------------------------------] ", + }, + { + style: BarStyle(), + name: "t,c,r{100,40,33}trim", + total: 100, + current: 40, + refill: 33, + trim: true, + want: "[++++++++++++++++++++++++++++++++======>----------------------------------------------------------]", + }, + { + style: BarStyle().Tip("<").Reverse(), + name: "t,c,r{100,40,33},rev", + total: 100, + current: 40, + refill: 33, + want: " [---------------------------------------------------------<======+++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().Tip("<").Reverse(), + name: "t,c,r{100,40,33}trim,rev", + total: 100, + current: 40, + refill: 33, + trim: true, + want: "[----------------------------------------------------------<======++++++++++++++++++++++++++++++++]", + }, + { + style: BarStyle(), + name: "t,c{100,99}", + total: 100, + current: 99, + want: " [=============================================================================================>-] ", + }, + { + style: BarStyle(), + name: "t,c{100,99}trim", + total: 100, + current: 99, + trim: true, + want: "[===============================================================================================>-]", + }, + { + style: BarStyle(), + name: "t,c{100,100}", + total: 100, + current: 100, + want: " [===============================================================================================] ", + }, + { + style: BarStyle(), + name: "t,c{100,100}trim", + total: 100, + current: 100, + trim: true, + want: "[=================================================================================================]", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,99}", + total: 100, + current: 100, + refill: 99, + want: " [++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++=] ", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,99}trim", + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++=]", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,99}rev", + total: 100, + current: 100, + refill: 99, + want: " [=++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,99}trim,rev", + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[=++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,100}", + total: 100, + current: 100, + refill: 100, + want: " [+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,100}trim", + total: 100, + current: 100, + refill: 100, + trim: true, + want: "[+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,100}rev", + total: 100, + current: 100, + refill: 100, + want: " [+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,100}trim", + total: 100, + current: 100, + refill: 100, + trim: true, + want: "[+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, + }, + 100: { + { + style: BarStyle(), + name: "t,c{100,0}", + total: 100, + current: 0, + want: " [------------------------------------------------------------------------------------------------] ", + }, + { + style: BarStyle(), + name: "t,c{100,0}trim", + total: 100, + current: 0, + trim: true, + want: "[--------------------------------------------------------------------------------------------------]", + }, + { + style: BarStyle(), + name: "t,c{100,1}", + total: 100, + current: 1, + want: " [>-----------------------------------------------------------------------------------------------] ", + }, + { + style: BarStyle(), + name: "t,c{100,1}trim", + total: 100, + current: 1, + trim: true, + want: "[>-------------------------------------------------------------------------------------------------]", + }, + { + style: BarStyle(), + name: "t,c{100,99}", + total: 100, + current: 99, + want: " [==============================================================================================>-] ", + }, + { + style: BarStyle(), + name: "t,c{100,99}trim", + total: 100, + current: 99, + trim: true, + want: "[================================================================================================>-]", + }, + { + style: BarStyle(), + name: "t,c{100,100}", + total: 100, + current: 100, + want: " [================================================================================================] ", + }, + { + style: BarStyle(), + name: "t,c{100,100}trim", + total: 100, + current: 100, + trim: true, + want: "[==================================================================================================]", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,99}", + total: 100, + current: 100, + refill: 99, + want: " [+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++=] ", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,99}trim", + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++=]", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,100}", + total: 100, + current: 100, + refill: 100, + want: " [++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,100}trim", + total: 100, + current: 100, + refill: 100, + trim: true, + want: "[++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,99}rev", + total: 100, + current: 100, + refill: 99, + want: " [=+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,99}trim,rev", + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[=+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,100}rev", + total: 100, + current: 100, + refill: 100, + want: " [++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,100}trim", + total: 100, + current: 100, + refill: 100, + trim: true, + want: "[++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, + { + style: BarStyle(), + name: "t,c,r{100,40,33}", + total: 100, + current: 40, + refill: 33, + want: " [++++++++++++++++++++++++++++++++=====>----------------------------------------------------------] ", + }, + { + style: BarStyle(), + name: "t,c,r{100,40,33}trim", + total: 100, + current: 40, + refill: 33, + trim: true, + want: "[++++++++++++++++++++++++++++++++======>-----------------------------------------------------------]", + }, + { + style: BarStyle().Tip("<").Reverse(), + name: "t,c,r{100,40,33},rev", + total: 100, + current: 40, + refill: 33, + want: " [----------------------------------------------------------<=====++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().Tip("<").Reverse(), + name: "t,c,r{100,40,33}trim,rev", + total: 100, + current: 40, + refill: 33, + trim: true, + want: "[-----------------------------------------------------------<======++++++++++++++++++++++++++++++++]", + }, + }, + } + + var tmpBuf bytes.Buffer + for tw, cases := range testSuite { + for _, tc := range cases { + s := newTestState(tc.style.Build()) + s.reqWidth = tc.barWidth + s.total = tc.total + s.current = tc.current + s.trimSpace = tc.trim + s.refill = tc.refill + s.completed = tc.total > 0 && tc.current >= tc.total + tmpBuf.Reset() + r, err := s.draw(newStatistics(tw, s)) + if err != nil { + t.FailNow() + } + _, err = tmpBuf.ReadFrom(r) + if err != nil { + t.FailNow() + } + by := tmpBuf.Bytes() + + got := string(by[:len(by)-1]) + if !utf8.ValidString(got) { + t.Fail() + } + if got != tc.want { + t.Errorf("termWidth:%d %q want: %q %d, got: %q %d\n", tw, tc.name, tc.want, utf8.RuneCountInString(tc.want), got, utf8.RuneCountInString(got)) + } + } + } +} + +func TestDrawTipOnComplete(t *testing.T) { + // key is termWidth + testSuite := map[int][]struct { + style BarStyleComposer + name string + total int64 + current int64 + refill int64 + barWidth int + trim bool + want string + }{ + 3: { + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[>]", + }, + }, + 4: { + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[=>]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[=>]", + }, + }, + 5: { + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[==>]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[==>]", + }, + }, + 6: { + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [=>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[===>]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [=>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[===>]", + }, + }, + 7: { + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [==>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[====>]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [==>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[====>]", + }, + }, + 8: { + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [===>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[=====>]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [===>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[=====>]", + }, + }, + 80: { + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [==========================================================================>-] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[============================================================================>-]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,bw{60,59,60}", + total: 60, + current: 59, + barWidth: 60, + want: " [========================================================>-] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,bw{60,59,60}trim", + total: 60, + current: 59, + barWidth: 60, + trim: true, + want: "[========================================================>-]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [===========================================================================>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[=============================================================================>]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,bw{60,60,60}", + total: 60, + current: 60, + barWidth: 60, + want: " [=========================================================>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,bw{60,60,60}trim", + total: 60, + current: 60, + barWidth: 60, + trim: true, + want: "[=========================================================>]", + }, + }, + 99: { + { + style: BarStyle().TipOnComplete(), + name: "t,c{100,99}", + total: 100, + current: 99, + want: " [=============================================================================================>-] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{100,99}trim", + total: 100, + current: 99, + trim: true, + want: "[===============================================================================================>-]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{100,100}", + total: 100, + current: 100, + want: " [==============================================================================================>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{100,100}trim", + total: 100, + current: 100, + trim: true, + want: "[================================================================================================>]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,r{100,100,99}", + total: 100, + current: 100, + refill: 99, + want: " [++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,r{100,100,99}trim", + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>]", + }, + { + style: BarStyle().Tip("<").TipOnComplete().Reverse(), + name: `t,c,r{100,100,99}.Tip("<").TipOnComplete().Reverse()`, + total: 100, + current: 100, + refill: 99, + want: " [<++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().Tip("<").TipOnComplete().Reverse(), + name: `t,c,r{100,100,99}.Tip("<").TipOnComplete().Reverse()trim`, + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[<++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,r{100,100,100}", + total: 100, + current: 100, + refill: 100, + want: " [++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,r{100,100,100}trim", + total: 100, + current: 100, + refill: 100, + trim: true, + want: "[++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>]", + }, + { + style: BarStyle().Tip("<").TipOnComplete().Reverse(), + name: `t,c,r{100,100,100}.Tip("<").TipOnComplete().Reverse()`, + total: 100, + current: 100, + refill: 100, + want: " [<++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().Tip("<").TipOnComplete().Reverse(), + name: `t,c,r{100,100,100}.Tip("<").TipOnComplete().Reverse()trim`, + total: 100, + current: 100, + refill: 100, + trim: true, + want: "[<++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, + }, + 100: { + { + style: BarStyle().TipOnComplete(), + name: "t,c{100,99}", + total: 100, + current: 99, + want: " [==============================================================================================>-] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{100,99}trim", + total: 100, + current: 99, + trim: true, + want: "[================================================================================================>-]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{100,100}", + total: 100, + current: 100, + want: " [===============================================================================================>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{100,100}trim", + total: 100, + current: 100, + trim: true, + want: "[=================================================================================================>]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,r{100,100,99}", + total: 100, + current: 100, + refill: 99, + want: " [+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,r{100,100,99}trim", + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,r{100,100,100}", + total: 100, + current: 100, + refill: 100, + want: " [+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,r{100,100,100}trim", + total: 100, + current: 100, + refill: 100, + trim: true, + want: "[+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>]", + }, + }, + } + + var tmpBuf bytes.Buffer + for tw, cases := range testSuite { + for _, tc := range cases { + s := newTestState(tc.style.Build()) + s.reqWidth = tc.barWidth + s.total = tc.total + s.current = tc.current + s.trimSpace = tc.trim + s.refill = tc.refill + s.completed = tc.total > 0 && tc.current >= tc.total + tmpBuf.Reset() + r, err := s.draw(newStatistics(tw, s)) + if err != nil { + t.FailNow() + } + _, err = tmpBuf.ReadFrom(r) + if err != nil { + t.FailNow() + } + by := tmpBuf.Bytes() + + got := string(by[:len(by)-1]) + if !utf8.ValidString(got) { + t.Fail() + } + if got != tc.want { + t.Errorf("termWidth:%d %q want: %q %d, got: %q %d\n", tw, tc.name, tc.want, utf8.RuneCountInString(tc.want), got, utf8.RuneCountInString(got)) + } + } + } +} + +func TestDrawDoubleWidth(t *testing.T) { + // key is termWidth + testSuite := map[int][]struct { + style BarStyleComposer + name string + total int64 + current int64 + refill int64 + barWidth int + trim bool + want string + }{ + 99: { + { + style: BarStyle().Lbound("の").Rbound("の"), + name: `t,c{100,1}.Lbound("の").Rbound("の")`, + total: 100, + current: 1, + want: " の>--------------------------------------------------------------------------------------------の ", + }, + { + style: BarStyle().Lbound("の").Rbound("の"), + name: `t,c{100,1}.Lbound("の").Rbound("の")`, + total: 100, + current: 2, + want: " の=>-------------------------------------------------------------------------------------------の ", + }, + { + style: BarStyle().Tip("だ"), + name: `t,c{100,1}Tip("だ")`, + total: 100, + current: 1, + want: " [だ---------------------------------------------------------------------------------------------] ", + }, + { + style: BarStyle().Tip("だ"), + name: `t,c{100,2}Tip("だ")`, + total: 100, + current: 2, + want: " [だ---------------------------------------------------------------------------------------------] ", + }, + { + style: BarStyle().Tip("だ"), + name: `t,c{100,3}Tip("だ")`, + total: 100, + current: 3, + want: " [=だ--------------------------------------------------------------------------------------------] ", + }, + { + style: BarStyle().Tip("だ"), + name: `t,c{100,99}Tip("だ")`, + total: 100, + current: 99, + want: " [============================================================================================だ-] ", + }, + { + style: BarStyle().Tip("だ"), + name: `t,c{100,100}Tip("だ")`, + total: 100, + current: 100, + want: " [===============================================================================================] ", + }, + { + style: BarStyle().Tip("だ").TipOnComplete(), + name: `t,c{100,100}.Tip("だ").TipOnComplete()`, + total: 100, + current: 100, + want: " [=============================================================================================だ] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").Padding("つ"), + name: `t,c{100,1}Filler("の").Tip("だ").Padding("つ")`, + total: 100, + current: 1, + want: " [だつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつ…] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").Padding("つ"), + name: `t,c{100,2}Filler("の").Tip("だ").Padding("つ")`, + total: 100, + current: 2, + want: " [だつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつ…] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").Padding("つ"), + name: `t,c{100,99}Filler("の").Tip("だ").Padding("つ")`, + total: 100, + current: 99, + want: " [ののののののののののののののののののののののののののののののののののののののののののののののだ…] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").Padding("つ"), + name: `t,c{100,100}.Filler("の").Tip("だ").Padding("つ")`, + total: 100, + current: 100, + want: " [ののののののののののののののののののののののののののののののののののののののののののののののの…] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").Padding("つ").Reverse(), + name: `t,c{100,100}Filler("の").Tip("だ").Padding("つ").Reverse()`, + total: 100, + current: 100, + want: " […ののののののののののののののののののののののののののののののののののののののののののののののの] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").TipOnComplete().Padding("つ"), + name: `t,c{100,99}Filler("の").Tip("だ").TipOnComplete().Padding("つ")`, + total: 100, + current: 99, + want: " [ののののののののののののののののののののののののののののののののののののののののののののののだ…] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").TipOnComplete().Padding("つ"), + name: `t,c{100,100}.Filler("の").Tip("だ").TipOnComplete().Padding("つ")`, + total: 100, + current: 100, + want: " [ののののののののののののののののののののののののののののののののののののののののののののののだ…] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").TipOnComplete().Padding("つ").Reverse(), + name: `t,c{100,100}.Filler("の").Tip("だ").TipOnComplete().Padding("つ").Reverse()`, + total: 100, + current: 100, + want: " […だのののののののののののののののののののののののののののののののののののののののののののののの] ", + }, + { + style: BarStyle().Refiller("の"), + name: `t,c,r{100,100,99}Refiller("の")`, + total: 100, + current: 100, + refill: 99, + want: " [ののののののののののののののののののののののののののののののののののののののののののののののの=] ", + }, + { + style: BarStyle().Refiller("の"), + name: `t,c,r{100,100,99}Refiller("の")trim`, + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[のののののののののののののののののののののののののののののののののののののののののののののののの=]", + }, + }, + } + + var tmpBuf bytes.Buffer + for tw, cases := range testSuite { + for _, tc := range cases { + s := newTestState(tc.style.Build()) + s.reqWidth = tc.barWidth + s.total = tc.total + s.current = tc.current + s.trimSpace = tc.trim + s.refill = tc.refill + s.completed = tc.total > 0 && tc.current >= tc.total + tmpBuf.Reset() + r, err := s.draw(newStatistics(tw, s)) + if err != nil { + t.FailNow() + } + _, err = tmpBuf.ReadFrom(r) + if err != nil { + t.FailNow() + } + by := tmpBuf.Bytes() + + got := string(by[:len(by)-1]) + if !utf8.ValidString(got) { + t.Fail() + } + if got != tc.want { + t.Errorf("termWidth:%d %q want: %q %d, got: %q %d\n", tw, tc.name, tc.want, utf8.RuneCountInString(tc.want), got, utf8.RuneCountInString(got)) + } + } + } +} + +func newTestState(filler BarFiller) *bState { + bs := &bState{ + filler: filler, + } + for i := 0; i < len(bs.buffers); i++ { + bs.buffers[i] = bytes.NewBuffer(make([]byte, 0, 512)) + } + return bs +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..6d6ccfe --- /dev/null +++ b/example_test.go @@ -0,0 +1,77 @@ +package mpb_test + +import ( + crand "crypto/rand" + "io" + "math/rand" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func Example() { + // initialize progress container, with custom width + p := mpb.New(mpb.WithWidth(64)) + + total := 100 + name := "Single Bar:" + // create a single bar, which will inherit container's width + bar := p.New(int64(total), + // BarFillerBuilder with custom style + mpb.BarStyle().Lbound("╢").Filler("▌").Tip("▌").Padding("░").Rbound("╟"), + 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( + decor.AverageETA(decor.ET_STYLE_GO, decor.WC{W: 4}), "done", + ), + ), + mpb.AppendDecorators(decor.Percentage()), + ) + // simulating some work + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) + bar.Increment() + } + // 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() { + // import crand "crypto/rand" + + var total int64 = 1024 * 1024 * 500 + reader := io.LimitReader(crand.Reader, total) + + p := mpb.New() + bar := p.AddBar(total, + mpb.AppendDecorators( + decor.CountersKibiByte("% .2f / % .2f"), + ), + ) + + // create proxy reader + proxyReader := bar.ProxyReader(reader) + defer proxyReader.Close() + + // and copy from reader, ignoring errors + _, _ = io.Copy(io.Discard, proxyReader) + + p.Wait() +} diff --git a/export_test.go b/export_test.go new file mode 100644 index 0000000..937ce57 --- /dev/null +++ b/export_test.go @@ -0,0 +1,6 @@ +package mpb + +// make syncWidth func public in test +var SyncWidth = syncWidth + +type PriorityQueue = priorityQueue diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9da11e5 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module github.com/vbauerster/mpb/v8 + +require ( + github.com/VividCortex/ewma v1.2.0 + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d + github.com/mattn/go-runewidth v0.0.15 + golang.org/x/sys v0.11.0 +) + +require github.com/rivo/uniseg v0.4.4 // indirect + +go 1.17 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..011a8cb --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/heap_manager.go b/heap_manager.go new file mode 100644 index 0000000..a680187 --- /dev/null +++ b/heap_manager.go @@ -0,0 +1,173 @@ +package mpb + +import "container/heap" + +type heapManager chan heapRequest + +type heapCmd int + +const ( + h_sync heapCmd = iota + h_push + h_iter + h_drain + h_fix + h_state + h_end +) + +type heapRequest struct { + cmd heapCmd + data interface{} +} + +type iterData struct { + iter chan<- *Bar + drop <-chan struct{} +} + +type pushData struct { + bar *Bar + sync bool +} + +type fixData struct { + bar *Bar + priority int + lazy bool +} + +func (m heapManager) run() { + var bHeap priorityQueue + var pMatrix, aMatrix map[int][]chan int + + var l int + var sync bool + + for req := range m { + switch req.cmd { + case h_push: + data := req.data.(pushData) + heap.Push(&bHeap, data.bar) + if !sync { + sync = data.sync + } + case h_sync: + if sync || l != bHeap.Len() { + pMatrix = make(map[int][]chan int) + aMatrix = make(map[int][]chan int) + for _, b := range bHeap { + table := b.wSyncTable() + for i, ch := range table[0] { + pMatrix[i] = append(pMatrix[i], ch) + } + for i, ch := range table[1] { + aMatrix[i] = append(aMatrix[i], ch) + } + } + sync = false + l = bHeap.Len() + } + drop := req.data.(<-chan struct{}) + syncWidth(pMatrix, drop) + syncWidth(aMatrix, drop) + case h_iter: + data := req.data.(iterData) + drop_iter: + for _, b := range bHeap { + select { + case data.iter <- b: + case <-data.drop: + break drop_iter + } + } + close(data.iter) + case h_drain: + data := req.data.(iterData) + drop_drain: + for bHeap.Len() != 0 { + select { + case data.iter <- heap.Pop(&bHeap).(*Bar): + case <-data.drop: + break drop_drain + } + } + close(data.iter) + case h_fix: + data := req.data.(fixData) + if data.bar.index < 0 { + break + } + data.bar.priority = data.priority + if !data.lazy { + heap.Fix(&bHeap, data.bar.index) + } + case h_state: + ch := req.data.(chan<- bool) + ch <- sync || l != bHeap.Len() + case h_end: + ch := req.data.(chan<- interface{}) + if ch != nil { + go func() { + ch <- []*Bar(bHeap) + }() + } + close(m) + } + } +} + +func (m heapManager) sync(drop <-chan struct{}) { + m <- heapRequest{cmd: h_sync, data: drop} +} + +func (m heapManager) push(b *Bar, sync bool) { + data := pushData{b, sync} + m <- heapRequest{cmd: h_push, data: data} +} + +func (m heapManager) iter(iter chan<- *Bar, drop <-chan struct{}) { + data := iterData{iter, drop} + m <- heapRequest{cmd: h_iter, data: data} +} + +func (m heapManager) drain(iter chan<- *Bar, drop <-chan struct{}) { + data := iterData{iter, drop} + m <- heapRequest{cmd: h_drain, data: data} +} + +func (m heapManager) fix(b *Bar, priority int, lazy bool) { + data := fixData{b, priority, lazy} + m <- heapRequest{cmd: h_fix, data: data} +} + +func (m heapManager) state(ch chan<- bool) { + m <- heapRequest{cmd: h_state, data: ch} +} + +func (m heapManager) end(ch chan<- interface{}) { + m <- heapRequest{cmd: h_end, data: ch} +} + +func syncWidth(matrix map[int][]chan int, drop <-chan struct{}) { + for _, column := range matrix { + go maxWidthDistributor(column, drop) + } +} + +func maxWidthDistributor(column []chan int, drop <-chan struct{}) { + var maxWidth int + for _, ch := range column { + select { + case w := <-ch: + if w > maxWidth { + maxWidth = w + } + case <-drop: + return + } + } + for _, ch := range column { + ch <- maxWidth + } +} diff --git a/internal/percentage.go b/internal/percentage.go new file mode 100644 index 0000000..4bc36f5 --- /dev/null +++ b/internal/percentage.go @@ -0,0 +1,19 @@ +package internal + +import "math" + +// Percentage is a helper function, to calculate percentage. +func Percentage(total, current int64, width uint) float64 { + if total <= 0 { + return 0 + } + if current >= total { + return float64(width) + } + return float64(int64(width)*current) / float64(total) +} + +// PercentageRound same as Percentage but with math.Round. +func PercentageRound(total, current int64, width uint) float64 { + return math.Round(Percentage(total, current, width)) +} diff --git a/internal/percentage_test.go b/internal/percentage_test.go new file mode 100644 index 0000000..515ad60 --- /dev/null +++ b/internal/percentage_test.go @@ -0,0 +1,71 @@ +package internal + +import "testing" + +func TestPercentage(t *testing.T) { + // key is barWidth + testSuite := map[uint][]struct { + name string + total int64 + current int64 + 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, 100}, + {"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, 100}, + }, + 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, 80}, + {"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, 80}, + }, + } + + for width, cases := range testSuite { + for _, tc := range cases { + got := int64(PercentageRound(tc.total, tc.current, width)) + if got != tc.expected { + t.Errorf("width %d; %s: Expected: %d, got: %d\n", width, tc.name, tc.expected, got) + } + } + } +} diff --git a/internal/width.go b/internal/width.go new file mode 100644 index 0000000..7677e40 --- /dev/null +++ b/internal/width.go @@ -0,0 +1,10 @@ +package internal + +// CheckRequestedWidth checks that requested width doesn't overflow +// available width +func CheckRequestedWidth(requested, available int) int { + if requested < 1 || requested >= available { + return available + } + return requested +} diff --git a/priority_queue.go b/priority_queue.go new file mode 100644 index 0000000..0863b57 --- /dev/null +++ b/priority_queue.go @@ -0,0 +1,37 @@ +package mpb + +import "container/heap" + +var _ heap.Interface = (*priorityQueue)(nil) + +type priorityQueue []*Bar + +func (pq priorityQueue) Len() int { return len(pq) } + +func (pq priorityQueue) Less(i, j int) bool { + // greater priority pops first + 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] + old[n-1] = nil // avoid memory leak + bar.index = -1 // for safety + *pq = old[:n-1] + return bar +} diff --git a/progress.go b/progress.go new file mode 100644 index 0000000..f275be3 --- /dev/null +++ b/progress.go @@ -0,0 +1,456 @@ +package mpb + +import ( + "bytes" + "context" + "fmt" + "io" + "math" + "os" + "sync" + "time" + + "github.com/vbauerster/mpb/v8/cwriter" + "github.com/vbauerster/mpb/v8/decor" +) + +const ( + defaultRefreshRate = 150 * time.Millisecond +) + +// DoneError represents an error when `*mpb.Progress` is done but its functionality is requested. +var DoneError = fmt.Errorf("%T instance can't be reused after it's done", (*Progress)(nil)) + +// Progress represents a container that renders one or more progress bars. +type Progress struct { + uwg *sync.WaitGroup + pwg, bwg sync.WaitGroup + operateState chan func(*pState) + interceptIO chan func(io.Writer) + done <-chan struct{} + cancel func() +} + +// pState holds bars in its priorityQueue, it gets passed to (*Progress).serve monitor goroutine. +type pState struct { + ctx context.Context + hm heapManager + dropS, dropD chan struct{} + renderReq chan time.Time + idCount int + popPriority int + + // following are provided/overrided by user + refreshRate time.Duration + reqWidth int + popCompleted bool + autoRefresh bool + delayRC <-chan struct{} + manualRC <-chan interface{} + shutdownNotifier chan<- interface{} + queueBars map[*Bar]*Bar + output io.Writer + debugOut io.Writer + uwg *sync.WaitGroup +} + +// New creates new Progress container instance. It's not possible to +// reuse instance after (*Progress).Wait method has been called. +func New(options ...ContainerOption) *Progress { + return NewWithContext(context.Background(), options...) +} + +// NewWithContext creates new Progress container instance with provided +// context. It's not possible to reuse instance after (*Progress).Wait +// method has been called. +func NewWithContext(ctx context.Context, options ...ContainerOption) *Progress { + if ctx == nil { + ctx = context.Background() + } + ctx, cancel := context.WithCancel(ctx) + s := &pState{ + ctx: ctx, + hm: make(heapManager), + dropS: make(chan struct{}), + dropD: make(chan struct{}), + renderReq: make(chan time.Time), + refreshRate: defaultRefreshRate, + popPriority: math.MinInt32, + queueBars: make(map[*Bar]*Bar), + output: os.Stdout, + debugOut: io.Discard, + } + + for _, opt := range options { + if opt != nil { + opt(s) + } + } + + p := &Progress{ + uwg: s.uwg, + operateState: make(chan func(*pState)), + interceptIO: make(chan func(io.Writer)), + cancel: cancel, + } + + cw := cwriter.New(s.output) + if s.manualRC != nil { + done := make(chan struct{}) + p.done = done + s.autoRefresh = false + go s.manualRefreshListener(done) + } else if cw.IsTerminal() || s.autoRefresh { + done := make(chan struct{}) + p.done = done + s.autoRefresh = true + go s.autoRefreshListener(done) + } else { + p.done = ctx.Done() + s.autoRefresh = false + } + + p.pwg.Add(1) + go p.serve(s, cw) + go s.hm.run() + return p +} + +// AddBar creates a bar with default bar filler. +func (p *Progress) AddBar(total int64, options ...BarOption) *Bar { + return p.New(total, BarStyle(), options...) +} + +// AddSpinner creates a bar with default spinner filler. +func (p *Progress) AddSpinner(total int64, options ...BarOption) *Bar { + return p.New(total, SpinnerStyle(), options...) +} + +// New creates a bar by calling `Build` method on provided `BarFillerBuilder`. +func (p *Progress) New(total int64, builder BarFillerBuilder, options ...BarOption) *Bar { + return p.MustAdd(total, builder.Build(), options...) +} + +// MustAdd creates a bar which renders itself by provided BarFiller. +// If `total <= 0` triggering complete event by increment methods is +// disabled. Panics if *Progress instance is done, i.e. called after +// (*Progress).Wait(). +func (p *Progress) MustAdd(total int64, filler BarFiller, options ...BarOption) *Bar { + bar, err := p.Add(total, filler, options...) + if err != nil { + panic(err) + } + return bar +} + +// Add creates a bar which renders itself by provided BarFiller. +// If `total <= 0` triggering complete event by increment methods +// is disabled. If *Progress instance is done, i.e. called after +// (*Progress).Wait(), return error == DoneError. +func (p *Progress) Add(total int64, filler BarFiller, options ...BarOption) (*Bar, error) { + if filler == nil { + filler = NopStyle().Build() + } + type result struct { + bar *Bar + bs *bState + } + ch := make(chan result) + select { + case p.operateState <- func(ps *pState) { + bs := ps.makeBarState(total, filler, options...) + bar := newBar(ps.ctx, p, bs) + if bs.waitBar != nil { + ps.queueBars[bs.waitBar] = bar + } else { + ps.hm.push(bar, true) + } + ps.idCount++ + ch <- result{bar, bs} + }: + res := <-ch + bar, bs := res.bar, res.bs + bar.TraverseDecorators(func(d decor.Decorator) { + if d, ok := d.(decor.AverageDecorator); ok { + bs.averageDecorators = append(bs.averageDecorators, d) + } + if d, ok := d.(decor.EwmaDecorator); ok { + bs.ewmaDecorators = append(bs.ewmaDecorators, d) + } + if d, ok := d.(decor.ShutdownListener); ok { + bs.shutdownListeners = append(bs.shutdownListeners, d) + } + }) + return bar, nil + case <-p.done: + return nil, DoneError + } +} + +func (p *Progress) traverseBars(cb func(b *Bar) bool) { + iter, drop := make(chan *Bar), make(chan struct{}) + select { + case p.operateState <- func(s *pState) { s.hm.iter(iter, drop) }: + for b := range iter { + if cb(b) { + close(drop) + break + } + } + case <-p.done: + } +} + +// UpdateBarPriority either immediately or lazy. +// With lazy flag order is updated after the next refresh cycle. +// If you don't care about laziness just use *Bar.SetPriority(int). +func (p *Progress) UpdateBarPriority(b *Bar, priority int, lazy bool) { + if b == nil { + return + } + select { + case p.operateState <- func(s *pState) { s.hm.fix(b, priority, lazy) }: + case <-p.done: + } +} + +// Write is implementation of io.Writer. +// Writing to `*mpb.Progress` will print lines above a running bar. +// Writes aren't flushed immediately, but at next refresh cycle. +// If Write is called after `*mpb.Progress` is done, `mpb.DoneError` +// is returned. +func (p *Progress) Write(b []byte) (int, error) { + type result struct { + n int + err error + } + ch := make(chan result) + select { + case p.interceptIO <- func(w io.Writer) { + n, err := w.Write(b) + ch <- result{n, err} + }: + res := <-ch + return res.n, res.err + case <-p.done: + return 0, DoneError + } +} + +// Wait waits for all bars to complete and finally shutdowns container. After +// this method has been called, there is no way to reuse (*Progress) instance. +func (p *Progress) Wait() { + // wait for user wg, if any + if p.uwg != nil { + p.uwg.Wait() + } + + p.bwg.Wait() + p.Shutdown() +} + +// Shutdown cancels any running bar immediately and then shutdowns (*Progress) +// instance. Normally this method shouldn't be called unless you know what you +// are doing. Proper way to shutdown is to call (*Progress).Wait() instead. +func (p *Progress) Shutdown() { + p.cancel() + p.pwg.Wait() +} + +func (p *Progress) serve(s *pState, cw *cwriter.Writer) { + defer p.pwg.Done() + render := func() error { return s.render(cw) } + var err error + + for { + select { + case op := <-p.operateState: + op(s) + case fn := <-p.interceptIO: + fn(cw) + case <-s.renderReq: + e := render() + if e != nil { + p.cancel() // cancel all bars + render = func() error { return nil } + err = e + } + case <-p.done: + update := make(chan bool) + for s.autoRefresh && err == nil { + s.hm.state(update) + if <-update { + err = render() + } else { + break + } + } + if err != nil { + _, _ = fmt.Fprintln(s.debugOut, err.Error()) + } + s.hm.end(s.shutdownNotifier) + return + } + } +} + +func (s pState) autoRefreshListener(done chan struct{}) { + if s.delayRC != nil { + <-s.delayRC + } + ticker := time.NewTicker(s.refreshRate) + defer ticker.Stop() + for { + select { + case t := <-ticker.C: + s.renderReq <- t + case <-s.ctx.Done(): + close(done) + return + } + } +} + +func (s pState) manualRefreshListener(done chan struct{}) { + for { + select { + case x := <-s.manualRC: + if t, ok := x.(time.Time); ok { + s.renderReq <- t + } else { + s.renderReq <- time.Now() + } + case <-s.ctx.Done(): + close(done) + return + } + } +} + +func (s *pState) render(cw *cwriter.Writer) (err error) { + s.hm.sync(s.dropS) + iter := make(chan *Bar) + go s.hm.iter(iter, s.dropS) + + var width, height int + if cw.IsTerminal() { + width, height, err = cw.GetTermSize() + if err != nil { + close(s.dropS) + return err + } + } else { + if s.reqWidth > 0 { + width = s.reqWidth + } else { + width = 100 + } + height = 100 + } + + for b := range iter { + go b.render(width) + } + + return s.flush(cw, height) +} + +func (s *pState) flush(cw *cwriter.Writer, height int) error { + var wg sync.WaitGroup + defer wg.Wait() // waiting for all s.hm.push to complete + + var popCount int + var rows []io.Reader + + iter := make(chan *Bar) + s.hm.drain(iter, s.dropD) + + for b := range iter { + frame := <-b.frameCh + if frame.err != nil { + close(s.dropD) + b.cancel() + return frame.err // b.frameCh is buffered it's ok to return here + } + var usedRows int + for i := len(frame.rows) - 1; i >= 0; i-- { + if row := frame.rows[i]; len(rows) < height { + rows = append(rows, row) + usedRows++ + } else { + _, _ = io.Copy(io.Discard, row) + } + } + if frame.shutdown != 0 && !frame.done { + if qb, ok := s.queueBars[b]; ok { + b.cancel() + delete(s.queueBars, b) + qb.priority = b.priority + wg.Add(1) + go func(b *Bar) { + s.hm.push(b, true) + wg.Done() + }(qb) + continue + } + if s.popCompleted && !frame.noPop { + switch frame.shutdown { + case 1: + b.priority = s.popPriority + s.popPriority++ + default: + b.cancel() + popCount += usedRows + continue + } + } else if frame.rmOnComplete { + b.cancel() + continue + } else { + b.cancel() + } + } + wg.Add(1) + go func(b *Bar) { + s.hm.push(b, false) + wg.Done() + }(b) + } + + for i := len(rows) - 1; i >= 0; i-- { + _, err := cw.ReadFrom(rows[i]) + if err != nil { + return err + } + } + + return cw.Flush(len(rows) - popCount) +} + +func (s pState) makeBarState(total int64, filler BarFiller, options ...BarOption) *bState { + bs := &bState{ + id: s.idCount, + priority: s.idCount, + reqWidth: s.reqWidth, + total: total, + filler: filler, + renderReq: s.renderReq, + autoRefresh: s.autoRefresh, + } + + if total > 0 { + bs.triggerComplete = true + } + + for _, opt := range options { + if opt != nil { + opt(bs) + } + } + + for i := 0; i < len(bs.buffers); i++ { + bs.buffers[i] = bytes.NewBuffer(make([]byte, 0, 512)) + } + + return bs +} diff --git a/progress_test.go b/progress_test.go new file mode 100644 index 0000000..7892062 --- /dev/null +++ b/progress_test.go @@ -0,0 +1,282 @@ +package mpb_test + +import ( + "bytes" + "container/heap" + "context" + "errors" + "io" + "strings" + "testing" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +const ( + timeout = 300 * time.Millisecond +) + +func TestWithContext(t *testing.T) { + shutdown := make(chan interface{}) + ctx, cancel := context.WithCancel(context.Background()) + p := mpb.NewWithContext(ctx, + mpb.WithOutput(io.Discard), + mpb.WithShutdownNotifier(shutdown), + ) + _ = p.AddBar(0) // never complete bar + _ = p.AddBar(0) // never complete bar + go func() { + time.Sleep(10 * time.Millisecond) + cancel() + }() + + p.Wait() + + select { + case v := <-shutdown: + if l := len(v.([]*mpb.Bar)); l != 2 { + t.Errorf("Expected len of bars: %d, got: %d", 2, l) + } + case <-time.After(timeout): + t.Errorf("Progress didn't shutdown after %v", timeout) + } +} + +func TestShutdownsWithErrFiller(t *testing.T) { + var debug bytes.Buffer + shutdown := make(chan interface{}) + p := mpb.New( + mpb.WithShutdownNotifier(shutdown), + mpb.WithOutput(io.Discard), + mpb.WithDebugOutput(&debug), + mpb.WithAutoRefresh(), + ) + + var errReturnCount int + testError := errors.New("test error") + bar := p.AddBar(100, + mpb.BarFillerMiddleware(func(base mpb.BarFiller) mpb.BarFiller { + return mpb.BarFillerFunc(func(w io.Writer, st decor.Statistics) error { + if st.Current >= 22 { + errReturnCount++ + return testError + } + return base.Fill(w, st) + }) + }), + ) + + go func() { + for bar.IsRunning() { + bar.Increment() + time.Sleep(10 * time.Millisecond) + } + }() + + p.Wait() + + if errReturnCount != 1 { + t.Errorf("Expected errReturnCount: %d, got: %d\n", 1, errReturnCount) + } + + select { + case v := <-shutdown: + if l := len(v.([]*mpb.Bar)); l != 0 { + t.Errorf("Expected len of bars: %d, got: %d\n", 0, l) + } + if err := strings.TrimSpace(debug.String()); err != testError.Error() { + t.Errorf("Expected err: %q, got %q\n", testError.Error(), err) + } + case <-time.After(timeout): + t.Errorf("Progress didn't shutdown after %v", timeout) + } +} + +func TestShutdownAfterBarAbortWithDrop(t *testing.T) { + shutdown := make(chan interface{}) + p := mpb.New( + mpb.WithShutdownNotifier(shutdown), + mpb.WithOutput(io.Discard), + mpb.WithAutoRefresh(), + ) + b := p.AddBar(100) + + var count int + for i := 0; !b.Aborted(); i++ { + if i >= 10 { + count++ + b.Abort(true) + } else { + b.Increment() + time.Sleep(10 * time.Millisecond) + } + } + + p.Wait() + + if count != 1 { + t.Errorf("Expected count: %d, got: %d", 1, count) + } + + select { + case v := <-shutdown: + if l := len(v.([]*mpb.Bar)); l != 0 { + t.Errorf("Expected len of bars: %d, got: %d", 0, l) + } + case <-time.After(timeout): + t.Errorf("Progress didn't shutdown after %v", timeout) + } +} + +func TestShutdownAfterBarAbortWithNoDrop(t *testing.T) { + shutdown := make(chan interface{}) + p := mpb.New( + mpb.WithShutdownNotifier(shutdown), + mpb.WithOutput(io.Discard), + mpb.WithAutoRefresh(), + ) + b := p.AddBar(100) + + var count int + for i := 0; !b.Aborted(); i++ { + if i >= 10 { + count++ + b.Abort(false) + } else { + b.Increment() + time.Sleep(10 * time.Millisecond) + } + } + + p.Wait() + + if count != 1 { + t.Errorf("Expected count: %d, got: %d", 1, count) + } + + select { + case v := <-shutdown: + if l := len(v.([]*mpb.Bar)); l != 1 { + t.Errorf("Expected len of bars: %d, got: %d", 1, l) + } + case <-time.After(timeout): + t.Errorf("Progress didn't shutdown after %v", timeout) + } +} + +func TestBarPristinePopOrder(t *testing.T) { + shutdown := make(chan interface{}) + ctx, cancel := context.WithCancel(context.Background()) + p := mpb.NewWithContext(ctx, + mpb.WithOutput(io.Discard), // auto refresh is disabled + mpb.WithShutdownNotifier(shutdown), + ) + a := p.AddBar(100, mpb.BarPriority(1), mpb.BarID(1)) + b := p.AddBar(100, mpb.BarPriority(2), mpb.BarID(2)) + c := p.AddBar(100, mpb.BarPriority(3), mpb.BarID(3)) + pristineOrder := []*mpb.Bar{c, b, a} + + go cancel() + + bars := (<-shutdown).([]*mpb.Bar) + if l := len(bars); l != 3 { + t.Fatalf("Expected len of bars: %d, got: %d", 3, l) + } + + p.Wait() + pq := mpb.PriorityQueue(bars) + + for _, b := range pristineOrder { + // higher priority pops first + if bar := heap.Pop(&pq).(*mpb.Bar); bar.ID() != b.ID() { + t.Errorf("Expected bar id: %d, got bar id: %d", b.ID(), bar.ID()) + } + } +} + +func makeUpdateBarPriorityTest(refresh, lazy bool) func(*testing.T) { + return func(t *testing.T) { + shutdown := make(chan interface{}) + refreshCh := make(chan interface{}) + ctx, cancel := context.WithCancel(context.Background()) + p := mpb.NewWithContext(ctx, + mpb.WithOutput(io.Discard), + mpb.WithManualRefresh(refreshCh), + mpb.WithShutdownNotifier(shutdown), + ) + a := p.AddBar(100, mpb.BarPriority(1), mpb.BarID(1)) + b := p.AddBar(100, mpb.BarPriority(2), mpb.BarID(2)) + c := p.AddBar(100, mpb.BarPriority(3), mpb.BarID(3)) + + p.UpdateBarPriority(c, 2, lazy) + p.UpdateBarPriority(b, 3, lazy) + checkOrder := []*mpb.Bar{b, c, a} // updated order + + if refresh { + refreshCh <- time.Now() + } else if lazy { + checkOrder = []*mpb.Bar{c, b, a} // pristine order + } + + go cancel() + + bars := (<-shutdown).([]*mpb.Bar) + if l := len(bars); l != 3 { + t.Fatalf("Expected len of bars: %d, got: %d", 3, l) + } + + p.Wait() + pq := mpb.PriorityQueue(bars) + + for _, b := range checkOrder { + // higher priority pops first + if bar := heap.Pop(&pq).(*mpb.Bar); bar.ID() != b.ID() { + t.Errorf("Expected bar id: %d, got bar id: %d", b.ID(), bar.ID()) + } + } + } +} + +func TestUpdateBarPriority(t *testing.T) { + makeUpdateBarPriorityTest(false, false)(t) + makeUpdateBarPriorityTest(true, false)(t) +} + +func TestUpdateBarPriorityLazy(t *testing.T) { + makeUpdateBarPriorityTest(false, true)(t) + makeUpdateBarPriorityTest(true, true)(t) +} + +func TestNoOutput(t *testing.T) { + var buf bytes.Buffer + p := mpb.New(mpb.WithOutput(&buf)) + bar := p.AddBar(100) + + go func() { + for !bar.Completed() { + bar.Increment() + } + }() + + p.Wait() + + if buf.Len() != 0 { + t.Errorf("Expected buf.Len == 0, got: %d\n", buf.Len()) + } +} + +func TestAddAfterDone(t *testing.T) { + p := mpb.New(mpb.WithOutput(io.Discard)) + bar := p.AddBar(100) + bar.IncrBy(100) + + p.Wait() + + _, err := p.Add(100, nil) + + if err != mpb.DoneError { + t.Errorf("Expected %q, got: %q\n", mpb.DoneError, err) + } +} diff --git a/proxyreader.go b/proxyreader.go new file mode 100644 index 0000000..b0e7720 --- /dev/null +++ b/proxyreader.go @@ -0,0 +1,96 @@ +package mpb + +import ( + "io" + "time" +) + +type proxyReader struct { + io.ReadCloser + bar *Bar +} + +func (x proxyReader) Read(p []byte) (int, error) { + n, err := x.ReadCloser.Read(p) + x.bar.IncrBy(n) + return n, err +} + +type proxyWriterTo struct { + proxyReader +} + +func (x proxyWriterTo) WriteTo(w io.Writer) (int64, error) { + n, err := x.ReadCloser.(io.WriterTo).WriteTo(w) + x.bar.IncrInt64(n) + return n, err +} + +type ewmaProxyReader struct { + io.ReadCloser + bar *Bar +} + +func (x ewmaProxyReader) Read(p []byte) (int, error) { + start := time.Now() + n, err := x.ReadCloser.Read(p) + x.bar.EwmaIncrBy(n, time.Since(start)) + return n, err +} + +type ewmaProxyWriterTo struct { + ewmaProxyReader +} + +func (x ewmaProxyWriterTo) WriteTo(w io.Writer) (int64, error) { + start := time.Now() + n, err := x.ReadCloser.(io.WriterTo).WriteTo(w) + x.bar.EwmaIncrInt64(n, time.Since(start)) + return n, err +} + +func newProxyReader(r io.Reader, b *Bar, hasEwma bool) io.ReadCloser { + rc := toReadCloser(r) + if hasEwma { + epr := ewmaProxyReader{rc, b} + if _, ok := r.(io.WriterTo); ok { + return ewmaProxyWriterTo{epr} + } + return epr + } + pr := proxyReader{rc, b} + if _, ok := r.(io.WriterTo); ok { + return proxyWriterTo{pr} + } + return pr +} + +func toReadCloser(r io.Reader) io.ReadCloser { + if rc, ok := r.(io.ReadCloser); ok { + return rc + } + return toNopReadCloser(r) +} + +func toNopReadCloser(r io.Reader) io.ReadCloser { + if _, ok := r.(io.WriterTo); ok { + return nopReadCloserWriterTo{r} + } + return nopReadCloser{r} +} + +type nopReadCloser struct { + io.Reader +} + +func (nopReadCloser) Close() error { return nil } + +type nopReadCloserWriterTo struct { + io.Reader +} + +func (nopReadCloserWriterTo) Close() error { return nil } + +func (c nopReadCloserWriterTo) WriteTo(w io.Writer) (int64, error) { + return c.Reader.(io.WriterTo).WriteTo(w) +} diff --git a/proxyreader_test.go b/proxyreader_test.go new file mode 100644 index 0000000..f7e9121 --- /dev/null +++ b/proxyreader_test.go @@ -0,0 +1,117 @@ +package mpb_test + +import ( + "bytes" + "io" + "strings" + "testing" + + "github.com/vbauerster/mpb/v8" +) + +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.` + +type testReader struct { + io.Reader + called bool +} + +func (r *testReader) Read(p []byte) (n int, err error) { + r.called = true + return r.Reader.Read(p) +} + +func TestProxyReader(t *testing.T) { + p := mpb.New(mpb.WithOutput(io.Discard)) + + tr := &testReader{strings.NewReader(content), false} + + bar := p.New(int64(len(content)), mpb.NopStyle()) + + var buf bytes.Buffer + _, err := io.Copy(&buf, bar.ProxyReader(tr)) + if err != nil { + t.Errorf("io.Copy: %s\n", err.Error()) + } + + p.Wait() + + if !tr.called { + t.Error("Read not called") + } + + if got := buf.String(); got != content { + t.Errorf("Expected content: %s, got: %s\n", content, got) + } +} + +type testReadCloser struct { + io.Reader + called bool +} + +func (r *testReadCloser) Close() error { + r.called = true + return nil +} + +func TestProxyReadCloser(t *testing.T) { + p := mpb.New(mpb.WithOutput(io.Discard)) + + tr := &testReadCloser{strings.NewReader(content), false} + + bar := p.New(int64(len(content)), mpb.NopStyle()) + + rc := bar.ProxyReader(tr) + _, err := io.Copy(io.Discard, rc) + if err != nil { + t.Errorf("io.Copy: %s\n", err.Error()) + } + _ = rc.Close() + + p.Wait() + + if !tr.called { + t.Error("Close not called") + } +} + +type testReaderWriterTo struct { + io.Reader + called bool +} + +func (r *testReaderWriterTo) WriteTo(w io.Writer) (n int64, err error) { + r.called = true + return r.Reader.(io.WriterTo).WriteTo(w) +} + +func TestProxyReaderWriterTo(t *testing.T) { + p := mpb.New(mpb.WithOutput(io.Discard)) + + tr := &testReaderWriterTo{strings.NewReader(content), false} + + bar := p.New(int64(len(content)), mpb.NopStyle()) + + var buf bytes.Buffer + _, err := io.Copy(&buf, bar.ProxyReader(tr)) + if err != nil { + t.Errorf("io.Copy: %s\n", err.Error()) + } + + p.Wait() + + if !tr.called { + t.Error("WriteTo not called") + } + + if got := buf.String(); got != content { + t.Errorf("Expected content: %s, got: %s\n", content, got) + } +} diff --git a/proxywriter.go b/proxywriter.go new file mode 100644 index 0000000..f260daf --- /dev/null +++ b/proxywriter.go @@ -0,0 +1,96 @@ +package mpb + +import ( + "io" + "time" +) + +type proxyWriter struct { + io.WriteCloser + bar *Bar +} + +func (x proxyWriter) Write(p []byte) (int, error) { + n, err := x.WriteCloser.Write(p) + x.bar.IncrBy(n) + return n, err +} + +type proxyReaderFrom struct { + proxyWriter +} + +func (x proxyReaderFrom) ReadFrom(r io.Reader) (int64, error) { + n, err := x.WriteCloser.(io.ReaderFrom).ReadFrom(r) + x.bar.IncrInt64(n) + return n, err +} + +type ewmaProxyWriter struct { + io.WriteCloser + bar *Bar +} + +func (x ewmaProxyWriter) Write(p []byte) (int, error) { + start := time.Now() + n, err := x.WriteCloser.Write(p) + x.bar.EwmaIncrBy(n, time.Since(start)) + return n, err +} + +type ewmaProxyReaderFrom struct { + ewmaProxyWriter +} + +func (x ewmaProxyReaderFrom) ReadFrom(r io.Reader) (int64, error) { + start := time.Now() + n, err := x.WriteCloser.(io.ReaderFrom).ReadFrom(r) + x.bar.EwmaIncrInt64(n, time.Since(start)) + return n, err +} + +func newProxyWriter(w io.Writer, b *Bar, hasEwma bool) io.WriteCloser { + wc := toWriteCloser(w) + if hasEwma { + epw := ewmaProxyWriter{wc, b} + if _, ok := w.(io.ReaderFrom); ok { + return ewmaProxyReaderFrom{epw} + } + return epw + } + pw := proxyWriter{wc, b} + if _, ok := w.(io.ReaderFrom); ok { + return proxyReaderFrom{pw} + } + return pw +} + +func toWriteCloser(w io.Writer) io.WriteCloser { + if wc, ok := w.(io.WriteCloser); ok { + return wc + } + return toNopWriteCloser(w) +} + +func toNopWriteCloser(w io.Writer) io.WriteCloser { + if _, ok := w.(io.ReaderFrom); ok { + return nopWriteCloserReaderFrom{w} + } + return nopWriteCloser{w} +} + +type nopWriteCloser struct { + io.Writer +} + +func (nopWriteCloser) Close() error { return nil } + +type nopWriteCloserReaderFrom struct { + io.Writer +} + +func (nopWriteCloserReaderFrom) Close() error { return nil } + +func (c nopWriteCloserReaderFrom) ReadFrom(r io.Reader) (int64, error) { + return c.Writer.(io.ReaderFrom).ReadFrom(r) +} diff --git a/proxywriter_test.go b/proxywriter_test.go new file mode 100644 index 0000000..b57357f --- /dev/null +++ b/proxywriter_test.go @@ -0,0 +1,120 @@ +package mpb_test + +import ( + "bytes" + "io" + "strings" + "testing" + + "github.com/vbauerster/mpb/v8" +) + +type testWriter struct { + io.Writer + called bool +} + +func (w *testWriter) Write(p []byte) (n int, err error) { + w.called = true + return w.Writer.Write(p) +} + +func TestProxyWriter(t *testing.T) { + p := mpb.New(mpb.WithOutput(io.Discard)) + + var buf bytes.Buffer + tw := &testWriter{&buf, false} + + bar := p.New(int64(len(content)), mpb.NopStyle()) + + _, err := io.Copy(bar.ProxyWriter(tw), strings.NewReader(content)) + if err != nil { + t.Errorf("io.Copy: %s\n", err.Error()) + } + + p.Wait() + + if !tw.called { + t.Error("Read not called") + } + + if got := buf.String(); got != content { + t.Errorf("Expected content: %s, got: %s\n", content, got) + } +} + +type testWriteCloser struct { + io.Writer + called bool +} + +func (w *testWriteCloser) Close() error { + w.called = true + return nil +} + +func TestProxyWriteCloser(t *testing.T) { + p := mpb.New(mpb.WithOutput(io.Discard)) + + var buf bytes.Buffer + tw := &testWriteCloser{&buf, false} + + bar := p.New(int64(len(content)), mpb.NopStyle()) + + wc := bar.ProxyWriter(tw) + _, err := io.Copy(wc, strings.NewReader(content)) + if err != nil { + t.Errorf("io.Copy: %s\n", err.Error()) + } + _ = wc.Close() + + p.Wait() + + if !tw.called { + t.Error("Close not called") + } +} + +type testWriterReadFrom struct { + io.Writer + called bool +} + +func (w *testWriterReadFrom) ReadFrom(r io.Reader) (n int64, err error) { + w.called = true + return w.Writer.(io.ReaderFrom).ReadFrom(r) +} + +type dumbReader struct { + r io.Reader +} + +func (r dumbReader) Read(p []byte) (int, error) { + return r.r.Read(p) +} + +func TestProxyWriterReadFrom(t *testing.T) { + p := mpb.New(mpb.WithOutput(io.Discard)) + + var buf bytes.Buffer + tw := &testWriterReadFrom{&buf, false} + + bar := p.New(int64(len(content)), mpb.NopStyle()) + + // To trigger ReadFrom, WriteTo needs to be hidden, hence a dumb wrapper + dr := dumbReader{strings.NewReader(content)} + _, err := io.Copy(bar.ProxyWriter(tw), dr) + if err != nil { + t.Errorf("io.Copy: %s\n", err.Error()) + } + + p.Wait() + + if !tw.called { + t.Error("ReadFrom not called") + } + + if got := buf.String(); got != content { + t.Errorf("Expected content: %s, got: %s\n", content, got) + } +} -- cgit v1.2.3