diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..2ffdb4d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,35 @@ +name: Test + +on: [push, pull_request] + +jobs: + test: + strategy: + matrix: + go-version: [1.16, 1.17] + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + # In order: + # * Module download cache + # * Build cache (Linux) + # * Build cache (Mac) + # * Build cache (Windows) + path: | + ~/go/pkg/mod + ~/.cache/go-build + ~/Library/Caches/go-build + %LocalAppData%\go-build + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Test + run: go test -race ./... diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0eb0f2f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: go - -go: - - 1.14.x - -script: - - go test -race ./... - - for i in _examples/*/; do go build $i/*.go || exit 1; done diff --git a/README.md b/README.md index bfb0c4d..413f9e1 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,24 @@ # Multi Progress Bar -[![GoDoc](https://godoc.org/github.com/vbauerster/mpb?status.svg)](https://godoc.org/github.com/vbauerster/mpb) -[![Build Status](https://travis-ci.org/vbauerster/mpb.svg?branch=master)](https://travis-ci.org/vbauerster/mpb) -[![Go Report Card](https://goreportcard.com/badge/github.com/vbauerster/mpb)](https://goreportcard.com/report/github.com/vbauerster/mpb) +[![GoDoc](https://pkg.go.dev/badge/github.com/vbauerster/mpb)](https://pkg.go.dev/github.com/vbauerster/mpb/v7) +[![Test status](https://github.com/vbauerster/mpb/actions/workflows/test.yml/badge.svg)](https://github.com/vbauerster/mpb/actions/workflows/test.yml) +[![Donate with PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/vbauerster) **mpb** is a Go lib for rendering progress bars in terminal applications. ## Features -* __Multiple Bars__: Multiple progress bars are supported -* __Dynamic Total__: Set total while bar is running -* __Dynamic Add/Remove__: Dynamically add or remove bars -* __Cancellation__: Cancel whole rendering process -* __Predefined Decorators__: Elapsed time, [ewma](https://github.com/VividCortex/ewma) based ETA, Percentage, Bytes counter -* __Decorator's width sync__: Synchronized decorator's width among multiple bars +- **Multiple Bars**: Multiple progress bars are supported +- **Dynamic Total**: Set total while bar is running +- **Dynamic Add/Remove**: Dynamically add or remove bars +- **Cancellation**: Cancel whole rendering process +- **Predefined Decorators**: Elapsed time, [ewma](https://github.com/VividCortex/ewma) based ETA, Percentage, Bytes counter +- **Decorator's width sync**: Synchronized decorator's width among multiple bars ## Usage #### [Rendering single bar](_examples/singleBar/main.go) + ```go package main @@ -25,8 +26,8 @@ "math/rand" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func main() { @@ -35,10 +36,10 @@ total := 100 name := "Single Bar:" - // adding a single bar, which will inherit container's width - bar := p.AddBar(int64(total), - // override DefaultBarStyle, which is "[=>-]<+" - mpb.BarStyle("╢▌▌░╟"), + // create a single bar, which will inherit container's width + bar := p.New(int64(total), + // BarFillerBuilder with custom style + mpb.BarStyle().Lbound("╢").Filler("▌").Tip("▌").Padding("░").Rbound("╟"), mpb.PrependDecorators( // display our name with one space on the right decor.Name(name, decor.WC{W: len(name) + 1, C: decor.DidentRight}), @@ -61,9 +62,10 @@ ``` #### [Rendering multiple bars](_examples/multiBars/main.go) + ```go var wg sync.WaitGroup - // pass &wg (optional), so p will wait for it eventually + // passed wg will be accounted at p.Wait() call p := mpb.New(mpb.WithWaitGroup(&wg)) total, numBars := 100, 3 wg.Add(numBars) @@ -81,7 +83,7 @@ // replace ETA decorator with "done" message, OnComplete event decor.OnComplete( // ETA decorator with ewma age of 60 - decor.EwmaETA(decor.ET_STYLE_GO, 60), "done", + decor.EwmaETA(decor.ET_STYLE_GO, 60, decor.WCSyncWidth), "done", ), ), ) @@ -101,7 +103,7 @@ } }() } - // Waiting for passed &wg and for all bars to complete and flush + // wait for passed wg and for all bars to complete and flush p.Wait() ``` diff --git a/_examples/barExtender/go.mod b/_examples/barExtender/go.mod index cb71f2a..ca38865 100644 --- a/_examples/barExtender/go.mod +++ b/_examples/barExtender/go.mod @@ -2,4 +2,4 @@ go 1.14 -require github.com/vbauerster/mpb/v5 v5.0.3 +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/barExtender/main.go b/_examples/barExtender/main.go index 0d5d2d5..bb70f20 100644 --- a/_examples/barExtender/main.go +++ b/_examples/barExtender/main.go @@ -7,19 +7,20 @@ "sync" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func main() { var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call p := mpb.New(mpb.WithWaitGroup(&wg)) total, numBars := 100, 3 wg.Add(numBars) for i := 0; i < numBars; i++ { name := fmt.Sprintf("Bar#%d:", i) - efn := func(w io.Writer, width int, s *decor.Statistics) { + efn := func(w io.Writer, _ int, s decor.Statistics) { if s.Completed { fmt.Fprintf(w, "Bar id: %d has been completed\n", s.ID) } @@ -55,6 +56,6 @@ } }() } - // wait for all bars to complete and flush + // wait for passed wg and for all bars to complete and flush p.Wait() } diff --git a/_examples/cancel/go.mod b/_examples/cancel/go.mod index f1a7f39..6b442db 100644 --- a/_examples/cancel/go.mod +++ b/_examples/cancel/go.mod @@ -2,4 +2,4 @@ go 1.14 -require github.com/vbauerster/mpb/v5 v5.0.3 +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/cancel/main.go b/_examples/cancel/main.go index b507601..1528ffd 100644 --- a/_examples/cancel/main.go +++ b/_examples/cancel/main.go @@ -7,8 +7,8 @@ "sync" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func main() { @@ -16,6 +16,7 @@ defer cancel() var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call p := mpb.NewWithContext(ctx, mpb.WithWaitGroup(&wg)) total := 300 numBars := 3 @@ -49,6 +50,6 @@ } }() } - + // wait for passed wg and for all bars to complete and flush p.Wait() } diff --git a/_examples/complex/go.mod b/_examples/complex/go.mod index da328e4..2dad190 100644 --- a/_examples/complex/go.mod +++ b/_examples/complex/go.mod @@ -2,4 +2,4 @@ go 1.14 -require github.com/vbauerster/mpb/v5 v5.0.3 +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/complex/main.go b/_examples/complex/main.go index aa45b74..a5d42f4 100644 --- a/_examples/complex/main.go +++ b/_examples/complex/main.go @@ -6,8 +6,8 @@ "sync" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func init() { @@ -16,7 +16,8 @@ func main() { doneWg := new(sync.WaitGroup) - p := mpb.New(mpb.WithWidth(64), mpb.WithWaitGroup(doneWg)) + // passed doneWg will be accounted at p.Wait() call + p := mpb.New(mpb.WithWaitGroup(doneWg)) numBars := 4 var bars []*mpb.Bar @@ -44,7 +45,8 @@ i := i go func() { task := fmt.Sprintf("Task#%02d:", i) - job := "\x1b[31;1;4minstalling\x1b[0m" + // ANSI escape sequences are not supported on Windows OS + job := "\x1b[31;1;4mつのだ☆HIRO\x1b[0m" // preparing delayed bars b := p.AddBar(rand.Int63n(101)+100, mpb.BarQueueAfter(bars[i]), @@ -63,7 +65,7 @@ go newTask(doneWg, b, numBars-i) }() } - + // wait for passed doneWg and for all bars to complete and flush p.Wait() } diff --git a/_examples/decoratorsOnTop/go.mod b/_examples/decoratorsOnTop/go.mod new file mode 100644 index 0000000..65e4023 --- /dev/null +++ b/_examples/decoratorsOnTop/go.mod @@ -0,0 +1,5 @@ +module github.com/vbauerster/mpb/_examples/decoratorsOnTop + +go 1.14 + +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/decoratorsOnTop/main.go b/_examples/decoratorsOnTop/main.go new file mode 100644 index 0000000..a6e0ae6 --- /dev/null +++ b/_examples/decoratorsOnTop/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "io" + "math/rand" + "time" + + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" +) + +func main() { + p := mpb.New() + + total := 100 + bar := p.New(int64(total), + mpb.NopStyle(), // make main bar style nop, so there are just decorators + mpb.BarExtender(extended(mpb.BarStyle())), // extend wtih normal bar on the next line + mpb.PrependDecorators( + decor.Name("Percentage: "), + decor.NewPercentage("%d"), + ), + mpb.AppendDecorators( + decor.Name("ETA: "), + decor.OnComplete( + decor.AverageETA(decor.ET_STYLE_GO), "done", + ), + ), + ) + // simulating some work + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) + bar.Increment() + } + // wait for our bar to complete and flush + p.Wait() +} + +func extended(builder mpb.BarFillerBuilder) mpb.BarFiller { + filler := builder.Build() + return mpb.BarFillerFunc(func(w io.Writer, reqWidth int, st decor.Statistics) { + filler.Fill(w, reqWidth, st) + w.Write([]byte("\n")) + }) +} diff --git a/_examples/differentWidth/go.mod b/_examples/differentWidth/go.mod index eb3ed39..31a8a42 100644 --- a/_examples/differentWidth/go.mod +++ b/_examples/differentWidth/go.mod @@ -2,4 +2,4 @@ go 1.14 -require github.com/vbauerster/mpb/v5 v5.0.3 +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/differentWidth/main.go b/_examples/differentWidth/main.go index 90b2c30..a358cff 100644 --- a/_examples/differentWidth/main.go +++ b/_examples/differentWidth/main.go @@ -6,15 +6,15 @@ "sync" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func main() { var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call p := mpb.New( mpb.WithWaitGroup(&wg), - // container's width. mpb.WithWidth(60), ) total, numBars := 100, 3 @@ -24,7 +24,7 @@ name := fmt.Sprintf("Bar#%d:", i) bar := p.AddBar(int64(total), // set BarWidth 40 for bar 1 and 2 - mpb.BarOptOn(mpb.BarWidth(40), func() bool { return i > 0 }), + mpb.BarOptional(mpb.BarWidth(40), i > 0), mpb.PrependDecorators( // simple name decorator decor.Name(name), @@ -55,6 +55,6 @@ } }() } - // wait for all bars to complete and flush + // wait for passed wg and for all bars to complete and flush p.Wait() } diff --git a/_examples/dynTotal/go.mod b/_examples/dynTotal/go.mod index 76f18f3..ee1c623 100644 --- a/_examples/dynTotal/go.mod +++ b/_examples/dynTotal/go.mod @@ -2,4 +2,4 @@ go 1.14 -require github.com/vbauerster/mpb/v5 v5.0.3 +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/dynTotal/main.go b/_examples/dynTotal/main.go index 55e8c0e..aabcc42 100644 --- a/_examples/dynTotal/main.go +++ b/_examples/dynTotal/main.go @@ -5,8 +5,8 @@ "math/rand" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func init() { @@ -17,6 +17,7 @@ p := mpb.New(mpb.WithWidth(64)) var total int64 + // new bar with 'trigger complete event' disabled, because total is zero bar := p.AddBar(total, mpb.PrependDecorators(decor.Counters(decor.UnitKiB, "% .1f / % .1f")), mpb.AppendDecorators(decor.Percentage()), diff --git a/_examples/io/go.mod b/_examples/io/go.mod index 5789e76..35e3563 100644 --- a/_examples/io/go.mod +++ b/_examples/io/go.mod @@ -2,4 +2,4 @@ go 1.14 -require github.com/vbauerster/mpb/v5 v5.0.3 +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/io/main.go b/_examples/io/main.go index 00a6dcd..339d853 100644 --- a/_examples/io/main.go +++ b/_examples/io/main.go @@ -6,8 +6,8 @@ "io/ioutil" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func main() { @@ -19,7 +19,8 @@ mpb.WithRefreshRate(180*time.Millisecond), ) - bar := p.AddBar(total, mpb.BarStyle("[=>-|"), + bar := p.New(total, + mpb.BarStyle().Rbound("|"), mpb.PrependDecorators( decor.CountersKibiByte("% .2f / % .2f"), ), diff --git a/_examples/merge/go.mod b/_examples/merge/go.mod index 50502ea..b3c7177 100644 --- a/_examples/merge/go.mod +++ b/_examples/merge/go.mod @@ -2,4 +2,4 @@ go 1.14 -require github.com/vbauerster/mpb/v5 v5.0.3 +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/merge/main.go b/_examples/merge/main.go index 03a5817..43f8c44 100644 --- a/_examples/merge/main.go +++ b/_examples/merge/main.go @@ -6,13 +6,13 @@ "sync" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func main() { var wg sync.WaitGroup - // pass &wg (optional), so p will wait for it eventually + // passed wg will be accounted at p.Wait() call p := mpb.New(mpb.WithWaitGroup(&wg), mpb.WithWidth(60)) total, numBars := 100, 3 wg.Add(numBars) @@ -27,11 +27,13 @@ "done", ), decor.WCSyncSpace, // Placeholder + decor.WCSyncSpace, // Placeholder ), ) } else { pdecorators = mpb.PrependDecorators( decor.CountersNoUnit("% .1d / % .1d", decor.WCSyncSpace), + decor.OnComplete(decor.Spinner(nil, decor.WCSyncSpace), "done"), decor.OnComplete(decor.Spinner(nil, decor.WCSyncSpace), "done"), ) } @@ -57,14 +59,14 @@ } }() } - // Waiting for passed &wg and for all bars to complete and flush + // wait for passed wg and for all bars to complete and flush p.Wait() } func newVariadicSpinner(wc decor.WC) decor.Decorator { spinner := decor.Spinner(nil) - f := func(s *decor.Statistics) string { + fn := func(s decor.Statistics) string { return strings.Repeat(spinner.Decor(s), int(s.Current/3)) } - return decor.Any(f, wc) + return decor.Any(fn, wc) } diff --git a/_examples/mexicanBar/go.mod b/_examples/mexicanBar/go.mod new file mode 100644 index 0000000..f9490e9 --- /dev/null +++ b/_examples/mexicanBar/go.mod @@ -0,0 +1,5 @@ +module github.com/vbauerster/mpb/_examples/mexicanBar + +go 1.14 + +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/mexicanBar/main.go b/_examples/mexicanBar/main.go new file mode 100644 index 0000000..ce2a733 --- /dev/null +++ b/_examples/mexicanBar/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "math/rand" + "time" + + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" +) + +func main() { + // initialize progress container, with custom width + p := mpb.New(mpb.WithWidth(80)) + + total := 100 + name := "Complex Filler:" + bs := mpb.BarStyle() + bs.Lbound("[\u001b[36;1m") + bs.Filler("_") + bs.Tip("\u001b[0m⛵\u001b[36;1m") + bs.Padding("_") + bs.Rbound("\u001b[0m]") + bar := p.New(int64(total), bs, + mpb.PrependDecorators(decor.Name(name)), + mpb.AppendDecorators(decor.Percentage()), + ) + // simulating some work + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) + bar.Increment() + } + // wait for our bar to complete + p.Wait() +} diff --git a/_examples/multiBars/go.mod b/_examples/multiBars/go.mod index bb356b6..1a37e92 100644 --- a/_examples/multiBars/go.mod +++ b/_examples/multiBars/go.mod @@ -2,4 +2,4 @@ go 1.14 -require github.com/vbauerster/mpb/v5 v5.0.3 +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/multiBars/main.go b/_examples/multiBars/main.go index 38dc864..ebca301 100644 --- a/_examples/multiBars/main.go +++ b/_examples/multiBars/main.go @@ -6,13 +6,13 @@ "sync" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func main() { var wg sync.WaitGroup - // pass &wg (optional), so p will wait for it eventually + // passed wg will be accounted at p.Wait() call p := mpb.New(mpb.WithWaitGroup(&wg)) total, numBars := 100, 3 wg.Add(numBars) @@ -30,7 +30,7 @@ // replace ETA decorator with "done" message, OnComplete event decor.OnComplete( // ETA decorator with ewma age of 60 - decor.EwmaETA(decor.ET_STYLE_GO, 60), "done", + decor.EwmaETA(decor.ET_STYLE_GO, 60, decor.WCSyncWidth), "done", ), ), ) @@ -50,6 +50,6 @@ } }() } - // Waiting for passed &wg and for all bars to complete and flush + // wait for passed wg and for all bars to complete and flush p.Wait() } diff --git a/_examples/panic/go.mod b/_examples/panic/go.mod index 3846072..5d81105 100644 --- a/_examples/panic/go.mod +++ b/_examples/panic/go.mod @@ -2,4 +2,4 @@ go 1.14 -require github.com/vbauerster/mpb/v5 v5.0.3 +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/panic/main.go b/_examples/panic/main.go index 1a7cfc1..a843ed5 100644 --- a/_examples/panic/main.go +++ b/_examples/panic/main.go @@ -3,18 +3,23 @@ import ( "fmt" "os" + "strings" "sync" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func main() { var wg sync.WaitGroup - p := mpb.New(mpb.WithWaitGroup(&wg), mpb.WithDebugOutput(os.Stderr)) + // passed wg will be accounted at p.Wait() call + p := mpb.New( + mpb.WithWaitGroup(&wg), + mpb.WithDebugOutput(os.Stderr), + ) - wantPanic := "Some really long panic panic panic panic panic panic panic, really it is very long" + wantPanic := strings.Repeat("Panic ", 64) numBars := 3 wg.Add(numBars) @@ -30,13 +35,13 @@ } }() } - + // wait for passed wg and for all bars to complete and flush p.Wait() } func panicDecorator(name, panicMsg string) decor.Decorator { - return decor.Any(func(s *decor.Statistics) string { - if s.ID == 1 && s.Current >= 42 { + return decor.Any(func(st decor.Statistics) string { + if st.ID == 1 && st.Current >= 42 { panic(panicMsg) } return name diff --git a/_examples/poplog/go.mod b/_examples/poplog/go.mod index a6ab5a9..0997bc9 100644 --- a/_examples/poplog/go.mod +++ b/_examples/poplog/go.mod @@ -2,4 +2,4 @@ go 1.14 -require github.com/vbauerster/mpb/v5 v5.0.3 +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/poplog/main.go b/_examples/poplog/main.go index 3ff054a..b481a1a 100644 --- a/_examples/poplog/main.go +++ b/_examples/poplog/main.go @@ -2,69 +2,44 @@ import ( "fmt" - "io" "math/rand" - "sync" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func main() { p := mpb.New(mpb.PopCompletedMode()) - total, numBars := 100, 2 + total, numBars := 100, 4 for i := 0; i < numBars; i++ { name := fmt.Sprintf("Bar#%d:", i) bar := p.AddBar(int64(total), - mpb.BarNoPop(), + mpb.BarFillerOnComplete(fmt.Sprintf("%s has been completed", name)), + mpb.BarFillerTrim(), mpb.PrependDecorators( - decor.Name(name), - decor.Percentage(decor.WCSyncSpace), + decor.OnComplete(decor.Name(name), ""), + decor.OnComplete(decor.NewPercentage(" % d "), ""), ), mpb.AppendDecorators( - decor.OnComplete( - decor.EwmaETA(decor.ET_STYLE_GO, 60), "done!", - ), + decor.OnComplete(decor.Name(" "), ""), + decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_GO, 60), ""), ), ) // simulating some work - go func() { - rng := rand.New(rand.NewSource(time.Now().UnixNano())) - max := 100 * time.Millisecond - for i := 0; i < total; i++ { - // start variable is solely for EWMA calculation - // EWMA's unit of measure is an iteration's duration - start := time.Now() - time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) - bar.Increment() - // we need to call DecoratorEwmaUpdate to fulfill ewma decorator's contract - bar.DecoratorEwmaUpdate(time.Since(start)) - } - }() + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + // start variable is solely for EWMA calculation + // EWMA's unit of measure is an iteration's duration + start := time.Now() + time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) + bar.Increment() + // we need to call DecoratorEwmaUpdate to fulfill ewma decorator's contract + bar.DecoratorEwmaUpdate(time.Since(start)) + } } - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - rng := rand.New(rand.NewSource(time.Now().UnixNano())) - max := 3000 * time.Millisecond - for i := 0; i < 10; i++ { - filler := makeLogBar(fmt.Sprintf("some log: %d", i)) - p.Add(0, filler).SetTotal(0, true) - time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) - } - }() - - wg.Wait() p.Wait() } - -func makeLogBar(msg string) mpb.BarFiller { - limit := "%%.%ds" - return mpb.BarFillerFunc(func(w io.Writer, width int, st *decor.Statistics) { - fmt.Fprintf(w, fmt.Sprintf(limit, width), msg) - }) -} diff --git a/_examples/quietMode/go.mod b/_examples/quietMode/go.mod index c856e05..6d43d51 100644 --- a/_examples/quietMode/go.mod +++ b/_examples/quietMode/go.mod @@ -2,4 +2,4 @@ go 1.14 -require github.com/vbauerster/mpb/v5 v5.0.3 +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/quietMode/main.go b/_examples/quietMode/main.go index a918175..c3c7d71 100644 --- a/_examples/quietMode/main.go +++ b/_examples/quietMode/main.go @@ -7,8 +7,8 @@ "sync" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) var quietMode bool @@ -20,16 +20,16 @@ func main() { flag.Parse() var wg sync.WaitGroup - // pass &wg (optional), so p will wait for it eventually + // passed wg will be accounted at p.Wait() call p := mpb.New( mpb.WithWaitGroup(&wg), - mpb.ContainerOptOn( + mpb.ContainerOptional( // setting to nil will: - // set output to ioutil.Discard and disable internal refresh rate - // cycling, in order to not consume much CPU, hovewer a single refresh - // still will be triggered on bar complete event, per each bar. + // set output to ioutil.Discard and disable refresh rate cycle, in + // order not to consume much CPU. Hovewer a single refresh still will + // be triggered on bar complete event, per each bar. mpb.WithOutput(nil), - func() bool { return quietMode }, + quietMode, ), ) total, numBars := 100, 3 @@ -68,7 +68,7 @@ } }() } - // Waiting for passed &wg and for all bars to complete and flush + // wait for passed wg and for all bars to complete and flush p.Wait() fmt.Println("done") } diff --git a/_examples/remove/go.mod b/_examples/remove/go.mod index 2f0ce70..5c0f6bf 100644 --- a/_examples/remove/go.mod +++ b/_examples/remove/go.mod @@ -2,4 +2,4 @@ go 1.14 -require github.com/vbauerster/mpb/v5 v5.0.3 +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/remove/main.go b/_examples/remove/main.go index 1836ee8..416df82 100644 --- a/_examples/remove/main.go +++ b/_examples/remove/main.go @@ -6,12 +6,13 @@ "sync" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func main() { var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call p := mpb.New(mpb.WithWaitGroup(&wg)) total := 100 numBars := 3 @@ -21,32 +22,34 @@ name := fmt.Sprintf("Bar#%d:", i) bar := p.AddBar(int64(total), mpb.BarID(i), - mpb.BarOptOn(mpb.BarRemoveOnComplete(), func() bool { return i == 0 }), + mpb.BarOptional(mpb.BarRemoveOnComplete(), i == 0), mpb.PrependDecorators( decor.Name(name), - decor.EwmaETA(decor.ET_STYLE_GO, 60, decor.WCSyncSpace), ), - mpb.AppendDecorators(decor.Percentage()), + mpb.AppendDecorators( + decor.Any(func(s decor.Statistics) string { + return fmt.Sprintf("completed: %v", s.Completed) + }, decor.WCSyncSpaceR), + decor.Any(func(s decor.Statistics) string { + return fmt.Sprintf("aborted: %v", s.Aborted) + }, decor.WCSyncSpaceR), + decor.OnComplete(decor.NewPercentage("%d", decor.WCSyncSpace), "done"), + decor.OnAbort(decor.NewPercentage("%d", decor.WCSyncSpace), "ohno"), + ), ) go func() { defer wg.Done() rng := rand.New(rand.NewSource(time.Now().UnixNano())) max := 100 * time.Millisecond for i := 0; !bar.Completed(); i++ { - // start variable is solely for EWMA calculation - // EWMA's unit of measure is an iteration's duration - start := time.Now() if bar.ID() == 2 && i >= 42 { - // aborting and removing while bar is running - bar.Abort(true) + bar.Abort(false) } time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) bar.Increment() - // we need to call DecoratorEwmaUpdate to fulfill ewma decorator's contract - bar.DecoratorEwmaUpdate(time.Since(start)) } }() } - + // wait for passed wg and for all bars to complete and flush p.Wait() } diff --git a/_examples/reverseBar/go.mod b/_examples/reverseBar/go.mod index b279926..a6b31e6 100644 --- a/_examples/reverseBar/go.mod +++ b/_examples/reverseBar/go.mod @@ -2,4 +2,4 @@ go 1.14 -require github.com/vbauerster/mpb/v5 v5.0.3 +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/reverseBar/main.go b/_examples/reverseBar/main.go index 2993f50..542b63c 100644 --- a/_examples/reverseBar/main.go +++ b/_examples/reverseBar/main.go @@ -6,22 +6,20 @@ "sync" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func main() { var wg sync.WaitGroup - // pass &wg (optional), so p will wait for it eventually + // passed wg will be accounted at p.Wait() call p := mpb.New(mpb.WithWaitGroup(&wg)) total, numBars := 100, 3 wg.Add(numBars) for i := 0; i < numBars; i++ { name := fmt.Sprintf("Bar#%d:", i) - bar := p.AddBar(int64(total), - // reverse Bar#1 - mpb.BarOptOn(mpb.BarReverse(), func() bool { return i == 1 }), + bar := p.New(int64(total), condBuilder(i == 1), mpb.PrependDecorators( // simple name decorator decor.Name(name), @@ -52,6 +50,17 @@ } }() } - // Waiting for passed &wg and for all bars to complete and flush + // wait for passed wg and for all bars to complete and flush p.Wait() } + +func condBuilder(cond bool) mpb.BarFillerBuilder { + return mpb.BarFillerBuilderFunc(func() mpb.BarFiller { + bs := mpb.BarStyle() + if cond { + // reverse Bar on cond + bs = bs.Tip("<").Reverse() + } + return bs.Build() + }) +} diff --git a/_examples/singleBar/go.mod b/_examples/singleBar/go.mod index c2a7d8c..9aeaab6 100644 --- a/_examples/singleBar/go.mod +++ b/_examples/singleBar/go.mod @@ -2,4 +2,4 @@ go 1.14 -require github.com/vbauerster/mpb/v5 v5.0.3 +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/singleBar/main.go b/_examples/singleBar/main.go index 5c6029f..925a2d5 100644 --- a/_examples/singleBar/main.go +++ b/_examples/singleBar/main.go @@ -4,8 +4,8 @@ "math/rand" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func main() { @@ -14,10 +14,10 @@ total := 100 name := "Single Bar:" - // adding a single bar, which will inherit container's width - bar := p.AddBar(int64(total), - // override DefaultBarStyle, which is "[=>-]<+" - mpb.BarStyle("╢▌▌░╟"), + // create a single bar, which will inherit container's width + bar := p.New(int64(total), + // BarFillerBuilder with custom style + mpb.BarStyle().Lbound("╢").Filler("▌").Tip("▌").Padding("░").Rbound("╟"), mpb.PrependDecorators( // display our name with one space on the right decor.Name(name, decor.WC{W: len(name) + 1, C: decor.DidentRight}), diff --git a/_examples/spinTipBar/go.mod b/_examples/spinTipBar/go.mod new file mode 100644 index 0000000..2a372f4 --- /dev/null +++ b/_examples/spinTipBar/go.mod @@ -0,0 +1,5 @@ +module github.com/vbauerster/mpb/_examples/spinTipBar + +go 1.14 + +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/spinTipBar/main.go b/_examples/spinTipBar/main.go new file mode 100644 index 0000000..7d3f2ef --- /dev/null +++ b/_examples/spinTipBar/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "math/rand" + "time" + + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" +) + +func main() { + // initialize progress container, with custom width + p := mpb.New(mpb.WithWidth(80)) + + total := 100 + name := "Single Bar:" + bar := p.New(int64(total), + mpb.BarStyle().Tip(`-`, `\`, `|`, `/`), + mpb.PrependDecorators(decor.Name(name)), + mpb.AppendDecorators(decor.Percentage()), + ) + // simulating some work + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) + bar.Increment() + } + // wait for our bar to complete and flush + p.Wait() +} diff --git a/_examples/spinnerBar/go.mod b/_examples/spinnerBar/go.mod index 05b8407..0875389 100644 --- a/_examples/spinnerBar/go.mod +++ b/_examples/spinnerBar/go.mod @@ -2,4 +2,4 @@ go 1.14 -require github.com/vbauerster/mpb/v5 v5.0.3 +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/spinnerBar/main.go b/_examples/spinnerBar/main.go index fb4d910..db3e080 100644 --- a/_examples/spinnerBar/main.go +++ b/_examples/spinnerBar/main.go @@ -6,56 +6,35 @@ "sync" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func main() { var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call p := mpb.New( mpb.WithWaitGroup(&wg), - mpb.WithWidth(13), + mpb.WithWidth(16), ) total, numBars := 101, 3 wg.Add(numBars) for i := 0; i < numBars; i++ { name := fmt.Sprintf("Bar#%d:", i) - var bar *mpb.Bar - if i == 0 { - bar = p.AddBar(int64(total), - // override mpb.DefaultBarStyle, which is "[=>-]<+" - mpb.BarStyle("╢▌▌░╟"), - mpb.PrependDecorators( - // simple name decorator - decor.Name(name), + bar := p.New(int64(total), condBuilder(i != 0), + mpb.PrependDecorators( + // simple name decorator + decor.Name(name), + ), + mpb.AppendDecorators( + // replace ETA decorator with "done" message, OnComplete event + decor.OnComplete( + // ETA decorator with ewma age of 60 + decor.EwmaETA(decor.ET_STYLE_GO, 60), "done", ), - mpb.AppendDecorators( - // replace ETA decorator with "done" message, OnComplete event - decor.OnComplete( - // ETA decorator with ewma age of 60 - decor.EwmaETA(decor.ET_STYLE_GO, 60), "done", - ), - ), - ) - } else { - bar = p.AddSpinner(int64(total), mpb.SpinnerOnMiddle, - // override mpb.DefaultSpinnerStyle - mpb.SpinnerStyle([]string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"}), - mpb.PrependDecorators( - // simple name decorator - decor.Name(name), - ), - mpb.AppendDecorators( - // replace ETA decorator with "done" message, OnComplete event - decor.OnComplete( - // ETA decorator with ewma age of 60 - decor.EwmaETA(decor.ET_STYLE_GO, 60), "done", - ), - ), - ) - } - + ), + ) // simulating some work go func() { defer wg.Done() @@ -72,6 +51,17 @@ } }() } - // wait for all bars to complete and flush + // wait for passed wg and for all bars to complete and flush p.Wait() } + +func condBuilder(cond bool) mpb.BarFillerBuilder { + return mpb.BarFillerBuilderFunc(func() mpb.BarFiller { + if cond { + // spinner Bar on cond + frames := []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"} + return mpb.SpinnerStyle(frames...).Build() + } + return mpb.BarStyle().Lbound("╢").Filler("▌").Tip("▌").Padding("░").Rbound("╟").Build() + }) +} diff --git a/_examples/spinnerDecorator/go.mod b/_examples/spinnerDecorator/go.mod index 5fe9df5..c9050fd 100644 --- a/_examples/spinnerDecorator/go.mod +++ b/_examples/spinnerDecorator/go.mod @@ -2,4 +2,4 @@ go 1.14 -require github.com/vbauerster/mpb/v5 v5.0.3 +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/spinnerDecorator/main.go b/_examples/spinnerDecorator/main.go index d0499e0..3ef27ba 100644 --- a/_examples/spinnerDecorator/main.go +++ b/_examples/spinnerDecorator/main.go @@ -6,13 +6,13 @@ "sync" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func main() { var wg sync.WaitGroup - // pass &wg (optional), so p will wait for it eventually + // passed wg will be accounted at p.Wait() call p := mpb.New(mpb.WithWaitGroup(&wg), mpb.WithWidth(64)) total, numBars := 100, 3 wg.Add(numBars) @@ -44,6 +44,6 @@ } }() } - // Waiting for passed &wg and for all bars to complete and flush + // wait for passed wg and for all bars to complete and flush p.Wait() } diff --git a/_examples/stress/go.mod b/_examples/stress/go.mod index c391186..0ed6759 100644 --- a/_examples/stress/go.mod +++ b/_examples/stress/go.mod @@ -2,4 +2,4 @@ go 1.14 -require github.com/vbauerster/mpb/v5 v5.0.3 +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/stress/main.go b/_examples/stress/main.go index ec46fd3..1b03731 100644 --- a/_examples/stress/main.go +++ b/_examples/stress/main.go @@ -6,8 +6,8 @@ "sync" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) const ( @@ -16,10 +16,8 @@ func main() { var wg sync.WaitGroup - p := mpb.New( - mpb.WithWaitGroup(&wg), - mpb.WithRefreshRate(50*time.Millisecond), - ) + // passed wg will be accounted at p.Wait() call + p := mpb.New(mpb.WithWaitGroup(&wg)) wg.Add(totalBars) for i := 0; i < totalBars; i++ { @@ -47,6 +45,6 @@ } }() } - + // wait for passed wg and for all bars to complete and flush p.Wait() } diff --git a/_examples/suppressBar/go.mod b/_examples/suppressBar/go.mod index 93845c4..1856f06 100644 --- a/_examples/suppressBar/go.mod +++ b/_examples/suppressBar/go.mod @@ -2,4 +2,7 @@ go 1.14 -require github.com/vbauerster/mpb/v5 v5.0.3 +require ( + github.com/mattn/go-runewidth v0.0.13 + github.com/vbauerster/mpb/v7 v7.3.2 +) diff --git a/_examples/suppressBar/main.go b/_examples/suppressBar/main.go index 906f2f9..7dc38c0 100644 --- a/_examples/suppressBar/main.go +++ b/_examples/suppressBar/main.go @@ -8,8 +8,9 @@ "sync" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/mattn/go-runewidth" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func main() { @@ -18,14 +19,31 @@ total := 100 msgCh := make(chan string) resumeCh := make(chan struct{}) - filler, nextCh := newCustomFiller(msgCh, resumeCh) - bar := p.Add(int64(total), filler, - mpb.PrependDecorators( - decor.Name("my bar:"), - ), - mpb.AppendDecorators( - newCustomPercentage(nextCh), - ), + nextCh := make(chan struct{}, 1) + bar := p.AddBar(int64(total), + mpb.BarFillerMiddleware(func(base mpb.BarFiller) mpb.BarFiller { + var msg *string + return mpb.BarFillerFunc(func(w io.Writer, reqWidth int, st decor.Statistics) { + select { + case m := <-msgCh: + defer func() { + msg = &m + }() + nextCh <- struct{}{} + case <-resumeCh: + msg = nil + default: + } + if msg != nil { + io.WriteString(w, runewidth.Truncate(*msg, st.AvailableWidth, "…")) + nextCh <- struct{}{} + } else { + base.Fill(w, reqWidth, st) + } + }) + }), + mpb.PrependDecorators(decor.Name("my bar:")), + mpb.AppendDecorators(newCustomPercentage(nextCh)), ) ew := &errorWrapper{} time.AfterFunc(2*time.Second, func() { @@ -76,54 +94,15 @@ ew.Unlock() } -type myBarFiller struct { - mpb.BarFiller - base mpb.BarFiller -} - -func (cf *myBarFiller) Base() mpb.BarFiller { - return cf.base -} - -func newCustomFiller(ch <-chan string, resume <-chan struct{}) (mpb.BarFiller, <-chan struct{}) { - base := mpb.NewBarFiller(mpb.DefaultBarStyle, false) - nextCh := make(chan struct{}, 1) - var msg *string - filler := mpb.BarFillerFunc(func(w io.Writer, width int, st *decor.Statistics) { +func newCustomPercentage(nextCh <-chan struct{}) decor.Decorator { + base := decor.Percentage() + fn := func(s decor.Statistics) string { select { - case m := <-ch: - defer func() { - msg = &m - }() - nextCh <- struct{}{} - case <-resume: - msg = nil - default: - } - if msg != nil { - limitFmt := fmt.Sprintf("%%.%ds", width) - fmt.Fprintf(w, limitFmt, *msg) - nextCh <- struct{}{} - } else { - base.Fill(w, width, st) - } - }) - cf := &myBarFiller{ - BarFiller: filler, - base: base, - } - return cf, nextCh -} - -func newCustomPercentage(ch <-chan struct{}) decor.Decorator { - base := decor.Percentage() - f := func(s *decor.Statistics) string { - select { - case <-ch: + case <-nextCh: return "" default: return base.Decor(s) } } - return decor.Any(f) + return decor.Any(fn) } diff --git a/_examples/tipOnComplete/go.mod b/_examples/tipOnComplete/go.mod new file mode 100644 index 0000000..c5e030b --- /dev/null +++ b/_examples/tipOnComplete/go.mod @@ -0,0 +1,5 @@ +module github.com/vbauerster/mpb/_examples/tipOnComplete + +go 1.14 + +require github.com/vbauerster/mpb/v7 v7.3.2 diff --git a/_examples/tipOnComplete/main.go b/_examples/tipOnComplete/main.go new file mode 100644 index 0000000..62a26b7 --- /dev/null +++ b/_examples/tipOnComplete/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "math/rand" + "time" + + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" +) + +func main() { + // initialize progress container, with custom width + p := mpb.New(mpb.WithWidth(80)) + + total := 100 + name := "Single Bar:" + bar := p.New(int64(total), + mpb.BarStyle().TipOnComplete(">"), + mpb.PrependDecorators(decor.Name(name)), + mpb.AppendDecorators(decor.Percentage()), + ) + // simulating some work + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) + bar.Increment() + } + // wait for our bar to complete and flush + p.Wait() +} diff --git a/bar.go b/bar.go index 1a4c66f..646cb47 100644 --- a/bar.go +++ b/bar.go @@ -5,91 +5,80 @@ "context" "fmt" "io" - "log" + "runtime/debug" "strings" + "sync" "time" - "unicode/utf8" - - "github.com/vbauerster/mpb/v5/decor" + + "github.com/acarl005/stripansi" + "github.com/mattn/go-runewidth" + "github.com/vbauerster/mpb/v7/decor" ) -// BarFiller interface. -// Bar renders itself by calling BarFiller's Fill method. You can -// literally have any bar kind, by implementing this interface and -// passing it to the *Progress.Add(...) *Bar method. -type BarFiller interface { - Fill(w io.Writer, width int, stat *decor.Statistics) -} - -// BarFillerFunc is function type adapter to convert function into Filler. -type BarFillerFunc func(w io.Writer, width int, stat *decor.Statistics) - -func (f BarFillerFunc) Fill(w io.Writer, width int, stat *decor.Statistics) { - f(w, width, stat) -} - -// Bar represents a progress Bar. +// Bar represents a progress bar. type Bar struct { priority int // used by heap index int // used by heap - extendedLines int toShutdown bool toDrop bool noPop bool hasEwmaDecorators bool operateState chan func(*bState) - frameCh chan io.Reader - syncTableCh chan [][]chan int - completed chan bool + frameCh chan *frame // cancel is called either by user or on complete event cancel func() // done is closed after cacheState is assigned done chan struct{} - // cacheState is populated, right after close(shutdown) + // cacheState is populated, right after close(b.done) cacheState *bState container *Progress - dlogger *log.Logger recoveredPanic interface{} } -type extFunc func(in io.Reader, tw int, st *decor.Statistics) (out io.Reader, lines int) - +type extenderFunc func(in io.Reader, reqWidth int, st decor.Statistics) (out io.Reader, lines int) + +// bState is actual bar state. It gets passed to *Bar.serve(...) monitor +// goroutine. type bState struct { - baseF BarFiller - filler BarFiller id int - width int + priority int + reqWidth int total int64 current int64 - lastN int64 - iterated bool + refill int64 + lastIncrement int64 trimSpace bool - toComplete bool + completed bool completeFlushed bool + aborted bool + triggerComplete bool + dropOnComplete bool noPop bool aDecorators []decor.Decorator pDecorators []decor.Decorator averageDecorators []decor.AverageDecorator ewmaDecorators []decor.EwmaDecorator shutdownListeners []decor.ShutdownListener - bufP, bufB, bufA *bytes.Buffer - extender extFunc - - // priority overrides *Bar's priority, if set - priority int - // dropOnComplete propagates to *Bar - dropOnComplete bool + buffers [3]*bytes.Buffer + filler BarFiller + middleware func(BarFiller) BarFiller + extender extenderFunc + // runningBar is a key for *pState.parkedBars runningBar *Bar debugOut io.Writer } +type frame struct { + reader io.Reader + lines int +} + func newBar(container *Progress, bs *bState) *Bar { - logPrefix := fmt.Sprintf("%sbar#%02d ", container.dlogger.Prefix(), bs.id) ctx, cancel := context.WithCancel(container.ctx) bar := &Bar{ @@ -98,12 +87,9 @@ toDrop: bs.dropOnComplete, noPop: bs.noPop, operateState: make(chan func(*bState)), - frameCh: make(chan io.Reader, 1), - syncTableCh: make(chan [][]chan int), - completed: make(chan bool, 1), + frameCh: make(chan *frame, 1), done: make(chan struct{}), cancel: cancel, - dlogger: log.New(bs.debugOut, logPrefix, log.Lshortfile), } go bar.serve(ctx, bs) @@ -116,7 +102,7 @@ if r == nil { panic("expected non nil io.Reader") } - return newProxyReader(r, b) + return b.newProxyReader(r) } // ID returs id of the bar. @@ -141,23 +127,24 @@ } } -// SetRefill fills bar with refill rune up to amount argument. -// Given default bar style is "[=>-]<+", refill rune is '+'. -// To set bar style use mpb.BarStyle(string) BarOption. +// SetRefill sets refill flag with specified amount. +// The underlying BarFiller will change its visual representation, to +// indicate refill event. Refill event may be referred to some retry +// operation for example. func (b *Bar) SetRefill(amount int64) { - type refiller interface { - SetRefill(int64) - } - b.operateState <- func(s *bState) { - if f, ok := s.baseF.(refiller); ok { - f.SetRefill(amount) - } + select { + case b.operateState <- func(s *bState) { + s.refill = amount + }: + case <-b.done: } } // TraverseDecorators traverses all available decorators and calls cb func on each. func (b *Bar) TraverseDecorators(cb func(decor.Decorator)) { - b.operateState <- func(s *bState) { + done := make(chan struct{}) + select { + case b.operateState <- func(s *bState) { for _, decorators := range [...][]decor.Decorator{ s.pDecorators, s.aDecorators, @@ -166,24 +153,28 @@ cb(extractBaseDecorator(d)) } } + close(done) + }: + <-done + case <-b.done: } } // SetTotal sets total dynamically. -// If total is less or equal to zero it takes progress' current value. -// If complete is true, complete event will be triggered. -func (b *Bar) SetTotal(total int64, complete bool) { - select { - case b.operateState <- func(s *bState) { - if total <= 0 { +// If total is negative it takes progress' current value. +func (b *Bar) SetTotal(total int64, triggerComplete bool) { + select { + case b.operateState <- func(s *bState) { + s.triggerComplete = triggerComplete + if total < 0 { s.total = s.current } else { s.total = total } - if complete && !s.toComplete { + if s.triggerComplete && !s.completed { s.current = s.total - s.toComplete = true - go b.refreshTillShutdown() + s.completed = true + go b.forceRefreshIfLastUncompleted() } }: case <-b.done: @@ -191,16 +182,16 @@ } // SetCurrent sets progress' current to an arbitrary value. +// Setting a negative value will cause a panic. func (b *Bar) SetCurrent(current int64) { select { case b.operateState <- func(s *bState) { - s.iterated = true - s.lastN = current - s.current + s.lastIncrement = current - s.current s.current = current - if s.total > 0 && s.current >= s.total { + if s.triggerComplete && s.current >= s.total { s.current = s.total - s.toComplete = true - go b.refreshTillShutdown() + s.completed = true + go b.forceRefreshIfLastUncompleted() } }: case <-b.done: @@ -219,15 +210,17 @@ // IncrInt64 increments progress by amount of n. func (b *Bar) IncrInt64(n int64) { - select { - case b.operateState <- func(s *bState) { - s.iterated = true - s.lastN = n + if n <= 0 { + return + } + select { + case b.operateState <- func(s *bState) { + s.lastIncrement = n s.current += n - if s.total > 0 && s.current >= s.total { + if s.triggerComplete && s.current >= s.total { s.current = s.total - s.toComplete = true - go b.refreshTillShutdown() + s.completed = true + go b.forceRefreshIfLastUncompleted() } }: case <-b.done: @@ -241,10 +234,18 @@ func (b *Bar) DecoratorEwmaUpdate(dur time.Duration) { select { case b.operateState <- func(s *bState) { - ewmaIterationUpdate(false, s, dur) - }: - case <-b.done: - ewmaIterationUpdate(true, b.cacheState, dur) + if s.lastIncrement > 0 { + s.decoratorEwmaUpdate(dur) + s.lastIncrement = 0 + } else { + panic("increment required before ewma iteration update") + } + }: + case <-b.done: + if b.cacheState.lastIncrement > 0 { + b.cacheState.decoratorEwmaUpdate(dur) + b.cacheState.lastIncrement = 0 + } } } @@ -254,9 +255,7 @@ func (b *Bar) DecoratorAverageAdjust(start time.Time) { select { case b.operateState <- func(s *bState) { - for _, d := range s.averageDecorators { - d.AverageAdjust(start) - } + s.decoratorAverageAdjust(start) }: case <-b.done: } @@ -266,32 +265,55 @@ // priority, i.e. bar will be on top. If you don't need to set priority // dynamically, better use BarPriority option. func (b *Bar) SetPriority(priority int) { - select { - case <-b.done: - default: - b.container.setBarPriority(b, priority) - } -} - -// Abort interrupts bar's running goroutine. Call this, if you'd like -// to stop/remove bar before completion event. It has no effect after -// completion event. If drop is true bar will be removed as well. + b.container.UpdateBarPriority(b, priority) +} + +// Abort interrupts bar's running goroutine. Abort won't be engaged +// if bar is already in complete state. If drop is true bar will be +// removed as well. func (b *Bar) Abort(drop bool) { - select { - case <-b.done: - default: - if drop { - b.container.dropBar(b) - } + done := make(chan struct{}) + select { + case b.operateState <- func(s *bState) { + if s.completed { + close(done) + return + } + s.aborted = true b.cancel() + // container must be run during lifetime of this inner goroutine + // we control this by done channel declared above + go func() { + if drop { + b.container.dropBar(b) + } else { + var uncompleted int + b.container.traverseBars(func(bar *Bar) bool { + if b != bar && !bar.Completed() { + uncompleted++ + return false + } + return true + }) + if uncompleted == 0 { + b.container.refreshCh <- time.Now() + } + } + close(done) // release hold of Abort + }() + }: + // guarantee: container is alive during lifetime of this hold + <-done + case <-b.done: } } // Completed reports whether the bar is in completed state. func (b *Bar) Completed() bool { - select { - case b.operateState <- func(s *bState) { b.completed <- s.toComplete }: - return <-b.completed + result := make(chan bool) + select { + case b.operateState <- func(s *bState) { result <- s.completed }: + return <-result case <-b.done: return true } @@ -304,54 +326,49 @@ case op := <-b.operateState: op(s) case <-ctx.Done(): + s.decoratorShutdownNotify() b.cacheState = s close(b.done) - // Notifying decorators about shutdown event - for _, sl := range s.shutdownListeners { - sl.Shutdown() - } return } } } func (b *Bar) render(tw int) { - if b.recoveredPanic != nil { - b.toShutdown = false - b.frameCh <- b.panicToFrame(tw) - return - } - select { - case b.operateState <- func(s *bState) { + select { + case b.operateState <- func(s *bState) { + stat := newStatistics(tw, s) defer func() { // recovering if user defined decorator panics for example if p := recover(); p != nil { - b.dlogger.Println(p) - b.recoveredPanic = p - b.toShutdown = !s.completeFlushed - b.frameCh <- b.panicToFrame(tw) + if b.recoveredPanic == nil { + if s.debugOut != nil { + fmt.Fprintln(s.debugOut, p) + _, _ = s.debugOut.Write(debug.Stack()) + } + s.extender = makePanicExtender(p) + b.toShutdown = !b.toShutdown + b.recoveredPanic = p + } + reader, lines := s.extender(nil, s.reqWidth, stat) + b.frameCh <- &frame{reader, lines + 1} } + s.completeFlushed = s.completed }() - - st := newStatistics(s) - frame := s.draw(tw, st) - frame, b.extendedLines = s.extender(frame, tw, st) - - b.toShutdown = s.toComplete && !s.completeFlushed - s.completeFlushed = s.toComplete - b.frameCh <- frame + reader, lines := s.extender(s.draw(stat), s.reqWidth, stat) + b.toShutdown = s.completed && !s.completeFlushed + b.frameCh <- &frame{reader, lines + 1} }: case <-b.done: s := b.cacheState - st := newStatistics(s) - frame := s.draw(tw, st) - frame, b.extendedLines = s.extender(frame, tw, st) - b.frameCh <- frame - } -} - -func (b *Bar) panicToFrame(termWidth int) io.Reader { - return strings.NewReader(fmt.Sprintf(fmt.Sprintf("%%.%dv\n", termWidth), b.recoveredPanic)) + stat := newStatistics(tw, s) + var r io.Reader + if b.recoveredPanic == nil { + r = s.draw(stat) + } + reader, lines := s.extender(r, s.reqWidth, stat) + b.frameCh <- &frame{reader, lines + 1} + } } func (b *Bar) subscribeDecorators() { @@ -369,61 +386,83 @@ shutdownListeners = append(shutdownListeners, d) } }) - b.operateState <- func(s *bState) { + b.hasEwmaDecorators = len(ewmaDecorators) != 0 + select { + case b.operateState <- func(s *bState) { s.averageDecorators = averageDecorators s.ewmaDecorators = ewmaDecorators s.shutdownListeners = shutdownListeners - } - b.hasEwmaDecorators = len(ewmaDecorators) != 0 -} - -func (b *Bar) refreshTillShutdown() { - for { - select { - case b.container.refreshCh <- time.Now(): - case <-b.done: - return + }: + case <-b.done: + } +} + +func (b *Bar) forceRefreshIfLastUncompleted() { + var uncompleted int + b.container.traverseBars(func(bar *Bar) bool { + if b != bar && !bar.Completed() { + uncompleted++ + return false + } + return true + }) + if uncompleted == 0 { + for { + select { + case b.container.refreshCh <- time.Now(): + case <-b.done: + return + } } } } func (b *Bar) wSyncTable() [][]chan int { - select { - case b.operateState <- func(s *bState) { b.syncTableCh <- s.wSyncTable() }: - return <-b.syncTableCh + result := make(chan [][]chan int) + select { + case b.operateState <- func(s *bState) { result <- s.wSyncTable() }: + return <-result case <-b.done: return b.cacheState.wSyncTable() } } -func (s *bState) draw(termWidth int, stat *decor.Statistics) io.Reader { +func (s *bState) draw(stat decor.Statistics) io.Reader { + bufP, bufB, bufA := s.buffers[0], s.buffers[1], s.buffers[2] + nlr := strings.NewReader("\n") + tw := stat.AvailableWidth for _, d := range s.pDecorators { - s.bufP.WriteString(d.Decor(stat)) - } - + str := d.Decor(stat) + stat.AvailableWidth -= runewidth.StringWidth(stripansi.Strip(str)) + bufP.WriteString(str) + } + if stat.AvailableWidth < 1 { + trunc := strings.NewReader(runewidth.Truncate(stripansi.Strip(bufP.String()), tw, "…")) + bufP.Reset() + return io.MultiReader(trunc, nlr) + } + + if !s.trimSpace && stat.AvailableWidth > 1 { + stat.AvailableWidth -= 2 + bufB.WriteByte(' ') + defer bufB.WriteByte(' ') + } + + tw = stat.AvailableWidth for _, d := range s.aDecorators { - s.bufA.WriteString(d.Decor(stat)) - } - - s.bufA.WriteByte('\n') - - prependCount := utf8.RuneCount(s.bufP.Bytes()) - appendCount := utf8.RuneCount(s.bufA.Bytes()) - 1 - - if fitWidth := s.width; termWidth > 1 { - if !s.trimSpace { - // reserve space for edge spaces - termWidth -= 2 - s.bufB.WriteByte(' ') - defer s.bufB.WriteByte(' ') - } - if prependCount+s.width+appendCount > termWidth { - fitWidth = termWidth - prependCount - appendCount - } - s.filler.Fill(s.bufB, fitWidth, stat) - } - - return io.MultiReader(s.bufP, s.bufB, s.bufA) + str := d.Decor(stat) + stat.AvailableWidth -= runewidth.StringWidth(stripansi.Strip(str)) + bufA.WriteString(str) + } + if stat.AvailableWidth < 1 { + trunc := strings.NewReader(runewidth.Truncate(stripansi.Strip(bufA.String()), tw, "…")) + bufA.Reset() + return io.MultiReader(bufP, bufB, trunc, nlr) + } + + s.filler.Fill(bufB, s.reqWidth, stat) + + return io.MultiReader(bufP, bufB, bufA, nlr) } func (s *bState) wSyncTable() [][]chan int { @@ -448,12 +487,66 @@ return table } -func newStatistics(s *bState) *decor.Statistics { - return &decor.Statistics{ - ID: s.id, - Completed: s.completeFlushed, - Total: s.total, - Current: s.current, +func (s bState) decoratorEwmaUpdate(dur time.Duration) { + wg := new(sync.WaitGroup) + for i := 0; i < len(s.ewmaDecorators); i++ { + switch d := s.ewmaDecorators[i]; i { + case len(s.ewmaDecorators) - 1: + d.EwmaUpdate(s.lastIncrement, dur) + default: + wg.Add(1) + go func() { + d.EwmaUpdate(s.lastIncrement, dur) + wg.Done() + }() + } + } + wg.Wait() +} + +func (s bState) decoratorAverageAdjust(start time.Time) { + wg := new(sync.WaitGroup) + for i := 0; i < len(s.averageDecorators); i++ { + switch d := s.averageDecorators[i]; i { + case len(s.averageDecorators) - 1: + d.AverageAdjust(start) + default: + wg.Add(1) + go func() { + d.AverageAdjust(start) + wg.Done() + }() + } + } + wg.Wait() +} + +func (s bState) decoratorShutdownNotify() { + wg := new(sync.WaitGroup) + for i := 0; i < len(s.shutdownListeners); i++ { + switch d := s.shutdownListeners[i]; i { + case len(s.shutdownListeners) - 1: + d.Shutdown() + default: + wg.Add(1) + go func() { + d.Shutdown() + wg.Done() + }() + } + } + wg.Wait() +} + +func newStatistics(tw int, s *bState) decor.Statistics { + return decor.Statistics{ + ID: s.id, + AvailableWidth: tw, + Total: s.total, + Current: s.current, + Refill: s.refill, + Completed: s.completeFlushed, + Aborted: s.aborted, } } @@ -464,13 +557,13 @@ return d } -func ewmaIterationUpdate(done bool, s *bState, dur time.Duration) { - if !done && !s.iterated { - panic("increment required before ewma iteration update") - } else { - s.iterated = false - } - for _, d := range s.ewmaDecorators { - d.EwmaUpdate(s.lastN, dur) - } -} +func makePanicExtender(p interface{}) extenderFunc { + pstr := fmt.Sprint(p) + return func(_ io.Reader, _ int, st decor.Statistics) (io.Reader, int) { + mr := io.MultiReader( + strings.NewReader(runewidth.Truncate(pstr, st.AvailableWidth, "…")), + strings.NewReader("\n"), + ) + return mr, 0 + } +} diff --git a/bar_filler.go b/bar_filler.go index 00bf0a4..81177fc 100644 --- a/bar_filler.go +++ b/bar_filler.go @@ -2,137 +2,49 @@ import ( "io" - "unicode/utf8" - "github.com/vbauerster/mpb/v5/decor" - "github.com/vbauerster/mpb/v5/internal" + "github.com/vbauerster/mpb/v7/decor" ) -const ( - rLeft = iota - rFill - rTip - rEmpty - rRight - rRevTip - rRefill -) - -// DefaultBarStyle is a string containing 7 runes. -// Each rune is a building block of a progress bar. +// BarFiller interface. +// Bar (without decorators) renders itself by calling BarFiller's Fill method. // -// '1st rune' stands for left boundary rune +// reqWidth is requested width set by `func WithWidth(int) ContainerOption`. +// If not set, it defaults to terminal width. // -// '2nd rune' stands for fill rune -// -// '3rd rune' stands for tip rune -// -// '4th rune' stands for empty rune -// -// '5th rune' stands for right boundary rune -// -// '6th rune' stands for reverse tip rune -// -// '7th rune' stands for refill rune -// -const DefaultBarStyle string = "[=>-]<+" - -type barFiller struct { - format [][]byte - tip []byte - refill int64 - reverse bool - flush func(w io.Writer, bb [][]byte) +type BarFiller interface { + Fill(w io.Writer, reqWidth int, stat decor.Statistics) } -// NewBarFiller constucts mpb.BarFiller, to be used with *Progress.Add(...) *Bar method. -func NewBarFiller(style string, reverse bool) BarFiller { - if style == "" { - style = DefaultBarStyle - } - bf := &barFiller{ - format: make([][]byte, utf8.RuneCountInString(style)), - reverse: reverse, - } - bf.SetStyle(style) - return bf +// BarFillerBuilder interface. +// Default implementations are: +// +// BarStyle() +// SpinnerStyle() +// NopStyle() +// +type BarFillerBuilder interface { + Build() BarFiller } -func (s *barFiller) SetStyle(style string) { - if !utf8.ValidString(style) { - return - } - src := make([][]byte, 0, utf8.RuneCountInString(style)) - for _, r := range style { - src = append(src, []byte(string(r))) - } - copy(s.format, src) - s.SetReverse(s.reverse) +// BarFillerFunc is function type adapter to convert compatible function +// into BarFiller interface. +type BarFillerFunc func(w io.Writer, reqWidth int, stat decor.Statistics) + +func (f BarFillerFunc) Fill(w io.Writer, reqWidth int, stat decor.Statistics) { + f(w, reqWidth, stat) } -func (s *barFiller) SetReverse(reverse bool) { - if reverse { - s.tip = s.format[rRevTip] - s.flush = reverseFlush - } else { - s.tip = s.format[rTip] - s.flush = normalFlush - } - s.reverse = reverse +// BarFillerBuilderFunc is function type adapter to convert compatible +// function into BarFillerBuilder interface. +type BarFillerBuilderFunc func() BarFiller + +func (f BarFillerBuilderFunc) Build() BarFiller { + return f() } -func (s *barFiller) SetRefill(amount int64) { - s.refill = amount +// NewBarFiller constructs a BarFiller from provided BarFillerBuilder. +// Deprecated. Prefer using `*Progress.New(...)` directly. +func NewBarFiller(b BarFillerBuilder) BarFiller { + return b.Build() } - -func (s *barFiller) Fill(w io.Writer, width int, stat *decor.Statistics) { - // don't count rLeft and rRight as progress - width -= 2 - if width < 2 { - return - } - w.Write(s.format[rLeft]) - defer w.Write(s.format[rRight]) - - bb := make([][]byte, width) - - cwidth := int(internal.PercentageRound(stat.Total, stat.Current, width)) - - for i := 0; i < cwidth; i++ { - bb[i] = s.format[rFill] - } - - if s.refill > 0 { - var rwidth int - if s.refill > stat.Current { - rwidth = cwidth - } else { - rwidth = int(internal.PercentageRound(stat.Total, int64(s.refill), width)) - } - for i := 0; i < rwidth; i++ { - bb[i] = s.format[rRefill] - } - } - - if cwidth > 0 && cwidth < width { - bb[cwidth-1] = s.tip - } - - for i := cwidth; i < width; i++ { - bb[i] = s.format[rEmpty] - } - - s.flush(w, bb) -} - -func normalFlush(w io.Writer, bb [][]byte) { - for i := 0; i < len(bb); i++ { - w.Write(bb[i]) - } -} - -func reverseFlush(w io.Writer, bb [][]byte) { - for i := len(bb) - 1; i >= 0; i-- { - w.Write(bb[i]) - } -} diff --git a/bar_filler_bar.go b/bar_filler_bar.go new file mode 100644 index 0000000..54b7bfd --- /dev/null +++ b/bar_filler_bar.go @@ -0,0 +1,256 @@ +package mpb + +import ( + "io" + + "github.com/acarl005/stripansi" + "github.com/mattn/go-runewidth" + "github.com/vbauerster/mpb/v7/decor" + "github.com/vbauerster/mpb/v7/internal" +) + +const ( + iLbound = iota + iRbound + iFiller + iRefiller + iPadding + components +) + +// BarStyleComposer interface. +type BarStyleComposer interface { + BarFillerBuilder + Lbound(string) BarStyleComposer + Rbound(string) BarStyleComposer + Filler(string) BarStyleComposer + Refiller(string) BarStyleComposer + Padding(string) BarStyleComposer + TipOnComplete(string) BarStyleComposer + Tip(frames ...string) BarStyleComposer + Reverse() BarStyleComposer +} + +type bFiller struct { + rev bool + components [components]*component + tip struct { + count uint + onComplete *component + frames []*component + } +} + +type component struct { + width int + bytes []byte +} + +type barStyle struct { + lbound string + rbound string + filler string + refiller string + padding string + tipOnComplete string + tipFrames []string + rev bool +} + +// BarStyle constructs default bar style which can be altered via +// BarStyleComposer interface. +func BarStyle() BarStyleComposer { + return &barStyle{ + lbound: "[", + rbound: "]", + filler: "=", + refiller: "+", + padding: "-", + tipFrames: []string{">"}, + } +} + +func (s *barStyle) Lbound(bound string) BarStyleComposer { + s.lbound = bound + return s +} + +func (s *barStyle) Rbound(bound string) BarStyleComposer { + s.rbound = bound + return s +} + +func (s *barStyle) Filler(filler string) BarStyleComposer { + s.filler = filler + return s +} + +func (s *barStyle) Refiller(refiller string) BarStyleComposer { + s.refiller = refiller + return s +} + +func (s *barStyle) Padding(padding string) BarStyleComposer { + s.padding = padding + return s +} + +func (s *barStyle) TipOnComplete(tip string) BarStyleComposer { + s.tipOnComplete = tip + return s +} + +func (s *barStyle) Tip(frames ...string) BarStyleComposer { + if len(frames) != 0 { + s.tipFrames = append(s.tipFrames[:0], frames...) + } + return s +} + +func (s *barStyle) Reverse() BarStyleComposer { + s.rev = true + return s +} + +func (s *barStyle) Build() BarFiller { + bf := &bFiller{rev: s.rev} + bf.components[iLbound] = &component{ + width: runewidth.StringWidth(stripansi.Strip(s.lbound)), + bytes: []byte(s.lbound), + } + bf.components[iRbound] = &component{ + width: runewidth.StringWidth(stripansi.Strip(s.rbound)), + bytes: []byte(s.rbound), + } + bf.components[iFiller] = &component{ + width: runewidth.StringWidth(stripansi.Strip(s.filler)), + bytes: []byte(s.filler), + } + bf.components[iRefiller] = &component{ + width: runewidth.StringWidth(stripansi.Strip(s.refiller)), + bytes: []byte(s.refiller), + } + bf.components[iPadding] = &component{ + width: runewidth.StringWidth(stripansi.Strip(s.padding)), + bytes: []byte(s.padding), + } + bf.tip.onComplete = &component{ + width: runewidth.StringWidth(stripansi.Strip(s.tipOnComplete)), + bytes: []byte(s.tipOnComplete), + } + bf.tip.frames = make([]*component, len(s.tipFrames)) + for i, t := range s.tipFrames { + bf.tip.frames[i] = &component{ + width: runewidth.StringWidth(stripansi.Strip(t)), + bytes: []byte(t), + } + } + return bf +} + +func (s *bFiller) Fill(w io.Writer, width int, stat decor.Statistics) { + width = internal.CheckRequestedWidth(width, stat.AvailableWidth) + brackets := s.components[iLbound].width + s.components[iRbound].width + // don't count brackets as progress + width -= brackets + if width < 0 { + return + } + + ow := optimisticWriter(w) + ow(s.components[iLbound].bytes) + defer ow(s.components[iRbound].bytes) + + if width == 0 { + return + } + + var filling [][]byte + var padding [][]byte + var tip *component + var filled int + var refWidth int + curWidth := int(internal.PercentageRound(stat.Total, stat.Current, uint(width))) + + if stat.Current >= stat.Total { + tip = s.tip.onComplete + } else { + tip = s.tip.frames[s.tip.count%uint(len(s.tip.frames))] + } + + if curWidth > 0 { + filling = append(filling, tip.bytes) + filled += tip.width + s.tip.count++ + } + + if stat.Refill > 0 { + refWidth = int(internal.PercentageRound(stat.Total, stat.Refill, uint(width))) + curWidth -= refWidth + refWidth += curWidth + } + + for filled < curWidth { + if curWidth-filled >= s.components[iFiller].width { + filling = append(filling, s.components[iFiller].bytes) + if s.components[iFiller].width == 0 { + break + } + filled += s.components[iFiller].width + } else { + filling = append(filling, []byte("…")) + filled++ + } + } + + for filled < refWidth { + if refWidth-filled >= s.components[iRefiller].width { + filling = append(filling, s.components[iRefiller].bytes) + if s.components[iRefiller].width == 0 { + break + } + filled += s.components[iRefiller].width + } else { + filling = append(filling, []byte("…")) + filled++ + } + } + + padWidth := width - filled + for padWidth > 0 { + if padWidth >= s.components[iPadding].width { + padding = append(padding, s.components[iPadding].bytes) + if s.components[iPadding].width == 0 { + break + } + padWidth -= s.components[iPadding].width + } else { + padding = append(padding, []byte("…")) + padWidth-- + } + } + + if s.rev { + flush(ow, padding, filling) + } else { + flush(ow, filling, padding) + } +} + +func flush(ow func([]byte), filling, padding [][]byte) { + for i := len(filling) - 1; i >= 0; i-- { + ow(filling[i]) + } + for i := 0; i < len(padding); i++ { + ow(padding[i]) + } +} + +func optimisticWriter(w io.Writer) func([]byte) { + return func(p []byte) { + _, err := w.Write(p) + if err != nil { + panic(err) + } + } +} diff --git a/bar_filler_nop.go b/bar_filler_nop.go new file mode 100644 index 0000000..1a7086f --- /dev/null +++ b/bar_filler_nop.go @@ -0,0 +1,14 @@ +package mpb + +import ( + "io" + + "github.com/vbauerster/mpb/v7/decor" +) + +// NopStyle provides BarFillerBuilder which builds NOP BarFiller. +func NopStyle() BarFillerBuilder { + return BarFillerBuilderFunc(func() BarFiller { + return BarFillerFunc(func(io.Writer, int, decor.Statistics) {}) + }) +} diff --git a/bar_filler_spinner.go b/bar_filler_spinner.go new file mode 100644 index 0000000..d38525e --- /dev/null +++ b/bar_filler_spinner.go @@ -0,0 +1,91 @@ +package mpb + +import ( + "io" + "strings" + + "github.com/acarl005/stripansi" + "github.com/mattn/go-runewidth" + "github.com/vbauerster/mpb/v7/decor" + "github.com/vbauerster/mpb/v7/internal" +) + +const ( + positionLeft = 1 + iota + positionRight +) + +// SpinnerStyleComposer interface. +type SpinnerStyleComposer interface { + BarFillerBuilder + PositionLeft() SpinnerStyleComposer + PositionRight() SpinnerStyleComposer +} + +type sFiller struct { + count uint + position uint + frames []string +} + +type spinnerStyle struct { + position uint + frames []string +} + +// SpinnerStyle constructs default spinner style which can be altered via +// SpinnerStyleComposer interface. +func SpinnerStyle(frames ...string) SpinnerStyleComposer { + ss := new(spinnerStyle) + if len(frames) != 0 { + ss.frames = append(ss.frames, frames...) + } else { + ss.frames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} + } + return ss +} + +func (s *spinnerStyle) PositionLeft() SpinnerStyleComposer { + s.position = positionLeft + return s +} + +func (s *spinnerStyle) PositionRight() SpinnerStyleComposer { + s.position = positionRight + return s +} + +func (s *spinnerStyle) Build() BarFiller { + sf := &sFiller{ + position: s.position, + frames: s.frames, + } + return sf +} + +func (s *sFiller) Fill(w io.Writer, width int, stat decor.Statistics) { + width = internal.CheckRequestedWidth(width, stat.AvailableWidth) + + frame := s.frames[s.count%uint(len(s.frames))] + frameWidth := runewidth.StringWidth(stripansi.Strip(frame)) + + if width < frameWidth { + return + } + + var err error + rest := width - frameWidth + switch s.position { + case positionLeft: + _, err = io.WriteString(w, frame+strings.Repeat(" ", rest)) + case positionRight: + _, err = io.WriteString(w, strings.Repeat(" ", rest)+frame) + default: + str := strings.Repeat(" ", rest/2) + frame + strings.Repeat(" ", rest/2+rest%2) + _, err = io.WriteString(w, str) + } + if err != nil { + panic(err) + } + s.count++ +} diff --git a/bar_option.go b/bar_option.go index 76f2050..4ba4905 100644 --- a/bar_option.go +++ b/bar_option.go @@ -4,11 +4,20 @@ "bytes" "io" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7/decor" ) -// BarOption is a function option which changes the default behavior of a bar. +// BarOption is a func option to alter default behavior of a bar. type BarOption func(*bState) + +func skipNil(decorators []decor.Decorator) (filtered []decor.Decorator) { + for _, d := range decorators { + if d != nil { + filtered = append(filtered, d) + } + } + return +} func (s *bState) addDecorators(dest *[]decor.Decorator, decorators ...decor.Decorator) { type mergeWrapper interface { @@ -25,14 +34,14 @@ // AppendDecorators let you inject decorators to the bar's right side. func AppendDecorators(decorators ...decor.Decorator) BarOption { return func(s *bState) { - s.addDecorators(&s.aDecorators, decorators...) + s.addDecorators(&s.aDecorators, skipNil(decorators)...) } } // PrependDecorators let you inject decorators to the bar's left side. func PrependDecorators(decorators ...decor.Decorator) BarOption { return func(s *bState) { - s.addDecorators(&s.pDecorators, decorators...) + s.addDecorators(&s.pDecorators, skipNil(decorators)...) } } @@ -46,7 +55,7 @@ // BarWidth sets bar width independent of the container. func BarWidth(width int) BarOption { return func(s *bState) { - s.width = width + s.reqWidth = width } } @@ -77,19 +86,25 @@ // BarFillerOnComplete replaces bar's filler with message, on complete event. func BarFillerOnComplete(message string) BarOption { - return func(s *bState) { - s.filler = makeBarFillerOnComplete(s.baseF, message) - } + return BarFillerMiddleware(func(base BarFiller) BarFiller { + return BarFillerFunc(func(w io.Writer, reqWidth int, st decor.Statistics) { + if st.Completed { + _, err := io.WriteString(w, message) + if err != nil { + panic(err) + } + } else { + base.Fill(w, reqWidth, st) + } + }) + }) } -func makeBarFillerOnComplete(filler BarFiller, message string) BarFiller { - return BarFillerFunc(func(w io.Writer, width int, st *decor.Statistics) { - if st.Completed { - io.WriteString(w, message) - } else { - filler.Fill(w, width, st) - } - }) +// BarFillerMiddleware provides a way to augment the underlying BarFiller. +func BarFillerMiddleware(middle func(BarFiller) BarFiller) BarOption { + return func(s *bState) { + s.middleware = middle + } } // BarPriority sets bar's priority. Zero is highest priority, i.e. bar @@ -101,47 +116,28 @@ } } -// BarExtender is an option to extend bar to the next new line, with -// arbitrary output. -func BarExtender(extender BarFiller) BarOption { - if extender == nil { +// BarExtender provides a way to extend bar to the next new line. +func BarExtender(filler BarFiller) BarOption { + if filler == nil { return nil } return func(s *bState) { - s.extender = makeExtFunc(extender) + s.extender = makeExtenderFunc(filler) } } -func makeExtFunc(extender BarFiller) extFunc { +func makeExtenderFunc(filler BarFiller) extenderFunc { buf := new(bytes.Buffer) - nl := []byte("\n") - return func(r io.Reader, tw int, st *decor.Statistics) (io.Reader, int) { - extender.Fill(buf, tw, st) - return io.MultiReader(r, buf), bytes.Count(buf.Bytes(), nl) + return func(r io.Reader, reqWidth int, st decor.Statistics) (io.Reader, int) { + filler.Fill(buf, reqWidth, st) + return io.MultiReader(r, buf), bytes.Count(buf.Bytes(), []byte("\n")) } } -// TrimSpace trims bar's edge spaces. -func TrimSpace() BarOption { +// BarFillerTrim removes leading and trailing space around the underlying BarFiller. +func BarFillerTrim() BarOption { return func(s *bState) { s.trimSpace = true - } -} - -// BarStyle overrides mpb.DefaultBarStyle which is "[=>-]<+". -// It's ok to pass string containing just 5 runes, for example "╢▌▌░╟", -// if you don't need to override '<' (reverse tip) and '+' (refill rune). -func BarStyle(style string) BarOption { - if style == "" { - return nil - } - type styleSetter interface { - SetStyle(string) - } - return func(s *bState) { - if t, ok := s.baseF.(styleSetter); ok { - t.SetStyle(style) - } } } @@ -153,52 +149,19 @@ } } -// BarReverse reverse mode, bar will progress from right to left. -func BarReverse() BarOption { - type revSetter interface { - SetReverse(bool) - } - return func(s *bState) { - if t, ok := s.baseF.(revSetter); ok { - t.SetReverse(true) - } - } -} - -// SpinnerStyle sets custom spinner style. -// Effective when Filler type is spinner. -func SpinnerStyle(frames []string) BarOption { - if len(frames) == 0 { - return nil - } - chk := func(filler BarFiller) (interface{}, bool) { - t, ok := filler.(*spinnerFiller) - return t, ok - } - cb := func(t interface{}) { - t.(*spinnerFiller).frames = frames - } - return MakeFillerTypeSpecificBarOption(chk, cb) -} - -// MakeFillerTypeSpecificBarOption makes BarOption specific to Filler's -// actual type. If you implement your own Filler, so most probably -// you'll need this. See BarStyle or SpinnerStyle for example. -func MakeFillerTypeSpecificBarOption( - typeChecker func(BarFiller) (interface{}, bool), - cb func(interface{}), -) BarOption { - return func(s *bState) { - if t, ok := typeChecker(s.baseF); ok { - cb(t) - } - } -} - -// BarOptOn returns option when condition evaluates to true. -func BarOptOn(option BarOption, condition func() bool) BarOption { - if condition() { +// BarOptional will invoke provided option only when cond is true. +func BarOptional(option BarOption, cond bool) BarOption { + if cond { return option } return nil } + +// BarOptOn will invoke provided option only when higher order predicate +// evaluates to true. +func BarOptOn(option BarOption, predicate func() bool) BarOption { + if predicate() { + return option + } + return nil +} diff --git a/bar_test.go b/bar_test.go index b442195..42451c0 100644 --- a/bar_test.go +++ b/bar_test.go @@ -10,12 +10,12 @@ "time" "unicode/utf8" - . "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func TestBarCompleted(t *testing.T) { - p := New(WithOutput(ioutil.Discard)) + p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(ioutil.Discard)) total := 80 bar := p.AddBar(int64(total)) @@ -33,17 +33,17 @@ } func TestBarID(t *testing.T) { - p := New(WithOutput(ioutil.Discard)) + p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(ioutil.Discard)) total := 100 wantID := 11 - bar := p.AddBar(int64(total), BarID(wantID)) - - go func(total int) { + bar := p.AddBar(int64(total), mpb.BarID(wantID)) + + go func() { for i := 0; i < total; i++ { time.Sleep(50 * time.Millisecond) bar.Increment() } - }(total) + }() gotID := bar.ID() if gotID != wantID { @@ -57,14 +57,13 @@ func TestBarSetRefill(t *testing.T) { var buf bytes.Buffer - width := 100 - p := New(WithOutput(&buf), WithWidth(width)) + p := mpb.New(mpb.WithOutput(&buf), mpb.WithWidth(100)) total := 100 till := 30 - refillRune, _ := utf8.DecodeLastRuneInString(DefaultBarStyle) - - bar := p.AddBar(int64(total), TrimSpace()) + refiller := "+" + + bar := p.New(int64(total), mpb.BarStyle().Refiller(refiller), mpb.BarFillerTrim()) bar.SetRefill(int64(till)) bar.IncrBy(till) @@ -77,7 +76,7 @@ p.Wait() wantBar := fmt.Sprintf("[%s%s]", - strings.Repeat(string(refillRune), till-1), + strings.Repeat(refiller, till-1), strings.Repeat("=", total-till-1), ) @@ -91,12 +90,12 @@ func TestBarHas100PercentWithOnCompleteDecorator(t *testing.T) { var buf bytes.Buffer - p := New(WithOutput(&buf)) + p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(&buf)) total := 50 bar := p.AddBar(int64(total), - AppendDecorators( + mpb.AppendDecorators( decor.OnComplete( decor.Percentage(), "done", ), @@ -119,13 +118,13 @@ func TestBarHas100PercentWithBarRemoveOnComplete(t *testing.T) { var buf bytes.Buffer - p := New(WithOutput(&buf)) + p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(&buf)) total := 50 bar := p.AddBar(int64(total), - BarRemoveOnComplete(), - AppendDecorators(decor.Percentage()), + mpb.BarRemoveOnComplete(), + mpb.AppendDecorators(decor.Percentage()), ) for i := 0; i < total; i++ { @@ -144,22 +143,29 @@ func TestBarStyle(t *testing.T) { var buf bytes.Buffer customFormat := "╢▌▌░╟" - p := New(WithOutput(&buf)) + runes := []rune(customFormat) total := 80 - bar := p.AddBar(int64(total), BarStyle(customFormat), TrimSpace()) - - for i := 0; i < total; i++ { - bar.Increment() - time.Sleep(10 * time.Millisecond) - } - - p.Wait() - - runes := []rune(customFormat) - wantBar := fmt.Sprintf("%s%s%s", + p := mpb.New(mpb.WithWidth(total), mpb.WithOutput(&buf)) + bs := mpb.BarStyle() + bs.Lbound(string(runes[0])) + bs.Filler(string(runes[1])) + bs.Tip(string(runes[2])) + bs.Padding(string(runes[3])) + bs.Rbound(string(runes[4])) + bar := p.New(int64(total), bs, mpb.BarFillerTrim()) + + for i := 0; i < total; i++ { + bar.Increment() + time.Sleep(10 * time.Millisecond) + } + + p.Wait() + + wantBar := fmt.Sprintf("%s%s%s%s", string(runes[0]), - strings.Repeat(string(runes[1]), total-2), - string(runes[len(runes)-1]), + strings.Repeat(string(runes[1]), total-3), + string(runes[2]), + string(runes[4]), ) got := string(getLastLine(buf.Bytes())) @@ -170,14 +176,18 @@ func TestBarPanicBeforeComplete(t *testing.T) { var buf bytes.Buffer - p := New(WithDebugOutput(&buf), WithOutput(ioutil.Discard)) + p := mpb.New( + mpb.WithWidth(80), + mpb.WithDebugOutput(&buf), + mpb.WithOutput(ioutil.Discard), + ) total := 100 panicMsg := "Upps!!!" var pCount uint32 bar := p.AddBar(int64(total), - PrependDecorators(panicDecorator(panicMsg, - func(st *decor.Statistics) bool { + mpb.PrependDecorators(panicDecorator(panicMsg, + func(st decor.Statistics) bool { if st.Current >= 42 { atomic.AddUint32(&pCount, 1) return true @@ -206,14 +216,18 @@ func TestBarPanicAfterComplete(t *testing.T) { var buf bytes.Buffer - p := New(WithDebugOutput(&buf), WithOutput(ioutil.Discard)) + p := mpb.New( + mpb.WithWidth(80), + mpb.WithDebugOutput(&buf), + mpb.WithOutput(ioutil.Discard), + ) total := 100 panicMsg := "Upps!!!" var pCount uint32 bar := p.AddBar(int64(total), - PrependDecorators(panicDecorator(panicMsg, - func(st *decor.Statistics) bool { + mpb.PrependDecorators(panicDecorator(panicMsg, + func(st decor.Statistics) bool { if st.Completed { atomic.AddUint32(&pCount, 1) return true @@ -230,8 +244,8 @@ p.Wait() - if pCount != 1 { - t.Errorf("Decor called after panic %d times\n", pCount-1) + if pCount > 2 { + t.Error("Decor called after panic more than 2 times\n") } barStr := buf.String() @@ -240,24 +254,65 @@ } } -func panicDecorator(panicMsg string, cond func(*decor.Statistics) bool) decor.Decorator { - d := &decorator{ - panicMsg: panicMsg, - cond: cond, - } - d.Init() - return d -} - -type decorator struct { - decor.WC - panicMsg string - cond func(*decor.Statistics) bool -} - -func (d *decorator) Decor(st *decor.Statistics) string { - if d.cond(st) { - panic(d.panicMsg) - } - return d.FormatMsg("") -} +func TestDecorStatisticsAvailableWidth(t *testing.T) { + total := 100 + down := make(chan struct{}) + checkDone := make(chan struct{}) + td1 := func(s decor.Statistics) string { + if s.AvailableWidth != 80 { + t.Errorf("expected AvailableWidth %d got %d\n", 80, s.AvailableWidth) + } + return fmt.Sprintf("\x1b[31;1;4m%s\x1b[0m", strings.Repeat("0", 20)) + } + td2 := func(s decor.Statistics) string { + defer func() { + select { + case checkDone <- struct{}{}: + default: + } + }() + if s.AvailableWidth != 40 { + t.Errorf("expected AvailableWidth %d got %d\n", 40, s.AvailableWidth) + } + return "" + } + p := mpb.New( + mpb.WithWidth(100), + mpb.WithShutdownNotifier(down), + mpb.WithOutput(ioutil.Discard), + ) + bar := p.AddBar(int64(total), + mpb.BarFillerTrim(), + mpb.PrependDecorators( + decor.Name(strings.Repeat("0", 20)), + decor.Any(td1), + ), + mpb.AppendDecorators( + decor.Name(strings.Repeat("0", 20)), + decor.Any(td2), + ), + ) + go func() { + for { + select { + case <-checkDone: + bar.Abort(true) + case <-down: + return + } + } + }() + for !bar.Completed() { + bar.Increment() + } + p.Wait() +} + +func panicDecorator(panicMsg string, cond func(decor.Statistics) bool) decor.Decorator { + return decor.Any(func(st decor.Statistics) string { + if cond(st) { + panic(panicMsg) + } + return "" + }) +} diff --git a/barbench_test.go b/barbench_test.go index 53a8141..7926afa 100644 --- a/barbench_test.go +++ b/barbench_test.go @@ -1,43 +1,58 @@ package mpb import ( - "io/ioutil" + "sync" "testing" - - "github.com/vbauerster/mpb/v5/decor" ) -func BenchmarkIncrSingleBar(b *testing.B) { - p := New(WithOutput(ioutil.Discard)) - bar := p.AddBar(int64(b.N)) - for i := 0; i < b.N; i++ { - bar.Increment() - } +const total = 1000 + +func BenchmarkIncrementOneBar(b *testing.B) { + benchBody(1, b) } -func BenchmarkIncrSingleBarWhileIsNotCompleted(b *testing.B) { - p := New(WithOutput(ioutil.Discard)) - bar := p.AddBar(int64(b.N)) - for !bar.Completed() { - bar.Increment() - } +func BenchmarkIncrementTwoBars(b *testing.B) { + benchBody(2, b) } -func BenchmarkIncrSingleBarWithNameDecorator(b *testing.B) { - p := New(WithOutput(ioutil.Discard)) - bar := p.AddBar(int64(b.N), PrependDecorators(decor.Name("test"))) - for i := 0; i < b.N; i++ { - bar.Increment() - } +func BenchmarkIncrementThreeBars(b *testing.B) { + benchBody(3, b) } -func BenchmarkIncrSingleBarWithNameAndEwmaETADecorator(b *testing.B) { - p := New(WithOutput(ioutil.Discard)) - bar := p.AddBar(int64(b.N), - PrependDecorators(decor.Name("test")), - AppendDecorators(decor.EwmaETA(decor.ET_STYLE_GO, 60)), - ) +func BenchmarkIncrementFourBars(b *testing.B) { + benchBody(4, b) +} + +func benchBody(n int, b *testing.B) { + p := New(WithOutput(nil), WithWidth(80)) + wg := new(sync.WaitGroup) + b.ResetTimer() for i := 0; i < b.N; i++ { - bar.Increment() + for j := 0; j < n; j++ { + switch j { + case n - 1: + bar := p.AddBar(total) + for c := 0; c < total; c++ { + bar.Increment() + } + if !bar.Completed() { + b.Fail() + } + default: + wg.Add(1) + go func() { + bar := p.AddBar(total) + for c := 0; c < total; c++ { + bar.Increment() + } + if !bar.Completed() { + b.Fail() + } + wg.Done() + }() + } + } + wg.Wait() } + p.Wait() } diff --git a/container_option.go b/container_option.go new file mode 100644 index 0000000..e523a17 --- /dev/null +++ b/container_option.go @@ -0,0 +1,117 @@ +package mpb + +import ( + "io" + "io/ioutil" + "sync" + "time" +) + +// ContainerOption is a func option to alter default behavior of a bar +// container. Container term refers to a Progress struct which can +// hold one or more Bars. +type ContainerOption func(*pState) + +// WithWaitGroup provides means to have a single joint point. If +// *sync.WaitGroup is provided, you can safely call just p.Wait() +// without calling Wait() on provided *sync.WaitGroup. Makes sense +// when there are more than one bar to render. +func WithWaitGroup(wg *sync.WaitGroup) ContainerOption { + return func(s *pState) { + s.uwg = wg + } +} + +// WithWidth sets container width. If not set it defaults to terminal +// width. A bar added to the container will inherit its width, unless +// overridden by `func BarWidth(int) BarOption`. +func WithWidth(width int) ContainerOption { + return func(s *pState) { + s.reqWidth = width + } +} + +// WithRefreshRate overrides default 120ms refresh rate. +func WithRefreshRate(d time.Duration) ContainerOption { + return func(s *pState) { + s.rr = d + } +} + +// WithManualRefresh disables internal auto refresh time.Ticker. +// Refresh will occur upon receive value from provided ch. +func WithManualRefresh(ch <-chan interface{}) ContainerOption { + return func(s *pState) { + s.externalRefresh = ch + } +} + +// WithRenderDelay delays rendering. By default rendering starts as +// soon as bar is added, with this option it's possible to delay +// rendering process by keeping provided chan unclosed. In other words +// rendering will start as soon as provided chan is closed. +func WithRenderDelay(ch <-chan struct{}) ContainerOption { + return func(s *pState) { + s.renderDelay = ch + } +} + +// WithShutdownNotifier provided chanel will be closed, after all bars +// have been rendered. +func WithShutdownNotifier(ch chan struct{}) ContainerOption { + return func(s *pState) { + select { + case <-ch: + default: + s.shutdownNotifier = ch + } + } +} + +// WithOutput overrides default os.Stdout output. Setting it to nil +// will effectively disable auto refresh rate and discard any output, +// useful if you want to disable progress bars with little overhead. +func WithOutput(w io.Writer) ContainerOption { + return func(s *pState) { + if w == nil { + s.output = ioutil.Discard + s.outputDiscarded = true + return + } + s.output = w + } +} + +// WithDebugOutput sets debug output. +func WithDebugOutput(w io.Writer) ContainerOption { + if w == nil { + return nil + } + return func(s *pState) { + s.debugOut = w + } +} + +// PopCompletedMode will pop and stop rendering completed bars. +func PopCompletedMode() ContainerOption { + return func(s *pState) { + s.popCompleted = true + } +} + +// ContainerOptional will invoke provided option only when cond is true. +func ContainerOptional(option ContainerOption, cond bool) ContainerOption { + if cond { + return option + } + return nil +} + +// ContainerOptOn will invoke provided option only when higher order +// predicate evaluates to true. +func ContainerOptOn(option ContainerOption, predicate func() bool) ContainerOption { + if predicate() { + return option + } + return nil +} diff --git a/cwriter/cuuAndEd_construction_bench_test.go b/cwriter/cuuAndEd_construction_bench_test.go new file mode 100644 index 0000000..25d585b --- /dev/null +++ b/cwriter/cuuAndEd_construction_bench_test.go @@ -0,0 +1,39 @@ +package cwriter + +import ( + "bytes" + "fmt" + "io/ioutil" + "strconv" + "testing" +) + +func BenchmarkWithFprintf(b *testing.B) { + cuuAndEd := "\x1b[%dA\x1b[J" + for i := 0; i < b.N; i++ { + fmt.Fprintf(ioutil.Discard, cuuAndEd, 4) + } +} + +func BenchmarkWithJoin(b *testing.B) { + bCuuAndEd := [][]byte{[]byte("\x1b["), []byte("A\x1b[J")} + for i := 0; i < b.N; i++ { + _, _ = ioutil.Discard.Write(bytes.Join(bCuuAndEd, []byte(strconv.Itoa(4)))) + } +} + +func BenchmarkWithAppend(b *testing.B) { + escOpen := []byte("\x1b[") + cuuAndEd := []byte("A\x1b[J") + for i := 0; i < b.N; i++ { + _, _ = ioutil.Discard.Write(append(strconv.AppendInt(escOpen, 4, 10), cuuAndEd...)) + } +} + +func BenchmarkWithCopy(b *testing.B) { + w := New(ioutil.Discard) + w.lines = 4 + for i := 0; i < b.N; i++ { + _ = w.ansiCuuAndEd() + } +} diff --git a/cwriter/doc.go b/cwriter/doc.go new file mode 100644 index 0000000..93c8f82 --- /dev/null +++ b/cwriter/doc.go @@ -0,0 +1,2 @@ +// Package cwriter is a console writer abstraction for the underlying OS. +package cwriter diff --git a/cwriter/util_bsd.go b/cwriter/util_bsd.go new file mode 100644 index 0000000..4e3564e --- /dev/null +++ b/cwriter/util_bsd.go @@ -0,0 +1,7 @@ +// +build darwin dragonfly freebsd netbsd openbsd + +package cwriter + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TIOCGETA diff --git a/cwriter/util_linux.go b/cwriter/util_linux.go new file mode 100644 index 0000000..253f12d --- /dev/null +++ b/cwriter/util_linux.go @@ -0,0 +1,7 @@ +// +build aix linux + +package cwriter + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TCGETS diff --git a/cwriter/util_solaris.go b/cwriter/util_solaris.go new file mode 100644 index 0000000..4b29ff5 --- /dev/null +++ b/cwriter/util_solaris.go @@ -0,0 +1,7 @@ +// +build solaris + +package cwriter + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TCGETA diff --git a/cwriter/util_zos.go b/cwriter/util_zos.go new file mode 100644 index 0000000..b7d67fc --- /dev/null +++ b/cwriter/util_zos.go @@ -0,0 +1,7 @@ +// +build zos + +package cwriter + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TCGETS diff --git a/cwriter/writer.go b/cwriter/writer.go index 9ec1ec6..eaf541c 100644 --- a/cwriter/writer.go +++ b/cwriter/writer.go @@ -3,25 +3,27 @@ import ( "bytes" "errors" - "fmt" "io" "os" - - "golang.org/x/crypto/ssh/terminal" + "strconv" ) -// NotATTY not a TeleTYpewriter error. -var NotATTY = errors.New("not a terminal") +// ErrNotTTY not a TeleTYpewriter error. +var ErrNotTTY = errors.New("not a terminal") -var cuuAndEd = fmt.Sprintf("%c[%%dA%[1]c[J", 27) +// http://ascii-table.com/ansi-escape-sequences.php +const ( + escOpen = "\x1b[" + cuuAndEd = "A\x1b[J" +) // Writer is a buffered the writer that updates the terminal. The // contents of writer will be flushed when Flush is called. type Writer struct { out io.Writer buf bytes.Buffer - lineCount int - fd uintptr + lines int + fd int isTerminal bool } @@ -29,18 +31,22 @@ func New(out io.Writer) *Writer { w := &Writer{out: out} if f, ok := out.(*os.File); ok { - w.fd = f.Fd() - w.isTerminal = terminal.IsTerminal(int(w.fd)) + w.fd = int(f.Fd()) + w.isTerminal = IsTerminal(w.fd) } return w } // Flush flushes the underlying buffer. -func (w *Writer) Flush(lineCount int) (err error) { - if w.lineCount > 0 { - w.clearLines() +func (w *Writer) Flush(lines int) (err error) { + // some terminals interpret 'cursor up 0' as 'cursor up 1' + if w.lines > 0 { + err = w.clearLines() + if err != nil { + return + } } - w.lineCount = lineCount + w.lines = lines _, err = w.buf.WriteTo(w.out) return } @@ -63,9 +69,16 @@ // GetWidth returns width of underlying terminal. func (w *Writer) GetWidth() (int, error) { - if w.isTerminal { - tw, _, err := terminal.GetSize(int(w.fd)) - return tw, err + if !w.isTerminal { + return -1, ErrNotTTY } - return -1, NotATTY + tw, _, err := GetSize(w.fd) + return tw, err } + +func (w *Writer) ansiCuuAndEd() error { + buf := make([]byte, 8) + buf = strconv.AppendInt(buf[:copy(buf, escOpen)], int64(w.lines), 10) + _, err := w.out.Write(append(buf, cuuAndEd...)) + return err +} diff --git a/cwriter/writer_posix.go b/cwriter/writer_posix.go index 3fb8b7d..f54a5d0 100644 --- a/cwriter/writer_posix.go +++ b/cwriter/writer_posix.go @@ -2,8 +2,25 @@ package cwriter -import "fmt" +import ( + "golang.org/x/sys/unix" +) -func (w *Writer) clearLines() { - fmt.Fprintf(w.out, cuuAndEd, w.lineCount) +func (w *Writer) clearLines() error { + return w.ansiCuuAndEd() } + +// GetSize returns the dimensions of the given terminal. +func GetSize(fd int) (width, height int, err error) { + ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) + if err != nil { + return -1, -1, err + } + return int(ws.Col), int(ws.Row), nil +} + +// IsTerminal returns whether the given file descriptor is a terminal. +func IsTerminal(fd int) bool { + _, err := unix.IoctlGetTermios(fd, ioctlReadTermios) + return err == nil +} diff --git a/cwriter/writer_windows.go b/cwriter/writer_windows.go index 7125289..8f99dbe 100644 --- a/cwriter/writer_windows.go +++ b/cwriter/writer_windows.go @@ -3,58 +3,71 @@ package cwriter import ( - "fmt" - "syscall" "unsafe" + + "golang.org/x/sys/windows" ) -var kernel32 = syscall.NewLazyDLL("kernel32.dll") +var kernel32 = windows.NewLazySystemDLL("kernel32.dll") var ( - procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") - procFillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute") ) -type coord struct { - x int16 - y int16 +func (w *Writer) clearLines() error { + if !w.isTerminal { + // hope it's cygwin or similar + return w.ansiCuuAndEd() + } + + var info windows.ConsoleScreenBufferInfo + if err := windows.GetConsoleScreenBufferInfo(windows.Handle(w.fd), &info); err != nil { + return err + } + + info.CursorPosition.Y -= int16(w.lines) + if info.CursorPosition.Y < 0 { + info.CursorPosition.Y = 0 + } + _, _, _ = procSetConsoleCursorPosition.Call( + uintptr(w.fd), + uintptr(uint32(uint16(info.CursorPosition.Y))<<16|uint32(uint16(info.CursorPosition.X))), + ) + + // clear the lines + cursor := &windows.Coord{ + X: info.Window.Left, + Y: info.CursorPosition.Y, + } + count := uint32(info.Size.X) * uint32(w.lines) + _, _, _ = procFillConsoleOutputCharacter.Call( + uintptr(w.fd), + uintptr(' '), + uintptr(count), + *(*uintptr)(unsafe.Pointer(cursor)), + uintptr(unsafe.Pointer(new(uint32))), + ) + return nil } -type smallRect struct { - left int16 - top int16 - right int16 - bottom int16 +// GetSize returns the visible dimensions of the given terminal. +// +// These dimensions don't include any scrollback buffer height. +func GetSize(fd int) (width, height int, err error) { + var info windows.ConsoleScreenBufferInfo + if err := windows.GetConsoleScreenBufferInfo(windows.Handle(fd), &info); err != nil { + return 0, 0, err + } + // terminal.GetSize from crypto/ssh adds "+ 1" to both width and height: + // https://go.googlesource.com/crypto/+/refs/heads/release-branch.go1.14/ssh/terminal/util_windows.go#75 + // but looks like this is a root cause of issue #66, so removing both "+ 1" have fixed it. + return int(info.Window.Right - info.Window.Left), int(info.Window.Bottom - info.Window.Top), nil } -type consoleScreenBufferInfo struct { - size coord - cursorPosition coord - attributes uint16 - window smallRect - maximumWindowSize coord +// IsTerminal returns whether the given file descriptor is a terminal. +func IsTerminal(fd int) bool { + var st uint32 + err := windows.GetConsoleMode(windows.Handle(fd), &st) + return err == nil } - -func (w *Writer) clearLines() { - if !w.isTerminal { - fmt.Fprintf(w.out, cuuAndEd, w.lineCount) - } - var info consoleScreenBufferInfo - procGetConsoleScreenBufferInfo.Call(w.fd, uintptr(unsafe.Pointer(&info))) - - info.cursorPosition.y -= int16(w.lineCount) - if info.cursorPosition.y < 0 { - info.cursorPosition.y = 0 - } - procSetConsoleCursorPosition.Call(w.fd, uintptr(uint32(uint16(info.cursorPosition.y))<<16|uint32(uint16(info.cursorPosition.x)))) - - // clear the lines - cursor := coord{ - x: info.window.left, - y: info.cursorPosition.y, - } - count := uint32(info.size.x) * uint32(w.lineCount) - procFillConsoleOutputCharacter.Call(w.fd, uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(new(uint32)))) -} diff --git a/decor/any.go b/decor/any.go index bf9cf51..39518f5 100644 --- a/decor/any.go +++ b/decor/any.go @@ -1,21 +1,21 @@ package decor // Any decorator displays text, that can be changed during decorator's -// lifetime via provided func call back. +// lifetime via provided DecorFunc. // -// `f` call back which provides string to display +// `fn` DecorFunc callback // // `wcc` optional WC config // -func Any(f func(*Statistics) string, wcc ...WC) Decorator { - return &any{initWC(wcc...), f} +func Any(fn DecorFunc, wcc ...WC) Decorator { + return &any{initWC(wcc...), fn} } type any struct { WC - f func(*Statistics) string + fn DecorFunc } -func (d *any) Decor(s *Statistics) string { - return d.FormatMsg(d.f(s)) +func (d *any) Decor(s Statistics) string { + return d.FormatMsg(d.fn(s)) } diff --git a/decor/counters.go b/decor/counters.go index 297bf93..4a5343d 100644 --- a/decor/counters.go +++ b/decor/counters.go @@ -2,6 +2,7 @@ import ( "fmt" + "strings" ) const ( @@ -31,7 +32,7 @@ // // `unit` one of [0|UnitKiB|UnitKB] zero for no unit // -// `pairFmt` printf compatible verbs for current and total, like "%f" or "%d" +// `pairFmt` printf compatible verbs for current and total pair // // `wcc` optional WC config // @@ -43,25 +44,200 @@ // pairFmt="% d / % d" output: "1 MB / 12 MB" // func Counters(unit int, pairFmt string, wcc ...WC) Decorator { - return Any(chooseSizeProducer(unit, pairFmt), wcc...) -} - -func chooseSizeProducer(unit int, format string) func(*Statistics) string { - if format == "" { - format = "%d / %d" - } - switch unit { - case UnitKiB: - return func(s *Statistics) string { - return fmt.Sprintf(format, SizeB1024(s.Current), SizeB1024(s.Total)) - } - case UnitKB: - return func(s *Statistics) string { - return fmt.Sprintf(format, SizeB1000(s.Current), SizeB1000(s.Total)) - } - default: - return func(s *Statistics) string { - return fmt.Sprintf(format, s.Current, s.Total) - } - } -} + producer := func(unit int, pairFmt string) DecorFunc { + if pairFmt == "" { + pairFmt = "%d / %d" + } else if strings.Count(pairFmt, "%") != 2 { + panic("expected pairFmt with exactly 2 verbs") + } + switch unit { + case UnitKiB: + return func(s Statistics) string { + return fmt.Sprintf(pairFmt, SizeB1024(s.Current), SizeB1024(s.Total)) + } + case UnitKB: + return func(s Statistics) string { + return fmt.Sprintf(pairFmt, SizeB1000(s.Current), SizeB1000(s.Total)) + } + default: + return func(s Statistics) string { + return fmt.Sprintf(pairFmt, s.Current, s.Total) + } + } + } + return Any(producer(unit, pairFmt), wcc...) +} + +// TotalNoUnit is a wrapper around Total with no unit param. +func TotalNoUnit(format string, wcc ...WC) Decorator { + return Total(0, format, wcc...) +} + +// TotalKibiByte is a wrapper around Total with predefined unit +// UnitKiB (bytes/1024). +func TotalKibiByte(format string, wcc ...WC) Decorator { + return Total(UnitKiB, format, wcc...) +} + +// TotalKiloByte is a wrapper around Total with predefined unit +// UnitKB (bytes/1000). +func TotalKiloByte(format string, wcc ...WC) Decorator { + return Total(UnitKB, format, wcc...) +} + +// Total decorator with dynamic unit measure adjustment. +// +// `unit` one of [0|UnitKiB|UnitKB] zero for no unit +// +// `format` printf compatible verb for Total +// +// `wcc` optional WC config +// +// format example if unit=UnitKiB: +// +// format="%.1f" output: "12.0MiB" +// format="% .1f" output: "12.0 MiB" +// format="%d" output: "12MiB" +// format="% d" output: "12 MiB" +// +func Total(unit int, format string, wcc ...WC) Decorator { + producer := func(unit int, format string) DecorFunc { + if format == "" { + format = "%d" + } else if strings.Count(format, "%") != 1 { + panic("expected format with exactly 1 verb") + } + + switch unit { + case UnitKiB: + return func(s Statistics) string { + return fmt.Sprintf(format, SizeB1024(s.Total)) + } + case UnitKB: + return func(s Statistics) string { + return fmt.Sprintf(format, SizeB1000(s.Total)) + } + default: + return func(s Statistics) string { + return fmt.Sprintf(format, s.Total) + } + } + } + return Any(producer(unit, format), wcc...) +} + +// CurrentNoUnit is a wrapper around Current with no unit param. +func CurrentNoUnit(format string, wcc ...WC) Decorator { + return Current(0, format, wcc...) +} + +// CurrentKibiByte is a wrapper around Current with predefined unit +// UnitKiB (bytes/1024). +func CurrentKibiByte(format string, wcc ...WC) Decorator { + return Current(UnitKiB, format, wcc...) +} + +// CurrentKiloByte is a wrapper around Current with predefined unit +// UnitKB (bytes/1000). +func CurrentKiloByte(format string, wcc ...WC) Decorator { + return Current(UnitKB, format, wcc...) +} + +// Current decorator with dynamic unit measure adjustment. +// +// `unit` one of [0|UnitKiB|UnitKB] zero for no unit +// +// `format` printf compatible verb for Current +// +// `wcc` optional WC config +// +// format example if unit=UnitKiB: +// +// format="%.1f" output: "12.0MiB" +// format="% .1f" output: "12.0 MiB" +// format="%d" output: "12MiB" +// format="% d" output: "12 MiB" +// +func Current(unit int, format string, wcc ...WC) Decorator { + producer := func(unit int, format string) DecorFunc { + if format == "" { + format = "%d" + } else if strings.Count(format, "%") != 1 { + panic("expected format with exactly 1 verb") + } + + switch unit { + case UnitKiB: + return func(s Statistics) string { + return fmt.Sprintf(format, SizeB1024(s.Current)) + } + case UnitKB: + return func(s Statistics) string { + return fmt.Sprintf(format, SizeB1000(s.Current)) + } + default: + return func(s Statistics) string { + return fmt.Sprintf(format, s.Current) + } + } + } + return Any(producer(unit, format), wcc...) +} + +// InvertedCurrentNoUnit is a wrapper around InvertedCurrent with no unit param. +func InvertedCurrentNoUnit(format string, wcc ...WC) Decorator { + return InvertedCurrent(0, format, wcc...) +} + +// InvertedCurrentKibiByte is a wrapper around InvertedCurrent with predefined unit +// UnitKiB (bytes/1024). +func InvertedCurrentKibiByte(format string, wcc ...WC) Decorator { + return InvertedCurrent(UnitKiB, format, wcc...) +} + +// InvertedCurrentKiloByte is a wrapper around InvertedCurrent with predefined unit +// UnitKB (bytes/1000). +func InvertedCurrentKiloByte(format string, wcc ...WC) Decorator { + return InvertedCurrent(UnitKB, format, wcc...) +} + +// InvertedCurrent decorator with dynamic unit measure adjustment. +// +// `unit` one of [0|UnitKiB|UnitKB] zero for no unit +// +// `format` printf compatible verb for InvertedCurrent +// +// `wcc` optional WC config +// +// format example if unit=UnitKiB: +// +// format="%.1f" output: "12.0MiB" +// format="% .1f" output: "12.0 MiB" +// format="%d" output: "12MiB" +// format="% d" output: "12 MiB" +// +func InvertedCurrent(unit int, format string, wcc ...WC) Decorator { + producer := func(unit int, format string) DecorFunc { + if format == "" { + format = "%d" + } else if strings.Count(format, "%") != 1 { + panic("expected format with exactly 1 verb") + } + + switch unit { + case UnitKiB: + return func(s Statistics) string { + return fmt.Sprintf(format, SizeB1024(s.Total-s.Current)) + } + case UnitKB: + return func(s Statistics) string { + return fmt.Sprintf(format, SizeB1000(s.Total-s.Current)) + } + default: + return func(s Statistics) string { + return fmt.Sprintf(format, s.Total-s.Current) + } + } + } + return Any(producer(unit, format), wcc...) +} diff --git a/decor/decorator.go b/decor/decorator.go index 5bca63d..9fec57b 100644 --- a/decor/decorator.go +++ b/decor/decorator.go @@ -3,9 +3,9 @@ import ( "fmt" "time" - "unicode/utf8" "github.com/acarl005/stripansi" + "github.com/mattn/go-runewidth" ) const ( @@ -47,21 +47,32 @@ // Statistics consists of progress related statistics, that Decorator // may need. type Statistics struct { - ID int - Completed bool - Total int64 - Current int64 + ID int + AvailableWidth int + Total int64 + Current int64 + Refill int64 + Completed bool + Aborted bool } // Decorator interface. -// Implementors should embed WC type, that way only single method -// Decor(*Statistics) needs to be implemented, the rest will be handled -// by WC type. +// Most of the time there is no need to implement this interface +// manually, as decor package already provides a wide range of decorators +// which implement this interface. If however built-in decorators don't +// meet your needs, you're free to implement your own one by implementing +// this particular interface. The easy way to go is to convert a +// `DecorFunc` into a `Decorator` interface by using provided +// `func Any(DecorFunc, ...WC) Decorator`. type Decorator interface { Configurator Synchronizer - Decor(*Statistics) string + Decor(Statistics) string } + +// DecorFunc func type. +// To be used with `func Any`(DecorFunc, ...WC) Decorator`. +type DecorFunc func(Statistics) string // Synchronizer interface. // All decorators implement this interface implicitly. Its Sync @@ -117,38 +128,35 @@ // W represents width and C represents bit set of width related config. // A decorator should embed WC, to enable width synchronization. type WC struct { - W int - C int - dynFormat string - wsync chan int + W int + C int + fill func(s string, w int) string + wsync chan int } // FormatMsg formats final message according to WC.W and WC.C. // Should be called by any Decorator implementation. func (wc *WC) FormatMsg(msg string) string { - var format string - runeCount := utf8.RuneCountInString(stripansi.Strip(msg)) - ansiCount := utf8.RuneCountInString(msg) - runeCount + pureWidth := runewidth.StringWidth(msg) + stripWidth := runewidth.StringWidth(stripansi.Strip(msg)) + maxCell := wc.W if (wc.C & DSyncWidth) != 0 { + cellCount := stripWidth if (wc.C & DextraSpace) != 0 { - runeCount++ + cellCount++ } - wc.wsync <- runeCount - max := <-wc.wsync - format = fmt.Sprintf(wc.dynFormat, ansiCount+max) - } else { - format = fmt.Sprintf(wc.dynFormat, ansiCount+wc.W) + wc.wsync <- cellCount + maxCell = <-wc.wsync } - return fmt.Sprintf(format, msg) + return wc.fill(msg, maxCell+(pureWidth-stripWidth)) } // Init initializes width related config. func (wc *WC) Init() WC { - wc.dynFormat = "%%" + wc.fill = runewidth.FillLeft if (wc.C & DidentRight) != 0 { - wc.dynFormat += "-" + wc.fill = runewidth.FillRight } - wc.dynFormat += "%ds" if (wc.C & DSyncWidth) != 0 { // it's deliberate choice to override wsync on each Init() call, // this way globals like WCSyncSpace can be reused diff --git a/decor/doc.go b/decor/doc.go index 6d26144..4e42993 100644 --- a/decor/doc.go +++ b/decor/doc.go @@ -1,21 +1,19 @@ -/* - Package decor provides common decorators for "github.com/vbauerster/mpb/v5" module. - - Some decorators returned by this package might have a closure state. It is ok to use - decorators concurrently, unless you share the same decorator among multiple - *mpb.Bar instances. To avoid data races, create new decorator per *mpb.Bar instance. - - Don't: - - p := mpb.New() - name := decor.Name("bar") - p.AddBar(100, mpb.AppendDecorators(name)) - p.AddBar(100, mpb.AppendDecorators(name)) - - Do: - - p := mpb.New() - p.AddBar(100, mpb.AppendDecorators(decor.Name("bar1"))) - p.AddBar(100, mpb.AppendDecorators(decor.Name("bar2"))) -*/ +// Package decor provides common decorators for "github.com/vbauerster/mpb/v7" module. +// +// Some decorators returned by this package might have a closure state. It is ok to use +// decorators concurrently, unless you share the same decorator among multiple +// *mpb.Bar instances. To avoid data races, create new decorator per *mpb.Bar instance. +// +// Don't: +// +// p := mpb.New() +// name := decor.Name("bar") +// p.AddBar(100, mpb.AppendDecorators(name)) +// p.AddBar(100, mpb.AppendDecorators(name)) +// +// Do: +// +// p := mpb.New() +// p.AddBar(100, mpb.AppendDecorators(decor.Name("bar1"))) +// p.AddBar(100, mpb.AppendDecorators(decor.Name("bar2"))) package decor diff --git a/decor/elapsed.go b/decor/elapsed.go index c9999a3..e389f15 100644 --- a/decor/elapsed.go +++ b/decor/elapsed.go @@ -25,11 +25,11 @@ func NewElapsed(style TimeStyle, startTime time.Time, wcc ...WC) Decorator { var msg string producer := chooseTimeProducer(style) - f := func(s *Statistics) string { + fn := func(s Statistics) string { if !s.Completed { msg = producer(time.Since(startTime)) } return msg } - return Any(f, wcc...) + return Any(fn, wcc...) } diff --git a/decor/eta.go b/decor/eta.go index 6cb27a2..d03caa7 100644 --- a/decor/eta.go +++ b/decor/eta.go @@ -63,7 +63,7 @@ producer func(time.Duration) string } -func (d *movingAverageETA) Decor(s *Statistics) string { +func (d *movingAverageETA) Decor(s Statistics) string { v := math.Round(d.average.Value()) remaining := time.Duration((s.Total - s.Current) * int64(v)) if d.normalizer != nil { @@ -117,7 +117,7 @@ producer func(time.Duration) string } -func (d *averageETA) Decor(s *Statistics) string { +func (d *averageETA) Decor(s Statistics) string { var remaining time.Duration if s.Current != 0 { durPerItem := float64(time.Since(d.startTime)) / float64(s.Current) diff --git a/decor/merge.go b/decor/merge.go index 520f13a..8476711 100644 --- a/decor/merge.go +++ b/decor/merge.go @@ -1,9 +1,10 @@ package decor import ( - "fmt" "strings" - "unicode/utf8" + + "github.com/acarl005/stripansi" + "github.com/mattn/go-runewidth" ) // Merge wraps its decorator argument with intention to sync width @@ -16,6 +17,9 @@ // +----+--------+---------+--------+ // func Merge(decorator Decorator, placeholders ...WC) Decorator { + if decorator == nil { + return nil + } if _, ok := decorator.Sync(); !ok || len(placeholders) == 0 { return decorator } @@ -64,18 +68,18 @@ return d.Decorator } -func (d *mergeDecorator) Decor(s *Statistics) string { +func (d *mergeDecorator) Decor(s Statistics) string { msg := d.Decorator.Decor(s) - msgLen := utf8.RuneCountInString(msg) + pureWidth := runewidth.StringWidth(msg) + stripWidth := runewidth.StringWidth(stripansi.Strip(msg)) + cellCount := stripWidth if (d.wc.C & DextraSpace) != 0 { - msgLen++ + cellCount++ } - var total int - max := utf8.RuneCountInString(d.placeHolders[0].FormatMsg("")) - total += max - pw := (msgLen - max) / len(d.placeHolders) - rem := (msgLen - max) % len(d.placeHolders) + total := runewidth.StringWidth(d.placeHolders[0].FormatMsg("")) + pw := (cellCount - total) / len(d.placeHolders) + rem := (cellCount - total) % len(d.placeHolders) var diff int for i := 1; i < len(d.placeHolders); i++ { @@ -87,20 +91,20 @@ width = 0 } } - max = utf8.RuneCountInString(ph.FormatMsg(strings.Repeat(" ", width))) + max := runewidth.StringWidth(ph.FormatMsg(strings.Repeat(" ", width))) total += max diff = max - pw } d.wc.wsync <- pw + rem - max = <-d.wc.wsync - return fmt.Sprintf(fmt.Sprintf(d.wc.dynFormat, max+total), msg) + max := <-d.wc.wsync + return d.wc.fill(msg, max+total+(pureWidth-stripWidth)) } type placeHolderDecorator struct { WC } -func (d *placeHolderDecorator) Decor(*Statistics) string { +func (d *placeHolderDecorator) Decor(Statistics) string { return "" } diff --git a/decor/name.go b/decor/name.go index a7d477e..3af3112 100644 --- a/decor/name.go +++ b/decor/name.go @@ -8,5 +8,5 @@ // `wcc` optional WC config // func Name(str string, wcc ...WC) Decorator { - return Any(func(*Statistics) string { return str }, wcc...) + return Any(func(Statistics) string { return str }, wcc...) } diff --git a/decor/on_abort.go b/decor/on_abort.go new file mode 100644 index 0000000..10ff670 --- /dev/null +++ b/decor/on_abort.go @@ -0,0 +1,41 @@ +package decor + +// OnAbort returns decorator, which wraps provided decorator with sole +// purpose to display provided message on abort event. It has no effect +// if bar.Abort(drop bool) is called with true argument. +// +// `decorator` Decorator to wrap +// +// `message` message to display on abort event +// +func OnAbort(decorator Decorator, message string) Decorator { + if decorator == nil { + return nil + } + d := &onAbortWrapper{ + Decorator: decorator, + msg: message, + } + if md, ok := decorator.(*mergeDecorator); ok { + d.Decorator, md.Decorator = md.Decorator, d + return md + } + return d +} + +type onAbortWrapper struct { + Decorator + msg string +} + +func (d *onAbortWrapper) Decor(s Statistics) string { + if s.Aborted { + wc := d.GetConf() + return wc.FormatMsg(d.msg) + } + return d.Decorator.Decor(s) +} + +func (d *onAbortWrapper) Base() Decorator { + return d.Decorator +} diff --git a/decor/on_complete.go b/decor/on_complete.go index 0a1526b..2ada2b3 100644 --- a/decor/on_complete.go +++ b/decor/on_complete.go @@ -1,6 +1,6 @@ package decor -// OnComplete returns decorator, which wraps provided decorator, with +// OnComplete returns decorator, which wraps provided decorator with // sole purpose to display provided message on complete event. // // `decorator` Decorator to wrap @@ -8,6 +8,9 @@ // `message` message to display on complete event // func OnComplete(decorator Decorator, message string) Decorator { + if decorator == nil { + return nil + } d := &onCompleteWrapper{ Decorator: decorator, msg: message, @@ -24,7 +27,7 @@ msg string } -func (d *onCompleteWrapper) Decor(s *Statistics) string { +func (d *onCompleteWrapper) Decor(s Statistics) string { if s.Completed { wc := d.GetConf() return wc.FormatMsg(d.msg) diff --git a/decor/on_condition.go b/decor/on_condition.go new file mode 100644 index 0000000..a9db065 --- /dev/null +++ b/decor/on_condition.go @@ -0,0 +1,27 @@ +package decor + +// OnPredicate returns decorator if predicate evaluates to true. +// +// `decorator` Decorator +// +// `predicate` func() bool +// +func OnPredicate(decorator Decorator, predicate func() bool) Decorator { + if predicate() { + return decorator + } + return nil +} + +// OnCondition returns decorator if condition is true. +// +// `decorator` Decorator +// +// `cond` bool +// +func OnCondition(decorator Decorator, cond bool) Decorator { + if cond { + return decorator + } + return nil +} diff --git a/decor/optimistic_string_writer.go b/decor/optimistic_string_writer.go new file mode 100644 index 0000000..ea9fda7 --- /dev/null +++ b/decor/optimistic_string_writer.go @@ -0,0 +1,12 @@ +package decor + +import "io" + +func optimisticStringWriter(w io.Writer) func(string) { + return func(s string) { + _, err := io.WriteString(w, s) + if err != nil { + panic(err) + } + } +} diff --git a/decor/percentage.go b/decor/percentage.go index 65ca7d3..6e7f5c6 100644 --- a/decor/percentage.go +++ b/decor/percentage.go @@ -2,10 +2,9 @@ import ( "fmt" - "io" "strconv" - "github.com/vbauerster/mpb/v5/internal" + "github.com/vbauerster/mpb/v7/internal" ) type percentageType float64 @@ -24,12 +23,12 @@ } } - io.WriteString(st, strconv.FormatFloat(float64(s), 'f', prec, 64)) - + osw := optimisticStringWriter(st) + osw(strconv.FormatFloat(float64(s), 'f', prec, 64)) if st.Flag(' ') { - io.WriteString(st, " ") + osw(" ") } - io.WriteString(st, "%") + osw("%") } // Percentage returns percentage decorator. It's a wrapper of NewPercentage. @@ -50,7 +49,7 @@ if format == "" { format = "% d" } - f := func(s *Statistics) string { + f := func(s Statistics) string { p := internal.Percentage(s.Total, s.Current, 100) return fmt.Sprintf(format, percentageType(p)) } diff --git a/decor/size_type.go b/decor/size_type.go index e4b9740..12879b8 100644 --- a/decor/size_type.go +++ b/decor/size_type.go @@ -2,8 +2,6 @@ import ( "fmt" - "io" - "math" "strconv" ) @@ -47,16 +45,16 @@ unit = _iMiB case self < _iTiB: unit = _iGiB - case self <= math.MaxInt64: + default: unit = _iTiB } - io.WriteString(st, strconv.FormatFloat(float64(self)/float64(unit), 'f', prec, 64)) - + osw := optimisticStringWriter(st) + osw(strconv.FormatFloat(float64(self)/float64(unit), 'f', prec, 64)) if st.Flag(' ') { - io.WriteString(st, " ") + osw(" ") } - io.WriteString(st, unit.String()) + osw(unit.String()) } const ( @@ -96,14 +94,14 @@ unit = _MB case self < _TB: unit = _GB - case self <= math.MaxInt64: + default: unit = _TB } - io.WriteString(st, strconv.FormatFloat(float64(self)/float64(unit), 'f', prec, 64)) - + osw := optimisticStringWriter(st) + osw(strconv.FormatFloat(float64(self)/float64(unit), 'f', prec, 64)) if st.Flag(' ') { - io.WriteString(st, " ") + osw(" ") } - io.WriteString(st, unit.String()) + osw(unit.String()) } diff --git a/decor/speed.go b/decor/speed.go index 8a48e3f..99cfde2 100644 --- a/decor/speed.go +++ b/decor/speed.go @@ -2,7 +2,6 @@ import ( "fmt" - "io" "math" "time" @@ -24,7 +23,7 @@ func (self *speedFormatter) Format(st fmt.State, verb rune) { self.Formatter.Format(st, verb) - io.WriteString(st, "/s") + optimisticStringWriter(st)("/s") } // EwmaSpeed exponential-weighted-moving-average based speed decorator. @@ -78,7 +77,7 @@ msg string } -func (d *movingAverageSpeed) Decor(s *Statistics) string { +func (d *movingAverageSpeed) Decor(s Statistics) string { if !s.Completed { var speed float64 if v := d.average.Value(); v > 0 { @@ -140,7 +139,7 @@ msg string } -func (d *averageSpeed) Decor(s *Statistics) string { +func (d *averageSpeed) Decor(s Statistics) string { if !s.Completed { speed := float64(s.Current) / float64(time.Since(d.startTime)) d.msg = d.producer(speed * 1e9) diff --git a/decor/speed_test.go b/decor/speed_test.go index 4f16aea..7f7d09d 100644 --- a/decor/speed_test.go +++ b/decor/speed_test.go @@ -122,7 +122,7 @@ for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { decor := NewAverageSpeed(tc.unit, tc.fmt, time.Now().Add(-tc.elapsed)) - stat := &Statistics{ + stat := Statistics{ Current: tc.current, } res := decor.Decor(stat) @@ -250,7 +250,7 @@ for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { decor := NewAverageSpeed(tc.unit, tc.fmt, time.Now().Add(-tc.elapsed)) - stat := &Statistics{ + stat := Statistics{ Current: tc.current, } res := decor.Decor(stat) diff --git a/decor/spinner.go b/decor/spinner.go index abfb2f7..6871639 100644 --- a/decor/spinner.go +++ b/decor/spinner.go @@ -12,7 +12,7 @@ frames = defaultSpinnerStyle } var count uint - f := func(s *Statistics) string { + f := func(s Statistics) string { frame := frames[count%uint(len(frames))] count++ return frame diff --git a/decorators_test.go b/decorators_test.go index 4bc475a..788cc85 100644 --- a/decorators_test.go +++ b/decorators_test.go @@ -4,8 +4,8 @@ "sync" "testing" - . "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func TestNameDecorator(t *testing.T) { @@ -32,7 +32,7 @@ } for _, test := range tests { - got := test.decorator.Decor(new(decor.Statistics)) + got := test.decorator.Decor(decor.Statistics{}) if got != test.want { t.Errorf("Want: %q, Got: %q\n", test.want, got) } @@ -40,7 +40,7 @@ } type step struct { - stat *decor.Statistics + stat decor.Statistics decorator decor.Decorator want string } @@ -50,36 +50,36 @@ testCases := [][]step{ { { - &decor.Statistics{Total: 100, Current: 8}, + decor.Statistics{Total: 100, Current: 8}, decor.Percentage(decor.WCSyncWidth), "8 %", }, { - &decor.Statistics{Total: 100, Current: 9}, + decor.Statistics{Total: 100, Current: 9}, decor.Percentage(decor.WCSyncWidth), "9 %", }, }, { { - &decor.Statistics{Total: 100, Current: 9}, + decor.Statistics{Total: 100, Current: 9}, decor.Percentage(decor.WCSyncWidth), " 9 %", }, { - &decor.Statistics{Total: 100, Current: 10}, + decor.Statistics{Total: 100, Current: 10}, decor.Percentage(decor.WCSyncWidth), "10 %", }, }, { { - &decor.Statistics{Total: 100, Current: 9}, + decor.Statistics{Total: 100, Current: 9}, decor.Percentage(decor.WCSyncWidth), " 9 %", }, { - &decor.Statistics{Total: 100, Current: 100}, + decor.Statistics{Total: 100, Current: 100}, decor.Percentage(decor.WCSyncWidth), "100 %", }, @@ -94,36 +94,36 @@ testCases := [][]step{ { { - &decor.Statistics{Total: 100, Current: 8}, + decor.Statistics{Total: 100, Current: 8}, decor.Percentage(decor.WCSyncWidthR), "8 %", }, { - &decor.Statistics{Total: 100, Current: 9}, + decor.Statistics{Total: 100, Current: 9}, decor.Percentage(decor.WCSyncWidthR), "9 %", }, }, { { - &decor.Statistics{Total: 100, Current: 9}, + decor.Statistics{Total: 100, Current: 9}, decor.Percentage(decor.WCSyncWidthR), "9 % ", }, { - &decor.Statistics{Total: 100, Current: 10}, + decor.Statistics{Total: 100, Current: 10}, decor.Percentage(decor.WCSyncWidthR), "10 %", }, }, { { - &decor.Statistics{Total: 100, Current: 9}, + decor.Statistics{Total: 100, Current: 9}, decor.Percentage(decor.WCSyncWidthR), "9 % ", }, { - &decor.Statistics{Total: 100, Current: 100}, + decor.Statistics{Total: 100, Current: 100}, decor.Percentage(decor.WCSyncWidthR), "100 %", }, @@ -138,36 +138,36 @@ testCases := [][]step{ { { - &decor.Statistics{Total: 100, Current: 8}, + decor.Statistics{Total: 100, Current: 8}, decor.Percentage(decor.WCSyncSpace), " 8 %", }, { - &decor.Statistics{Total: 100, Current: 9}, + decor.Statistics{Total: 100, Current: 9}, decor.Percentage(decor.WCSyncSpace), " 9 %", }, }, { { - &decor.Statistics{Total: 100, Current: 9}, + decor.Statistics{Total: 100, Current: 9}, decor.Percentage(decor.WCSyncSpace), " 9 %", }, { - &decor.Statistics{Total: 100, Current: 10}, + decor.Statistics{Total: 100, Current: 10}, decor.Percentage(decor.WCSyncSpace), " 10 %", }, }, { { - &decor.Statistics{Total: 100, Current: 9}, + decor.Statistics{Total: 100, Current: 9}, decor.Percentage(decor.WCSyncSpace), " 9 %", }, { - &decor.Statistics{Total: 100, Current: 100}, + decor.Statistics{Total: 100, Current: 100}, decor.Percentage(decor.WCSyncSpace), " 100 %", }, @@ -182,18 +182,20 @@ t.Fail() } - numBars := len(testCases[0]) - var wg sync.WaitGroup for _, columnCase := range testCases { + mpb.SyncWidth(toSyncMatrix(columnCase)) + numBars := len(columnCase) + gott := make([]chan string, numBars) + wg := new(sync.WaitGroup) wg.Add(numBars) - SyncWidth(toSyncMatrix(columnCase)) - gott := make([]chan string, numBars) - for i := 0; i < numBars; i++ { - gott[i] = make(chan string, 1) - go func(s step, ch chan string) { + for i, step := range columnCase { + step := step + ch := make(chan string, 1) + go func() { defer wg.Done() - ch <- s.decorator.Decor(s.stat) - }(columnCase[i], gott[i]) + ch <- step.decorator.Decor(step.stat) + }() + gott[i] = ch } wg.Wait() diff --git a/draw_test.go b/draw_test.go index cc59513..0c7888a 100644 --- a/draw_test.go +++ b/draw_test.go @@ -6,355 +6,1419 @@ "unicode/utf8" ) -func TestDraw(t *testing.T) { +func TestDrawDefault(t *testing.T) { // key is termWidth testSuite := map[int][]struct { - name string - total, current int64 - barWidth int - trimSpace bool - reverse bool - rup int64 - want string + style BarStyleComposer + name string + total int64 + current int64 + refill int64 + barWidth int + trim bool + want string }{ 0: { { - name: "t,c,bw{60,20,80}", + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: "", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "", + }, + }, + 1: { + { + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: "", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "", + }, + }, + 2: { + { + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: " ", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "[]", + }, + }, + 3: { + { + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: " ", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "[-]", + }, + { + style: BarStyle(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " ", + }, + { + style: BarStyle(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[>]", + }, + { + style: BarStyle(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " ", + }, + { + style: BarStyle(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[=]", + }, + }, + 4: { + { + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: " [] ", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "[>-]", + }, + { + style: BarStyle(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [] ", + }, + { + style: BarStyle(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[=>]", + }, + { + style: BarStyle(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [] ", + }, + { + style: BarStyle(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[==]", + }, + }, + 5: { + { + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: " [-] ", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "[>--]", + }, + { + style: BarStyle(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [>] ", + }, + { + style: BarStyle(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[==>]", + }, + { + style: BarStyle(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [=] ", + }, + { + style: BarStyle(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[===]", + }, + }, + 6: { + { + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: " [>-] ", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "[>---]", + }, + { + style: BarStyle(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [=>] ", + }, + { + style: BarStyle(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[===>]", + }, + { + style: BarStyle(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [==] ", + }, + { + style: BarStyle(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[====]", + }, + }, + 7: { + { + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: " [>--] ", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "[=>---]", + }, + { + style: BarStyle(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [==>] ", + }, + { + style: BarStyle(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[====>]", + }, + { + style: BarStyle(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [===] ", + }, + { + style: BarStyle(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[=====]", + }, + }, + 8: { + { + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: " [>---] ", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "[=>----]", + }, + { + style: BarStyle(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [===>] ", + }, + { + style: BarStyle(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[=====>]", + }, + { + style: BarStyle(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [====] ", + }, + { + style: BarStyle(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[======]", + }, + }, + 80: { + { + style: BarStyle(), + name: "t,c{60,20}", + total: 60, + current: 20, + want: " [========================>---------------------------------------------------] ", + }, + { + style: BarStyle(), + name: "t,c{60,20}trim", + total: 60, + current: 20, + trim: true, + want: "[=========================>----------------------------------------------------]", + }, + { + style: BarStyle(), + name: "t,c,bw{60,20,60}", total: 60, current: 20, - barWidth: 80, - want: "", - }, - { - name: "t,c,bw{60,20,80}", - total: 60, - current: 20, - barWidth: 80, - trimSpace: true, - want: "", - }, - }, - 1: { - { - name: "t,c,bw{60,20,80}", + barWidth: 60, + want: " [==================>---------------------------------------] ", + }, + { + style: BarStyle(), + name: "t,c,bw{60,20,60}trim", total: 60, current: 20, - barWidth: 80, - want: "", - }, - { - name: "t,c,bw{60,20,80}", - total: 60, - current: 20, - barWidth: 80, - trimSpace: true, - want: "", - }, - }, - 2: { - { - name: "t,c,bw{60,20,80}", + barWidth: 60, + trim: true, + want: "[==================>---------------------------------------]", + }, + { + style: BarStyle(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [==========================================================================>-] ", + }, + { + style: BarStyle(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[============================================================================>-]", + }, + { + style: BarStyle(), + name: "t,c,bw{60,59,60}", total: 60, - current: 20, - barWidth: 80, - want: " ", - }, - { - name: "t,c,bw,trim{60,20,80,true}", - total: 60, - current: 20, - barWidth: 80, - trimSpace: true, - want: "", - }, - }, - 3: { - { - name: "t,c,bw{60,20,80}", + current: 59, + barWidth: 60, + want: " [========================================================>-] ", + }, + { + style: BarStyle(), + name: "t,c,bw{60,59,60}trim", total: 60, - current: 20, - barWidth: 80, - want: " ", - }, - { - name: "t,c,bw,trim{60,20,80,true}", - total: 60, - current: 20, - barWidth: 80, - trimSpace: true, - want: "", - }, - }, - 4: { - { - name: "t,c,bw{60,20,80}", + current: 59, + barWidth: 60, + trim: true, + want: "[========================================================>-]", + }, + { + style: BarStyle(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [============================================================================] ", + }, + { + style: BarStyle(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[==============================================================================]", + }, + { + style: BarStyle(), + name: "t,c,bw{60,60,60}", total: 60, - current: 20, - barWidth: 80, - want: " ", - }, - { - name: "t,c,bw,trim{60,20,80,true}", - total: 60, - current: 20, - barWidth: 80, - trimSpace: true, - want: "[>-]", - }, - }, - 5: { - { - name: "t,c,bw{60,20,80}", + current: 60, + barWidth: 60, + want: " [==========================================================] ", + }, + { + style: BarStyle(), + name: "t,c,bw{60,60,60}trim", total: 60, - current: 20, - barWidth: 80, - want: " ", - }, - { - name: "t,c,bw,trim{60,20,80,true}", - total: 60, - current: 20, - barWidth: 80, - trimSpace: true, - want: "[>--]", - }, - }, - 6: { - { - name: "t,c,bw{60,20,80}", - total: 60, - current: 20, - barWidth: 80, - want: " [>-] ", - }, - { - name: "t,c,bw,trim{60,20,80,true}", - total: 60, - current: 20, - barWidth: 80, - trimSpace: true, - want: "[>---]", - }, - }, - 7: { - { - name: "t,c,bw{60,20,80}", - total: 60, - current: 20, - barWidth: 80, - want: " [>--] ", - }, - { - name: "t,c,bw,trim{60,20,80,true}", - total: 60, - current: 20, - barWidth: 80, - trimSpace: true, - want: "[=>---]", - }, - }, - 8: { - { - name: "t,c,bw{60,20,80}", - total: 60, - current: 20, - barWidth: 80, - want: " [>---] ", - }, - { - name: "t,c,bw,trim{60,20,80,true}", - total: 60, - current: 20, - barWidth: 80, - trimSpace: true, - want: "[=>----]", - }, - }, - 80: { - { - name: "t,c,bw{60,20,80}", - total: 60, - current: 20, - barWidth: 80, - want: " [========================>---------------------------------------------------] ", - }, - { - name: "t,c,bw,trim{60,20,80,true}", - total: 60, - current: 20, - barWidth: 80, - trimSpace: true, - want: "[=========================>----------------------------------------------------]", + current: 60, + barWidth: 60, + trim: true, + want: "[==========================================================]", + }, + }, + 99: { + { + style: BarStyle(), + name: "t,c{100,1}", + total: 100, + current: 1, + want: " [>----------------------------------------------------------------------------------------------] ", + }, + { + style: BarStyle(), + name: "t,c{100,1}trim", + total: 100, + current: 1, + trim: true, + want: "[>------------------------------------------------------------------------------------------------]", + }, + { + style: BarStyle(), + name: "t,c,r{100,40,33}", + total: 100, + current: 40, + refill: 33, + want: " [+++++++++++++++++++++++++++++++======>---------------------------------------------------------] ", + }, + { + style: BarStyle(), + name: "t,c,r{100,40,33}trim", + total: 100, + current: 40, + refill: 33, + trim: true, + want: "[++++++++++++++++++++++++++++++++======>----------------------------------------------------------]", + }, + { + style: BarStyle().Tip("<").Reverse(), + name: "t,c,r{100,40,33},rev", + total: 100, + current: 40, + refill: 33, + want: " [---------------------------------------------------------<======+++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().Tip("<").Reverse(), + name: "t,c,r{100,40,33}trim,rev", + total: 100, + current: 40, + refill: 33, + trim: true, + want: "[----------------------------------------------------------<======++++++++++++++++++++++++++++++++]", + }, + { + style: BarStyle(), + name: "t,c{100,99}", + total: 100, + current: 99, + want: " [=============================================================================================>-] ", + }, + { + style: BarStyle(), + name: "t,c{100,99}trim", + total: 100, + current: 99, + trim: true, + want: "[===============================================================================================>-]", + }, + { + style: BarStyle(), + name: "t,c{100,100}", + total: 100, + current: 100, + want: " [===============================================================================================] ", + }, + { + style: BarStyle(), + name: "t,c{100,100}trim", + total: 100, + current: 100, + trim: true, + want: "[=================================================================================================]", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,99}", + total: 100, + current: 100, + refill: 99, + want: " [++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++=] ", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,99}trim", + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++=]", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,99}rev", + total: 100, + current: 100, + refill: 99, + want: " [=++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,99}trim,rev", + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[=++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,100}", + total: 100, + current: 100, + refill: 100, + want: " [+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,100}trim", + total: 100, + current: 100, + refill: 100, + trim: true, + want: "[+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,100}rev", + total: 100, + current: 100, + refill: 100, + want: " [+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,100}trim", + total: 100, + current: 100, + refill: 100, + trim: true, + want: "[+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", }, }, 100: { { - name: "t,c,bw{100,100,0}", - total: 100, - current: 0, - barWidth: 100, - want: " [------------------------------------------------------------------------------------------------] ", - }, - { - name: "t,c,bw,trim{100,100,0,true}", - total: 100, - current: 0, - barWidth: 100, - trimSpace: true, - want: "[--------------------------------------------------------------------------------------------------]", - }, - { - name: "t,c,bw{100,1,100}", - total: 100, - current: 1, - barWidth: 100, - want: " [>-----------------------------------------------------------------------------------------------] ", - }, - { - name: "t,c,bw,trim{100,1,100,true}", - total: 100, - current: 1, - barWidth: 100, - trimSpace: true, - want: "[>-------------------------------------------------------------------------------------------------]", - }, - { - name: "t,c,bw{100,33,100}", - total: 100, - current: 33, - barWidth: 100, - want: " [===============================>----------------------------------------------------------------] ", - }, - { - name: "t,c,bw,trim{100,33,100,true}", - total: 100, - current: 33, - barWidth: 100, - trimSpace: true, - want: "[===============================>------------------------------------------------------------------]", - }, - { - name: "t,c,bw,trim,rev{100,33,100,true,true}", - total: 100, - current: 33, - barWidth: 100, - trimSpace: true, - reverse: true, - want: "[------------------------------------------------------------------<===============================]", - }, - { - name: "t,c,bw,rup{100,33,100,33}", - total: 100, - current: 33, - barWidth: 100, - rup: 33, - want: " [+++++++++++++++++++++++++++++++>----------------------------------------------------------------] ", - }, - { - name: "t,c,bw,rup,trim{100,33,100,33,true}", - total: 100, - current: 33, - barWidth: 100, - rup: 33, - trimSpace: true, - want: "[+++++++++++++++++++++++++++++++>------------------------------------------------------------------]", - }, - { - name: "t,c,bw,rup,trim,rev{100,33,100,33,true,true}", - total: 100, - current: 33, - barWidth: 100, - rup: 33, - trimSpace: true, - reverse: true, - want: "[------------------------------------------------------------------<+++++++++++++++++++++++++++++++]", - }, - { - name: "t,c,bw,rup{100,40,100,32}", - total: 100, - current: 40, - barWidth: 100, - rup: 33, - want: " [++++++++++++++++++++++++++++++++=====>----------------------------------------------------------] ", - }, - { - name: "t,c,bw,rup,trim{100,40,100,32,true}", - total: 100, - current: 40, - barWidth: 100, - rup: 33, - trimSpace: true, - want: "[++++++++++++++++++++++++++++++++======>-----------------------------------------------------------]", - }, - { - name: "t,c,bw{100,99,100}", - total: 100, - current: 99, - barWidth: 100, - want: " [==============================================================================================>-] ", - }, - { - name: "t,c,bw,trim{100,99,100,true}", - total: 100, - current: 99, - barWidth: 100, - trimSpace: true, - want: "[================================================================================================>-]", - }, - { - name: "t,c,bw{100,100,100}", - total: 100, - current: 100, - barWidth: 100, - want: " [================================================================================================] ", - }, - { - name: "t,c,bw,trim{100,100,100,true}", - total: 100, - current: 100, - barWidth: 100, - trimSpace: true, - want: "[==================================================================================================]", + style: BarStyle(), + name: "t,c{100,0}", + total: 100, + current: 0, + want: " [------------------------------------------------------------------------------------------------] ", + }, + { + style: BarStyle(), + name: "t,c{100,0}trim", + total: 100, + current: 0, + trim: true, + want: "[--------------------------------------------------------------------------------------------------]", + }, + { + style: BarStyle(), + name: "t,c{100,1}", + total: 100, + current: 1, + want: " [>-----------------------------------------------------------------------------------------------] ", + }, + { + style: BarStyle(), + name: "t,c{100,1}trim", + total: 100, + current: 1, + trim: true, + want: "[>-------------------------------------------------------------------------------------------------]", + }, + { + style: BarStyle(), + name: "t,c{100,99}", + total: 100, + current: 99, + want: " [==============================================================================================>-] ", + }, + { + style: BarStyle(), + name: "t,c{100,99}trim", + total: 100, + current: 99, + trim: true, + want: "[================================================================================================>-]", + }, + { + style: BarStyle(), + name: "t,c{100,100}", + total: 100, + current: 100, + want: " [================================================================================================] ", + }, + { + style: BarStyle(), + name: "t,c{100,100}trim", + total: 100, + current: 100, + trim: true, + want: "[==================================================================================================]", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,99}", + total: 100, + current: 100, + refill: 99, + want: " [+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++=] ", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,99}trim", + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++=]", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,100}", + total: 100, + current: 100, + refill: 100, + want: " [++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,100}trim", + total: 100, + current: 100, + refill: 100, + trim: true, + want: "[++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,99}rev", + total: 100, + current: 100, + refill: 99, + want: " [=+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,99}trim,rev", + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[=+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,100}rev", + total: 100, + current: 100, + refill: 100, + want: " [++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,100}trim", + total: 100, + current: 100, + refill: 100, + trim: true, + want: "[++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, + { + style: BarStyle(), + name: "t,c,r{100,40,33}", + total: 100, + current: 40, + refill: 33, + want: " [++++++++++++++++++++++++++++++++=====>----------------------------------------------------------] ", + }, + { + style: BarStyle(), + name: "t,c,r{100,40,33}trim", + total: 100, + current: 40, + refill: 33, + trim: true, + want: "[++++++++++++++++++++++++++++++++======>-----------------------------------------------------------]", + }, + { + style: BarStyle().Tip("<").Reverse(), + name: "t,c,r{100,40,33},rev", + total: 100, + current: 40, + refill: 33, + want: " [----------------------------------------------------------<=====++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().Tip("<").Reverse(), + name: "t,c,r{100,40,33}trim,rev", + total: 100, + current: 40, + refill: 33, + trim: true, + want: "[-----------------------------------------------------------<======++++++++++++++++++++++++++++++++]", }, }, } var tmpBuf bytes.Buffer - for termWidth, cases := range testSuite { + for tw, cases := range testSuite { for _, tc := range cases { - s := newTestState(tc.reverse) - s.width = tc.barWidth + s := newTestState(tc.style.Build()) + s.reqWidth = tc.barWidth s.total = tc.total s.current = tc.current - s.trimSpace = tc.trimSpace - if tc.rup > 0 { - if f, ok := s.filler.(interface{ SetRefill(int64) }); ok { - f.SetRefill(tc.rup) - } + s.trimSpace = tc.trim + s.refill = tc.refill + tmpBuf.Reset() + _, err := tmpBuf.ReadFrom(s.draw(newStatistics(tw, s))) + if err != nil { + t.FailNow() } - tmpBuf.Reset() - tmpBuf.ReadFrom(s.draw(termWidth, newStatistics(s))) by := tmpBuf.Bytes() - by = by[:len(by)-1] - if utf8.RuneCount(by) > termWidth { - t.Errorf("termWidth:%d %q barWidth:%d overflow termWidth\n", termWidth, tc.name, utf8.RuneCount(by)) + got := string(by[:len(by)-1]) + if !utf8.ValidString(got) { + t.Fail() } - - got := string(by) if got != tc.want { - t.Errorf("termWidth:%d %q want: %q %d, got: %q %d\n", termWidth, tc.name, tc.want, len(tc.want), got, len(got)) + t.Errorf("termWidth:%d %q want: %q %d, got: %q %d\n", tw, tc.name, tc.want, utf8.RuneCountInString(tc.want), got, utf8.RuneCountInString(got)) } } } } -func newTestState(reverse bool) *bState { - s := &bState{ - filler: NewBarFiller(DefaultBarStyle, reverse), - bufP: new(bytes.Buffer), - bufB: new(bytes.Buffer), - bufA: new(bytes.Buffer), +func TestDrawTipOnComplete(t *testing.T) { + // key is termWidth + testSuite := map[int][]struct { + style BarStyleComposer + name string + total int64 + current int64 + refill int64 + barWidth int + trim bool + want string + }{ + 3: { + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[>]", + }, + }, + 4: { + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[=>]", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[=>]", + }, + }, + 5: { + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [>] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[==>]", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [>] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[==>]", + }, + }, + 6: { + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [=>] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[===>]", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [=>] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[===>]", + }, + }, + 7: { + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [==>] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[====>]", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [==>] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[====>]", + }, + }, + 8: { + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [===>] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[=====>]", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [===>] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[=====>]", + }, + }, + 80: { + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [==========================================================================>-] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[============================================================================>-]", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c,bw{60,59,60}", + total: 60, + current: 59, + barWidth: 60, + want: " [========================================================>-] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c,bw{60,59,60}trim", + total: 60, + current: 59, + barWidth: 60, + trim: true, + want: "[========================================================>-]", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [===========================================================================>] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[=============================================================================>]", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c,bw{60,60,60}", + total: 60, + current: 60, + barWidth: 60, + want: " [=========================================================>] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c,bw{60,60,60}trim", + total: 60, + current: 60, + barWidth: 60, + trim: true, + want: "[=========================================================>]", + }, + }, + 99: { + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{100,99}", + total: 100, + current: 99, + want: " [=============================================================================================>-] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{100,99}trim", + total: 100, + current: 99, + trim: true, + want: "[===============================================================================================>-]", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{100,100}", + total: 100, + current: 100, + want: " [==============================================================================================>] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{100,100}trim", + total: 100, + current: 100, + trim: true, + want: "[================================================================================================>]", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c,r{100,100,99}", + total: 100, + current: 100, + refill: 99, + want: " [++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c,r{100,100,99}trim", + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>]", + }, + { + style: BarStyle().TipOnComplete("<").Reverse(), + name: `t,c,r{100,100,99}TipOnComplete("<").Reverse()`, + total: 100, + current: 100, + refill: 99, + want: " [<++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().TipOnComplete("<").Reverse(), + name: `t,c,r{100,100,99}TipOnComplete("<").Reverse()trim`, + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[<++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c,r{100,100,100}", + total: 100, + current: 100, + refill: 100, + want: " [++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c,r{100,100,100}trim", + total: 100, + current: 100, + refill: 100, + trim: true, + want: "[++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>]", + }, + { + style: BarStyle().TipOnComplete("<").Reverse(), + name: `t,c,r{100,100,100}TipOnComplete("<").Reverse()`, + total: 100, + current: 100, + refill: 100, + want: " [<++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().TipOnComplete("<").Reverse(), + name: `t,c,r{100,100,100}TipOnComplete("<").Reverse()trim`, + total: 100, + current: 100, + refill: 100, + trim: true, + want: "[<++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, + }, + 100: { + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{100,99}", + total: 100, + current: 99, + want: " [==============================================================================================>-] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{100,99}trim", + total: 100, + current: 99, + trim: true, + want: "[================================================================================================>-]", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{100,100}", + total: 100, + current: 100, + want: " [===============================================================================================>] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c{100,100}trim", + total: 100, + current: 100, + trim: true, + want: "[=================================================================================================>]", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c,r{100,100,99}", + total: 100, + current: 100, + refill: 99, + want: " [+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c,r{100,100,99}trim", + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>]", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c,r{100,100,100}", + total: 100, + current: 100, + refill: 100, + want: " [+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>] ", + }, + { + style: BarStyle().TipOnComplete(">"), + name: "t,c,r{100,100,100}trim", + total: 100, + current: 100, + refill: 100, + trim: true, + want: "[+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>]", + }, + }, } - return s + + var tmpBuf bytes.Buffer + for tw, cases := range testSuite { + for _, tc := range cases { + s := newTestState(tc.style.Build()) + s.reqWidth = tc.barWidth + s.total = tc.total + s.current = tc.current + s.trimSpace = tc.trim + s.refill = tc.refill + tmpBuf.Reset() + _, err := tmpBuf.ReadFrom(s.draw(newStatistics(tw, s))) + if err != nil { + t.FailNow() + } + by := tmpBuf.Bytes() + + got := string(by[:len(by)-1]) + if !utf8.ValidString(got) { + t.Fail() + } + if got != tc.want { + t.Errorf("termWidth:%d %q want: %q %d, got: %q %d\n", tw, tc.name, tc.want, utf8.RuneCountInString(tc.want), got, utf8.RuneCountInString(got)) + } + } + } } + +func TestDrawDoubleWidth(t *testing.T) { + // key is termWidth + testSuite := map[int][]struct { + style BarStyleComposer + name string + total int64 + current int64 + refill int64 + barWidth int + trim bool + want string + }{ + 99: { + { + style: BarStyle().Lbound("の").Rbound("の"), + name: `t,c{100,1}.Lbound("の").Rbound("の")`, + total: 100, + current: 1, + want: " の>--------------------------------------------------------------------------------------------の ", + }, + { + style: BarStyle().Lbound("の").Rbound("の"), + name: `t,c{100,1}.Lbound("の").Rbound("の")`, + total: 100, + current: 2, + want: " の=>-------------------------------------------------------------------------------------------の ", + }, + { + style: BarStyle().Tip("だ"), + name: `t,c{100,1}Tip("だ")`, + total: 100, + current: 1, + want: " [だ---------------------------------------------------------------------------------------------] ", + }, + { + style: BarStyle().Tip("だ"), + name: `t,c{100,2}Tip("だ")`, + total: 100, + current: 2, + want: " [だ---------------------------------------------------------------------------------------------] ", + }, + { + style: BarStyle().Tip("だ"), + name: `t,c{100,3}Tip("だ")`, + total: 100, + current: 3, + want: " [=だ--------------------------------------------------------------------------------------------] ", + }, + { + style: BarStyle().Tip("だ"), + name: `t,c{100,99}Tip("だ")`, + total: 100, + current: 99, + want: " [============================================================================================だ-] ", + }, + { + style: BarStyle().Tip("だ"), + name: `t,c{100,100}Tip("だ")`, + total: 100, + current: 100, + want: " [===============================================================================================] ", + }, + { + style: BarStyle().TipOnComplete("だ"), + name: `t,c{100,100}TipOnComplete("だ")`, + total: 100, + current: 100, + want: " [=============================================================================================だ] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").Padding("つ"), + name: `t,c{100,1}Filler("の").Tip("だ").Padding("つ")`, + total: 100, + current: 1, + want: " [だつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつ…] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").Padding("つ"), + name: `t,c{100,2}Filler("の").Tip("だ").Padding("つ")`, + total: 100, + current: 2, + want: " [だつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつ…] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").Padding("つ"), + name: `t,c{100,99}Filler("の").Tip("だ").Padding("つ")`, + total: 100, + current: 99, + want: " [ののののののののののののののののののののののののののののののののののののののののののののののだ…] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").Padding("つ"), + name: `t,c{100,100}.Filler("の").Tip("だ").Padding("つ")`, + total: 100, + current: 100, + want: " […ののののののののののののののののののののののののののののののののののののののののののののののの] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").Padding("つ").Reverse(), + name: `t,c{100,100}Filler("の").Tip("だ").Padding("つ").Reverse()`, + total: 100, + current: 100, + want: " [ののののののののののののののののののののののののののののののののののののののののののののののの…] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").TipOnComplete("だ").Padding("つ"), + name: `t,c{100,99}Filler("の").Tip("だ").TipOnComplete("だ").Padding("つ")`, + total: 100, + current: 99, + want: " [ののののののののののののののののののののののののののののののののののののののののののののののだ…] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").TipOnComplete("だ").Padding("つ"), + name: `t,c{100,100}.Filler("の").Tip("だ").TipOnComplete("だ").Padding("つ")`, + total: 100, + current: 100, + want: " […ののののののののののののののののののののののののののののののののののののののののののののののだ] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").TipOnComplete("だ").Padding("つ").Reverse(), + name: `t,c{100,100}.Filler("の").Tip("だ").TipOnComplete("だ").Padding("つ").Reverse()`, + total: 100, + current: 100, + want: " [だのののののののののののののののののののののののののののののののののののののののののののののの…] ", + }, + { + style: BarStyle().Refiller("の"), + name: `t,c,r{100,100,99}Refiller("の")`, + total: 100, + current: 100, + refill: 99, + want: " [ののののののののののののののののののののののののののののののののののののののののののののののの=] ", + }, + { + style: BarStyle().Refiller("の"), + name: `t,c,r{100,100,99}Refiller("の")trim`, + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[のののののののののののののののののののののののののののののののののののののののののののののののの=]", + }, + }, + } + + var tmpBuf bytes.Buffer + for tw, cases := range testSuite { + for _, tc := range cases { + s := newTestState(tc.style.Build()) + s.reqWidth = tc.barWidth + s.total = tc.total + s.current = tc.current + s.trimSpace = tc.trim + s.refill = tc.refill + tmpBuf.Reset() + _, err := tmpBuf.ReadFrom(s.draw(newStatistics(tw, s))) + if err != nil { + t.FailNow() + } + by := tmpBuf.Bytes() + + got := string(by[:len(by)-1]) + if !utf8.ValidString(got) { + t.Fail() + } + if got != tc.want { + t.Errorf("termWidth:%d %q want: %q %d, got: %q %d\n", tw, tc.name, tc.want, utf8.RuneCountInString(tc.want), got, utf8.RuneCountInString(got)) + } + } + } +} + +func newTestState(filler BarFiller) *bState { + bs := &bState{ + filler: filler, + } + for i := 0; i < len(bs.buffers); i++ { + bs.buffers[i] = bytes.NewBuffer(make([]byte, 0, 512)) + } + return bs +} diff --git a/example_test.go b/example_test.go index b25928d..72596c4 100644 --- a/example_test.go +++ b/example_test.go @@ -7,8 +7,8 @@ "math/rand" "time" - "github.com/vbauerster/mpb/v5" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func Example() { @@ -17,10 +17,10 @@ total := 100 name := "Single Bar:" - // adding a single bar, which will inherit container's width - bar := p.AddBar(int64(total), - // override DefaultBarStyle, which is "[=>-]<+" - mpb.BarStyle("╢▌▌░╟"), + // create a single bar, which will inherit container's width + bar := p.New(int64(total), + // BarFillerBuilder with custom style + mpb.BarStyle().Lbound("╢").Filler("▌").Tip("▌").Padding("░").Rbound("╟"), mpb.PrependDecorators( // display our name with one space on the right decor.Name(name, decor.WC{W: len(name) + 1, C: decor.DidentRight}), @@ -78,7 +78,7 @@ defer proxyReader.Close() // and copy from reader, ignoring errors - io.Copy(ioutil.Discard, proxyReader) + _, _ = io.Copy(ioutil.Discard, proxyReader) p.Wait() } diff --git a/export_test.go b/export_test.go index 7f5cb84..fba0eaf 100644 --- a/export_test.go +++ b/export_test.go @@ -2,3 +2,4 @@ // make syncWidth func public in test var SyncWidth = syncWidth +var MaxWidthDistributor = &maxWidthDistributor diff --git a/go.mod b/go.mod index 672191f..8fa790d 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ -module github.com/vbauerster/mpb/v5 +module github.com/vbauerster/mpb/v7 require ( - github.com/VividCortex/ewma v1.1.1 + github.com/VividCortex/ewma v1.2.0 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d - golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4 - golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 // indirect + github.com/mattn/go-runewidth v0.0.13 + golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 ) go 1.14 diff --git a/go.sum b/go.sum index 9a41197..aebe4d9 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,10 @@ -github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= -github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4 h1:QmwruyY+bKbDDL0BaglrbZABEali68eoMFhTZpCjYVA= -golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/percentage.go b/internal/percentage.go index 7e261cb..4bc36f5 100644 --- a/internal/percentage.go +++ b/internal/percentage.go @@ -3,13 +3,17 @@ import "math" // Percentage is a helper function, to calculate percentage. -func Percentage(total, current int64, width int) float64 { +func Percentage(total, current int64, width uint) float64 { if total <= 0 { return 0 + } + if current >= total { + return float64(width) } return float64(int64(width)*current) / float64(total) } -func PercentageRound(total, current int64, width int) float64 { +// PercentageRound same as Percentage but with math.Round. +func PercentageRound(total, current int64, width uint) float64 { return math.Round(Percentage(total, current, width)) } diff --git a/internal/percentage_test.go b/internal/percentage_test.go index a2fa3f7..515ad60 100644 --- a/internal/percentage_test.go +++ b/internal/percentage_test.go @@ -4,7 +4,7 @@ func TestPercentage(t *testing.T) { // key is barWidth - testSuite := map[int][]struct { + testSuite := map[uint][]struct { name string total int64 current int64 @@ -21,8 +21,7 @@ {"t,c,e{100,50,50}", 100, 50, 50}, {"t,c,e{100,99,99}", 100, 99, 99}, {"t,c,e{100,100,100}", 100, 100, 100}, - {"t,c,e{100,101,101}", 100, 101, 101}, - {"t,c,e{100,102,101}", 100, 102, 102}, + {"t,c,e{100,101,101}", 100, 101, 100}, {"t,c,e{120,0,0}", 120, 0, 0}, {"t,c,e{120,10,8}", 120, 10, 8}, {"t,c,e{120,15,13}", 120, 15, 13}, @@ -33,8 +32,7 @@ {"t,c,e{120,118,98}", 120, 118, 98}, {"t,c,e{120,119,99}", 120, 119, 99}, {"t,c,e{120,120,100}", 120, 120, 100}, - {"t,c,e{120,121,101}", 120, 121, 101}, - {"t,c,e{120,122,101}", 120, 122, 102}, + {"t,c,e{120,121,101}", 120, 121, 100}, }, 80: { {"t,c,e{-1,-1,0}", -1, -1, 0}, @@ -47,8 +45,7 @@ {"t,c,e{100,50,40}", 100, 50, 40}, {"t,c,e{100,99,79}", 100, 99, 79}, {"t,c,e{100,100,80}", 100, 100, 80}, - {"t,c,e{100,101,81}", 100, 101, 81}, - {"t,c,e{100,102,82}", 100, 102, 82}, + {"t,c,e{100,101,81}", 100, 101, 80}, {"t,c,e{120,0,0}", 120, 0, 0}, {"t,c,e{120,10,7}", 120, 10, 7}, {"t,c,e{120,15,10}", 120, 15, 10}, @@ -59,8 +56,7 @@ {"t,c,e{120,118,79}", 120, 118, 79}, {"t,c,e{120,119,79}", 120, 119, 79}, {"t,c,e{120,120,80}", 120, 120, 80}, - {"t,c,e{120,121,81}", 120, 121, 81}, - {"t,c,e{120,122,81}", 120, 122, 81}, + {"t,c,e{120,121,81}", 120, 121, 80}, }, } diff --git a/internal/width.go b/internal/width.go new file mode 100644 index 0000000..7677e40 --- /dev/null +++ b/internal/width.go @@ -0,0 +1,10 @@ +package internal + +// CheckRequestedWidth checks that requested width doesn't overflow +// available width +func CheckRequestedWidth(requested, available int) int { + if requested < 1 || requested >= available { + return available + } + return requested +} diff --git a/options.go b/options.go deleted file mode 100644 index 0488702..0000000 --- a/options.go +++ /dev/null @@ -1,105 +0,0 @@ -package mpb - -import ( - "io" - "io/ioutil" - "sync" - "time" -) - -// ContainerOption is a function option which changes the default -// behavior of progress container, if passed to mpb.New(...ContainerOption). -type ContainerOption func(*pState) - -// WithWaitGroup provides means to have a single joint point. If -// *sync.WaitGroup is provided, you can safely call just p.Wait() -// without calling Wait() on provided *sync.WaitGroup. Makes sense -// when there are more than one bar to render. -func WithWaitGroup(wg *sync.WaitGroup) ContainerOption { - return func(s *pState) { - s.uwg = wg - } -} - -// WithWidth sets container width. Default is 80. Bars inherit this -// width, as long as no BarWidth is applied. -func WithWidth(w int) ContainerOption { - return func(s *pState) { - if w < 0 { - return - } - s.width = w - } -} - -// WithRefreshRate overrides default 120ms refresh rate. -func WithRefreshRate(d time.Duration) ContainerOption { - return func(s *pState) { - s.rr = d - } -} - -// WithManualRefresh disables internal auto refresh time.Ticker. -// Refresh will occur upon receive value from provided ch. -func WithManualRefresh(ch <-chan time.Time) ContainerOption { - return func(s *pState) { - s.refreshSrc = ch - } -} - -// WithRenderDelay delays rendering. By default rendering starts as -// soon as bar is added, with this option it's possible to delay -// rendering process by keeping provided chan unclosed. In other words -// rendering will start as soon as provided chan is closed. -func WithRenderDelay(ch <-chan struct{}) ContainerOption { - return func(s *pState) { - s.renderDelay = ch - } -} - -// WithShutdownNotifier provided chanel will be closed, after all bars -// have been rendered. -func WithShutdownNotifier(ch chan struct{}) ContainerOption { - return func(s *pState) { - s.shutdownNotifier = ch - } -} - -// WithOutput overrides default os.Stdout output. Setting it to nil -// will effectively disable auto refresh rate and discard any output, -// useful if you want to disable progress bars with little overhead. -func WithOutput(w io.Writer) ContainerOption { - return func(s *pState) { - if w == nil { - s.refreshSrc = make(chan time.Time) - s.output = ioutil.Discard - return - } - s.output = w - } -} - -// WithDebugOutput sets debug output. -func WithDebugOutput(w io.Writer) ContainerOption { - if w == nil { - return nil - } - return func(s *pState) { - s.debugOut = w - } -} - -// PopCompletedMode will pop and stop rendering completed bars. -func PopCompletedMode() ContainerOption { - return func(s *pState) { - s.popCompleted = true - } -} - -// ContainerOptOn returns option when condition evaluates to true. -func ContainerOptOn(option ContainerOption, condition func() bool) ContainerOption { - if condition() { - return option - } - return nil -} diff --git a/progress.go b/progress.go index a366b92..123af17 100644 --- a/progress.go +++ b/progress.go @@ -6,24 +6,22 @@ "context" "fmt" "io" - "io/ioutil" - "log" + "math" "os" "sync" "time" - "github.com/vbauerster/mpb/v5/cwriter" - "github.com/vbauerster/mpb/v5/decor" + "github.com/vbauerster/mpb/v7/cwriter" + "github.com/vbauerster/mpb/v7/decor" ) const ( // default RefreshRate - prr = 120 * time.Millisecond - // default width - pwidth = 80 + prr = 150 * time.Millisecond ) -// Progress represents the container that renders Progress bars +// Progress represents a container that renders one or more progress +// bars. type Progress struct { ctx context.Context uwg *sync.WaitGroup @@ -33,24 +31,25 @@ done chan struct{} refreshCh chan time.Time once sync.Once - dlogger *log.Logger -} - +} + +// pState holds bars in its priorityQueue. It gets passed to +// *Progress.serve(...) monitor goroutine. type pState struct { bHeap priorityQueue heapUpdated bool pMatrix map[int][]chan int aMatrix map[int][]chan int barShutdownQueue []*Bar - barPopQueue []*Bar // following are provided/overrided by user idCount int - width int + reqWidth int popCompleted bool + outputDiscarded bool rr time.Duration uwg *sync.WaitGroup - refreshSrc <-chan time.Time + externalRefresh <-chan interface{} renderDelay <-chan struct{} shutdownNotifier chan struct{} parkedBars map[*Bar]*Bar @@ -70,11 +69,9 @@ func NewWithContext(ctx context.Context, options ...ContainerOption) *Progress { s := &pState{ bHeap: priorityQueue{}, - width: pwidth, rr: prr, parkedBars: make(map[*Bar]*Bar), output: os.Stdout, - debugOut: ioutil.Discard, } for _, opt := range options { @@ -90,7 +87,6 @@ bwg: new(sync.WaitGroup), operateState: make(chan func(*pState)), done: make(chan struct{}), - dlogger: log.New(s.debugOut, "[mpb] ", log.Lshortfile), } p.cwg.Add(1) @@ -98,22 +94,27 @@ return p } -// AddBar creates a new progress bar and adds it to the rendering queue. +// AddBar creates a bar with default bar filler. func (p *Progress) AddBar(total int64, options ...BarOption) *Bar { - return p.Add(total, NewBarFiller(DefaultBarStyle, false), options...) -} - -// AddSpinner creates a new spinner bar and adds it to the rendering queue. -func (p *Progress) AddSpinner(total int64, alignment SpinnerAlignment, options ...BarOption) *Bar { - return p.Add(total, NewSpinnerFiller(DefaultSpinnerStyle, alignment), options...) + return p.New(total, BarStyle(), options...) +} + +// AddSpinner creates a bar with default spinner filler. +func (p *Progress) AddSpinner(total int64, options ...BarOption) *Bar { + return p.New(total, SpinnerStyle(), options...) +} + +// New creates a bar with provided BarFillerBuilder. +func (p *Progress) New(total int64, builder BarFillerBuilder, options ...BarOption) *Bar { + return p.Add(total, builder.Build(), options...) } // Add creates a bar which renders itself by provided filler. -// Set total to 0, if you plan to update it later. +// If `total <= 0` trigger complete event is disabled until reset with *bar.SetTotal(int64, bool). // Panics if *Progress instance is done, i.e. called after *Progress.Wait(). func (p *Progress) Add(total int64, filler BarFiller, options ...BarOption) *Bar { if filler == nil { - filler = NewBarFiller(DefaultBarStyle, false) + filler = NopStyle().Build() } p.bwg.Add(1) result := make(chan *Bar) @@ -153,7 +154,25 @@ } } -func (p *Progress) setBarPriority(b *Bar, priority int) { +func (p *Progress) traverseBars(cb func(b *Bar) bool) { + done := make(chan struct{}) + select { + case p.operateState <- func(s *pState) { + for i := 0; i < s.bHeap.Len(); i++ { + bar := s.bHeap[i] + if !cb(bar) { + break + } + } + close(done) + }: + <-done + case <-p.done: + } +} + +// UpdateBarPriority same as *Bar.SetPriority(int). +func (p *Progress) UpdateBarPriority(b *Bar, priority int) { select { case p.operateState <- func(s *pState) { if b.index < 0 { @@ -166,14 +185,9 @@ } } -// UpdateBarPriority same as *Bar.SetPriority(int). -func (p *Progress) UpdateBarPriority(b *Bar, priority int) { - p.setBarPriority(b, priority) -} - -// BarCount returns bars count +// BarCount returns bars count. func (p *Progress) BarCount() int { - result := make(chan int, 1) + result := make(chan int) select { case p.operateState <- func(s *pState) { result <- s.bHeap.Len() }: return <-result @@ -182,7 +196,7 @@ } } -// Wait waits far all bars to complete and finally shutdowns container. +// Wait waits for all bars to complete and finally shutdowns container. // After this method has been called, there is no way to reuse *Progress // instance. func (p *Progress) Wait() { @@ -215,12 +229,69 @@ op(s) case <-p.refreshCh: if err := s.render(cw); err != nil { - go p.dlogger.Println(err) + if s.debugOut != nil { + _, e := fmt.Fprintln(s.debugOut, err) + if e != nil { + panic(err) + } + } else { + panic(err) + } } case <-s.shutdownNotifier: + for s.heapUpdated { + if err := s.render(cw); err != nil { + if s.debugOut != nil { + _, e := fmt.Fprintln(s.debugOut, err) + if e != nil { + panic(err) + } + } else { + panic(err) + } + } + } return } } +} + +func (s *pState) newTicker(done <-chan struct{}) chan time.Time { + ch := make(chan time.Time) + if s.shutdownNotifier == nil { + s.shutdownNotifier = make(chan struct{}) + } + go func() { + if s.renderDelay != nil { + <-s.renderDelay + } + var internalRefresh <-chan time.Time + if !s.outputDiscarded { + if s.externalRefresh == nil { + ticker := time.NewTicker(s.rr) + defer ticker.Stop() + internalRefresh = ticker.C + } + } else { + s.externalRefresh = nil + } + for { + select { + case t := <-internalRefresh: + ch <- t + case x := <-s.externalRefresh: + if t, ok := x.(time.Time); ok { + ch <- t + } else { + ch <- time.Now() + } + case <-done: + close(s.shutdownNotifier) + return + } + } + }() + return ch } func (s *pState) render(cw *cwriter.Writer) error { @@ -233,7 +304,7 @@ tw, err := cw.GetWidth() if err != nil { - tw = s.width + tw = s.reqWidth } for i := 0; i < s.bHeap.Len(); i++ { bar := s.bHeap[i] @@ -244,20 +315,29 @@ } func (s *pState) flush(cw *cwriter.Writer) error { - var lineCount int - bm := make(map[*Bar]struct{}, s.bHeap.Len()) + var totalLines int + bm := make(map[*Bar]int, s.bHeap.Len()) for s.bHeap.Len() > 0 { b := heap.Pop(&s.bHeap).(*Bar) - cw.ReadFrom(<-b.frameCh) + frame := <-b.frameCh + _, err := cw.ReadFrom(frame.reader) + if err != nil { + return err + } if b.toShutdown { - // shutdown at next flush - // this ensures no bar ends up with less than 100% rendered - defer func() { + if b.recoveredPanic != nil { s.barShutdownQueue = append(s.barShutdownQueue, b) - }() - } - lineCount += b.extendedLines + 1 - bm[b] = struct{}{} + b.toShutdown = false + } else { + // shutdown at next flush + // this ensures no bar ends up with less than 100% rendered + defer func() { + s.barShutdownQueue = append(s.barShutdownQueue, b) + }() + } + } + bm[b] = frame.lines + totalLines += frame.lines } for _, b := range s.barShutdownQueue { @@ -267,59 +347,23 @@ delete(s.parkedBars, b) b.toDrop = true } + if s.popCompleted && !b.noPop { + totalLines -= bm[b] + b.toDrop = true + } if b.toDrop { delete(bm, b) s.heapUpdated = true - } else if s.popCompleted { - if b := b; !b.noPop { - defer func() { - s.barPopQueue = append(s.barPopQueue, b) - }() - } } b.cancel() } s.barShutdownQueue = s.barShutdownQueue[0:0] - - for _, b := range s.barPopQueue { - delete(bm, b) - s.heapUpdated = true - lineCount -= b.extendedLines + 1 - } - s.barPopQueue = s.barPopQueue[0:0] for b := range bm { heap.Push(&s.bHeap, b) } - return cw.Flush(lineCount) -} - -func (s *pState) newTicker(done <-chan struct{}) chan time.Time { - ch := make(chan time.Time) - if s.shutdownNotifier == nil { - s.shutdownNotifier = make(chan struct{}) - } - go func() { - if s.renderDelay != nil { - <-s.renderDelay - } - if s.refreshSrc == nil { - ticker := time.NewTicker(s.rr) - defer ticker.Stop() - s.refreshSrc = ticker.C - } - for { - select { - case tick := <-s.refreshSrc: - ch <- tick - case <-done: - close(s.shutdownNotifier) - return - } - } - }() - return ch + return cw.Flush(totalLines) } func (s *pState) updateSyncMatrix() { @@ -342,16 +386,17 @@ func (s *pState) makeBarState(total int64, filler BarFiller, options ...BarOption) *bState { bs := &bState{ + id: s.idCount, + priority: s.idCount, + reqWidth: s.reqWidth, total: total, - baseF: extractBaseFiller(filler), filler: filler, - priority: s.idCount, - id: s.idCount, - width: s.width, + extender: func(r io.Reader, _ int, _ decor.Statistics) (io.Reader, int) { return r, 0 }, debugOut: s.debugOut, - extender: func(r io.Reader, _ int, _ *decor.Statistics) (io.Reader, int) { - return r, 0 - }, + } + + if total > 0 { + bs.triggerComplete = true } for _, opt := range options { @@ -360,40 +405,36 @@ } } + if bs.middleware != nil { + bs.filler = bs.middleware(filler) + bs.middleware = nil + } + if s.popCompleted && !bs.noPop { - bs.priority = -1 - } - - bs.bufP = bytes.NewBuffer(make([]byte, 0, bs.width)) - bs.bufB = bytes.NewBuffer(make([]byte, 0, bs.width)) - bs.bufA = bytes.NewBuffer(make([]byte, 0, bs.width)) + bs.priority = -(math.MaxInt32 - s.idCount) + } + + for i := 0; i < len(bs.buffers); i++ { + bs.buffers[i] = bytes.NewBuffer(make([]byte, 0, 512)) + } return bs } func syncWidth(matrix map[int][]chan int) { for _, column := range matrix { - column := column - go func() { - var maxWidth int - for _, ch := range column { - if w := <-ch; w > maxWidth { - maxWidth = w - } - } - for _, ch := range column { - ch <- maxWidth - } - }() - } -} - -func extractBaseFiller(f BarFiller) BarFiller { - type wrapper interface { - Base() BarFiller - } - if f, ok := f.(wrapper); ok { - return extractBaseFiller(f.Base()) - } - return f -} + go maxWidthDistributor(column) + } +} + +var maxWidthDistributor = func(column []chan int) { + var maxWidth int + for _, ch := range column { + if w := <-ch; w > maxWidth { + maxWidth = w + } + } + for _, ch := range column { + ch <- maxWidth + } +} diff --git a/progress_test.go b/progress_test.go index 3390d2c..64e0fa6 100644 --- a/progress_test.go +++ b/progress_test.go @@ -9,7 +9,8 @@ "testing" "time" - "github.com/vbauerster/mpb/v5" + "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v7/decor" ) func init() { @@ -23,12 +24,13 @@ wg.Add(1) b := p.AddBar(100) go func() { + rng := rand.New(rand.NewSource(time.Now().UnixNano())) for i := 0; i < 100; i++ { if i == 33 { wg.Done() } b.Increment() - time.Sleep(randomDuration(100 * time.Millisecond)) + time.Sleep((time.Duration(rng.Intn(10)+1) * (10 * time.Millisecond)) / 2) } }() @@ -50,7 +52,7 @@ bars := make([]*mpb.Bar, 3) for i := 0; i < 3; i++ { b := p.AddBar(100) - bars[i] = b + rng := rand.New(rand.NewSource(time.Now().UnixNano())) go func(n int) { for i := 0; !b.Completed(); i++ { if n == 0 && i >= 33 { @@ -58,15 +60,16 @@ wg.Done() } b.Increment() - time.Sleep(randomDuration(100 * time.Millisecond)) + time.Sleep((time.Duration(rng.Intn(10)+1) * (10 * time.Millisecond)) / 2) } }(i) + bars[i] = b } wg.Wait() count := p.BarCount() if count != 2 { - t.Errorf("BarCount want: %q, got: %q\n", 2, count) + t.Errorf("BarCount want: %d, got: %d\n", 2, count) } bars[1].Abort(true) bars[2].Abort(true) @@ -74,36 +77,107 @@ } func TestWithContext(t *testing.T) { + shutdown := make(chan struct{}) ctx, cancel := context.WithCancel(context.Background()) - shutdown := make(chan struct{}) - p := mpb.NewWithContext(ctx, - mpb.WithOutput(ioutil.Discard), - mpb.WithRefreshRate(50*time.Millisecond), - mpb.WithShutdownNotifier(shutdown), - ) + p := mpb.NewWithContext(ctx, mpb.WithShutdownNotifier(shutdown), mpb.WithOutput(ioutil.Discard)) - total := 10000 - numBars := 3 - bars := make([]*mpb.Bar, 0, numBars) + start := make(chan struct{}) + done := make(chan struct{}) + fail := make(chan struct{}) + bar := p.AddBar(0) // never complete bar + go func() { + close(start) + for !bar.Completed() { + bar.Increment() + time.Sleep(randomDuration(100 * time.Millisecond)) + } + close(done) + }() + + go func() { + select { + case <-done: + p.Wait() + case <-time.After(150 * time.Millisecond): + close(fail) + } + }() + + <-start + cancel() + select { + case <-shutdown: + case <-fail: + t.Error("Progress didn't shutdown") + } +} + +// MaxWidthDistributor shouldn't stuck in the middle while removing or aborting a bar +func TestMaxWidthDistributor(t *testing.T) { + + makeWrapper := func(f func([]chan int), start, end chan struct{}) func([]chan int) { + return func(column []chan int) { + start <- struct{}{} + f(column) + <-end + } + } + + ready := make(chan struct{}) + start := make(chan struct{}) + end := make(chan struct{}) + *mpb.MaxWidthDistributor = makeWrapper(*mpb.MaxWidthDistributor, start, end) + + total := 80 + numBars := 6 + p := mpb.New(mpb.WithOutput(ioutil.Discard)) for i := 0; i < numBars; i++ { - bar := p.AddBar(int64(total)) - bars = append(bars, bar) + bar := p.AddBar(int64(total), + mpb.BarOptional(mpb.BarRemoveOnComplete(), i == 0), + mpb.PrependDecorators( + decor.EwmaETA(decor.ET_STYLE_GO, 60, decor.WCSyncSpace), + ), + ) go func() { - for !bar.Completed() { - bar.Increment() - time.Sleep(randomDuration(100 * time.Millisecond)) + <-ready + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + for i := 0; i < total; i++ { + start := time.Now() + if id := bar.ID(); id > 1 && i >= 42 { + if id&1 == 1 { + bar.Abort(true) + } else { + bar.Abort(false) + } + } + time.Sleep((time.Duration(rng.Intn(10)+1) * (50 * time.Millisecond)) / 2) + bar.IncrInt64(rand.Int63n(5) + 1) + bar.DecoratorEwmaUpdate(time.Since(start)) } }() } - time.Sleep(50 * time.Millisecond) - cancel() + go func() { + <-ready + p.Wait() + close(start) + }() - p.Wait() - select { - case <-shutdown: - case <-time.After(100 * time.Millisecond): - t.Error("Progress didn't stop") + res := t.Run("maxWidthDistributor", func(t *testing.T) { + close(ready) + for v := range start { + timer := time.NewTimer(100 * time.Millisecond) + select { + case end <- v: + timer.Stop() + case <-timer.C: + t.FailNow() + } + } + }) + + if !res { + t.Error("maxWidthDistributor stuck in the middle") } } diff --git a/proxyreader.go b/proxyreader.go index 316f438..25f195b 100644 --- a/proxyreader.go +++ b/proxyreader.go @@ -11,73 +11,69 @@ bar *Bar } -func (x *proxyReader) Read(p []byte) (int, error) { +func (x proxyReader) Read(p []byte) (int, error) { n, err := x.ReadCloser.Read(p) x.bar.IncrBy(n) if err == io.EOF { - go x.bar.SetTotal(0, true) + go x.bar.SetTotal(-1, true) } return n, err } type proxyWriterTo struct { - io.ReadCloser // *proxyReader - wt io.WriterTo - bar *Bar + proxyReader + wt io.WriterTo } -func (x *proxyWriterTo) WriteTo(w io.Writer) (int64, error) { +func (x proxyWriterTo) WriteTo(w io.Writer) (int64, error) { n, err := x.wt.WriteTo(w) x.bar.IncrInt64(n) if err == io.EOF { - go x.bar.SetTotal(0, true) + go x.bar.SetTotal(-1, true) } return n, err } type ewmaProxyReader struct { - io.ReadCloser // *proxyReader - bar *Bar - iT time.Time + proxyReader } -func (x *ewmaProxyReader) Read(p []byte) (int, error) { - n, err := x.ReadCloser.Read(p) +func (x ewmaProxyReader) Read(p []byte) (int, error) { + start := time.Now() + n, err := x.proxyReader.Read(p) if n > 0 { - x.bar.DecoratorEwmaUpdate(time.Since(x.iT)) - x.iT = time.Now() + x.bar.DecoratorEwmaUpdate(time.Since(start)) } return n, err } type ewmaProxyWriterTo struct { - io.ReadCloser // *ewmaProxyReader - wt io.WriterTo // *proxyWriterTo - bar *Bar - iT time.Time + ewmaProxyReader + wt proxyWriterTo } -func (x *ewmaProxyWriterTo) WriteTo(w io.Writer) (int64, error) { +func (x ewmaProxyWriterTo) WriteTo(w io.Writer) (int64, error) { + start := time.Now() n, err := x.wt.WriteTo(w) if n > 0 { - x.bar.DecoratorEwmaUpdate(time.Since(x.iT)) - x.iT = time.Now() + x.bar.DecoratorEwmaUpdate(time.Since(start)) } return n, err } -func newProxyReader(r io.Reader, bar *Bar) io.ReadCloser { - rc := toReadCloser(r) - rc = &proxyReader{rc, bar} - - if wt, isWriterTo := r.(io.WriterTo); bar.hasEwmaDecorators { - now := time.Now() - rc = &ewmaProxyReader{rc, bar, now} - if isWriterTo { - rc = &ewmaProxyWriterTo{rc, wt, bar, now} +func (b *Bar) newProxyReader(r io.Reader) (rc io.ReadCloser) { + pr := proxyReader{toReadCloser(r), b} + if wt, ok := r.(io.WriterTo); ok { + pw := proxyWriterTo{pr, wt} + if b.hasEwmaDecorators { + rc = ewmaProxyWriterTo{ewmaProxyReader{pr}, pw} + } else { + rc = pw } - } else if isWriterTo { - rc = &proxyWriterTo{rc, wt, bar} + } else if b.hasEwmaDecorators { + rc = ewmaProxyReader{pr} + } else { + rc = pr } return rc } diff --git a/proxyreader_test.go b/proxyreader_test.go index 14bbca8..789695e 100644 --- a/proxyreader_test.go +++ b/proxyreader_test.go @@ -7,7 +7,7 @@ "strings" "testing" - "github.com/vbauerster/mpb/v5" + "github.com/vbauerster/mpb/v7" ) const content = `Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do @@ -33,7 +33,7 @@ tReader := &testReader{strings.NewReader(content), false} - bar := p.AddBar(int64(len(content)), mpb.TrimSpace()) + bar := p.AddBar(int64(len(content)), mpb.BarFillerTrim()) var buf bytes.Buffer _, err := io.Copy(&buf, bar.ProxyReader(tReader)) @@ -67,10 +67,9 @@ p := mpb.New(mpb.WithOutput(ioutil.Discard)) var reader io.Reader = strings.NewReader(content) - wt := reader.(io.WriterTo) - tReader := &testWriterTo{reader, wt, false} + tReader := &testWriterTo{reader, reader.(io.WriterTo), false} - bar := p.AddBar(int64(len(content)), mpb.TrimSpace()) + bar := p.AddBar(int64(len(content)), mpb.BarFillerTrim()) var buf bytes.Buffer _, err := io.Copy(&buf, bar.ProxyReader(tReader)) diff --git a/spinner_filler.go b/spinner_filler.go deleted file mode 100644 index 517725f..0000000 --- a/spinner_filler.go +++ /dev/null @@ -1,61 +0,0 @@ -package mpb - -import ( - "io" - "strings" - "unicode/utf8" - - "github.com/vbauerster/mpb/v5/decor" -) - -// SpinnerAlignment enum. -type SpinnerAlignment int - -// SpinnerAlignment kinds. -const ( - SpinnerOnLeft SpinnerAlignment = iota - SpinnerOnMiddle - SpinnerOnRight -) - -// DefaultSpinnerStyle is a slice of strings, which makes a spinner. -var DefaultSpinnerStyle = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} - -type spinnerFiller struct { - frames []string - count uint - alignment SpinnerAlignment -} - -// NewSpinnerFiller constucts mpb.BarFiller, to be used with *Progress.Add(...) *Bar method. -func NewSpinnerFiller(style []string, alignment SpinnerAlignment) BarFiller { - if len(style) == 0 { - style = DefaultSpinnerStyle - } - filler := &spinnerFiller{ - frames: style, - alignment: alignment, - } - return filler -} - -func (s *spinnerFiller) Fill(w io.Writer, width int, stat *decor.Statistics) { - - frame := s.frames[s.count%uint(len(s.frames))] - frameWidth := utf8.RuneCountInString(frame) - - if width < frameWidth { - return - } - - switch rest := width - frameWidth; s.alignment { - case SpinnerOnLeft: - io.WriteString(w, frame+strings.Repeat(" ", rest)) - case SpinnerOnMiddle: - str := strings.Repeat(" ", rest/2) + frame + strings.Repeat(" ", rest/2+rest%2) - io.WriteString(w, str) - case SpinnerOnRight: - io.WriteString(w, strings.Repeat(" ", rest)+frame) - } - s.count++ -}