├── .github └── workflows │ ├── golangci-lint.yml │ └── test.yml ├── .gitignore ├── CONTRIBUTING ├── README.md ├── UNLICENSE ├── _examples ├── .gitignore ├── barExtender │ ├── go.mod │ └── main.go ├── barExtenderRev │ ├── go.mod │ └── main.go ├── cancel │ ├── go.mod │ └── main.go ├── complex │ ├── go.mod │ └── main.go ├── decoratorsOnTop │ ├── go.mod │ └── main.go ├── differentWidth │ ├── go.mod │ └── main.go ├── dynTotal │ ├── go.mod │ └── main.go ├── gomodtidyall ├── io │ ├── go.mod │ └── main.go ├── mexicanBar │ ├── go.mod │ └── main.go ├── multiBars │ ├── go.mod │ └── main.go ├── poplog │ ├── go.mod │ └── main.go ├── progressAsWriter │ ├── go.mod │ └── main.go ├── quietMode │ ├── go.mod │ └── main.go ├── remove │ ├── go.mod │ └── main.go ├── reverseBar │ ├── go.mod │ └── main.go ├── singleBar │ ├── go.mod │ └── main.go ├── spinTipBar │ ├── go.mod │ └── main.go ├── spinnerBar │ ├── go.mod │ └── main.go ├── spinnerDecorator │ ├── go.mod │ └── main.go ├── stress │ ├── go.mod │ └── main.go ├── suppressBar │ ├── go.mod │ └── main.go └── tipOnComplete │ ├── go.mod │ └── main.go ├── _svg ├── godEMrCZmJkHYH1X9dN4Nm0U7.svg ├── hIpTa3A5rQz65ssiVuRJu87X6.svg └── wHzf1M7sd7B3zVa2scBMnjqRf.svg ├── bar.go ├── bar_filler.go ├── bar_filler_bar.go ├── bar_filler_nop.go ├── bar_filler_spinner.go ├── bar_option.go ├── bar_test.go ├── barbench_test.go ├── container_option.go ├── cwriter ├── cuuAndEd_construction_bench_test.go ├── doc.go ├── util_bsd.go ├── util_linux.go ├── util_solaris.go ├── util_zos.go ├── writer.go ├── writer_posix.go └── writer_windows.go ├── decor ├── any.go ├── counters.go ├── decorator.go ├── doc.go ├── elapsed.go ├── eta.go ├── meta.go ├── moving_average.go ├── name.go ├── on_abort.go ├── on_compete_or_on_abort.go ├── on_complete.go ├── on_condition.go ├── percentage.go ├── percentage_test.go ├── size_type.go ├── size_type_test.go ├── sizeb1000_string.go ├── sizeb1024_string.go ├── speed.go ├── speed_test.go └── spinner.go ├── decorators_test.go ├── doc.go ├── draw_test.go ├── example_test.go ├── export_test.go ├── go.mod ├── go.sum ├── heap_manager.go ├── internal ├── percentage.go ├── percentage_test.go └── width.go ├── priority_queue.go ├── progress.go ├── progress_test.go ├── proxyreader.go ├── proxyreader_test.go ├── proxywriter.go └── proxywriter_test.go /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | on: 4 | push: 5 | tags-ignore: 6 | - v* 7 | branches: 8 | - master 9 | - main 10 | pull_request: 11 | 12 | permissions: 13 | contents: read 14 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 15 | pull-requests: read 16 | 17 | jobs: 18 | golangci: 19 | strategy: 20 | matrix: 21 | go-version: [stable] 22 | os: [ubuntu-latest, macos-latest] 23 | runs-on: ${{ matrix.os }} 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-go@v5 27 | with: 28 | go-version: ${{ matrix.go-version }} 29 | - uses: golangci/golangci-lint-action@v7 30 | with: 31 | version: latest 32 | 33 | # Optional: golangci-lint command line arguments. 34 | # args: --issues-exit-code=0 35 | 36 | # Optional: show only new issues if it's a pull request. The default value is `false`. 37 | only-new-issues: true 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | tags-ignore: 6 | - v* 7 | branches: 8 | - master 9 | - main 10 | pull_request: 11 | 12 | permissions: 13 | contents: read 14 | pull-requests: read 15 | 16 | jobs: 17 | test: 18 | strategy: 19 | matrix: 20 | go-version: [stable, oldstable] 21 | os: [ubuntu-latest, macos-latest, windows-latest] 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-go@v5 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | - run: go test -race ./... 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Test binary, build with `go test -c` 2 | *.test 3 | 4 | # Output of the go coverage tool, specifically when used with LiteIDE 5 | *.out 6 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | When contributing your first changes, please include an empty commit for 2 | copyright waiver using the following message (replace 'John Doe' with 3 | your name or nickname): 4 | 5 | John Doe Copyright Waiver 6 | 7 | I dedicate any and all copyright interest in this software to the 8 | public domain. I make this dedication for the benefit of the public at 9 | large and to the detriment of my heirs and successors. I intend this 10 | dedication to be an overt act of relinquishment in perpetuity of all 11 | present and future rights to this software under copyright law. 12 | 13 | The command to create an empty commit from the command-line is: 14 | 15 | git commit --allow-empty 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multi Progress Bar 2 | 3 | [![GoDoc](https://pkg.go.dev/badge/github.com/vbauerster/mpb)](https://pkg.go.dev/github.com/vbauerster/mpb/v8) 4 | [![Test status](https://github.com/vbauerster/mpb/actions/workflows/test.yml/badge.svg)](https://github.com/vbauerster/mpb/actions/workflows/test.yml) 5 | [![Lint status](https://github.com/vbauerster/mpb/actions/workflows/golangci-lint.yml/badge.svg)](https://github.com/vbauerster/mpb/actions/workflows/golangci-lint.yml) 6 | 7 | **mpb** is a Go lib for rendering progress bars in terminal applications. 8 | 9 | ## Features 10 | 11 | - **Multiple Bars**: Multiple progress bars are supported 12 | - **Dynamic Total**: Set total while bar is running 13 | - **Dynamic Add/Remove**: Dynamically add or remove bars 14 | - **Cancellation**: Cancel whole rendering process 15 | - **Predefined Decorators**: Elapsed time, [ewma](https://github.com/VividCortex/ewma) based ETA, Percentage, Bytes counter 16 | - **Decorator's width sync**: Synchronized decorator's width among multiple bars 17 | 18 | ## Usage 19 | 20 | #### [Rendering single bar](_examples/singleBar/main.go) 21 | 22 | ```go 23 | package main 24 | 25 | import ( 26 | "math/rand" 27 | "time" 28 | 29 | "github.com/vbauerster/mpb/v8" 30 | "github.com/vbauerster/mpb/v8/decor" 31 | ) 32 | 33 | func main() { 34 | // initialize progress container, with custom width 35 | p := mpb.New(mpb.WithWidth(64)) 36 | 37 | total := 100 38 | name := "Single Bar:" 39 | // create a single bar, which will inherit container's width 40 | bar := p.New(int64(total), 41 | // BarFillerBuilder with custom style 42 | mpb.BarStyle().Lbound("╢").Filler("▌").Tip("▌").Padding("░").Rbound("╟"), 43 | mpb.PrependDecorators( 44 | // display our name with one space on the right 45 | decor.Name(name, decor.WC{C: decor.DindentRight | decor.DextraSpace}), 46 | // replace ETA decorator with "done" message, OnComplete event 47 | decor.OnComplete(decor.AverageETA(decor.ET_STYLE_GO), "done"), 48 | ), 49 | mpb.AppendDecorators(decor.Percentage()), 50 | ) 51 | // simulating some work 52 | max := 100 * time.Millisecond 53 | for i := 0; i < total; i++ { 54 | time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) 55 | bar.Increment() 56 | } 57 | // wait for our bar to complete and flush 58 | p.Wait() 59 | } 60 | ``` 61 | 62 | #### [Rendering multiple bars](_examples/multiBars/main.go) 63 | 64 | ```go 65 | var wg sync.WaitGroup 66 | // passed wg will be accounted at p.Wait() call 67 | p := mpb.New(mpb.WithWaitGroup(&wg)) 68 | total, numBars := 100, 3 69 | wg.Add(numBars) 70 | 71 | for i := 0; i < numBars; i++ { 72 | name := fmt.Sprintf("Bar#%d:", i) 73 | bar := p.AddBar(int64(total), 74 | mpb.PrependDecorators( 75 | // simple name decorator 76 | decor.Name(name), 77 | // decor.DSyncWidth bit enables column width synchronization 78 | decor.Percentage(decor.WCSyncSpace), 79 | ), 80 | mpb.AppendDecorators( 81 | // replace ETA decorator with "done" message, OnComplete event 82 | decor.OnComplete( 83 | // ETA decorator with ewma age of 30 84 | decor.EwmaETA(decor.ET_STYLE_GO, 30, decor.WCSyncWidth), "done", 85 | ), 86 | ), 87 | ) 88 | // simulating some work 89 | go func() { 90 | defer wg.Done() 91 | rng := rand.New(rand.NewSource(time.Now().UnixNano())) 92 | max := 100 * time.Millisecond 93 | for i := 0; i < total; i++ { 94 | // start variable is solely for EWMA calculation 95 | // EWMA's unit of measure is an iteration's duration 96 | start := time.Now() 97 | time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) 98 | // we need to call EwmaIncrement to fulfill ewma decorator's contract 99 | bar.EwmaIncrement(time.Since(start)) 100 | } 101 | }() 102 | } 103 | // wait for passed wg and for all bars to complete and flush 104 | p.Wait() 105 | ``` 106 | 107 | #### [dynTotal example](_examples/dynTotal/main.go) 108 | 109 | ![dynTotal](_svg/godEMrCZmJkHYH1X9dN4Nm0U7.svg) 110 | 111 | #### [complex example](_examples/complex/main.go) 112 | 113 | ![complex](_svg/wHzf1M7sd7B3zVa2scBMnjqRf.svg) 114 | 115 | #### [io example](_examples/io/main.go) 116 | 117 | ![io](_svg/hIpTa3A5rQz65ssiVuRJu87X6.svg) 118 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /_examples/.gitignore: -------------------------------------------------------------------------------- 1 | go.sum 2 | -------------------------------------------------------------------------------- /_examples/barExtender/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/barExtender 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/barExtender/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "math/rand" 7 | "sync" 8 | "time" 9 | 10 | "github.com/vbauerster/mpb/v8" 11 | "github.com/vbauerster/mpb/v8/decor" 12 | ) 13 | 14 | func main() { 15 | var wg sync.WaitGroup 16 | // passed wg will be accounted at p.Wait() call 17 | p := mpb.New(mpb.WithWaitGroup(&wg)) 18 | total, numBars := 100, 3 19 | wg.Add(numBars) 20 | 21 | for i := 0; i < numBars; i++ { 22 | name := fmt.Sprintf("Bar#%d:", i) 23 | efn := func(w io.Writer, s decor.Statistics) (err error) { 24 | if s.Completed { 25 | _, err = fmt.Fprintf(w, "Bar id: %d has been completed\n", s.ID) 26 | } 27 | return err 28 | } 29 | bar := p.AddBar(int64(total), 30 | mpb.BarExtender(mpb.BarFillerFunc(efn), false), 31 | mpb.PrependDecorators( 32 | // simple name decorator 33 | decor.Name(name), 34 | // decor.DSyncWidth bit enables column width synchronization 35 | decor.Percentage(decor.WCSyncSpace), 36 | ), 37 | mpb.AppendDecorators( 38 | // replace ETA decorator with "done" message, OnComplete event 39 | decor.OnComplete( 40 | // ETA decorator with ewma age of 30 41 | decor.EwmaETA(decor.ET_STYLE_GO, 30), "done", 42 | ), 43 | ), 44 | ) 45 | // simulating some work 46 | go func() { 47 | defer wg.Done() 48 | rng := rand.New(rand.NewSource(time.Now().UnixNano())) 49 | max := 100 * time.Millisecond 50 | for i := 0; i < total; i++ { 51 | // start variable is solely for EWMA calculation 52 | // EWMA's unit of measure is an iteration's duration 53 | start := time.Now() 54 | time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) 55 | // we need to call EwmaIncrement to fulfill ewma decorator's contract 56 | bar.EwmaIncrement(time.Since(start)) 57 | } 58 | }() 59 | } 60 | // wait for passed wg and for all bars to complete and flush 61 | p.Wait() 62 | } 63 | -------------------------------------------------------------------------------- /_examples/barExtenderRev/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/barExtenderRev 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/barExtenderRev/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "math/rand" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/vbauerster/mpb/v8" 11 | "github.com/vbauerster/mpb/v8/decor" 12 | ) 13 | 14 | var curTask uint32 15 | var doneTasks uint32 16 | 17 | type task struct { 18 | id uint32 19 | total int64 20 | bar *mpb.Bar 21 | } 22 | 23 | func main() { 24 | numTasks := 4 25 | 26 | var total int64 27 | var filler mpb.BarFiller 28 | tasks := make([]*task, numTasks) 29 | 30 | for i := 0; i < numTasks; i++ { 31 | task := &task{ 32 | id: uint32(i), 33 | total: rand.Int63n(666) + 100, 34 | } 35 | total += task.total 36 | filler = middleware(filler, task.id) 37 | tasks[i] = task 38 | } 39 | 40 | filler = newLineMiddleware(filler) 41 | 42 | p := mpb.New() 43 | 44 | for i := 0; i < numTasks; i++ { 45 | bar := p.AddBar(tasks[i].total, 46 | mpb.BarExtender(filler, true), // all bars share same extender filler 47 | mpb.BarFuncOptional(func() mpb.BarOption { 48 | return mpb.BarQueueAfter(tasks[i-1].bar) 49 | }, i != 0), 50 | mpb.PrependDecorators( 51 | decor.Name("current:", decor.WCSyncWidthR), 52 | ), 53 | mpb.AppendDecorators( 54 | decor.Percentage(decor.WCSyncWidth), 55 | ), 56 | ) 57 | tasks[i].bar = bar 58 | } 59 | 60 | tb := p.AddBar(0, 61 | mpb.PrependDecorators( 62 | decor.Any(func(st decor.Statistics) string { 63 | return fmt.Sprintf("TOTAL(%d/%d)", atomic.LoadUint32(&doneTasks), len(tasks)) 64 | }, decor.WCSyncWidthR), 65 | ), 66 | mpb.AppendDecorators( 67 | decor.Percentage(decor.WCSyncWidth), 68 | ), 69 | ) 70 | 71 | tb.SetTotal(total, false) 72 | 73 | for _, t := range tasks { 74 | atomic.StoreUint32(&curTask, t.id) 75 | complete(t.bar, tb) 76 | atomic.AddUint32(&doneTasks, 1) 77 | } 78 | 79 | tb.EnableTriggerComplete() 80 | 81 | p.Wait() 82 | } 83 | 84 | func middleware(base mpb.BarFiller, id uint32) mpb.BarFiller { 85 | var done bool 86 | fn := func(w io.Writer, st decor.Statistics) error { 87 | if !done { 88 | if atomic.LoadUint32(&curTask) != id { 89 | _, err := fmt.Fprintf(w, " Taksk %02d\n", id) 90 | return err 91 | } 92 | if !st.Completed { 93 | _, err := fmt.Fprintf(w, "=> Taksk %02d\n", id) 94 | return err 95 | } 96 | done = true 97 | } 98 | _, err := fmt.Fprintf(w, " Taksk %02d: Done!\n", id) 99 | return err 100 | } 101 | if base == nil { 102 | return mpb.BarFillerFunc(fn) 103 | } 104 | return mpb.BarFillerFunc(func(w io.Writer, st decor.Statistics) error { 105 | err := fn(w, st) 106 | if err != nil { 107 | return err 108 | } 109 | return base.Fill(w, st) 110 | }) 111 | } 112 | 113 | func newLineMiddleware(base mpb.BarFiller) mpb.BarFiller { 114 | return mpb.BarFillerFunc(func(w io.Writer, st decor.Statistics) error { 115 | _, err := fmt.Fprintln(w) 116 | if err != nil { 117 | return err 118 | } 119 | return base.Fill(w, st) 120 | }) 121 | } 122 | 123 | func complete(bar, totalBar *mpb.Bar) { 124 | max := 100 * time.Millisecond 125 | for !bar.Completed() { 126 | n := rand.Int63n(10) + 1 127 | incrementBars(n, bar, totalBar) 128 | time.Sleep(time.Duration(n) * max / 10) 129 | } 130 | bar.Wait() 131 | } 132 | 133 | func incrementBars(n int64, bb ...*mpb.Bar) { 134 | for _, b := range bb { 135 | b.IncrInt64(n) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /_examples/cancel/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/cancel 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/cancel/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "sync" 8 | "time" 9 | 10 | "github.com/vbauerster/mpb/v8" 11 | "github.com/vbauerster/mpb/v8/decor" 12 | ) 13 | 14 | func main() { 15 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 16 | defer cancel() 17 | 18 | var wg sync.WaitGroup 19 | // passed wg will be accounted at p.Wait() call 20 | p := mpb.NewWithContext(ctx, mpb.WithWaitGroup(&wg)) 21 | total := 300 22 | numBars := 3 23 | wg.Add(numBars) 24 | 25 | for i := 0; i < numBars; i++ { 26 | name := fmt.Sprintf("Bar#%02d: ", i) 27 | bar := p.AddBar(int64(total), 28 | mpb.PrependDecorators( 29 | decor.Name(name, decor.WCSyncWidthR), 30 | decor.EwmaETA(decor.ET_STYLE_GO, 30, decor.WCSyncWidth), 31 | ), 32 | mpb.AppendDecorators( 33 | // note that OnComplete will not be fired, because of cancel 34 | decor.OnComplete(decor.Percentage(decor.WC{W: 5}), "done"), 35 | ), 36 | ) 37 | 38 | go func() { 39 | defer wg.Done() 40 | rng := rand.New(rand.NewSource(time.Now().UnixNano())) 41 | max := 100 * time.Millisecond 42 | for bar.IsRunning() { 43 | // start variable is solely for EWMA calculation 44 | // EWMA's unit of measure is an iteration's duration 45 | start := time.Now() 46 | time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) 47 | // we need to call EwmaIncrement to fulfill ewma decorator's contract 48 | bar.EwmaIncrement(time.Since(start)) 49 | } 50 | }() 51 | } 52 | // wait for passed wg and for all bars to complete and flush 53 | p.Wait() 54 | } 55 | -------------------------------------------------------------------------------- /_examples/complex/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/complex 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/fatih/color v1.18.0 9 | github.com/vbauerster/mpb/v8 v8.10.1 10 | ) 11 | 12 | require ( 13 | github.com/VividCortex/ewma v1.2.0 // indirect 14 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 15 | github.com/mattn/go-colorable v0.1.14 // indirect 16 | github.com/mattn/go-isatty v0.0.20 // indirect 17 | github.com/mattn/go-runewidth v0.0.16 // indirect 18 | github.com/rivo/uniseg v0.4.7 // indirect 19 | golang.org/x/sys v0.33.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /_examples/complex/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "time" 7 | 8 | "github.com/fatih/color" 9 | "github.com/vbauerster/mpb/v8" 10 | "github.com/vbauerster/mpb/v8/decor" 11 | ) 12 | 13 | func main() { 14 | numBars := 4 15 | // to support color in Windows following both options are required 16 | p := mpb.New( 17 | mpb.WithOutput(color.Output), 18 | mpb.WithAutoRefresh(), 19 | ) 20 | 21 | red, green := color.New(color.FgRed), color.New(color.FgGreen) 22 | 23 | for i := 0; i < numBars; i++ { 24 | task := fmt.Sprintf("Task#%02d:", i) 25 | queue := make([]*mpb.Bar, 2) 26 | queue[0] = p.AddBar(rand.Int63n(201)+100, 27 | mpb.PrependDecorators( 28 | decor.Name(task, decor.WC{C: decor.DindentRight | decor.DextraSpace}), 29 | decor.Name("downloading", decor.WCSyncSpaceR), 30 | decor.CountersNoUnit("%d / %d", decor.WCSyncWidth), 31 | ), 32 | mpb.AppendDecorators( 33 | decor.OnComplete(decor.Percentage(decor.WC{W: 5}), "done"), 34 | ), 35 | ) 36 | queue[1] = p.AddBar(rand.Int63n(101)+100, 37 | mpb.BarQueueAfter(queue[0]), // this bar is queued 38 | mpb.BarFillerClearOnComplete(), 39 | mpb.PrependDecorators( 40 | decor.Name(task, decor.WC{C: decor.DindentRight | decor.DextraSpace}), 41 | decor.OnCompleteMeta( 42 | decor.OnComplete( 43 | decor.Meta(decor.Name("installing", decor.WCSyncSpaceR), toMetaFunc(red)), 44 | "done!", 45 | ), 46 | toMetaFunc(green), 47 | ), 48 | decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_MMSS, 0, decor.WCSyncWidth), ""), 49 | ), 50 | mpb.AppendDecorators( 51 | decor.OnComplete(decor.Percentage(decor.WC{W: 5}), ""), 52 | ), 53 | ) 54 | 55 | go func() { 56 | for _, b := range queue { 57 | complete(b) 58 | } 59 | }() 60 | } 61 | 62 | p.Wait() 63 | } 64 | 65 | func complete(bar *mpb.Bar) { 66 | max := 100 * time.Millisecond 67 | for !bar.Completed() { 68 | // start variable is solely for EWMA calculation 69 | // EWMA's unit of measure is an iteration's duration 70 | start := time.Now() 71 | time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) 72 | // we need to call EwmaIncrement to fulfill ewma decorator's contract 73 | bar.EwmaIncrInt64(rand.Int63n(5)+1, time.Since(start)) 74 | } 75 | } 76 | 77 | func toMetaFunc(c *color.Color) func(string) string { 78 | return func(s string) string { 79 | return c.Sprint(s) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /_examples/decoratorsOnTop/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/decoratorsOnTop 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/decoratorsOnTop/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "math/rand" 6 | "time" 7 | 8 | "github.com/vbauerster/mpb/v8" 9 | "github.com/vbauerster/mpb/v8/decor" 10 | ) 11 | 12 | func main() { 13 | p := mpb.New() 14 | 15 | total := 100 16 | bar := p.New(int64(total), 17 | mpb.NopStyle(), // make main bar style nop, so there are just decorators 18 | mpb.BarExtender(extended(mpb.BarStyle().Build()), false), // extend with normal bar on the next line 19 | mpb.PrependDecorators( 20 | decor.Name("Percentage: "), 21 | decor.NewPercentage("%d"), 22 | ), 23 | mpb.AppendDecorators( 24 | decor.Name("ETA: "), 25 | decor.OnComplete( 26 | decor.AverageETA(decor.ET_STYLE_GO), "done", 27 | ), 28 | ), 29 | ) 30 | // simulating some work 31 | max := 100 * time.Millisecond 32 | for i := 0; i < total; i++ { 33 | time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) 34 | bar.Increment() 35 | } 36 | // wait for our bar to complete and flush 37 | p.Wait() 38 | } 39 | 40 | func extended(base mpb.BarFiller) mpb.BarFiller { 41 | return mpb.BarFillerFunc(func(w io.Writer, st decor.Statistics) error { 42 | err := base.Fill(w, st) 43 | if err != nil { 44 | return err 45 | } 46 | _, err = io.WriteString(w, "\n") 47 | return err 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /_examples/differentWidth/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/differentWidth 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/differentWidth/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "sync" 7 | "time" 8 | 9 | "github.com/vbauerster/mpb/v8" 10 | "github.com/vbauerster/mpb/v8/decor" 11 | ) 12 | 13 | func main() { 14 | var wg sync.WaitGroup 15 | // passed wg will be accounted at p.Wait() call 16 | p := mpb.New( 17 | mpb.WithWaitGroup(&wg), 18 | mpb.WithWidth(60), 19 | ) 20 | total, numBars := 100, 3 21 | wg.Add(numBars) 22 | 23 | for i := 0; i < numBars; i++ { 24 | name := fmt.Sprintf("Bar#%d:", i) 25 | bar := p.AddBar(int64(total), 26 | // set BarWidth 40 for bar 1 and 2 27 | mpb.BarOptional(mpb.BarWidth(40), i > 0), 28 | mpb.PrependDecorators( 29 | // simple name decorator 30 | decor.Name(name), 31 | // decor.DSyncWidth bit enables column width synchronization 32 | decor.Percentage(decor.WCSyncSpace), 33 | ), 34 | mpb.AppendDecorators( 35 | // replace ETA decorator with "done" message, OnComplete event 36 | decor.OnComplete( 37 | // ETA decorator with ewma age of 30 38 | decor.EwmaETA(decor.ET_STYLE_GO, 30), "done", 39 | ), 40 | ), 41 | ) 42 | // simulating some work 43 | go func() { 44 | defer wg.Done() 45 | rng := rand.New(rand.NewSource(time.Now().UnixNano())) 46 | max := 100 * time.Millisecond 47 | for i := 0; i < total; i++ { 48 | // start variable is solely for EWMA calculation 49 | // EWMA's unit of measure is an iteration's duration 50 | start := time.Now() 51 | time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) 52 | // we need to call EwmaIncrement to fulfill ewma decorator's contract 53 | bar.EwmaIncrement(time.Since(start)) 54 | } 55 | }() 56 | } 57 | // wait for passed wg and for all bars to complete and flush 58 | p.Wait() 59 | } 60 | -------------------------------------------------------------------------------- /_examples/dynTotal/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/dynTotal 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/dynTotal/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "math/rand" 6 | "time" 7 | 8 | "github.com/vbauerster/mpb/v8" 9 | "github.com/vbauerster/mpb/v8/decor" 10 | ) 11 | 12 | func main() { 13 | p := mpb.New(mpb.WithWidth(64)) 14 | 15 | // new bar with 'trigger complete event' disabled, because total is zero 16 | bar := p.AddBar(0, 17 | mpb.PrependDecorators(decor.Counters(decor.SizeB1024(0), "% .1f / % .1f")), 18 | mpb.AppendDecorators(decor.Percentage()), 19 | ) 20 | 21 | maxSleep := 100 * time.Millisecond 22 | read := makeStream(200) 23 | for { 24 | n, err := read() 25 | if err == io.EOF { 26 | // triggering complete event now 27 | bar.SetTotal(-1, true) 28 | break 29 | } 30 | // increment methods won't trigger complete event because bar was constructed with total = 0 31 | bar.IncrBy(n) 32 | // following call is not required, it's called to show some progress instead of an empty bar 33 | bar.SetTotal(bar.Current()+2048, false) 34 | time.Sleep(time.Duration(rand.Intn(10)+1) * maxSleep / 10) 35 | } 36 | 37 | p.Wait() 38 | } 39 | 40 | func makeStream(limit int) func() (int, error) { 41 | return func() (int, error) { 42 | if limit <= 0 { 43 | return 0, io.EOF 44 | } 45 | limit-- 46 | return rand.Intn(1024) + 1, nil 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /_examples/gomodtidyall: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | for d in *; do 5 | [ ! -d "$d" ] && continue 6 | pushd "$d" >/dev/null 2>&1 7 | go mod tidy 8 | popd >/dev/null 2>&1 9 | done 10 | -------------------------------------------------------------------------------- /_examples/io/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/io 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/io/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "io" 6 | "time" 7 | 8 | "github.com/vbauerster/mpb/v8" 9 | "github.com/vbauerster/mpb/v8/decor" 10 | ) 11 | 12 | func main() { 13 | var total int64 = 64 * 1024 * 1024 14 | 15 | r, w := io.Pipe() 16 | 17 | go func() { 18 | for i := 0; i < 1024; i++ { 19 | _, _ = io.Copy(w, io.LimitReader(rand.Reader, 64*1024)) 20 | time.Sleep(time.Second / 10) 21 | } 22 | _ = w.Close() 23 | }() 24 | 25 | p := mpb.New( 26 | mpb.WithWidth(60), 27 | mpb.WithRefreshRate(180*time.Millisecond), 28 | ) 29 | 30 | bar := p.New(total, 31 | mpb.BarStyle().Rbound("|"), 32 | mpb.PrependDecorators( 33 | decor.Counters(decor.SizeB1024(0), "% .2f / % .2f"), 34 | ), 35 | mpb.AppendDecorators( 36 | decor.EwmaETA(decor.ET_STYLE_GO, 30), 37 | decor.Name(" ] "), 38 | decor.EwmaSpeed(decor.SizeB1024(0), "% .2f", 30), 39 | ), 40 | ) 41 | 42 | // create proxy reader 43 | proxyReader := bar.ProxyReader(r) 44 | defer func() { 45 | _ = proxyReader.Close() 46 | }() 47 | 48 | // copy from proxyReader, ignoring errors 49 | _, _ = io.Copy(io.Discard, proxyReader) 50 | 51 | p.Wait() 52 | } 53 | -------------------------------------------------------------------------------- /_examples/mexicanBar/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/mexicanBar 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/mexicanBar/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/vbauerster/mpb/v8" 8 | "github.com/vbauerster/mpb/v8/decor" 9 | ) 10 | 11 | func main() { 12 | // initialize progress container, with custom width 13 | p := mpb.New(mpb.WithWidth(80)) 14 | 15 | total := 100 16 | name := "Complex Filler:" 17 | bs := mpb.BarStyle() 18 | bs = bs.LboundMeta(func(s string) string { 19 | return "\033[34m" + s + "\033[0m" // blue 20 | }) 21 | bs = bs.Filler("_").FillerMeta(func(s string) string { 22 | return "\033[36m" + s + "\033[0m" // cyan 23 | }) 24 | bs = bs.Tip("⛵").TipMeta(func(s string) string { 25 | return "\033[31m" + s + "\033[0m" // red 26 | }) 27 | bs = bs.TipOnComplete() // leave tip on complete 28 | bs = bs.Padding("_").PaddingMeta(func(s string) string { 29 | return "\033[36m" + s + "\033[0m" // cyan 30 | }) 31 | bs = bs.RboundMeta(func(s string) string { 32 | return "\033[34m" + s + "\033[0m" // blue 33 | }) 34 | bar := p.New(int64(total), bs, 35 | mpb.PrependDecorators(decor.Name(name)), 36 | mpb.AppendDecorators(decor.Percentage()), 37 | ) 38 | // simulating some work 39 | max := 100 * time.Millisecond 40 | for i := 0; i < total; i++ { 41 | time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) 42 | bar.Increment() 43 | } 44 | // wait for our bar to complete 45 | p.Wait() 46 | } 47 | -------------------------------------------------------------------------------- /_examples/multiBars/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/multiBars 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/multiBars/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "sync" 7 | "time" 8 | 9 | "github.com/vbauerster/mpb/v8" 10 | "github.com/vbauerster/mpb/v8/decor" 11 | ) 12 | 13 | func main() { 14 | var wg sync.WaitGroup 15 | // passed wg will be accounted at p.Wait() call 16 | p := mpb.New(mpb.WithWaitGroup(&wg)) 17 | total, numBars := 100, 3 18 | wg.Add(numBars) 19 | 20 | for i := 0; i < numBars; i++ { 21 | name := fmt.Sprintf("Bar#%d:", i) 22 | bar := p.AddBar(int64(total), 23 | mpb.PrependDecorators( 24 | // simple name decorator 25 | decor.Name(name), 26 | // decor.DSyncWidth bit enables column width synchronization 27 | decor.Percentage(decor.WCSyncSpace), 28 | ), 29 | mpb.AppendDecorators( 30 | // replace ETA decorator with "done" message, OnComplete event 31 | decor.OnComplete( 32 | // ETA decorator with ewma age of 30 33 | decor.EwmaETA(decor.ET_STYLE_GO, 30, decor.WCSyncWidth), "done", 34 | ), 35 | ), 36 | ) 37 | // simulating some work 38 | go func() { 39 | defer wg.Done() 40 | rng := rand.New(rand.NewSource(time.Now().UnixNano())) 41 | max := 100 * time.Millisecond 42 | for i := 0; i < total; i++ { 43 | // start variable is solely for EWMA calculation 44 | // EWMA's unit of measure is an iteration's duration 45 | start := time.Now() 46 | time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) 47 | // we need to call EwmaIncrement to fulfill ewma decorator's contract 48 | bar.EwmaIncrement(time.Since(start)) 49 | } 50 | }() 51 | } 52 | // wait for passed wg and for all bars to complete and flush 53 | p.Wait() 54 | } 55 | -------------------------------------------------------------------------------- /_examples/poplog/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/poplog 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/poplog/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "time" 7 | 8 | "github.com/vbauerster/mpb/v8" 9 | "github.com/vbauerster/mpb/v8/decor" 10 | ) 11 | 12 | func main() { 13 | rng := rand.New(rand.NewSource(time.Now().UnixNano())) 14 | p := mpb.New(mpb.PopCompletedMode()) 15 | total, numBars := 100, 4 16 | for i := 0; i < numBars; i++ { 17 | name := fmt.Sprintf("Bar#%d:", i) 18 | bar := p.AddBar(int64(total), 19 | mpb.BarFillerOnComplete(fmt.Sprintf("%s has been completed", name)), 20 | mpb.BarFillerTrim(), 21 | mpb.PrependDecorators( 22 | decor.OnComplete(decor.Name(name), ""), 23 | decor.OnComplete(decor.NewPercentage(" % d "), ""), 24 | ), 25 | mpb.AppendDecorators( 26 | decor.OnComplete(decor.Name(" "), ""), 27 | decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_GO, 30), ""), 28 | ), 29 | ) 30 | // simulating some work 31 | max := 100 * time.Millisecond 32 | for i := 0; i < total; i++ { 33 | // start variable is solely for EWMA calculation 34 | // EWMA's unit of measure is an iteration's duration 35 | start := time.Now() 36 | time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) 37 | // we need to call EwmaIncrement to fulfill ewma decorator's contract 38 | bar.EwmaIncrement(time.Since(start)) 39 | } 40 | } 41 | 42 | p.Wait() 43 | } 44 | -------------------------------------------------------------------------------- /_examples/progressAsWriter/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/progressAsWriter 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/progressAsWriter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math/rand" 7 | "sync" 8 | "time" 9 | 10 | "github.com/vbauerster/mpb/v8" 11 | "github.com/vbauerster/mpb/v8/decor" 12 | ) 13 | 14 | func main() { 15 | total, numBars := 100, 2 16 | var bwg, qwg sync.WaitGroup 17 | bwg.Add(numBars) 18 | qwg.Add(1) 19 | done := make(chan interface{}) 20 | p := mpb.New(mpb.WithWidth(64), mpb.WithShutdownNotifier(done), mpb.WithWaitGroup(&qwg)) 21 | 22 | log.SetOutput(p) 23 | 24 | go func() { 25 | defer qwg.Done() 26 | for { 27 | select { 28 | case <-done: 29 | // after done, underlying io.Writer returns mpb.ErrDone 30 | // so following isn't printed 31 | log.Println("all done") 32 | return 33 | default: 34 | log.Println("waiting for done") 35 | time.Sleep(150 * time.Millisecond) 36 | } 37 | } 38 | }() 39 | 40 | nopBar := p.MustAdd(0, nil) 41 | 42 | for i := 0; i < numBars; i++ { 43 | name := fmt.Sprintf("Bar#%d:", i) 44 | bar := p.AddBar(int64(total), 45 | mpb.PrependDecorators( 46 | decor.Name(name), 47 | decor.Percentage(decor.WCSyncSpace), 48 | ), 49 | mpb.AppendDecorators( 50 | decor.OnComplete( 51 | decor.EwmaETA(decor.ET_STYLE_GO, 30, decor.WCSyncWidth), "done", 52 | ), 53 | ), 54 | ) 55 | // simulating some work 56 | go func() { 57 | defer bwg.Done() 58 | rng := rand.New(rand.NewSource(time.Now().UnixNano())) 59 | max := 100 * time.Millisecond 60 | for i := 0; i < total; i++ { 61 | // start variable is solely for EWMA calculation 62 | // EWMA's unit of measure is an iteration's duration 63 | start := time.Now() 64 | time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) 65 | // we need to call EwmaIncrement to fulfill ewma decorator's contract 66 | bar.EwmaIncrement(time.Since(start)) 67 | } 68 | log.Println(name, "done") 69 | }() 70 | } 71 | 72 | bwg.Wait() 73 | log.Println("completing nop bar") 74 | nopBar.EnableTriggerComplete() 75 | 76 | p.Wait() 77 | } 78 | -------------------------------------------------------------------------------- /_examples/quietMode/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/quietMode 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/quietMode/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "math/rand" 8 | "sync" 9 | "time" 10 | 11 | "github.com/vbauerster/mpb/v8" 12 | "github.com/vbauerster/mpb/v8/decor" 13 | ) 14 | 15 | var quietMode bool 16 | 17 | func init() { 18 | flag.BoolVar(&quietMode, "q", false, "quiet mode") 19 | } 20 | 21 | func main() { 22 | flag.Parse() 23 | var wg sync.WaitGroup 24 | // passed wg will be accounted at p.Wait() call 25 | p := mpb.New( 26 | mpb.WithWaitGroup(&wg), 27 | mpb.ContainerOptional(mpb.WithOutput(io.Discard), quietMode), 28 | ) 29 | total, numBars := 100, 3 30 | wg.Add(numBars) 31 | 32 | for i := 0; i < numBars; i++ { 33 | name := fmt.Sprintf("Bar#%d:", i) 34 | bar := p.AddBar(int64(total), 35 | mpb.PrependDecorators( 36 | // simple name decorator 37 | decor.Name(name), 38 | // decor.DSyncWidth bit enables column width synchronization 39 | decor.Percentage(decor.WCSyncSpace), 40 | ), 41 | mpb.AppendDecorators( 42 | // replace ETA decorator with "done" message, OnComplete event 43 | decor.OnComplete( 44 | // ETA decorator with ewma age of 30 45 | decor.EwmaETA(decor.ET_STYLE_GO, 30), "done", 46 | ), 47 | ), 48 | ) 49 | // simulating some work 50 | go func() { 51 | defer wg.Done() 52 | rng := rand.New(rand.NewSource(time.Now().UnixNano())) 53 | max := 100 * time.Millisecond 54 | for i := 0; i < total; i++ { 55 | // start variable is solely for EWMA calculation 56 | // EWMA's unit of measure is an iteration's duration 57 | start := time.Now() 58 | time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) 59 | // we need to call EwmaIncrement to fulfill ewma decorator's contract 60 | bar.EwmaIncrement(time.Since(start)) 61 | } 62 | }() 63 | } 64 | // wait for passed wg and for all bars to complete and flush 65 | p.Wait() 66 | fmt.Println("done") 67 | } 68 | -------------------------------------------------------------------------------- /_examples/remove/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/remove 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/remove/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "sync" 7 | "time" 8 | 9 | "github.com/vbauerster/mpb/v8" 10 | "github.com/vbauerster/mpb/v8/decor" 11 | ) 12 | 13 | func main() { 14 | var wg sync.WaitGroup 15 | // passed wg will be accounted at p.Wait() call 16 | p := mpb.New(mpb.WithWaitGroup(&wg)) 17 | total := 100 18 | numBars := 3 19 | wg.Add(numBars) 20 | 21 | for i := 0; i < numBars; i++ { 22 | name := fmt.Sprintf("Bar#%d:", i) 23 | bar := p.AddBar(int64(total), 24 | mpb.BarID(i), 25 | mpb.BarOptional(mpb.BarRemoveOnComplete(), i == 0), 26 | mpb.PrependDecorators( 27 | decor.Name(name), 28 | ), 29 | mpb.AppendDecorators( 30 | decor.Any(func(s decor.Statistics) string { 31 | return fmt.Sprintf("completed: %v", s.Completed) 32 | }, decor.WCSyncSpaceR), 33 | decor.Any(func(s decor.Statistics) string { 34 | return fmt.Sprintf("aborted: %v", s.Aborted) 35 | }, decor.WCSyncSpaceR), 36 | decor.OnComplete(decor.NewPercentage("%d", decor.WCSyncSpace), "done"), 37 | decor.OnAbort(decor.NewPercentage("%d", decor.WCSyncSpace), "ohno"), 38 | ), 39 | ) 40 | go func() { 41 | defer wg.Done() 42 | rng := rand.New(rand.NewSource(time.Now().UnixNano())) 43 | max := 100 * time.Millisecond 44 | for i := 0; bar.IsRunning(); i++ { 45 | if bar.ID() == 2 && i >= 42 { 46 | go bar.Abort(false) 47 | } 48 | time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) 49 | bar.Increment() 50 | } 51 | }() 52 | } 53 | // wait for passed wg and for all bars to complete and flush 54 | p.Wait() 55 | } 56 | -------------------------------------------------------------------------------- /_examples/reverseBar/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/reverseBar 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/reverseBar/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "sync" 7 | "time" 8 | 9 | "github.com/vbauerster/mpb/v8" 10 | "github.com/vbauerster/mpb/v8/decor" 11 | ) 12 | 13 | func main() { 14 | var wg sync.WaitGroup 15 | // passed wg will be accounted at p.Wait() call 16 | p := mpb.New(mpb.WithWaitGroup(&wg)) 17 | total, numBars := 100, 3 18 | wg.Add(numBars) 19 | 20 | condFillerBuilder := func(cond bool) mpb.BarFillerBuilder { 21 | if cond { // reverse Bar on cond 22 | return mpb.BarStyle().Tip("<").Reverse() 23 | } 24 | return mpb.BarStyle() 25 | } 26 | 27 | for i := 0; i < numBars; i++ { 28 | name := fmt.Sprintf("Bar#%d:", i) 29 | bar := p.New(int64(total), 30 | condFillerBuilder(i == 1), 31 | mpb.PrependDecorators( 32 | // simple name decorator 33 | decor.Name(name), 34 | // decor.DSyncWidth bit enables column width synchronization 35 | decor.Percentage(decor.WCSyncSpace), 36 | ), 37 | mpb.AppendDecorators( 38 | // replace ETA decorator with "done" message, OnComplete event 39 | decor.OnComplete( 40 | // ETA decorator with ewma age of 30 41 | decor.EwmaETA(decor.ET_STYLE_GO, 30), "done", 42 | ), 43 | ), 44 | ) 45 | // simulating some work 46 | go func() { 47 | defer wg.Done() 48 | rng := rand.New(rand.NewSource(time.Now().UnixNano())) 49 | max := 100 * time.Millisecond 50 | for i := 0; i < total; i++ { 51 | // start variable is solely for EWMA calculation 52 | // EWMA's unit of measure is an iteration's duration 53 | start := time.Now() 54 | time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) 55 | // we need to call EwmaIncrement to fulfill ewma decorator's contract 56 | bar.EwmaIncrement(time.Since(start)) 57 | } 58 | }() 59 | } 60 | // wait for passed wg and for all bars to complete and flush 61 | p.Wait() 62 | } 63 | -------------------------------------------------------------------------------- /_examples/singleBar/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/singleBar 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/singleBar/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/vbauerster/mpb/v8" 8 | "github.com/vbauerster/mpb/v8/decor" 9 | ) 10 | 11 | func main() { 12 | // initialize progress container, with custom width 13 | p := mpb.New(mpb.WithWidth(64)) 14 | 15 | total := 100 16 | name := "Single Bar:" 17 | // create a single bar, which will inherit container's width 18 | bar := p.New(int64(total), 19 | // BarFillerBuilder with custom style 20 | mpb.BarStyle().Lbound("╢").Filler("▌").Tip("▌").Padding("░").Rbound("╟"), 21 | mpb.PrependDecorators( 22 | // display our name with one space on the right 23 | decor.Name(name, decor.WC{C: decor.DindentRight | decor.DextraSpace}), 24 | // replace ETA decorator with "done" message, OnComplete event 25 | decor.OnComplete(decor.AverageETA(decor.ET_STYLE_GO), "done"), 26 | ), 27 | mpb.AppendDecorators(decor.Percentage()), 28 | ) 29 | // simulating some work 30 | max := 100 * time.Millisecond 31 | for i := 0; i < total; i++ { 32 | time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) 33 | bar.Increment() 34 | } 35 | // wait for our bar to complete and flush 36 | p.Wait() 37 | } 38 | -------------------------------------------------------------------------------- /_examples/spinTipBar/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/spinTipBar 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/spinTipBar/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/vbauerster/mpb/v8" 8 | "github.com/vbauerster/mpb/v8/decor" 9 | ) 10 | 11 | func main() { 12 | // initialize progress container, with custom width 13 | p := mpb.New(mpb.WithWidth(80)) 14 | 15 | total := 100 16 | name := "Single Bar:" 17 | bar := p.New(int64(total), 18 | mpb.BarStyle().Tip(`-`, `\`, `|`, `/`).TipMeta(func(s string) string { 19 | return "\033[31m" + s + "\033[0m" // red 20 | }), 21 | mpb.PrependDecorators(decor.Name(name)), 22 | mpb.AppendDecorators(decor.Percentage()), 23 | ) 24 | // simulating some work 25 | max := 100 * time.Millisecond 26 | for i := 0; i < total; i++ { 27 | time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) 28 | bar.Increment() 29 | } 30 | // wait for our bar to complete and flush 31 | p.Wait() 32 | } 33 | -------------------------------------------------------------------------------- /_examples/spinnerBar/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/spinnerBar 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/spinnerBar/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "sync" 7 | "time" 8 | 9 | "github.com/vbauerster/mpb/v8" 10 | "github.com/vbauerster/mpb/v8/decor" 11 | ) 12 | 13 | func main() { 14 | var wg sync.WaitGroup 15 | // passed wg will be accounted at p.Wait() call 16 | p := mpb.New( 17 | mpb.WithWaitGroup(&wg), 18 | mpb.WithWidth(16), 19 | ) 20 | total, numBars := 101, 3 21 | wg.Add(numBars) 22 | 23 | condFillerBuilder := func(cond bool) mpb.BarFillerBuilder { 24 | if cond { 25 | s := mpb.SpinnerStyle("∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙") 26 | return s.Meta(func(s string) string { 27 | return "\033[31m" + s + "\033[0m" // red 28 | }) 29 | } 30 | return mpb.BarStyle().Lbound("╢").Filler("▌").Tip("▌").Padding("░").Rbound("╟") 31 | } 32 | 33 | for i := 0; i < numBars; i++ { 34 | name := fmt.Sprintf("Bar#%d:", i) 35 | bar := p.New(int64(total), 36 | condFillerBuilder(i != 0), 37 | mpb.PrependDecorators( 38 | // simple name decorator 39 | decor.Name(name), 40 | ), 41 | mpb.AppendDecorators( 42 | // replace ETA decorator with "done" message, OnComplete event 43 | decor.OnComplete( 44 | // ETA decorator with ewma age of 30 45 | decor.EwmaETA(decor.ET_STYLE_GO, 30), "done", 46 | ), 47 | ), 48 | ) 49 | // simulating some work 50 | go func() { 51 | defer wg.Done() 52 | rng := rand.New(rand.NewSource(time.Now().UnixNano())) 53 | max := 100 * time.Millisecond 54 | for i := 0; i < total; i++ { 55 | // start variable is solely for EWMA calculation 56 | // EWMA's unit of measure is an iteration's duration 57 | start := time.Now() 58 | time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) 59 | // we need to call EwmaIncrement to fulfill ewma decorator's contract 60 | bar.EwmaIncrement(time.Since(start)) 61 | } 62 | }() 63 | } 64 | // wait for passed wg and for all bars to complete and flush 65 | p.Wait() 66 | } 67 | -------------------------------------------------------------------------------- /_examples/spinnerDecorator/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/spinnerDecorator 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/spinnerDecorator/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "sync" 7 | "time" 8 | 9 | "github.com/vbauerster/mpb/v8" 10 | "github.com/vbauerster/mpb/v8/decor" 11 | ) 12 | 13 | func main() { 14 | var wg sync.WaitGroup 15 | // passed wg will be accounted at p.Wait() call 16 | p := mpb.New(mpb.WithWaitGroup(&wg), mpb.WithWidth(64)) 17 | total, numBars := 100, 3 18 | wg.Add(numBars) 19 | 20 | for i := 0; i < numBars; i++ { 21 | name := fmt.Sprintf("Bar#%d:", i) 22 | bar := p.AddBar(int64(total), 23 | mpb.PrependDecorators( 24 | // simple name decorator 25 | decor.Name(name), 26 | decor.OnComplete( 27 | // spinner decorator with default style 28 | decor.Spinner(nil, decor.WCSyncSpace), "done", 29 | ), 30 | ), 31 | mpb.AppendDecorators( 32 | // decor.DSyncWidth bit enables column width synchronization 33 | decor.Percentage(decor.WCSyncWidth), 34 | ), 35 | ) 36 | // simulating some work 37 | go func() { 38 | defer wg.Done() 39 | rng := rand.New(rand.NewSource(time.Now().UnixNano())) 40 | max := 100 * time.Millisecond 41 | for i := 0; i < total; i++ { 42 | time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) 43 | bar.Increment() 44 | } 45 | }() 46 | } 47 | // wait for passed wg and for all bars to complete and flush 48 | p.Wait() 49 | } 50 | -------------------------------------------------------------------------------- /_examples/stress/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/stress 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/pkg/profile v1.7.0 9 | github.com/vbauerster/mpb/v8 v8.10.1 10 | ) 11 | 12 | require ( 13 | github.com/VividCortex/ewma v1.2.0 // indirect 14 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 15 | github.com/felixge/fgprof v0.9.5 // indirect 16 | github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect 17 | github.com/mattn/go-runewidth v0.0.16 // indirect 18 | github.com/rivo/uniseg v0.4.7 // indirect 19 | golang.org/x/sys v0.33.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /_examples/stress/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "math/rand" 7 | "os" 8 | "sync" 9 | "time" 10 | 11 | "github.com/pkg/profile" 12 | "github.com/vbauerster/mpb/v8" 13 | "github.com/vbauerster/mpb/v8/decor" 14 | ) 15 | 16 | const ( 17 | totalBars = 42 18 | ) 19 | 20 | var proftype = flag.String("prof", "", "profile type (cpu, mem)") 21 | 22 | func main() { 23 | flag.Parse() 24 | switch *proftype { 25 | case "cpu": 26 | defer profile.Start(profile.CPUProfile, profile.ProfilePath("."), profile.NoShutdownHook).Stop() 27 | case "mem": 28 | defer profile.Start(profile.MemProfile, profile.ProfilePath("."), profile.NoShutdownHook).Stop() 29 | } 30 | var wg sync.WaitGroup 31 | // passed wg will be accounted at p.Wait() call 32 | p := mpb.New( 33 | mpb.WithWaitGroup(&wg), 34 | mpb.WithDebugOutput(os.Stderr), 35 | mpb.WithQueueLen(totalBars), // totalBars is known ahead of time so overriding default queue len is good idea 36 | ) 37 | wg.Add(totalBars) 38 | 39 | for i := 0; i < totalBars; i++ { 40 | name := fmt.Sprintf("Bar#%02d: ", i) 41 | total := rand.Intn(320) + 10 42 | bar := p.AddBar(int64(total), 43 | mpb.PrependDecorators( 44 | decor.Name(name, decor.WCSyncWidthR), 45 | decor.OnComplete(decor.Percentage(decor.WCSyncWidth), "done"), 46 | ), 47 | mpb.AppendDecorators( 48 | decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_GO, 30, decor.WCSyncWidth), ""), 49 | decor.EwmaSpeed(decor.SizeB1024(0), "", 30, decor.WCSyncSpace), 50 | ), 51 | ) 52 | 53 | go func() { 54 | defer wg.Done() 55 | rng := rand.New(rand.NewSource(time.Now().UnixNano())) 56 | max := 100 * time.Millisecond 57 | for bar.IsRunning() { 58 | start := time.Now() 59 | time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) 60 | bar.EwmaIncrement(time.Since(start)) 61 | } 62 | }() 63 | } 64 | // wait for passed wg and for all bars to complete and flush 65 | p.Wait() 66 | } 67 | -------------------------------------------------------------------------------- /_examples/suppressBar/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/suppressBar 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/suppressBar/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "math/rand" 8 | "sync" 9 | "time" 10 | 11 | "github.com/vbauerster/mpb/v8" 12 | "github.com/vbauerster/mpb/v8/decor" 13 | ) 14 | 15 | func main() { 16 | p := mpb.New() 17 | 18 | total, numBars := 100, 3 19 | err := new(errorWrapper) 20 | timer := time.AfterFunc(2*time.Second, func() { 21 | err.set(errors.New("timeout"), rand.Intn(numBars)) 22 | }) 23 | defer timer.Stop() 24 | 25 | for i := 0; i < numBars; i++ { 26 | msgCh := make(chan string, 1) 27 | bar := p.AddBar(int64(total), 28 | mpb.PrependDecorators(newTitleDecorator(fmt.Sprintf("Bar#%d:", i), msgCh, 16)), 29 | mpb.AppendDecorators(decor.Percentage(decor.WCSyncWidth)), 30 | ) 31 | // simulating some work 32 | barID := i 33 | go func() { 34 | max := 100 * time.Millisecond 35 | for i := 0; i < total; i++ { 36 | if err.check(barID) { 37 | msgCh <- fmt.Sprintf("%s at %d, retrying...", err.Error(), i) 38 | err.reset() 39 | i-- 40 | bar.SetRefill(int64(i)) 41 | continue 42 | } 43 | time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) 44 | bar.Increment() 45 | } 46 | }() 47 | } 48 | 49 | p.Wait() 50 | } 51 | 52 | type errorWrapper struct { 53 | sync.RWMutex 54 | err error 55 | barID int 56 | } 57 | 58 | func (ew *errorWrapper) Error() string { 59 | ew.RLock() 60 | defer ew.RUnlock() 61 | return ew.err.Error() 62 | } 63 | 64 | func (ew *errorWrapper) check(barID int) bool { 65 | ew.RLock() 66 | defer ew.RUnlock() 67 | return ew.err != nil && ew.barID == barID 68 | } 69 | 70 | func (ew *errorWrapper) set(err error, barID int) { 71 | ew.Lock() 72 | ew.err = err 73 | ew.barID = barID 74 | ew.Unlock() 75 | } 76 | 77 | func (ew *errorWrapper) reset() { 78 | ew.Lock() 79 | ew.err = nil 80 | ew.Unlock() 81 | } 82 | 83 | type title struct { 84 | decor.Decorator 85 | name string 86 | msgCh <-chan string 87 | msg string 88 | count int 89 | limit int 90 | } 91 | 92 | func (d *title) Decor(stat decor.Statistics) (string, int) { 93 | if d.count == 0 { 94 | select { 95 | case msg := <-d.msgCh: 96 | d.count = d.limit 97 | d.msg = msg 98 | default: 99 | return d.Decorator.Decor(stat) 100 | } 101 | } 102 | d.count-- 103 | _, _ = d.Format("") 104 | return fmt.Sprintf("%s %s", d.name, d.msg), math.MaxInt 105 | } 106 | 107 | func newTitleDecorator(name string, msgCh <-chan string, limit int) decor.Decorator { 108 | return &title{ 109 | Decorator: decor.Name(name), 110 | name: name, 111 | msgCh: msgCh, 112 | limit: limit, 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /_examples/tipOnComplete/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/_examples/tipOnComplete 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require github.com/vbauerster/mpb/v8 v8.10.1 8 | 9 | require ( 10 | github.com/VividCortex/ewma v1.2.0 // indirect 11 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.4.7 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_examples/tipOnComplete/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/vbauerster/mpb/v8" 8 | "github.com/vbauerster/mpb/v8/decor" 9 | ) 10 | 11 | func main() { 12 | // initialize progress container, with custom width 13 | p := mpb.New(mpb.WithWidth(80)) 14 | 15 | total := 100 16 | name := "Single Bar:" 17 | bar := p.New(int64(total), 18 | mpb.BarStyle().TipOnComplete(), 19 | mpb.PrependDecorators(decor.Name(name)), 20 | mpb.AppendDecorators(decor.Percentage()), 21 | ) 22 | // simulating some work 23 | max := 100 * time.Millisecond 24 | for i := 0; i < total; i++ { 25 | time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) 26 | bar.Increment() 27 | } 28 | // wait for our bar to complete and flush 29 | p.Wait() 30 | } 31 | -------------------------------------------------------------------------------- /bar_filler.go: -------------------------------------------------------------------------------- 1 | package mpb 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/vbauerster/mpb/v8/decor" 7 | ) 8 | 9 | // BarFiller interface. 10 | // Bar (without decorators) renders itself by calling BarFiller's Fill method. 11 | type BarFiller interface { 12 | Fill(io.Writer, decor.Statistics) error 13 | } 14 | 15 | // BarFillerBuilder interface. 16 | // Default implementations are: 17 | // 18 | // BarStyle() 19 | // SpinnerStyle() 20 | // NopStyle() 21 | type BarFillerBuilder interface { 22 | Build() BarFiller 23 | } 24 | 25 | // BarFillerFunc is function type adapter to convert compatible function 26 | // into BarFiller interface. 27 | type BarFillerFunc func(io.Writer, decor.Statistics) error 28 | 29 | func (f BarFillerFunc) Fill(w io.Writer, stat decor.Statistics) error { 30 | return f(w, stat) 31 | } 32 | -------------------------------------------------------------------------------- /bar_filler_bar.go: -------------------------------------------------------------------------------- 1 | package mpb 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/mattn/go-runewidth" 7 | "github.com/vbauerster/mpb/v8/decor" 8 | "github.com/vbauerster/mpb/v8/internal" 9 | ) 10 | 11 | const ( 12 | iLbound = iota 13 | iRefiller 14 | iFiller 15 | iTip 16 | iPadding 17 | iRbound 18 | iLen 19 | ) 20 | 21 | var defaultBarStyle = [iLen]string{"[", "+", "=", ">", "-", "]"} 22 | 23 | // BarStyleComposer interface. 24 | type BarStyleComposer interface { 25 | BarFillerBuilder 26 | Lbound(string) BarStyleComposer 27 | LboundMeta(func(string) string) BarStyleComposer 28 | Rbound(string) BarStyleComposer 29 | RboundMeta(func(string) string) BarStyleComposer 30 | Filler(string) BarStyleComposer 31 | FillerMeta(func(string) string) BarStyleComposer 32 | Refiller(string) BarStyleComposer 33 | RefillerMeta(func(string) string) BarStyleComposer 34 | Padding(string) BarStyleComposer 35 | PaddingMeta(func(string) string) BarStyleComposer 36 | Tip(frames ...string) BarStyleComposer 37 | TipMeta(func(string) string) BarStyleComposer 38 | TipOnComplete() BarStyleComposer 39 | Reverse() BarStyleComposer 40 | } 41 | 42 | type component struct { 43 | width int 44 | bytes []byte 45 | } 46 | 47 | type barSection struct { 48 | meta func(string) string 49 | bytes []byte 50 | } 51 | 52 | type barSections [iLen]barSection 53 | 54 | type barFiller struct { 55 | components [iLen]component 56 | metas [iLen]func(string) string 57 | flushOp func(barSections, io.Writer) error 58 | tip struct { 59 | onComplete bool 60 | count uint 61 | frames []component 62 | } 63 | } 64 | 65 | type barStyle struct { 66 | style [iLen]string 67 | metas [iLen]func(string) string 68 | tipFrames []string 69 | tipOnComplete bool 70 | rev bool 71 | } 72 | 73 | // BarStyle constructs default bar style which can be altered via 74 | // BarStyleComposer interface. 75 | func BarStyle() BarStyleComposer { 76 | bs := barStyle{ 77 | style: defaultBarStyle, 78 | tipFrames: []string{defaultBarStyle[iTip]}, 79 | } 80 | return bs 81 | } 82 | 83 | func (s barStyle) Lbound(bound string) BarStyleComposer { 84 | s.style[iLbound] = bound 85 | return s 86 | } 87 | 88 | func (s barStyle) LboundMeta(fn func(string) string) BarStyleComposer { 89 | s.metas[iLbound] = fn 90 | return s 91 | } 92 | 93 | func (s barStyle) Rbound(bound string) BarStyleComposer { 94 | s.style[iRbound] = bound 95 | return s 96 | } 97 | 98 | func (s barStyle) RboundMeta(fn func(string) string) BarStyleComposer { 99 | s.metas[iRbound] = fn 100 | return s 101 | } 102 | 103 | func (s barStyle) Filler(filler string) BarStyleComposer { 104 | s.style[iFiller] = filler 105 | return s 106 | } 107 | 108 | func (s barStyle) FillerMeta(fn func(string) string) BarStyleComposer { 109 | s.metas[iFiller] = fn 110 | return s 111 | } 112 | 113 | func (s barStyle) Refiller(refiller string) BarStyleComposer { 114 | s.style[iRefiller] = refiller 115 | return s 116 | } 117 | 118 | func (s barStyle) RefillerMeta(fn func(string) string) BarStyleComposer { 119 | s.metas[iRefiller] = fn 120 | return s 121 | } 122 | 123 | func (s barStyle) Padding(padding string) BarStyleComposer { 124 | s.style[iPadding] = padding 125 | return s 126 | } 127 | 128 | func (s barStyle) PaddingMeta(fn func(string) string) BarStyleComposer { 129 | s.metas[iPadding] = fn 130 | return s 131 | } 132 | 133 | func (s barStyle) Tip(frames ...string) BarStyleComposer { 134 | if len(frames) != 0 { 135 | s.tipFrames = frames 136 | } 137 | return s 138 | } 139 | 140 | func (s barStyle) TipMeta(fn func(string) string) BarStyleComposer { 141 | s.metas[iTip] = fn 142 | return s 143 | } 144 | 145 | func (s barStyle) TipOnComplete() BarStyleComposer { 146 | s.tipOnComplete = true 147 | return s 148 | } 149 | 150 | func (s barStyle) Reverse() BarStyleComposer { 151 | s.rev = true 152 | return s 153 | } 154 | 155 | func (s barStyle) Build() BarFiller { 156 | bf := &barFiller{metas: s.metas} 157 | bf.components[iLbound] = component{ 158 | width: runewidth.StringWidth(s.style[iLbound]), 159 | bytes: []byte(s.style[iLbound]), 160 | } 161 | bf.components[iRbound] = component{ 162 | width: runewidth.StringWidth(s.style[iRbound]), 163 | bytes: []byte(s.style[iRbound]), 164 | } 165 | bf.components[iFiller] = component{ 166 | width: runewidth.StringWidth(s.style[iFiller]), 167 | bytes: []byte(s.style[iFiller]), 168 | } 169 | bf.components[iRefiller] = component{ 170 | width: runewidth.StringWidth(s.style[iRefiller]), 171 | bytes: []byte(s.style[iRefiller]), 172 | } 173 | bf.components[iPadding] = component{ 174 | width: runewidth.StringWidth(s.style[iPadding]), 175 | bytes: []byte(s.style[iPadding]), 176 | } 177 | bf.tip.onComplete = s.tipOnComplete 178 | bf.tip.frames = make([]component, 0, len(s.tipFrames)) 179 | for _, t := range s.tipFrames { 180 | bf.tip.frames = append(bf.tip.frames, component{ 181 | width: runewidth.StringWidth(t), 182 | bytes: []byte(t), 183 | }) 184 | } 185 | if s.rev { 186 | bf.flushOp = barSections.flushRev 187 | } else { 188 | bf.flushOp = barSections.flush 189 | } 190 | return bf 191 | } 192 | 193 | func (s *barFiller) Fill(w io.Writer, stat decor.Statistics) error { 194 | width := internal.CheckRequestedWidth(stat.RequestedWidth, stat.AvailableWidth) 195 | // don't count brackets as progress 196 | width -= (s.components[iLbound].width + s.components[iRbound].width) 197 | if width < 0 { 198 | return nil 199 | } 200 | 201 | var tip component 202 | var refilling, filling, padding []byte 203 | var fillCount int 204 | curWidth := int(internal.PercentageRound(stat.Total, stat.Current, uint(width))) 205 | 206 | if curWidth != 0 { 207 | if !stat.Completed || s.tip.onComplete { 208 | tip = s.tip.frames[s.tip.count%uint(len(s.tip.frames))] 209 | s.tip.count++ 210 | fillCount += tip.width 211 | } 212 | switch refWidth := 0; { 213 | case stat.Refill != 0: 214 | refWidth = int(internal.PercentageRound(stat.Total, stat.Refill, uint(width))) 215 | curWidth -= refWidth 216 | refWidth += curWidth 217 | fallthrough 218 | default: 219 | for w := s.components[iFiller].width; curWidth-fillCount >= w; fillCount += w { 220 | filling = append(filling, s.components[iFiller].bytes...) 221 | } 222 | for w := s.components[iRefiller].width; refWidth-fillCount >= w; fillCount += w { 223 | refilling = append(refilling, s.components[iRefiller].bytes...) 224 | } 225 | } 226 | } 227 | 228 | for w := s.components[iPadding].width; width-fillCount >= w; fillCount += w { 229 | padding = append(padding, s.components[iPadding].bytes...) 230 | } 231 | 232 | for w := 1; width-fillCount >= w; fillCount += w { 233 | padding = append(padding, "…"...) 234 | } 235 | 236 | return s.flushOp(barSections{ 237 | {s.metas[iLbound], s.components[iLbound].bytes}, 238 | {s.metas[iRefiller], refilling}, 239 | {s.metas[iFiller], filling}, 240 | {s.metas[iTip], tip.bytes}, 241 | {s.metas[iPadding], padding}, 242 | {s.metas[iRbound], s.components[iRbound].bytes}, 243 | }, w) 244 | } 245 | 246 | func (s barSection) flush(w io.Writer) (err error) { 247 | if s.meta != nil { 248 | _, err = io.WriteString(w, s.meta(string(s.bytes))) 249 | } else { 250 | _, err = w.Write(s.bytes) 251 | } 252 | return err 253 | } 254 | 255 | func (bb barSections) flush(w io.Writer) error { 256 | for _, s := range bb { 257 | err := s.flush(w) 258 | if err != nil { 259 | return err 260 | } 261 | } 262 | return nil 263 | } 264 | 265 | func (bb barSections) flushRev(w io.Writer) error { 266 | bb[0], bb[len(bb)-1] = bb[len(bb)-1], bb[0] 267 | for i := len(bb) - 1; i >= 0; i-- { 268 | err := bb[i].flush(w) 269 | if err != nil { 270 | return err 271 | } 272 | } 273 | return nil 274 | } 275 | -------------------------------------------------------------------------------- /bar_filler_nop.go: -------------------------------------------------------------------------------- 1 | package mpb 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/vbauerster/mpb/v8/decor" 7 | ) 8 | 9 | // barFillerBuilderFunc is function type adapter to convert compatible 10 | // function into BarFillerBuilder interface. 11 | type barFillerBuilderFunc func() BarFiller 12 | 13 | func (f barFillerBuilderFunc) Build() BarFiller { 14 | return f() 15 | } 16 | 17 | // NopStyle provides BarFillerBuilder which builds NOP BarFiller. 18 | func NopStyle() BarFillerBuilder { 19 | return barFillerBuilderFunc(func() BarFiller { 20 | return BarFillerFunc(func(io.Writer, decor.Statistics) error { 21 | return nil 22 | }) 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /bar_filler_spinner.go: -------------------------------------------------------------------------------- 1 | package mpb 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | 7 | "github.com/mattn/go-runewidth" 8 | "github.com/vbauerster/mpb/v8/decor" 9 | "github.com/vbauerster/mpb/v8/internal" 10 | ) 11 | 12 | const ( 13 | positionLeft = 1 + iota 14 | positionRight 15 | ) 16 | 17 | var defaultSpinnerStyle = [...]string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} 18 | 19 | // SpinnerStyleComposer interface. 20 | type SpinnerStyleComposer interface { 21 | BarFillerBuilder 22 | PositionLeft() SpinnerStyleComposer 23 | PositionRight() SpinnerStyleComposer 24 | Meta(func(string) string) SpinnerStyleComposer 25 | } 26 | 27 | type spinnerFiller struct { 28 | frames []string 29 | count uint 30 | meta func(string) string 31 | position func(string, int) string 32 | } 33 | 34 | type spinnerStyle struct { 35 | position uint 36 | frames []string 37 | meta func(string) string 38 | } 39 | 40 | // SpinnerStyle constructs default spinner style which can be altered via 41 | // SpinnerStyleComposer interface. 42 | func SpinnerStyle(frames ...string) SpinnerStyleComposer { 43 | var ss spinnerStyle 44 | if len(frames) != 0 { 45 | ss.frames = frames 46 | } else { 47 | ss.frames = defaultSpinnerStyle[:] 48 | } 49 | return ss 50 | } 51 | 52 | func (s spinnerStyle) PositionLeft() SpinnerStyleComposer { 53 | s.position = positionLeft 54 | return s 55 | } 56 | 57 | func (s spinnerStyle) PositionRight() SpinnerStyleComposer { 58 | s.position = positionRight 59 | return s 60 | } 61 | 62 | func (s spinnerStyle) Meta(fn func(string) string) SpinnerStyleComposer { 63 | s.meta = fn 64 | return s 65 | } 66 | 67 | func (s spinnerStyle) Build() BarFiller { 68 | sf := &spinnerFiller{frames: s.frames} 69 | switch s.position { 70 | case positionLeft: 71 | sf.position = func(frame string, padWidth int) string { 72 | return frame + strings.Repeat(" ", padWidth) 73 | } 74 | case positionRight: 75 | sf.position = func(frame string, padWidth int) string { 76 | return strings.Repeat(" ", padWidth) + frame 77 | } 78 | default: 79 | sf.position = func(frame string, padWidth int) string { 80 | return strings.Repeat(" ", padWidth/2) + frame + strings.Repeat(" ", padWidth/2+padWidth%2) 81 | } 82 | } 83 | if s.meta != nil { 84 | sf.meta = s.meta 85 | } else { 86 | sf.meta = func(s string) string { return s } 87 | } 88 | return sf 89 | } 90 | 91 | func (s *spinnerFiller) Fill(w io.Writer, stat decor.Statistics) error { 92 | width := internal.CheckRequestedWidth(stat.RequestedWidth, stat.AvailableWidth) 93 | frame := s.frames[s.count%uint(len(s.frames))] 94 | frameWidth := runewidth.StringWidth(frame) 95 | s.count++ 96 | 97 | if width < frameWidth { 98 | return nil 99 | } 100 | 101 | _, err := io.WriteString(w, s.position(s.meta(frame), width-frameWidth)) 102 | return err 103 | } 104 | -------------------------------------------------------------------------------- /bar_option.go: -------------------------------------------------------------------------------- 1 | package mpb 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | 7 | "github.com/vbauerster/mpb/v8/decor" 8 | ) 9 | 10 | // BarOption is a func option to alter default behavior of a bar. 11 | type BarOption func(*bState) 12 | 13 | // PrependDecorators let you inject decorators to the bar's left side. 14 | func PrependDecorators(decorators ...decor.Decorator) BarOption { 15 | var group []decor.Decorator 16 | for _, decorator := range decorators { 17 | if decorator != nil { 18 | group = append(group, decorator) 19 | } 20 | } 21 | return func(s *bState) { 22 | s.decorGroups[0] = group 23 | } 24 | } 25 | 26 | // AppendDecorators let you inject decorators to the bar's right side. 27 | func AppendDecorators(decorators ...decor.Decorator) BarOption { 28 | var group []decor.Decorator 29 | for _, decorator := range decorators { 30 | if decorator != nil { 31 | group = append(group, decorator) 32 | } 33 | } 34 | return func(s *bState) { 35 | s.decorGroups[1] = group 36 | } 37 | } 38 | 39 | // BarID sets bar id. 40 | func BarID(id int) BarOption { 41 | return func(s *bState) { 42 | s.id = id 43 | } 44 | } 45 | 46 | // BarWidth sets bar width independent of the container. 47 | func BarWidth(width int) BarOption { 48 | return func(s *bState) { 49 | s.reqWidth = width 50 | } 51 | } 52 | 53 | // BarQueueAfter puts this (being constructed) bar into the queue. 54 | // BarPriority will be inherited from the argument bar. 55 | // When argument bar completes or aborts queued bar replaces its place. 56 | func BarQueueAfter(bar *Bar) BarOption { 57 | return func(s *bState) { 58 | s.waitBar = bar 59 | } 60 | } 61 | 62 | // BarRemoveOnComplete removes both bar's filler and its decorators 63 | // on complete event. 64 | func BarRemoveOnComplete() BarOption { 65 | return func(s *bState) { 66 | s.rmOnComplete = true 67 | } 68 | } 69 | 70 | // BarFillerClearOnComplete clears bar's filler on complete event. 71 | // It's shortcut for BarFillerOnComplete(""). 72 | func BarFillerClearOnComplete() BarOption { 73 | return BarFillerOnComplete("") 74 | } 75 | 76 | // BarFillerOnComplete replaces bar's filler with message, on complete event. 77 | func BarFillerOnComplete(message string) BarOption { 78 | return BarFillerMiddleware(func(base BarFiller) BarFiller { 79 | return BarFillerFunc(func(w io.Writer, st decor.Statistics) error { 80 | if st.Completed { 81 | _, err := io.WriteString(w, message) 82 | return err 83 | } 84 | return base.Fill(w, st) 85 | }) 86 | }) 87 | } 88 | 89 | // BarFillerClearOnAbort clears bar's filler on abort event. 90 | // It's shortcut for BarFillerOnAbort(""). 91 | func BarFillerClearOnAbort() BarOption { 92 | return BarFillerOnAbort("") 93 | } 94 | 95 | // BarFillerOnAbort replaces bar's filler with message, on abort event. 96 | func BarFillerOnAbort(message string) BarOption { 97 | return BarFillerMiddleware(func(base BarFiller) BarFiller { 98 | return BarFillerFunc(func(w io.Writer, st decor.Statistics) error { 99 | if st.Aborted { 100 | _, err := io.WriteString(w, message) 101 | return err 102 | } 103 | return base.Fill(w, st) 104 | }) 105 | }) 106 | } 107 | 108 | // BarFillerMiddleware provides a way to augment the underlying BarFiller. 109 | func BarFillerMiddleware(middle func(BarFiller) BarFiller) BarOption { 110 | if middle == nil { 111 | return nil 112 | } 113 | return func(s *bState) { 114 | s.filler = middle(s.filler) 115 | } 116 | } 117 | 118 | // BarPriority sets bar's priority. Zero is highest priority, i.e. bar 119 | // will be on top. This option isn't effective with `BarQueueAfter` option. 120 | func BarPriority(priority int) BarOption { 121 | return func(s *bState) { 122 | s.priority = priority 123 | } 124 | } 125 | 126 | // BarExtender extends bar with arbitrary lines. Provided BarFiller will be 127 | // called at each render/flush cycle. Any lines written to the underlying 128 | // io.Writer will extend the bar either in above (rev = true) or below 129 | // (rev = false) direction. 130 | func BarExtender(filler BarFiller, rev bool) BarOption { 131 | if filler == nil { 132 | return nil 133 | } 134 | if f, ok := filler.(BarFillerFunc); ok && f == nil { 135 | return nil 136 | } 137 | fn := makeExtenderFunc(filler, rev) 138 | return func(s *bState) { 139 | s.extender = fn 140 | } 141 | } 142 | 143 | func makeExtenderFunc(filler BarFiller, rev bool) extenderFunc { 144 | buf := new(bytes.Buffer) 145 | base := func(stat decor.Statistics, rows ...io.Reader) ([]io.Reader, error) { 146 | err := filler.Fill(buf, stat) 147 | if err != nil { 148 | buf.Reset() 149 | return rows, err 150 | } 151 | for { 152 | line, err := buf.ReadBytes('\n') 153 | if err != nil { 154 | buf.Reset() 155 | break 156 | } 157 | rows = append(rows, bytes.NewReader(line)) 158 | } 159 | return rows, err 160 | } 161 | if !rev { 162 | return base 163 | } 164 | return func(stat decor.Statistics, rows ...io.Reader) ([]io.Reader, error) { 165 | rows, err := base(stat, rows...) 166 | if err != nil { 167 | return rows, err 168 | } 169 | for left, right := 0, len(rows)-1; left < right; left, right = left+1, right-1 { 170 | rows[left], rows[right] = rows[right], rows[left] 171 | } 172 | return rows, err 173 | } 174 | } 175 | 176 | // BarFillerTrim removes leading and trailing space around the underlying BarFiller. 177 | func BarFillerTrim() BarOption { 178 | return func(s *bState) { 179 | s.trimSpace = true 180 | } 181 | } 182 | 183 | // BarNoPop disables bar pop out of container. Effective when 184 | // PopCompletedMode of container is enabled. 185 | func BarNoPop() BarOption { 186 | return func(s *bState) { 187 | s.noPop = true 188 | } 189 | } 190 | 191 | // BarOptional will return provided option only when cond is true. 192 | func BarOptional(option BarOption, cond bool) BarOption { 193 | if cond { 194 | return option 195 | } 196 | return nil 197 | } 198 | 199 | // BarOptOn will return provided option only when predicate evaluates to true. 200 | func BarOptOn(option BarOption, predicate func() bool) BarOption { 201 | if predicate() { 202 | return option 203 | } 204 | return nil 205 | } 206 | 207 | // BarFuncOptional will call option and return its value only when cond is true. 208 | func BarFuncOptional(option func() BarOption, cond bool) BarOption { 209 | if cond { 210 | return option() 211 | } 212 | return nil 213 | } 214 | 215 | // BarFuncOptOn will call option and return its value only when predicate evaluates to true. 216 | func BarFuncOptOn(option func() BarOption, predicate func() bool) BarOption { 217 | if predicate() { 218 | return option() 219 | } 220 | return nil 221 | } 222 | -------------------------------------------------------------------------------- /bar_test.go: -------------------------------------------------------------------------------- 1 | package mpb_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "strings" 9 | "testing" 10 | "time" 11 | "unicode/utf8" 12 | 13 | "github.com/vbauerster/mpb/v8" 14 | "github.com/vbauerster/mpb/v8/decor" 15 | ) 16 | 17 | func TestBarCompleted(t *testing.T) { 18 | p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) 19 | total := 80 20 | bar := p.AddBar(int64(total)) 21 | 22 | if bar.Completed() { 23 | t.Error("expected bar not to complete") 24 | } 25 | 26 | bar.IncrBy(total) 27 | 28 | if !bar.Completed() { 29 | t.Error("expected bar to complete") 30 | } 31 | 32 | p.Wait() 33 | } 34 | 35 | func TestBarAborted(t *testing.T) { 36 | p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) 37 | total := 80 38 | bar := p.AddBar(int64(total)) 39 | 40 | if bar.Aborted() { 41 | t.Error("expected bar not to be aborted") 42 | } 43 | 44 | bar.Abort(false) 45 | 46 | if !bar.Aborted() { 47 | t.Error("expected bar to be aborted") 48 | } 49 | 50 | p.Wait() 51 | } 52 | 53 | func TestBarSetTotal(t *testing.T) { 54 | p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) 55 | bar := p.AddBar(0) 56 | 57 | bar.SetTotal(0, false) 58 | if bar.Completed() { 59 | t.Error("expected bar not to complete") 60 | } 61 | 62 | bar.SetTotal(0, true) 63 | if !bar.Completed() { 64 | t.Error("expected bar to complete") 65 | } 66 | 67 | p.Wait() 68 | } 69 | 70 | func TestBarEnableTriggerCompleteZeroBar(t *testing.T) { 71 | p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) 72 | bar := p.AddBar(0) // never complete bar 73 | 74 | if bar.Completed() { 75 | t.Error("expected bar not to complete") 76 | } 77 | 78 | // Calling bar.SetTotal(0, true) has same effect 79 | // but this one is more concise and intuitive 80 | bar.EnableTriggerComplete() 81 | 82 | if !bar.Completed() { 83 | t.Error("expected bar to complete") 84 | } 85 | 86 | p.Wait() 87 | } 88 | 89 | func TestBarEnableTriggerCompleteAndIncrementBefore(t *testing.T) { 90 | p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) 91 | bar := p.AddBar(0) // never complete bar 92 | 93 | targetTotal := int64(80) 94 | 95 | for _, f := range []func(){ 96 | func() { bar.SetTotal(40, false) }, 97 | func() { bar.IncrBy(60) }, 98 | func() { bar.SetTotal(targetTotal, false) }, 99 | func() { bar.IncrBy(20) }, 100 | } { 101 | f() 102 | if bar.Completed() { 103 | t.Error("expected bar not to complete") 104 | } 105 | } 106 | 107 | bar.EnableTriggerComplete() 108 | 109 | if !bar.Completed() { 110 | t.Error("expected bar to complete") 111 | } 112 | 113 | if current := bar.Current(); current != targetTotal { 114 | t.Errorf("Expected current: %d, got: %d", targetTotal, current) 115 | } 116 | 117 | p.Wait() 118 | } 119 | 120 | func TestBarEnableTriggerCompleteAndIncrementAfter(t *testing.T) { 121 | p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) 122 | bar := p.AddBar(0) // never complete bar 123 | 124 | targetTotal := int64(80) 125 | 126 | for _, f := range []func(){ 127 | func() { bar.SetTotal(40, false) }, 128 | func() { bar.IncrBy(60) }, 129 | func() { bar.SetTotal(targetTotal, false) }, 130 | func() { bar.EnableTriggerComplete() }, // disables any next SetTotal 131 | func() { bar.SetTotal(100, true) }, // nop 132 | } { 133 | f() 134 | if bar.Completed() { 135 | t.Error("expected bar not to complete") 136 | } 137 | } 138 | 139 | bar.IncrBy(20) 140 | 141 | if !bar.Completed() { 142 | t.Error("expected bar to complete") 143 | } 144 | 145 | if current := bar.Current(); current != targetTotal { 146 | t.Errorf("Expected current: %d, got: %d", targetTotal, current) 147 | } 148 | 149 | p.Wait() 150 | } 151 | 152 | func TestBarID(t *testing.T) { 153 | p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) 154 | total := 100 155 | wantID := 11 156 | bar := p.AddBar(int64(total), mpb.BarID(wantID)) 157 | 158 | gotID := bar.ID() 159 | if gotID != wantID { 160 | t.Errorf("Expected bar id: %d, got %d", wantID, gotID) 161 | } 162 | 163 | bar.IncrBy(total) 164 | 165 | p.Wait() 166 | } 167 | 168 | func TestBarSetRefill(t *testing.T) { 169 | var buf bytes.Buffer 170 | p := mpb.New( 171 | mpb.WithWidth(100), 172 | mpb.WithOutput(&buf), 173 | mpb.WithAutoRefresh(), 174 | ) 175 | 176 | total := 100 177 | till := 30 178 | refiller := "+" 179 | 180 | bar := p.New(int64(total), mpb.BarStyle().Refiller(refiller), mpb.BarFillerTrim()) 181 | 182 | bar.IncrBy(till) 183 | bar.SetRefill(int64(till)) 184 | bar.IncrBy(total - till) 185 | 186 | p.Wait() 187 | 188 | wantBar := fmt.Sprintf("[%s%s]", 189 | strings.Repeat(refiller, till-1), 190 | strings.Repeat("=", total-till-1), 191 | ) 192 | 193 | got := string(bytes.Split(buf.Bytes(), []byte("\n"))[0]) 194 | 195 | if !strings.Contains(got, wantBar) { 196 | t.Errorf("Want bar: %q, got bar: %q", wantBar, got) 197 | } 198 | } 199 | 200 | func TestBarHas100PercentWithBarRemoveOnComplete(t *testing.T) { 201 | var buf bytes.Buffer 202 | p := mpb.New( 203 | mpb.WithWidth(80), 204 | mpb.WithOutput(&buf), 205 | mpb.WithAutoRefresh(), 206 | ) 207 | 208 | total := 50 209 | 210 | bar := p.AddBar(int64(total), 211 | mpb.BarRemoveOnComplete(), 212 | mpb.AppendDecorators(decor.Percentage()), 213 | ) 214 | 215 | bar.IncrBy(total) 216 | 217 | p.Wait() 218 | 219 | hundred := "100 %" 220 | if !bytes.Contains(buf.Bytes(), []byte(hundred)) { 221 | t.Errorf("Bar's buffer does not contain: %q", hundred) 222 | } 223 | } 224 | 225 | func TestBarStyle(t *testing.T) { 226 | var buf bytes.Buffer 227 | customFormat := "╢▌▌░╟" 228 | runes := []rune(customFormat) 229 | total := 80 230 | p := mpb.New( 231 | mpb.WithWidth(80), 232 | mpb.WithOutput(&buf), 233 | mpb.WithAutoRefresh(), 234 | ) 235 | bs := mpb.BarStyle() 236 | bs = bs.Lbound(string(runes[0])) 237 | bs = bs.Filler(string(runes[1])) 238 | bs = bs.Tip(string(runes[2])) 239 | bs = bs.Padding(string(runes[3])) 240 | bs = bs.Rbound(string(runes[4])) 241 | bar := p.New(int64(total), bs, mpb.BarFillerTrim()) 242 | 243 | bar.IncrBy(total) 244 | 245 | p.Wait() 246 | 247 | wantBar := fmt.Sprintf("%s%s%s%s", 248 | string(runes[0]), 249 | strings.Repeat(string(runes[1]), total-3), 250 | string(runes[2]), 251 | string(runes[4]), 252 | ) 253 | got := string(bytes.Split(buf.Bytes(), []byte("\n"))[0]) 254 | 255 | if !strings.Contains(got, wantBar) { 256 | t.Errorf("Want bar: %q:%d, got bar: %q:%d", wantBar, utf8.RuneCountInString(wantBar), got, utf8.RuneCountInString(got)) 257 | } 258 | } 259 | 260 | func TestDecorStatisticsAvailableWidth(t *testing.T) { 261 | ch := make(chan int, 2) 262 | td1 := func(s decor.Statistics) string { 263 | ch <- s.AvailableWidth 264 | return strings.Repeat("0", 20) 265 | } 266 | td2 := func(s decor.Statistics) string { 267 | ch <- s.AvailableWidth 268 | return "" 269 | } 270 | ctx, cancel := context.WithCancel(context.Background()) 271 | refresh := make(chan interface{}) 272 | p := mpb.NewWithContext(ctx, 273 | mpb.WithWidth(100), 274 | mpb.WithManualRefresh(refresh), 275 | mpb.WithOutput(io.Discard), 276 | ) 277 | _ = p.AddBar(0, 278 | mpb.BarFillerTrim(), 279 | mpb.PrependDecorators( 280 | decor.Name(strings.Repeat("0", 20)), 281 | decor.Meta( 282 | decor.Any(td1), 283 | func(s string) string { 284 | return "\x1b[31;1m" + s + "\x1b[0m" 285 | }, 286 | ), 287 | ), 288 | mpb.AppendDecorators( 289 | decor.Name(strings.Repeat("0", 20)), 290 | decor.Any(td2), 291 | ), 292 | ) 293 | refresh <- time.Now() 294 | go func() { 295 | time.Sleep(10 * time.Millisecond) 296 | cancel() 297 | }() 298 | p.Wait() 299 | 300 | if availableWidth := <-ch; availableWidth != 80 { 301 | t.Errorf("expected AvailableWidth %d got %d", 80, availableWidth) 302 | } 303 | 304 | if availableWidth := <-ch; availableWidth != 40 { 305 | t.Errorf("expected AvailableWidth %d got %d", 40, availableWidth) 306 | } 307 | } 308 | 309 | func TestBarQueueAfterBar(t *testing.T) { 310 | shutdown := make(chan interface{}) 311 | ctx, cancel := context.WithCancel(context.Background()) 312 | p := mpb.NewWithContext(ctx, 313 | mpb.WithOutput(io.Discard), 314 | mpb.WithAutoRefresh(), 315 | mpb.WithShutdownNotifier(shutdown), 316 | ) 317 | a := p.AddBar(100) 318 | b := p.AddBar(100, mpb.BarQueueAfter(a)) 319 | identity := map[*mpb.Bar]string{ 320 | a: "a", 321 | b: "b", 322 | } 323 | 324 | a.IncrBy(100) 325 | a.Wait() 326 | cancel() 327 | 328 | bars := (<-shutdown).([]*mpb.Bar) 329 | if l := len(bars); l != 1 { 330 | t.Errorf("Expected len of bars: %d, got: %d", 1, l) 331 | } 332 | 333 | p.Wait() 334 | if bars[0] != b { 335 | t.Errorf("Expected bars[0] == b, got: %s", identity[bars[0]]) 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /barbench_test.go: -------------------------------------------------------------------------------- 1 | package mpb_test 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | 7 | "github.com/vbauerster/mpb/v8" 8 | ) 9 | 10 | const total = 1000 11 | 12 | func BenchmarkNopStyleB1(b *testing.B) { 13 | bench(b, mpb.NopStyle(), false, 1) 14 | } 15 | 16 | func BenchmarkNopStyleWithAutoRefreshB1(b *testing.B) { 17 | bench(b, mpb.NopStyle(), true, 1) 18 | } 19 | 20 | func BenchmarkNopStylesB2(b *testing.B) { 21 | bench(b, mpb.NopStyle(), false, 2) 22 | } 23 | 24 | func BenchmarkNopStylesWithAutoRefreshB2(b *testing.B) { 25 | bench(b, mpb.NopStyle(), true, 2) 26 | } 27 | 28 | func BenchmarkNopStylesB3(b *testing.B) { 29 | bench(b, mpb.NopStyle(), false, 3) 30 | } 31 | 32 | func BenchmarkNopStylesWithAutoRefreshB3(b *testing.B) { 33 | bench(b, mpb.NopStyle(), true, 3) 34 | } 35 | 36 | func BenchmarkBarStyleB1(b *testing.B) { 37 | bench(b, mpb.BarStyle(), false, 1) 38 | } 39 | 40 | func BenchmarkBarStyleWithAutoRefreshB1(b *testing.B) { 41 | bench(b, mpb.BarStyle(), true, 1) 42 | } 43 | 44 | func BenchmarkBarStylesB2(b *testing.B) { 45 | bench(b, mpb.BarStyle(), false, 2) 46 | } 47 | 48 | func BenchmarkBarStylesWithAutoRefreshB2(b *testing.B) { 49 | bench(b, mpb.BarStyle(), true, 2) 50 | } 51 | 52 | func BenchmarkBarStylesB3(b *testing.B) { 53 | bench(b, mpb.BarStyle(), false, 3) 54 | } 55 | 56 | func BenchmarkBarStylesWithAutoRefreshB3(b *testing.B) { 57 | bench(b, mpb.BarStyle(), true, 3) 58 | } 59 | 60 | func bench(b *testing.B, builder mpb.BarFillerBuilder, autoRefresh bool, n int) { 61 | p := mpb.New( 62 | mpb.WithWidth(100), 63 | mpb.WithOutput(io.Discard), 64 | mpb.ContainerOptional(mpb.WithAutoRefresh(), autoRefresh), 65 | ) 66 | defer p.Wait() 67 | b.ResetTimer() 68 | for i := 0; i < b.N; i++ { 69 | var bars []*mpb.Bar 70 | for j := 0; j < n; j++ { 71 | bars = append(bars, p.New(total, builder)) 72 | switch j { 73 | case n - 1: 74 | complete(bars[j]) 75 | default: 76 | go complete(bars[j]) 77 | } 78 | } 79 | for _, bar := range bars { 80 | bar.Wait() 81 | } 82 | } 83 | } 84 | 85 | func complete(bar *mpb.Bar) { 86 | for i := 0; i < total; i++ { 87 | bar.Increment() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /container_option.go: -------------------------------------------------------------------------------- 1 | package mpb 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // ContainerOption is a func option to alter default behavior of a bar 10 | // container. Container term refers to a Progress struct which can 11 | // hold one or more Bars. 12 | type ContainerOption func(*pState) 13 | 14 | // WithWaitGroup provides means to have a single joint point. If 15 | // *sync.WaitGroup is provided, you can safely call just p.Wait() 16 | // without calling Wait() on provided *sync.WaitGroup. Makes sense 17 | // when there are more than one bar to render. 18 | func WithWaitGroup(wg *sync.WaitGroup) ContainerOption { 19 | return func(s *pState) { 20 | s.uwg = wg 21 | } 22 | } 23 | 24 | // WithWidth sets container width. If not set it defaults to terminal 25 | // width. A bar added to the container will inherit its width, unless 26 | // overridden by `func BarWidth(int) BarOption`. 27 | func WithWidth(width int) ContainerOption { 28 | return func(s *pState) { 29 | s.reqWidth = width 30 | } 31 | } 32 | 33 | // WithQueueLen sets buffer size of heap manager channel. Ideally it must be 34 | // kept at MAX value, where MAX is number of bars to be rendered at the same 35 | // time. If len < MAX then backpressure to the scheduler will be increased as 36 | // MAX-len extra goroutines will be launched at each render cycle. 37 | // Default queue len is 128. 38 | func WithQueueLen(len int) ContainerOption { 39 | return func(s *pState) { 40 | s.hmQueueLen = len 41 | } 42 | } 43 | 44 | // WithRefreshRate overrides default 150ms refresh rate. 45 | func WithRefreshRate(d time.Duration) ContainerOption { 46 | return func(s *pState) { 47 | s.refreshRate = d 48 | } 49 | } 50 | 51 | // WithManualRefresh disables internal auto refresh time.Ticker. 52 | // Refresh will occur upon receive value from provided ch. 53 | func WithManualRefresh(ch <-chan interface{}) ContainerOption { 54 | return func(s *pState) { 55 | s.manualRC = ch 56 | } 57 | } 58 | 59 | // WithRenderDelay delays rendering. By default rendering starts as 60 | // soon as bar is added, with this option it's possible to delay 61 | // rendering process by keeping provided chan unclosed. In other words 62 | // rendering will start as soon as provided chan is closed. 63 | func WithRenderDelay(ch <-chan struct{}) ContainerOption { 64 | return func(s *pState) { 65 | s.delayRC = ch 66 | } 67 | } 68 | 69 | // WithShutdownNotifier value of type `[]*mpb.Bar` will be send into provided 70 | // channel upon container shutdown. 71 | func WithShutdownNotifier(ch chan<- interface{}) ContainerOption { 72 | return func(s *pState) { 73 | s.shutdownNotifier = ch 74 | } 75 | } 76 | 77 | // WithOutput overrides default os.Stdout output. If underlying io.Writer 78 | // is not a terminal then auto refresh is disabled unless WithAutoRefresh 79 | // option is set. 80 | func WithOutput(w io.Writer) ContainerOption { 81 | if w == nil { 82 | w = io.Discard 83 | } 84 | return func(s *pState) { 85 | s.output = w 86 | } 87 | } 88 | 89 | // WithDebugOutput sets debug output. 90 | func WithDebugOutput(w io.Writer) ContainerOption { 91 | if w == nil { 92 | w = io.Discard 93 | } 94 | return func(s *pState) { 95 | s.debugOut = w 96 | } 97 | } 98 | 99 | // WithAutoRefresh force auto refresh regardless of what output is set to. 100 | // Applicable only if not WithManualRefresh set. 101 | func WithAutoRefresh() ContainerOption { 102 | return func(s *pState) { 103 | s.autoRefresh = true 104 | } 105 | } 106 | 107 | // PopCompletedMode pop completed bars out of progress container. 108 | // In this mode completed bars get moved to the top and stop 109 | // participating in rendering cycle. 110 | func PopCompletedMode() ContainerOption { 111 | return func(s *pState) { 112 | s.popCompleted = true 113 | } 114 | } 115 | 116 | // ContainerOptional will return provided option only when cond is true. 117 | func ContainerOptional(option ContainerOption, cond bool) ContainerOption { 118 | if cond { 119 | return option 120 | } 121 | return nil 122 | } 123 | 124 | // ContainerOptOn will return provided option only when predicate evaluates to true. 125 | func ContainerOptOn(option ContainerOption, predicate func() bool) ContainerOption { 126 | if predicate() { 127 | return option 128 | } 129 | return nil 130 | } 131 | 132 | // ContainerFuncOptional will call option and return its value only when cond is true. 133 | func ContainerFuncOptional(option func() ContainerOption, cond bool) ContainerOption { 134 | if cond { 135 | return option() 136 | } 137 | return nil 138 | } 139 | 140 | // ContainerFuncOptOn will call option and return its value only when predicate evaluates to true. 141 | func ContainerFuncOptOn(option func() ContainerOption, predicate func() bool) ContainerOption { 142 | if predicate() { 143 | return option() 144 | } 145 | return nil 146 | } 147 | -------------------------------------------------------------------------------- /cwriter/cuuAndEd_construction_bench_test.go: -------------------------------------------------------------------------------- 1 | package cwriter 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | "testing" 9 | ) 10 | 11 | const lines = 99 12 | 13 | var out = io.Discard 14 | 15 | func BenchmarkWithFprintf(b *testing.B) { 16 | verb := fmt.Sprintf("%s%%d%s", escOpen, cuuAndEd) 17 | b.ResetTimer() 18 | for i := 0; i < b.N; i++ { 19 | _, _ = fmt.Fprintf(out, verb, lines) 20 | } 21 | } 22 | 23 | func BenchmarkWithJoin(b *testing.B) { 24 | bCuuAndEd := [][]byte{[]byte(escOpen), []byte(cuuAndEd)} 25 | for i := 0; i < b.N; i++ { 26 | _, _ = out.Write(bytes.Join(bCuuAndEd, []byte(strconv.Itoa(lines)))) 27 | } 28 | } 29 | 30 | func BenchmarkWithAppend(b *testing.B) { 31 | escOpen := []byte(escOpen) 32 | cuuAndEd := []byte(cuuAndEd) 33 | for i := 0; i < b.N; i++ { 34 | _, _ = out.Write(append(strconv.AppendInt(escOpen, int64(lines), 10), cuuAndEd...)) 35 | } 36 | } 37 | 38 | func BenchmarkWithCurrentImpl(b *testing.B) { 39 | w := New(out) 40 | b.ResetTimer() 41 | for i := 0; i < b.N; i++ { 42 | _ = w.ew.ansiCuuAndEd(out, lines) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /cwriter/doc.go: -------------------------------------------------------------------------------- 1 | // Package cwriter is a console writer abstraction for the underlying OS. 2 | package cwriter 3 | -------------------------------------------------------------------------------- /cwriter/util_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || netbsd || openbsd 2 | 3 | package cwriter 4 | 5 | import "golang.org/x/sys/unix" 6 | 7 | const ioctlReadTermios = unix.TIOCGETA 8 | -------------------------------------------------------------------------------- /cwriter/util_linux.go: -------------------------------------------------------------------------------- 1 | //go:build aix || linux 2 | 3 | package cwriter 4 | 5 | import "golang.org/x/sys/unix" 6 | 7 | const ioctlReadTermios = unix.TCGETS 8 | -------------------------------------------------------------------------------- /cwriter/util_solaris.go: -------------------------------------------------------------------------------- 1 | //go:build solaris 2 | 3 | package cwriter 4 | 5 | import "golang.org/x/sys/unix" 6 | 7 | const ioctlReadTermios = unix.TCGETA 8 | -------------------------------------------------------------------------------- /cwriter/util_zos.go: -------------------------------------------------------------------------------- 1 | //go:build zos 2 | 3 | package cwriter 4 | 5 | import "golang.org/x/sys/unix" 6 | 7 | const ioctlReadTermios = unix.TCGETS 8 | -------------------------------------------------------------------------------- /cwriter/writer.go: -------------------------------------------------------------------------------- 1 | package cwriter 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "os" 8 | "strconv" 9 | ) 10 | 11 | // https://github.com/dylanaraps/pure-sh-bible#cursor-movement 12 | const ( 13 | escOpen = "\x1b[" 14 | cuuAndEd = "A\x1b[J" 15 | ) 16 | 17 | // ErrNotTTY not a TeleTYpewriter error. 18 | var ErrNotTTY = errors.New("not a terminal") 19 | 20 | // New returns a new Writer with defaults. 21 | func New(out io.Writer) *Writer { 22 | w := &Writer{ 23 | Buffer: new(bytes.Buffer), 24 | out: out, 25 | termSize: func(_ int) (int, int, error) { 26 | return -1, -1, ErrNotTTY 27 | }, 28 | } 29 | if f, ok := out.(*os.File); ok { 30 | w.fd = int(f.Fd()) 31 | if IsTerminal(w.fd) { 32 | w.terminal = true 33 | w.termSize = func(fd int) (int, int, error) { 34 | return GetSize(fd) 35 | } 36 | } 37 | } 38 | bb := make([]byte, 16) 39 | w.ew = escWriter(bb[:copy(bb, []byte(escOpen))]) 40 | return w 41 | } 42 | 43 | // IsTerminal tells whether underlying io.Writer is terminal. 44 | func (w *Writer) IsTerminal() bool { 45 | return w.terminal 46 | } 47 | 48 | // GetTermSize returns WxH of underlying terminal. 49 | func (w *Writer) GetTermSize() (width, height int, err error) { 50 | return w.termSize(w.fd) 51 | } 52 | 53 | type escWriter []byte 54 | 55 | func (b escWriter) ansiCuuAndEd(out io.Writer, n int) error { 56 | b = strconv.AppendInt(b, int64(n), 10) 57 | _, err := out.Write(append(b, []byte(cuuAndEd)...)) 58 | return err 59 | } 60 | -------------------------------------------------------------------------------- /cwriter/writer_posix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package cwriter 4 | 5 | import ( 6 | "bytes" 7 | "io" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | // Writer is a buffered terminal writer, which moves cursor N lines up 13 | // on each flush except the first one, where N is a number of lines of 14 | // a previous flush. 15 | type Writer struct { 16 | *bytes.Buffer 17 | out io.Writer 18 | ew escWriter 19 | fd int 20 | terminal bool 21 | termSize func(int) (int, int, error) 22 | } 23 | 24 | // Flush flushes the underlying buffer. 25 | // It's caller's responsibility to pass correct number of lines. 26 | func (w *Writer) Flush(lines int) error { 27 | _, err := w.WriteTo(w.out) 28 | // some terminals interpret 'cursor up 0' as 'cursor up 1' 29 | if err == nil && lines > 0 { 30 | err = w.ew.ansiCuuAndEd(w, lines) 31 | } 32 | return err 33 | } 34 | 35 | // GetSize returns the dimensions of the given terminal. 36 | func GetSize(fd int) (width, height int, err error) { 37 | ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) 38 | if err != nil { 39 | return -1, -1, err 40 | } 41 | return int(ws.Col), int(ws.Row), nil 42 | } 43 | 44 | // IsTerminal returns whether the given file descriptor is a terminal. 45 | func IsTerminal(fd int) bool { 46 | _, err := unix.IoctlGetTermios(fd, ioctlReadTermios) 47 | return err == nil 48 | } 49 | -------------------------------------------------------------------------------- /cwriter/writer_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package cwriter 4 | 5 | import ( 6 | "bytes" 7 | "io" 8 | "unsafe" 9 | 10 | "golang.org/x/sys/windows" 11 | ) 12 | 13 | var kernel32 = windows.NewLazySystemDLL("kernel32.dll") 14 | 15 | var ( 16 | procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") 17 | procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") 18 | ) 19 | 20 | // Writer is a buffered terminal writer, which moves cursor N lines up 21 | // on each flush except the first one, where N is a number of lines of 22 | // a previous flush. 23 | type Writer struct { 24 | *bytes.Buffer 25 | out io.Writer 26 | ew escWriter 27 | lines int 28 | fd int 29 | terminal bool 30 | termSize func(int) (int, int, error) 31 | } 32 | 33 | // Flush flushes the underlying buffer. 34 | // It's caller's responsibility to pass correct number of lines. 35 | func (w *Writer) Flush(lines int) error { 36 | if w.lines > 0 { 37 | err := w.clearLines(w.lines) 38 | if err != nil { 39 | return err 40 | } 41 | } 42 | w.lines = lines 43 | _, err := w.WriteTo(w.out) 44 | return err 45 | } 46 | 47 | func (w *Writer) clearLines(n int) error { 48 | if !w.terminal { 49 | // hope it's cygwin or similar 50 | return w.ew.ansiCuuAndEd(w.out, n) 51 | } 52 | 53 | var info windows.ConsoleScreenBufferInfo 54 | if err := windows.GetConsoleScreenBufferInfo(windows.Handle(w.fd), &info); err != nil { 55 | return err 56 | } 57 | 58 | info.CursorPosition.Y -= int16(n) 59 | if info.CursorPosition.Y < 0 { 60 | info.CursorPosition.Y = 0 61 | } 62 | _, _, _ = procSetConsoleCursorPosition.Call( 63 | uintptr(w.fd), 64 | uintptr(uint32(uint16(info.CursorPosition.Y))<<16|uint32(uint16(info.CursorPosition.X))), 65 | ) 66 | 67 | // clear the lines 68 | cursor := &windows.Coord{ 69 | X: info.Window.Left, 70 | Y: info.CursorPosition.Y, 71 | } 72 | count := uint32(info.Size.X) * uint32(n) 73 | _, _, _ = procFillConsoleOutputCharacter.Call( 74 | uintptr(w.fd), 75 | uintptr(' '), 76 | uintptr(count), 77 | *(*uintptr)(unsafe.Pointer(cursor)), 78 | uintptr(unsafe.Pointer(new(uint32))), 79 | ) 80 | return nil 81 | } 82 | 83 | // GetSize returns the visible dimensions of the given terminal. 84 | // These dimensions don't include any scrollback buffer height. 85 | func GetSize(fd int) (width, height int, err error) { 86 | var info windows.ConsoleScreenBufferInfo 87 | if err := windows.GetConsoleScreenBufferInfo(windows.Handle(fd), &info); err != nil { 88 | return 0, 0, err 89 | } 90 | // terminal.GetSize from crypto/ssh adds "+ 1" to both width and height: 91 | // https://go.googlesource.com/crypto/+/refs/heads/release-branch.go1.14/ssh/terminal/util_windows.go#75 92 | // but looks like this is a root cause of issue #66, so removing both "+ 1" have fixed it. 93 | return int(info.Window.Right - info.Window.Left), int(info.Window.Bottom - info.Window.Top), nil 94 | } 95 | 96 | // IsTerminal returns whether the given file descriptor is a terminal. 97 | func IsTerminal(fd int) bool { 98 | var st uint32 99 | err := windows.GetConsoleMode(windows.Handle(fd), &st) 100 | return err == nil 101 | } 102 | -------------------------------------------------------------------------------- /decor/any.go: -------------------------------------------------------------------------------- 1 | package decor 2 | 3 | var _ Decorator = any{} 4 | 5 | // Any decorator. 6 | // Converts DecorFunc into Decorator. 7 | // 8 | // `fn` DecorFunc callback 9 | // `wcc` optional WC config 10 | func Any(fn DecorFunc, wcc ...WC) Decorator { 11 | return any{initWC(wcc...), fn} 12 | } 13 | 14 | type any struct { 15 | WC 16 | fn DecorFunc 17 | } 18 | 19 | func (d any) Decor(s Statistics) (string, int) { 20 | return d.Format(d.fn(s)) 21 | } 22 | -------------------------------------------------------------------------------- /decor/counters.go: -------------------------------------------------------------------------------- 1 | package decor 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // CountersNoUnit is a wrapper around Counters with no unit param. 8 | func CountersNoUnit(pairFmt string, wcc ...WC) Decorator { 9 | return Counters(0, pairFmt, wcc...) 10 | } 11 | 12 | // CountersKibiByte is a wrapper around Counters with predefined unit 13 | // as SizeB1024(0). 14 | func CountersKibiByte(pairFmt string, wcc ...WC) Decorator { 15 | return Counters(SizeB1024(0), pairFmt, wcc...) 16 | } 17 | 18 | // CountersKiloByte is a wrapper around Counters with predefined unit 19 | // as SizeB1000(0). 20 | func CountersKiloByte(pairFmt string, wcc ...WC) Decorator { 21 | return Counters(SizeB1000(0), pairFmt, wcc...) 22 | } 23 | 24 | // Counters decorator with dynamic unit measure adjustment. 25 | // 26 | // `unit` one of [0|SizeB1024(0)|SizeB1000(0)] 27 | // 28 | // `pairFmt` printf compatible verbs for current and total 29 | // 30 | // `wcc` optional WC config 31 | // 32 | // pairFmt example if unit=SizeB1000(0): 33 | // 34 | // pairFmt="%d / %d" output: "1MB / 12MB" 35 | // pairFmt="% d / % d" output: "1 MB / 12 MB" 36 | // pairFmt="%.1f / %.1f" output: "1.0MB / 12.0MB" 37 | // pairFmt="% .1f / % .1f" output: "1.0 MB / 12.0 MB" 38 | // pairFmt="%f / %f" output: "1.000000MB / 12.000000MB" 39 | // pairFmt="% f / % f" output: "1.000000 MB / 12.000000 MB" 40 | func Counters(unit interface{}, pairFmt string, wcc ...WC) Decorator { 41 | producer := func() DecorFunc { 42 | switch unit.(type) { 43 | case SizeB1024: 44 | if pairFmt == "" { 45 | pairFmt = "% d / % d" 46 | } 47 | return func(s Statistics) string { 48 | return fmt.Sprintf(pairFmt, SizeB1024(s.Current), SizeB1024(s.Total)) 49 | } 50 | case SizeB1000: 51 | if pairFmt == "" { 52 | pairFmt = "% d / % d" 53 | } 54 | return func(s Statistics) string { 55 | return fmt.Sprintf(pairFmt, SizeB1000(s.Current), SizeB1000(s.Total)) 56 | } 57 | default: 58 | if pairFmt == "" { 59 | pairFmt = "%d / %d" 60 | } 61 | return func(s Statistics) string { 62 | return fmt.Sprintf(pairFmt, s.Current, s.Total) 63 | } 64 | } 65 | } 66 | return Any(producer(), wcc...) 67 | } 68 | 69 | // TotalNoUnit is a wrapper around Total with no unit param. 70 | func TotalNoUnit(format string, wcc ...WC) Decorator { 71 | return Total(0, format, wcc...) 72 | } 73 | 74 | // TotalKibiByte is a wrapper around Total with predefined unit 75 | // as SizeB1024(0). 76 | func TotalKibiByte(format string, wcc ...WC) Decorator { 77 | return Total(SizeB1024(0), format, wcc...) 78 | } 79 | 80 | // TotalKiloByte is a wrapper around Total with predefined unit 81 | // as SizeB1000(0). 82 | func TotalKiloByte(format string, wcc ...WC) Decorator { 83 | return Total(SizeB1000(0), format, wcc...) 84 | } 85 | 86 | // Total decorator with dynamic unit measure adjustment. 87 | // 88 | // `unit` one of [0|SizeB1024(0)|SizeB1000(0)] 89 | // 90 | // `format` printf compatible verb for Total 91 | // 92 | // `wcc` optional WC config 93 | // 94 | // format example if unit=SizeB1024(0): 95 | // 96 | // format="%d" output: "12MiB" 97 | // format="% d" output: "12 MiB" 98 | // format="%.1f" output: "12.0MiB" 99 | // format="% .1f" output: "12.0 MiB" 100 | // format="%f" output: "12.000000MiB" 101 | // format="% f" output: "12.000000 MiB" 102 | func Total(unit interface{}, format string, wcc ...WC) Decorator { 103 | producer := func() DecorFunc { 104 | switch unit.(type) { 105 | case SizeB1024: 106 | if format == "" { 107 | format = "% d" 108 | } 109 | return func(s Statistics) string { 110 | return fmt.Sprintf(format, SizeB1024(s.Total)) 111 | } 112 | case SizeB1000: 113 | if format == "" { 114 | format = "% d" 115 | } 116 | return func(s Statistics) string { 117 | return fmt.Sprintf(format, SizeB1000(s.Total)) 118 | } 119 | default: 120 | if format == "" { 121 | format = "%d" 122 | } 123 | return func(s Statistics) string { 124 | return fmt.Sprintf(format, s.Total) 125 | } 126 | } 127 | } 128 | return Any(producer(), wcc...) 129 | } 130 | 131 | // CurrentNoUnit is a wrapper around Current with no unit param. 132 | func CurrentNoUnit(format string, wcc ...WC) Decorator { 133 | return Current(0, format, wcc...) 134 | } 135 | 136 | // CurrentKibiByte is a wrapper around Current with predefined unit 137 | // as SizeB1024(0). 138 | func CurrentKibiByte(format string, wcc ...WC) Decorator { 139 | return Current(SizeB1024(0), format, wcc...) 140 | } 141 | 142 | // CurrentKiloByte is a wrapper around Current with predefined unit 143 | // as SizeB1000(0). 144 | func CurrentKiloByte(format string, wcc ...WC) Decorator { 145 | return Current(SizeB1000(0), format, wcc...) 146 | } 147 | 148 | // Current decorator with dynamic unit measure adjustment. 149 | // 150 | // `unit` one of [0|SizeB1024(0)|SizeB1000(0)] 151 | // 152 | // `format` printf compatible verb for Current 153 | // 154 | // `wcc` optional WC config 155 | // 156 | // format example if unit=SizeB1024(0): 157 | // 158 | // format="%d" output: "12MiB" 159 | // format="% d" output: "12 MiB" 160 | // format="%.1f" output: "12.0MiB" 161 | // format="% .1f" output: "12.0 MiB" 162 | // format="%f" output: "12.000000MiB" 163 | // format="% f" output: "12.000000 MiB" 164 | func Current(unit interface{}, format string, wcc ...WC) Decorator { 165 | producer := func() DecorFunc { 166 | switch unit.(type) { 167 | case SizeB1024: 168 | if format == "" { 169 | format = "% d" 170 | } 171 | return func(s Statistics) string { 172 | return fmt.Sprintf(format, SizeB1024(s.Current)) 173 | } 174 | case SizeB1000: 175 | if format == "" { 176 | format = "% d" 177 | } 178 | return func(s Statistics) string { 179 | return fmt.Sprintf(format, SizeB1000(s.Current)) 180 | } 181 | default: 182 | if format == "" { 183 | format = "%d" 184 | } 185 | return func(s Statistics) string { 186 | return fmt.Sprintf(format, s.Current) 187 | } 188 | } 189 | } 190 | return Any(producer(), wcc...) 191 | } 192 | 193 | // InvertedCurrentNoUnit is a wrapper around InvertedCurrent with no unit param. 194 | func InvertedCurrentNoUnit(format string, wcc ...WC) Decorator { 195 | return InvertedCurrent(0, format, wcc...) 196 | } 197 | 198 | // InvertedCurrentKibiByte is a wrapper around InvertedCurrent with predefined unit 199 | // as SizeB1024(0). 200 | func InvertedCurrentKibiByte(format string, wcc ...WC) Decorator { 201 | return InvertedCurrent(SizeB1024(0), format, wcc...) 202 | } 203 | 204 | // InvertedCurrentKiloByte is a wrapper around InvertedCurrent with predefined unit 205 | // as SizeB1000(0). 206 | func InvertedCurrentKiloByte(format string, wcc ...WC) Decorator { 207 | return InvertedCurrent(SizeB1000(0), format, wcc...) 208 | } 209 | 210 | // InvertedCurrent decorator with dynamic unit measure adjustment. 211 | // 212 | // `unit` one of [0|SizeB1024(0)|SizeB1000(0)] 213 | // 214 | // `format` printf compatible verb for InvertedCurrent 215 | // 216 | // `wcc` optional WC config 217 | // 218 | // format example if unit=SizeB1024(0): 219 | // 220 | // format="%d" output: "12MiB" 221 | // format="% d" output: "12 MiB" 222 | // format="%.1f" output: "12.0MiB" 223 | // format="% .1f" output: "12.0 MiB" 224 | // format="%f" output: "12.000000MiB" 225 | // format="% f" output: "12.000000 MiB" 226 | func InvertedCurrent(unit interface{}, format string, wcc ...WC) Decorator { 227 | producer := func() DecorFunc { 228 | switch unit.(type) { 229 | case SizeB1024: 230 | if format == "" { 231 | format = "% d" 232 | } 233 | return func(s Statistics) string { 234 | return fmt.Sprintf(format, SizeB1024(s.Total-s.Current)) 235 | } 236 | case SizeB1000: 237 | if format == "" { 238 | format = "% d" 239 | } 240 | return func(s Statistics) string { 241 | return fmt.Sprintf(format, SizeB1000(s.Total-s.Current)) 242 | } 243 | default: 244 | if format == "" { 245 | format = "%d" 246 | } 247 | return func(s Statistics) string { 248 | return fmt.Sprintf(format, s.Total-s.Current) 249 | } 250 | } 251 | } 252 | return Any(producer(), wcc...) 253 | } 254 | -------------------------------------------------------------------------------- /decor/decorator.go: -------------------------------------------------------------------------------- 1 | package decor 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/mattn/go-runewidth" 8 | ) 9 | 10 | const ( 11 | // DindentRight sets indentation from right to left. 12 | // 13 | // |foo |b | DindentRight is set 14 | // | foo| b| DindentRight is not set 15 | DindentRight = 1 << iota 16 | 17 | // DextraSpace bit adds extra indentation space. 18 | DextraSpace 19 | 20 | // DSyncWidth bit enables same column width synchronization. 21 | // Effective with multiple bars only. 22 | DSyncWidth 23 | 24 | // DSyncWidthR is shortcut for DSyncWidth|DindentRight 25 | DSyncWidthR = DSyncWidth | DindentRight 26 | 27 | // DSyncSpace is shortcut for DSyncWidth|DextraSpace 28 | DSyncSpace = DSyncWidth | DextraSpace 29 | 30 | // DSyncSpaceR is shortcut for DSyncWidth|DextraSpace|DindentRight 31 | DSyncSpaceR = DSyncWidth | DextraSpace | DindentRight 32 | ) 33 | 34 | // TimeStyle enum. 35 | type TimeStyle int 36 | 37 | // TimeStyle kinds. 38 | const ( 39 | ET_STYLE_GO TimeStyle = iota 40 | ET_STYLE_HHMMSS 41 | ET_STYLE_HHMM 42 | ET_STYLE_MMSS 43 | ) 44 | 45 | // Statistics contains fields which are necessary for implementing 46 | // `decor.Decorator` and `mpb.BarFiller` interfaces. 47 | type Statistics struct { 48 | AvailableWidth int // calculated width initially equal to terminal width 49 | RequestedWidth int // width set by `mpb.WithWidth` 50 | ID int 51 | Total int64 52 | Current int64 53 | Refill int64 54 | Completed bool 55 | Aborted bool 56 | } 57 | 58 | // Decorator interface. 59 | // Most of the time there is no need to implement this interface 60 | // manually, as decor package already provides a wide range of decorators 61 | // which implement this interface. If however built-in decorators don't 62 | // meet your needs, you're free to implement your own one by implementing 63 | // this particular interface. The easy way to go is to convert a 64 | // `DecorFunc` into a `Decorator` interface by using provided 65 | // `func Any(DecorFunc, ...WC) Decorator`. 66 | type Decorator interface { 67 | Synchronizer 68 | Formatter 69 | Decor(Statistics) (str string, viewWidth int) 70 | } 71 | 72 | // DecorFunc func type. 73 | // To be used with `func Any(DecorFunc, ...WC) Decorator`. 74 | type DecorFunc func(Statistics) string 75 | 76 | // Synchronizer interface. 77 | // All decorators implement this interface implicitly. Its Sync 78 | // method exposes width sync channel, if DSyncWidth bit is set. 79 | type Synchronizer interface { 80 | Sync() (chan int, bool) 81 | } 82 | 83 | // Formatter interface. 84 | // Format method needs to be called from within Decorator.Decor method 85 | // in order to format string according to decor.WC settings. 86 | // No need to implement manually as long as decor.WC is embedded. 87 | type Formatter interface { 88 | Format(string) (_ string, width int) 89 | } 90 | 91 | // Wrapper interface. 92 | // If you're implementing custom Decorator by wrapping a built-in one, 93 | // it is necessary to implement this interface to retain functionality 94 | // of built-in Decorator. 95 | type Wrapper interface { 96 | Unwrap() Decorator 97 | } 98 | 99 | // EwmaDecorator interface. 100 | // EWMA based decorators should implement this one. 101 | type EwmaDecorator interface { 102 | EwmaUpdate(int64, time.Duration) 103 | } 104 | 105 | // AverageDecorator interface. 106 | // Average decorators should implement this interface to provide start 107 | // time adjustment facility, for resume-able tasks. 108 | type AverageDecorator interface { 109 | AverageAdjust(time.Time) 110 | } 111 | 112 | // ShutdownListener interface. 113 | // If decorator needs to be notified once upon bar shutdown event, so 114 | // this is the right interface to implement. 115 | type ShutdownListener interface { 116 | OnShutdown() 117 | } 118 | 119 | // Global convenience instances of WC with sync width bit set. 120 | // To be used with multiple bars only, i.e. not effective for single bar usage. 121 | var ( 122 | WCSyncWidth = WC{C: DSyncWidth} 123 | WCSyncWidthR = WC{C: DSyncWidthR} 124 | WCSyncSpace = WC{C: DSyncSpace} 125 | WCSyncSpaceR = WC{C: DSyncSpaceR} 126 | ) 127 | 128 | // WC is a struct with two public fields W and C, both of int type. 129 | // W represents width and C represents bit set of width related config. 130 | // A decorator should embed WC, to enable width synchronization. 131 | type WC struct { 132 | W int 133 | C int 134 | fill func(s string, w int) string 135 | wsync chan int 136 | } 137 | 138 | // Format should be called by any Decorator implementation. 139 | // Returns formatted string and its view (visual) width. 140 | func (wc WC) Format(str string) (string, int) { 141 | width := runewidth.StringWidth(str) 142 | if wc.W > width { 143 | width = wc.W 144 | } else if (wc.C & DextraSpace) != 0 { 145 | width++ 146 | } 147 | if (wc.C & DSyncWidth) != 0 { 148 | wc.wsync <- width 149 | width = <-wc.wsync 150 | } 151 | return wc.fill(str, width), width 152 | } 153 | 154 | // Init initializes width related config. 155 | func (wc *WC) Init() WC { 156 | if (wc.C & DindentRight) != 0 { 157 | wc.fill = runewidth.FillRight 158 | } else { 159 | wc.fill = runewidth.FillLeft 160 | } 161 | if (wc.C & DSyncWidth) != 0 { 162 | // it's deliberate choice to override wsync on each Init() call, 163 | // this way globals like WCSyncSpace can be reused 164 | wc.wsync = make(chan int) 165 | } 166 | return *wc 167 | } 168 | 169 | // Sync is implementation of Synchronizer interface. 170 | func (wc WC) Sync() (chan int, bool) { 171 | if (wc.C&DSyncWidth) != 0 && wc.wsync == nil { 172 | panic(fmt.Sprintf("%T is not initialized", wc)) 173 | } 174 | return wc.wsync, (wc.C & DSyncWidth) != 0 175 | } 176 | 177 | func initWC(wcc ...WC) WC { 178 | var wc WC 179 | for _, nwc := range wcc { 180 | wc = nwc 181 | } 182 | return wc.Init() 183 | } 184 | -------------------------------------------------------------------------------- /decor/doc.go: -------------------------------------------------------------------------------- 1 | // Package decor provides common decorators for "github.com/vbauerster/mpb/v8" module. 2 | // 3 | // Some decorators returned by this package might have a closure state. It is ok to use 4 | // decorators concurrently, unless you share the same decorator among multiple 5 | // *mpb.Bar instances. To avoid data races, create new decorator per *mpb.Bar instance. 6 | // 7 | // Don't: 8 | // 9 | // p := mpb.New() 10 | // name := decor.Name("bar") 11 | // p.AddBar(100, mpb.AppendDecorators(name)) 12 | // p.AddBar(100, mpb.AppendDecorators(name)) 13 | // 14 | // Do: 15 | // 16 | // p := mpb.New() 17 | // p.AddBar(100, mpb.AppendDecorators(decor.Name("bar1"))) 18 | // p.AddBar(100, mpb.AppendDecorators(decor.Name("bar2"))) 19 | package decor 20 | -------------------------------------------------------------------------------- /decor/elapsed.go: -------------------------------------------------------------------------------- 1 | package decor 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Elapsed decorator. It's wrapper of NewElapsed. 8 | // 9 | // `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS] 10 | // 11 | // `wcc` optional WC config 12 | func Elapsed(style TimeStyle, wcc ...WC) Decorator { 13 | return NewElapsed(style, time.Now(), wcc...) 14 | } 15 | 16 | // NewElapsed returns elapsed time decorator. 17 | // 18 | // `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS] 19 | // 20 | // `start` start time 21 | // 22 | // `wcc` optional WC config 23 | func NewElapsed(style TimeStyle, start time.Time, wcc ...WC) Decorator { 24 | var msg string 25 | producer := chooseTimeProducer(style) 26 | fn := func(s Statistics) string { 27 | if !s.Completed && !s.Aborted { 28 | msg = producer(time.Since(start)) 29 | } 30 | return msg 31 | } 32 | return Any(fn, wcc...) 33 | } 34 | -------------------------------------------------------------------------------- /decor/eta.go: -------------------------------------------------------------------------------- 1 | package decor 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "time" 7 | 8 | "github.com/VividCortex/ewma" 9 | ) 10 | 11 | var ( 12 | _ Decorator = (*movingAverageETA)(nil) 13 | _ EwmaDecorator = (*movingAverageETA)(nil) 14 | _ Decorator = (*averageETA)(nil) 15 | _ AverageDecorator = (*averageETA)(nil) 16 | ) 17 | 18 | // TimeNormalizer interface. Implementers could be passed into 19 | // MovingAverageETA, in order to affect i.e. normalize its output. 20 | type TimeNormalizer interface { 21 | Normalize(time.Duration) time.Duration 22 | } 23 | 24 | // TimeNormalizerFunc is function type adapter to convert function 25 | // into TimeNormalizer. 26 | type TimeNormalizerFunc func(time.Duration) time.Duration 27 | 28 | func (f TimeNormalizerFunc) Normalize(src time.Duration) time.Duration { 29 | return f(src) 30 | } 31 | 32 | // EwmaETA exponential-weighted-moving-average based ETA decorator. For this 33 | // decorator to work correctly you have to measure each iteration's duration 34 | // and pass it to one of the (*Bar).EwmaIncr... family methods. 35 | func EwmaETA(style TimeStyle, age float64, wcc ...WC) Decorator { 36 | return EwmaNormalizedETA(style, age, nil, wcc...) 37 | } 38 | 39 | // EwmaNormalizedETA same as EwmaETA but with TimeNormalizer option. 40 | func EwmaNormalizedETA(style TimeStyle, age float64, normalizer TimeNormalizer, wcc ...WC) Decorator { 41 | var average ewma.MovingAverage 42 | if age == 0 { 43 | average = ewma.NewMovingAverage() 44 | } else { 45 | average = ewma.NewMovingAverage(age) 46 | } 47 | return MovingAverageETA(style, average, normalizer, wcc...) 48 | } 49 | 50 | // MovingAverageETA decorator relies on MovingAverage implementation to calculate its average. 51 | // 52 | // `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS] 53 | // 54 | // `average` implementation of MovingAverage interface 55 | // 56 | // `normalizer` available implementations are [FixedIntervalTimeNormalizer|MaxTolerateTimeNormalizer] 57 | // 58 | // `wcc` optional WC config 59 | func MovingAverageETA(style TimeStyle, average ewma.MovingAverage, normalizer TimeNormalizer, wcc ...WC) Decorator { 60 | if average == nil { 61 | average = NewMedian() 62 | } 63 | d := &movingAverageETA{ 64 | WC: initWC(wcc...), 65 | producer: chooseTimeProducer(style), 66 | average: average, 67 | normalizer: normalizer, 68 | } 69 | return d 70 | } 71 | 72 | type movingAverageETA struct { 73 | WC 74 | producer func(time.Duration) string 75 | average ewma.MovingAverage 76 | normalizer TimeNormalizer 77 | zDur time.Duration 78 | } 79 | 80 | func (d *movingAverageETA) Decor(s Statistics) (string, int) { 81 | v := math.Round(d.average.Value()) 82 | remaining := time.Duration((s.Total - s.Current) * int64(v)) 83 | if d.normalizer != nil { 84 | remaining = d.normalizer.Normalize(remaining) 85 | } 86 | return d.Format(d.producer(remaining)) 87 | } 88 | 89 | func (d *movingAverageETA) EwmaUpdate(n int64, dur time.Duration) { 90 | if n <= 0 { 91 | d.zDur += dur 92 | return 93 | } 94 | durPerItem := float64(d.zDur+dur) / float64(n) 95 | if math.IsInf(durPerItem, 0) || math.IsNaN(durPerItem) { 96 | d.zDur += dur 97 | return 98 | } 99 | d.zDur = 0 100 | d.average.Add(durPerItem) 101 | } 102 | 103 | // AverageETA decorator. It's wrapper of NewAverageETA. 104 | // 105 | // `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS] 106 | // 107 | // `wcc` optional WC config 108 | func AverageETA(style TimeStyle, wcc ...WC) Decorator { 109 | return NewAverageETA(style, time.Now(), nil, wcc...) 110 | } 111 | 112 | // NewAverageETA decorator with user provided start time. 113 | // 114 | // `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS] 115 | // 116 | // `start` start time 117 | // 118 | // `normalizer` available implementations are [FixedIntervalTimeNormalizer|MaxTolerateTimeNormalizer] 119 | // 120 | // `wcc` optional WC config 121 | func NewAverageETA(style TimeStyle, start time.Time, normalizer TimeNormalizer, wcc ...WC) Decorator { 122 | d := &averageETA{ 123 | WC: initWC(wcc...), 124 | start: start, 125 | normalizer: normalizer, 126 | producer: chooseTimeProducer(style), 127 | } 128 | return d 129 | } 130 | 131 | type averageETA struct { 132 | WC 133 | start time.Time 134 | normalizer TimeNormalizer 135 | producer func(time.Duration) string 136 | } 137 | 138 | func (d *averageETA) Decor(s Statistics) (string, int) { 139 | var remaining time.Duration 140 | if s.Current != 0 { 141 | durPerItem := float64(time.Since(d.start)) / float64(s.Current) 142 | durPerItem = math.Round(durPerItem) 143 | remaining = time.Duration((s.Total - s.Current) * int64(durPerItem)) 144 | if d.normalizer != nil { 145 | remaining = d.normalizer.Normalize(remaining) 146 | } 147 | } 148 | return d.Format(d.producer(remaining)) 149 | } 150 | 151 | func (d *averageETA) AverageAdjust(start time.Time) { 152 | d.start = start 153 | } 154 | 155 | // MaxTolerateTimeNormalizer returns implementation of TimeNormalizer. 156 | func MaxTolerateTimeNormalizer(maxTolerate time.Duration) TimeNormalizer { 157 | var normalized time.Duration 158 | var lastCall time.Time 159 | return TimeNormalizerFunc(func(remaining time.Duration) time.Duration { 160 | if diff := normalized - remaining; diff <= 0 || diff > maxTolerate || remaining < time.Minute { 161 | normalized = remaining 162 | lastCall = time.Now() 163 | return remaining 164 | } 165 | normalized -= time.Since(lastCall) 166 | lastCall = time.Now() 167 | if normalized > 0 { 168 | return normalized 169 | } 170 | return remaining 171 | }) 172 | } 173 | 174 | // FixedIntervalTimeNormalizer returns implementation of TimeNormalizer. 175 | func FixedIntervalTimeNormalizer(updInterval int) TimeNormalizer { 176 | var normalized time.Duration 177 | var lastCall time.Time 178 | var count int 179 | return TimeNormalizerFunc(func(remaining time.Duration) time.Duration { 180 | if count == 0 || remaining < time.Minute { 181 | count = updInterval 182 | normalized = remaining 183 | lastCall = time.Now() 184 | return remaining 185 | } 186 | count-- 187 | normalized -= time.Since(lastCall) 188 | lastCall = time.Now() 189 | if normalized > 0 { 190 | return normalized 191 | } 192 | return remaining 193 | }) 194 | } 195 | 196 | func chooseTimeProducer(style TimeStyle) func(time.Duration) string { 197 | switch style { 198 | case ET_STYLE_HHMMSS: 199 | return func(remaining time.Duration) string { 200 | hours := int64(remaining/time.Hour) % 60 201 | minutes := int64(remaining/time.Minute) % 60 202 | seconds := int64(remaining/time.Second) % 60 203 | return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds) 204 | } 205 | case ET_STYLE_HHMM: 206 | return func(remaining time.Duration) string { 207 | hours := int64(remaining/time.Hour) % 60 208 | minutes := int64(remaining/time.Minute) % 60 209 | return fmt.Sprintf("%02d:%02d", hours, minutes) 210 | } 211 | case ET_STYLE_MMSS: 212 | return func(remaining time.Duration) string { 213 | hours := int64(remaining/time.Hour) % 60 214 | minutes := int64(remaining/time.Minute) % 60 215 | seconds := int64(remaining/time.Second) % 60 216 | if hours > 0 { 217 | return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds) 218 | } 219 | return fmt.Sprintf("%02d:%02d", minutes, seconds) 220 | } 221 | default: 222 | return func(remaining time.Duration) string { 223 | return remaining.Truncate(time.Second).String() 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /decor/meta.go: -------------------------------------------------------------------------------- 1 | package decor 2 | 3 | var ( 4 | _ Decorator = metaWrapper{} 5 | _ Wrapper = metaWrapper{} 6 | ) 7 | 8 | // Meta wrap decorator. 9 | // Provided fn is supposed to wrap output of given decorator 10 | // with meta information like ANSI escape codes for example. 11 | // Primary usage intention is to set SGR display attributes. 12 | // 13 | // `decorator` Decorator to wrap 14 | // `fn` func to apply meta information 15 | func Meta(decorator Decorator, fn func(string) string) Decorator { 16 | if decorator == nil { 17 | return nil 18 | } 19 | return metaWrapper{decorator, fn} 20 | } 21 | 22 | type metaWrapper struct { 23 | Decorator 24 | fn func(string) string 25 | } 26 | 27 | func (d metaWrapper) Decor(s Statistics) (string, int) { 28 | str, width := d.Decorator.Decor(s) 29 | return d.fn(str), width 30 | } 31 | 32 | func (d metaWrapper) Unwrap() Decorator { 33 | return d.Decorator 34 | } 35 | -------------------------------------------------------------------------------- /decor/moving_average.go: -------------------------------------------------------------------------------- 1 | package decor 2 | 3 | import ( 4 | "sort" 5 | "sync" 6 | 7 | "github.com/VividCortex/ewma" 8 | ) 9 | 10 | var ( 11 | _ ewma.MovingAverage = (*threadSafeMovingAverage)(nil) 12 | _ ewma.MovingAverage = (*medianWindow)(nil) 13 | _ sort.Interface = (*medianWindow)(nil) 14 | ) 15 | 16 | type threadSafeMovingAverage struct { 17 | ewma.MovingAverage 18 | mu sync.Mutex 19 | } 20 | 21 | func (s *threadSafeMovingAverage) Add(value float64) { 22 | s.mu.Lock() 23 | s.MovingAverage.Add(value) 24 | s.mu.Unlock() 25 | } 26 | 27 | func (s *threadSafeMovingAverage) Value() float64 { 28 | s.mu.Lock() 29 | defer s.mu.Unlock() 30 | return s.MovingAverage.Value() 31 | } 32 | 33 | func (s *threadSafeMovingAverage) Set(value float64) { 34 | s.mu.Lock() 35 | s.MovingAverage.Set(value) 36 | s.mu.Unlock() 37 | } 38 | 39 | // NewThreadSafeMovingAverage converts provided ewma.MovingAverage 40 | // into thread safe ewma.MovingAverage. 41 | func NewThreadSafeMovingAverage(average ewma.MovingAverage) ewma.MovingAverage { 42 | if tsma, ok := average.(*threadSafeMovingAverage); ok { 43 | return tsma 44 | } 45 | return &threadSafeMovingAverage{MovingAverage: average} 46 | } 47 | 48 | type medianWindow [3]float64 49 | 50 | func (s *medianWindow) Len() int { return len(s) } 51 | func (s *medianWindow) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 52 | func (s *medianWindow) Less(i, j int) bool { return s[i] < s[j] } 53 | 54 | func (s *medianWindow) Add(value float64) { 55 | s[0], s[1] = s[1], s[2] 56 | s[2] = value 57 | } 58 | 59 | func (s *medianWindow) Value() float64 { 60 | tmp := *s 61 | sort.Sort(&tmp) 62 | return tmp[1] 63 | } 64 | 65 | func (s *medianWindow) Set(value float64) { 66 | for i := 0; i < len(s); i++ { 67 | s[i] = value 68 | } 69 | } 70 | 71 | // NewMedian is fixed last 3 samples median MovingAverage. 72 | func NewMedian() ewma.MovingAverage { 73 | return new(medianWindow) 74 | } 75 | -------------------------------------------------------------------------------- /decor/name.go: -------------------------------------------------------------------------------- 1 | package decor 2 | 3 | // Name decorator displays text that is set once and can't be changed 4 | // during decorator's lifetime. 5 | // 6 | // `str` string to display 7 | // 8 | // `wcc` optional WC config 9 | func Name(str string, wcc ...WC) Decorator { 10 | return Any(func(Statistics) string { return str }, wcc...) 11 | } 12 | -------------------------------------------------------------------------------- /decor/on_abort.go: -------------------------------------------------------------------------------- 1 | package decor 2 | 3 | var ( 4 | _ Decorator = onAbortWrapper{} 5 | _ Wrapper = onAbortWrapper{} 6 | _ Decorator = onAbortMetaWrapper{} 7 | _ Wrapper = onAbortMetaWrapper{} 8 | ) 9 | 10 | // OnAbort wrap decorator. 11 | // Displays provided message on abort event. 12 | // Has no effect if bar.Abort(true) is called. 13 | // 14 | // `decorator` Decorator to wrap 15 | // `message` message to display 16 | func OnAbort(decorator Decorator, message string) Decorator { 17 | if decorator == nil { 18 | return nil 19 | } 20 | return onAbortWrapper{decorator, message} 21 | } 22 | 23 | type onAbortWrapper struct { 24 | Decorator 25 | msg string 26 | } 27 | 28 | func (d onAbortWrapper) Decor(s Statistics) (string, int) { 29 | if s.Aborted { 30 | return d.Format(d.msg) 31 | } 32 | return d.Decorator.Decor(s) 33 | } 34 | 35 | func (d onAbortWrapper) Unwrap() Decorator { 36 | return d.Decorator 37 | } 38 | 39 | // OnAbortMeta wrap decorator. 40 | // Provided fn is supposed to wrap output of given decorator 41 | // with meta information like ANSI escape codes for example. 42 | // Primary usage intention is to set SGR display attributes. 43 | // 44 | // `decorator` Decorator to wrap 45 | // `fn` func to apply meta information 46 | func OnAbortMeta(decorator Decorator, fn func(string) string) Decorator { 47 | if decorator == nil { 48 | return nil 49 | } 50 | return onAbortMetaWrapper{decorator, fn} 51 | } 52 | 53 | type onAbortMetaWrapper struct { 54 | Decorator 55 | fn func(string) string 56 | } 57 | 58 | func (d onAbortMetaWrapper) Decor(s Statistics) (string, int) { 59 | if s.Aborted { 60 | str, width := d.Decorator.Decor(s) 61 | return d.fn(str), width 62 | } 63 | return d.Decorator.Decor(s) 64 | } 65 | 66 | func (d onAbortMetaWrapper) Unwrap() Decorator { 67 | return d.Decorator 68 | } 69 | -------------------------------------------------------------------------------- /decor/on_compete_or_on_abort.go: -------------------------------------------------------------------------------- 1 | package decor 2 | 3 | // OnCompleteOrOnAbort wrap decorator. 4 | // Displays provided message on complete or on abort event. 5 | // 6 | // `decorator` Decorator to wrap 7 | // `message` message to display 8 | func OnCompleteOrOnAbort(decorator Decorator, message string) Decorator { 9 | return OnComplete(OnAbort(decorator, message), message) 10 | } 11 | 12 | // OnCompleteMetaOrOnAbortMeta wrap decorator. 13 | // Provided fn is supposed to wrap output of given decorator 14 | // with meta information like ANSI escape codes for example. 15 | // Primary usage intention is to set SGR display attributes. 16 | // 17 | // `decorator` Decorator to wrap 18 | // `fn` func to apply meta information 19 | func OnCompleteMetaOrOnAbortMeta(decorator Decorator, fn func(string) string) Decorator { 20 | return OnCompleteMeta(OnAbortMeta(decorator, fn), fn) 21 | } 22 | -------------------------------------------------------------------------------- /decor/on_complete.go: -------------------------------------------------------------------------------- 1 | package decor 2 | 3 | var ( 4 | _ Decorator = onCompleteWrapper{} 5 | _ Wrapper = onCompleteWrapper{} 6 | _ Decorator = onCompleteMetaWrapper{} 7 | _ Wrapper = onCompleteMetaWrapper{} 8 | ) 9 | 10 | // OnComplete wrap decorator. 11 | // Displays provided message on complete event. 12 | // 13 | // `decorator` Decorator to wrap 14 | // `message` message to display 15 | func OnComplete(decorator Decorator, message string) Decorator { 16 | if decorator == nil { 17 | return nil 18 | } 19 | return onCompleteWrapper{decorator, message} 20 | } 21 | 22 | type onCompleteWrapper struct { 23 | Decorator 24 | msg string 25 | } 26 | 27 | func (d onCompleteWrapper) Decor(s Statistics) (string, int) { 28 | if s.Completed { 29 | return d.Format(d.msg) 30 | } 31 | return d.Decorator.Decor(s) 32 | } 33 | 34 | func (d onCompleteWrapper) Unwrap() Decorator { 35 | return d.Decorator 36 | } 37 | 38 | // OnCompleteMeta wrap decorator. 39 | // Provided fn is supposed to wrap output of given decorator 40 | // with meta information like ANSI escape codes for example. 41 | // Primary usage intention is to set SGR display attributes. 42 | // 43 | // `decorator` Decorator to wrap 44 | // `fn` func to apply meta information 45 | func OnCompleteMeta(decorator Decorator, fn func(string) string) Decorator { 46 | if decorator == nil { 47 | return nil 48 | } 49 | return onCompleteMetaWrapper{decorator, fn} 50 | } 51 | 52 | type onCompleteMetaWrapper struct { 53 | Decorator 54 | fn func(string) string 55 | } 56 | 57 | func (d onCompleteMetaWrapper) Decor(s Statistics) (string, int) { 58 | if s.Completed { 59 | str, width := d.Decorator.Decor(s) 60 | return d.fn(str), width 61 | } 62 | return d.Decorator.Decor(s) 63 | } 64 | 65 | func (d onCompleteMetaWrapper) Unwrap() Decorator { 66 | return d.Decorator 67 | } 68 | -------------------------------------------------------------------------------- /decor/on_condition.go: -------------------------------------------------------------------------------- 1 | package decor 2 | 3 | // OnCondition applies decorator only if a condition is true. 4 | // 5 | // `decorator` Decorator 6 | // 7 | // `cond` bool 8 | func OnCondition(decorator Decorator, cond bool) Decorator { 9 | return Conditional(cond, decorator, nil) 10 | } 11 | 12 | // OnPredicate applies decorator only if a predicate evaluates to true. 13 | // 14 | // `decorator` Decorator 15 | // 16 | // `predicate` func() bool 17 | func OnPredicate(decorator Decorator, predicate func() bool) Decorator { 18 | return Predicative(predicate, decorator, nil) 19 | } 20 | 21 | // Conditional returns decorator `a` if condition is true, otherwise 22 | // decorator `b`. 23 | // 24 | // `cond` bool 25 | // 26 | // `a` Decorator 27 | // 28 | // `b` Decorator 29 | func Conditional(cond bool, a, b Decorator) Decorator { 30 | if cond { 31 | return a 32 | } else { 33 | return b 34 | } 35 | } 36 | 37 | // Predicative returns decorator `a` if predicate evaluates to true, 38 | // otherwise decorator `b`. 39 | // 40 | // `predicate` func() bool 41 | // 42 | // `a` Decorator 43 | // 44 | // `b` Decorator 45 | func Predicative(predicate func() bool, a, b Decorator) Decorator { 46 | if predicate() { 47 | return a 48 | } else { 49 | return b 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /decor/percentage.go: -------------------------------------------------------------------------------- 1 | package decor 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/vbauerster/mpb/v8/internal" 8 | ) 9 | 10 | var _ fmt.Formatter = percentageType(0) 11 | 12 | type percentageType float64 13 | 14 | func (s percentageType) Format(st fmt.State, verb rune) { 15 | prec := -1 16 | switch verb { 17 | case 'f', 'e', 'E': 18 | prec = 6 // default prec of fmt.Printf("%f|%e|%E") 19 | fallthrough 20 | case 'b', 'g', 'G', 'x', 'X': 21 | if p, ok := st.Precision(); ok { 22 | prec = p 23 | } 24 | default: 25 | verb, prec = 'f', 0 26 | } 27 | 28 | b := strconv.AppendFloat(make([]byte, 0, 16), float64(s), byte(verb), prec, 64) 29 | if st.Flag(' ') { 30 | b = append(b, ' ', '%') 31 | } else { 32 | b = append(b, '%') 33 | } 34 | _, err := st.Write(b) 35 | if err != nil { 36 | panic(err) 37 | } 38 | } 39 | 40 | // Percentage returns percentage decorator. It's a wrapper of NewPercentage. 41 | func Percentage(wcc ...WC) Decorator { 42 | return NewPercentage("% d", wcc...) 43 | } 44 | 45 | // NewPercentage percentage decorator with custom format string. 46 | // 47 | // `format` printf compatible verb 48 | // 49 | // `wcc` optional WC config 50 | // 51 | // format examples: 52 | // 53 | // format="%d" output: "1%" 54 | // format="% d" output: "1 %" 55 | // format="%.1f" output: "1.0%" 56 | // format="% .1f" output: "1.0 %" 57 | // format="%f" output: "1.000000%" 58 | // format="% f" output: "1.000000 %" 59 | func NewPercentage(format string, wcc ...WC) Decorator { 60 | if format == "" { 61 | format = "% d" 62 | } 63 | f := func(s Statistics) string { 64 | p := internal.Percentage(uint(s.Total), uint(s.Current), 100) 65 | return fmt.Sprintf(format, percentageType(p)) 66 | } 67 | return Any(f, wcc...) 68 | } 69 | -------------------------------------------------------------------------------- /decor/percentage_test.go: -------------------------------------------------------------------------------- 1 | package decor 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestPercentageType(t *testing.T) { 9 | cases := map[string]struct { 10 | value float64 11 | verb string 12 | expected string 13 | }{ 14 | "10 %d": {10, "%d", "10%"}, 15 | "10 %s": {10, "%s", "10%"}, 16 | "10 %f": {10, "%f", "10.000000%"}, 17 | "10 %.6f": {10, "%.6f", "10.000000%"}, 18 | "10 %.0f": {10, "%.0f", "10%"}, 19 | "10 %.1f": {10, "%.1f", "10.0%"}, 20 | "10 %.2f": {10, "%.2f", "10.00%"}, 21 | "10 %.3f": {10, "%.3f", "10.000%"}, 22 | 23 | "10 % d": {10, "% d", "10 %"}, 24 | "10 % s": {10, "% s", "10 %"}, 25 | "10 % f": {10, "% f", "10.000000 %"}, 26 | "10 % .6f": {10, "% .6f", "10.000000 %"}, 27 | "10 % .0f": {10, "% .0f", "10 %"}, 28 | "10 % .1f": {10, "% .1f", "10.0 %"}, 29 | "10 % .2f": {10, "% .2f", "10.00 %"}, 30 | "10 % .3f": {10, "% .3f", "10.000 %"}, 31 | 32 | "10.5 %d": {10.5, "%d", "10%"}, 33 | "10.5 %s": {10.5, "%s", "10%"}, 34 | "10.5 %f": {10.5, "%f", "10.500000%"}, 35 | "10.5 %.6f": {10.5, "%.6f", "10.500000%"}, 36 | "10.5 %.0f": {10.5, "%.0f", "10%"}, 37 | "10.5 %.1f": {10.5, "%.1f", "10.5%"}, 38 | "10.5 %.2f": {10.5, "%.2f", "10.50%"}, 39 | "10.5 %.3f": {10.5, "%.3f", "10.500%"}, 40 | 41 | "10.5 % d": {10.5, "% d", "10 %"}, 42 | "10.5 % s": {10.5, "% s", "10 %"}, 43 | "10.5 % f": {10.5, "% f", "10.500000 %"}, 44 | "10.5 % .6f": {10.5, "% .6f", "10.500000 %"}, 45 | "10.5 % .0f": {10.5, "% .0f", "10 %"}, 46 | "10.5 % .1f": {10.5, "% .1f", "10.5 %"}, 47 | "10.5 % .2f": {10.5, "% .2f", "10.50 %"}, 48 | "10.5 % .3f": {10.5, "% .3f", "10.500 %"}, 49 | } 50 | for name, tc := range cases { 51 | t.Run(name, func(t *testing.T) { 52 | got := fmt.Sprintf(tc.verb, percentageType(tc.value)) 53 | if got != tc.expected { 54 | t.Fatalf("expected: %q, got: %q\n", tc.expected, got) 55 | } 56 | }) 57 | } 58 | } 59 | 60 | func TestPercentageDecor(t *testing.T) { 61 | cases := []struct { 62 | name string 63 | fmt string 64 | current int64 65 | total int64 66 | expected string 67 | }{ 68 | { 69 | name: "tot:100 cur:0 fmt:none", 70 | fmt: "", 71 | current: 0, 72 | total: 100, 73 | expected: "0 %", 74 | }, 75 | { 76 | name: "tot:100 cur:10 fmt:none", 77 | fmt: "", 78 | current: 10, 79 | total: 100, 80 | expected: "10 %", 81 | }, 82 | { 83 | name: "tot:100 cur:10 fmt:%.2f", 84 | fmt: "%.2f", 85 | current: 10, 86 | total: 100, 87 | expected: "10.00%", 88 | }, 89 | { 90 | name: "tot:99 cur:10 fmt:%.2f", 91 | fmt: "%.2f", 92 | current: 11, 93 | total: 99, 94 | expected: "11.11%", 95 | }, 96 | } 97 | for _, tc := range cases { 98 | t.Run(tc.name, func(t *testing.T) { 99 | decor := NewPercentage(tc.fmt) 100 | stat := Statistics{ 101 | Total: tc.total, 102 | Current: tc.current, 103 | } 104 | res, _ := decor.Decor(stat) 105 | if res != tc.expected { 106 | t.Fatalf("expected: %q, got: %q\n", tc.expected, res) 107 | } 108 | }) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /decor/size_type.go: -------------------------------------------------------------------------------- 1 | //go:generate go tool stringer -type=SizeB1024 -trimprefix=_i 2 | //go:generate go tool stringer -type=SizeB1000 -trimprefix=_ 3 | 4 | package decor 5 | 6 | import ( 7 | "fmt" 8 | "strconv" 9 | ) 10 | 11 | var ( 12 | _ fmt.Formatter = SizeB1024(0) 13 | _ fmt.Stringer = SizeB1024(0) 14 | _ fmt.Formatter = SizeB1000(0) 15 | _ fmt.Stringer = SizeB1000(0) 16 | ) 17 | 18 | const ( 19 | _ib SizeB1024 = iota + 1 20 | _iKiB SizeB1024 = 1 << (iota * 10) 21 | _iMiB 22 | _iGiB 23 | _iTiB 24 | ) 25 | 26 | // SizeB1024 named type, which implements fmt.Formatter interface. It 27 | // adjusts its value according to byte size multiple by 1024 and appends 28 | // appropriate size marker (KiB, MiB, GiB, TiB). 29 | type SizeB1024 int64 30 | 31 | func (s SizeB1024) Format(f fmt.State, verb rune) { 32 | prec := -1 33 | switch verb { 34 | case 'f', 'e', 'E': 35 | prec = 6 // default prec of fmt.Printf("%f|%e|%E") 36 | fallthrough 37 | case 'b', 'g', 'G', 'x', 'X': 38 | if p, ok := f.Precision(); ok { 39 | prec = p 40 | } 41 | default: 42 | verb, prec = 'f', 0 43 | } 44 | 45 | var unit SizeB1024 46 | switch { 47 | case s < _iKiB: 48 | unit = _ib 49 | case s < _iMiB: 50 | unit = _iKiB 51 | case s < _iGiB: 52 | unit = _iMiB 53 | case s < _iTiB: 54 | unit = _iGiB 55 | default: 56 | unit = _iTiB 57 | } 58 | 59 | b := strconv.AppendFloat(make([]byte, 0, 24), float64(s)/float64(unit), byte(verb), prec, 64) 60 | if f.Flag(' ') { 61 | b = append(b, ' ') 62 | } 63 | b = append(b, []byte(unit.String())...) 64 | _, err := f.Write(b) 65 | if err != nil { 66 | panic(err) 67 | } 68 | } 69 | 70 | const ( 71 | _b SizeB1000 = 1 72 | _KB SizeB1000 = _b * 1000 73 | _MB SizeB1000 = _KB * 1000 74 | _GB SizeB1000 = _MB * 1000 75 | _TB SizeB1000 = _GB * 1000 76 | ) 77 | 78 | // SizeB1000 named type, which implements fmt.Formatter interface. It 79 | // adjusts its value according to byte size multiple by 1000 and appends 80 | // appropriate size marker (KB, MB, GB, TB). 81 | type SizeB1000 int64 82 | 83 | func (s SizeB1000) Format(f fmt.State, verb rune) { 84 | prec := -1 85 | switch verb { 86 | case 'f', 'e', 'E': 87 | prec = 6 // default prec of fmt.Printf("%f|%e|%E") 88 | fallthrough 89 | case 'b', 'g', 'G', 'x', 'X': 90 | if p, ok := f.Precision(); ok { 91 | prec = p 92 | } 93 | default: 94 | verb, prec = 'f', 0 95 | } 96 | 97 | var unit SizeB1000 98 | switch { 99 | case s < _KB: 100 | unit = _b 101 | case s < _MB: 102 | unit = _KB 103 | case s < _GB: 104 | unit = _MB 105 | case s < _TB: 106 | unit = _GB 107 | default: 108 | unit = _TB 109 | } 110 | 111 | b := strconv.AppendFloat(make([]byte, 0, 24), float64(s)/float64(unit), byte(verb), prec, 64) 112 | if f.Flag(' ') { 113 | b = append(b, ' ') 114 | } 115 | b = append(b, []byte(unit.String())...) 116 | _, err := f.Write(b) 117 | if err != nil { 118 | panic(err) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /decor/size_type_test.go: -------------------------------------------------------------------------------- 1 | package decor 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestB1024(t *testing.T) { 9 | cases := map[string]struct { 10 | value int64 11 | verb string 12 | expected string 13 | }{ 14 | "verb %d": {12345678, "%d", "12MiB"}, 15 | "verb %s": {12345678, "%s", "12MiB"}, 16 | "verb %f": {12345678, "%f", "11.773756MiB"}, 17 | "verb %.6f": {12345678, "%.6f", "11.773756MiB"}, 18 | "verb %.0f": {12345678, "%.0f", "12MiB"}, 19 | "verb %.1f": {12345678, "%.1f", "11.8MiB"}, 20 | "verb %.2f": {12345678, "%.2f", "11.77MiB"}, 21 | "verb %.3f": {12345678, "%.3f", "11.774MiB"}, 22 | "verb % d": {12345678, "% d", "12 MiB"}, 23 | "verb % s": {12345678, "% s", "12 MiB"}, 24 | "verb % f": {12345678, "% f", "11.773756 MiB"}, 25 | "verb % .6f": {12345678, "% .6f", "11.773756 MiB"}, 26 | "verb % .0f": {12345678, "% .0f", "12 MiB"}, 27 | "verb % .1f": {12345678, "% .1f", "11.8 MiB"}, 28 | "verb % .2f": {12345678, "% .2f", "11.77 MiB"}, 29 | "verb % .3f": {12345678, "% .3f", "11.774 MiB"}, 30 | 31 | "1000 %d": {1000, "%d", "1000b"}, 32 | "1000 %s": {1000, "%s", "1000b"}, 33 | "1000 %f": {1000, "%f", "1000.000000b"}, 34 | "1000 %.6f": {1000, "%.6f", "1000.000000b"}, 35 | "1000 %.0f": {1000, "%.0f", "1000b"}, 36 | "1000 %.1f": {1000, "%.1f", "1000.0b"}, 37 | "1000 %.2f": {1000, "%.2f", "1000.00b"}, 38 | "1000 %.3f": {1000, "%.3f", "1000.000b"}, 39 | "1024 %d": {1024, "%d", "1KiB"}, 40 | "1024 %s": {1024, "%s", "1KiB"}, 41 | "1024 %f": {1024, "%f", "1.000000KiB"}, 42 | "1024 %.6f": {1024, "%.6f", "1.000000KiB"}, 43 | "1024 %.0f": {1024, "%.0f", "1KiB"}, 44 | "1024 %.1f": {1024, "%.1f", "1.0KiB"}, 45 | "1024 %.2f": {1024, "%.2f", "1.00KiB"}, 46 | "1024 %.3f": {1024, "%.3f", "1.000KiB"}, 47 | 48 | "3*MiB+100KiB %d": {3*int64(_iMiB) + 100*int64(_iKiB), "%d", "3MiB"}, 49 | "3*MiB+100KiB %s": {3*int64(_iMiB) + 100*int64(_iKiB), "%s", "3MiB"}, 50 | "3*MiB+100KiB %f": {3*int64(_iMiB) + 100*int64(_iKiB), "%f", "3.097656MiB"}, 51 | "3*MiB+100KiB %.6f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.6f", "3.097656MiB"}, 52 | "3*MiB+100KiB %.0f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.0f", "3MiB"}, 53 | "3*MiB+100KiB %.1f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.1f", "3.1MiB"}, 54 | "3*MiB+100KiB %.2f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.2f", "3.10MiB"}, 55 | "3*MiB+100KiB %.3f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.3f", "3.098MiB"}, 56 | 57 | "2*GiB %d": {2 * int64(_iGiB), "%d", "2GiB"}, 58 | "2*GiB %s": {2 * int64(_iGiB), "%s", "2GiB"}, 59 | "2*GiB %f": {2 * int64(_iGiB), "%f", "2.000000GiB"}, 60 | "2*GiB %.6f": {2 * int64(_iGiB), "%.6f", "2.000000GiB"}, 61 | "2*GiB %.0f": {2 * int64(_iGiB), "%.0f", "2GiB"}, 62 | "2*GiB %.1f": {2 * int64(_iGiB), "%.1f", "2.0GiB"}, 63 | "2*GiB %.2f": {2 * int64(_iGiB), "%.2f", "2.00GiB"}, 64 | "2*GiB %.3f": {2 * int64(_iGiB), "%.3f", "2.000GiB"}, 65 | 66 | "4*TiB %d": {4 * int64(_iTiB), "%d", "4TiB"}, 67 | "4*TiB %s": {4 * int64(_iTiB), "%s", "4TiB"}, 68 | "4*TiB %f": {4 * int64(_iTiB), "%f", "4.000000TiB"}, 69 | "4*TiB %.6f": {4 * int64(_iTiB), "%.6f", "4.000000TiB"}, 70 | "4*TiB %.0f": {4 * int64(_iTiB), "%.0f", "4TiB"}, 71 | "4*TiB %.1f": {4 * int64(_iTiB), "%.1f", "4.0TiB"}, 72 | "4*TiB %.2f": {4 * int64(_iTiB), "%.2f", "4.00TiB"}, 73 | "4*TiB %.3f": {4 * int64(_iTiB), "%.3f", "4.000TiB"}, 74 | } 75 | for name, tc := range cases { 76 | t.Run(name, func(t *testing.T) { 77 | got := fmt.Sprintf(tc.verb, SizeB1024(tc.value)) 78 | if got != tc.expected { 79 | t.Fatalf("expected: %q, got: %q\n", tc.expected, got) 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func TestB1000(t *testing.T) { 86 | cases := map[string]struct { 87 | value int64 88 | verb string 89 | expected string 90 | }{ 91 | "verb %d": {12345678, "%d", "12MB"}, 92 | "verb %s": {12345678, "%s", "12MB"}, 93 | "verb %f": {12345678, "%f", "12.345678MB"}, 94 | "verb %.6f": {12345678, "%.6f", "12.345678MB"}, 95 | "verb %.0f": {12345678, "%.0f", "12MB"}, 96 | "verb %.1f": {12345678, "%.1f", "12.3MB"}, 97 | "verb %.2f": {12345678, "%.2f", "12.35MB"}, 98 | "verb %.3f": {12345678, "%.3f", "12.346MB"}, 99 | "verb % d": {12345678, "% d", "12 MB"}, 100 | "verb % s": {12345678, "% s", "12 MB"}, 101 | "verb % f": {12345678, "% f", "12.345678 MB"}, 102 | "verb % .6f": {12345678, "% .6f", "12.345678 MB"}, 103 | "verb % .0f": {12345678, "% .0f", "12 MB"}, 104 | "verb % .1f": {12345678, "% .1f", "12.3 MB"}, 105 | "verb % .2f": {12345678, "% .2f", "12.35 MB"}, 106 | "verb % .3f": {12345678, "% .3f", "12.346 MB"}, 107 | 108 | "1000 %d": {1000, "%d", "1KB"}, 109 | "1000 %s": {1000, "%s", "1KB"}, 110 | "1000 %f": {1000, "%f", "1.000000KB"}, 111 | "1000 %.6f": {1000, "%.6f", "1.000000KB"}, 112 | "1000 %.0f": {1000, "%.0f", "1KB"}, 113 | "1000 %.1f": {1000, "%.1f", "1.0KB"}, 114 | "1000 %.2f": {1000, "%.2f", "1.00KB"}, 115 | "1000 %.3f": {1000, "%.3f", "1.000KB"}, 116 | "1024 %d": {1024, "%d", "1KB"}, 117 | "1024 %s": {1024, "%s", "1KB"}, 118 | "1024 %f": {1024, "%f", "1.024000KB"}, 119 | "1024 %.6f": {1024, "%.6f", "1.024000KB"}, 120 | "1024 %.0f": {1024, "%.0f", "1KB"}, 121 | "1024 %.1f": {1024, "%.1f", "1.0KB"}, 122 | "1024 %.2f": {1024, "%.2f", "1.02KB"}, 123 | "1024 %.3f": {1024, "%.3f", "1.024KB"}, 124 | 125 | "3*MB+100*KB %d": {3*int64(_MB) + 100*int64(_KB), "%d", "3MB"}, 126 | "3*MB+100*KB %s": {3*int64(_MB) + 100*int64(_KB), "%s", "3MB"}, 127 | "3*MB+100*KB %f": {3*int64(_MB) + 100*int64(_KB), "%f", "3.100000MB"}, 128 | "3*MB+100*KB %.6f": {3*int64(_MB) + 100*int64(_KB), "%.6f", "3.100000MB"}, 129 | "3*MB+100*KB %.0f": {3*int64(_MB) + 100*int64(_KB), "%.0f", "3MB"}, 130 | "3*MB+100*KB %.1f": {3*int64(_MB) + 100*int64(_KB), "%.1f", "3.1MB"}, 131 | "3*MB+100*KB %.2f": {3*int64(_MB) + 100*int64(_KB), "%.2f", "3.10MB"}, 132 | "3*MB+100*KB %.3f": {3*int64(_MB) + 100*int64(_KB), "%.3f", "3.100MB"}, 133 | 134 | "2*GB %d": {2 * int64(_GB), "%d", "2GB"}, 135 | "2*GB %s": {2 * int64(_GB), "%s", "2GB"}, 136 | "2*GB %f": {2 * int64(_GB), "%f", "2.000000GB"}, 137 | "2*GB %.6f": {2 * int64(_GB), "%.6f", "2.000000GB"}, 138 | "2*GB %.0f": {2 * int64(_GB), "%.0f", "2GB"}, 139 | "2*GB %.1f": {2 * int64(_GB), "%.1f", "2.0GB"}, 140 | "2*GB %.2f": {2 * int64(_GB), "%.2f", "2.00GB"}, 141 | "2*GB %.3f": {2 * int64(_GB), "%.3f", "2.000GB"}, 142 | 143 | "4*TB %d": {4 * int64(_TB), "%d", "4TB"}, 144 | "4*TB %s": {4 * int64(_TB), "%s", "4TB"}, 145 | "4*TB %f": {4 * int64(_TB), "%f", "4.000000TB"}, 146 | "4*TB %.6f": {4 * int64(_TB), "%.6f", "4.000000TB"}, 147 | "4*TB %.0f": {4 * int64(_TB), "%.0f", "4TB"}, 148 | "4*TB %.1f": {4 * int64(_TB), "%.1f", "4.0TB"}, 149 | "4*TB %.2f": {4 * int64(_TB), "%.2f", "4.00TB"}, 150 | "4*TB %.3f": {4 * int64(_TB), "%.3f", "4.000TB"}, 151 | } 152 | for name, tc := range cases { 153 | t.Run(name, func(t *testing.T) { 154 | got := fmt.Sprintf(tc.verb, SizeB1000(tc.value)) 155 | if got != tc.expected { 156 | t.Fatalf("expected: %q, got: %q\n", tc.expected, got) 157 | } 158 | }) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /decor/sizeb1000_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=SizeB1000 -trimprefix=_"; DO NOT EDIT. 2 | 3 | package decor 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[_b-1] 12 | _ = x[_KB-1000] 13 | _ = x[_MB-1000000] 14 | _ = x[_GB-1000000000] 15 | _ = x[_TB-1000000000000] 16 | } 17 | 18 | const ( 19 | _SizeB1000_name_0 = "b" 20 | _SizeB1000_name_1 = "KB" 21 | _SizeB1000_name_2 = "MB" 22 | _SizeB1000_name_3 = "GB" 23 | _SizeB1000_name_4 = "TB" 24 | ) 25 | 26 | func (i SizeB1000) String() string { 27 | switch { 28 | case i == 1: 29 | return _SizeB1000_name_0 30 | case i == 1000: 31 | return _SizeB1000_name_1 32 | case i == 1000000: 33 | return _SizeB1000_name_2 34 | case i == 1000000000: 35 | return _SizeB1000_name_3 36 | case i == 1000000000000: 37 | return _SizeB1000_name_4 38 | default: 39 | return "SizeB1000(" + strconv.FormatInt(int64(i), 10) + ")" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /decor/sizeb1024_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=SizeB1024 -trimprefix=_i"; DO NOT EDIT. 2 | 3 | package decor 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[_ib-1] 12 | _ = x[_iKiB-1024] 13 | _ = x[_iMiB-1048576] 14 | _ = x[_iGiB-1073741824] 15 | _ = x[_iTiB-1099511627776] 16 | } 17 | 18 | const ( 19 | _SizeB1024_name_0 = "b" 20 | _SizeB1024_name_1 = "KiB" 21 | _SizeB1024_name_2 = "MiB" 22 | _SizeB1024_name_3 = "GiB" 23 | _SizeB1024_name_4 = "TiB" 24 | ) 25 | 26 | func (i SizeB1024) String() string { 27 | switch { 28 | case i == 1: 29 | return _SizeB1024_name_0 30 | case i == 1024: 31 | return _SizeB1024_name_1 32 | case i == 1048576: 33 | return _SizeB1024_name_2 34 | case i == 1073741824: 35 | return _SizeB1024_name_3 36 | case i == 1099511627776: 37 | return _SizeB1024_name_4 38 | default: 39 | return "SizeB1024(" + strconv.FormatInt(int64(i), 10) + ")" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /decor/speed.go: -------------------------------------------------------------------------------- 1 | package decor 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "math" 7 | "time" 8 | 9 | "github.com/VividCortex/ewma" 10 | ) 11 | 12 | var ( 13 | _ Decorator = (*movingAverageSpeed)(nil) 14 | _ EwmaDecorator = (*movingAverageSpeed)(nil) 15 | _ Decorator = (*averageSpeed)(nil) 16 | _ AverageDecorator = (*averageSpeed)(nil) 17 | ) 18 | 19 | // FmtAsSpeed adds "/s" to the end of the input formatter. To be 20 | // used with SizeB1000 or SizeB1024 types, for example: 21 | // 22 | // fmt.Printf("%.1f", FmtAsSpeed(SizeB1024(2048))) 23 | func FmtAsSpeed(input fmt.Formatter) fmt.Formatter { 24 | return &speedFormatter{input} 25 | } 26 | 27 | type speedFormatter struct { 28 | fmt.Formatter 29 | } 30 | 31 | func (s *speedFormatter) Format(st fmt.State, verb rune) { 32 | s.Formatter.Format(st, verb) 33 | _, err := io.WriteString(st, "/s") 34 | if err != nil { 35 | panic(err) 36 | } 37 | } 38 | 39 | // EwmaSpeed exponential-weighted-moving-average based speed decorator. 40 | // For this decorator to work correctly you have to measure each iteration's 41 | // duration and pass it to one of the (*Bar).EwmaIncr... family methods. 42 | func EwmaSpeed(unit interface{}, format string, age float64, wcc ...WC) Decorator { 43 | var average ewma.MovingAverage 44 | if age == 0 { 45 | average = ewma.NewMovingAverage() 46 | } else { 47 | average = ewma.NewMovingAverage(age) 48 | } 49 | return MovingAverageSpeed(unit, format, average, wcc...) 50 | } 51 | 52 | // MovingAverageSpeed decorator relies on MovingAverage implementation 53 | // to calculate its average. 54 | // 55 | // `unit` one of [0|SizeB1024(0)|SizeB1000(0)] 56 | // 57 | // `format` printf compatible verb for value, like "%f" or "%d" 58 | // 59 | // `average` MovingAverage implementation 60 | // 61 | // `wcc` optional WC config 62 | // 63 | // format examples: 64 | // 65 | // unit=SizeB1024(0), format="%.1f" output: "1.0MiB/s" 66 | // unit=SizeB1024(0), format="% .1f" output: "1.0 MiB/s" 67 | // unit=SizeB1000(0), format="%.1f" output: "1.0MB/s" 68 | // unit=SizeB1000(0), format="% .1f" output: "1.0 MB/s" 69 | func MovingAverageSpeed(unit interface{}, format string, average ewma.MovingAverage, wcc ...WC) Decorator { 70 | d := &movingAverageSpeed{ 71 | WC: initWC(wcc...), 72 | producer: chooseSpeedProducer(unit, format), 73 | average: average, 74 | } 75 | return d 76 | } 77 | 78 | type movingAverageSpeed struct { 79 | WC 80 | producer func(float64) string 81 | average ewma.MovingAverage 82 | zDur time.Duration 83 | } 84 | 85 | func (d *movingAverageSpeed) Decor(_ Statistics) (string, int) { 86 | var str string 87 | // ewma implementation may return 0 before accumulating certain number of samples 88 | if v := d.average.Value(); v != 0 { 89 | str = d.producer(1e9 / v) 90 | } else { 91 | str = d.producer(0) 92 | } 93 | return d.Format(str) 94 | } 95 | 96 | func (d *movingAverageSpeed) EwmaUpdate(n int64, dur time.Duration) { 97 | if n <= 0 { 98 | d.zDur += dur 99 | return 100 | } 101 | durPerByte := float64(d.zDur+dur) / float64(n) 102 | if math.IsInf(durPerByte, 0) || math.IsNaN(durPerByte) { 103 | d.zDur += dur 104 | return 105 | } 106 | d.zDur = 0 107 | d.average.Add(durPerByte) 108 | } 109 | 110 | // AverageSpeed decorator with dynamic unit measure adjustment. It's 111 | // a wrapper of NewAverageSpeed. 112 | func AverageSpeed(unit interface{}, format string, wcc ...WC) Decorator { 113 | return NewAverageSpeed(unit, format, time.Now(), wcc...) 114 | } 115 | 116 | // NewAverageSpeed decorator with dynamic unit measure adjustment and 117 | // user provided start time. 118 | // 119 | // `unit` one of [0|SizeB1024(0)|SizeB1000(0)] 120 | // 121 | // `format` printf compatible verb for value, like "%f" or "%d" 122 | // 123 | // `start` start time 124 | // 125 | // `wcc` optional WC config 126 | // 127 | // format examples: 128 | // 129 | // unit=SizeB1024(0), format="%.1f" output: "1.0MiB/s" 130 | // unit=SizeB1024(0), format="% .1f" output: "1.0 MiB/s" 131 | // unit=SizeB1000(0), format="%.1f" output: "1.0MB/s" 132 | // unit=SizeB1000(0), format="% .1f" output: "1.0 MB/s" 133 | func NewAverageSpeed(unit interface{}, format string, start time.Time, wcc ...WC) Decorator { 134 | d := &averageSpeed{ 135 | WC: initWC(wcc...), 136 | start: start, 137 | producer: chooseSpeedProducer(unit, format), 138 | } 139 | return d 140 | } 141 | 142 | type averageSpeed struct { 143 | WC 144 | start time.Time 145 | producer func(float64) string 146 | msg string 147 | } 148 | 149 | func (d *averageSpeed) Decor(s Statistics) (string, int) { 150 | if !s.Completed { 151 | speed := float64(s.Current) / float64(time.Since(d.start)) 152 | d.msg = d.producer(speed * 1e9) 153 | } 154 | return d.Format(d.msg) 155 | } 156 | 157 | func (d *averageSpeed) AverageAdjust(start time.Time) { 158 | d.start = start 159 | } 160 | 161 | func chooseSpeedProducer(unit interface{}, format string) func(float64) string { 162 | switch unit.(type) { 163 | case SizeB1024: 164 | if format == "" { 165 | format = "% d" 166 | } 167 | return func(speed float64) string { 168 | return fmt.Sprintf(format, FmtAsSpeed(SizeB1024(math.Round(speed)))) 169 | } 170 | case SizeB1000: 171 | if format == "" { 172 | format = "% d" 173 | } 174 | return func(speed float64) string { 175 | return fmt.Sprintf(format, FmtAsSpeed(SizeB1000(math.Round(speed)))) 176 | } 177 | default: 178 | if format == "" { 179 | format = "%f" 180 | } 181 | return func(speed float64) string { 182 | return fmt.Sprintf(format, speed) 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /decor/speed_test.go: -------------------------------------------------------------------------------- 1 | package decor 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestAverageSpeedSizeB1024(t *testing.T) { 9 | cases := []struct { 10 | name string 11 | fmt string 12 | unit interface{} 13 | current int64 14 | elapsed time.Duration 15 | expected string 16 | }{ 17 | { 18 | name: "empty fmt", 19 | unit: SizeB1024(0), 20 | fmt: "", 21 | current: 0, 22 | elapsed: time.Second, 23 | expected: "0 b/s", 24 | }, 25 | { 26 | name: "SizeB1024(0):%d:0b", 27 | unit: SizeB1024(0), 28 | fmt: "%d", 29 | current: 0, 30 | elapsed: time.Second, 31 | expected: "0b/s", 32 | }, 33 | { 34 | name: "SizeB1024(0):%f:0b", 35 | unit: SizeB1024(0), 36 | fmt: "%f", 37 | current: 0, 38 | elapsed: time.Second, 39 | expected: "0.000000b/s", 40 | }, 41 | { 42 | name: "SizeB1024(0):% .2f:0b", 43 | unit: SizeB1024(0), 44 | fmt: "% .2f", 45 | current: 0, 46 | elapsed: time.Second, 47 | expected: "0.00 b/s", 48 | }, 49 | { 50 | name: "SizeB1024(0):%d:1b", 51 | unit: SizeB1024(0), 52 | fmt: "%d", 53 | current: 1, 54 | elapsed: time.Second, 55 | expected: "1b/s", 56 | }, 57 | { 58 | name: "SizeB1024(0):% .2f:1b", 59 | unit: SizeB1024(0), 60 | fmt: "% .2f", 61 | current: 1, 62 | elapsed: time.Second, 63 | expected: "1.00 b/s", 64 | }, 65 | { 66 | name: "SizeB1024(0):%d:KiB", 67 | unit: SizeB1024(0), 68 | fmt: "%d", 69 | current: 2 * int64(_iKiB), 70 | elapsed: 1 * time.Second, 71 | expected: "2KiB/s", 72 | }, 73 | { 74 | name: "SizeB1024(0):% .f:KiB", 75 | unit: SizeB1024(0), 76 | fmt: "% .2f", 77 | current: 2 * int64(_iKiB), 78 | elapsed: 1 * time.Second, 79 | expected: "2.00 KiB/s", 80 | }, 81 | { 82 | name: "SizeB1024(0):%d:MiB", 83 | unit: SizeB1024(0), 84 | fmt: "%d", 85 | current: 2 * int64(_iMiB), 86 | elapsed: 1 * time.Second, 87 | expected: "2MiB/s", 88 | }, 89 | { 90 | name: "SizeB1024(0):% .2f:MiB", 91 | unit: SizeB1024(0), 92 | fmt: "% .2f", 93 | current: 2 * int64(_iMiB), 94 | elapsed: 1 * time.Second, 95 | expected: "2.00 MiB/s", 96 | }, 97 | { 98 | name: "SizeB1024(0):%d:GiB", 99 | unit: SizeB1024(0), 100 | fmt: "%d", 101 | current: 2 * int64(_iGiB), 102 | elapsed: 1 * time.Second, 103 | expected: "2GiB/s", 104 | }, 105 | { 106 | name: "SizeB1024(0):% .2f:GiB", 107 | unit: SizeB1024(0), 108 | fmt: "% .2f", 109 | current: 2 * int64(_iGiB), 110 | elapsed: 1 * time.Second, 111 | expected: "2.00 GiB/s", 112 | }, 113 | { 114 | name: "SizeB1024(0):%d:TiB", 115 | unit: SizeB1024(0), 116 | fmt: "%d", 117 | current: 2 * int64(_iTiB), 118 | elapsed: 1 * time.Second, 119 | expected: "2TiB/s", 120 | }, 121 | { 122 | name: "SizeB1024(0):% .2f:TiB", 123 | unit: SizeB1024(0), 124 | fmt: "% .2f", 125 | current: 2 * int64(_iTiB), 126 | elapsed: 1 * time.Second, 127 | expected: "2.00 TiB/s", 128 | }, 129 | } 130 | for _, tc := range cases { 131 | t.Run(tc.name, func(t *testing.T) { 132 | decor := NewAverageSpeed(tc.unit, tc.fmt, time.Now().Add(-tc.elapsed)) 133 | stat := Statistics{ 134 | Current: tc.current, 135 | } 136 | res, _ := decor.Decor(stat) 137 | if res != tc.expected { 138 | t.Fatalf("expected: %q, got: %q\n", tc.expected, res) 139 | } 140 | }) 141 | } 142 | } 143 | 144 | func TestAverageSpeedSizeB1000(t *testing.T) { 145 | cases := []struct { 146 | name string 147 | fmt string 148 | unit interface{} 149 | current int64 150 | elapsed time.Duration 151 | expected string 152 | }{ 153 | { 154 | name: "empty fmt", 155 | unit: SizeB1000(0), 156 | fmt: "", 157 | current: 0, 158 | elapsed: time.Second, 159 | expected: "0 b/s", 160 | }, 161 | { 162 | name: "SizeB1000(0):%d:0b", 163 | unit: SizeB1000(0), 164 | fmt: "%d", 165 | current: 0, 166 | elapsed: time.Second, 167 | expected: "0b/s", 168 | }, 169 | { 170 | name: "SizeB1000(0):%f:0b", 171 | unit: SizeB1000(0), 172 | fmt: "%f", 173 | current: 0, 174 | elapsed: time.Second, 175 | expected: "0.000000b/s", 176 | }, 177 | { 178 | name: "SizeB1000(0):% .2f:0b", 179 | unit: SizeB1000(0), 180 | fmt: "% .2f", 181 | current: 0, 182 | elapsed: time.Second, 183 | expected: "0.00 b/s", 184 | }, 185 | { 186 | name: "SizeB1000(0):%d:1b", 187 | unit: SizeB1000(0), 188 | fmt: "%d", 189 | current: 1, 190 | elapsed: time.Second, 191 | expected: "1b/s", 192 | }, 193 | { 194 | name: "SizeB1000(0):% .2f:1b", 195 | unit: SizeB1000(0), 196 | fmt: "% .2f", 197 | current: 1, 198 | elapsed: time.Second, 199 | expected: "1.00 b/s", 200 | }, 201 | { 202 | name: "SizeB1000(0):%d:KB", 203 | unit: SizeB1000(0), 204 | fmt: "%d", 205 | current: 2 * int64(_KB), 206 | elapsed: 1 * time.Second, 207 | expected: "2KB/s", 208 | }, 209 | { 210 | name: "SizeB1000(0):% .f:KB", 211 | unit: SizeB1000(0), 212 | fmt: "% .2f", 213 | current: 2 * int64(_KB), 214 | elapsed: 1 * time.Second, 215 | expected: "2.00 KB/s", 216 | }, 217 | { 218 | name: "SizeB1000(0):%d:MB", 219 | unit: SizeB1000(0), 220 | fmt: "%d", 221 | current: 2 * int64(_MB), 222 | elapsed: 1 * time.Second, 223 | expected: "2MB/s", 224 | }, 225 | { 226 | name: "SizeB1000(0):% .2f:MB", 227 | unit: SizeB1000(0), 228 | fmt: "% .2f", 229 | current: 2 * int64(_MB), 230 | elapsed: 1 * time.Second, 231 | expected: "2.00 MB/s", 232 | }, 233 | { 234 | name: "SizeB1000(0):%d:GB", 235 | unit: SizeB1000(0), 236 | fmt: "%d", 237 | current: 2 * int64(_GB), 238 | elapsed: 1 * time.Second, 239 | expected: "2GB/s", 240 | }, 241 | { 242 | name: "SizeB1000(0):% .2f:GB", 243 | unit: SizeB1000(0), 244 | fmt: "% .2f", 245 | current: 2 * int64(_GB), 246 | elapsed: 1 * time.Second, 247 | expected: "2.00 GB/s", 248 | }, 249 | { 250 | name: "SizeB1000(0):%d:TB", 251 | unit: SizeB1000(0), 252 | fmt: "%d", 253 | current: 2 * int64(_TB), 254 | elapsed: 1 * time.Second, 255 | expected: "2TB/s", 256 | }, 257 | { 258 | name: "SizeB1000(0):% .2f:TB", 259 | unit: SizeB1000(0), 260 | fmt: "% .2f", 261 | current: 2 * int64(_TB), 262 | elapsed: 1 * time.Second, 263 | expected: "2.00 TB/s", 264 | }, 265 | } 266 | for _, tc := range cases { 267 | t.Run(tc.name, func(t *testing.T) { 268 | decor := NewAverageSpeed(tc.unit, tc.fmt, time.Now().Add(-tc.elapsed)) 269 | stat := Statistics{ 270 | Current: tc.current, 271 | } 272 | res, _ := decor.Decor(stat) 273 | if res != tc.expected { 274 | t.Fatalf("expected: %q, got: %q\n", tc.expected, res) 275 | } 276 | }) 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /decor/spinner.go: -------------------------------------------------------------------------------- 1 | package decor 2 | 3 | var defaultSpinnerStyle = [...]string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} 4 | 5 | // Spinner returns spinner decorator. 6 | // 7 | // `frames` spinner frames, if nil or len==0, default is used 8 | // 9 | // `wcc` optional WC config 10 | func Spinner(frames []string, wcc ...WC) Decorator { 11 | if len(frames) == 0 { 12 | frames = defaultSpinnerStyle[:] 13 | } 14 | var count uint 15 | f := func(s Statistics) string { 16 | frame := frames[count%uint(len(frames))] 17 | count++ 18 | return frame 19 | } 20 | return Any(f, wcc...) 21 | } 22 | -------------------------------------------------------------------------------- /decorators_test.go: -------------------------------------------------------------------------------- 1 | package mpb_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/vbauerster/mpb/v8" 7 | "github.com/vbauerster/mpb/v8/decor" 8 | ) 9 | 10 | func TestNameDecorator(t *testing.T) { 11 | tests := []struct { 12 | decorator decor.Decorator 13 | want string 14 | }{ 15 | { 16 | decorator: decor.Name("Test"), 17 | want: "Test", 18 | }, 19 | { 20 | decorator: decor.Name("Test", decor.WC{W: len("Test")}), 21 | want: "Test", 22 | }, 23 | { 24 | decorator: decor.Name("Test", decor.WC{W: 10}), 25 | want: " Test", 26 | }, 27 | { 28 | decorator: decor.Name("Test", decor.WC{W: 10, C: decor.DindentRight}), 29 | want: "Test ", 30 | }, 31 | } 32 | 33 | for _, test := range tests { 34 | got, _ := test.decorator.Decor(decor.Statistics{}) 35 | if got != test.want { 36 | t.Errorf("Want: %q, Got: %q\n", test.want, got) 37 | } 38 | } 39 | } 40 | 41 | type step struct { 42 | stat decor.Statistics 43 | decorator decor.Decorator 44 | want string 45 | } 46 | 47 | func TestPercentageDwidthSync(t *testing.T) { 48 | 49 | testCases := [][]step{ 50 | { 51 | { 52 | decor.Statistics{Total: 100, Current: 8}, 53 | decor.Percentage(decor.WCSyncWidth), 54 | "8 %", 55 | }, 56 | { 57 | decor.Statistics{Total: 100, Current: 9}, 58 | decor.Percentage(decor.WCSyncWidth), 59 | "9 %", 60 | }, 61 | }, 62 | { 63 | { 64 | decor.Statistics{Total: 100, Current: 9}, 65 | decor.Percentage(decor.WCSyncWidth), 66 | " 9 %", 67 | }, 68 | { 69 | decor.Statistics{Total: 100, Current: 10}, 70 | decor.Percentage(decor.WCSyncWidth), 71 | "10 %", 72 | }, 73 | }, 74 | { 75 | { 76 | decor.Statistics{Total: 100, Current: 9}, 77 | decor.Percentage(decor.WCSyncWidth), 78 | " 9 %", 79 | }, 80 | { 81 | decor.Statistics{Total: 100, Current: 100}, 82 | decor.Percentage(decor.WCSyncWidth), 83 | "100 %", 84 | }, 85 | }, 86 | } 87 | 88 | testDecoratorConcurrently(t, testCases) 89 | } 90 | 91 | func TestPercentageDwidthSyncDindentRight(t *testing.T) { 92 | 93 | testCases := [][]step{ 94 | { 95 | { 96 | decor.Statistics{Total: 100, Current: 8}, 97 | decor.Percentage(decor.WCSyncWidthR), 98 | "8 %", 99 | }, 100 | { 101 | decor.Statistics{Total: 100, Current: 9}, 102 | decor.Percentage(decor.WCSyncWidthR), 103 | "9 %", 104 | }, 105 | }, 106 | { 107 | { 108 | decor.Statistics{Total: 100, Current: 9}, 109 | decor.Percentage(decor.WCSyncWidthR), 110 | "9 % ", 111 | }, 112 | { 113 | decor.Statistics{Total: 100, Current: 10}, 114 | decor.Percentage(decor.WCSyncWidthR), 115 | "10 %", 116 | }, 117 | }, 118 | { 119 | { 120 | decor.Statistics{Total: 100, Current: 9}, 121 | decor.Percentage(decor.WCSyncWidthR), 122 | "9 % ", 123 | }, 124 | { 125 | decor.Statistics{Total: 100, Current: 100}, 126 | decor.Percentage(decor.WCSyncWidthR), 127 | "100 %", 128 | }, 129 | }, 130 | } 131 | 132 | testDecoratorConcurrently(t, testCases) 133 | } 134 | 135 | func TestPercentageDSyncSpace(t *testing.T) { 136 | 137 | testCases := [][]step{ 138 | { 139 | { 140 | decor.Statistics{Total: 100, Current: 8}, 141 | decor.Percentage(decor.WCSyncSpace), 142 | " 8 %", 143 | }, 144 | { 145 | decor.Statistics{Total: 100, Current: 9}, 146 | decor.Percentage(decor.WCSyncSpace), 147 | " 9 %", 148 | }, 149 | }, 150 | { 151 | { 152 | decor.Statistics{Total: 100, Current: 9}, 153 | decor.Percentage(decor.WCSyncSpace), 154 | " 9 %", 155 | }, 156 | { 157 | decor.Statistics{Total: 100, Current: 10}, 158 | decor.Percentage(decor.WCSyncSpace), 159 | " 10 %", 160 | }, 161 | }, 162 | { 163 | { 164 | decor.Statistics{Total: 100, Current: 9}, 165 | decor.Percentage(decor.WCSyncSpace), 166 | " 9 %", 167 | }, 168 | { 169 | decor.Statistics{Total: 100, Current: 100}, 170 | decor.Percentage(decor.WCSyncSpace), 171 | " 100 %", 172 | }, 173 | }, 174 | } 175 | 176 | testDecoratorConcurrently(t, testCases) 177 | } 178 | 179 | func testDecoratorConcurrently(t *testing.T, testCases [][]step) { 180 | if len(testCases) == 0 { 181 | t.Fail() 182 | } 183 | 184 | for _, columnCase := range testCases { 185 | mpb.SyncWidth(toSyncMatrix(columnCase), nil) 186 | var results []chan string 187 | for _, step := range columnCase { 188 | step := step 189 | ch := make(chan string) 190 | go func() { 191 | str, _ := step.decorator.Decor(step.stat) 192 | ch <- str 193 | }() 194 | results = append(results, ch) 195 | } 196 | 197 | for i, ch := range results { 198 | res := <-ch 199 | want := columnCase[i].want 200 | if res != want { 201 | t.Errorf("Want: %q, Got: %q\n", want, res) 202 | } 203 | } 204 | } 205 | } 206 | 207 | func toSyncMatrix(ss []step) map[int][]chan int { 208 | var column []chan int 209 | for _, s := range ss { 210 | if ch, ok := s.decorator.Sync(); ok { 211 | column = append(column, ch) 212 | } 213 | } 214 | return map[int][]chan int{0: column} 215 | } 216 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package mpb is a library for rendering progress bars in terminal applications. 2 | package mpb 3 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package mpb_test 2 | 3 | import ( 4 | crand "crypto/rand" 5 | "io" 6 | "math/rand" 7 | "time" 8 | 9 | "github.com/vbauerster/mpb/v8" 10 | "github.com/vbauerster/mpb/v8/decor" 11 | ) 12 | 13 | func Example() { 14 | // initialize progress container, with custom width 15 | p := mpb.New(mpb.WithWidth(64)) 16 | 17 | total := 100 18 | name := "Single Bar:" 19 | // create a single bar, which will inherit container's width 20 | bar := p.New(int64(total), 21 | // BarFillerBuilder with custom style 22 | mpb.BarStyle().Lbound("╢").Filler("▌").Tip("▌").Padding("░").Rbound("╟"), 23 | mpb.PrependDecorators( 24 | // display our name with one space on the right 25 | decor.Name(name, decor.WC{C: decor.DindentRight | decor.DextraSpace}), 26 | // replace ETA decorator with "done" message, OnComplete event 27 | decor.OnComplete(decor.AverageETA(decor.ET_STYLE_GO), "done"), 28 | ), 29 | mpb.AppendDecorators(decor.Percentage()), 30 | ) 31 | // simulating some work 32 | max := 100 * time.Millisecond 33 | for i := 0; i < total; i++ { 34 | time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) 35 | bar.Increment() 36 | } 37 | // wait for our bar to complete and flush 38 | p.Wait() 39 | } 40 | 41 | func ExampleBar_Completed() { 42 | p := mpb.New() 43 | bar := p.AddBar(100) 44 | 45 | max := 100 * time.Millisecond 46 | for !bar.Completed() { 47 | time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) 48 | bar.Increment() 49 | } 50 | 51 | p.Wait() 52 | } 53 | 54 | func ExampleBar_ProxyReader() { 55 | // import crand "crypto/rand" 56 | 57 | var total int64 = 1024 * 1024 * 500 58 | reader := io.LimitReader(crand.Reader, total) 59 | 60 | p := mpb.New() 61 | bar := p.AddBar(total, 62 | mpb.AppendDecorators( 63 | decor.CountersKibiByte("% .2f / % .2f"), 64 | ), 65 | ) 66 | 67 | // create proxy reader 68 | proxyReader := bar.ProxyReader(reader) 69 | defer func() { 70 | _ = proxyReader.Close() 71 | }() 72 | 73 | // and copy from reader, ignoring errors 74 | _, _ = io.Copy(io.Discard, proxyReader) 75 | 76 | p.Wait() 77 | } 78 | -------------------------------------------------------------------------------- /export_test.go: -------------------------------------------------------------------------------- 1 | package mpb 2 | 3 | // make syncWidth func public in test 4 | var SyncWidth = syncWidth 5 | 6 | type PriorityQueue = priorityQueue 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vbauerster/mpb/v8 2 | 3 | require ( 4 | github.com/VividCortex/ewma v1.2.0 5 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d 6 | github.com/mattn/go-runewidth v0.0.16 7 | golang.org/x/sys v0.33.0 8 | ) 9 | 10 | require ( 11 | github.com/rivo/uniseg v0.4.7 // indirect 12 | golang.org/x/mod v0.24.0 // indirect 13 | golang.org/x/sync v0.13.0 // indirect 14 | golang.org/x/tools v0.32.0 // indirect 15 | ) 16 | 17 | go 1.23.0 18 | 19 | toolchain go1.24.2 20 | 21 | tool golang.org/x/tools/cmd/stringer 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= 2 | github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= 3 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= 4 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= 5 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 6 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 7 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 8 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 9 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 10 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 11 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 12 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 13 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 14 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 15 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 16 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 17 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 18 | golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= 19 | golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= 20 | -------------------------------------------------------------------------------- /heap_manager.go: -------------------------------------------------------------------------------- 1 | package mpb 2 | 3 | import "container/heap" 4 | 5 | type heapManager chan heapRequest 6 | 7 | type heapCmd int 8 | 9 | const ( 10 | h_sync heapCmd = iota 11 | h_push 12 | h_iter 13 | h_fix 14 | h_state 15 | h_end 16 | ) 17 | 18 | type heapRequest struct { 19 | cmd heapCmd 20 | data interface{} 21 | } 22 | 23 | type iterData struct { 24 | drop <-chan struct{} 25 | iter chan<- *Bar 26 | iterPop chan<- *Bar 27 | } 28 | 29 | type pushData struct { 30 | bar *Bar 31 | sync bool 32 | } 33 | 34 | type fixData struct { 35 | bar *Bar 36 | priority int 37 | lazy bool 38 | } 39 | 40 | func (m heapManager) run() { 41 | var bHeap priorityQueue 42 | var pMatrix, aMatrix map[int][]chan int 43 | 44 | var l int 45 | var sync bool 46 | 47 | for req := range m { 48 | switch req.cmd { 49 | case h_push: 50 | data := req.data.(pushData) 51 | heap.Push(&bHeap, data.bar) 52 | sync = sync || data.sync 53 | case h_sync: 54 | if sync || l != bHeap.Len() { 55 | pMatrix = make(map[int][]chan int) 56 | aMatrix = make(map[int][]chan int) 57 | for _, b := range bHeap { 58 | table := b.wSyncTable() 59 | for i, ch := range table[0] { 60 | pMatrix[i] = append(pMatrix[i], ch) 61 | } 62 | for i, ch := range table[1] { 63 | aMatrix[i] = append(aMatrix[i], ch) 64 | } 65 | } 66 | sync = false 67 | l = bHeap.Len() 68 | } 69 | drop := req.data.(<-chan struct{}) 70 | syncWidth(pMatrix, drop) 71 | syncWidth(aMatrix, drop) 72 | case h_iter: 73 | data := req.data.(iterData) 74 | loop: // unordered iteration 75 | for _, b := range bHeap { 76 | select { 77 | case data.iter <- b: 78 | case <-data.drop: 79 | data.iterPop = nil 80 | break loop 81 | } 82 | } 83 | close(data.iter) 84 | if data.iterPop == nil { 85 | break 86 | } 87 | loop_pop: // ordered iteration 88 | for bHeap.Len() != 0 { 89 | bar := heap.Pop(&bHeap).(*Bar) 90 | select { 91 | case data.iterPop <- bar: 92 | case <-data.drop: 93 | heap.Push(&bHeap, bar) 94 | break loop_pop 95 | } 96 | } 97 | close(data.iterPop) 98 | case h_fix: 99 | data := req.data.(fixData) 100 | if data.bar.index < 0 { 101 | break 102 | } 103 | data.bar.priority = data.priority 104 | if !data.lazy { 105 | heap.Fix(&bHeap, data.bar.index) 106 | } 107 | case h_state: 108 | ch := req.data.(chan<- bool) 109 | ch <- sync || l != bHeap.Len() 110 | case h_end: 111 | ch := req.data.(chan<- interface{}) 112 | if ch != nil { 113 | go func() { 114 | ch <- []*Bar(bHeap) 115 | }() 116 | } 117 | close(m) 118 | } 119 | } 120 | } 121 | 122 | func (m heapManager) sync(drop <-chan struct{}) { 123 | m <- heapRequest{cmd: h_sync, data: drop} 124 | } 125 | 126 | func (m heapManager) push(b *Bar, sync bool) { 127 | data := pushData{b, sync} 128 | req := heapRequest{cmd: h_push, data: data} 129 | select { 130 | case m <- req: 131 | default: 132 | go func() { 133 | m <- req 134 | }() 135 | } 136 | } 137 | 138 | func (m heapManager) iter(drop <-chan struct{}, iter, iterPop chan<- *Bar) { 139 | data := iterData{drop, iter, iterPop} 140 | m <- heapRequest{cmd: h_iter, data: data} 141 | } 142 | 143 | func (m heapManager) fix(b *Bar, priority int, lazy bool) { 144 | data := fixData{b, priority, lazy} 145 | m <- heapRequest{cmd: h_fix, data: data} 146 | } 147 | 148 | func (m heapManager) state(ch chan<- bool) { 149 | m <- heapRequest{cmd: h_state, data: ch} 150 | } 151 | 152 | func (m heapManager) end(ch chan<- interface{}) { 153 | m <- heapRequest{cmd: h_end, data: ch} 154 | } 155 | 156 | func syncWidth(matrix map[int][]chan int, drop <-chan struct{}) { 157 | for _, column := range matrix { 158 | go maxWidthDistributor(column, drop) 159 | } 160 | } 161 | 162 | func maxWidthDistributor(column []chan int, drop <-chan struct{}) { 163 | var maxWidth int 164 | for _, ch := range column { 165 | select { 166 | case w := <-ch: 167 | if w > maxWidth { 168 | maxWidth = w 169 | } 170 | case <-drop: 171 | return 172 | } 173 | } 174 | for _, ch := range column { 175 | ch <- maxWidth 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /internal/percentage.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "math" 4 | 5 | // Percentage is a helper function, to calculate percentage. 6 | func Percentage(total, current, width uint) float64 { 7 | if total == 0 { 8 | return 0 9 | } 10 | if current >= total { 11 | return float64(width) 12 | } 13 | return float64(width*current) / float64(total) 14 | } 15 | 16 | // PercentageRound same as Percentage but with math.Round. 17 | func PercentageRound(total, current int64, width uint) float64 { 18 | if total < 0 || current < 0 { 19 | return 0 20 | } 21 | return math.Round(Percentage(uint(total), uint(current), width)) 22 | } 23 | -------------------------------------------------------------------------------- /internal/percentage_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "testing" 4 | 5 | func TestPercentage(t *testing.T) { 6 | // key is barWidth 7 | testSuite := map[uint][]struct { 8 | name string 9 | total int64 10 | current int64 11 | expected int64 12 | }{ 13 | 100: { 14 | {"t,c,e{-1,-1,0}", -1, -1, 0}, 15 | {"t,c,e{0,-1,0}", 0, -1, 0}, 16 | {"t,c,e{0,0,0}", 0, 0, 0}, 17 | {"t,c,e{0,1,0}", 0, 1, 0}, 18 | {"t,c,e{100,0,0}", 100, 0, 0}, 19 | {"t,c,e{100,10,10}", 100, 10, 10}, 20 | {"t,c,e{100,15,15}", 100, 15, 15}, 21 | {"t,c,e{100,50,50}", 100, 50, 50}, 22 | {"t,c,e{100,99,99}", 100, 99, 99}, 23 | {"t,c,e{100,100,100}", 100, 100, 100}, 24 | {"t,c,e{100,101,101}", 100, 101, 100}, 25 | {"t,c,e{120,0,0}", 120, 0, 0}, 26 | {"t,c,e{120,10,8}", 120, 10, 8}, 27 | {"t,c,e{120,15,13}", 120, 15, 13}, 28 | {"t,c,e{120,50,42}", 120, 50, 42}, 29 | {"t,c,e{120,60,50}", 120, 60, 50}, 30 | {"t,c,e{120,99,83}", 120, 99, 83}, 31 | {"t,c,e{120,101,84}", 120, 101, 84}, 32 | {"t,c,e{120,118,98}", 120, 118, 98}, 33 | {"t,c,e{120,119,99}", 120, 119, 99}, 34 | {"t,c,e{120,120,100}", 120, 120, 100}, 35 | {"t,c,e{120,121,101}", 120, 121, 100}, 36 | }, 37 | 80: { 38 | {"t,c,e{-1,-1,0}", -1, -1, 0}, 39 | {"t,c,e{0,-1,0}", 0, -1, 0}, 40 | {"t,c,e{0,0,0}", 0, 0, 0}, 41 | {"t,c,e{0,1,0}", 0, 1, 0}, 42 | {"t,c,e{100,0,0}", 100, 0, 0}, 43 | {"t,c,e{100,10,8}", 100, 10, 8}, 44 | {"t,c,e{100,15,12}", 100, 15, 12}, 45 | {"t,c,e{100,50,40}", 100, 50, 40}, 46 | {"t,c,e{100,99,79}", 100, 99, 79}, 47 | {"t,c,e{100,100,80}", 100, 100, 80}, 48 | {"t,c,e{100,101,81}", 100, 101, 80}, 49 | {"t,c,e{120,0,0}", 120, 0, 0}, 50 | {"t,c,e{120,10,7}", 120, 10, 7}, 51 | {"t,c,e{120,15,10}", 120, 15, 10}, 52 | {"t,c,e{120,50,33}", 120, 50, 33}, 53 | {"t,c,e{120,60,40}", 120, 60, 40}, 54 | {"t,c,e{120,99,66}", 120, 99, 66}, 55 | {"t,c,e{120,101,67}", 120, 101, 67}, 56 | {"t,c,e{120,118,79}", 120, 118, 79}, 57 | {"t,c,e{120,119,79}", 120, 119, 79}, 58 | {"t,c,e{120,120,80}", 120, 120, 80}, 59 | {"t,c,e{120,121,81}", 120, 121, 80}, 60 | }, 61 | } 62 | 63 | for width, cases := range testSuite { 64 | for _, tc := range cases { 65 | got := int64(PercentageRound(tc.total, tc.current, width)) 66 | if got != tc.expected { 67 | t.Errorf("width %d; %s: Expected: %d, got: %d\n", width, tc.name, tc.expected, got) 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/width.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | // CheckRequestedWidth checks that requested width doesn't overflow 4 | // available width 5 | func CheckRequestedWidth(requested, available int) int { 6 | if requested < 1 || requested > available { 7 | return available 8 | } 9 | return requested 10 | } 11 | -------------------------------------------------------------------------------- /priority_queue.go: -------------------------------------------------------------------------------- 1 | package mpb 2 | 3 | import "container/heap" 4 | 5 | var _ heap.Interface = (*priorityQueue)(nil) 6 | 7 | type priorityQueue []*Bar 8 | 9 | func (pq priorityQueue) Len() int { return len(pq) } 10 | 11 | func (pq priorityQueue) Less(i, j int) bool { 12 | // greater priority pops first 13 | return pq[i].priority > pq[j].priority 14 | } 15 | 16 | func (pq priorityQueue) Swap(i, j int) { 17 | pq[i], pq[j] = pq[j], pq[i] 18 | pq[i].index = i 19 | pq[j].index = j 20 | } 21 | 22 | func (pq *priorityQueue) Push(x interface{}) { 23 | s := *pq 24 | b := x.(*Bar) 25 | b.index = len(s) 26 | *pq = append(s, b) 27 | } 28 | 29 | func (pq *priorityQueue) Pop() interface{} { 30 | var b *Bar 31 | s := *pq 32 | i := len(s) - 1 33 | b, s[i] = s[i], nil // nil to avoid memory leak 34 | b.index = -1 // for safety 35 | *pq = s[:i] 36 | return b 37 | } 38 | -------------------------------------------------------------------------------- /progress.go: -------------------------------------------------------------------------------- 1 | package mpb 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "math" 9 | "os" 10 | "sync" 11 | "time" 12 | 13 | "github.com/vbauerster/mpb/v8/cwriter" 14 | "github.com/vbauerster/mpb/v8/decor" 15 | ) 16 | 17 | const defaultRefreshRate = 150 * time.Millisecond 18 | const defaultHmQueueLength = 128 19 | 20 | // ErrDone represents use after `(*Progress).Wait()` error. 21 | var ErrDone = fmt.Errorf("%T instance can't be reused after %[1]T.Wait()", (*Progress)(nil)) 22 | 23 | // Progress represents a container that renders one or more progress bars. 24 | type Progress struct { 25 | uwg *sync.WaitGroup 26 | pwg, bwg sync.WaitGroup 27 | operateState chan func(*pState) 28 | interceptIO chan func(io.Writer) 29 | done <-chan struct{} 30 | cancel func() 31 | } 32 | 33 | // pState holds bars in its priorityQueue, it gets passed to (*Progress).serve monitor goroutine. 34 | type pState struct { 35 | ctx context.Context 36 | hm heapManager 37 | iterDrop chan struct{} 38 | renderReq chan time.Time 39 | idCount int 40 | popPriority int 41 | 42 | // following are provided/overrode by user 43 | hmQueueLen int 44 | reqWidth int 45 | refreshRate time.Duration 46 | popCompleted bool 47 | autoRefresh bool 48 | delayRC <-chan struct{} 49 | manualRC <-chan interface{} 50 | shutdownNotifier chan<- interface{} 51 | queueBars map[*Bar]*Bar 52 | output io.Writer 53 | debugOut io.Writer 54 | uwg *sync.WaitGroup 55 | } 56 | 57 | // New creates new Progress container instance. It's not possible to 58 | // reuse instance after `(*Progress).Wait` method has been called. 59 | func New(options ...ContainerOption) *Progress { 60 | return NewWithContext(context.Background(), options...) 61 | } 62 | 63 | // NewWithContext creates new Progress container instance with provided 64 | // context. It's not possible to reuse instance after `(*Progress).Wait` 65 | // method has been called. 66 | func NewWithContext(ctx context.Context, options ...ContainerOption) *Progress { 67 | if ctx == nil { 68 | ctx = context.Background() 69 | } 70 | ctx, cancel := context.WithCancel(ctx) 71 | s := &pState{ 72 | ctx: ctx, 73 | hmQueueLen: defaultHmQueueLength, 74 | iterDrop: make(chan struct{}), 75 | renderReq: make(chan time.Time), 76 | popPriority: math.MinInt32, 77 | refreshRate: defaultRefreshRate, 78 | queueBars: make(map[*Bar]*Bar), 79 | output: os.Stdout, 80 | debugOut: io.Discard, 81 | } 82 | 83 | for _, opt := range options { 84 | if opt != nil { 85 | opt(s) 86 | } 87 | } 88 | 89 | s.hm = make(heapManager, s.hmQueueLen) 90 | 91 | p := &Progress{ 92 | uwg: s.uwg, 93 | operateState: make(chan func(*pState)), 94 | interceptIO: make(chan func(io.Writer)), 95 | cancel: cancel, 96 | } 97 | 98 | cw := cwriter.New(s.output) 99 | if s.manualRC != nil { 100 | done := make(chan struct{}) 101 | p.done = done 102 | s.autoRefresh = false 103 | go s.manualRefreshListener(done) 104 | } else if cw.IsTerminal() || s.autoRefresh { 105 | done := make(chan struct{}) 106 | p.done = done 107 | s.autoRefresh = true 108 | go s.autoRefreshListener(done) 109 | } else { 110 | p.done = ctx.Done() 111 | s.autoRefresh = false 112 | } 113 | 114 | p.pwg.Add(1) 115 | go p.serve(s, cw) 116 | go s.hm.run() 117 | return p 118 | } 119 | 120 | // AddBar creates a bar with default bar filler. 121 | func (p *Progress) AddBar(total int64, options ...BarOption) *Bar { 122 | return p.New(total, BarStyle(), options...) 123 | } 124 | 125 | // AddSpinner creates a bar with default spinner filler. 126 | func (p *Progress) AddSpinner(total int64, options ...BarOption) *Bar { 127 | return p.New(total, SpinnerStyle(), options...) 128 | } 129 | 130 | // New creates a bar by calling `Build` method on provided `BarFillerBuilder`. 131 | func (p *Progress) New(total int64, builder BarFillerBuilder, options ...BarOption) *Bar { 132 | if builder == nil { 133 | return p.MustAdd(total, nil, options...) 134 | } 135 | return p.MustAdd(total, builder.Build(), options...) 136 | } 137 | 138 | // MustAdd creates a bar which renders itself by provided BarFiller. 139 | // If `total <= 0` triggering complete event by increment methods is 140 | // disabled. Panics if called after `(*Progress).Wait()`. 141 | func (p *Progress) MustAdd(total int64, filler BarFiller, options ...BarOption) *Bar { 142 | bar, err := p.Add(total, filler, options...) 143 | if err != nil { 144 | panic(err) 145 | } 146 | return bar 147 | } 148 | 149 | // Add creates a bar which renders itself by provided BarFiller. 150 | // If `total <= 0` triggering complete event by increment methods 151 | // is disabled. If called after `(*Progress).Wait()` then 152 | // `(nil, ErrDone)` is returned. 153 | func (p *Progress) Add(total int64, filler BarFiller, options ...BarOption) (*Bar, error) { 154 | if filler == nil { 155 | filler = NopStyle().Build() 156 | } else if f, ok := filler.(BarFillerFunc); ok && f == nil { 157 | filler = NopStyle().Build() 158 | } 159 | ch := make(chan *Bar) 160 | select { 161 | case p.operateState <- func(ps *pState) { 162 | bs := ps.makeBarState(total, filler, options...) 163 | bar := newBar(ps.ctx, p, bs) 164 | if bs.waitBar != nil { 165 | ps.queueBars[bs.waitBar] = bar 166 | } else { 167 | ps.hm.push(bar, true) 168 | } 169 | ps.idCount++ 170 | ch <- bar 171 | }: 172 | return <-ch, nil 173 | case <-p.done: 174 | return nil, ErrDone 175 | } 176 | } 177 | 178 | func (p *Progress) traverseBars(cb func(b *Bar) bool) { 179 | drop, iter := make(chan struct{}), make(chan *Bar) 180 | select { 181 | case p.operateState <- func(s *pState) { s.hm.iter(drop, iter, nil) }: 182 | for b := range iter { 183 | if !cb(b) { 184 | close(drop) 185 | break 186 | } 187 | } 188 | case <-p.done: 189 | } 190 | } 191 | 192 | // UpdateBarPriority either immediately or lazy. 193 | // With lazy flag order is updated after the next refresh cycle. 194 | // If you don't care about laziness just use `(*Bar).SetPriority(int)`. 195 | func (p *Progress) UpdateBarPriority(b *Bar, priority int, lazy bool) { 196 | if b == nil { 197 | return 198 | } 199 | select { 200 | case p.operateState <- func(s *pState) { s.hm.fix(b, priority, lazy) }: 201 | case <-p.done: 202 | } 203 | } 204 | 205 | // Write is implementation of io.Writer. 206 | // Writing to `*Progress` will print lines above a running bar. 207 | // Writes aren't flushed immediately, but at next refresh cycle. 208 | // If called after `(*Progress).Wait()` then `(0, ErrDone)` is returned. 209 | func (p *Progress) Write(b []byte) (int, error) { 210 | type result struct { 211 | n int 212 | err error 213 | } 214 | ch := make(chan result) 215 | select { 216 | case p.interceptIO <- func(w io.Writer) { 217 | n, err := w.Write(b) 218 | ch <- result{n, err} 219 | }: 220 | res := <-ch 221 | return res.n, res.err 222 | case <-p.done: 223 | return 0, ErrDone 224 | } 225 | } 226 | 227 | // Wait waits for all bars to complete and finally shutdowns container. After 228 | // this method has been called, there is no way to reuse `*Progress` instance. 229 | func (p *Progress) Wait() { 230 | p.bwg.Wait() 231 | p.Shutdown() 232 | // wait for user wg, if any 233 | if p.uwg != nil { 234 | p.uwg.Wait() 235 | } 236 | } 237 | 238 | // Shutdown cancels any running bar immediately and then shutdowns `*Progress` 239 | // instance. Normally this method shouldn't be called unless you know what you 240 | // are doing. Proper way to shutdown is to call `(*Progress).Wait()` instead. 241 | func (p *Progress) Shutdown() { 242 | p.cancel() 243 | p.pwg.Wait() 244 | } 245 | 246 | func (p *Progress) serve(s *pState, cw *cwriter.Writer) { 247 | defer p.pwg.Done() 248 | var err error 249 | var w *cwriter.Writer 250 | renderReq := s.renderReq 251 | operateState := p.operateState 252 | interceptIO := p.interceptIO 253 | 254 | if s.delayRC != nil { 255 | w = cwriter.New(io.Discard) 256 | } else { 257 | w, cw = cw, nil 258 | } 259 | 260 | for { 261 | select { 262 | case <-s.delayRC: 263 | w, cw = cw, nil 264 | s.delayRC = nil 265 | case op := <-operateState: 266 | op(s) 267 | case fn := <-interceptIO: 268 | fn(w) 269 | case <-renderReq: 270 | err = s.render(w) 271 | if err != nil { 272 | // (*pState).(autoRefreshListener|manualRefreshListener) may block 273 | // if not launching following short lived goroutine 274 | go func() { 275 | for { 276 | select { 277 | case <-s.renderReq: 278 | case <-p.done: 279 | return 280 | } 281 | } 282 | }() 283 | p.cancel() // cancel all bars 284 | renderReq = nil 285 | operateState = nil 286 | interceptIO = nil 287 | } 288 | case <-p.done: 289 | if err != nil { 290 | _, _ = fmt.Fprintln(s.debugOut, err.Error()) 291 | } else if s.autoRefresh { 292 | update := make(chan bool) 293 | for i := 0; i == 0 || <-update; i++ { 294 | if err := s.render(w); err != nil { 295 | _, _ = fmt.Fprintln(s.debugOut, err.Error()) 296 | break 297 | } 298 | s.hm.state(update) 299 | } 300 | } 301 | s.hm.end(s.shutdownNotifier) 302 | return 303 | } 304 | } 305 | } 306 | 307 | func (s *pState) autoRefreshListener(done chan struct{}) { 308 | ticker := time.NewTicker(s.refreshRate) 309 | defer ticker.Stop() 310 | for { 311 | select { 312 | case t := <-ticker.C: 313 | s.renderReq <- t 314 | case <-s.ctx.Done(): 315 | close(done) 316 | return 317 | } 318 | } 319 | } 320 | 321 | func (s *pState) manualRefreshListener(done chan struct{}) { 322 | for { 323 | select { 324 | case x := <-s.manualRC: 325 | if t, ok := x.(time.Time); ok { 326 | s.renderReq <- t 327 | } else { 328 | s.renderReq <- time.Now() 329 | } 330 | case <-s.ctx.Done(): 331 | close(done) 332 | return 333 | } 334 | } 335 | } 336 | 337 | func (s *pState) render(cw *cwriter.Writer) (err error) { 338 | iter, iterPop := make(chan *Bar), make(chan *Bar) 339 | s.hm.sync(s.iterDrop) 340 | s.hm.iter(s.iterDrop, iter, iterPop) 341 | 342 | var width, height int 343 | if cw.IsTerminal() { 344 | width, height, err = cw.GetTermSize() 345 | if err != nil { 346 | close(s.iterDrop) 347 | return err 348 | } 349 | } else { 350 | if s.reqWidth > 0 { 351 | width = s.reqWidth 352 | } else { 353 | width = 80 354 | } 355 | height = width 356 | } 357 | 358 | var barCount int 359 | for b := range iter { 360 | barCount++ 361 | go b.render(width) 362 | } 363 | 364 | return s.flush(cw, height, barCount, iterPop) 365 | } 366 | 367 | func (s *pState) flush(cw *cwriter.Writer, height, barCount int, iter <-chan *Bar) error { 368 | var total, popCount int 369 | rows := make([][]io.Reader, 0, barCount) 370 | 371 | for b := range iter { 372 | frame := <-b.frameCh 373 | if frame.err != nil { 374 | close(s.iterDrop) 375 | b.cancel() 376 | return frame.err // b.frameCh is buffered it's ok to return here 377 | } 378 | var discarded int 379 | for i := len(frame.rows) - 1; i >= 0; i-- { 380 | if total < height { 381 | total++ 382 | } else { 383 | _, _ = io.Copy(io.Discard, frame.rows[i]) // Found IsInBounds 384 | discarded++ 385 | } 386 | } 387 | rows = append(rows, frame.rows) 388 | 389 | switch frame.shutdown { 390 | case 1: 391 | b.cancel() 392 | if qb, ok := s.queueBars[b]; ok { 393 | delete(s.queueBars, b) 394 | qb.priority = b.priority 395 | s.hm.push(qb, true) 396 | } else if s.popCompleted && !frame.noPop { 397 | b.priority = s.popPriority 398 | s.popPriority++ 399 | s.hm.push(b, false) 400 | } else if !frame.rmOnComplete { 401 | s.hm.push(b, false) 402 | } 403 | case 2: 404 | if s.popCompleted && !frame.noPop { 405 | popCount += len(frame.rows) - discarded 406 | continue 407 | } 408 | fallthrough 409 | default: 410 | s.hm.push(b, false) 411 | } 412 | } 413 | 414 | for i := len(rows) - 1; i >= 0; i-- { 415 | for _, r := range rows[i] { 416 | _, err := cw.ReadFrom(r) 417 | if err != nil { 418 | return err 419 | } 420 | } 421 | } 422 | 423 | return cw.Flush(total - popCount) 424 | } 425 | 426 | func (s pState) makeBarState(total int64, filler BarFiller, options ...BarOption) *bState { 427 | bs := &bState{ 428 | id: s.idCount, 429 | priority: s.idCount, 430 | reqWidth: s.reqWidth, 431 | total: total, 432 | filler: filler, 433 | renderReq: s.renderReq, 434 | autoRefresh: s.autoRefresh, 435 | extender: func(_ decor.Statistics, rows ...io.Reader) ([]io.Reader, error) { 436 | return rows, nil 437 | }, 438 | } 439 | 440 | if total > 0 { 441 | bs.triggerComplete = true 442 | } 443 | 444 | for _, opt := range options { 445 | if opt != nil { 446 | opt(bs) 447 | } 448 | } 449 | 450 | for _, group := range bs.decorGroups { 451 | for _, d := range group { 452 | if d, ok := unwrap(d).(decor.EwmaDecorator); ok { 453 | bs.ewmaDecorators = append(bs.ewmaDecorators, d) 454 | } 455 | } 456 | } 457 | 458 | bs.buffers[0] = bytes.NewBuffer(make([]byte, 0, 128)) // prepend 459 | bs.buffers[1] = bytes.NewBuffer(make([]byte, 0, 128)) // append 460 | bs.buffers[2] = bytes.NewBuffer(make([]byte, 0, 256)) // filler 461 | 462 | return bs 463 | } 464 | -------------------------------------------------------------------------------- /progress_test.go: -------------------------------------------------------------------------------- 1 | package mpb_test 2 | 3 | import ( 4 | "bytes" 5 | "container/heap" 6 | "context" 7 | "errors" 8 | "io" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/vbauerster/mpb/v8" 14 | "github.com/vbauerster/mpb/v8/decor" 15 | ) 16 | 17 | const ( 18 | timeout = 300 * time.Millisecond 19 | ) 20 | 21 | func TestWithContext(t *testing.T) { 22 | shutdown := make(chan interface{}) 23 | ctx, cancel := context.WithCancel(context.Background()) 24 | p := mpb.NewWithContext(ctx, 25 | mpb.WithOutput(io.Discard), 26 | mpb.WithShutdownNotifier(shutdown), 27 | ) 28 | _ = p.AddBar(0) // never complete bar 29 | _ = p.AddBar(0) // never complete bar 30 | go func() { 31 | time.Sleep(10 * time.Millisecond) 32 | cancel() 33 | }() 34 | 35 | p.Wait() 36 | 37 | select { 38 | case v := <-shutdown: 39 | if l := len(v.([]*mpb.Bar)); l != 2 { 40 | t.Errorf("Expected len of bars: %d, got: %d", 2, l) 41 | } 42 | case <-time.After(timeout): 43 | t.Errorf("Progress didn't shutdown after %v", timeout) 44 | } 45 | } 46 | 47 | func TestShutdownsWithErrFiller(t *testing.T) { 48 | var debug bytes.Buffer 49 | shutdown := make(chan interface{}) 50 | p := mpb.New( 51 | mpb.WithShutdownNotifier(shutdown), 52 | mpb.WithOutput(io.Discard), 53 | mpb.WithDebugOutput(&debug), 54 | mpb.WithAutoRefresh(), 55 | ) 56 | 57 | var errReturnCount int 58 | testError := errors.New("test error") 59 | bar := p.AddBar(100, 60 | mpb.BarFillerMiddleware(func(base mpb.BarFiller) mpb.BarFiller { 61 | return mpb.BarFillerFunc(func(w io.Writer, st decor.Statistics) error { 62 | if st.Current >= 22 { 63 | errReturnCount++ 64 | return testError 65 | } 66 | return base.Fill(w, st) 67 | }) 68 | }), 69 | ) 70 | 71 | go func() { 72 | for bar.IsRunning() { 73 | bar.Increment() 74 | time.Sleep(10 * time.Millisecond) 75 | } 76 | }() 77 | 78 | p.Wait() 79 | 80 | if errReturnCount != 1 { 81 | t.Errorf("Expected errReturnCount: %d, got: %d\n", 1, errReturnCount) 82 | } 83 | 84 | select { 85 | case v := <-shutdown: 86 | if l := len(v.([]*mpb.Bar)); l != 0 { 87 | t.Errorf("Expected len of bars: %d, got: %d\n", 0, l) 88 | } 89 | if err := strings.TrimSpace(debug.String()); err != testError.Error() { 90 | t.Errorf("Expected err: %q, got %q\n", testError.Error(), err) 91 | } 92 | case <-time.After(timeout): 93 | t.Errorf("Progress didn't shutdown after %v", timeout) 94 | } 95 | } 96 | 97 | func TestShutdownAfterBarAbortWithDrop(t *testing.T) { 98 | shutdown := make(chan interface{}) 99 | p := mpb.New( 100 | mpb.WithShutdownNotifier(shutdown), 101 | mpb.WithOutput(io.Discard), 102 | mpb.WithAutoRefresh(), 103 | ) 104 | b := p.AddBar(100) 105 | 106 | var count int 107 | for i := 0; !b.Aborted(); i++ { 108 | if i >= 10 { 109 | count++ 110 | b.Abort(true) 111 | } else { 112 | b.Increment() 113 | time.Sleep(10 * time.Millisecond) 114 | } 115 | } 116 | 117 | p.Wait() 118 | 119 | if count != 1 { 120 | t.Errorf("Expected count: %d, got: %d", 1, count) 121 | } 122 | 123 | select { 124 | case v := <-shutdown: 125 | if l := len(v.([]*mpb.Bar)); l != 0 { 126 | t.Errorf("Expected len of bars: %d, got: %d", 0, l) 127 | } 128 | case <-time.After(timeout): 129 | t.Errorf("Progress didn't shutdown after %v", timeout) 130 | } 131 | } 132 | 133 | func TestShutdownAfterBarAbortWithNoDrop(t *testing.T) { 134 | shutdown := make(chan interface{}) 135 | p := mpb.New( 136 | mpb.WithShutdownNotifier(shutdown), 137 | mpb.WithOutput(io.Discard), 138 | mpb.WithAutoRefresh(), 139 | ) 140 | b := p.AddBar(100) 141 | 142 | var count int 143 | for i := 0; !b.Aborted(); i++ { 144 | if i >= 10 { 145 | count++ 146 | b.Abort(false) 147 | } else { 148 | b.Increment() 149 | time.Sleep(10 * time.Millisecond) 150 | } 151 | } 152 | 153 | p.Wait() 154 | 155 | if count != 1 { 156 | t.Errorf("Expected count: %d, got: %d", 1, count) 157 | } 158 | 159 | select { 160 | case v := <-shutdown: 161 | if l := len(v.([]*mpb.Bar)); l != 1 { 162 | t.Errorf("Expected len of bars: %d, got: %d", 1, l) 163 | } 164 | case <-time.After(timeout): 165 | t.Errorf("Progress didn't shutdown after %v", timeout) 166 | } 167 | } 168 | 169 | func TestBarPristinePopOrder(t *testing.T) { 170 | shutdown := make(chan interface{}) 171 | ctx, cancel := context.WithCancel(context.Background()) 172 | p := mpb.NewWithContext(ctx, 173 | mpb.WithOutput(io.Discard), // auto refresh is disabled 174 | mpb.WithShutdownNotifier(shutdown), 175 | ) 176 | a := p.AddBar(100, mpb.BarPriority(1), mpb.BarID(1)) 177 | b := p.AddBar(100, mpb.BarPriority(2), mpb.BarID(2)) 178 | c := p.AddBar(100, mpb.BarPriority(3), mpb.BarID(3)) 179 | pristineOrder := []*mpb.Bar{c, b, a} 180 | 181 | go cancel() 182 | 183 | bars := (<-shutdown).([]*mpb.Bar) 184 | if l := len(bars); l != 3 { 185 | t.Fatalf("Expected len of bars: %d, got: %d", 3, l) 186 | } 187 | 188 | p.Wait() 189 | pq := mpb.PriorityQueue(bars) 190 | 191 | for _, b := range pristineOrder { 192 | // higher priority pops first 193 | if bar := heap.Pop(&pq).(*mpb.Bar); bar.ID() != b.ID() { 194 | t.Errorf("Expected bar id: %d, got bar id: %d", b.ID(), bar.ID()) 195 | } 196 | } 197 | } 198 | 199 | func makeUpdateBarPriorityTest(refresh, lazy bool) func(*testing.T) { 200 | return func(t *testing.T) { 201 | shutdown := make(chan interface{}) 202 | refreshCh := make(chan interface{}) 203 | ctx, cancel := context.WithCancel(context.Background()) 204 | p := mpb.NewWithContext(ctx, 205 | mpb.WithOutput(io.Discard), 206 | mpb.WithManualRefresh(refreshCh), 207 | mpb.WithShutdownNotifier(shutdown), 208 | ) 209 | a := p.AddBar(100, mpb.BarPriority(1), mpb.BarID(1)) 210 | b := p.AddBar(100, mpb.BarPriority(2), mpb.BarID(2)) 211 | c := p.AddBar(100, mpb.BarPriority(3), mpb.BarID(3)) 212 | 213 | p.UpdateBarPriority(c, 2, lazy) 214 | p.UpdateBarPriority(b, 3, lazy) 215 | checkOrder := []*mpb.Bar{b, c, a} // updated order 216 | 217 | if refresh { 218 | refreshCh <- time.Now() 219 | } else if lazy { 220 | checkOrder = []*mpb.Bar{c, b, a} // pristine order 221 | } 222 | 223 | go cancel() 224 | 225 | bars := (<-shutdown).([]*mpb.Bar) 226 | if l := len(bars); l != 3 { 227 | t.Fatalf("Expected len of bars: %d, got: %d", 3, l) 228 | } 229 | 230 | p.Wait() 231 | pq := mpb.PriorityQueue(bars) 232 | 233 | for _, b := range checkOrder { 234 | // higher priority pops first 235 | if bar := heap.Pop(&pq).(*mpb.Bar); bar.ID() != b.ID() { 236 | t.Errorf("Expected bar id: %d, got bar id: %d", b.ID(), bar.ID()) 237 | } 238 | } 239 | } 240 | } 241 | 242 | func TestUpdateBarPriority(t *testing.T) { 243 | makeUpdateBarPriorityTest(false, false)(t) 244 | makeUpdateBarPriorityTest(true, false)(t) 245 | } 246 | 247 | func TestUpdateBarPriorityLazy(t *testing.T) { 248 | makeUpdateBarPriorityTest(false, true)(t) 249 | makeUpdateBarPriorityTest(true, true)(t) 250 | } 251 | 252 | func TestNoOutput(t *testing.T) { 253 | var buf bytes.Buffer 254 | p := mpb.New(mpb.WithOutput(&buf)) 255 | bar := p.AddBar(100) 256 | 257 | go func() { 258 | for !bar.Completed() { 259 | bar.Increment() 260 | } 261 | }() 262 | 263 | p.Wait() 264 | 265 | if buf.Len() != 0 { 266 | t.Errorf("Expected buf.Len == 0, got: %d\n", buf.Len()) 267 | } 268 | } 269 | 270 | func TestAddAfterDone(t *testing.T) { 271 | p := mpb.New(mpb.WithOutput(io.Discard)) 272 | bar := p.AddBar(100) 273 | bar.IncrBy(100) 274 | 275 | p.Wait() 276 | 277 | _, err := p.Add(100, nil) 278 | 279 | if err != mpb.ErrDone { 280 | t.Errorf("Expected %q, got: %q\n", mpb.ErrDone, err) 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /proxyreader.go: -------------------------------------------------------------------------------- 1 | package mpb 2 | 3 | import ( 4 | "io" 5 | "time" 6 | ) 7 | 8 | type proxyReader struct { 9 | io.ReadCloser 10 | bar *Bar 11 | } 12 | 13 | func (x proxyReader) Read(p []byte) (int, error) { 14 | n, err := x.ReadCloser.Read(p) 15 | x.bar.IncrBy(n) 16 | return n, err 17 | } 18 | 19 | type proxyWriterTo struct { 20 | proxyReader 21 | } 22 | 23 | func (x proxyWriterTo) WriteTo(w io.Writer) (int64, error) { 24 | n, err := x.ReadCloser.(io.WriterTo).WriteTo(w) 25 | x.bar.IncrInt64(n) 26 | return n, err 27 | } 28 | 29 | type ewmaProxyReader struct { 30 | io.ReadCloser 31 | bar *Bar 32 | } 33 | 34 | func (x ewmaProxyReader) Read(p []byte) (int, error) { 35 | start := time.Now() 36 | n, err := x.ReadCloser.Read(p) 37 | x.bar.EwmaIncrBy(n, time.Since(start)) 38 | return n, err 39 | } 40 | 41 | type ewmaProxyWriterTo struct { 42 | ewmaProxyReader 43 | } 44 | 45 | func (x ewmaProxyWriterTo) WriteTo(w io.Writer) (int64, error) { 46 | start := time.Now() 47 | n, err := x.ReadCloser.(io.WriterTo).WriteTo(w) 48 | x.bar.EwmaIncrInt64(n, time.Since(start)) 49 | return n, err 50 | } 51 | 52 | func newProxyReader(r io.Reader, b *Bar, hasEwma bool) io.ReadCloser { 53 | rc := toReadCloser(r) 54 | if hasEwma { 55 | epr := ewmaProxyReader{rc, b} 56 | if _, ok := r.(io.WriterTo); ok { 57 | return ewmaProxyWriterTo{epr} 58 | } 59 | return epr 60 | } 61 | pr := proxyReader{rc, b} 62 | if _, ok := r.(io.WriterTo); ok { 63 | return proxyWriterTo{pr} 64 | } 65 | return pr 66 | } 67 | 68 | func toReadCloser(r io.Reader) io.ReadCloser { 69 | if rc, ok := r.(io.ReadCloser); ok { 70 | return rc 71 | } 72 | return io.NopCloser(r) 73 | } 74 | -------------------------------------------------------------------------------- /proxyreader_test.go: -------------------------------------------------------------------------------- 1 | package mpb_test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/vbauerster/mpb/v8" 10 | ) 11 | 12 | const content = `Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do 13 | eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 14 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 15 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit 16 | esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat 17 | cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id 18 | est laborum.` 19 | 20 | type testReader struct { 21 | io.Reader 22 | called bool 23 | } 24 | 25 | func (r *testReader) Read(p []byte) (n int, err error) { 26 | r.called = true 27 | return r.Reader.Read(p) 28 | } 29 | 30 | func TestProxyReader(t *testing.T) { 31 | p := mpb.New(mpb.WithOutput(io.Discard)) 32 | 33 | tr := &testReader{strings.NewReader(content), false} 34 | 35 | bar := p.New(int64(len(content)), mpb.NopStyle()) 36 | 37 | var buf bytes.Buffer 38 | _, err := io.Copy(&buf, bar.ProxyReader(tr)) 39 | if err != nil { 40 | t.Errorf("io.Copy: %s\n", err.Error()) 41 | } 42 | 43 | p.Wait() 44 | 45 | if !tr.called { 46 | t.Error("Read not called") 47 | } 48 | 49 | if got := buf.String(); got != content { 50 | t.Errorf("Expected content: %s, got: %s\n", content, got) 51 | } 52 | } 53 | 54 | type testReadCloser struct { 55 | io.Reader 56 | called bool 57 | } 58 | 59 | func (r *testReadCloser) Close() error { 60 | r.called = true 61 | return nil 62 | } 63 | 64 | func TestProxyReadCloser(t *testing.T) { 65 | p := mpb.New(mpb.WithOutput(io.Discard)) 66 | 67 | tr := &testReadCloser{strings.NewReader(content), false} 68 | 69 | bar := p.New(int64(len(content)), mpb.NopStyle()) 70 | 71 | rc := bar.ProxyReader(tr) 72 | _, err := io.Copy(io.Discard, rc) 73 | if err != nil { 74 | t.Errorf("io.Copy: %s\n", err.Error()) 75 | } 76 | _ = rc.Close() 77 | 78 | p.Wait() 79 | 80 | if !tr.called { 81 | t.Error("Close not called") 82 | } 83 | } 84 | 85 | type testReaderWriterTo struct { 86 | io.Reader 87 | called bool 88 | } 89 | 90 | func (r *testReaderWriterTo) WriteTo(w io.Writer) (n int64, err error) { 91 | r.called = true 92 | return r.Reader.(io.WriterTo).WriteTo(w) 93 | } 94 | 95 | func TestProxyReaderWriterTo(t *testing.T) { 96 | p := mpb.New(mpb.WithOutput(io.Discard)) 97 | 98 | tr := &testReaderWriterTo{strings.NewReader(content), false} 99 | 100 | bar := p.New(int64(len(content)), mpb.NopStyle()) 101 | 102 | var buf bytes.Buffer 103 | _, err := io.Copy(&buf, bar.ProxyReader(tr)) 104 | if err != nil { 105 | t.Errorf("io.Copy: %s\n", err.Error()) 106 | } 107 | 108 | p.Wait() 109 | 110 | if !tr.called { 111 | t.Error("WriteTo not called") 112 | } 113 | 114 | if got := buf.String(); got != content { 115 | t.Errorf("Expected content: %s, got: %s\n", content, got) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /proxywriter.go: -------------------------------------------------------------------------------- 1 | package mpb 2 | 3 | import ( 4 | "io" 5 | "time" 6 | ) 7 | 8 | type proxyWriter struct { 9 | io.WriteCloser 10 | bar *Bar 11 | } 12 | 13 | func (x proxyWriter) Write(p []byte) (int, error) { 14 | n, err := x.WriteCloser.Write(p) 15 | x.bar.IncrBy(n) 16 | return n, err 17 | } 18 | 19 | type proxyReaderFrom struct { 20 | proxyWriter 21 | } 22 | 23 | func (x proxyReaderFrom) ReadFrom(r io.Reader) (int64, error) { 24 | n, err := x.WriteCloser.(io.ReaderFrom).ReadFrom(r) 25 | x.bar.IncrInt64(n) 26 | return n, err 27 | } 28 | 29 | type ewmaProxyWriter struct { 30 | io.WriteCloser 31 | bar *Bar 32 | } 33 | 34 | func (x ewmaProxyWriter) Write(p []byte) (int, error) { 35 | start := time.Now() 36 | n, err := x.WriteCloser.Write(p) 37 | x.bar.EwmaIncrBy(n, time.Since(start)) 38 | return n, err 39 | } 40 | 41 | type ewmaProxyReaderFrom struct { 42 | ewmaProxyWriter 43 | } 44 | 45 | func (x ewmaProxyReaderFrom) ReadFrom(r io.Reader) (int64, error) { 46 | start := time.Now() 47 | n, err := x.WriteCloser.(io.ReaderFrom).ReadFrom(r) 48 | x.bar.EwmaIncrInt64(n, time.Since(start)) 49 | return n, err 50 | } 51 | 52 | func newProxyWriter(w io.Writer, b *Bar, hasEwma bool) io.WriteCloser { 53 | wc := toWriteCloser(w) 54 | if hasEwma { 55 | epw := ewmaProxyWriter{wc, b} 56 | if _, ok := w.(io.ReaderFrom); ok { 57 | return ewmaProxyReaderFrom{epw} 58 | } 59 | return epw 60 | } 61 | pw := proxyWriter{wc, b} 62 | if _, ok := w.(io.ReaderFrom); ok { 63 | return proxyReaderFrom{pw} 64 | } 65 | return pw 66 | } 67 | 68 | func toWriteCloser(w io.Writer) io.WriteCloser { 69 | if wc, ok := w.(io.WriteCloser); ok { 70 | return wc 71 | } 72 | return toNopWriteCloser(w) 73 | } 74 | 75 | func toNopWriteCloser(w io.Writer) io.WriteCloser { 76 | if _, ok := w.(io.ReaderFrom); ok { 77 | return nopWriteCloserReaderFrom{w} 78 | } 79 | return nopWriteCloser{w} 80 | } 81 | 82 | type nopWriteCloser struct { 83 | io.Writer 84 | } 85 | 86 | func (nopWriteCloser) Close() error { return nil } 87 | 88 | type nopWriteCloserReaderFrom struct { 89 | io.Writer 90 | } 91 | 92 | func (nopWriteCloserReaderFrom) Close() error { return nil } 93 | 94 | func (c nopWriteCloserReaderFrom) ReadFrom(r io.Reader) (int64, error) { 95 | return c.Writer.(io.ReaderFrom).ReadFrom(r) 96 | } 97 | -------------------------------------------------------------------------------- /proxywriter_test.go: -------------------------------------------------------------------------------- 1 | package mpb_test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/vbauerster/mpb/v8" 10 | ) 11 | 12 | type testWriter struct { 13 | io.Writer 14 | called bool 15 | } 16 | 17 | func (w *testWriter) Write(p []byte) (n int, err error) { 18 | w.called = true 19 | return w.Writer.Write(p) 20 | } 21 | 22 | func TestProxyWriter(t *testing.T) { 23 | p := mpb.New(mpb.WithOutput(io.Discard)) 24 | 25 | var buf bytes.Buffer 26 | tw := &testWriter{&buf, false} 27 | 28 | bar := p.New(int64(len(content)), mpb.NopStyle()) 29 | 30 | _, err := io.Copy(bar.ProxyWriter(tw), strings.NewReader(content)) 31 | if err != nil { 32 | t.Errorf("io.Copy: %s\n", err.Error()) 33 | } 34 | 35 | p.Wait() 36 | 37 | if !tw.called { 38 | t.Error("Read not called") 39 | } 40 | 41 | if got := buf.String(); got != content { 42 | t.Errorf("Expected content: %s, got: %s\n", content, got) 43 | } 44 | } 45 | 46 | type testWriteCloser struct { 47 | io.Writer 48 | called bool 49 | } 50 | 51 | func (w *testWriteCloser) Close() error { 52 | w.called = true 53 | return nil 54 | } 55 | 56 | func TestProxyWriteCloser(t *testing.T) { 57 | p := mpb.New(mpb.WithOutput(io.Discard)) 58 | 59 | var buf bytes.Buffer 60 | tw := &testWriteCloser{&buf, false} 61 | 62 | bar := p.New(int64(len(content)), mpb.NopStyle()) 63 | 64 | wc := bar.ProxyWriter(tw) 65 | _, err := io.Copy(wc, strings.NewReader(content)) 66 | if err != nil { 67 | t.Errorf("io.Copy: %s\n", err.Error()) 68 | } 69 | _ = wc.Close() 70 | 71 | p.Wait() 72 | 73 | if !tw.called { 74 | t.Error("Close not called") 75 | } 76 | } 77 | 78 | type testWriterReadFrom struct { 79 | io.Writer 80 | called bool 81 | } 82 | 83 | func (w *testWriterReadFrom) ReadFrom(r io.Reader) (n int64, err error) { 84 | w.called = true 85 | return w.Writer.(io.ReaderFrom).ReadFrom(r) 86 | } 87 | 88 | type dumbReader struct { 89 | r io.Reader 90 | } 91 | 92 | func (r dumbReader) Read(p []byte) (int, error) { 93 | return r.r.Read(p) 94 | } 95 | 96 | func TestProxyWriterReadFrom(t *testing.T) { 97 | p := mpb.New(mpb.WithOutput(io.Discard)) 98 | 99 | var buf bytes.Buffer 100 | tw := &testWriterReadFrom{&buf, false} 101 | 102 | bar := p.New(int64(len(content)), mpb.NopStyle()) 103 | 104 | // To trigger ReadFrom, WriteTo needs to be hidden, hence a dumb wrapper 105 | dr := dumbReader{strings.NewReader(content)} 106 | _, err := io.Copy(bar.ProxyWriter(tw), dr) 107 | if err != nil { 108 | t.Errorf("io.Copy: %s\n", err.Error()) 109 | } 110 | 111 | p.Wait() 112 | 113 | if !tw.called { 114 | t.Error("ReadFrom not called") 115 | } 116 | 117 | if got := buf.String(); got != content { 118 | t.Errorf("Expected content: %s, got: %s\n", content, got) 119 | } 120 | } 121 | --------------------------------------------------------------------------------