diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..d8c89e0 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,39 @@ +name: golangci-lint + +on: + push: + tags: + - v* + branches: + - master + - main + pull_request: + +permissions: + contents: read + # Optional: allow read access to pull request. Use with `only-new-issues` option. + pull-requests: read + +jobs: + golangci: + strategy: + matrix: + go-version: ['stable'] + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + cache: false + - uses: golangci/golangci-lint-action@v6 + with: + # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. + version: latest + + # Optional: golangci-lint command line arguments. + # args: --issues-exit-code=0 + + # Optional: show only new issues if it's a pull request. The default value is `false`. + only-new-issues: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8dfb293 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +name: test + +on: + push: + tags: + - v* + branches: + - master + - main + pull_request: + +permissions: + contents: read + pull-requests: read + +jobs: + test: + strategy: + matrix: + go-version: ['stable', 'oldstable'] + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + - run: go test -race ./... diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9a203a6..0000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -language: go -arch: - - amd64 - - ppc64le - -go: - - 1.14.x - -script: - - go test -race ./... - - for i in _examples/*/; do go build $i/*.go || exit 1; done diff --git a/CONTRIBUTING b/CONTRIBUTING new file mode 100644 index 0000000..6ca5453 --- /dev/null +++ b/CONTRIBUTING @@ -0,0 +1,15 @@ +When contributing your first changes, please include an empty commit for +copyright waiver using the following message (replace 'John Doe' with +your name or nickname): + + John Doe Copyright Waiver + + I dedicate any and all copyright interest in this software to the + public domain. I make this dedication for the benefit of the public at + large and to the detriment of my heirs and successors. I intend this + dedication to be an overt act of relinquishment in perpetuity of all + present and future rights to this software under copyright law. + +The command to create an empty commit from the command-line is: + + git commit --allow-empty diff --git a/README.md b/README.md index a87786d..af97c92 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Multi Progress Bar -[![GoDoc](https://pkg.go.dev/badge/github.com/vbauerster/mpb)](https://pkg.go.dev/github.com/vbauerster/mpb/v6) -[![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/v8) +[![Test status](https://github.com/vbauerster/mpb/actions/workflows/test.yml/badge.svg)](https://github.com/vbauerster/mpb/actions/workflows/test.yml) +[![Lint status](https://github.com/vbauerster/mpb/actions/workflows/golangci-lint.yml/badge.svg)](https://github.com/vbauerster/mpb/actions/workflows/golangci-lint.yml) **mpb** is a Go lib for rendering progress bars in terminal applications. @@ -26,8 +26,8 @@ "math/rand" "time" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) func main() { @@ -36,17 +36,15 @@ total := 100 name := "Single Bar:" - // adding a single bar, which will inherit container's width - bar := p.Add(int64(total), - // progress bar filler with customized style - mpb.NewBarFiller("╢▌▌░╟"), + // 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}), + decor.Name(name, decor.WC{C: decor.DindentRight | decor.DextraSpace}), // replace ETA decorator with "done" message, OnComplete event - decor.OnComplete( - decor.AverageETA(decor.ET_STYLE_GO, decor.WC{W: 4}), "done", - ), + decor.OnComplete(decor.AverageETA(decor.ET_STYLE_GO), "done"), ), mpb.AppendDecorators(decor.Percentage()), ) @@ -65,7 +63,7 @@ ```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) @@ -82,8 +80,8 @@ 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", + // ETA decorator with ewma age of 30 + decor.EwmaETA(decor.ET_STYLE_GO, 30, decor.WCSyncWidth), "done", ), ), ) @@ -97,13 +95,12 @@ // 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)) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) } }() } - // 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 be164a9..f082eaf 100644 --- a/_examples/barExtender/go.mod +++ b/_examples/barExtender/go.mod @@ -1,5 +1,13 @@ module github.com/vbauerster/mpb/_examples/barExtender -go 1.14 +go 1.17 -require github.com/vbauerster/mpb/v6 v6.0.3 +require github.com/vbauerster/mpb/v8 v8.8.2 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/barExtender/main.go b/_examples/barExtender/main.go index 34e760f..965925b 100644 --- a/_examples/barExtender/main.go +++ b/_examples/barExtender/main.go @@ -7,24 +7,27 @@ "sync" "time" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) func main() { var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call p := mpb.New(mpb.WithWaitGroup(&wg)) total, numBars := 100, 3 wg.Add(numBars) for i := 0; i < numBars; i++ { name := fmt.Sprintf("Bar#%d:", i) - efn := func(w io.Writer, _ int, s decor.Statistics) { + efn := func(w io.Writer, s decor.Statistics) (err error) { if s.Completed { - fmt.Fprintf(w, "Bar id: %d has been completed\n", s.ID) + _, err = fmt.Fprintf(w, "Bar id: %d has been completed\n", s.ID) } + return err } - bar := p.AddBar(int64(total), mpb.BarExtender(mpb.BarFillerFunc(efn)), + bar := p.AddBar(int64(total), + mpb.BarExtender(mpb.BarFillerFunc(efn), false), mpb.PrependDecorators( // simple name decorator decor.Name(name), @@ -34,8 +37,8 @@ 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", + // ETA decorator with ewma age of 30 + decor.EwmaETA(decor.ET_STYLE_GO, 30), "done", ), ), ) @@ -49,12 +52,11 @@ // 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() - // since EWMA based decorator is used, DecoratorEwmaUpdate should be called - bar.DecoratorEwmaUpdate(time.Since(start)) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) } }() } - // 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/barExtenderRev/go.mod b/_examples/barExtenderRev/go.mod new file mode 100644 index 0000000..00ed99f --- /dev/null +++ b/_examples/barExtenderRev/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/barExtenderRev + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.8.2 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/barExtenderRev/main.go b/_examples/barExtenderRev/main.go new file mode 100644 index 0000000..aa9d534 --- /dev/null +++ b/_examples/barExtenderRev/main.go @@ -0,0 +1,137 @@ +package main + +import ( + "fmt" + "io" + "math/rand" + "sync/atomic" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +var curTask uint32 +var doneTasks uint32 + +type task struct { + id uint32 + total int64 + bar *mpb.Bar +} + +func main() { + numTasks := 4 + + var total int64 + var filler mpb.BarFiller + tasks := make([]*task, numTasks) + + for i := 0; i < numTasks; i++ { + task := &task{ + id: uint32(i), + total: rand.Int63n(666) + 100, + } + total += task.total + filler = middleware(filler, task.id) + tasks[i] = task + } + + filler = newLineMiddleware(filler) + + p := mpb.New() + + for i := 0; i < numTasks; i++ { + bar := p.AddBar(tasks[i].total, + mpb.BarExtender(filler, true), // all bars share same extender filler + mpb.BarFuncOptional(func() mpb.BarOption { + return mpb.BarQueueAfter(tasks[i-1].bar) + }, i != 0), + mpb.PrependDecorators( + decor.Name("current:", decor.WCSyncWidthR), + ), + mpb.AppendDecorators( + decor.Percentage(decor.WCSyncWidth), + ), + ) + tasks[i].bar = bar + } + + tb := p.AddBar(0, + mpb.PrependDecorators( + decor.Any(func(st decor.Statistics) string { + return fmt.Sprintf("TOTAL(%d/%d)", atomic.LoadUint32(&doneTasks), len(tasks)) + }, decor.WCSyncWidthR), + ), + mpb.AppendDecorators( + decor.Percentage(decor.WCSyncWidth), + ), + ) + + tb.SetTotal(total, false) + + for _, t := range tasks { + atomic.StoreUint32(&curTask, t.id) + complete(t.bar, tb) + atomic.AddUint32(&doneTasks, 1) + } + + tb.EnableTriggerComplete() + + p.Wait() +} + +func middleware(base mpb.BarFiller, id uint32) mpb.BarFiller { + var done bool + fn := func(w io.Writer, st decor.Statistics) error { + if !done { + if atomic.LoadUint32(&curTask) != id { + _, err := fmt.Fprintf(w, " Taksk %02d\n", id) + return err + } + if !st.Completed { + _, err := fmt.Fprintf(w, "=> Taksk %02d\n", id) + return err + } + done = true + } + _, err := fmt.Fprintf(w, " Taksk %02d: Done!\n", id) + return err + } + if base == nil { + return mpb.BarFillerFunc(fn) + } + return mpb.BarFillerFunc(func(w io.Writer, st decor.Statistics) error { + err := fn(w, st) + if err != nil { + return err + } + return base.Fill(w, st) + }) +} + +func newLineMiddleware(base mpb.BarFiller) mpb.BarFiller { + return mpb.BarFillerFunc(func(w io.Writer, st decor.Statistics) error { + _, err := fmt.Fprintln(w) + if err != nil { + return err + } + return base.Fill(w, st) + }) +} + +func complete(bar, totalBar *mpb.Bar) { + max := 100 * time.Millisecond + for !bar.Completed() { + n := rand.Int63n(10) + 1 + incrementBars(n, bar, totalBar) + time.Sleep(time.Duration(n) * max / 10) + } + bar.Wait() +} + +func incrementBars(n int64, bb ...*mpb.Bar) { + for _, b := range bb { + b.IncrInt64(n) + } +} diff --git a/_examples/cancel/go.mod b/_examples/cancel/go.mod index e6192d2..32ad805 100644 --- a/_examples/cancel/go.mod +++ b/_examples/cancel/go.mod @@ -1,5 +1,13 @@ module github.com/vbauerster/mpb/_examples/cancel -go 1.14 +go 1.17 -require github.com/vbauerster/mpb/v6 v6.0.3 +require github.com/vbauerster/mpb/v8 v8.8.2 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/cancel/main.go b/_examples/cancel/main.go index 6f4a3de..a60bfe5 100644 --- a/_examples/cancel/main.go +++ b/_examples/cancel/main.go @@ -7,8 +7,8 @@ "sync" "time" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) func main() { @@ -16,17 +16,18 @@ defer cancel() var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call p := mpb.NewWithContext(ctx, mpb.WithWaitGroup(&wg)) total := 300 numBars := 3 wg.Add(numBars) for i := 0; i < numBars; i++ { - name := fmt.Sprintf("Bar#%d:", i) + name := fmt.Sprintf("Bar#%02d: ", i) bar := p.AddBar(int64(total), mpb.PrependDecorators( - decor.Name(name), - decor.EwmaETA(decor.ET_STYLE_GO, 60, decor.WCSyncSpace), + decor.Name(name, decor.WCSyncWidthR), + decor.EwmaETA(decor.ET_STYLE_GO, 30, decor.WCSyncWidth), ), mpb.AppendDecorators( // note that OnComplete will not be fired, because of cancel @@ -38,17 +39,16 @@ defer wg.Done() rng := rand.New(rand.NewSource(time.Now().UnixNano())) max := 100 * time.Millisecond - for !bar.Completed() { + for bar.IsRunning() { // start variable is solely for EWMA calculation // EWMA's unit of measure is an iteration's duration start := time.Now() time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) - bar.Increment() - // since EWMA based decorator is used, DecoratorEwmaUpdate should be called - bar.DecoratorEwmaUpdate(time.Since(start)) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) } }() } - + // wait for passed wg and for all bars to complete and flush p.Wait() } diff --git a/_examples/complex/go.mod b/_examples/complex/go.mod index b6b796e..67934de 100644 --- a/_examples/complex/go.mod +++ b/_examples/complex/go.mod @@ -1,5 +1,18 @@ module github.com/vbauerster/mpb/_examples/complex -go 1.14 +go 1.17 -require github.com/vbauerster/mpb/v6 v6.0.3 +require ( + github.com/fatih/color v1.17.0 + github.com/vbauerster/mpb/v8 v8.8.2 +) + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/complex/main.go b/_examples/complex/main.go index 85aa98f..7647dbd 100644 --- a/_examples/complex/main.go +++ b/_examples/complex/main.go @@ -3,81 +3,79 @@ import ( "fmt" "math/rand" - "sync" "time" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/fatih/color" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) -func init() { - rand.Seed(time.Now().UnixNano()) -} +func main() { + numBars := 4 + // to support color in Windows following both options are required + p := mpb.New( + mpb.WithOutput(color.Output), + mpb.WithAutoRefresh(), + ) -func main() { - doneWg := new(sync.WaitGroup) - p := mpb.New(mpb.WithWaitGroup(doneWg)) - numBars := 4 + red, green := color.New(color.FgRed), color.New(color.FgGreen) - var bars []*mpb.Bar - var downloadWgg []*sync.WaitGroup for i := 0; i < numBars; i++ { - wg := new(sync.WaitGroup) - wg.Add(1) - downloadWgg = append(downloadWgg, wg) task := fmt.Sprintf("Task#%02d:", i) - job := "downloading" - b := p.AddBar(rand.Int63n(201)+100, + queue := make([]*mpb.Bar, 2) + queue[0] = p.AddBar(rand.Int63n(201)+100, mpb.PrependDecorators( - decor.Name(task, decor.WC{W: len(task) + 1, C: decor.DidentRight}), - decor.Name(job, decor.WCSyncSpaceR), + decor.Name(task, decor.WC{C: decor.DindentRight | decor.DextraSpace}), + decor.Name("downloading", decor.WCSyncSpaceR), decor.CountersNoUnit("%d / %d", decor.WCSyncWidth), ), - mpb.AppendDecorators(decor.Percentage(decor.WC{W: 5})), + mpb.AppendDecorators( + decor.OnComplete(decor.Percentage(decor.WC{W: 5}), "done"), + ), ) - go newTask(wg, b, i+1) - bars = append(bars, b) - } + queue[1] = p.AddBar(rand.Int63n(101)+100, + mpb.BarQueueAfter(queue[0]), // this bar is queued + mpb.BarFillerClearOnComplete(), + mpb.PrependDecorators( + decor.Name(task, decor.WC{C: decor.DindentRight | decor.DextraSpace}), + decor.OnCompleteMeta( + decor.OnComplete( + decor.Meta(decor.Name("installing", decor.WCSyncSpaceR), toMetaFunc(red)), + "done!", + ), + toMetaFunc(green), + ), + decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_MMSS, 0, decor.WCSyncWidth), ""), + ), + mpb.AppendDecorators( + decor.OnComplete(decor.Percentage(decor.WC{W: 5}), ""), + ), + ) - for i := 0; i < numBars; i++ { - doneWg.Add(1) - i := i go func() { - task := fmt.Sprintf("Task#%02d:", i) - // 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]), - mpb.BarFillerClearOnComplete(), - mpb.PrependDecorators( - decor.Name(task, decor.WC{W: len(task) + 1, C: decor.DidentRight}), - decor.OnComplete(decor.Name(job, decor.WCSyncSpaceR), "done!"), - decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_MMSS, 0, decor.WCSyncWidth), ""), - ), - mpb.AppendDecorators( - decor.OnComplete(decor.Percentage(decor.WC{W: 5}), ""), - ), - ) - // waiting for download to complete, before starting install job - downloadWgg[i].Wait() - go newTask(doneWg, b, numBars-i) + for _, b := range queue { + complete(b) + } }() } p.Wait() } -func newTask(wg *sync.WaitGroup, bar *mpb.Bar, incrBy int) { - defer wg.Done() +func complete(bar *mpb.Bar) { max := 100 * time.Millisecond for !bar.Completed() { // start variable is solely for EWMA calculation // EWMA's unit of measure is an iteration's duration start := time.Now() time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) - bar.IncrBy(incrBy) - // we need to call DecoratorEwmaUpdate to fulfill ewma decorator's contract - bar.DecoratorEwmaUpdate(time.Since(start)) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrInt64(rand.Int63n(5)+1, time.Since(start)) } } + +func toMetaFunc(c *color.Color) func(string) string { + return func(s string) string { + return c.Sprint(s) + } +} diff --git a/_examples/decoratorsOnTop/go.mod b/_examples/decoratorsOnTop/go.mod index 299a87f..ec44635 100644 --- a/_examples/decoratorsOnTop/go.mod +++ b/_examples/decoratorsOnTop/go.mod @@ -1,5 +1,13 @@ module github.com/vbauerster/mpb/_examples/decoratorsOnTop -go 1.14 +go 1.17 -require github.com/vbauerster/mpb/v6 v6.0.3 +require github.com/vbauerster/mpb/v8 v8.8.2 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/decoratorsOnTop/main.go b/_examples/decoratorsOnTop/main.go index bdd0bd5..f35ad82 100644 --- a/_examples/decoratorsOnTop/main.go +++ b/_examples/decoratorsOnTop/main.go @@ -5,15 +5,17 @@ "math/rand" "time" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) func main() { p := mpb.New() total := 100 - bar := p.Add(int64(total), nil, + bar := p.New(int64(total), + mpb.NopStyle(), // make main bar style nop, so there are just decorators + mpb.BarExtender(extended(mpb.BarStyle().Build()), false), // extend wtih normal bar on the next line mpb.PrependDecorators( decor.Name("Percentage: "), decor.NewPercentage("%d"), @@ -24,7 +26,6 @@ decor.AverageETA(decor.ET_STYLE_GO), "done", ), ), - mpb.BarExtender(nlBarFiller(mpb.NewBarFiller("╢▌▌░╟"))), ) // simulating some work max := 100 * time.Millisecond @@ -36,9 +37,13 @@ p.Wait() } -func nlBarFiller(filler mpb.BarFiller) mpb.BarFiller { - return mpb.BarFillerFunc(func(w io.Writer, reqWidth int, st decor.Statistics) { - filler.Fill(w, reqWidth, st) - w.Write([]byte("\n")) +func extended(base mpb.BarFiller) mpb.BarFiller { + return mpb.BarFillerFunc(func(w io.Writer, st decor.Statistics) error { + err := base.Fill(w, st) + if err != nil { + return err + } + _, err = io.WriteString(w, "\n") + return err }) } diff --git a/_examples/differentWidth/go.mod b/_examples/differentWidth/go.mod index 57700e2..c038d63 100644 --- a/_examples/differentWidth/go.mod +++ b/_examples/differentWidth/go.mod @@ -1,5 +1,13 @@ module github.com/vbauerster/mpb/_examples/differentWidth -go 1.14 +go 1.17 -require github.com/vbauerster/mpb/v6 v6.0.3 +require github.com/vbauerster/mpb/v8 v8.8.2 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/differentWidth/main.go b/_examples/differentWidth/main.go index 7ed5b42..683876d 100644 --- a/_examples/differentWidth/main.go +++ b/_examples/differentWidth/main.go @@ -6,15 +6,15 @@ "sync" "time" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) func main() { var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call p := mpb.New( mpb.WithWaitGroup(&wg), - // container's width. mpb.WithWidth(60), ) total, numBars := 100, 3 @@ -34,8 +34,8 @@ 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", + // ETA decorator with ewma age of 30 + decor.EwmaETA(decor.ET_STYLE_GO, 30), "done", ), ), ) @@ -49,12 +49,11 @@ // 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)) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) } }() } - // 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 2ae2bad..eba1c53 100644 --- a/_examples/dynTotal/go.mod +++ b/_examples/dynTotal/go.mod @@ -1,5 +1,13 @@ module github.com/vbauerster/mpb/_examples/dynTotal -go 1.14 +go 1.17 -require github.com/vbauerster/mpb/v6 v6.0.3 +require github.com/vbauerster/mpb/v8 v8.8.2 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/dynTotal/main.go b/_examples/dynTotal/main.go index 99342ba..212ef13 100644 --- a/_examples/dynTotal/main.go +++ b/_examples/dynTotal/main.go @@ -5,21 +5,16 @@ "math/rand" "time" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) - -func init() { - rand.Seed(time.Now().UnixNano()) -} func main() { 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")), + bar := p.AddBar(0, + mpb.PrependDecorators(decor.Counters(decor.SizeB1024(0), "% .1f / % .1f")), mpb.AppendDecorators(decor.Percentage()), ) @@ -27,20 +22,17 @@ read := makeStream(200) for { n, err := read() - total += int64(n) if err == io.EOF { + // triggering complete event now + bar.SetTotal(-1, true) break } - // while total is unknown, - // set it to a positive number which is greater than current total, - // to make sure no complete event is triggered by next IncrBy call. - bar.SetTotal(total+2048, false) + // increment methods won't trigger complete event because bar was constructed with total = 0 bar.IncrBy(n) + // following call is not required, it's called to show some progress instead of an empty bar + bar.SetTotal(bar.Current()+2048, false) time.Sleep(time.Duration(rand.Intn(10)+1) * maxSleep / 10) } - - // force bar complete event, note true flag - bar.SetTotal(total, true) p.Wait() } diff --git a/_examples/gomodtidyall b/_examples/gomodtidyall new file mode 100755 index 0000000..5b633bc --- /dev/null +++ b/_examples/gomodtidyall @@ -0,0 +1,9 @@ +#!/bin/sh +set -e + +for d in *; do + [ ! -d "$d" ] && continue + pushd "$d" >/dev/null 2>&1 + go mod tidy + popd >/dev/null 2>&1 +done diff --git a/_examples/io/go.mod b/_examples/io/go.mod index 2eba870..23b5e0c 100644 --- a/_examples/io/go.mod +++ b/_examples/io/go.mod @@ -1,5 +1,13 @@ module github.com/vbauerster/mpb/_examples/io -go 1.14 +go 1.17 -require github.com/vbauerster/mpb/v6 v6.0.3 +require github.com/vbauerster/mpb/v8 v8.8.2 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/io/main.go b/_examples/io/main.go index bd07df9..8d765a7 100644 --- a/_examples/io/main.go +++ b/_examples/io/main.go @@ -3,40 +3,48 @@ import ( "crypto/rand" "io" - "io/ioutil" "time" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) func main() { - var total int64 = 1024 * 1024 * 500 - reader := io.LimitReader(rand.Reader, total) + var total int64 = 64 * 1024 * 1024 + + r, w := io.Pipe() + + go func() { + for i := 0; i < 1024; i++ { + _, _ = io.Copy(w, io.LimitReader(rand.Reader, 64*1024)) + time.Sleep(time.Second / 10) + } + w.Close() + }() p := mpb.New( mpb.WithWidth(60), mpb.WithRefreshRate(180*time.Millisecond), ) - bar := p.Add(total, - mpb.NewBarFiller("[=>-|"), + bar := p.New(total, + mpb.BarStyle().Rbound("|"), mpb.PrependDecorators( - decor.CountersKibiByte("% .2f / % .2f"), + decor.Counters(decor.SizeB1024(0), "% .2f / % .2f"), ), mpb.AppendDecorators( - decor.EwmaETA(decor.ET_STYLE_GO, 90), + decor.EwmaETA(decor.ET_STYLE_GO, 30), decor.Name(" ] "), - decor.EwmaSpeed(decor.UnitKiB, "% .2f", 60), + decor.EwmaSpeed(decor.SizeB1024(0), "% .2f", 30), ), ) // create proxy reader - proxyReader := bar.ProxyReader(reader) + proxyReader := bar.ProxyReader(r) defer proxyReader.Close() // copy from proxyReader, ignoring errors - io.Copy(ioutil.Discard, proxyReader) + _, _ = io.Copy(io.Discard, proxyReader) p.Wait() } diff --git a/_examples/merge/go.mod b/_examples/merge/go.mod deleted file mode 100644 index 748b441..0000000 --- a/_examples/merge/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/vbauerster/mpb/_examples/merge - -go 1.14 - -require github.com/vbauerster/mpb/v6 v6.0.3 diff --git a/_examples/merge/main.go b/_examples/merge/main.go deleted file mode 100644 index 5b54e14..0000000 --- a/_examples/merge/main.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "math/rand" - "strings" - "sync" - "time" - - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" -) - -func main() { - var wg sync.WaitGroup - // pass &wg (optional), so p will wait for it eventually - p := mpb.New(mpb.WithWaitGroup(&wg), mpb.WithWidth(60)) - total, numBars := 100, 3 - wg.Add(numBars) - - for i := 0; i < numBars; i++ { - var pdecorators mpb.BarOption - if i == 0 { - pdecorators = mpb.PrependDecorators( - decor.Merge( - decor.OnComplete( - newVariadicSpinner(decor.WCSyncSpace), - "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"), - ) - } - bar := p.AddBar(int64(total), - pdecorators, - mpb.AppendDecorators( - decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_GO, 60), "done"), - ), - ) - // simulating some work - go func() { - defer wg.Done() - rng := rand.New(rand.NewSource(time.Now().UnixNano())) - max := 100 * time.Millisecond - for i := 0; i < total; i++ { - // start variable is solely for EWMA calculation - // EWMA's unit of measure is an iteration's duration - start := time.Now() - time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) - bar.Increment() - // we need to call DecoratorEwmaUpdate to fulfill ewma decorator's contract - bar.DecoratorEwmaUpdate(time.Since(start)) - } - }() - } - // Waiting for passed &wg and for all bars to complete and flush - p.Wait() -} - -func newVariadicSpinner(wc decor.WC) decor.Decorator { - spinner := decor.Spinner(nil) - fn := func(s decor.Statistics) string { - return strings.Repeat(spinner.Decor(s), int(s.Current/3)) - } - return decor.Any(fn, wc) -} diff --git a/_examples/mexicanBar/go.mod b/_examples/mexicanBar/go.mod new file mode 100644 index 0000000..bbff3d0 --- /dev/null +++ b/_examples/mexicanBar/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/mexicanBar + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.8.2 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/mexicanBar/main.go b/_examples/mexicanBar/main.go new file mode 100644 index 0000000..8c2ecae --- /dev/null +++ b/_examples/mexicanBar/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "math/rand" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + // initialize progress container, with custom width + p := mpb.New(mpb.WithWidth(80)) + + total := 100 + name := "Complex Filler:" + bs := mpb.BarStyle() + bs = bs.LboundMeta(func(s string) string { + return "\033[34m" + s + "\033[0m" // blue + }) + bs = bs.Filler("_").FillerMeta(func(s string) string { + return "\033[36m" + s + "\033[0m" // cyan + }) + bs = bs.Tip("⛵").TipMeta(func(s string) string { + return "\033[31m" + s + "\033[0m" // red + }) + bs = bs.TipOnComplete() // leave tip on complete + bs = bs.Padding("_").PaddingMeta(func(s string) string { + return "\033[36m" + s + "\033[0m" // cyan + }) + bs = bs.RboundMeta(func(s string) string { + return "\033[34m" + s + "\033[0m" // blue + }) + bar := p.New(int64(total), bs, + mpb.PrependDecorators(decor.Name(name)), + mpb.AppendDecorators(decor.Percentage()), + ) + // simulating some work + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) + bar.Increment() + } + // wait for our bar to complete + p.Wait() +} diff --git a/_examples/multiBars/go.mod b/_examples/multiBars/go.mod index 2a1ac25..133d133 100644 --- a/_examples/multiBars/go.mod +++ b/_examples/multiBars/go.mod @@ -1,5 +1,13 @@ module github.com/vbauerster/mpb/_examples/multiBars -go 1.14 +go 1.17 -require github.com/vbauerster/mpb/v6 v6.0.3 +require github.com/vbauerster/mpb/v8 v8.8.2 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/multiBars/main.go b/_examples/multiBars/main.go index 65cfeef..3d42d70 100644 --- a/_examples/multiBars/main.go +++ b/_examples/multiBars/main.go @@ -6,13 +6,13 @@ "sync" "time" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/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) @@ -29,8 +29,8 @@ 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", + // ETA decorator with ewma age of 30 + decor.EwmaETA(decor.ET_STYLE_GO, 30, decor.WCSyncWidth), "done", ), ), ) @@ -44,12 +44,11 @@ // 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)) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) } }() } - // 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 deleted file mode 100644 index 15a905b..0000000 --- a/_examples/panic/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/vbauerster/mpb/_examples/panic - -go 1.14 - -require github.com/vbauerster/mpb/v6 v6.0.3 diff --git a/_examples/panic/main.go b/_examples/panic/main.go deleted file mode 100644 index a569c49..0000000 --- a/_examples/panic/main.go +++ /dev/null @@ -1,45 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strings" - "sync" - "time" - - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" -) - -func main() { - var wg sync.WaitGroup - p := mpb.New(mpb.WithWaitGroup(&wg), mpb.WithDebugOutput(os.Stderr)) - - wantPanic := strings.Repeat("Panic ", 64) - numBars := 3 - wg.Add(numBars) - - for i := 0; i < numBars; i++ { - name := fmt.Sprintf("b#%02d:", i) - bar := p.AddBar(100, mpb.BarID(i), mpb.PrependDecorators(panicDecorator(name, wantPanic))) - - go func() { - defer wg.Done() - for i := 0; i < 100; i++ { - time.Sleep(50 * time.Millisecond) - bar.Increment() - } - }() - } - - p.Wait() -} - -func panicDecorator(name, panicMsg string) decor.Decorator { - 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 d1de4d5..9ff0f24 100644 --- a/_examples/poplog/go.mod +++ b/_examples/poplog/go.mod @@ -1,5 +1,13 @@ module github.com/vbauerster/mpb/_examples/poplog -go 1.14 +go 1.17 -require github.com/vbauerster/mpb/v6 v6.0.3 +require github.com/vbauerster/mpb/v8 v8.8.2 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/poplog/main.go b/_examples/poplog/main.go index d787444..f4efd6a 100644 --- a/_examples/poplog/main.go +++ b/_examples/poplog/main.go @@ -5,13 +5,13 @@ "math/rand" "time" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) func main() { + rng := rand.New(rand.NewSource(time.Now().UnixNano())) p := mpb.New(mpb.PopCompletedMode()) - total, numBars := 100, 4 for i := 0; i < numBars; i++ { name := fmt.Sprintf("Bar#%d:", i) @@ -24,20 +24,18 @@ ), mpb.AppendDecorators( decor.OnComplete(decor.Name(" "), ""), - decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_GO, 60), ""), + decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_GO, 30), ""), ), ) // simulating some work - 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)) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) } } diff --git a/_examples/progressAsWriter/go.mod b/_examples/progressAsWriter/go.mod new file mode 100644 index 0000000..371d88b --- /dev/null +++ b/_examples/progressAsWriter/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/progressAsWriter + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.8.2 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/progressAsWriter/main.go b/_examples/progressAsWriter/main.go new file mode 100644 index 0000000..057ad03 --- /dev/null +++ b/_examples/progressAsWriter/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "fmt" + "log" + "math/rand" + "sync" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + total, numBars := 100, 2 + var bwg, qwg sync.WaitGroup + bwg.Add(numBars) + qwg.Add(1) + done := make(chan interface{}) + p := mpb.New(mpb.WithWidth(64), mpb.WithShutdownNotifier(done), mpb.WithWaitGroup(&qwg)) + + log.SetOutput(p) + + go func() { + defer qwg.Done() + for { + select { + case <-done: + // after done, underlying io.Writer returns mpb.DoneError + // so following isn't printed + log.Println("all done") + return + default: + log.Println("waiting for done") + time.Sleep(150 * time.Millisecond) + } + } + }() + + nopBar := p.MustAdd(0, nil) + + for i := 0; i < numBars; i++ { + name := fmt.Sprintf("Bar#%d:", i) + bar := p.AddBar(int64(total), + mpb.PrependDecorators( + decor.Name(name), + decor.Percentage(decor.WCSyncSpace), + ), + mpb.AppendDecorators( + decor.OnComplete( + decor.EwmaETA(decor.ET_STYLE_GO, 30, decor.WCSyncWidth), "done", + ), + ), + ) + // simulating some work + go func() { + defer bwg.Done() + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + // start variable is solely for EWMA calculation + // EWMA's unit of measure is an iteration's duration + start := time.Now() + time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) + } + log.Println(name, "done") + }() + } + + bwg.Wait() + log.Println("completing nop bar") + nopBar.EnableTriggerComplete() + + p.Wait() +} diff --git a/_examples/quietMode/go.mod b/_examples/quietMode/go.mod index 03d30bb..857e680 100644 --- a/_examples/quietMode/go.mod +++ b/_examples/quietMode/go.mod @@ -1,5 +1,13 @@ module github.com/vbauerster/mpb/_examples/quietMode -go 1.14 +go 1.17 -require github.com/vbauerster/mpb/v6 v6.0.3 +require github.com/vbauerster/mpb/v8 v8.8.2 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/quietMode/main.go b/_examples/quietMode/main.go index 3d1b3cb..315fc3c 100644 --- a/_examples/quietMode/main.go +++ b/_examples/quietMode/main.go @@ -3,12 +3,13 @@ import ( "flag" "fmt" + "io" "math/rand" "sync" "time" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) var quietMode bool @@ -20,17 +21,10 @@ 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.ContainerOptional( - // setting to nil will: - // 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), - quietMode, - ), + mpb.ContainerOptional(mpb.WithOutput(io.Discard), quietMode), ) total, numBars := 100, 3 wg.Add(numBars) @@ -47,8 +41,8 @@ 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", + // ETA decorator with ewma age of 30 + decor.EwmaETA(decor.ET_STYLE_GO, 30), "done", ), ), ) @@ -62,13 +56,12 @@ // 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)) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) } }() } - // 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 48bee75..dfc61da 100644 --- a/_examples/remove/go.mod +++ b/_examples/remove/go.mod @@ -1,5 +1,13 @@ module github.com/vbauerster/mpb/_examples/remove -go 1.14 +go 1.17 -require github.com/vbauerster/mpb/v6 v6.0.3 +require github.com/vbauerster/mpb/v8 v8.8.2 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/remove/main.go b/_examples/remove/main.go index ab8d90d..e9894ed 100644 --- a/_examples/remove/main.go +++ b/_examples/remove/main.go @@ -6,12 +6,13 @@ "sync" "time" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) func main() { var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call p := mpb.New(mpb.WithWaitGroup(&wg)) total := 100 numBars := 3 @@ -24,29 +25,31 @@ 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() + for i := 0; bar.IsRunning(); i++ { if bar.ID() == 2 && i >= 42 { - // aborting and removing while bar is running - bar.Abort(true) + go 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 8e07e8c..2716b73 100644 --- a/_examples/reverseBar/go.mod +++ b/_examples/reverseBar/go.mod @@ -1,5 +1,13 @@ module github.com/vbauerster/mpb/_examples/reverseBar -go 1.14 +go 1.17 -require github.com/vbauerster/mpb/v6 v6.0.3 +require github.com/vbauerster/mpb/v8 v8.8.2 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/reverseBar/main.go b/_examples/reverseBar/main.go index 23450cc..9ff3c74 100644 --- a/_examples/reverseBar/main.go +++ b/_examples/reverseBar/main.go @@ -6,22 +6,28 @@ "sync" "time" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/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) + condFillerBuilder := func(cond bool) mpb.BarFillerBuilder { + if cond { // reverse Bar on cond + return mpb.BarStyle().Tip("<").Reverse() + } + return mpb.BarStyle() + } + for i := 0; i < numBars; i++ { name := fmt.Sprintf("Bar#%d:", i) - bar := p.Add(int64(total), - // reverse Bar#1 - mpb.NewBarFillerPick("", i == 1), + bar := p.New(int64(total), + condFillerBuilder(i == 1), mpb.PrependDecorators( // simple name decorator decor.Name(name), @@ -31,8 +37,8 @@ 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", + // ETA decorator with ewma age of 30 + decor.EwmaETA(decor.ET_STYLE_GO, 30), "done", ), ), ) @@ -46,12 +52,11 @@ // 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)) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) } }() } - // 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/singleBar/go.mod b/_examples/singleBar/go.mod index 6cd0933..1d761e3 100644 --- a/_examples/singleBar/go.mod +++ b/_examples/singleBar/go.mod @@ -1,5 +1,13 @@ module github.com/vbauerster/mpb/_examples/singleBar -go 1.14 +go 1.17 -require github.com/vbauerster/mpb/v6 v6.0.3 +require github.com/vbauerster/mpb/v8 v8.8.2 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/singleBar/main.go b/_examples/singleBar/main.go index f0ba8ea..12d65ca 100644 --- a/_examples/singleBar/main.go +++ b/_examples/singleBar/main.go @@ -4,8 +4,8 @@ "math/rand" "time" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) func main() { @@ -14,17 +14,15 @@ total := 100 name := "Single Bar:" - // adding a single bar, which will inherit container's width - bar := p.Add(int64(total), - // progress bar filler with customized style - mpb.NewBarFiller("╢▌▌░╟"), + // 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}), + decor.Name(name, decor.WC{C: decor.DindentRight | decor.DextraSpace}), // replace ETA decorator with "done" message, OnComplete event - decor.OnComplete( - decor.AverageETA(decor.ET_STYLE_GO, decor.WC{W: 4}), "done", - ), + decor.OnComplete(decor.AverageETA(decor.ET_STYLE_GO), "done"), ), mpb.AppendDecorators(decor.Percentage()), ) diff --git a/_examples/spinTipBar/go.mod b/_examples/spinTipBar/go.mod new file mode 100644 index 0000000..5c6327e --- /dev/null +++ b/_examples/spinTipBar/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/spinTipBar + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.8.2 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/spinTipBar/main.go b/_examples/spinTipBar/main.go new file mode 100644 index 0000000..62eab3f --- /dev/null +++ b/_examples/spinTipBar/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "math/rand" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + // initialize progress container, with custom width + p := mpb.New(mpb.WithWidth(80)) + + total := 100 + name := "Single Bar:" + bar := p.New(int64(total), + mpb.BarStyle().Tip(`-`, `\`, `|`, `/`).TipMeta(func(s string) string { + return "\033[31m" + s + "\033[0m" // red + }), + mpb.PrependDecorators(decor.Name(name)), + mpb.AppendDecorators(decor.Percentage()), + ) + // simulating some work + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) + bar.Increment() + } + // wait for our bar to complete and flush + p.Wait() +} diff --git a/_examples/spinnerBar/go.mod b/_examples/spinnerBar/go.mod index 4f28efb..8dc9d53 100644 --- a/_examples/spinnerBar/go.mod +++ b/_examples/spinnerBar/go.mod @@ -1,5 +1,13 @@ module github.com/vbauerster/mpb/_examples/spinnerBar -go 1.14 +go 1.17 -require github.com/vbauerster/mpb/v6 v6.0.3 +require github.com/vbauerster/mpb/v8 v8.8.2 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/spinnerBar/main.go b/_examples/spinnerBar/main.go index 7867037..c564452 100644 --- a/_examples/spinnerBar/main.go +++ b/_examples/spinnerBar/main.go @@ -6,56 +6,46 @@ "sync" "time" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) func main() { var wg sync.WaitGroup + // passed wg will be accounted at p.Wait() call p := mpb.New( mpb.WithWaitGroup(&wg), - mpb.WithWidth(14), + mpb.WithWidth(16), ) total, numBars := 101, 3 wg.Add(numBars) - spinnerStyle := []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"} + condFillerBuilder := func(cond bool) mpb.BarFillerBuilder { + if cond { + s := mpb.SpinnerStyle("∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙") + return s.Meta(func(s string) string { + return "\033[31m" + s + "\033[0m" // red + }) + } + return mpb.BarStyle().Lbound("╢").Filler("▌").Tip("▌").Padding("░").Rbound("╟") + } for i := 0; i < numBars; i++ { name := fmt.Sprintf("Bar#%d:", i) - var bar *mpb.Bar - if i == 0 { - bar = p.Add(int64(total), - mpb.NewBarFiller("╢▌▌░╟"), - mpb.PrependDecorators( - // simple name decorator - decor.Name(name), + bar := p.New(int64(total), + condFillerBuilder(i != 0), + mpb.PrependDecorators( + // simple name decorator + decor.Name(name), + ), + mpb.AppendDecorators( + // replace ETA decorator with "done" message, OnComplete event + decor.OnComplete( + // ETA decorator with ewma age of 30 + decor.EwmaETA(decor.ET_STYLE_GO, 30), "done", ), - 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.Add(int64(total), - mpb.NewSpinnerFiller(spinnerStyle, mpb.SpinnerOnMiddle), - 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() @@ -66,12 +56,11 @@ // 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)) + // we need to call EwmaIncrement to fulfill ewma decorator's contract + bar.EwmaIncrement(time.Since(start)) } }() } - // 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/spinnerDecorator/go.mod b/_examples/spinnerDecorator/go.mod index 4bb0416..b12d3c1 100644 --- a/_examples/spinnerDecorator/go.mod +++ b/_examples/spinnerDecorator/go.mod @@ -1,5 +1,13 @@ module github.com/vbauerster/mpb/_examples/spinnerDecorator -go 1.14 +go 1.17 -require github.com/vbauerster/mpb/v6 v6.0.3 +require github.com/vbauerster/mpb/v8 v8.8.2 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/spinnerDecorator/main.go b/_examples/spinnerDecorator/main.go index 03fcd72..d4a1ac7 100644 --- a/_examples/spinnerDecorator/main.go +++ b/_examples/spinnerDecorator/main.go @@ -6,13 +6,13 @@ "sync" "time" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/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 a7f5409..db3075a 100644 --- a/_examples/stress/go.mod +++ b/_examples/stress/go.mod @@ -1,5 +1,18 @@ module github.com/vbauerster/mpb/_examples/stress -go 1.14 +go 1.17 -require github.com/vbauerster/mpb/v6 v6.0.3 +require ( + github.com/pkg/profile v1.7.0 + github.com/vbauerster/mpb/v8 v8.8.2 +) + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/felixge/fgprof v0.9.4 // indirect + github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/stress/main.go b/_examples/stress/main.go index 1dd199f..1537321 100644 --- a/_examples/stress/main.go +++ b/_examples/stress/main.go @@ -1,25 +1,35 @@ package main import ( + "flag" "fmt" "math/rand" + "os" "sync" "time" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/pkg/profile" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) const ( - totalBars = 32 + totalBars = 42 ) +var proftype = flag.String("prof", "", "profile type (cpu, mem)") + func main() { + flag.Parse() + switch *proftype { + case "cpu": + defer profile.Start(profile.CPUProfile, profile.ProfilePath("."), profile.NoShutdownHook).Stop() + case "mem": + defer profile.Start(profile.MemProfile, profile.ProfilePath("."), profile.NoShutdownHook).Stop() + } var wg sync.WaitGroup - 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), mpb.WithDebugOutput(os.Stderr)) wg.Add(totalBars) for i := 0; i < totalBars; i++ { @@ -27,13 +37,12 @@ total := rand.Intn(320) + 10 bar := p.AddBar(int64(total), mpb.PrependDecorators( - decor.Name(name), - decor.Elapsed(decor.ET_STYLE_GO, decor.WCSyncSpace), + decor.Name(name, decor.WCSyncWidthR), + decor.OnComplete(decor.Percentage(decor.WCSyncWidth), "done"), ), mpb.AppendDecorators( - decor.OnComplete( - decor.Percentage(decor.WC{W: 5}), "done", - ), + decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_GO, 30, decor.WCSyncWidth), ""), + decor.EwmaSpeed(decor.SizeB1024(0), "", 30, decor.WCSyncSpace), ), ) @@ -41,12 +50,13 @@ defer wg.Done() rng := rand.New(rand.NewSource(time.Now().UnixNano())) max := 100 * time.Millisecond - for !bar.Completed() { + for bar.IsRunning() { + start := time.Now() time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) - bar.Increment() + bar.EwmaIncrement(time.Since(start)) } }() } - + // 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 0d10445..ab12bc5 100644 --- a/_examples/suppressBar/go.mod +++ b/_examples/suppressBar/go.mod @@ -1,8 +1,13 @@ module github.com/vbauerster/mpb/_examples/suppressBar -go 1.14 +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.8.2 require ( - github.com/mattn/go-runewidth v0.0.10 - github.com/vbauerster/mpb/v6 v6.0.3 + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect ) diff --git a/_examples/suppressBar/main.go b/_examples/suppressBar/main.go index f3dfc56..6d3ba6b 100644 --- a/_examples/suppressBar/main.go +++ b/_examples/suppressBar/main.go @@ -3,77 +3,56 @@ import ( "errors" "fmt" - "io" + "math" "math/rand" "sync" "time" - "github.com/mattn/go-runewidth" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) func main() { p := mpb.New() - total := 100 - msgCh := make(chan string) - resumeCh := make(chan struct{}) - nextCh := make(chan struct{}, 1) - 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: + total, numBars := 100, 3 + err := new(errorWrapper) + timer := time.AfterFunc(2*time.Second, func() { + err.set(errors.New("timeout"), rand.Intn(numBars)) + }) + defer timer.Stop() + + for i := 0; i < numBars; i++ { + msgCh := make(chan string, 1) + bar := p.AddBar(int64(total), + mpb.PrependDecorators(newTitleDecorator(fmt.Sprintf("Bar#%d:", i), msgCh, 16)), + mpb.AppendDecorators(decor.Percentage(decor.WCSyncWidth)), + ) + // simulating some work + barID := i + go func() { + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + if err.check(barID) { + msgCh <- fmt.Sprintf("%s at %d, retrying...", err.Error(), i) + err.reset() + i-- + bar.SetRefill(int64(i)) + continue } - 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() { - ew.reset(errors.New("timeout")) - }) - // simulating some work - go func() { - rng := rand.New(rand.NewSource(time.Now().UnixNano())) - max := 100 * time.Millisecond - for i := 0; i < total; i++ { - time.Sleep(time.Duration(rng.Intn(10)+1) * max / 10) - if ew.isErr() { - msgCh <- fmt.Sprintf("%s at %d, retrying...", ew.Error(), i) - go ew.reset(nil) - i-- - bar.SetRefill(int64(i)) - time.Sleep(3 * time.Second) - resumeCh <- struct{}{} - continue + time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) + bar.Increment() } - bar.Increment() - } - }() + }() + } p.Wait() } type errorWrapper struct { sync.RWMutex - err error + err error + barID int } func (ew *errorWrapper) Error() string { @@ -82,27 +61,54 @@ return ew.err.Error() } -func (ew *errorWrapper) isErr() bool { +func (ew *errorWrapper) check(barID int) bool { ew.RLock() defer ew.RUnlock() - return ew.err != nil + return ew.err != nil && ew.barID == barID } -func (ew *errorWrapper) reset(err error) { +func (ew *errorWrapper) set(err error, barID int) { ew.Lock() ew.err = err + ew.barID = barID ew.Unlock() } -func newCustomPercentage(nextCh <-chan struct{}) decor.Decorator { - base := decor.Percentage() - fn := func(s decor.Statistics) string { +func (ew *errorWrapper) reset() { + ew.Lock() + ew.err = nil + ew.Unlock() +} + +type title struct { + decor.Decorator + name string + msgCh <-chan string + msg string + count int + limit int +} + +func (d *title) Decor(stat decor.Statistics) (string, int) { + if d.count == 0 { select { - case <-nextCh: - return "" + case msg := <-d.msgCh: + d.count = d.limit + d.msg = msg default: - return base.Decor(s) + return d.Decorator.Decor(stat) } } - return decor.Any(fn) + d.count-- + _, _ = d.Format("") + return fmt.Sprintf("%s %s", d.name, d.msg), math.MaxInt } + +func newTitleDecorator(name string, msgCh <-chan string, limit int) decor.Decorator { + return &title{ + Decorator: decor.Name(name), + name: name, + msgCh: msgCh, + limit: limit, + } +} diff --git a/_examples/tipOnComplete/go.mod b/_examples/tipOnComplete/go.mod new file mode 100644 index 0000000..691adad --- /dev/null +++ b/_examples/tipOnComplete/go.mod @@ -0,0 +1,13 @@ +module github.com/vbauerster/mpb/_examples/tipOnComplete + +go 1.17 + +require github.com/vbauerster/mpb/v8 v8.8.2 + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.24.0 // indirect +) diff --git a/_examples/tipOnComplete/main.go b/_examples/tipOnComplete/main.go new file mode 100644 index 0000000..852240e --- /dev/null +++ b/_examples/tipOnComplete/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "math/rand" + "time" + + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" +) + +func main() { + // initialize progress container, with custom width + p := mpb.New(mpb.WithWidth(80)) + + total := 100 + name := "Single Bar:" + bar := p.New(int64(total), + mpb.BarStyle().TipOnComplete(), + mpb.PrependDecorators(decor.Name(name)), + mpb.AppendDecorators(decor.Percentage()), + ) + // simulating some work + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) + bar.Increment() + } + // wait for our bar to complete and flush + p.Wait() +} diff --git a/bar.go b/bar.go index f18ef96..73753f0 100644 --- a/bar.go +++ b/bar.go @@ -3,109 +3,118 @@ import ( "bytes" "context" - "fmt" "io" - "log" - "runtime/debug" "strings" + "sync" "time" "github.com/acarl005/stripansi" "github.com/mattn/go-runewidth" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8/decor" ) // 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 - - // 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 *bState - - container *Progress - dlogger *log.Logger - recoveredPanic interface{} -} - -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. + index int // used by heap + priority int // used by heap + frameCh chan *renderFrame + operateState chan func(*bState) + container *Progress + bs *bState + bsOk chan struct{} + ctx context.Context + cancel func() +} + +type syncTable [2][]chan int +type extenderFunc func(decor.Statistics, ...io.Reader) ([]io.Reader, error) + +// bState is actual bar's state. type bState struct { - id int - priority int - reqWidth int - total int64 - current int64 - refill int64 - lastN int64 - iterated bool - trimSpace bool - completed bool - completeFlushed 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 - filler BarFiller - middleware func(BarFiller) BarFiller - extender extenderFunc - - // runningBar is a key for *pState.parkedBars - runningBar *Bar - - debugOut io.Writer -} - -func newBar(container *Progress, bs *bState) *Bar { - logPrefix := fmt.Sprintf("%sbar#%02d ", container.dlogger.Prefix(), bs.id) - ctx, cancel := context.WithCancel(container.ctx) + id int + priority int + reqWidth int + shutdown int + total int64 + current int64 + refill int64 + trimSpace bool + aborted bool + triggerComplete bool + rmOnComplete bool + noPop bool + autoRefresh bool + buffers [3]*bytes.Buffer + decorators [2][]decor.Decorator + ewmaDecorators []decor.EwmaDecorator + filler BarFiller + extender extenderFunc + renderReq chan<- time.Time + waitBar *Bar // key for (*pState).queueBars +} + +type renderFrame struct { + rows []io.Reader + shutdown int + rmOnComplete bool + noPop bool + err error +} + +func newBar(ctx context.Context, container *Progress, bs *bState) *Bar { + ctx, cancel := context.WithCancel(ctx) bar := &Bar{ + priority: bs.priority, + frameCh: make(chan *renderFrame, 1), + operateState: make(chan func(*bState)), + bsOk: make(chan struct{}), container: container, - priority: bs.priority, - toDrop: bs.dropOnComplete, - noPop: bs.noPop, - operateState: make(chan func(*bState)), - frameCh: make(chan io.Reader, 1), - syncTableCh: make(chan [][]chan int, 1), - completed: make(chan bool, 1), - done: make(chan struct{}), + ctx: ctx, cancel: cancel, - dlogger: log.New(bs.debugOut, logPrefix, log.Lshortfile), - } - - go bar.serve(ctx, bs) + } + + container.bwg.Add(1) + go bar.serve(bs) return bar } -// ProxyReader wraps r with metrics required for progress tracking. -// Panics if r is nil. +// ProxyReader wraps io.Reader with metrics required for progress +// tracking. If `r` is 'unknown total/size' reader it's mandatory +// to call `(*Bar).SetTotal(-1, true)` after the wrapper returns +// `io.EOF`. If bar is already completed or aborted, returns nil. +// Panics if `r` is nil. func (b *Bar) ProxyReader(r io.Reader) io.ReadCloser { if r == nil { panic("expected non nil io.Reader") } - return newProxyReader(r, b) + result := make(chan io.ReadCloser) + select { + case b.operateState <- func(s *bState) { + result <- newProxyReader(r, b, len(s.ewmaDecorators) != 0) + }: + return <-result + case <-b.ctx.Done(): + return nil + } +} + +// ProxyWriter wraps io.Writer with metrics required for progress tracking. +// If bar is already completed or aborted, returns nil. +// Panics if `w` is nil. +func (b *Bar) ProxyWriter(w io.Writer) io.WriteCloser { + if w == nil { + panic("expected non nil io.Writer") + } + result := make(chan io.WriteCloser) + select { + case b.operateState <- func(s *bState) { + result <- newProxyWriter(w, b, len(s.ewmaDecorators) != 0) + }: + return <-result + case <-b.ctx.Done(): + return nil + } } // ID returs id of the bar. @@ -114,19 +123,19 @@ select { case b.operateState <- func(s *bState) { result <- s.id }: return <-result - case <-b.done: - return b.cacheState.id - } -} - -// Current returns bar's current number, in other words sum of all increments. + case <-b.bsOk: + return b.bs.id + } +} + +// Current returns bar's current value, in other words sum of all increments. func (b *Bar) Current() int64 { result := make(chan int64) select { case b.operateState <- func(s *bState) { result <- s.current }: return <-result - case <-b.done: - return b.cacheState.current + case <-b.bsOk: + return b.bs.current } } @@ -137,65 +146,98 @@ func (b *Bar) SetRefill(amount int64) { select { case b.operateState <- func(s *bState) { - s.refill = amount - }: - case <-b.done: - } -} - -// TraverseDecorators traverses all available decorators and calls cb func on each. + if amount < s.current { + s.refill = amount + } else { + s.refill = s.current + } + }: + case <-b.ctx.Done(): + } +} + +// TraverseDecorators traverses available decorators and calls cb func +// on each in a new goroutine. Decorators implementing decor.Wrapper +// interface are unwrapped first. func (b *Bar) TraverseDecorators(cb func(decor.Decorator)) { select { case b.operateState <- func(s *bState) { - for _, decorators := range [...][]decor.Decorator{ - s.pDecorators, - s.aDecorators, - } { + var wg sync.WaitGroup + for _, decorators := range s.decorators { + wg.Add(len(decorators)) for _, d := range decorators { - cb(extractBaseDecorator(d)) + d := d + go func() { + cb(unwrap(d)) + wg.Done() + }() } } - }: - case <-b.done: - } -} - -// SetTotal sets total dynamically. -// If total is less than or equal to zero 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 { + wg.Wait() + }: + case <-b.ctx.Done(): + } +} + +// EnableTriggerComplete enables triggering complete event. It's effective +// only for bars which were constructed with `total <= 0`. If `curren >= total` +// at the moment of call, complete event is triggered right away. +func (b *Bar) EnableTriggerComplete() { + select { + case b.operateState <- func(s *bState) { + if s.triggerComplete { + return + } + if s.current >= s.total { + s.current = s.total + s.triggerCompletion(b) + } else { + s.triggerComplete = true + } + }: + case <-b.ctx.Done(): + } +} + +// SetTotal sets total to an arbitrary value. It's effective only for bar +// which was constructed with `total <= 0`. Setting total to negative value +// is equivalent to `(*Bar).SetTotal((*Bar).Current(), bool)` but faster. +// If `complete` is true complete event is triggered right away. +// Calling `(*Bar).EnableTriggerComplete` makes this one no operational. +func (b *Bar) SetTotal(total int64, complete bool) { + select { + case b.operateState <- func(s *bState) { + if s.triggerComplete { + return + } + if total < 0 { s.total = s.current } else { s.total = total } - if s.triggerComplete && !s.completed { + if complete { s.current = s.total - s.completed = true - go b.refreshTillShutdown() - } - }: - case <-b.done: + s.triggerCompletion(b) + } + }: + case <-b.ctx.Done(): } } // 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 + if current < 0 { + return + } + select { + case b.operateState <- func(s *bState) { s.current = current if s.triggerComplete && s.current >= s.total { s.current = s.total - s.completed = true - go b.refreshTillShutdown() - } - }: - case <-b.done: + s.triggerCompletion(b) + } + }: + case <-b.ctx.Done(): } } @@ -213,280 +255,356 @@ func (b *Bar) IncrInt64(n int64) { select { case b.operateState <- func(s *bState) { - s.iterated = true - s.lastN = n s.current += n if s.triggerComplete && s.current >= s.total { s.current = s.total - s.completed = true - go b.refreshTillShutdown() - } - }: - case <-b.done: - } -} - -// DecoratorEwmaUpdate updates all EWMA based decorators. Should be -// called on each iteration, because EWMA's unit of measure is an -// iteration's duration. Panics if called before *Bar.Incr... family -// methods. -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) - } -} - -// DecoratorAverageAdjust adjusts all average based decorators. Call -// if you need to adjust start time of all average based decorators -// or after progress resume. + s.triggerCompletion(b) + } + }: + case <-b.ctx.Done(): + } +} + +// EwmaIncrement is a shorthand for b.EwmaIncrInt64(1, iterDur). +func (b *Bar) EwmaIncrement(iterDur time.Duration) { + b.EwmaIncrInt64(1, iterDur) +} + +// EwmaIncrBy is a shorthand for b.EwmaIncrInt64(int64(n), iterDur). +func (b *Bar) EwmaIncrBy(n int, iterDur time.Duration) { + b.EwmaIncrInt64(int64(n), iterDur) +} + +// EwmaIncrInt64 increments progress by amount of n and updates EWMA based +// decorators by dur of a single iteration. +func (b *Bar) EwmaIncrInt64(n int64, iterDur time.Duration) { + select { + case b.operateState <- func(s *bState) { + var wg sync.WaitGroup + wg.Add(len(s.ewmaDecorators)) + for _, d := range s.ewmaDecorators { + d := d + go func() { + d.EwmaUpdate(n, iterDur) + wg.Done() + }() + } + s.current += n + if s.triggerComplete && s.current >= s.total { + s.current = s.total + s.triggerCompletion(b) + } + wg.Wait() + }: + case <-b.ctx.Done(): + } +} + +// EwmaSetCurrent sets progress' current to an arbitrary value and updates +// EWMA based decorators by dur of a single iteration. +func (b *Bar) EwmaSetCurrent(current int64, iterDur time.Duration) { + if current < 0 { + return + } + select { + case b.operateState <- func(s *bState) { + n := current - s.current + var wg sync.WaitGroup + wg.Add(len(s.ewmaDecorators)) + for _, d := range s.ewmaDecorators { + d := d + go func() { + d.EwmaUpdate(n, iterDur) + wg.Done() + }() + } + s.current = current + if s.triggerComplete && s.current >= s.total { + s.current = s.total + s.triggerCompletion(b) + } + wg.Wait() + }: + case <-b.ctx.Done(): + } +} + +// DecoratorAverageAdjust adjusts decorators implementing decor.AverageDecorator interface. +// Call if there is need to set start time after decorators have been constructed. func (b *Bar) DecoratorAverageAdjust(start time.Time) { - select { - case b.operateState <- func(s *bState) { - for _, d := range s.averageDecorators { + b.TraverseDecorators(func(d decor.Decorator) { + if d, ok := d.(decor.AverageDecorator); ok { d.AverageAdjust(start) } - }: - case <-b.done: - } + }) } // SetPriority changes bar's order among multiple bars. Zero is highest // priority, i.e. bar will be on top. If you don't need to set priority // dynamically, better use BarPriority option. func (b *Bar) SetPriority(priority int) { - 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, false) +} + +// Abort interrupts bar's running goroutine. Abort won't be engaged +// if bar is already in complete state. If drop is true bar will be +// removed as well. To make sure that bar has been removed call +// `(*Bar).Wait()` method. func (b *Bar) Abort(drop bool) { select { - case <-b.done: - default: - if drop { - b.container.dropBar(b) - } - b.cancel() + case b.operateState <- func(s *bState) { + if s.aborted || s.completed() { + return + } + s.aborted = true + s.rmOnComplete = drop + s.triggerCompletion(b) + }: + case <-b.ctx.Done(): + } +} + +// Aborted reports whether the bar is in aborted state. +func (b *Bar) Aborted() bool { + result := make(chan bool) + select { + case b.operateState <- func(s *bState) { result <- s.aborted }: + return <-result + case <-b.bsOk: + return b.bs.aborted } } // Completed reports whether the bar is in completed state. func (b *Bar) Completed() bool { - select { - case b.operateState <- func(s *bState) { b.completed <- s.completed }: - return <-b.completed - case <-b.done: + result := make(chan bool) + select { + case b.operateState <- func(s *bState) { result <- s.completed() }: + return <-result + case <-b.bsOk: + return b.bs.completed() + } +} + +// IsRunning reports whether the bar is in running state. +func (b *Bar) IsRunning() bool { + select { + case <-b.ctx.Done(): + return false + default: return true } } -func (b *Bar) serve(ctx context.Context, s *bState) { - defer b.container.bwg.Done() +// Wait blocks until bar is completed or aborted. +func (b *Bar) Wait() { + <-b.bsOk +} + +func (b *Bar) serve(bs *bState) { + decoratorsOnShutdown := func(decorators []decor.Decorator) { + for _, d := range decorators { + if d, ok := unwrap(d).(decor.ShutdownListener); ok { + b.container.bwg.Add(1) + go func() { + d.OnShutdown() + b.container.bwg.Done() + }() + } + } + } for { select { case op := <-b.operateState: - op(s) - case <-ctx.Done(): - b.cacheState = s - close(b.done) - // Notifying decorators about shutdown event - for _, sl := range s.shutdownListeners { - sl.Shutdown() + op(bs) + case <-b.ctx.Done(): + decoratorsOnShutdown(bs.decorators[0]) + decoratorsOnShutdown(bs.decorators[1]) + // bar can be aborted by canceling parent ctx without calling b.Abort + bs.aborted = !bs.completed() + b.bs = bs + close(b.bsOk) + b.container.bwg.Done() + return + } + } +} + +func (b *Bar) render(tw int) { + fn := func(s *bState) { + frame := new(renderFrame) + stat := s.newStatistics(tw) + r, err := s.draw(stat) + if err != nil { + for _, buf := range s.buffers { + buf.Reset() } + frame.err = err + b.frameCh <- frame return } - } -} - -func (b *Bar) render(tw int) { - 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 { - if b.recoveredPanic == nil { - s.extender = makePanicExtender(p) - b.toShutdown = !b.toShutdown - b.recoveredPanic = p - } - frame, lines := s.extender(nil, s.reqWidth, stat) - b.extendedLines = lines - b.frameCh <- frame - b.dlogger.Println(p) + frame.rows, frame.err = s.extender(stat, r) + if s.aborted || s.completed() { + frame.shutdown = s.shutdown + frame.rmOnComplete = s.rmOnComplete + frame.noPop = s.noPop + // post increment makes sure OnComplete decorators are rendered + s.shutdown++ + } + b.frameCh <- frame + } + select { + case b.operateState <- fn: + case <-b.bsOk: + fn(b.bs) + } +} + +func (b *Bar) tryEarlyRefresh(renderReq chan<- time.Time) { + var otherRunning int + b.container.traverseBars(func(bar *Bar) bool { + if b != bar && bar.IsRunning() { + otherRunning++ + return false // stop traverse + } + return true // continue traverse + }) + if otherRunning == 0 { + for { + select { + case renderReq <- time.Now(): + case <-b.ctx.Done(): + return } - s.completeFlushed = s.completed - }() - frame, lines := s.extender(s.draw(stat), s.reqWidth, stat) - b.extendedLines = lines - b.toShutdown = s.completed && !s.completeFlushed - b.frameCh <- frame - }: - case <-b.done: - s := b.cacheState - stat := newStatistics(tw, s) - var r io.Reader - if b.recoveredPanic == nil { - r = s.draw(stat) - } - frame, lines := s.extender(r, s.reqWidth, stat) - b.extendedLines = lines - b.frameCh <- frame - } -} - -func (b *Bar) subscribeDecorators() { - var averageDecorators []decor.AverageDecorator - var ewmaDecorators []decor.EwmaDecorator - var shutdownListeners []decor.ShutdownListener - b.TraverseDecorators(func(d decor.Decorator) { - if d, ok := d.(decor.AverageDecorator); ok { - averageDecorators = append(averageDecorators, d) - } - if d, ok := d.(decor.EwmaDecorator); ok { - ewmaDecorators = append(ewmaDecorators, d) - } - if d, ok := d.(decor.ShutdownListener); ok { - shutdownListeners = append(shutdownListeners, d) - } - }) - select { - case b.operateState <- func(s *bState) { - s.averageDecorators = averageDecorators - s.ewmaDecorators = ewmaDecorators - s.shutdownListeners = shutdownListeners - }: - b.hasEwmaDecorators = len(ewmaDecorators) != 0 - case <-b.done: - } -} - -func (b *Bar) refreshTillShutdown() { - 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 - case <-b.done: - return b.cacheState.wSyncTable() - } -} - -func (s *bState) draw(stat decor.Statistics) io.Reader { - if !s.trimSpace { + } + } +} + +func (b *Bar) wSyncTable() syncTable { + result := make(chan syncTable) + select { + case b.operateState <- func(s *bState) { result <- s.wSyncTable() }: + return <-result + case <-b.bsOk: + return b.bs.wSyncTable() + } +} + +func (s *bState) draw(stat decor.Statistics) (_ io.Reader, err error) { + decorFiller := func(buf *bytes.Buffer, decorators []decor.Decorator) (err error) { + for _, d := range decorators { + // need to call Decor in any case becase of width synchronization + str, width := d.Decor(stat) + if err != nil { + continue + } + if w := stat.AvailableWidth - width; w >= 0 { + _, err = buf.WriteString(str) + stat.AvailableWidth = w + } else if stat.AvailableWidth > 0 { + trunc := runewidth.Truncate(stripansi.Strip(str), stat.AvailableWidth, "…") + _, err = buf.WriteString(trunc) + stat.AvailableWidth = 0 + } + } + return err + } + + for i, buf := range s.buffers[:2] { + err = decorFiller(buf, s.decorators[i]) + if err != nil { + return nil, err + } + } + + spaces := []io.Reader{ + strings.NewReader(" "), + strings.NewReader(" "), + } + if s.trimSpace || stat.AvailableWidth < 2 { + for _, r := range spaces { + _, _ = io.Copy(io.Discard, r) + } + } else { stat.AvailableWidth -= 2 - s.bufB.WriteByte(' ') - defer s.bufB.WriteByte(' ') - } - - nlr := strings.NewReader("\n") - tw := stat.AvailableWidth - for _, d := range s.pDecorators { - str := d.Decor(stat) - stat.AvailableWidth -= runewidth.StringWidth(stripansi.Strip(str)) - s.bufP.WriteString(str) - } - if stat.AvailableWidth <= 0 { - trunc := strings.NewReader(runewidth.Truncate(stripansi.Strip(s.bufP.String()), tw, "…")) - s.bufP.Reset() - return io.MultiReader(trunc, s.bufB, nlr) - } - - tw = stat.AvailableWidth - for _, d := range s.aDecorators { - str := d.Decor(stat) - stat.AvailableWidth -= runewidth.StringWidth(stripansi.Strip(str)) - s.bufA.WriteString(str) - } - if stat.AvailableWidth <= 0 { - trunc := strings.NewReader(runewidth.Truncate(stripansi.Strip(s.bufA.String()), tw, "…")) - s.bufA.Reset() - return io.MultiReader(s.bufP, s.bufB, trunc, nlr) - } - - s.filler.Fill(s.bufB, s.reqWidth, stat) - - return io.MultiReader(s.bufP, s.bufB, s.bufA, nlr) -} - -func (s *bState) wSyncTable() [][]chan int { - columns := make([]chan int, 0, len(s.pDecorators)+len(s.aDecorators)) - var pCount int - for _, d := range s.pDecorators { - if ch, ok := d.Sync(); ok { - columns = append(columns, ch) - pCount++ - } - } - var aCount int - for _, d := range s.aDecorators { - if ch, ok := d.Sync(); ok { - columns = append(columns, ch) - aCount++ - } - } - table := make([][]chan int, 2) - table[0] = columns[0:pCount] - table[1] = columns[pCount : pCount+aCount : pCount+aCount] + } + + err = s.filler.Fill(s.buffers[2], stat) + if err != nil { + return nil, err + } + + return io.MultiReader( + s.buffers[0], + spaces[0], + s.buffers[2], + spaces[1], + s.buffers[1], + strings.NewReader("\n"), + ), nil +} + +func (s *bState) wSyncTable() (table syncTable) { + var count int + var row []chan int + + for i, decorators := range s.decorators { + for _, d := range decorators { + if ch, ok := d.Sync(); ok { + row = append(row, ch) + count++ + } + } + switch i { + case 0: + table[i] = row[0:count] + default: + table[i] = row[len(table[i-1]):count] + } + } return table } -func newStatistics(tw int, s *bState) decor.Statistics { +func (s *bState) populateEwmaDecorators(decorators []decor.Decorator) { + for _, d := range decorators { + if d, ok := unwrap(d).(decor.EwmaDecorator); ok { + s.ewmaDecorators = append(s.ewmaDecorators, d) + } + } +} + +func (s *bState) triggerCompletion(b *Bar) { + s.triggerComplete = true + if s.autoRefresh { + // Technically this call isn't required, but if refresh rate is set to + // one hour for example and bar completes within a few minutes p.Wait() + // will wait for one hour. This call helps to avoid unnecessary waiting. + go b.tryEarlyRefresh(s.renderReq) + } else { + b.cancel() + } +} + +func (s bState) completed() bool { + return s.triggerComplete && s.current == s.total +} + +func (s bState) newStatistics(tw int) decor.Statistics { return decor.Statistics{ + AvailableWidth: tw, + RequestedWidth: s.reqWidth, ID: s.id, - AvailableWidth: tw, Total: s.total, Current: s.current, Refill: s.refill, - Completed: s.completeFlushed, - } -} - -func extractBaseDecorator(d decor.Decorator) decor.Decorator { + Completed: s.completed(), + Aborted: s.aborted, + } +} + +func unwrap(d decor.Decorator) decor.Decorator { if d, ok := d.(decor.Wrapper); ok { - return extractBaseDecorator(d.Base()) + return unwrap(d.Unwrap()) } 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) - stack := debug.Stack() - stackLines := bytes.Count(stack, []byte("\n")) - return func(_ io.Reader, _ int, st decor.Statistics) (io.Reader, int) { - mr := io.MultiReader( - strings.NewReader(runewidth.Truncate(pstr, st.AvailableWidth, "…")), - strings.NewReader(fmt.Sprintf("\n%#v\n", st)), - bytes.NewReader(stack), - ) - return mr, stackLines + 1 - } -} diff --git a/bar_filler.go b/bar_filler.go index c8cedaa..379cfea 100644 --- a/bar_filler.go +++ b/bar_filler.go @@ -3,29 +3,29 @@ import ( "io" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8/decor" ) // BarFiller interface. // Bar (without decorators) renders itself by calling BarFiller's Fill method. -// -// reqWidth is requested width, set by `func WithWidth(int) ContainerOption`. -// If not set, it defaults to terminal width. -// -// Default implementations can be obtained via: -// -// func NewBarFiller(style string) BarFiller -// func NewBarFillerRev(style string) BarFiller -// func NewBarFillerPick(style string, rev bool) BarFiller -// func NewSpinnerFiller(style []string, alignment SpinnerAlignment) BarFiller -// type BarFiller interface { - Fill(w io.Writer, reqWidth int, stat decor.Statistics) + Fill(io.Writer, decor.Statistics) error } -// BarFillerFunc is function type adapter to convert function into BarFiller. -type BarFillerFunc func(w io.Writer, reqWidth int, stat decor.Statistics) +// BarFillerBuilder interface. +// Default implementations are: +// +// BarStyle() +// SpinnerStyle() +// NopStyle() +type BarFillerBuilder interface { + Build() BarFiller +} -func (f BarFillerFunc) Fill(w io.Writer, reqWidth int, stat decor.Statistics) { - f(w, reqWidth, stat) +// BarFillerFunc is function type adapter to convert compatible function +// into BarFiller interface. +type BarFillerFunc func(io.Writer, decor.Statistics) error + +func (f BarFillerFunc) Fill(w io.Writer, stat decor.Statistics) error { + return f(w, stat) } diff --git a/bar_filler_bar.go b/bar_filler_bar.go index 1c339e9..7a036d9 100644 --- a/bar_filler_bar.go +++ b/bar_filler_bar.go @@ -1,191 +1,290 @@ package mpb import ( - "bytes" "io" - "unicode/utf8" "github.com/mattn/go-runewidth" - "github.com/rivo/uniseg" - "github.com/vbauerster/mpb/v6/decor" - "github.com/vbauerster/mpb/v6/internal" + "github.com/vbauerster/mpb/v8/decor" + "github.com/vbauerster/mpb/v8/internal" ) const ( - rLeft = iota - rFill - rTip - rSpace - rRight - rRevTip - rRefill + iLbound = iota + iRbound + iRefiller + iFiller + iTip + iPadding + components ) -// BarDefaultStyle is a style for rendering a progress bar. -// It consist of 7 ordered runes: -// -// '1st rune' stands for left boundary rune -// -// '2nd rune' stands for fill rune -// -// '3rd rune' stands for tip rune -// -// '4th rune' stands for space rune -// -// '5th rune' stands for right boundary rune -// -// '6th rune' stands for reverse tip rune -// -// '7th rune' stands for refill rune -// -const BarDefaultStyle string = "[=>-]<+" - -type barFiller struct { - format [][]byte - rwidth []int - tip []byte - refill int64 - reverse bool - flush func(io.Writer, *space, [][]byte) -} - -type space struct { - space []byte - rwidth int - count int -} - -// NewBarFiller returns a BarFiller implementation which renders a -// progress bar in regular direction. If style is empty string, -// BarDefaultStyle is applied. To be used with `*Progress.Add(...) -// *Bar` method. -func NewBarFiller(style string) BarFiller { - return newBarFiller(style, false) -} - -// NewBarFillerRev returns a BarFiller implementation which renders a -// progress bar in reverse direction. If style is empty string, -// BarDefaultStyle is applied. To be used with `*Progress.Add(...) -// *Bar` method. -func NewBarFillerRev(style string) BarFiller { - return newBarFiller(style, true) -} - -// NewBarFillerPick pick between regular and reverse BarFiller implementation -// based on rev param. To be used with `*Progress.Add(...) *Bar` method. -func NewBarFillerPick(style string, rev bool) BarFiller { - return newBarFiller(style, rev) -} - -func newBarFiller(style string, rev bool) BarFiller { - bf := &barFiller{ - format: make([][]byte, len(BarDefaultStyle)), - rwidth: make([]int, len(BarDefaultStyle)), - reverse: rev, - } - bf.parse(BarDefaultStyle) - if style != "" && style != BarDefaultStyle { - bf.parse(style) +var defaultBarStyle = [components]string{"[", "]", "+", "=", ">", "-"} + +// BarStyleComposer interface. +type BarStyleComposer interface { + BarFillerBuilder + Lbound(string) BarStyleComposer + LboundMeta(func(string) string) BarStyleComposer + Rbound(string) BarStyleComposer + RboundMeta(func(string) string) BarStyleComposer + Filler(string) BarStyleComposer + FillerMeta(func(string) string) BarStyleComposer + Refiller(string) BarStyleComposer + RefillerMeta(func(string) string) BarStyleComposer + Padding(string) BarStyleComposer + PaddingMeta(func(string) string) BarStyleComposer + Tip(frames ...string) BarStyleComposer + TipMeta(func(string) string) BarStyleComposer + TipOnComplete() BarStyleComposer + Reverse() BarStyleComposer +} + +type component struct { + width int + bytes []byte +} + +type flushSection struct { + meta func(io.Writer, []byte) error + bytes []byte +} + +type bFiller struct { + components [components]component + meta [components]func(io.Writer, []byte) error + flush func(io.Writer, ...flushSection) error + tip struct { + onComplete bool + count uint + frames []component + } +} + +type barStyle struct { + style [components]string + metaFuncs [components]func(io.Writer, []byte) error + tipFrames []string + tipOnComplete bool + rev bool +} + +// BarStyle constructs default bar style which can be altered via +// BarStyleComposer interface. +func BarStyle() BarStyleComposer { + bs := barStyle{ + style: defaultBarStyle, + tipFrames: []string{defaultBarStyle[iTip]}, + } + for i := range bs.metaFuncs { + bs.metaFuncs[i] = defaultMeta + } + return bs +} + +func (s barStyle) Lbound(bound string) BarStyleComposer { + s.style[iLbound] = bound + return s +} + +func (s barStyle) LboundMeta(fn func(string) string) BarStyleComposer { + s.metaFuncs[iLbound] = makeMetaFunc(fn) + return s +} + +func (s barStyle) Rbound(bound string) BarStyleComposer { + s.style[iRbound] = bound + return s +} + +func (s barStyle) RboundMeta(fn func(string) string) BarStyleComposer { + s.metaFuncs[iRbound] = makeMetaFunc(fn) + return s +} + +func (s barStyle) Filler(filler string) BarStyleComposer { + s.style[iFiller] = filler + return s +} + +func (s barStyle) FillerMeta(fn func(string) string) BarStyleComposer { + s.metaFuncs[iFiller] = makeMetaFunc(fn) + return s +} + +func (s barStyle) Refiller(refiller string) BarStyleComposer { + s.style[iRefiller] = refiller + return s +} + +func (s barStyle) RefillerMeta(fn func(string) string) BarStyleComposer { + s.metaFuncs[iRefiller] = makeMetaFunc(fn) + return s +} + +func (s barStyle) Padding(padding string) BarStyleComposer { + s.style[iPadding] = padding + return s +} + +func (s barStyle) PaddingMeta(fn func(string) string) BarStyleComposer { + s.metaFuncs[iPadding] = makeMetaFunc(fn) + return s +} + +func (s barStyle) Tip(frames ...string) BarStyleComposer { + if len(frames) != 0 { + s.tipFrames = frames + } + return s +} + +func (s barStyle) TipMeta(fn func(string) string) BarStyleComposer { + s.metaFuncs[iTip] = makeMetaFunc(fn) + return s +} + +func (s barStyle) TipOnComplete() BarStyleComposer { + s.tipOnComplete = true + return s +} + +func (s barStyle) Reverse() BarStyleComposer { + s.rev = true + return s +} + +func (s barStyle) Build() BarFiller { + bf := &bFiller{ + meta: s.metaFuncs, + } + bf.components[iLbound] = component{ + width: runewidth.StringWidth(s.style[iLbound]), + bytes: []byte(s.style[iLbound]), + } + bf.components[iRbound] = component{ + width: runewidth.StringWidth(s.style[iRbound]), + bytes: []byte(s.style[iRbound]), + } + bf.components[iFiller] = component{ + width: runewidth.StringWidth(s.style[iFiller]), + bytes: []byte(s.style[iFiller]), + } + bf.components[iRefiller] = component{ + width: runewidth.StringWidth(s.style[iRefiller]), + bytes: []byte(s.style[iRefiller]), + } + bf.components[iPadding] = component{ + width: runewidth.StringWidth(s.style[iPadding]), + bytes: []byte(s.style[iPadding]), + } + bf.tip.onComplete = s.tipOnComplete + bf.tip.frames = make([]component, len(s.tipFrames)) + for i, t := range s.tipFrames { + bf.tip.frames[i] = component{ + width: runewidth.StringWidth(t), + bytes: []byte(t), + } + } + if s.rev { + bf.flush = func(w io.Writer, sections ...flushSection) error { + for i := len(sections) - 1; i >= 0; i-- { + if s := sections[i]; len(s.bytes) != 0 { + err := s.meta(w, s.bytes) + if err != nil { + return err + } + } + } + return nil + } + } else { + bf.flush = func(w io.Writer, sections ...flushSection) error { + for _, s := range sections { + if len(s.bytes) != 0 { + err := s.meta(w, s.bytes) + if err != nil { + return err + } + } + } + return nil + } } return bf } -func (s *barFiller) parse(style string) { - if !utf8.ValidString(style) { - panic("invalid bar style") - } - srcFormat := make([][]byte, len(BarDefaultStyle)) - srcRwidth := make([]int, len(BarDefaultStyle)) - i := 0 - for gr := uniseg.NewGraphemes(style); i < len(BarDefaultStyle) && gr.Next(); i++ { - srcFormat[i] = gr.Bytes() - srcRwidth[i] = runewidth.StringWidth(gr.Str()) - } - copy(s.format, srcFormat[:i]) - copy(s.rwidth, srcRwidth[:i]) - if s.reverse { - s.tip = s.format[rRevTip] - s.flush = reverseFlush - } else { - s.tip = s.format[rTip] - s.flush = regularFlush - } -} - -func (s *barFiller) Fill(w io.Writer, reqWidth int, stat decor.Statistics) { - width := internal.CheckRequestedWidth(reqWidth, stat.AvailableWidth) - brackets := s.rwidth[rLeft] + s.rwidth[rRight] - if width < brackets { - return - } +func (s *bFiller) Fill(w io.Writer, stat decor.Statistics) error { + width := internal.CheckRequestedWidth(stat.RequestedWidth, stat.AvailableWidth) // don't count brackets as progress - width -= brackets - - w.Write(s.format[rLeft]) - defer w.Write(s.format[rRight]) - - cwidth := int(internal.PercentageRound(stat.Total, stat.Current, width)) - space := &space{ - space: s.format[rSpace], - rwidth: s.rwidth[rSpace], - count: width - cwidth, - } - - index, refill := 0, 0 - bb := make([][]byte, cwidth) - - if cwidth > 0 && cwidth != width { - bb[index] = s.tip - cwidth -= s.rwidth[rTip] - index++ - } - - if stat.Refill > 0 { - refill = int(internal.PercentageRound(stat.Total, int64(stat.Refill), width)) - if refill > cwidth { - refill = cwidth - } - cwidth -= refill - } - - for cwidth > 0 { - bb[index] = s.format[rFill] - cwidth -= s.rwidth[rFill] - index++ - } - - for refill > 0 { - bb[index] = s.format[rRefill] - refill -= s.rwidth[rRefill] - index++ - } - - if cwidth+refill < 0 || space.rwidth > 1 { - buf := new(bytes.Buffer) - s.flush(buf, space, bb[:index]) - io.WriteString(w, runewidth.Truncate(buf.String(), width, "…")) - return - } - - s.flush(w, space, bb) -} - -func regularFlush(w io.Writer, space *space, bb [][]byte) { - for i := len(bb) - 1; i >= 0; i-- { - w.Write(bb[i]) - } - for space.count > 0 { - w.Write(space.space) - space.count -= space.rwidth - } -} - -func reverseFlush(w io.Writer, space *space, bb [][]byte) { - for space.count > 0 { - w.Write(space.space) - space.count -= space.rwidth - } - for i := 0; i < len(bb); i++ { - w.Write(bb[i]) - } -} + width -= (s.components[iLbound].width + s.components[iRbound].width) + if width < 0 { + return nil + } + + err := s.meta[iLbound](w, s.components[iLbound].bytes) + if err != nil { + return err + } + + if width == 0 { + return s.meta[iRbound](w, s.components[iRbound].bytes) + } + + var tip component + var refilling, filling, padding []byte + var fillCount int + curWidth := int(internal.PercentageRound(stat.Total, stat.Current, uint(width))) + + if curWidth != 0 { + if !stat.Completed || s.tip.onComplete { + tip = s.tip.frames[s.tip.count%uint(len(s.tip.frames))] + s.tip.count++ + fillCount += tip.width + } + switch refWidth := 0; { + case stat.Refill != 0: + refWidth = int(internal.PercentageRound(stat.Total, stat.Refill, uint(width))) + curWidth -= refWidth + refWidth += curWidth + fallthrough + default: + for w := s.components[iFiller].width; curWidth-fillCount >= w; fillCount += w { + filling = append(filling, s.components[iFiller].bytes...) + } + for w := s.components[iRefiller].width; refWidth-fillCount >= w; fillCount += w { + refilling = append(refilling, s.components[iRefiller].bytes...) + } + } + } + + for w := s.components[iPadding].width; width-fillCount >= w; fillCount += w { + padding = append(padding, s.components[iPadding].bytes...) + } + + for w := 1; width-fillCount >= w; fillCount += w { + padding = append(padding, "…"...) + } + + err = s.flush(w, + flushSection{s.meta[iRefiller], refilling}, + flushSection{s.meta[iFiller], filling}, + flushSection{s.meta[iTip], tip.bytes}, + flushSection{s.meta[iPadding], padding}, + ) + if err != nil { + return err + } + return s.meta[iRbound](w, s.components[iRbound].bytes) +} + +func makeMetaFunc(fn func(string) string) func(io.Writer, []byte) error { + return func(w io.Writer, p []byte) (err error) { + _, err = io.WriteString(w, fn(string(p))) + return err + } +} + +func defaultMeta(w io.Writer, p []byte) (err error) { + _, err = w.Write(p) + return err +} diff --git a/bar_filler_nop.go b/bar_filler_nop.go new file mode 100644 index 0000000..a23c61b --- /dev/null +++ b/bar_filler_nop.go @@ -0,0 +1,24 @@ +package mpb + +import ( + "io" + + "github.com/vbauerster/mpb/v8/decor" +) + +// barFillerBuilderFunc is function type adapter to convert compatible +// function into BarFillerBuilder interface. +type barFillerBuilderFunc func() BarFiller + +func (f barFillerBuilderFunc) Build() BarFiller { + return f() +} + +// NopStyle provides BarFillerBuilder which builds NOP BarFiller. +func NopStyle() BarFillerBuilder { + return barFillerBuilderFunc(func() BarFiller { + return BarFillerFunc(func(io.Writer, decor.Statistics) error { + return nil + }) + }) +} diff --git a/bar_filler_spinner.go b/bar_filler_spinner.go index 0817b19..c9fd463 100644 --- a/bar_filler_spinner.go +++ b/bar_filler_spinner.go @@ -5,61 +5,99 @@ "strings" "github.com/mattn/go-runewidth" - "github.com/vbauerster/mpb/v6/decor" - "github.com/vbauerster/mpb/v6/internal" + "github.com/vbauerster/mpb/v8/decor" + "github.com/vbauerster/mpb/v8/internal" ) -// SpinnerAlignment enum. -type SpinnerAlignment int - -// SpinnerAlignment kinds. const ( - SpinnerOnLeft SpinnerAlignment = iota - SpinnerOnMiddle - SpinnerOnRight + positionLeft = 1 + iota + positionRight ) -// SpinnerDefaultStyle is a style for rendering a spinner. -var SpinnerDefaultStyle = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} +var defaultSpinnerStyle = [...]string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} -type spinnerFiller struct { - frames []string - count uint - alignment SpinnerAlignment +// SpinnerStyleComposer interface. +type SpinnerStyleComposer interface { + BarFillerBuilder + PositionLeft() SpinnerStyleComposer + PositionRight() SpinnerStyleComposer + Meta(func(string) string) SpinnerStyleComposer } -// NewSpinnerFiller returns a BarFiller implementation which renders -// a spinner. If style is nil or zero length, SpinnerDefaultStyle is -// applied. To be used with `*Progress.Add(...) *Bar` method. -func NewSpinnerFiller(style []string, alignment SpinnerAlignment) BarFiller { - if len(style) == 0 { - style = SpinnerDefaultStyle - } - filler := &spinnerFiller{ - frames: style, - alignment: alignment, - } - return filler +type sFiller struct { + frames []string + count uint + meta func(string) string + position func(string, int) string } -func (s *spinnerFiller) Fill(w io.Writer, reqWidth int, stat decor.Statistics) { - width := internal.CheckRequestedWidth(reqWidth, stat.AvailableWidth) +type spinnerStyle struct { + position uint + frames []string + meta func(string) string +} +// SpinnerStyle constructs default spinner style which can be altered via +// SpinnerStyleComposer interface. +func SpinnerStyle(frames ...string) SpinnerStyleComposer { + ss := spinnerStyle{ + meta: func(s string) string { return s }, + } + if len(frames) != 0 { + ss.frames = frames + } else { + ss.frames = defaultSpinnerStyle[:] + } + return ss +} + +func (s spinnerStyle) PositionLeft() SpinnerStyleComposer { + s.position = positionLeft + return s +} + +func (s spinnerStyle) PositionRight() SpinnerStyleComposer { + s.position = positionRight + return s +} + +func (s spinnerStyle) Meta(fn func(string) string) SpinnerStyleComposer { + s.meta = fn + return s +} + +func (s spinnerStyle) Build() BarFiller { + sf := &sFiller{ + frames: s.frames, + meta: s.meta, + } + switch s.position { + case positionLeft: + sf.position = func(frame string, padWidth int) string { + return frame + strings.Repeat(" ", padWidth) + } + case positionRight: + sf.position = func(frame string, padWidth int) string { + return strings.Repeat(" ", padWidth) + frame + } + default: + sf.position = func(frame string, padWidth int) string { + return strings.Repeat(" ", padWidth/2) + frame + strings.Repeat(" ", padWidth/2+padWidth%2) + } + } + return sf +} + +func (s *sFiller) Fill(w io.Writer, stat decor.Statistics) error { + width := internal.CheckRequestedWidth(stat.RequestedWidth, stat.AvailableWidth) frame := s.frames[s.count%uint(len(s.frames))] frameWidth := runewidth.StringWidth(frame) + s.count++ if width < frameWidth { - return + return nil } - 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++ + _, err := io.WriteString(w, s.position(s.meta(frame), width-frameWidth)) + return err } diff --git a/bar_option.go b/bar_option.go index e359c11..6247a33 100644 --- a/bar_option.go +++ b/bar_option.go @@ -4,36 +4,37 @@ "bytes" "io" - "github.com/vbauerster/mpb/v6/decor" - "github.com/vbauerster/mpb/v6/internal" + "github.com/vbauerster/mpb/v8/decor" ) // BarOption is a func option to alter default behavior of a bar. type BarOption func(*bState) -func (s *bState) addDecorators(dest *[]decor.Decorator, decorators ...decor.Decorator) { - type mergeWrapper interface { - MergeUnwrap() []decor.Decorator - } +func inspect(decorators []decor.Decorator) (dest []decor.Decorator) { for _, decorator := range decorators { - if mw, ok := decorator.(mergeWrapper); ok { - *dest = append(*dest, mw.MergeUnwrap()...) - } - *dest = append(*dest, decorator) + if decorator == nil { + continue + } + dest = append(dest, decorator) + } + return +} + +// PrependDecorators let you inject decorators to the bar's left side. +func PrependDecorators(decorators ...decor.Decorator) BarOption { + decorators = inspect(decorators) + return func(s *bState) { + s.populateEwmaDecorators(decorators) + s.decorators[0] = decorators } } // 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...) - } -} - -// 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...) + decorators = inspect(decorators) + return func(s *bState) { + s.populateEwmaDecorators(decorators) + s.decorators[1] = decorators } } @@ -51,14 +52,12 @@ } } -// BarQueueAfter queues this (being constructed) bar to relplace -// runningBar after it has been completed. -func BarQueueAfter(runningBar *Bar) BarOption { - if runningBar == nil { - return nil - } - return func(s *bState) { - s.runningBar = runningBar +// BarQueueAfter puts this (being constructed) bar into the queue. +// BarPriority will be inherited from the argument bar. +// When argument bar completes or aborts queued bar replaces its place. +func BarQueueAfter(bar *Bar) BarOption { + return func(s *bState) { + s.waitBar = bar } } @@ -66,7 +65,7 @@ // on complete event. func BarRemoveOnComplete() BarOption { return func(s *bState) { - s.dropOnComplete = true + s.rmOnComplete = true } } @@ -79,47 +78,81 @@ // BarFillerOnComplete replaces bar's filler with message, on complete event. func BarFillerOnComplete(message string) BarOption { return BarFillerMiddleware(func(base BarFiller) BarFiller { - return BarFillerFunc(func(w io.Writer, reqWidth int, st decor.Statistics) { + return BarFillerFunc(func(w io.Writer, st decor.Statistics) error { if st.Completed { - io.WriteString(w, message) - } else { - base.Fill(w, reqWidth, st) + _, err := io.WriteString(w, message) + return err } + return base.Fill(w, st) }) }) } // BarFillerMiddleware provides a way to augment the underlying BarFiller. func BarFillerMiddleware(middle func(BarFiller) BarFiller) BarOption { - return func(s *bState) { - s.middleware = middle + if middle == nil { + return nil + } + return func(s *bState) { + s.filler = middle(s.filler) } } // BarPriority sets bar's priority. Zero is highest priority, i.e. bar -// will be on top. If `BarReplaceOnComplete` option is supplied, this -// option is ignored. +// will be on top. This option isn't effective with `BarQueueAfter` option. func BarPriority(priority int) BarOption { return func(s *bState) { s.priority = priority } } -// BarExtender provides a way to extend bar to the next new line. -func BarExtender(filler BarFiller) BarOption { +// BarExtender extends bar with arbitrary lines. Provided BarFiller will be +// called at each render/flush cycle. Any lines written to the underlying +// io.Writer will extend the bar either in above (rev = true) or below +// (rev = false) direction. +func BarExtender(filler BarFiller, rev bool) BarOption { if filler == nil { return nil } - return func(s *bState) { - s.extender = makeExtenderFunc(filler) - } -} - -func makeExtenderFunc(filler BarFiller) extenderFunc { + if f, ok := filler.(BarFillerFunc); ok && f == nil { + return nil + } + fn := makeExtenderFunc(filler, rev) + return func(s *bState) { + s.extender = fn + } +} + +func makeExtenderFunc(filler BarFiller, rev bool) extenderFunc { buf := new(bytes.Buffer) - 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")) + base := func(stat decor.Statistics, rows ...io.Reader) ([]io.Reader, error) { + err := filler.Fill(buf, stat) + if err != nil { + buf.Reset() + return rows, err + } + for { + line, err := buf.ReadBytes('\n') + if err != nil { + buf.Reset() + break + } + rows = append(rows, bytes.NewReader(line)) + } + return rows, err + } + if !rev { + return base + } + return func(stat decor.Statistics, rows ...io.Reader) ([]io.Reader, error) { + rows, err := base(stat, rows...) + if err != nil { + return rows, err + } + for left, right := 0, len(rows)-1; left < right; left, right = left+1, right-1 { + rows[left], rows[right] = rows[right], rows[left] + } + return rows, err } } @@ -138,16 +171,34 @@ } } -// BarOptional will invoke provided option only when pick is true. -func BarOptional(option BarOption, pick bool) BarOption { - return BarOptOn(option, internal.Predicate(pick)) -} - -// BarOptOn will invoke provided option only when higher order predicate -// evaluates to true. +// BarOptional will return provided option only when cond is true. +func BarOptional(option BarOption, cond bool) BarOption { + if cond { + return option + } + return nil +} + +// BarOptOn will return provided option only when predicate evaluates to true. func BarOptOn(option BarOption, predicate func() bool) BarOption { if predicate() { return option } return nil } + +// BarFuncOptional will call option and return its value only when cond is true. +func BarFuncOptional(option func() BarOption, cond bool) BarOption { + if cond { + return option() + } + return nil +} + +// BarFuncOptOn will call option and return its value only when predicate evaluates to true. +func BarFuncOptOn(option func() BarOption, predicate func() bool) BarOption { + if predicate() { + return option() + } + return nil +} diff --git a/bar_test.go b/bar_test.go index 1f0067c..f92b91a 100644 --- a/bar_test.go +++ b/bar_test.go @@ -2,123 +2,208 @@ import ( "bytes" + "context" "fmt" - "io/ioutil" + "io" "strings" - "sync/atomic" "testing" "time" "unicode/utf8" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) func TestBarCompleted(t *testing.T) { - p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(ioutil.Discard)) + p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) total := 80 bar := p.AddBar(int64(total)) - var count int - for !bar.Completed() { - time.Sleep(10 * time.Millisecond) - bar.Increment() - count++ - } - - p.Wait() - if count != total { - t.Errorf("got count: %d, expected %d\n", count, total) - } + if bar.Completed() { + t.Error("expected bar not to complete") + } + + bar.IncrBy(total) + + if !bar.Completed() { + t.Error("expected bar to complete") + } + + p.Wait() +} + +func TestBarAborted(t *testing.T) { + p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) + total := 80 + bar := p.AddBar(int64(total)) + + if bar.Aborted() { + t.Error("expected bar not to be aborted") + } + + bar.Abort(false) + + if !bar.Aborted() { + t.Error("expected bar to be aborted") + } + + p.Wait() +} + +func TestBarSetTotal(t *testing.T) { + p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) + bar := p.AddBar(0) + + bar.SetTotal(0, false) + if bar.Completed() { + t.Error("expected bar not to complete") + } + + bar.SetTotal(0, true) + if !bar.Completed() { + t.Error("expected bar to complete") + } + + p.Wait() +} + +func TestBarEnableTriggerCompleteZeroBar(t *testing.T) { + p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) + bar := p.AddBar(0) // never complete bar + + if bar.Completed() { + t.Error("expected bar not to complete") + } + + // Calling bar.SetTotal(0, true) has same effect + // but this one is more concise and intuitive + bar.EnableTriggerComplete() + + if !bar.Completed() { + t.Error("expected bar to complete") + } + + p.Wait() +} + +func TestBarEnableTriggerCompleteAndIncrementBefore(t *testing.T) { + p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) + bar := p.AddBar(0) // never complete bar + + targetTotal := int64(80) + + for _, f := range []func(){ + func() { bar.SetTotal(40, false) }, + func() { bar.IncrBy(60) }, + func() { bar.SetTotal(targetTotal, false) }, + func() { bar.IncrBy(20) }, + } { + f() + if bar.Completed() { + t.Error("expected bar not to complete") + } + } + + bar.EnableTriggerComplete() + + if !bar.Completed() { + t.Error("expected bar to complete") + } + + if current := bar.Current(); current != targetTotal { + t.Errorf("Expected current: %d, got: %d", targetTotal, current) + } + + p.Wait() +} + +func TestBarEnableTriggerCompleteAndIncrementAfter(t *testing.T) { + p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) + bar := p.AddBar(0) // never complete bar + + targetTotal := int64(80) + + for _, f := range []func(){ + func() { bar.SetTotal(40, false) }, + func() { bar.IncrBy(60) }, + func() { bar.SetTotal(targetTotal, false) }, + func() { bar.EnableTriggerComplete() }, // disables any next SetTotal + func() { bar.SetTotal(100, true) }, // nop + } { + f() + if bar.Completed() { + t.Error("expected bar not to complete") + } + } + + bar.IncrBy(20) + + if !bar.Completed() { + t.Error("expected bar to complete") + } + + if current := bar.Current(); current != targetTotal { + t.Errorf("Expected current: %d, got: %d", targetTotal, current) + } + + p.Wait() } func TestBarID(t *testing.T) { - p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(ioutil.Discard)) + p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(io.Discard)) total := 100 wantID := 11 bar := p.AddBar(int64(total), mpb.BarID(wantID)) - go func() { - for i := 0; i < total; i++ { - time.Sleep(50 * time.Millisecond) - bar.Increment() - } - }() - gotID := bar.ID() if gotID != wantID { - t.Errorf("Expected bar id: %d, got %d\n", wantID, gotID) - } - - bar.Abort(true) + t.Errorf("Expected bar id: %d, got %d", wantID, gotID) + } + + bar.IncrBy(total) + p.Wait() } func TestBarSetRefill(t *testing.T) { var buf bytes.Buffer - - p := mpb.New(mpb.WithOutput(&buf), mpb.WithWidth(100)) + p := mpb.New( + mpb.WithWidth(100), + mpb.WithOutput(&buf), + mpb.WithAutoRefresh(), + ) total := 100 till := 30 - refillRune, _ := utf8.DecodeLastRuneInString(mpb.BarDefaultStyle) - - bar := p.AddBar(int64(total), mpb.BarFillerTrim()) - + refiller := "+" + + bar := p.New(int64(total), mpb.BarStyle().Refiller(refiller), mpb.BarFillerTrim()) + + bar.IncrBy(till) bar.SetRefill(int64(till)) - bar.IncrBy(till) - - for i := 0; i < total-till; i++ { - bar.Increment() - time.Sleep(10 * time.Millisecond) - } + bar.IncrBy(total - till) p.Wait() wantBar := fmt.Sprintf("[%s%s]", - strings.Repeat(string(refillRune), till-1), + strings.Repeat(refiller, till-1), strings.Repeat("=", total-till-1), ) - got := string(getLastLine(buf.Bytes())) + got := string(bytes.Split(buf.Bytes(), []byte("\n"))[0]) if !strings.Contains(got, wantBar) { - t.Errorf("Want bar: %q, got bar: %q\n", wantBar, got) - } -} - -func TestBarHas100PercentWithOnCompleteDecorator(t *testing.T) { - var buf bytes.Buffer - - p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(&buf)) - - total := 50 - - bar := p.AddBar(int64(total), - mpb.AppendDecorators( - decor.OnComplete( - decor.Percentage(), "done", - ), - ), - ) - - for i := 0; i < total; i++ { - bar.Increment() - time.Sleep(10 * time.Millisecond) - } - - p.Wait() - - hundred := "100 %" - if !bytes.Contains(buf.Bytes(), []byte(hundred)) { - t.Errorf("Bar's buffer does not contain: %q\n", hundred) + t.Errorf("Want bar: %q, got bar: %q", wantBar, got) } } func TestBarHas100PercentWithBarRemoveOnComplete(t *testing.T) { var buf bytes.Buffer - - p := mpb.New(mpb.WithWidth(80), mpb.WithOutput(&buf)) + p := mpb.New( + mpb.WithWidth(80), + mpb.WithOutput(&buf), + mpb.WithAutoRefresh(), + ) total := 50 @@ -127,131 +212,126 @@ mpb.AppendDecorators(decor.Percentage()), ) - for i := 0; i < total; i++ { - bar.Increment() - time.Sleep(10 * time.Millisecond) - } + bar.IncrBy(total) p.Wait() hundred := "100 %" if !bytes.Contains(buf.Bytes(), []byte(hundred)) { - t.Errorf("Bar's buffer does not contain: %q\n", hundred) + t.Errorf("Bar's buffer does not contain: %q", hundred) } } func TestBarStyle(t *testing.T) { var buf bytes.Buffer customFormat := "╢▌▌░╟" + runes := []rune(customFormat) total := 80 - p := mpb.New(mpb.WithWidth(total), mpb.WithOutput(&buf)) - bar := p.Add(int64(total), mpb.NewBarFiller(customFormat), mpb.BarFillerTrim()) - - for i := 0; i < total; i++ { - bar.Increment() - time.Sleep(10 * time.Millisecond) - } - - p.Wait() - - runes := []rune(customFormat) - wantBar := fmt.Sprintf("%s%s%s", - string(runes[0]), - strings.Repeat(string(runes[1]), total-2), - string(runes[len(runes)-1]), - ) - got := string(getLastLine(buf.Bytes())) - - if !strings.Contains(got, wantBar) { - t.Errorf("Want bar: %q:%d, got bar: %q:%d\n", wantBar, utf8.RuneCountInString(wantBar), got, utf8.RuneCountInString(got)) - } -} - -func TestBarPanicBeforeComplete(t *testing.T) { - var buf bytes.Buffer 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), - mpb.PrependDecorators(panicDecorator(panicMsg, - func(st decor.Statistics) bool { - if st.Current >= 42 { - atomic.AddUint32(&pCount, 1) - return true - } - return false - }, - )), - ) - - for i := 0; i < total; i++ { + mpb.WithOutput(&buf), + mpb.WithAutoRefresh(), + ) + bs := mpb.BarStyle() + bs = bs.Lbound(string(runes[0])) + bs = bs.Filler(string(runes[1])) + bs = bs.Tip(string(runes[2])) + bs = bs.Padding(string(runes[3])) + bs = bs.Rbound(string(runes[4])) + bar := p.New(int64(total), bs, mpb.BarFillerTrim()) + + bar.IncrBy(total) + + p.Wait() + + wantBar := fmt.Sprintf("%s%s%s%s", + string(runes[0]), + strings.Repeat(string(runes[1]), total-3), + string(runes[2]), + string(runes[4]), + ) + got := string(bytes.Split(buf.Bytes(), []byte("\n"))[0]) + + if !strings.Contains(got, wantBar) { + t.Errorf("Want bar: %q:%d, got bar: %q:%d", wantBar, utf8.RuneCountInString(wantBar), got, utf8.RuneCountInString(got)) + } +} + +func TestDecorStatisticsAvailableWidth(t *testing.T) { + ch := make(chan int, 2) + td1 := func(s decor.Statistics) string { + ch <- s.AvailableWidth + return strings.Repeat("0", 20) + } + td2 := func(s decor.Statistics) string { + ch <- s.AvailableWidth + return "" + } + ctx, cancel := context.WithCancel(context.Background()) + refresh := make(chan interface{}) + p := mpb.NewWithContext(ctx, + mpb.WithWidth(100), + mpb.WithManualRefresh(refresh), + mpb.WithOutput(io.Discard), + ) + _ = p.AddBar(0, + mpb.BarFillerTrim(), + mpb.PrependDecorators( + decor.Name(strings.Repeat("0", 20)), + decor.Meta( + decor.Any(td1), + func(s string) string { + return "\x1b[31;1m" + s + "\x1b[0m" + }, + ), + ), + mpb.AppendDecorators( + decor.Name(strings.Repeat("0", 20)), + decor.Any(td2), + ), + ) + refresh <- time.Now() + go func() { time.Sleep(10 * time.Millisecond) - bar.Increment() - } - - p.Wait() - - if pCount != 1 { - t.Errorf("Decor called after panic %d times\n", pCount-1) - } - - barStr := buf.String() - if !strings.Contains(barStr, panicMsg) { - t.Errorf("%q doesn't contain %q\n", barStr, panicMsg) - } -} - -func TestBarPanicAfterComplete(t *testing.T) { - var buf bytes.Buffer - 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), - mpb.PrependDecorators(panicDecorator(panicMsg, - func(st decor.Statistics) bool { - if st.Completed { - atomic.AddUint32(&pCount, 1) - return true - } - return false - }, - )), - ) - - for i := 0; i < total; i++ { - time.Sleep(10 * time.Millisecond) - bar.Increment() - } - - p.Wait() - - if pCount > 2 { - t.Error("Decor called after panic more than 2 times\n") - } - - barStr := buf.String() - if !strings.Contains(barStr, panicMsg) { - t.Errorf("%q doesn't contain %q\n", barStr, panicMsg) - } -} - -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 "" - }) -} + cancel() + }() + p.Wait() + + if availableWidth := <-ch; availableWidth != 80 { + t.Errorf("expected AvailableWidth %d got %d", 80, availableWidth) + } + + if availableWidth := <-ch; availableWidth != 40 { + t.Errorf("expected AvailableWidth %d got %d", 40, availableWidth) + } +} + +func TestBarQueueAfterBar(t *testing.T) { + shutdown := make(chan interface{}) + ctx, cancel := context.WithCancel(context.Background()) + p := mpb.NewWithContext(ctx, + mpb.WithOutput(io.Discard), + mpb.WithAutoRefresh(), + mpb.WithShutdownNotifier(shutdown), + ) + a := p.AddBar(100) + b := p.AddBar(100, mpb.BarQueueAfter(a)) + identity := map[*mpb.Bar]string{ + a: "a", + b: "b", + } + + a.IncrBy(100) + a.Wait() + cancel() + + bars := (<-shutdown).([]*mpb.Bar) + if l := len(bars); l != 1 { + t.Errorf("Expected len of bars: %d, got: %d", 1, l) + } + + p.Wait() + if bars[0] != b { + t.Errorf("Expected bars[0] == b, got: %s", identity[bars[0]]) + } +} diff --git a/barbench_test.go b/barbench_test.go index 76beece..77e0868 100644 --- a/barbench_test.go +++ b/barbench_test.go @@ -1,43 +1,89 @@ -package mpb +package mpb_test import ( - "io/ioutil" + "io" "testing" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" ) -func BenchmarkIncrSingleBar(b *testing.B) { - p := New(WithOutput(ioutil.Discard), WithWidth(80)) - bar := p.AddBar(int64(b.N)) +const total = 1000 + +func BenchmarkNopStyleB1(b *testing.B) { + bench(b, mpb.NopStyle(), false, 1) +} + +func BenchmarkNopStyleWithAutoRefreshB1(b *testing.B) { + bench(b, mpb.NopStyle(), true, 1) +} + +func BenchmarkNopStylesB2(b *testing.B) { + bench(b, mpb.NopStyle(), false, 2) +} + +func BenchmarkNopStylesWithAutoRefreshB2(b *testing.B) { + bench(b, mpb.NopStyle(), true, 2) +} + +func BenchmarkNopStylesB3(b *testing.B) { + bench(b, mpb.NopStyle(), false, 3) +} + +func BenchmarkNopStylesWithAutoRefreshB3(b *testing.B) { + bench(b, mpb.NopStyle(), true, 3) +} + +func BenchmarkBarStyleB1(b *testing.B) { + bench(b, mpb.BarStyle(), false, 1) +} + +func BenchmarkBarStyleWithAutoRefreshB1(b *testing.B) { + bench(b, mpb.BarStyle(), true, 1) +} + +func BenchmarkBarStylesB2(b *testing.B) { + bench(b, mpb.BarStyle(), false, 2) +} + +func BenchmarkBarStylesWithAutoRefreshB2(b *testing.B) { + bench(b, mpb.BarStyle(), true, 2) +} + +func BenchmarkBarStylesB3(b *testing.B) { + bench(b, mpb.BarStyle(), false, 3) +} + +func BenchmarkBarStylesWithAutoRefreshB3(b *testing.B) { + bench(b, mpb.BarStyle(), true, 3) +} + +func bench(b *testing.B, builder mpb.BarFillerBuilder, autoRefresh bool, n int) { + p := mpb.New( + mpb.WithWidth(100), + mpb.WithOutput(io.Discard), + mpb.ContainerOptional(mpb.WithAutoRefresh(), autoRefresh), + ) + defer p.Wait() + b.ResetTimer() for i := 0; i < b.N; i++ { + var bars []*mpb.Bar + for j := 0; j < n; j++ { + bars = append(bars, p.New(total, builder)) + switch j { + case n - 1: + complete(bars[j]) + default: + go complete(bars[j]) + } + } + for _, bar := range bars { + bar.Wait() + } + } +} + +func complete(bar *mpb.Bar) { + for i := 0; i < total; i++ { bar.Increment() } } - -func BenchmarkIncrSingleBarWhileIsNotCompleted(b *testing.B) { - p := New(WithOutput(ioutil.Discard), WithWidth(80)) - bar := p.AddBar(int64(b.N)) - for !bar.Completed() { - bar.Increment() - } -} - -func BenchmarkIncrSingleBarWithNameDecorator(b *testing.B) { - p := New(WithOutput(ioutil.Discard), WithWidth(80)) - bar := p.AddBar(int64(b.N), PrependDecorators(decor.Name("test"))) - for i := 0; i < b.N; i++ { - bar.Increment() - } -} - -func BenchmarkIncrSingleBarWithNameAndEwmaETADecorator(b *testing.B) { - p := New(WithOutput(ioutil.Discard), WithWidth(80)) - bar := p.AddBar(int64(b.N), - PrependDecorators(decor.Name("test")), - AppendDecorators(decor.EwmaETA(decor.ET_STYLE_GO, 60)), - ) - for i := 0; i < b.N; i++ { - bar.Increment() - } -} diff --git a/container_option.go b/container_option.go index b92c757..177620e 100644 --- a/container_option.go +++ b/container_option.go @@ -2,11 +2,8 @@ import ( "io" - "io/ioutil" "sync" "time" - - "github.com/vbauerster/mpb/v6/internal" ) // ContainerOption is a func option to alter default behavior of a bar @@ -33,10 +30,10 @@ } } -// WithRefreshRate overrides default 120ms refresh rate. +// WithRefreshRate overrides default 150ms refresh rate. func WithRefreshRate(d time.Duration) ContainerOption { return func(s *pState) { - s.rr = d + s.refreshRate = d } } @@ -44,7 +41,7 @@ // Refresh will occur upon receive value from provided ch. func WithManualRefresh(ch <-chan interface{}) ContainerOption { return func(s *pState) { - s.externalRefresh = ch + s.manualRC = ch } } @@ -54,28 +51,26 @@ // rendering will start as soon as provided chan is closed. func WithRenderDelay(ch <-chan struct{}) ContainerOption { return func(s *pState) { - s.renderDelay = ch + s.delayRC = ch } } -// WithShutdownNotifier provided chanel will be closed, after all bars -// have been rendered. -func WithShutdownNotifier(ch chan struct{}) ContainerOption { +// WithShutdownNotifier value of type `[]*mpb.Bar` will be send into provided +// channel upon container shutdown. +func WithShutdownNotifier(ch chan<- interface{}) ContainerOption { return func(s *pState) { s.shutdownNotifier = ch } } -// WithOutput overrides default os.Stdout output. 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. +// WithOutput overrides default os.Stdout output. If underlying io.Writer +// is not a terminal then auto refresh is disabled unless WithAutoRefresh +// option is set. func WithOutput(w io.Writer) ContainerOption { + if w == nil { + w = io.Discard + } return func(s *pState) { - if w == nil { - s.output = ioutil.Discard - s.outputDiscarded = true - return - } s.output = w } } @@ -83,30 +78,58 @@ // WithDebugOutput sets debug output. func WithDebugOutput(w io.Writer) ContainerOption { if w == nil { - return nil + w = io.Discard } return func(s *pState) { s.debugOut = w } } -// PopCompletedMode will pop and stop rendering completed bars. +// WithAutoRefresh force auto refresh regardless of what output is set to. +// Applicable only if not WithManualRefresh set. +func WithAutoRefresh() ContainerOption { + return func(s *pState) { + s.autoRefresh = true + } +} + +// PopCompletedMode pop completed bars out of progress container. +// In this mode completed bars get moved to the top and stop +// participating in rendering cycle. func PopCompletedMode() ContainerOption { return func(s *pState) { s.popCompleted = true } } -// ContainerOptional will invoke provided option only when pick is true. -func ContainerOptional(option ContainerOption, pick bool) ContainerOption { - return ContainerOptOn(option, internal.Predicate(pick)) +// ContainerOptional will return provided option only when cond is true. +func ContainerOptional(option ContainerOption, cond bool) ContainerOption { + if cond { + return option + } + return nil } -// ContainerOptOn will invoke provided option only when higher order -// predicate evaluates to true. +// ContainerOptOn will return provided option only when predicate evaluates to true. func ContainerOptOn(option ContainerOption, predicate func() bool) ContainerOption { if predicate() { return option } return nil } + +// ContainerFuncOptional will call option and return its value only when cond is true. +func ContainerFuncOptional(option func() ContainerOption, cond bool) ContainerOption { + if cond { + return option() + } + return nil +} + +// ContainerFuncOptOn will call option and return its value only when predicate evaluates to true. +func ContainerFuncOptOn(option func() ContainerOption, predicate func() bool) ContainerOption { + if predicate() { + return option() + } + return nil +} diff --git a/cwriter/cuuAndEd_construction_bench_test.go b/cwriter/cuuAndEd_construction_bench_test.go index af80be9..996d5a8 100644 --- a/cwriter/cuuAndEd_construction_bench_test.go +++ b/cwriter/cuuAndEd_construction_bench_test.go @@ -3,37 +3,43 @@ import ( "bytes" "fmt" - "io/ioutil" + "io" "strconv" "testing" ) +var ( + out = io.Discard + lines = 99 +) + func BenchmarkWithFprintf(b *testing.B) { - cuuAndEd := "\x1b[%dA\x1b[J" + verb := fmt.Sprintf("%s%%d%s", escOpen, cuuAndEd) + b.ResetTimer() for i := 0; i < b.N; i++ { - fmt.Fprintf(ioutil.Discard, cuuAndEd, 4) + fmt.Fprintf(out, verb, lines) } } func BenchmarkWithJoin(b *testing.B) { - bCuuAndEd := [][]byte{[]byte("\x1b["), []byte("A\x1b[J")} + bCuuAndEd := [][]byte{[]byte(escOpen), []byte(cuuAndEd)} for i := 0; i < b.N; i++ { - ioutil.Discard.Write(bytes.Join(bCuuAndEd, []byte(strconv.Itoa(4)))) + _, _ = out.Write(bytes.Join(bCuuAndEd, []byte(strconv.Itoa(lines)))) } } func BenchmarkWithAppend(b *testing.B) { - escOpen := []byte("\x1b[") - cuuAndEd := []byte("A\x1b[J") + escOpen := []byte(escOpen) + cuuAndEd := []byte(cuuAndEd) for i := 0; i < b.N; i++ { - ioutil.Discard.Write(append(strconv.AppendInt(escOpen, 4, 10), cuuAndEd...)) + _, _ = out.Write(append(strconv.AppendInt(escOpen, int64(lines), 10), cuuAndEd...)) } } -func BenchmarkWithCopy(b *testing.B) { - w := New(ioutil.Discard) - w.lineCount = 4 +func BenchmarkWithCurrentImpl(b *testing.B) { + w := New(out) + b.ResetTimer() for i := 0; i < b.N; i++ { - w.ansiCuuAndEd() + _ = w.ew.ansiCuuAndEd(out, lines) } } diff --git a/cwriter/util_bsd.go b/cwriter/util_bsd.go index 4e3564e..215643b 100644 --- a/cwriter/util_bsd.go +++ b/cwriter/util_bsd.go @@ -1,4 +1,4 @@ -// +build darwin dragonfly freebsd netbsd openbsd +//go:build darwin || dragonfly || freebsd || netbsd || openbsd package cwriter diff --git a/cwriter/util_linux.go b/cwriter/util_linux.go index 253f12d..7d0e761 100644 --- a/cwriter/util_linux.go +++ b/cwriter/util_linux.go @@ -1,4 +1,4 @@ -// +build aix linux +//go:build aix || linux package cwriter diff --git a/cwriter/util_solaris.go b/cwriter/util_solaris.go index 4b29ff5..981f574 100644 --- a/cwriter/util_solaris.go +++ b/cwriter/util_solaris.go @@ -1,4 +1,4 @@ -// +build solaris +//go:build solaris package cwriter diff --git a/cwriter/util_zos.go b/cwriter/util_zos.go new file mode 100644 index 0000000..5daf003 --- /dev/null +++ b/cwriter/util_zos.go @@ -0,0 +1,7 @@ +//go:build zos + +package cwriter + +import "golang.org/x/sys/unix" + +const ioctlReadTermios = unix.TCGETS diff --git a/cwriter/writer.go b/cwriter/writer.go index 1ade547..23a72d3 100644 --- a/cwriter/writer.go +++ b/cwriter/writer.go @@ -8,77 +8,52 @@ "strconv" ) -// ErrNotTTY not a TeleTYpewriter error. -var ErrNotTTY = errors.New("not a terminal") - -// http://ascii-table.com/ansi-escape-sequences.php +// https://github.com/dylanaraps/pure-sh-bible#cursor-movement 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 int - isTerminal bool -} +// ErrNotTTY not a TeleTYpewriter error. +var ErrNotTTY = errors.New("not a terminal") // New returns a new Writer with defaults. func New(out io.Writer) *Writer { - w := &Writer{out: out} + w := &Writer{ + Buffer: new(bytes.Buffer), + out: out, + termSize: func(_ int) (int, int, error) { + return -1, -1, ErrNotTTY + }, + } if f, ok := out.(*os.File); ok { w.fd = int(f.Fd()) - w.isTerminal = IsTerminal(w.fd) + if IsTerminal(w.fd) { + w.terminal = true + w.termSize = func(fd int) (int, int, error) { + return GetSize(fd) + } + } } + bb := make([]byte, 16) + w.ew = escWriter(bb[:copy(bb, []byte(escOpen))]) return w } -// Flush flushes the underlying buffer. -func (w *Writer) Flush(lineCount int) (err error) { - // some terminals interpret 'cursor up 0' as 'cursor up 1' - if w.lineCount > 0 { - err = w.clearLines() - if err != nil { - return - } - } - w.lineCount = lineCount - _, err = w.buf.WriteTo(w.out) - return +// IsTerminal tells whether underlying io.Writer is terminal. +func (w *Writer) IsTerminal() bool { + return w.terminal } -// Write appends the contents of p to the underlying buffer. -func (w *Writer) Write(p []byte) (n int, err error) { - return w.buf.Write(p) +// GetTermSize returns WxH of underlying terminal. +func (w *Writer) GetTermSize() (width, height int, err error) { + return w.termSize(w.fd) } -// WriteString writes string to the underlying buffer. -func (w *Writer) WriteString(s string) (n int, err error) { - return w.buf.WriteString(s) +type escWriter []byte + +func (b escWriter) ansiCuuAndEd(out io.Writer, n int) error { + b = strconv.AppendInt(b, int64(n), 10) + _, err := out.Write(append(b, []byte(cuuAndEd)...)) + return err } - -// ReadFrom reads from the provided io.Reader and writes to the -// underlying buffer. -func (w *Writer) ReadFrom(r io.Reader) (n int64, err error) { - return w.buf.ReadFrom(r) -} - -// GetWidth returns width of underlying terminal. -func (w *Writer) GetWidth() (int, error) { - if !w.isTerminal { - return -1, ErrNotTTY - } - tw, _, err := GetSize(w.fd) - return tw, err -} - -func (w *Writer) ansiCuuAndEd() (err error) { - buf := make([]byte, 8) - buf = strconv.AppendInt(buf[:copy(buf, escOpen)], int64(w.lineCount), 10) - _, err = w.out.Write(append(buf, cuuAndEd...)) - return -} diff --git a/cwriter/writer_posix.go b/cwriter/writer_posix.go index f54a5d0..e80d757 100644 --- a/cwriter/writer_posix.go +++ b/cwriter/writer_posix.go @@ -1,13 +1,35 @@ -// +build !windows +//go:build !windows package cwriter import ( + "bytes" + "io" + "golang.org/x/sys/unix" ) -func (w *Writer) clearLines() error { - return w.ansiCuuAndEd() +// Writer is a buffered terminal writer, which moves cursor N lines up +// on each flush except the first one, where N is a number of lines of +// a previous flush. +type Writer struct { + *bytes.Buffer + out io.Writer + ew escWriter + fd int + terminal bool + termSize func(int) (int, int, error) +} + +// Flush flushes the underlying buffer. +// It's caller's responsibility to pass correct number of lines. +func (w *Writer) Flush(lines int) error { + _, err := w.WriteTo(w.out) + // some terminals interpret 'cursor up 0' as 'cursor up 1' + if err == nil && lines > 0 { + err = w.ew.ansiCuuAndEd(w, lines) + } + return err } // GetSize returns the dimensions of the given terminal. diff --git a/cwriter/writer_windows.go b/cwriter/writer_windows.go index 1a69c81..44293f2 100644 --- a/cwriter/writer_windows.go +++ b/cwriter/writer_windows.go @@ -1,8 +1,10 @@ -// +build windows +//go:build windows package cwriter import ( + "bytes" + "io" "unsafe" "golang.org/x/sys/windows" @@ -15,10 +17,37 @@ procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") ) -func (w *Writer) clearLines() error { - if !w.isTerminal { +// Writer is a buffered terminal writer, which moves cursor N lines up +// on each flush except the first one, where N is a number of lines of +// a previous flush. +type Writer struct { + *bytes.Buffer + out io.Writer + ew escWriter + lines int + fd int + terminal bool + termSize func(int) (int, int, error) +} + +// Flush flushes the underlying buffer. +// It's caller's responsibility to pass correct number of lines. +func (w *Writer) Flush(lines int) error { + if w.lines > 0 { + err := w.clearLines(w.lines) + if err != nil { + return err + } + } + w.lines = lines + _, err := w.WriteTo(w.out) + return err +} + +func (w *Writer) clearLines(n int) error { + if !w.terminal { // hope it's cygwin or similar - return w.ansiCuuAndEd() + return w.ew.ansiCuuAndEd(w.out, n) } var info windows.ConsoleScreenBufferInfo @@ -26,7 +55,7 @@ return err } - info.CursorPosition.Y -= int16(w.lineCount) + info.CursorPosition.Y -= int16(n) if info.CursorPosition.Y < 0 { info.CursorPosition.Y = 0 } @@ -40,7 +69,7 @@ X: info.Window.Left, Y: info.CursorPosition.Y, } - count := uint32(info.Size.X) * uint32(w.lineCount) + count := uint32(info.Size.X) * uint32(n) _, _, _ = procFillConsoleOutputCharacter.Call( uintptr(w.fd), uintptr(' '), @@ -52,7 +81,6 @@ } // 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 diff --git a/decor/any.go b/decor/any.go index 39518f5..ca208d8 100644 --- a/decor/any.go +++ b/decor/any.go @@ -1,14 +1,14 @@ package decor -// Any decorator displays text, that can be changed during decorator's -// lifetime via provided DecorFunc. +var _ Decorator = any{} + +// Any decorator. +// Converts DecorFunc into Decorator. // // `fn` DecorFunc callback -// // `wcc` optional WC config -// func Any(fn DecorFunc, wcc ...WC) Decorator { - return &any{initWC(wcc...), fn} + return any{initWC(wcc...), fn} } type any struct { @@ -16,6 +16,6 @@ fn DecorFunc } -func (d *any) Decor(s Statistics) string { - return d.FormatMsg(d.fn(s)) +func (d any) Decor(s Statistics) (string, int) { + return d.Format(d.fn(s)) } diff --git a/decor/counters.go b/decor/counters.go index 4a5343d..0420275 100644 --- a/decor/counters.go +++ b/decor/counters.go @@ -2,13 +2,6 @@ import ( "fmt" - "strings" -) - -const ( - _ = iota - UnitKiB - UnitKB ) // CountersNoUnit is a wrapper around Counters with no unit param. @@ -17,55 +10,60 @@ } // CountersKibiByte is a wrapper around Counters with predefined unit -// UnitKiB (bytes/1024). +// as SizeB1024(0). func CountersKibiByte(pairFmt string, wcc ...WC) Decorator { - return Counters(UnitKiB, pairFmt, wcc...) + return Counters(SizeB1024(0), pairFmt, wcc...) } // CountersKiloByte is a wrapper around Counters with predefined unit -// UnitKB (bytes/1000). +// as SizeB1000(0). func CountersKiloByte(pairFmt string, wcc ...WC) Decorator { - return Counters(UnitKB, pairFmt, wcc...) + return Counters(SizeB1000(0), pairFmt, wcc...) } // Counters decorator with dynamic unit measure adjustment. // -// `unit` one of [0|UnitKiB|UnitKB] zero for no unit -// -// `pairFmt` printf compatible verbs for current and total pair -// -// `wcc` optional WC config -// -// pairFmt example if unit=UnitKB: -// +// `unit` one of [0|SizeB1024(0)|SizeB1000(0)] +// +// `pairFmt` printf compatible verbs for current and total +// +// `wcc` optional WC config +// +// pairFmt example if unit=SizeB1000(0): +// +// pairFmt="%d / %d" output: "1MB / 12MB" +// pairFmt="% d / % d" output: "1 MB / 12 MB" // pairFmt="%.1f / %.1f" output: "1.0MB / 12.0MB" // pairFmt="% .1f / % .1f" output: "1.0 MB / 12.0 MB" -// pairFmt="%d / %d" output: "1MB / 12MB" -// pairFmt="% d / % d" output: "1 MB / 12 MB" -// -func Counters(unit int, pairFmt string, wcc ...WC) Decorator { - 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: +// pairFmt="%f / %f" output: "1.000000MB / 12.000000MB" +// pairFmt="% f / % f" output: "1.000000 MB / 12.000000 MB" +func Counters(unit interface{}, pairFmt string, wcc ...WC) Decorator { + producer := func() DecorFunc { + switch unit.(type) { + case SizeB1024: + if pairFmt == "" { + pairFmt = "% d / % d" + } return func(s Statistics) string { return fmt.Sprintf(pairFmt, SizeB1024(s.Current), SizeB1024(s.Total)) } - case UnitKB: + case SizeB1000: + if pairFmt == "" { + pairFmt = "% d / % d" + } return func(s Statistics) string { return fmt.Sprintf(pairFmt, SizeB1000(s.Current), SizeB1000(s.Total)) } default: + if pairFmt == "" { + pairFmt = "%d / %d" + } return func(s Statistics) string { return fmt.Sprintf(pairFmt, s.Current, s.Total) } } } - return Any(producer(unit, pairFmt), wcc...) + return Any(producer(), wcc...) } // TotalNoUnit is a wrapper around Total with no unit param. @@ -74,56 +72,60 @@ } // TotalKibiByte is a wrapper around Total with predefined unit -// UnitKiB (bytes/1024). +// as SizeB1024(0). func TotalKibiByte(format string, wcc ...WC) Decorator { - return Total(UnitKiB, format, wcc...) + return Total(SizeB1024(0), format, wcc...) } // TotalKiloByte is a wrapper around Total with predefined unit -// UnitKB (bytes/1000). +// as SizeB1000(0). func TotalKiloByte(format string, wcc ...WC) Decorator { - return Total(UnitKB, format, wcc...) + return Total(SizeB1000(0), format, wcc...) } // Total decorator with dynamic unit measure adjustment. // -// `unit` one of [0|UnitKiB|UnitKB] zero for no unit +// `unit` one of [0|SizeB1024(0)|SizeB1000(0)] // // `format` printf compatible verb for Total // // `wcc` optional WC config // -// format example if unit=UnitKiB: -// +// format example if unit=SizeB1024(0): +// +// format="%d" output: "12MiB" +// format="% d" output: "12 MiB" // format="%.1f" output: "12.0MiB" // format="% .1f" output: "12.0 MiB" -// format="%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: +// format="%f" output: "12.000000MiB" +// format="% f" output: "12.000000 MiB" +func Total(unit interface{}, format string, wcc ...WC) Decorator { + producer := func() DecorFunc { + switch unit.(type) { + case SizeB1024: + if format == "" { + format = "% d" + } return func(s Statistics) string { return fmt.Sprintf(format, SizeB1024(s.Total)) } - case UnitKB: + case SizeB1000: + if format == "" { + format = "% d" + } return func(s Statistics) string { return fmt.Sprintf(format, SizeB1000(s.Total)) } default: + if format == "" { + format = "%d" + } return func(s Statistics) string { return fmt.Sprintf(format, s.Total) } } } - return Any(producer(unit, format), wcc...) + return Any(producer(), wcc...) } // CurrentNoUnit is a wrapper around Current with no unit param. @@ -132,56 +134,60 @@ } // CurrentKibiByte is a wrapper around Current with predefined unit -// UnitKiB (bytes/1024). +// as SizeB1024(0). func CurrentKibiByte(format string, wcc ...WC) Decorator { - return Current(UnitKiB, format, wcc...) + return Current(SizeB1024(0), format, wcc...) } // CurrentKiloByte is a wrapper around Current with predefined unit -// UnitKB (bytes/1000). +// as SizeB1000(0). func CurrentKiloByte(format string, wcc ...WC) Decorator { - return Current(UnitKB, format, wcc...) + return Current(SizeB1000(0), format, wcc...) } // Current decorator with dynamic unit measure adjustment. // -// `unit` one of [0|UnitKiB|UnitKB] zero for no unit +// `unit` one of [0|SizeB1024(0)|SizeB1000(0)] // // `format` printf compatible verb for Current // // `wcc` optional WC config // -// format example if unit=UnitKiB: -// +// format example if unit=SizeB1024(0): +// +// format="%d" output: "12MiB" +// format="% d" output: "12 MiB" // format="%.1f" output: "12.0MiB" // format="% .1f" output: "12.0 MiB" -// format="%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: +// format="%f" output: "12.000000MiB" +// format="% f" output: "12.000000 MiB" +func Current(unit interface{}, format string, wcc ...WC) Decorator { + producer := func() DecorFunc { + switch unit.(type) { + case SizeB1024: + if format == "" { + format = "% d" + } return func(s Statistics) string { return fmt.Sprintf(format, SizeB1024(s.Current)) } - case UnitKB: + case SizeB1000: + if format == "" { + format = "% d" + } return func(s Statistics) string { return fmt.Sprintf(format, SizeB1000(s.Current)) } default: + if format == "" { + format = "%d" + } return func(s Statistics) string { return fmt.Sprintf(format, s.Current) } } } - return Any(producer(unit, format), wcc...) + return Any(producer(), wcc...) } // InvertedCurrentNoUnit is a wrapper around InvertedCurrent with no unit param. @@ -190,54 +196,58 @@ } // InvertedCurrentKibiByte is a wrapper around InvertedCurrent with predefined unit -// UnitKiB (bytes/1024). +// as SizeB1024(0). func InvertedCurrentKibiByte(format string, wcc ...WC) Decorator { - return InvertedCurrent(UnitKiB, format, wcc...) + return InvertedCurrent(SizeB1024(0), format, wcc...) } // InvertedCurrentKiloByte is a wrapper around InvertedCurrent with predefined unit -// UnitKB (bytes/1000). +// as SizeB1000(0). func InvertedCurrentKiloByte(format string, wcc ...WC) Decorator { - return InvertedCurrent(UnitKB, format, wcc...) + return InvertedCurrent(SizeB1000(0), format, wcc...) } // InvertedCurrent decorator with dynamic unit measure adjustment. // -// `unit` one of [0|UnitKiB|UnitKB] zero for no unit +// `unit` one of [0|SizeB1024(0)|SizeB1000(0)] // // `format` printf compatible verb for InvertedCurrent // // `wcc` optional WC config // -// format example if unit=UnitKiB: -// +// format example if unit=SizeB1024(0): +// +// format="%d" output: "12MiB" +// format="% d" output: "12 MiB" // format="%.1f" output: "12.0MiB" // format="% .1f" output: "12.0 MiB" -// format="%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: +// format="%f" output: "12.000000MiB" +// format="% f" output: "12.000000 MiB" +func InvertedCurrent(unit interface{}, format string, wcc ...WC) Decorator { + producer := func() DecorFunc { + switch unit.(type) { + case SizeB1024: + if format == "" { + format = "% d" + } return func(s Statistics) string { return fmt.Sprintf(format, SizeB1024(s.Total-s.Current)) } - case UnitKB: + case SizeB1000: + if format == "" { + format = "% d" + } return func(s Statistics) string { return fmt.Sprintf(format, SizeB1000(s.Total-s.Current)) } default: + if format == "" { + format = "%d" + } return func(s Statistics) string { return fmt.Sprintf(format, s.Total-s.Current) } } } - return Any(producer(unit, format), wcc...) -} + return Any(producer(), wcc...) +} diff --git a/decor/decorator.go b/decor/decorator.go index e81fae3..6bec115 100644 --- a/decor/decorator.go +++ b/decor/decorator.go @@ -4,33 +4,31 @@ "fmt" "time" - "github.com/acarl005/stripansi" "github.com/mattn/go-runewidth" ) const ( - // DidentRight bit specifies identation direction. - // |foo |b | With DidentRight - // | foo| b| Without DidentRight - DidentRight = 1 << iota + // DindentRight sets indentation from right to left. + // + // |foo |b | DindentRight is set + // | foo| b| DindentRight is not set + DindentRight = 1 << iota - // DextraSpace bit adds extra space, makes sense with DSyncWidth only. - // When DidentRight bit set, the space will be added to the right, - // otherwise to the left. + // DextraSpace bit adds extra indentation space. DextraSpace // DSyncWidth bit enables same column width synchronization. // Effective with multiple bars only. DSyncWidth - // DSyncWidthR is shortcut for DSyncWidth|DidentRight - DSyncWidthR = DSyncWidth | DidentRight + // DSyncWidthR is shortcut for DSyncWidth|DindentRight + DSyncWidthR = DSyncWidth | DindentRight // DSyncSpace is shortcut for DSyncWidth|DextraSpace DSyncSpace = DSyncWidth | DextraSpace - // DSyncSpaceR is shortcut for DSyncWidth|DextraSpace|DidentRight - DSyncSpaceR = DSyncWidth | DextraSpace | DidentRight + // DSyncSpaceR is shortcut for DSyncWidth|DextraSpace|DindentRight + DSyncSpaceR = DSyncWidth | DextraSpace | DindentRight ) // TimeStyle enum. @@ -44,15 +42,17 @@ ET_STYLE_MMSS ) -// Statistics consists of progress related statistics, that Decorator -// may need. +// Statistics contains fields which are necessary for implementing +// `decor.Decorator` and `mpb.BarFiller` interfaces. type Statistics struct { + AvailableWidth int // calculated width initially equal to terminal width + RequestedWidth int // width set by `mpb.WithWidth` ID int - AvailableWidth int Total int64 Current int64 Refill int64 Completed bool + Aborted bool } // Decorator interface. @@ -64,13 +64,13 @@ // `DecorFunc` into a `Decorator` interface by using provided // `func Any(DecorFunc, ...WC) Decorator`. type Decorator interface { - Configurator Synchronizer - Decor(Statistics) string + Formatter + Decor(Statistics) (str string, viewWidth int) } // DecorFunc func type. -// To be used with `func Any`(DecorFunc, ...WC) Decorator`. +// To be used with `func Any(DecorFunc, ...WC) Decorator`. type DecorFunc func(Statistics) string // Synchronizer interface. @@ -80,10 +80,12 @@ Sync() (chan int, bool) } -// Configurator interface. -type Configurator interface { - GetConf() WC - SetConf(WC) +// Formatter interface. +// Format method needs to be called from within Decorator.Decor method +// in order to format string according to decor.WC settings. +// No need to implement manually as long as decor.WC is embedded. +type Formatter interface { + Format(string) (_ string, width int) } // Wrapper interface. @@ -91,7 +93,7 @@ // it is necessary to implement this interface to retain functionality // of built-in Decorator. type Wrapper interface { - Base() Decorator + Unwrap() Decorator } // EwmaDecorator interface. @@ -111,7 +113,7 @@ // If decorator needs to be notified once upon bar shutdown event, so // this is the right interface to implement. type ShutdownListener interface { - Shutdown() + OnShutdown() } // Global convenience instances of WC with sync width bit set. @@ -133,28 +135,28 @@ 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 { - pureWidth := runewidth.StringWidth(msg) - stripWidth := runewidth.StringWidth(stripansi.Strip(msg)) - maxCell := wc.W +// Format should be called by any Decorator implementation. +// Returns formatted string and its view (visual) width. +func (wc WC) Format(str string) (string, int) { + width := runewidth.StringWidth(str) + if wc.W > width { + width = wc.W + } else if (wc.C & DextraSpace) != 0 { + width++ + } if (wc.C & DSyncWidth) != 0 { - cellCount := stripWidth - if (wc.C & DextraSpace) != 0 { - cellCount++ - } - wc.wsync <- cellCount - maxCell = <-wc.wsync + wc.wsync <- width + width = <-wc.wsync } - return wc.fill(msg, maxCell+(pureWidth-stripWidth)) + return wc.fill(str, width), width } // Init initializes width related config. func (wc *WC) Init() WC { - wc.fill = runewidth.FillLeft - if (wc.C & DidentRight) != 0 { + if (wc.C & DindentRight) != 0 { wc.fill = runewidth.FillRight + } else { + wc.fill = runewidth.FillLeft } if (wc.C & DSyncWidth) != 0 { // it's deliberate choice to override wsync on each Init() call, @@ -165,21 +167,11 @@ } // Sync is implementation of Synchronizer interface. -func (wc *WC) Sync() (chan int, bool) { +func (wc WC) Sync() (chan int, bool) { if (wc.C&DSyncWidth) != 0 && wc.wsync == nil { panic(fmt.Sprintf("%T is not initialized", wc)) } return wc.wsync, (wc.C & DSyncWidth) != 0 -} - -// GetConf is implementation of Configurator interface. -func (wc *WC) GetConf() WC { - return *wc -} - -// SetConf is implementation of Configurator interface. -func (wc *WC) SetConf(conf WC) { - *wc = conf.Init() } func initWC(wcc ...WC) WC { diff --git a/decor/doc.go b/decor/doc.go index bfbb82e..d41aa50 100644 --- a/decor/doc.go +++ b/decor/doc.go @@ -1,20 +1,19 @@ -// Package decor provides common decorators for "github.com/vbauerster/mpb/v6" 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/v8" module. +// +// Some decorators returned by this package might have a closure state. It is ok to use +// decorators concurrently, unless you share the same decorator among multiple +// *mpb.Bar instances. To avoid data races, create new decorator per *mpb.Bar instance. +// +// Don't: +// +// p := mpb.New() +// name := decor.Name("bar") +// p.AddBar(100, mpb.AppendDecorators(name)) +// p.AddBar(100, mpb.AppendDecorators(name)) +// +// Do: +// +// p := mpb.New() +// p.AddBar(100, mpb.AppendDecorators(decor.Name("bar1"))) +// p.AddBar(100, mpb.AppendDecorators(decor.Name("bar2"))) package decor diff --git a/decor/elapsed.go b/decor/elapsed.go index e389f15..f3ed7a8 100644 --- a/decor/elapsed.go +++ b/decor/elapsed.go @@ -9,7 +9,6 @@ // `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS] // // `wcc` optional WC config -// func Elapsed(style TimeStyle, wcc ...WC) Decorator { return NewElapsed(style, time.Now(), wcc...) } @@ -18,16 +17,15 @@ // // `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS] // -// `startTime` start time +// `start` start time // // `wcc` optional WC config -// -func NewElapsed(style TimeStyle, startTime time.Time, wcc ...WC) Decorator { +func NewElapsed(style TimeStyle, start time.Time, wcc ...WC) Decorator { var msg string producer := chooseTimeProducer(style) fn := func(s Statistics) string { - if !s.Completed { - msg = producer(time.Since(startTime)) + if !s.Completed && !s.Aborted { + msg = producer(time.Since(start)) } return msg } diff --git a/decor/eta.go b/decor/eta.go index d03caa7..64ec74a 100644 --- a/decor/eta.go +++ b/decor/eta.go @@ -8,6 +8,13 @@ "github.com/VividCortex/ewma" ) +var ( + _ Decorator = (*movingAverageETA)(nil) + _ EwmaDecorator = (*movingAverageETA)(nil) + _ Decorator = (*averageETA)(nil) + _ AverageDecorator = (*averageETA)(nil) +) + // TimeNormalizer interface. Implementors could be passed into // MovingAverageETA, in order to affect i.e. normalize its output. type TimeNormalizer interface { @@ -22,18 +29,22 @@ return f(src) } -// EwmaETA exponential-weighted-moving-average based ETA decorator. -// For this decorator to work correctly you have to measure each -// iteration's duration and pass it to the -// *Bar.DecoratorEwmaUpdate(time.Duration) method after each increment. +// EwmaETA exponential-weighted-moving-average based ETA decorator. For this +// decorator to work correctly you have to measure each iteration's duration +// and pass it to one of the (*Bar).EwmaIncr... family methods. func EwmaETA(style TimeStyle, age float64, wcc ...WC) Decorator { + return EwmaNormalizedETA(style, age, nil, wcc...) +} + +// EwmaNormalizedETA same as EwmaETA but with TimeNormalizer option. +func EwmaNormalizedETA(style TimeStyle, age float64, normalizer TimeNormalizer, wcc ...WC) Decorator { var average ewma.MovingAverage if age == 0 { average = ewma.NewMovingAverage() } else { average = ewma.NewMovingAverage(age) } - return MovingAverageETA(style, NewThreadSafeMovingAverage(average), nil, wcc...) + return MovingAverageETA(style, average, normalizer, wcc...) } // MovingAverageETA decorator relies on MovingAverage implementation to calculate its average. @@ -45,38 +56,47 @@ // `normalizer` available implementations are [FixedIntervalTimeNormalizer|MaxTolerateTimeNormalizer] // // `wcc` optional WC config -// func MovingAverageETA(style TimeStyle, average ewma.MovingAverage, normalizer TimeNormalizer, wcc ...WC) Decorator { + if average == nil { + average = NewMedian() + } d := &movingAverageETA{ WC: initWC(wcc...), + producer: chooseTimeProducer(style), average: average, normalizer: normalizer, - producer: chooseTimeProducer(style), } return d } type movingAverageETA struct { WC + producer func(time.Duration) string average ewma.MovingAverage normalizer TimeNormalizer - producer func(time.Duration) string -} - -func (d *movingAverageETA) Decor(s Statistics) string { + zDur time.Duration +} + +func (d *movingAverageETA) Decor(s Statistics) (string, int) { v := math.Round(d.average.Value()) remaining := time.Duration((s.Total - s.Current) * int64(v)) if d.normalizer != nil { remaining = d.normalizer.Normalize(remaining) } - return d.FormatMsg(d.producer(remaining)) + return d.Format(d.producer(remaining)) } func (d *movingAverageETA) EwmaUpdate(n int64, dur time.Duration) { - durPerItem := float64(dur) / float64(n) + if n <= 0 { + d.zDur += dur + return + } + durPerItem := float64(d.zDur+dur) / float64(n) if math.IsInf(durPerItem, 0) || math.IsNaN(durPerItem) { + d.zDur += dur return } + d.zDur = 0 d.average.Add(durPerItem) } @@ -85,7 +105,6 @@ // `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS] // // `wcc` optional WC config -// func AverageETA(style TimeStyle, wcc ...WC) Decorator { return NewAverageETA(style, time.Now(), nil, wcc...) } @@ -94,16 +113,15 @@ // // `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS] // -// `startTime` start time +// `start` start time // // `normalizer` available implementations are [FixedIntervalTimeNormalizer|MaxTolerateTimeNormalizer] // // `wcc` optional WC config -// -func NewAverageETA(style TimeStyle, startTime time.Time, normalizer TimeNormalizer, wcc ...WC) Decorator { +func NewAverageETA(style TimeStyle, start time.Time, normalizer TimeNormalizer, wcc ...WC) Decorator { d := &averageETA{ WC: initWC(wcc...), - startTime: startTime, + start: start, normalizer: normalizer, producer: chooseTimeProducer(style), } @@ -112,26 +130,26 @@ type averageETA struct { WC - startTime time.Time + start time.Time normalizer TimeNormalizer producer func(time.Duration) string } -func (d *averageETA) Decor(s Statistics) string { +func (d *averageETA) Decor(s Statistics) (string, int) { var remaining time.Duration if s.Current != 0 { - durPerItem := float64(time.Since(d.startTime)) / float64(s.Current) + durPerItem := float64(time.Since(d.start)) / float64(s.Current) durPerItem = math.Round(durPerItem) remaining = time.Duration((s.Total - s.Current) * int64(durPerItem)) if d.normalizer != nil { remaining = d.normalizer.Normalize(remaining) } } - return d.FormatMsg(d.producer(remaining)) -} - -func (d *averageETA) AverageAdjust(startTime time.Time) { - d.startTime = startTime + return d.Format(d.producer(remaining)) +} + +func (d *averageETA) AverageAdjust(start time.Time) { + d.start = start } // MaxTolerateTimeNormalizer returns implementation of TimeNormalizer. @@ -196,8 +214,7 @@ } default: return func(remaining time.Duration) string { - // strip off nanoseconds - return ((remaining / time.Second) * time.Second).String() - } - } -} + return remaining.Truncate(time.Second).String() + } + } +} diff --git a/decor/merge.go b/decor/merge.go deleted file mode 100644 index e41406a..0000000 --- a/decor/merge.go +++ /dev/null @@ -1,107 +0,0 @@ -package decor - -import ( - "strings" - - "github.com/acarl005/stripansi" - "github.com/mattn/go-runewidth" -) - -// Merge wraps its decorator argument with intention to sync width -// with several decorators of another bar. Visual example: -// -// +----+--------+---------+--------+ -// | B1 | MERGE(D, P1, Pn) | -// +----+--------+---------+--------+ -// | B2 | D0 | D1 | Dn | -// +----+--------+---------+--------+ -// -func Merge(decorator Decorator, placeholders ...WC) Decorator { - if _, ok := decorator.Sync(); !ok || len(placeholders) == 0 { - return decorator - } - md := &mergeDecorator{ - Decorator: decorator, - wc: decorator.GetConf(), - placeHolders: make([]*placeHolderDecorator, len(placeholders)), - } - decorator.SetConf(WC{}) - for i, wc := range placeholders { - if (wc.C & DSyncWidth) == 0 { - return decorator - } - md.placeHolders[i] = &placeHolderDecorator{wc.Init()} - } - return md -} - -type mergeDecorator struct { - Decorator - wc WC - placeHolders []*placeHolderDecorator -} - -func (d *mergeDecorator) GetConf() WC { - return d.wc -} - -func (d *mergeDecorator) SetConf(conf WC) { - d.wc = conf.Init() -} - -func (d *mergeDecorator) MergeUnwrap() []Decorator { - decorators := make([]Decorator, len(d.placeHolders)) - for i, ph := range d.placeHolders { - decorators[i] = ph - } - return decorators -} - -func (d *mergeDecorator) Sync() (chan int, bool) { - return d.wc.Sync() -} - -func (d *mergeDecorator) Base() Decorator { - return d.Decorator -} - -func (d *mergeDecorator) Decor(s Statistics) string { - msg := d.Decorator.Decor(s) - pureWidth := runewidth.StringWidth(msg) - stripWidth := runewidth.StringWidth(stripansi.Strip(msg)) - cellCount := stripWidth - if (d.wc.C & DextraSpace) != 0 { - cellCount++ - } - - 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++ { - ph := d.placeHolders[i] - width := pw - diff - if (ph.WC.C & DextraSpace) != 0 { - width-- - if width < 0 { - width = 0 - } - } - max := runewidth.StringWidth(ph.FormatMsg(strings.Repeat(" ", width))) - total += max - diff = max - pw - } - - d.wc.wsync <- pw + rem - max := <-d.wc.wsync - return d.wc.fill(msg, max+total+(pureWidth-stripWidth)) -} - -type placeHolderDecorator struct { - WC -} - -func (d *placeHolderDecorator) Decor(Statistics) string { - return "" -} diff --git a/decor/meta.go b/decor/meta.go new file mode 100644 index 0000000..0045a31 --- /dev/null +++ b/decor/meta.go @@ -0,0 +1,34 @@ +package decor + +var ( + _ Decorator = metaWrapper{} + _ Wrapper = metaWrapper{} +) + +// Meta wrap decorator. +// Provided fn is supposed to wrap output of given decorator +// with meta information like ANSI escape codes for example. +// Primary usage intention is to set SGR display attributes. +// +// `decorator` Decorator to wrap +// `fn` func to apply meta information +func Meta(decorator Decorator, fn func(string) string) Decorator { + if decorator == nil { + return nil + } + return metaWrapper{decorator, fn} +} + +type metaWrapper struct { + Decorator + fn func(string) string +} + +func (d metaWrapper) Decor(s Statistics) (string, int) { + str, width := d.Decorator.Decor(s) + return d.fn(str), width +} + +func (d metaWrapper) Unwrap() Decorator { + return d.Decorator +} diff --git a/decor/moving_average.go b/decor/moving_average.go index 50ac9c3..165ef1e 100644 --- a/decor/moving_average.go +++ b/decor/moving_average.go @@ -5,6 +5,12 @@ "sync" "github.com/VividCortex/ewma" +) + +var ( + _ ewma.MovingAverage = (*threadSafeMovingAverage)(nil) + _ ewma.MovingAverage = (*medianWindow)(nil) + _ sort.Interface = (*medianWindow)(nil) ) type threadSafeMovingAverage struct { @@ -64,5 +70,5 @@ // NewMedian is fixed last 3 samples median MovingAverage. func NewMedian() ewma.MovingAverage { - return NewThreadSafeMovingAverage(new(medianWindow)) + return new(medianWindow) } diff --git a/decor/name.go b/decor/name.go index 3af3112..31ac123 100644 --- a/decor/name.go +++ b/decor/name.go @@ -6,7 +6,6 @@ // `str` string to display // // `wcc` optional WC config -// func Name(str string, wcc ...WC) Decorator { return Any(func(Statistics) string { return str }, wcc...) } diff --git a/decor/on_abort.go b/decor/on_abort.go new file mode 100644 index 0000000..3e35ddf --- /dev/null +++ b/decor/on_abort.go @@ -0,0 +1,68 @@ +package decor + +var ( + _ Decorator = onAbortWrapper{} + _ Wrapper = onAbortWrapper{} + _ Decorator = onAbortMetaWrapper{} + _ Wrapper = onAbortMetaWrapper{} +) + +// OnAbort wrap decorator. +// Displays provided message on abort event. +// Has no effect if bar.Abort(true) is called. +// +// `decorator` Decorator to wrap +// `message` message to display +func OnAbort(decorator Decorator, message string) Decorator { + if decorator == nil { + return nil + } + return onAbortWrapper{decorator, message} +} + +type onAbortWrapper struct { + Decorator + msg string +} + +func (d onAbortWrapper) Decor(s Statistics) (string, int) { + if s.Aborted { + return d.Format(d.msg) + } + return d.Decorator.Decor(s) +} + +func (d onAbortWrapper) Unwrap() Decorator { + return d.Decorator +} + +// OnAbortMeta wrap decorator. +// Provided fn is supposed to wrap output of given decorator +// with meta information like ANSI escape codes for example. +// Primary usage intention is to set SGR display attributes. +// +// `decorator` Decorator to wrap +// `fn` func to apply meta information +func OnAbortMeta(decorator Decorator, fn func(string) string) Decorator { + if decorator == nil { + return nil + } + return onAbortMetaWrapper{decorator, fn} +} + +type onAbortMetaWrapper struct { + Decorator + fn func(string) string +} + +func (d onAbortMetaWrapper) Decor(s Statistics) (string, int) { + if s.Aborted { + str, width := d.Decorator.Decor(s) + return d.fn(str), width + } + return d.Decorator.Decor(s) +} + +func (d onAbortMetaWrapper) Unwrap() Decorator { + return d.Decorator +} diff --git a/decor/on_compete_or_on_abort.go b/decor/on_compete_or_on_abort.go new file mode 100644 index 0000000..f9ca841 --- /dev/null +++ b/decor/on_compete_or_on_abort.go @@ -0,0 +1,21 @@ +package decor + +// OnCompleteOrOnAbort wrap decorator. +// Displays provided message on complete or on abort event. +// +// `decorator` Decorator to wrap +// `message` message to display +func OnCompleteOrOnAbort(decorator Decorator, message string) Decorator { + return OnComplete(OnAbort(decorator, message), message) +} + +// OnCompleteMetaOrOnAbortMeta wrap decorator. +// Provided fn is supposed to wrap output of given decorator +// with meta information like ANSI escape codes for example. +// Primary usage intention is to set SGR display attributes. +// +// `decorator` Decorator to wrap +// `fn` func to apply meta information +func OnCompleteMetaOrOnAbortMeta(decorator Decorator, fn func(string) string) Decorator { + return OnCompleteMeta(OnAbortMeta(decorator, fn), fn) +} diff --git a/decor/on_complete.go b/decor/on_complete.go index f46b19a..f18b5a6 100644 --- a/decor/on_complete.go +++ b/decor/on_complete.go @@ -1,22 +1,22 @@ package decor -// OnComplete returns decorator, which wraps provided decorator, with -// sole purpose to display provided message on complete event. +var ( + _ Decorator = onCompleteWrapper{} + _ Wrapper = onCompleteWrapper{} + _ Decorator = onCompleteMetaWrapper{} + _ Wrapper = onCompleteMetaWrapper{} +) + +// OnComplete wrap decorator. +// Displays provided message on complete event. // // `decorator` Decorator to wrap -// -// `message` message to display on complete event -// +// `message` message to display func OnComplete(decorator Decorator, message string) Decorator { - d := &onCompleteWrapper{ - Decorator: decorator, - msg: message, + if decorator == nil { + return nil } - if md, ok := decorator.(*mergeDecorator); ok { - d.Decorator, md.Decorator = md.Decorator, d - return md - } - return d + return onCompleteWrapper{decorator, message} } type onCompleteWrapper struct { @@ -24,14 +24,44 @@ msg string } -func (d *onCompleteWrapper) Decor(s Statistics) string { +func (d onCompleteWrapper) Decor(s Statistics) (string, int) { if s.Completed { - wc := d.GetConf() - return wc.FormatMsg(d.msg) + return d.Format(d.msg) } return d.Decorator.Decor(s) } -func (d *onCompleteWrapper) Base() Decorator { +func (d onCompleteWrapper) Unwrap() Decorator { return d.Decorator } + +// OnCompleteMeta wrap decorator. +// Provided fn is supposed to wrap output of given decorator +// with meta information like ANSI escape codes for example. +// Primary usage intention is to set SGR display attributes. +// +// `decorator` Decorator to wrap +// `fn` func to apply meta information +func OnCompleteMeta(decorator Decorator, fn func(string) string) Decorator { + if decorator == nil { + return nil + } + return onCompleteMetaWrapper{decorator, fn} +} + +type onCompleteMetaWrapper struct { + Decorator + fn func(string) string +} + +func (d onCompleteMetaWrapper) Decor(s Statistics) (string, int) { + if s.Completed { + str, width := d.Decorator.Decor(s) + return d.fn(str), width + } + return d.Decorator.Decor(s) +} + +func (d onCompleteMetaWrapper) Unwrap() Decorator { + return d.Decorator +} diff --git a/decor/on_condition.go b/decor/on_condition.go new file mode 100644 index 0000000..f4626c3 --- /dev/null +++ b/decor/on_condition.go @@ -0,0 +1,51 @@ +package decor + +// OnCondition applies decorator only if a condition is true. +// +// `decorator` Decorator +// +// `cond` bool +func OnCondition(decorator Decorator, cond bool) Decorator { + return Conditional(cond, decorator, nil) +} + +// OnPredicate applies decorator only if a predicate evaluates to true. +// +// `decorator` Decorator +// +// `predicate` func() bool +func OnPredicate(decorator Decorator, predicate func() bool) Decorator { + return Predicative(predicate, decorator, nil) +} + +// Conditional returns decorator `a` if condition is true, otherwise +// decorator `b`. +// +// `cond` bool +// +// `a` Decorator +// +// `b` Decorator +func Conditional(cond bool, a, b Decorator) Decorator { + if cond { + return a + } else { + return b + } +} + +// Predicative returns decorator `a` if predicate evaluates to true, +// otherwise decorator `b`. +// +// `predicate` func() bool +// +// `a` Decorator +// +// `b` Decorator +func Predicative(predicate func() bool, a, b Decorator) Decorator { + if predicate() { + return a + } else { + return b + } +} diff --git a/decor/percentage.go b/decor/percentage.go index f4922bb..547117b 100644 --- a/decor/percentage.go +++ b/decor/percentage.go @@ -2,34 +2,39 @@ import ( "fmt" - "io" "strconv" - "github.com/vbauerster/mpb/v6/internal" + "github.com/vbauerster/mpb/v8/internal" ) + +var _ fmt.Formatter = percentageType(0) type percentageType float64 func (s percentageType) Format(st fmt.State, verb rune) { - var prec int + prec := -1 switch verb { - case 'd': - case 's': - prec = -1 - default: + case 'f', 'e', 'E': + prec = 6 // default prec of fmt.Printf("%f|%e|%E") + fallthrough + case 'b', 'g', 'G', 'x', 'X': if p, ok := st.Precision(); ok { prec = p - } else { - prec = 6 } + default: + verb, prec = 'f', 0 } - io.WriteString(st, strconv.FormatFloat(float64(s), 'f', prec, 64)) - + b := strconv.AppendFloat(make([]byte, 0, 16), float64(s), byte(verb), prec, 64) if st.Flag(' ') { - io.WriteString(st, " ") + b = append(b, ' ', '%') + } else { + b = append(b, '%') } - io.WriteString(st, "%") + _, err := st.Write(b) + if err != nil { + panic(err) + } } // Percentage returns percentage decorator. It's a wrapper of NewPercentage. @@ -39,19 +44,24 @@ // NewPercentage percentage decorator with custom format string. // +// `format` printf compatible verb +// +// `wcc` optional WC config +// // format examples: // +// format="%d" output: "1%" +// format="% d" output: "1 %" // format="%.1f" output: "1.0%" // format="% .1f" output: "1.0 %" -// format="%d" output: "1%" -// format="% d" output: "1 %" -// +// format="%f" output: "1.000000%" +// format="% f" output: "1.000000 %" func NewPercentage(format string, wcc ...WC) Decorator { if format == "" { format = "% d" } f := func(s Statistics) string { - p := internal.Percentage(s.Total, s.Current, 100) + p := internal.Percentage(uint(s.Total), uint(s.Current), 100) return fmt.Sprintf(format, percentageType(p)) } return Any(f, wcc...) diff --git a/decor/percentage_test.go b/decor/percentage_test.go new file mode 100644 index 0000000..a3ad458 --- /dev/null +++ b/decor/percentage_test.go @@ -0,0 +1,110 @@ +package decor + +import ( + "fmt" + "testing" +) + +func TestPercentageType(t *testing.T) { + cases := map[string]struct { + value float64 + verb string + expected string + }{ + "10 %d": {10, "%d", "10%"}, + "10 %s": {10, "%s", "10%"}, + "10 %f": {10, "%f", "10.000000%"}, + "10 %.6f": {10, "%.6f", "10.000000%"}, + "10 %.0f": {10, "%.0f", "10%"}, + "10 %.1f": {10, "%.1f", "10.0%"}, + "10 %.2f": {10, "%.2f", "10.00%"}, + "10 %.3f": {10, "%.3f", "10.000%"}, + + "10 % d": {10, "% d", "10 %"}, + "10 % s": {10, "% s", "10 %"}, + "10 % f": {10, "% f", "10.000000 %"}, + "10 % .6f": {10, "% .6f", "10.000000 %"}, + "10 % .0f": {10, "% .0f", "10 %"}, + "10 % .1f": {10, "% .1f", "10.0 %"}, + "10 % .2f": {10, "% .2f", "10.00 %"}, + "10 % .3f": {10, "% .3f", "10.000 %"}, + + "10.5 %d": {10.5, "%d", "10%"}, + "10.5 %s": {10.5, "%s", "10%"}, + "10.5 %f": {10.5, "%f", "10.500000%"}, + "10.5 %.6f": {10.5, "%.6f", "10.500000%"}, + "10.5 %.0f": {10.5, "%.0f", "10%"}, + "10.5 %.1f": {10.5, "%.1f", "10.5%"}, + "10.5 %.2f": {10.5, "%.2f", "10.50%"}, + "10.5 %.3f": {10.5, "%.3f", "10.500%"}, + + "10.5 % d": {10.5, "% d", "10 %"}, + "10.5 % s": {10.5, "% s", "10 %"}, + "10.5 % f": {10.5, "% f", "10.500000 %"}, + "10.5 % .6f": {10.5, "% .6f", "10.500000 %"}, + "10.5 % .0f": {10.5, "% .0f", "10 %"}, + "10.5 % .1f": {10.5, "% .1f", "10.5 %"}, + "10.5 % .2f": {10.5, "% .2f", "10.50 %"}, + "10.5 % .3f": {10.5, "% .3f", "10.500 %"}, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := fmt.Sprintf(tc.verb, percentageType(tc.value)) + if got != tc.expected { + t.Fatalf("expected: %q, got: %q\n", tc.expected, got) + } + }) + } +} + +func TestPercentageDecor(t *testing.T) { + cases := []struct { + name string + fmt string + current int64 + total int64 + expected string + }{ + { + name: "tot:100 cur:0 fmt:none", + fmt: "", + current: 0, + total: 100, + expected: "0 %", + }, + { + name: "tot:100 cur:10 fmt:none", + fmt: "", + current: 10, + total: 100, + expected: "10 %", + }, + { + name: "tot:100 cur:10 fmt:%.2f", + fmt: "%.2f", + current: 10, + total: 100, + expected: "10.00%", + }, + { + name: "tot:99 cur:10 fmt:%.2f", + fmt: "%.2f", + current: 11, + total: 99, + expected: "11.11%", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + decor := NewPercentage(tc.fmt) + stat := Statistics{ + Total: tc.total, + Current: tc.current, + } + res, _ := decor.Decor(stat) + if res != tc.expected { + t.Fatalf("expected: %q, got: %q\n", tc.expected, res) + } + }) + } +} diff --git a/decor/size_type.go b/decor/size_type.go index e4b9740..d9950b6 100644 --- a/decor/size_type.go +++ b/decor/size_type.go @@ -2,13 +2,18 @@ import ( "fmt" - "io" - "math" "strconv" ) //go:generate stringer -type=SizeB1024 -trimprefix=_i //go:generate stringer -type=SizeB1000 -trimprefix=_ + +var ( + _ fmt.Formatter = SizeB1024(0) + _ fmt.Stringer = SizeB1024(0) + _ fmt.Formatter = SizeB1000(0) + _ fmt.Stringer = SizeB1000(0) +) const ( _ib SizeB1024 = iota + 1 @@ -24,17 +29,17 @@ type SizeB1024 int64 func (self SizeB1024) Format(st fmt.State, verb rune) { - var prec int + prec := -1 switch verb { - case 'd': - case 's': - prec = -1 - default: + case 'f', 'e', 'E': + prec = 6 // default prec of fmt.Printf("%f|%e|%E") + fallthrough + case 'b', 'g', 'G', 'x', 'X': if p, ok := st.Precision(); ok { prec = p - } else { - prec = 6 } + default: + verb, prec = 'f', 0 } var unit SizeB1024 @@ -47,16 +52,19 @@ 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)) - + b := strconv.AppendFloat(make([]byte, 0, 24), float64(self)/float64(unit), byte(verb), prec, 64) if st.Flag(' ') { - io.WriteString(st, " ") + b = append(b, ' ') } - io.WriteString(st, unit.String()) + b = append(b, []byte(unit.String())...) + _, err := st.Write(b) + if err != nil { + panic(err) + } } const ( @@ -73,17 +81,17 @@ type SizeB1000 int64 func (self SizeB1000) Format(st fmt.State, verb rune) { - var prec int + prec := -1 switch verb { - case 'd': - case 's': - prec = -1 - default: + case 'f', 'e', 'E': + prec = 6 // default prec of fmt.Printf("%f|%e|%E") + fallthrough + case 'b', 'g', 'G', 'x', 'X': if p, ok := st.Precision(); ok { prec = p - } else { - prec = 6 } + default: + verb, prec = 'f', 0 } var unit SizeB1000 @@ -96,14 +104,17 @@ 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)) - + b := strconv.AppendFloat(make([]byte, 0, 24), float64(self)/float64(unit), byte(verb), prec, 64) if st.Flag(' ') { - io.WriteString(st, " ") + b = append(b, ' ') } - io.WriteString(st, unit.String()) + b = append(b, []byte(unit.String())...) + _, err := st.Write(b) + if err != nil { + panic(err) + } } diff --git a/decor/size_type_test.go b/decor/size_type_test.go index 8601e25..117d9fb 100644 --- a/decor/size_type_test.go +++ b/decor/size_type_test.go @@ -11,37 +11,66 @@ verb string expected string }{ - "verb %f": {12345678, "%f", "11.773756MiB"}, - "verb %.0f": {12345678, "%.0f", "12MiB"}, - "verb %.1f": {12345678, "%.1f", "11.8MiB"}, - "verb %.2f": {12345678, "%.2f", "11.77MiB"}, - "verb %.3f": {12345678, "%.3f", "11.774MiB"}, - + "verb %d": {12345678, "%d", "12MiB"}, + "verb %s": {12345678, "%s", "12MiB"}, + "verb %f": {12345678, "%f", "11.773756MiB"}, + "verb %.6f": {12345678, "%.6f", "11.773756MiB"}, + "verb %.0f": {12345678, "%.0f", "12MiB"}, + "verb %.1f": {12345678, "%.1f", "11.8MiB"}, + "verb %.2f": {12345678, "%.2f", "11.77MiB"}, + "verb %.3f": {12345678, "%.3f", "11.774MiB"}, + "verb % d": {12345678, "% d", "12 MiB"}, + "verb % s": {12345678, "% s", "12 MiB"}, "verb % f": {12345678, "% f", "11.773756 MiB"}, + "verb % .6f": {12345678, "% .6f", "11.773756 MiB"}, "verb % .0f": {12345678, "% .0f", "12 MiB"}, "verb % .1f": {12345678, "% .1f", "11.8 MiB"}, "verb % .2f": {12345678, "% .2f", "11.77 MiB"}, "verb % .3f": {12345678, "% .3f", "11.774 MiB"}, - "1000 %f": {1000, "%f", "1000.000000b"}, - "1000 %d": {1000, "%d", "1000b"}, - "1000 %s": {1000, "%s", "1000b"}, - "1024 %f": {1024, "%f", "1.000000KiB"}, - "1024 %d": {1024, "%d", "1KiB"}, - "1024 %.1f": {1024, "%.1f", "1.0KiB"}, - "1024 %s": {1024, "%s", "1KiB"}, - "3*MiB+140KiB %f": {3*int64(_iMiB) + 140*int64(_iKiB), "%f", "3.136719MiB"}, - "3*MiB+140KiB %d": {3*int64(_iMiB) + 140*int64(_iKiB), "%d", "3MiB"}, - "3*MiB+140KiB %.1f": {3*int64(_iMiB) + 140*int64(_iKiB), "%.1f", "3.1MiB"}, - "3*MiB+140KiB %s": {3*int64(_iMiB) + 140*int64(_iKiB), "%s", "3.13671875MiB"}, - "2*GiB %f": {2 * int64(_iGiB), "%f", "2.000000GiB"}, - "2*GiB %d": {2 * int64(_iGiB), "%d", "2GiB"}, - "2*GiB %.1f": {2 * int64(_iGiB), "%.1f", "2.0GiB"}, - "2*GiB %s": {2 * int64(_iGiB), "%s", "2GiB"}, - "4*TiB %f": {4 * int64(_iTiB), "%f", "4.000000TiB"}, - "4*TiB %d": {4 * int64(_iTiB), "%d", "4TiB"}, - "4*TiB %.1f": {4 * int64(_iTiB), "%.1f", "4.0TiB"}, - "4*TiB %s": {4 * int64(_iTiB), "%s", "4TiB"}, + "1000 %d": {1000, "%d", "1000b"}, + "1000 %s": {1000, "%s", "1000b"}, + "1000 %f": {1000, "%f", "1000.000000b"}, + "1000 %.6f": {1000, "%.6f", "1000.000000b"}, + "1000 %.0f": {1000, "%.0f", "1000b"}, + "1000 %.1f": {1000, "%.1f", "1000.0b"}, + "1000 %.2f": {1000, "%.2f", "1000.00b"}, + "1000 %.3f": {1000, "%.3f", "1000.000b"}, + "1024 %d": {1024, "%d", "1KiB"}, + "1024 %s": {1024, "%s", "1KiB"}, + "1024 %f": {1024, "%f", "1.000000KiB"}, + "1024 %.6f": {1024, "%.6f", "1.000000KiB"}, + "1024 %.0f": {1024, "%.0f", "1KiB"}, + "1024 %.1f": {1024, "%.1f", "1.0KiB"}, + "1024 %.2f": {1024, "%.2f", "1.00KiB"}, + "1024 %.3f": {1024, "%.3f", "1.000KiB"}, + + "3*MiB+100KiB %d": {3*int64(_iMiB) + 100*int64(_iKiB), "%d", "3MiB"}, + "3*MiB+100KiB %s": {3*int64(_iMiB) + 100*int64(_iKiB), "%s", "3MiB"}, + "3*MiB+100KiB %f": {3*int64(_iMiB) + 100*int64(_iKiB), "%f", "3.097656MiB"}, + "3*MiB+100KiB %.6f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.6f", "3.097656MiB"}, + "3*MiB+100KiB %.0f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.0f", "3MiB"}, + "3*MiB+100KiB %.1f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.1f", "3.1MiB"}, + "3*MiB+100KiB %.2f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.2f", "3.10MiB"}, + "3*MiB+100KiB %.3f": {3*int64(_iMiB) + 100*int64(_iKiB), "%.3f", "3.098MiB"}, + + "2*GiB %d": {2 * int64(_iGiB), "%d", "2GiB"}, + "2*GiB %s": {2 * int64(_iGiB), "%s", "2GiB"}, + "2*GiB %f": {2 * int64(_iGiB), "%f", "2.000000GiB"}, + "2*GiB %.6f": {2 * int64(_iGiB), "%.6f", "2.000000GiB"}, + "2*GiB %.0f": {2 * int64(_iGiB), "%.0f", "2GiB"}, + "2*GiB %.1f": {2 * int64(_iGiB), "%.1f", "2.0GiB"}, + "2*GiB %.2f": {2 * int64(_iGiB), "%.2f", "2.00GiB"}, + "2*GiB %.3f": {2 * int64(_iGiB), "%.3f", "2.000GiB"}, + + "4*TiB %d": {4 * int64(_iTiB), "%d", "4TiB"}, + "4*TiB %s": {4 * int64(_iTiB), "%s", "4TiB"}, + "4*TiB %f": {4 * int64(_iTiB), "%f", "4.000000TiB"}, + "4*TiB %.6f": {4 * int64(_iTiB), "%.6f", "4.000000TiB"}, + "4*TiB %.0f": {4 * int64(_iTiB), "%.0f", "4TiB"}, + "4*TiB %.1f": {4 * int64(_iTiB), "%.1f", "4.0TiB"}, + "4*TiB %.2f": {4 * int64(_iTiB), "%.2f", "4.00TiB"}, + "4*TiB %.3f": {4 * int64(_iTiB), "%.3f", "4.000TiB"}, } for name, tc := range cases { t.Run(name, func(t *testing.T) { @@ -59,37 +88,66 @@ verb string expected string }{ - "verb %f": {12345678, "%f", "12.345678MB"}, - "verb %.0f": {12345678, "%.0f", "12MB"}, - "verb %.1f": {12345678, "%.1f", "12.3MB"}, - "verb %.2f": {12345678, "%.2f", "12.35MB"}, - "verb %.3f": {12345678, "%.3f", "12.346MB"}, - + "verb %d": {12345678, "%d", "12MB"}, + "verb %s": {12345678, "%s", "12MB"}, + "verb %f": {12345678, "%f", "12.345678MB"}, + "verb %.6f": {12345678, "%.6f", "12.345678MB"}, + "verb %.0f": {12345678, "%.0f", "12MB"}, + "verb %.1f": {12345678, "%.1f", "12.3MB"}, + "verb %.2f": {12345678, "%.2f", "12.35MB"}, + "verb %.3f": {12345678, "%.3f", "12.346MB"}, + "verb % d": {12345678, "% d", "12 MB"}, + "verb % s": {12345678, "% s", "12 MB"}, "verb % f": {12345678, "% f", "12.345678 MB"}, + "verb % .6f": {12345678, "% .6f", "12.345678 MB"}, "verb % .0f": {12345678, "% .0f", "12 MB"}, "verb % .1f": {12345678, "% .1f", "12.3 MB"}, "verb % .2f": {12345678, "% .2f", "12.35 MB"}, "verb % .3f": {12345678, "% .3f", "12.346 MB"}, - "1000 %f": {1000, "%f", "1.000000KB"}, - "1000 %d": {1000, "%d", "1KB"}, - "1000 %s": {1000, "%s", "1KB"}, - "1024 %f": {1024, "%f", "1.024000KB"}, - "1024 %d": {1024, "%d", "1KB"}, - "1024 %.1f": {1024, "%.1f", "1.0KB"}, - "1024 %s": {1024, "%s", "1.024KB"}, - "3*MB+140*KB %f": {3*int64(_MB) + 140*int64(_KB), "%f", "3.140000MB"}, - "3*MB+140*KB %d": {3*int64(_MB) + 140*int64(_KB), "%d", "3MB"}, - "3*MB+140*KB %.1f": {3*int64(_MB) + 140*int64(_KB), "%.1f", "3.1MB"}, - "3*MB+140*KB %s": {3*int64(_MB) + 140*int64(_KB), "%s", "3.14MB"}, - "2*GB %f": {2 * int64(_GB), "%f", "2.000000GB"}, - "2*GB %d": {2 * int64(_GB), "%d", "2GB"}, - "2*GB %.1f": {2 * int64(_GB), "%.1f", "2.0GB"}, - "2*GB %s": {2 * int64(_GB), "%s", "2GB"}, - "4*TB %f": {4 * int64(_TB), "%f", "4.000000TB"}, - "4*TB %d": {4 * int64(_TB), "%d", "4TB"}, - "4*TB %.1f": {4 * int64(_TB), "%.1f", "4.0TB"}, - "4*TB %s": {4 * int64(_TB), "%s", "4TB"}, + "1000 %d": {1000, "%d", "1KB"}, + "1000 %s": {1000, "%s", "1KB"}, + "1000 %f": {1000, "%f", "1.000000KB"}, + "1000 %.6f": {1000, "%.6f", "1.000000KB"}, + "1000 %.0f": {1000, "%.0f", "1KB"}, + "1000 %.1f": {1000, "%.1f", "1.0KB"}, + "1000 %.2f": {1000, "%.2f", "1.00KB"}, + "1000 %.3f": {1000, "%.3f", "1.000KB"}, + "1024 %d": {1024, "%d", "1KB"}, + "1024 %s": {1024, "%s", "1KB"}, + "1024 %f": {1024, "%f", "1.024000KB"}, + "1024 %.6f": {1024, "%.6f", "1.024000KB"}, + "1024 %.0f": {1024, "%.0f", "1KB"}, + "1024 %.1f": {1024, "%.1f", "1.0KB"}, + "1024 %.2f": {1024, "%.2f", "1.02KB"}, + "1024 %.3f": {1024, "%.3f", "1.024KB"}, + + "3*MB+100*KB %d": {3*int64(_MB) + 100*int64(_KB), "%d", "3MB"}, + "3*MB+100*KB %s": {3*int64(_MB) + 100*int64(_KB), "%s", "3MB"}, + "3*MB+100*KB %f": {3*int64(_MB) + 100*int64(_KB), "%f", "3.100000MB"}, + "3*MB+100*KB %.6f": {3*int64(_MB) + 100*int64(_KB), "%.6f", "3.100000MB"}, + "3*MB+100*KB %.0f": {3*int64(_MB) + 100*int64(_KB), "%.0f", "3MB"}, + "3*MB+100*KB %.1f": {3*int64(_MB) + 100*int64(_KB), "%.1f", "3.1MB"}, + "3*MB+100*KB %.2f": {3*int64(_MB) + 100*int64(_KB), "%.2f", "3.10MB"}, + "3*MB+100*KB %.3f": {3*int64(_MB) + 100*int64(_KB), "%.3f", "3.100MB"}, + + "2*GB %d": {2 * int64(_GB), "%d", "2GB"}, + "2*GB %s": {2 * int64(_GB), "%s", "2GB"}, + "2*GB %f": {2 * int64(_GB), "%f", "2.000000GB"}, + "2*GB %.6f": {2 * int64(_GB), "%.6f", "2.000000GB"}, + "2*GB %.0f": {2 * int64(_GB), "%.0f", "2GB"}, + "2*GB %.1f": {2 * int64(_GB), "%.1f", "2.0GB"}, + "2*GB %.2f": {2 * int64(_GB), "%.2f", "2.00GB"}, + "2*GB %.3f": {2 * int64(_GB), "%.3f", "2.000GB"}, + + "4*TB %d": {4 * int64(_TB), "%d", "4TB"}, + "4*TB %s": {4 * int64(_TB), "%s", "4TB"}, + "4*TB %f": {4 * int64(_TB), "%f", "4.000000TB"}, + "4*TB %.6f": {4 * int64(_TB), "%.6f", "4.000000TB"}, + "4*TB %.0f": {4 * int64(_TB), "%.0f", "4TB"}, + "4*TB %.1f": {4 * int64(_TB), "%.1f", "4.0TB"}, + "4*TB %.2f": {4 * int64(_TB), "%.2f", "4.00TB"}, + "4*TB %.3f": {4 * int64(_TB), "%.3f", "4.000TB"}, } for name, tc := range cases { t.Run(name, func(t *testing.T) { diff --git a/decor/speed.go b/decor/speed.go index 634edab..b643e10 100644 --- a/decor/speed.go +++ b/decor/speed.go @@ -9,11 +9,17 @@ "github.com/VividCortex/ewma" ) +var ( + _ Decorator = (*movingAverageSpeed)(nil) + _ EwmaDecorator = (*movingAverageSpeed)(nil) + _ Decorator = (*averageSpeed)(nil) + _ AverageDecorator = (*averageSpeed)(nil) +) + // FmtAsSpeed adds "/s" to the end of the input formatter. To be // used with SizeB1000 or SizeB1024 types, for example: // // fmt.Printf("%.1f", FmtAsSpeed(SizeB1024(2048))) -// func FmtAsSpeed(input fmt.Formatter) fmt.Formatter { return &speedFormatter{input} } @@ -22,29 +28,31 @@ fmt.Formatter } -func (self *speedFormatter) Format(st fmt.State, verb rune) { - self.Formatter.Format(st, verb) - io.WriteString(st, "/s") +func (s *speedFormatter) Format(st fmt.State, verb rune) { + s.Formatter.Format(st, verb) + _, err := io.WriteString(st, "/s") + if err != nil { + panic(err) + } } // EwmaSpeed exponential-weighted-moving-average based speed decorator. -// For this decorator to work correctly you have to measure each -// iteration's duration and pass it to the -// *Bar.DecoratorEwmaUpdate(time.Duration) method after each increment. -func EwmaSpeed(unit int, format string, age float64, wcc ...WC) Decorator { +// For this decorator to work correctly you have to measure each iteration's +// duration and pass it to one of the (*Bar).EwmaIncr... family methods. +func EwmaSpeed(unit interface{}, format string, age float64, wcc ...WC) Decorator { var average ewma.MovingAverage if age == 0 { average = ewma.NewMovingAverage() } else { average = ewma.NewMovingAverage(age) } - return MovingAverageSpeed(unit, format, NewThreadSafeMovingAverage(average), wcc...) + return MovingAverageSpeed(unit, format, average, wcc...) } // MovingAverageSpeed decorator relies on MovingAverage implementation // to calculate its average. // -// `unit` one of [0|UnitKiB|UnitKB] zero for no unit +// `unit` one of [0|SizeB1024(0)|SizeB1000(0)] // // `format` printf compatible verb for value, like "%f" or "%d" // @@ -54,19 +62,15 @@ // // format examples: // -// unit=UnitKiB, format="%.1f" output: "1.0MiB/s" -// unit=UnitKiB, format="% .1f" output: "1.0 MiB/s" -// unit=UnitKB, format="%.1f" output: "1.0MB/s" -// unit=UnitKB, format="% .1f" output: "1.0 MB/s" -// -func MovingAverageSpeed(unit int, format string, average ewma.MovingAverage, wcc ...WC) Decorator { - if format == "" { - format = "%.0f" - } +// unit=SizeB1024(0), format="%.1f" output: "1.0MiB/s" +// unit=SizeB1024(0), format="% .1f" output: "1.0 MiB/s" +// unit=SizeB1000(0), format="%.1f" output: "1.0MB/s" +// unit=SizeB1000(0), format="% .1f" output: "1.0 MB/s" +func MovingAverageSpeed(unit interface{}, format string, average ewma.MovingAverage, wcc ...WC) Decorator { d := &movingAverageSpeed{ WC: initWC(wcc...), + producer: chooseSpeedProducer(unit, format), average: average, - producer: chooseSpeedProducer(unit, format), } return d } @@ -75,95 +79,105 @@ WC producer func(float64) string average ewma.MovingAverage - msg string + zDur time.Duration } -func (d *movingAverageSpeed) Decor(s Statistics) string { - if !s.Completed { - var speed float64 - if v := d.average.Value(); v > 0 { - speed = 1 / v - } - d.msg = d.producer(speed * 1e9) +func (d *movingAverageSpeed) Decor(_ Statistics) (string, int) { + var str string + // ewma implementation may return 0 before accumulating certain number of samples + if v := d.average.Value(); v != 0 { + str = d.producer(1e9 / v) + } else { + str = d.producer(0) } - return d.FormatMsg(d.msg) + return d.Format(str) } func (d *movingAverageSpeed) EwmaUpdate(n int64, dur time.Duration) { - durPerByte := float64(dur) / float64(n) - if math.IsInf(durPerByte, 0) || math.IsNaN(durPerByte) { + if n <= 0 { + d.zDur += dur return } + durPerByte := float64(d.zDur+dur) / float64(n) + if math.IsInf(durPerByte, 0) || math.IsNaN(durPerByte) { + d.zDur += dur + return + } + d.zDur = 0 d.average.Add(durPerByte) } // AverageSpeed decorator with dynamic unit measure adjustment. It's // a wrapper of NewAverageSpeed. -func AverageSpeed(unit int, format string, wcc ...WC) Decorator { +func AverageSpeed(unit interface{}, format string, wcc ...WC) Decorator { return NewAverageSpeed(unit, format, time.Now(), wcc...) } // NewAverageSpeed decorator with dynamic unit measure adjustment and // user provided start time. // -// `unit` one of [0|UnitKiB|UnitKB] zero for no unit +// `unit` one of [0|SizeB1024(0)|SizeB1000(0)] // // `format` printf compatible verb for value, like "%f" or "%d" // -// `startTime` start time +// `start` start time // // `wcc` optional WC config // // format examples: // -// unit=UnitKiB, format="%.1f" output: "1.0MiB/s" -// unit=UnitKiB, format="% .1f" output: "1.0 MiB/s" -// unit=UnitKB, format="%.1f" output: "1.0MB/s" -// unit=UnitKB, format="% .1f" output: "1.0 MB/s" -// -func NewAverageSpeed(unit int, format string, startTime time.Time, wcc ...WC) Decorator { - if format == "" { - format = "%.0f" - } +// unit=SizeB1024(0), format="%.1f" output: "1.0MiB/s" +// unit=SizeB1024(0), format="% .1f" output: "1.0 MiB/s" +// unit=SizeB1000(0), format="%.1f" output: "1.0MB/s" +// unit=SizeB1000(0), format="% .1f" output: "1.0 MB/s" +func NewAverageSpeed(unit interface{}, format string, start time.Time, wcc ...WC) Decorator { d := &averageSpeed{ - WC: initWC(wcc...), - startTime: startTime, - producer: chooseSpeedProducer(unit, format), + WC: initWC(wcc...), + start: start, + producer: chooseSpeedProducer(unit, format), } return d } type averageSpeed struct { WC - startTime time.Time - producer func(float64) string - msg string + start time.Time + producer func(float64) string + msg string } -func (d *averageSpeed) Decor(s Statistics) string { +func (d *averageSpeed) Decor(s Statistics) (string, int) { if !s.Completed { - speed := float64(s.Current) / float64(time.Since(d.startTime)) + speed := float64(s.Current) / float64(time.Since(d.start)) d.msg = d.producer(speed * 1e9) } - - return d.FormatMsg(d.msg) + return d.Format(d.msg) } -func (d *averageSpeed) AverageAdjust(startTime time.Time) { - d.startTime = startTime +func (d *averageSpeed) AverageAdjust(start time.Time) { + d.start = start } -func chooseSpeedProducer(unit int, format string) func(float64) string { - switch unit { - case UnitKiB: +func chooseSpeedProducer(unit interface{}, format string) func(float64) string { + switch unit.(type) { + case SizeB1024: + if format == "" { + format = "% d" + } return func(speed float64) string { return fmt.Sprintf(format, FmtAsSpeed(SizeB1024(math.Round(speed)))) } - case UnitKB: + case SizeB1000: + if format == "" { + format = "% d" + } return func(speed float64) string { return fmt.Sprintf(format, FmtAsSpeed(SizeB1000(math.Round(speed)))) } default: + if format == "" { + format = "%f" + } return func(speed float64) string { return fmt.Sprintf(format, speed) } diff --git a/decor/speed_test.go b/decor/speed_test.go index 7f7d09d..2fe770b 100644 --- a/decor/speed_test.go +++ b/decor/speed_test.go @@ -5,114 +5,122 @@ "time" ) -func TestSpeedKiBDecor(t *testing.T) { +func TestAverageSpeedSizeB1024(t *testing.T) { cases := []struct { name string fmt string - unit int + unit interface{} current int64 elapsed time.Duration expected string }{ { name: "empty fmt", - unit: UnitKiB, + unit: SizeB1024(0), fmt: "", current: 0, elapsed: time.Second, + expected: "0 b/s", + }, + { + name: "SizeB1024(0):%d:0b", + unit: SizeB1024(0), + fmt: "%d", + current: 0, + elapsed: time.Second, expected: "0b/s", }, { - name: "UnitKiB:%d:0b", - unit: UnitKiB, - fmt: "%d", - current: 0, - elapsed: time.Second, - expected: "0b/s", - }, - { - name: "UnitKiB:% .2f:0b", - unit: UnitKiB, + name: "SizeB1024(0):%f:0b", + unit: SizeB1024(0), + fmt: "%f", + current: 0, + elapsed: time.Second, + expected: "0.000000b/s", + }, + { + name: "SizeB1024(0):% .2f:0b", + unit: SizeB1024(0), fmt: "% .2f", current: 0, elapsed: time.Second, expected: "0.00 b/s", }, { - name: "UnitKiB:%d:1b", - unit: UnitKiB, + name: "SizeB1024(0):%d:1b", + unit: SizeB1024(0), fmt: "%d", current: 1, elapsed: time.Second, expected: "1b/s", }, { - name: "UnitKiB:% .2f:1b", - unit: UnitKiB, + name: "SizeB1024(0):% .2f:1b", + unit: SizeB1024(0), fmt: "% .2f", current: 1, elapsed: time.Second, expected: "1.00 b/s", }, { - name: "UnitKiB:%d:KiB", - unit: UnitKiB, + name: "SizeB1024(0):%d:KiB", + unit: SizeB1024(0), fmt: "%d", current: 2 * int64(_iKiB), elapsed: 1 * time.Second, expected: "2KiB/s", }, { - name: "UnitKiB:% .f:KiB", - unit: UnitKiB, + name: "SizeB1024(0):% .f:KiB", + unit: SizeB1024(0), fmt: "% .2f", current: 2 * int64(_iKiB), elapsed: 1 * time.Second, expected: "2.00 KiB/s", }, { - name: "UnitKiB:%d:MiB", - unit: UnitKiB, + name: "SizeB1024(0):%d:MiB", + unit: SizeB1024(0), fmt: "%d", current: 2 * int64(_iMiB), elapsed: 1 * time.Second, expected: "2MiB/s", }, { - name: "UnitKiB:% .2f:MiB", - unit: UnitKiB, + name: "SizeB1024(0):% .2f:MiB", + unit: SizeB1024(0), fmt: "% .2f", current: 2 * int64(_iMiB), elapsed: 1 * time.Second, expected: "2.00 MiB/s", }, { - name: "UnitKiB:%d:GiB", - unit: UnitKiB, + name: "SizeB1024(0):%d:GiB", + unit: SizeB1024(0), fmt: "%d", current: 2 * int64(_iGiB), elapsed: 1 * time.Second, expected: "2GiB/s", }, { - name: "UnitKiB:% .2f:GiB", - unit: UnitKiB, + name: "SizeB1024(0):% .2f:GiB", + unit: SizeB1024(0), fmt: "% .2f", current: 2 * int64(_iGiB), elapsed: 1 * time.Second, expected: "2.00 GiB/s", }, { - name: "UnitKiB:%d:TiB", - unit: UnitKiB, + name: "SizeB1024(0):%d:TiB", + unit: SizeB1024(0), fmt: "%d", current: 2 * int64(_iTiB), elapsed: 1 * time.Second, expected: "2TiB/s", }, { - name: "UnitKiB:% .2f:TiB", - unit: UnitKiB, + name: "SizeB1024(0):% .2f:TiB", + unit: SizeB1024(0), fmt: "% .2f", current: 2 * int64(_iTiB), elapsed: 1 * time.Second, @@ -125,7 +133,7 @@ stat := Statistics{ Current: tc.current, } - res := decor.Decor(stat) + res, _ := decor.Decor(stat) if res != tc.expected { t.Fatalf("expected: %q, got: %q\n", tc.expected, res) } @@ -133,114 +141,122 @@ } } -func TestSpeedKBDecor(t *testing.T) { +func TestAverageSpeedSizeB1000(t *testing.T) { cases := []struct { name string fmt string - unit int + unit interface{} current int64 elapsed time.Duration expected string }{ { name: "empty fmt", - unit: UnitKB, + unit: SizeB1000(0), fmt: "", current: 0, elapsed: time.Second, + expected: "0 b/s", + }, + { + name: "SizeB1000(0):%d:0b", + unit: SizeB1000(0), + fmt: "%d", + current: 0, + elapsed: time.Second, expected: "0b/s", }, { - name: "UnitKB:%d:0b", - unit: UnitKB, - fmt: "%d", - current: 0, - elapsed: time.Second, - expected: "0b/s", - }, - { - name: "UnitKB:% .2f:0b", - unit: UnitKB, + name: "SizeB1000(0):%f:0b", + unit: SizeB1000(0), + fmt: "%f", + current: 0, + elapsed: time.Second, + expected: "0.000000b/s", + }, + { + name: "SizeB1000(0):% .2f:0b", + unit: SizeB1000(0), fmt: "% .2f", current: 0, elapsed: time.Second, expected: "0.00 b/s", }, { - name: "UnitKB:%d:1b", - unit: UnitKB, + name: "SizeB1000(0):%d:1b", + unit: SizeB1000(0), fmt: "%d", current: 1, elapsed: time.Second, expected: "1b/s", }, { - name: "UnitKB:% .2f:1b", - unit: UnitKB, + name: "SizeB1000(0):% .2f:1b", + unit: SizeB1000(0), fmt: "% .2f", current: 1, elapsed: time.Second, expected: "1.00 b/s", }, { - name: "UnitKB:%d:KB", - unit: UnitKB, + name: "SizeB1000(0):%d:KB", + unit: SizeB1000(0), fmt: "%d", current: 2 * int64(_KB), elapsed: 1 * time.Second, expected: "2KB/s", }, { - name: "UnitKB:% .f:KB", - unit: UnitKB, + name: "SizeB1000(0):% .f:KB", + unit: SizeB1000(0), fmt: "% .2f", current: 2 * int64(_KB), elapsed: 1 * time.Second, expected: "2.00 KB/s", }, { - name: "UnitKB:%d:MB", - unit: UnitKB, + name: "SizeB1000(0):%d:MB", + unit: SizeB1000(0), fmt: "%d", current: 2 * int64(_MB), elapsed: 1 * time.Second, expected: "2MB/s", }, { - name: "UnitKB:% .2f:MB", - unit: UnitKB, + name: "SizeB1000(0):% .2f:MB", + unit: SizeB1000(0), fmt: "% .2f", current: 2 * int64(_MB), elapsed: 1 * time.Second, expected: "2.00 MB/s", }, { - name: "UnitKB:%d:GB", - unit: UnitKB, + name: "SizeB1000(0):%d:GB", + unit: SizeB1000(0), fmt: "%d", current: 2 * int64(_GB), elapsed: 1 * time.Second, expected: "2GB/s", }, { - name: "UnitKB:% .2f:GB", - unit: UnitKB, + name: "SizeB1000(0):% .2f:GB", + unit: SizeB1000(0), fmt: "% .2f", current: 2 * int64(_GB), elapsed: 1 * time.Second, expected: "2.00 GB/s", }, { - name: "UnitKB:%d:TB", - unit: UnitKB, + name: "SizeB1000(0):%d:TB", + unit: SizeB1000(0), fmt: "%d", current: 2 * int64(_TB), elapsed: 1 * time.Second, expected: "2TB/s", }, { - name: "UnitKB:% .2f:TB", - unit: UnitKB, + name: "SizeB1000(0):% .2f:TB", + unit: SizeB1000(0), fmt: "% .2f", current: 2 * int64(_TB), elapsed: 1 * time.Second, @@ -253,7 +269,7 @@ stat := Statistics{ Current: tc.current, } - res := decor.Decor(stat) + res, _ := decor.Decor(stat) if res != tc.expected { t.Fatalf("expected: %q, got: %q\n", tc.expected, res) } diff --git a/decor/spinner.go b/decor/spinner.go index 6871639..9d2f890 100644 --- a/decor/spinner.go +++ b/decor/spinner.go @@ -1,6 +1,6 @@ package decor -var defaultSpinnerStyle = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} +var defaultSpinnerStyle = [...]string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} // Spinner returns spinner decorator. // @@ -9,7 +9,7 @@ // `wcc` optional WC config func Spinner(frames []string, wcc ...WC) Decorator { if len(frames) == 0 { - frames = defaultSpinnerStyle + frames = defaultSpinnerStyle[:] } var count uint f := func(s Statistics) string { diff --git a/decorators_test.go b/decorators_test.go index 99c49ce..7bc916f 100644 --- a/decorators_test.go +++ b/decorators_test.go @@ -1,11 +1,10 @@ package mpb_test import ( - "sync" "testing" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) func TestNameDecorator(t *testing.T) { @@ -26,13 +25,13 @@ want: " Test", }, { - decorator: decor.Name("Test", decor.WC{W: 10, C: decor.DidentRight}), + decorator: decor.Name("Test", decor.WC{W: 10, C: decor.DindentRight}), want: "Test ", }, } for _, test := range tests { - got := test.decorator.Decor(decor.Statistics{}) + got, _ := test.decorator.Decor(decor.Statistics{}) if got != test.want { t.Errorf("Want: %q, Got: %q\n", test.want, got) } @@ -89,7 +88,7 @@ testDecoratorConcurrently(t, testCases) } -func TestPercentageDwidthSyncDidentRight(t *testing.T) { +func TestPercentageDwidthSyncDindentRight(t *testing.T) { testCases := [][]step{ { @@ -183,30 +182,25 @@ } for _, columnCase := range testCases { - mpb.SyncWidth(toSyncMatrix(columnCase)) - numBars := len(columnCase) - gott := make([]chan string, numBars) - wg := new(sync.WaitGroup) - wg.Add(numBars) - for i, step := range columnCase { + mpb.SyncWidth(toSyncMatrix(columnCase), nil) + var results []chan string + for _, step := range columnCase { step := step - ch := make(chan string, 1) + ch := make(chan string) go func() { - defer wg.Done() - ch <- step.decorator.Decor(step.stat) + str, _ := step.decorator.Decor(step.stat) + ch <- str }() - gott[i] = ch - } - wg.Wait() - - for i, ch := range gott { - got := <-ch + results = append(results, ch) + } + + for i, ch := range results { + res := <-ch want := columnCase[i].want - if got != want { - t.Errorf("Want: %q, Got: %q\n", want, got) + if res != want { + t.Errorf("Want: %q, Got: %q\n", want, res) } } - } } diff --git a/draw_test.go b/draw_test.go index 2553ca6..eab5751 100644 --- a/draw_test.go +++ b/draw_test.go @@ -6,27 +6,28 @@ "unicode/utf8" ) -func TestDraw(t *testing.T) { +func TestDrawDefault(t *testing.T) { // key is termWidth testSuite := map[int][]struct { + style BarStyleComposer name string - style string total int64 current int64 refill int64 barWidth int trim bool - reverse bool want string }{ 0: { { + style: BarStyle(), name: "t,c{60,20}", total: 60, current: 20, - want: "… ", - }, - { + want: "", + }, + { + style: BarStyle(), name: "t,c{60,20}trim", total: 60, current: 20, @@ -36,12 +37,14 @@ }, 1: { { + style: BarStyle(), name: "t,c{60,20}", total: 60, current: 20, - want: "… ", - }, - { + want: "", + }, + { + style: BarStyle(), name: "t,c{60,20}trim", total: 60, current: 20, @@ -51,12 +54,14 @@ }, 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, @@ -66,102 +71,296 @@ }, 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, @@ -169,6 +368,7 @@ want: "[=========================>----------------------------------------------------]", }, { + style: BarStyle(), name: "t,c,bw{60,20,60}", total: 60, current: 20, @@ -176,6 +376,7 @@ want: " [==================>---------------------------------------] ", }, { + style: BarStyle(), name: "t,c,bw{60,20,60}trim", total: 60, current: 20, @@ -183,15 +384,230 @@ trim: true, want: "[==================>---------------------------------------]", }, + { + style: BarStyle(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [==========================================================================>-] ", + }, + { + style: BarStyle(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[============================================================================>-]", + }, + { + style: BarStyle(), + name: "t,c,bw{60,59,60}", + total: 60, + current: 59, + barWidth: 60, + want: " [========================================================>-] ", + }, + { + style: BarStyle(), + name: "t,c,bw{60,59,60}trim", + total: 60, + current: 59, + barWidth: 60, + trim: true, + want: "[========================================================>-]", + }, + { + style: BarStyle(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [============================================================================] ", + }, + { + style: BarStyle(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[==============================================================================]", + }, + { + style: BarStyle(), + name: "t,c,bw{60,60,60}", + total: 60, + current: 60, + barWidth: 60, + want: " [==========================================================] ", + }, + { + style: BarStyle(), + name: "t,c,bw{60,60,60}trim", + total: 60, + current: 60, + barWidth: 60, + trim: true, + want: "[==========================================================]", + }, + }, + 99: { + { + style: BarStyle(), + name: "t,c{100,1}", + total: 100, + current: 1, + want: " [>----------------------------------------------------------------------------------------------] ", + }, + { + style: BarStyle(), + name: "t,c{100,1}trim", + total: 100, + current: 1, + trim: true, + want: "[>------------------------------------------------------------------------------------------------]", + }, + { + style: BarStyle(), + name: "t,c,r{100,40,33}", + total: 100, + current: 40, + refill: 33, + want: " [+++++++++++++++++++++++++++++++======>---------------------------------------------------------] ", + }, + { + style: BarStyle(), + name: "t,c,r{100,40,33}trim", + total: 100, + current: 40, + refill: 33, + trim: true, + want: "[++++++++++++++++++++++++++++++++======>----------------------------------------------------------]", + }, + { + style: BarStyle().Tip("<").Reverse(), + name: "t,c,r{100,40,33},rev", + total: 100, + current: 40, + refill: 33, + want: " [---------------------------------------------------------<======+++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().Tip("<").Reverse(), + name: "t,c,r{100,40,33}trim,rev", + total: 100, + current: 40, + refill: 33, + trim: true, + want: "[----------------------------------------------------------<======++++++++++++++++++++++++++++++++]", + }, + { + style: BarStyle(), + name: "t,c{100,99}", + total: 100, + current: 99, + want: " [=============================================================================================>-] ", + }, + { + style: BarStyle(), + name: "t,c{100,99}trim", + total: 100, + current: 99, + trim: true, + want: "[===============================================================================================>-]", + }, + { + style: BarStyle(), + name: "t,c{100,100}", + total: 100, + current: 100, + want: " [===============================================================================================] ", + }, + { + style: BarStyle(), + name: "t,c{100,100}trim", + total: 100, + current: 100, + trim: true, + want: "[=================================================================================================]", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,99}", + total: 100, + current: 100, + refill: 99, + want: " [++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++=] ", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,99}trim", + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++=]", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,99}rev", + total: 100, + current: 100, + refill: 99, + want: " [=++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,99}trim,rev", + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[=++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,100}", + total: 100, + current: 100, + refill: 100, + want: " [+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle(), + name: "t,c,r{100,100,100}trim", + total: 100, + current: 100, + refill: 100, + trim: true, + want: "[+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,100}rev", + total: 100, + current: 100, + refill: 100, + want: " [+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().Reverse(), + name: "t,c,r{100,100,100}trim", + total: 100, + current: 100, + refill: 100, + trim: true, + want: "[+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, }, 100: { { + style: BarStyle(), name: "t,c{100,0}", total: 100, current: 0, want: " [------------------------------------------------------------------------------------------------] ", }, { + style: BarStyle(), name: "t,c{100,0}trim", total: 100, current: 0, @@ -199,12 +615,21 @@ want: "[--------------------------------------------------------------------------------------------------]", }, { + style: BarStyle(), name: "t,c{100,1}", total: 100, current: 1, want: " [>-----------------------------------------------------------------------------------------------] ", }, { + style: BarStyle().Tip(""), + name: "t,c{100,1}empty_tip", + total: 100, + current: 1, + want: " [=-----------------------------------------------------------------------------------------------] ", + }, + { + style: BarStyle(), name: "t,c{100,1}trim", total: 100, current: 1, @@ -212,12 +637,21 @@ want: "[>-------------------------------------------------------------------------------------------------]", }, { + style: BarStyle(), name: "t,c{100,99}", total: 100, current: 99, want: " [==============================================================================================>-] ", }, { + style: BarStyle().Tip(""), + name: "t,c{100,99}empty_tip", + total: 100, + current: 99, + want: " [===============================================================================================-] ", + }, + { + style: BarStyle(), name: "t,c{100,99}trim", total: 100, current: 99, @@ -225,12 +659,14 @@ 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, @@ -238,6 +674,32 @@ 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, @@ -246,51 +708,41 @@ want: "[++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", }, { - name: "t,c{100,33}", - total: 100, - current: 33, - want: " [===============================>----------------------------------------------------------------] ", - }, - { - name: "t,c{100,33}trim", - total: 100, - current: 33, - trim: true, - want: "[===============================>------------------------------------------------------------------]", - }, - { - name: "t,c{100,33}trim,rev", - total: 100, - current: 33, - trim: true, - reverse: true, - want: "[------------------------------------------------------------------<===============================]", - }, - { - name: "t,c,r{100,33,33}", - total: 100, - current: 33, - refill: 33, - want: " [+++++++++++++++++++++++++++++++>----------------------------------------------------------------] ", - }, - { - name: "t,c,r{100,33,33}trim", - total: 100, - current: 33, - refill: 33, - trim: true, - want: "[+++++++++++++++++++++++++++++++>------------------------------------------------------------------]", - }, - { - name: "t,c,r{100,33,33}trim,rev", - total: 100, - current: 33, - refill: 33, - trim: true, - reverse: 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, @@ -298,6 +750,7 @@ want: " [++++++++++++++++++++++++++++++++=====>----------------------------------------------------------] ", }, { + style: BarStyle(), name: "t,c,r{100,40,33}trim", total: 100, current: 40, @@ -306,39 +759,21 @@ want: "[++++++++++++++++++++++++++++++++======>-----------------------------------------------------------]", }, { + style: BarStyle().Tip("<").Reverse(), name: "t,c,r{100,40,33},rev", total: 100, current: 40, refill: 33, - reverse: true, want: " [----------------------------------------------------------<=====++++++++++++++++++++++++++++++++] ", }, { + style: BarStyle().Tip("<").Reverse(), name: "t,c,r{100,40,33}trim,rev", total: 100, current: 40, refill: 33, trim: true, - reverse: true, want: "[-----------------------------------------------------------<======++++++++++++++++++++++++++++++++]", - }, - { - name: "[=の-] t,c{100,1}", - style: "[=の-]", - total: 100, - current: 1, - want: " [の---------------------------------------------------------------------------------------------…] ", - }, - }, - 197: { - { - name: "t,c,r{97486999,2805950,2805483}trim", - total: 97486999, - current: 2805950, - refill: 2805483, - barWidth: 60, - trim: true, - want: "[+>--------------------------------------------------------]", }, }, } @@ -346,14 +781,472 @@ var tmpBuf bytes.Buffer for tw, cases := range testSuite { for _, tc := range cases { - s := newTestState(tc.style, tc.reverse) - s.reqWidth = tc.barWidth - s.total = tc.total + ps := pState{reqWidth: tc.barWidth} + s := ps.makeBarState(tc.total, tc.style.Build()) s.current = tc.current s.trimSpace = tc.trim s.refill = tc.refill + r, err := s.draw(s.newStatistics(tw)) + if err != nil { + t.Fatalf("tw: %d case %q draw error: %s", tw, tc.name, err.Error()) + } tmpBuf.Reset() - tmpBuf.ReadFrom(s.draw(newStatistics(tw, s))) + _, err = tmpBuf.ReadFrom(r) + if err != nil { + t.FailNow() + } + by := tmpBuf.Bytes() + got := string(by[:len(by)-1]) + if !utf8.ValidString(got) { + t.Fail() + } + if got != tc.want { + t.Errorf("termWidth:%d %q want: %q %d, got: %q %d\n", tw, tc.name, tc.want, utf8.RuneCountInString(tc.want), got, utf8.RuneCountInString(got)) + } + } + } +} + +func TestDrawTipOnComplete(t *testing.T) { + // key is termWidth + testSuite := map[int][]struct { + style BarStyleComposer + name string + total int64 + current int64 + refill int64 + barWidth int + trim bool + want string + }{ + 3: { + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[>]", + }, + }, + 4: { + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[=>]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[=>]", + }, + }, + 5: { + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[==>]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[==>]", + }, + }, + 6: { + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [=>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[===>]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [=>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[===>]", + }, + }, + 7: { + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [==>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[====>]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [==>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[====>]", + }, + }, + 8: { + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [===>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[=====>]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [===>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[=====>]", + }, + }, + 80: { + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}", + total: 60, + current: 59, + want: " [==========================================================================>-] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,59}trim", + total: 60, + current: 59, + trim: true, + want: "[============================================================================>-]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,bw{60,59,60}", + total: 60, + current: 59, + barWidth: 60, + want: " [========================================================>-] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,bw{60,59,60}trim", + total: 60, + current: 59, + barWidth: 60, + trim: true, + want: "[========================================================>-]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}", + total: 60, + current: 60, + want: " [===========================================================================>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{60,60}trim", + total: 60, + current: 60, + trim: true, + want: "[=============================================================================>]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,bw{60,60,60}", + total: 60, + current: 60, + barWidth: 60, + want: " [=========================================================>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,bw{60,60,60}trim", + total: 60, + current: 60, + barWidth: 60, + trim: true, + want: "[=========================================================>]", + }, + }, + 99: { + { + style: BarStyle().TipOnComplete(), + name: "t,c{100,99}", + total: 100, + current: 99, + want: " [=============================================================================================>-] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{100,99}trim", + total: 100, + current: 99, + trim: true, + want: "[===============================================================================================>-]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{100,100}", + total: 100, + current: 100, + want: " [==============================================================================================>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{100,100}trim", + total: 100, + current: 100, + trim: true, + want: "[================================================================================================>]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,r{100,100,99}", + total: 100, + current: 100, + refill: 99, + want: " [++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,r{100,100,99}trim", + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>]", + }, + { + style: BarStyle().Tip("<").TipOnComplete().Reverse(), + name: `t,c,r{100,100,99}.Tip("<").TipOnComplete().Reverse()`, + total: 100, + current: 100, + refill: 99, + want: " [<++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().Tip("<").TipOnComplete().Reverse(), + name: `t,c,r{100,100,99}.Tip("<").TipOnComplete().Reverse()trim`, + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[<++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,r{100,100,100}", + total: 100, + current: 100, + refill: 100, + want: " [++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,r{100,100,100}trim", + total: 100, + current: 100, + refill: 100, + trim: true, + want: "[++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>]", + }, + { + style: BarStyle().Tip("<").TipOnComplete().Reverse(), + name: `t,c,r{100,100,100}.Tip("<").TipOnComplete().Reverse()`, + total: 100, + current: 100, + refill: 100, + want: " [<++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++] ", + }, + { + style: BarStyle().Tip("<").TipOnComplete().Reverse(), + name: `t,c,r{100,100,100}.Tip("<").TipOnComplete().Reverse()trim`, + total: 100, + current: 100, + refill: 100, + trim: true, + want: "[<++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]", + }, + }, + 100: { + { + style: BarStyle().TipOnComplete(), + name: "t,c{100,99}", + total: 100, + current: 99, + want: " [==============================================================================================>-] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{100,99}trim", + total: 100, + current: 99, + trim: true, + want: "[================================================================================================>-]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{100,100}", + total: 100, + current: 100, + want: " [===============================================================================================>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c{100,100}trim", + total: 100, + current: 100, + trim: true, + want: "[=================================================================================================>]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,r{100,100,99}", + total: 100, + current: 100, + refill: 99, + want: " [+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,r{100,100,99}trim", + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>]", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,r{100,100,100}", + total: 100, + current: 100, + refill: 100, + want: " [+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>] ", + }, + { + style: BarStyle().TipOnComplete(), + name: "t,c,r{100,100,100}trim", + total: 100, + current: 100, + refill: 100, + trim: true, + want: "[+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>]", + }, + }, + } + + var tmpBuf bytes.Buffer + for tw, cases := range testSuite { + for _, tc := range cases { + ps := pState{reqWidth: tc.barWidth} + s := ps.makeBarState(tc.total, tc.style.Build()) + s.current = tc.current + s.trimSpace = tc.trim + s.refill = tc.refill + r, err := s.draw(s.newStatistics(tw)) + if err != nil { + t.Fatalf("tw: %d case %q draw error: %s", tw, tc.name, err.Error()) + } + tmpBuf.Reset() + _, err = tmpBuf.ReadFrom(r) + if err != nil { + t.FailNow() + } by := tmpBuf.Bytes() got := string(by[:len(by)-1]) @@ -367,12 +1260,177 @@ } } -func newTestState(style string, rev bool) *bState { - s := &bState{ - filler: NewBarFillerPick(style, rev), - bufP: new(bytes.Buffer), - bufB: new(bytes.Buffer), - bufA: new(bytes.Buffer), +func TestDrawDoubleWidth(t *testing.T) { + // key is termWidth + testSuite := map[int][]struct { + style BarStyleComposer + name string + total int64 + current int64 + refill int64 + barWidth int + trim bool + want string + }{ + 99: { + { + style: BarStyle().Lbound("の").Rbound("の"), + name: `t,c{100,1}.Lbound("の").Rbound("の")`, + total: 100, + current: 1, + want: " の>--------------------------------------------------------------------------------------------の ", + }, + { + style: BarStyle().Lbound("の").Rbound("の"), + name: `t,c{100,1}.Lbound("の").Rbound("の")`, + total: 100, + current: 2, + want: " の=>-------------------------------------------------------------------------------------------の ", + }, + { + style: BarStyle().Tip("だ"), + name: `t,c{100,1}Tip("だ")`, + total: 100, + current: 1, + want: " [だ---------------------------------------------------------------------------------------------] ", + }, + { + style: BarStyle().Tip("だ"), + name: `t,c{100,2}Tip("だ")`, + total: 100, + current: 2, + want: " [だ---------------------------------------------------------------------------------------------] ", + }, + { + style: BarStyle().Tip("だ"), + name: `t,c{100,3}Tip("だ")`, + total: 100, + current: 3, + want: " [=だ--------------------------------------------------------------------------------------------] ", + }, + { + style: BarStyle().Tip("だ"), + name: `t,c{100,99}Tip("だ")`, + total: 100, + current: 99, + want: " [============================================================================================だ-] ", + }, + { + style: BarStyle().Tip("だ"), + name: `t,c{100,100}Tip("だ")`, + total: 100, + current: 100, + want: " [===============================================================================================] ", + }, + { + style: BarStyle().Tip("だ").TipOnComplete(), + name: `t,c{100,100}.Tip("だ").TipOnComplete()`, + total: 100, + current: 100, + want: " [=============================================================================================だ] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").Padding("つ"), + name: `t,c{100,1}Filler("の").Tip("だ").Padding("つ")`, + total: 100, + current: 1, + want: " [だつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつ…] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").Padding("つ"), + name: `t,c{100,2}Filler("の").Tip("だ").Padding("つ")`, + total: 100, + current: 2, + want: " [だつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつつ…] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").Padding("つ"), + name: `t,c{100,99}Filler("の").Tip("だ").Padding("つ")`, + total: 100, + current: 99, + want: " [ののののののののののののののののののののののののののののののののののののののののののののののだ…] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").Padding("つ"), + name: `t,c{100,100}.Filler("の").Tip("だ").Padding("つ")`, + total: 100, + current: 100, + want: " [ののののののののののののののののののののののののののののののののののののののののののののののの…] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").Padding("つ").Reverse(), + name: `t,c{100,100}Filler("の").Tip("だ").Padding("つ").Reverse()`, + total: 100, + current: 100, + want: " […ののののののののののののののののののののののののののののののののののののののののののののののの] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").TipOnComplete().Padding("つ"), + name: `t,c{100,99}Filler("の").Tip("だ").TipOnComplete().Padding("つ")`, + total: 100, + current: 99, + want: " [ののののののののののののののののののののののののののののののののののののののののののののののだ…] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").TipOnComplete().Padding("つ"), + name: `t,c{100,100}.Filler("の").Tip("だ").TipOnComplete().Padding("つ")`, + total: 100, + current: 100, + want: " [ののののののののののののののののののののののののののののののののののののののののののののののだ…] ", + }, + { + style: BarStyle().Filler("の").Tip("だ").TipOnComplete().Padding("つ").Reverse(), + name: `t,c{100,100}.Filler("の").Tip("だ").TipOnComplete().Padding("つ").Reverse()`, + total: 100, + current: 100, + want: " […だのののののののののののののののののののののののののののののののののののののののののののののの] ", + }, + { + style: BarStyle().Refiller("の"), + name: `t,c,r{100,100,99}Refiller("の")`, + total: 100, + current: 100, + refill: 99, + want: " [ののののののののののののののののののののののののののののののののののののののののののののののの=] ", + }, + { + style: BarStyle().Refiller("の"), + name: `t,c,r{100,100,99}Refiller("の")trim`, + total: 100, + current: 100, + refill: 99, + trim: true, + want: "[のののののののののののののののののののののののののののののののののののののののののののののののの=]", + }, + }, } - return s + + var tmpBuf bytes.Buffer + for tw, cases := range testSuite { + for _, tc := range cases { + ps := pState{reqWidth: tc.barWidth} + s := ps.makeBarState(tc.total, tc.style.Build()) + s.current = tc.current + s.trimSpace = tc.trim + s.refill = tc.refill + r, err := s.draw(s.newStatistics(tw)) + if err != nil { + t.Fatalf("tw: %d case %q draw error: %s", tw, tc.name, err.Error()) + } + tmpBuf.Reset() + _, err = tmpBuf.ReadFrom(r) + if err != nil { + t.FailNow() + } + by := tmpBuf.Bytes() + + got := string(by[:len(by)-1]) + if !utf8.ValidString(got) { + t.Fail() + } + if got != tc.want { + t.Errorf("termWidth:%d %q want: %q %d, got: %q %d\n", tw, tc.name, tc.want, utf8.RuneCountInString(tc.want), got, utf8.RuneCountInString(got)) + } + } + } } diff --git a/example_test.go b/example_test.go index 6f25c1b..dd7e6a2 100644 --- a/example_test.go +++ b/example_test.go @@ -3,12 +3,11 @@ import ( crand "crypto/rand" "io" - "io/ioutil" "math/rand" "time" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) func Example() { @@ -17,31 +16,23 @@ total := 100 name := "Single Bar:" - // adding a single bar, which will inherit container's width - bar := p.Add(int64(total), - // progress bar filler with customized style - mpb.NewBarFiller("╢▌▌░╟"), + // 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}), + decor.Name(name, decor.WC{C: decor.DindentRight | decor.DextraSpace}), // replace ETA decorator with "done" message, OnComplete event - decor.OnComplete( - // ETA decorator with ewma age of 60, and width reservation of 4 - decor.EwmaETA(decor.ET_STYLE_GO, 60, decor.WC{W: 4}), "done", - ), + decor.OnComplete(decor.AverageETA(decor.ET_STYLE_GO), "done"), ), mpb.AppendDecorators(decor.Percentage()), ) // simulating some work max := 100 * time.Millisecond for i := 0; i < total; i++ { - // start variable is solely for EWMA calculation - // EWMA's unit of measure is an iteration's duration - start := time.Now() time.Sleep(time.Duration(rand.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 our bar to complete and flush p.Wait() @@ -78,7 +69,7 @@ defer proxyReader.Close() // and copy from reader, ignoring errors - io.Copy(ioutil.Discard, proxyReader) + _, _ = io.Copy(io.Discard, proxyReader) p.Wait() } diff --git a/export_test.go b/export_test.go index fba0eaf..937ce57 100644 --- a/export_test.go +++ b/export_test.go @@ -2,4 +2,5 @@ // make syncWidth func public in test var SyncWidth = syncWidth -var MaxWidthDistributor = &maxWidthDistributor + +type PriorityQueue = priorityQueue diff --git a/go.mod b/go.mod index 55d523e..71de411 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,12 @@ -module github.com/vbauerster/mpb/v6 +module github.com/vbauerster/mpb/v8 require ( - github.com/VividCortex/ewma v1.1.1 + github.com/VividCortex/ewma v1.2.0 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d - github.com/mattn/go-runewidth v0.0.10 - github.com/rivo/uniseg v0.2.0 - golang.org/x/sys v0.0.0-20210324051608-47abb6519492 + github.com/mattn/go-runewidth v0.0.16 + golang.org/x/sys v0.24.0 ) -go 1.14 +require github.com/rivo/uniseg v0.4.7 // indirect + +go 1.17 diff --git a/go.sum b/go.sum index 4809b4a..8066177 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,11 @@ -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= -github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= -github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492 h1:Paq34FxTluEPvVyayQqMPgHm+vTOrIifmcYxFBx9TLg= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/heap_manager.go b/heap_manager.go new file mode 100644 index 0000000..a680187 --- /dev/null +++ b/heap_manager.go @@ -0,0 +1,173 @@ +package mpb + +import "container/heap" + +type heapManager chan heapRequest + +type heapCmd int + +const ( + h_sync heapCmd = iota + h_push + h_iter + h_drain + h_fix + h_state + h_end +) + +type heapRequest struct { + cmd heapCmd + data interface{} +} + +type iterData struct { + iter chan<- *Bar + drop <-chan struct{} +} + +type pushData struct { + bar *Bar + sync bool +} + +type fixData struct { + bar *Bar + priority int + lazy bool +} + +func (m heapManager) run() { + var bHeap priorityQueue + var pMatrix, aMatrix map[int][]chan int + + var l int + var sync bool + + for req := range m { + switch req.cmd { + case h_push: + data := req.data.(pushData) + heap.Push(&bHeap, data.bar) + if !sync { + sync = data.sync + } + case h_sync: + if sync || l != bHeap.Len() { + pMatrix = make(map[int][]chan int) + aMatrix = make(map[int][]chan int) + for _, b := range bHeap { + table := b.wSyncTable() + for i, ch := range table[0] { + pMatrix[i] = append(pMatrix[i], ch) + } + for i, ch := range table[1] { + aMatrix[i] = append(aMatrix[i], ch) + } + } + sync = false + l = bHeap.Len() + } + drop := req.data.(<-chan struct{}) + syncWidth(pMatrix, drop) + syncWidth(aMatrix, drop) + case h_iter: + data := req.data.(iterData) + drop_iter: + for _, b := range bHeap { + select { + case data.iter <- b: + case <-data.drop: + break drop_iter + } + } + close(data.iter) + case h_drain: + data := req.data.(iterData) + drop_drain: + for bHeap.Len() != 0 { + select { + case data.iter <- heap.Pop(&bHeap).(*Bar): + case <-data.drop: + break drop_drain + } + } + close(data.iter) + case h_fix: + data := req.data.(fixData) + if data.bar.index < 0 { + break + } + data.bar.priority = data.priority + if !data.lazy { + heap.Fix(&bHeap, data.bar.index) + } + case h_state: + ch := req.data.(chan<- bool) + ch <- sync || l != bHeap.Len() + case h_end: + ch := req.data.(chan<- interface{}) + if ch != nil { + go func() { + ch <- []*Bar(bHeap) + }() + } + close(m) + } + } +} + +func (m heapManager) sync(drop <-chan struct{}) { + m <- heapRequest{cmd: h_sync, data: drop} +} + +func (m heapManager) push(b *Bar, sync bool) { + data := pushData{b, sync} + m <- heapRequest{cmd: h_push, data: data} +} + +func (m heapManager) iter(iter chan<- *Bar, drop <-chan struct{}) { + data := iterData{iter, drop} + m <- heapRequest{cmd: h_iter, data: data} +} + +func (m heapManager) drain(iter chan<- *Bar, drop <-chan struct{}) { + data := iterData{iter, drop} + m <- heapRequest{cmd: h_drain, data: data} +} + +func (m heapManager) fix(b *Bar, priority int, lazy bool) { + data := fixData{b, priority, lazy} + m <- heapRequest{cmd: h_fix, data: data} +} + +func (m heapManager) state(ch chan<- bool) { + m <- heapRequest{cmd: h_state, data: ch} +} + +func (m heapManager) end(ch chan<- interface{}) { + m <- heapRequest{cmd: h_end, data: ch} +} + +func syncWidth(matrix map[int][]chan int, drop <-chan struct{}) { + for _, column := range matrix { + go maxWidthDistributor(column, drop) + } +} + +func maxWidthDistributor(column []chan int, drop <-chan struct{}) { + var maxWidth int + for _, ch := range column { + select { + case w := <-ch: + if w > maxWidth { + maxWidth = w + } + case <-drop: + return + } + } + for _, ch := range column { + ch <- maxWidth + } +} diff --git a/internal/percentage.go b/internal/percentage.go index a8ef8be..e25cf99 100644 --- a/internal/percentage.go +++ b/internal/percentage.go @@ -3,17 +3,20 @@ import "math" // Percentage is a helper function, to calculate percentage. -func Percentage(total, current int64, width int) float64 { - if total <= 0 { +func Percentage(total, current, width uint) float64 { + if total == 0 { return 0 } if current >= total { return float64(width) } - return float64(int64(width)*current) / float64(total) + return float64(width*current) / float64(total) } // PercentageRound same as Percentage but with math.Round. -func PercentageRound(total, current int64, width int) float64 { - return math.Round(Percentage(total, current, width)) +func PercentageRound(total, current int64, width uint) float64 { + if total < 0 || current < 0 { + return 0 + } + return math.Round(Percentage(uint(total), uint(current), width)) } diff --git a/internal/percentage_test.go b/internal/percentage_test.go index 6d5410d..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 diff --git a/internal/predicate.go b/internal/predicate.go deleted file mode 100644 index 1e4dd24..0000000 --- a/internal/predicate.go +++ /dev/null @@ -1,6 +0,0 @@ -package internal - -// Predicate helper for internal use. -func Predicate(pick bool) func() bool { - return func() bool { return pick } -} diff --git a/internal/width.go b/internal/width.go index 216320f..842e811 100644 --- a/internal/width.go +++ b/internal/width.go @@ -3,7 +3,7 @@ // CheckRequestedWidth checks that requested width doesn't overflow // available width func CheckRequestedWidth(requested, available int) int { - if requested <= 0 || requested >= available { + if requested < 1 || requested > available { return available } return requested diff --git a/priority_queue.go b/priority_queue.go index 29d9bd5..0863b57 100644 --- a/priority_queue.go +++ b/priority_queue.go @@ -1,12 +1,16 @@ package mpb -// A priorityQueue implements heap.Interface +import "container/heap" + +var _ heap.Interface = (*priorityQueue)(nil) + type priorityQueue []*Bar func (pq priorityQueue) Len() int { return len(pq) } func (pq priorityQueue) Less(i, j int) bool { - return pq[i].priority < pq[j].priority + // greater priority pops first + return pq[i].priority > pq[j].priority } func (pq priorityQueue) Swap(i, j int) { @@ -16,17 +20,18 @@ } func (pq *priorityQueue) Push(x interface{}) { - s := *pq + n := len(*pq) bar := x.(*Bar) - bar.index = len(s) - s = append(s, bar) - *pq = s + bar.index = n + *pq = append(*pq, bar) } func (pq *priorityQueue) Pop() interface{} { - s := *pq - *pq = s[0 : len(s)-1] - bar := s[len(s)-1] + old := *pq + n := len(old) + bar := old[n-1] + old[n-1] = nil // avoid memory leak bar.index = -1 // for safety + *pq = old[:n-1] return bar } diff --git a/progress.go b/progress.go index 5a3f962..5c57eaf 100644 --- a/progress.go +++ b/progress.go @@ -2,80 +2,81 @@ import ( "bytes" - "container/heap" "context" "fmt" "io" - "io/ioutil" - "log" "math" "os" "sync" "time" - "github.com/vbauerster/mpb/v6/cwriter" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8/cwriter" + "github.com/vbauerster/mpb/v8/decor" ) -const ( - // default RefreshRate - prr = 120 * time.Millisecond -) - -// Progress represents a container that renders one or more progress -// bars. +const defaultRefreshRate = 150 * time.Millisecond + +// DoneError represents use after `(*Progress).Wait()` error. +var DoneError = fmt.Errorf("%T instance can't be reused after %[1]T.Wait()", (*Progress)(nil)) + +// Progress represents a container that renders one or more progress bars. type Progress struct { + uwg *sync.WaitGroup + pwg, bwg sync.WaitGroup + operateState chan func(*pState) + interceptIO chan func(io.Writer) + done <-chan struct{} + cancel func() +} + +// pState holds bars in its priorityQueue, it gets passed to (*Progress).serve monitor goroutine. +type pState struct { ctx context.Context - uwg *sync.WaitGroup - cwg *sync.WaitGroup - bwg *sync.WaitGroup - operateState chan func(*pState) - 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 + hm heapManager + dropS, dropD chan struct{} + renderReq chan time.Time + idCount int + popPriority int // following are provided/overrided by user - idCount int + refreshRate time.Duration reqWidth int popCompleted bool - outputDiscarded bool - rr time.Duration - uwg *sync.WaitGroup - externalRefresh <-chan interface{} - renderDelay <-chan struct{} - shutdownNotifier chan struct{} - parkedBars map[*Bar]*Bar + autoRefresh bool + delayRC <-chan struct{} + manualRC <-chan interface{} + shutdownNotifier chan<- interface{} + queueBars map[*Bar]*Bar output io.Writer debugOut io.Writer + uwg *sync.WaitGroup } // New creates new Progress container instance. It's not possible to -// reuse instance after *Progress.Wait() method has been called. +// reuse instance after `(*Progress).Wait` method has been called. func New(options ...ContainerOption) *Progress { return NewWithContext(context.Background(), options...) } // NewWithContext creates new Progress container instance with provided -// context. It's not possible to reuse instance after *Progress.Wait() +// context. It's not possible to reuse instance after `(*Progress).Wait` // method has been called. func NewWithContext(ctx context.Context, options ...ContainerOption) *Progress { + if ctx == nil { + ctx = context.Background() + } + ctx, cancel := context.WithCancel(ctx) s := &pState{ - bHeap: priorityQueue{}, - rr: prr, - parkedBars: make(map[*Bar]*Bar), - output: os.Stdout, - debugOut: ioutil.Discard, + ctx: ctx, + hm: make(heapManager), + dropS: make(chan struct{}), + dropD: make(chan struct{}), + renderReq: make(chan time.Time), + popPriority: math.MinInt32, + refreshRate: defaultRefreshRate, + queueBars: make(map[*Bar]*Bar), + output: os.Stdout, + debugOut: io.Discard, } for _, opt := range options { @@ -85,286 +86,363 @@ } p := &Progress{ - ctx: ctx, uwg: s.uwg, - cwg: new(sync.WaitGroup), - 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) - go p.serve(s, cwriter.New(s.output)) + interceptIO: make(chan func(io.Writer)), + cancel: cancel, + } + + cw := cwriter.New(s.output) + if s.manualRC != nil { + done := make(chan struct{}) + p.done = done + s.autoRefresh = false + go s.manualRefreshListener(done) + } else if cw.IsTerminal() || s.autoRefresh { + done := make(chan struct{}) + p.done = done + s.autoRefresh = true + go s.autoRefreshListener(done) + } else { + p.done = ctx.Done() + s.autoRefresh = false + } + + p.pwg.Add(1) + go p.serve(s, cw) + go s.hm.run() return p } -// AddBar creates a bar with default bar filler. Different filler can -// be choosen and applied via `*Progress.Add(...) *Bar` method. +// AddBar creates a bar with default bar filler. func (p *Progress) AddBar(total int64, options ...BarOption) *Bar { - return p.Add(total, NewBarFiller(BarDefaultStyle), options...) -} - -// AddSpinner creates a bar with default spinner filler. Different -// filler can be choosen and applied via `*Progress.Add(...) *Bar` -// method. -func (p *Progress) AddSpinner(total int64, alignment SpinnerAlignment, options ...BarOption) *Bar { - return p.Add(total, NewSpinnerFiller(SpinnerDefaultStyle, alignment), options...) -} - -// Add creates a bar which renders itself by provided filler. -// 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 { + return p.New(total, BarStyle(), options...) +} + +// AddSpinner creates a bar with default spinner filler. +func (p *Progress) AddSpinner(total int64, options ...BarOption) *Bar { + return p.New(total, SpinnerStyle(), options...) +} + +// New creates a bar by calling `Build` method on provided `BarFillerBuilder`. +func (p *Progress) New(total int64, builder BarFillerBuilder, options ...BarOption) *Bar { + if builder == nil { + return p.MustAdd(total, nil, options...) + } + return p.MustAdd(total, builder.Build(), options...) +} + +// MustAdd creates a bar which renders itself by provided BarFiller. +// If `total <= 0` triggering complete event by increment methods is +// disabled. Panics if called after `(*Progress).Wait()`. +func (p *Progress) MustAdd(total int64, filler BarFiller, options ...BarOption) *Bar { + bar, err := p.Add(total, filler, options...) + if err != nil { + panic(err) + } + return bar +} + +// Add creates a bar which renders itself by provided BarFiller. +// If `total <= 0` triggering complete event by increment methods +// is disabled. If called after `(*Progress).Wait()` then +// `(nil, DoneError)` is returned. +func (p *Progress) Add(total int64, filler BarFiller, options ...BarOption) (*Bar, error) { if filler == nil { - filler = BarFillerFunc(func(io.Writer, int, decor.Statistics) {}) - } - p.bwg.Add(1) - result := make(chan *Bar) + filler = NopStyle().Build() + } else if f, ok := filler.(BarFillerFunc); ok && f == nil { + filler = NopStyle().Build() + } + ch := make(chan *Bar) select { case p.operateState <- func(ps *pState) { bs := ps.makeBarState(total, filler, options...) - bar := newBar(p, bs) - if bs.runningBar != nil { - bs.runningBar.noPop = true - ps.parkedBars[bs.runningBar] = bar + bar := newBar(ps.ctx, p, bs) + if bs.waitBar != nil { + ps.queueBars[bs.waitBar] = bar } else { - heap.Push(&ps.bHeap, bar) - ps.heapUpdated = true + ps.hm.push(bar, true) } ps.idCount++ - result <- bar + ch <- bar }: - bar := <-result - bar.subscribeDecorators() - return bar + return <-ch, nil case <-p.done: - p.bwg.Done() - panic(fmt.Sprintf("%T instance can't be reused after it's done!", p)) - } -} - -func (p *Progress) dropBar(b *Bar) { + return nil, DoneError + } +} + +func (p *Progress) traverseBars(cb func(b *Bar) bool) { + iter, drop := make(chan *Bar), make(chan struct{}) select { - case p.operateState <- func(s *pState) { - if b.index < 0 { - return - } - heap.Remove(&s.bHeap, b.index) - s.heapUpdated = true + case p.operateState <- func(s *pState) { s.hm.iter(iter, drop) }: + for b := range iter { + if !cb(b) { + close(drop) + break + } + } + case <-p.done: + } +} + +// UpdateBarPriority either immediately or lazy. +// With lazy flag order is updated after the next refresh cycle. +// If you don't care about laziness just use `(*Bar).SetPriority(int)`. +func (p *Progress) UpdateBarPriority(b *Bar, priority int, lazy bool) { + if b == nil { + return + } + select { + case p.operateState <- func(s *pState) { s.hm.fix(b, priority, lazy) }: + case <-p.done: + } +} + +// Write is implementation of io.Writer. +// Writing to `*Progress` will print lines above a running bar. +// Writes aren't flushed immediately, but at next refresh cycle. +// If called after `(*Progress).Wait()` then `(0, DoneError)` +// is returned. +func (p *Progress) Write(b []byte) (int, error) { + type result struct { + n int + err error + } + ch := make(chan result) + select { + case p.interceptIO <- func(w io.Writer) { + n, err := w.Write(b) + ch <- result{n, err} }: + res := <-ch + return res.n, res.err case <-p.done: - } -} - -func (p *Progress) setBarPriority(b *Bar, priority int) { - select { - case p.operateState <- func(s *pState) { - if b.index < 0 { - return - } - b.priority = priority - heap.Fix(&s.bHeap, b.index) - }: - case <-p.done: - } -} - -// UpdateBarPriority same as *Bar.SetPriority(int). -func (p *Progress) UpdateBarPriority(b *Bar, priority int) { - p.setBarPriority(b, priority) -} - -// BarCount returns bars count. -func (p *Progress) BarCount() int { - result := make(chan int, 1) - select { - case p.operateState <- func(s *pState) { result <- s.bHeap.Len() }: - return <-result - case <-p.done: - return 0 - } -} - -// Wait waits for all bars to complete and finally shutdowns container. -// After this method has been called, there is no way to reuse *Progress -// instance. + return 0, DoneError + } +} + +// Wait waits for all bars to complete and finally shutdowns container. After +// this method has been called, there is no way to reuse `*Progress` instance. func (p *Progress) Wait() { + p.bwg.Wait() + p.Shutdown() + // wait for user wg, if any if p.uwg != nil { - // wait for user wg p.uwg.Wait() } - - // wait for bars to quit, if any - p.bwg.Wait() - - p.once.Do(p.shutdown) - - // wait for container to quit - p.cwg.Wait() -} - -func (p *Progress) shutdown() { - close(p.done) +} + +// Shutdown cancels any running bar immediately and then shutdowns `*Progress` +// instance. Normally this method shouldn't be called unless you know what you +// are doing. Proper way to shutdown is to call `(*Progress).Wait()` instead. +func (p *Progress) Shutdown() { + p.cancel() + p.pwg.Wait() } func (p *Progress) serve(s *pState, cw *cwriter.Writer) { - defer p.cwg.Done() - - p.refreshCh = s.newTicker(p.done) + defer p.pwg.Done() + var err error + var w *cwriter.Writer + renderReq := s.renderReq + operateState := p.operateState + interceptIO := p.interceptIO + + if s.delayRC != nil { + w = cwriter.New(io.Discard) + } else { + w, cw = cw, nil + } for { select { - case op := <-p.operateState: + case <-s.delayRC: + w, cw = cw, nil + s.delayRC = nil + case op := <-operateState: op(s) - case <-p.refreshCh: - if err := s.render(cw); err != nil { - p.dlogger.Println(err) - } - case <-s.shutdownNotifier: - if s.heapUpdated { - if err := s.render(cw); err != nil { - p.dlogger.Println(err) + case fn := <-interceptIO: + fn(w) + case <-renderReq: + err = s.render(w) + if err != nil { + // (*pState).(autoRefreshListener|manualRefreshListener) may block + // if not launching following short lived goroutine + go func() { + for { + select { + case <-s.renderReq: + case <-p.done: + return + } + } + }() + p.cancel() // cancel all bars + renderReq = nil + operateState = nil + interceptIO = nil + } + case <-p.done: + if err != nil { + _, _ = fmt.Fprintln(s.debugOut, err.Error()) + } else if s.autoRefresh { + update := make(chan bool) + for i := 0; i == 0 || <-update; i++ { + if err := s.render(w); err != nil { + _, _ = fmt.Fprintln(s.debugOut, err.Error()) + break + } + s.hm.state(update) } } + s.hm.end(s.shutdownNotifier) 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 - } +func (s *pState) autoRefreshListener(done chan struct{}) { + ticker := time.NewTicker(s.refreshRate) + defer ticker.Stop() + for { + select { + case t := <-ticker.C: + s.renderReq <- t + case <-s.ctx.Done(): + close(done) + return + } + } +} + +func (s *pState) manualRefreshListener(done chan struct{}) { + for { + select { + case x := <-s.manualRC: + if t, ok := x.(time.Time); ok { + s.renderReq <- t + } else { + s.renderReq <- time.Now() + } + case <-s.ctx.Done(): + close(done) + return + } + } +} + +func (s *pState) render(cw *cwriter.Writer) (err error) { + s.hm.sync(s.dropS) + iter := make(chan *Bar) + go s.hm.iter(iter, s.dropS) + + var width, height int + if cw.IsTerminal() { + width, height, err = cw.GetTermSize() + if err != nil { + close(s.dropS) + return err + } + } else { + if s.reqWidth > 0 { + width = s.reqWidth } else { - 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 { - if s.heapUpdated { - s.updateSyncMatrix() - s.heapUpdated = false - } - syncWidth(s.pMatrix) - syncWidth(s.aMatrix) - - tw, err := cw.GetWidth() - if err != nil { - tw = s.reqWidth - } - for i := 0; i < s.bHeap.Len(); i++ { - bar := s.bHeap[i] - go bar.render(tw) - } - - return s.flush(cw) -} - -func (s *pState) flush(cw *cwriter.Writer) error { - var lineCount int - bm := make(map[*Bar]struct{}, s.bHeap.Len()) - for s.bHeap.Len() > 0 { - b := heap.Pop(&s.bHeap).(*Bar) - cw.ReadFrom(<-b.frameCh) - if b.toShutdown { - if b.recoveredPanic != nil { - s.barShutdownQueue = append(s.barShutdownQueue, b) - b.toShutdown = false + width = 80 + } + height = width + } + + for b := range iter { + go b.render(width) + } + + return s.flush(cw, height) +} + +func (s *pState) flush(cw *cwriter.Writer, height int) error { + var wg sync.WaitGroup + defer wg.Wait() // waiting for all s.push to complete + + var popCount int + var rows []io.Reader + + iter := make(chan *Bar) + s.hm.drain(iter, s.dropD) + + for b := range iter { + frame := <-b.frameCh + if frame.err != nil { + close(s.dropD) + b.cancel() + return frame.err // b.frameCh is buffered it's ok to return here + } + var usedRows int + for i := len(frame.rows) - 1; i >= 0; i-- { + if row := frame.rows[i]; len(rows) < height { + rows = append(rows, row) + usedRows++ } else { - // shutdown at next flush - // this ensures no bar ends up with less than 100% rendered - defer func() { - s.barShutdownQueue = append(s.barShutdownQueue, b) - }() - } - } - lineCount += b.extendedLines + 1 - bm[b] = struct{}{} - } - - for _, b := range s.barShutdownQueue { - if parkedBar := s.parkedBars[b]; parkedBar != nil { - parkedBar.priority = b.priority - heap.Push(&s.bHeap, parkedBar) - delete(s.parkedBars, b) - b.toDrop = true - } - if s.popCompleted && !b.noPop { - lineCount -= b.extendedLines + 1 - b.toDrop = true - } - if b.toDrop { - delete(bm, b) - s.heapUpdated = true - } - b.cancel() - } - s.barShutdownQueue = s.barShutdownQueue[0:0] - - for b := range bm { - heap.Push(&s.bHeap, b) - } - - return cw.Flush(lineCount) -} - -func (s *pState) updateSyncMatrix() { - s.pMatrix = make(map[int][]chan int) - s.aMatrix = make(map[int][]chan int) - for i := 0; i < s.bHeap.Len(); i++ { - bar := s.bHeap[i] - table := bar.wSyncTable() - pRow, aRow := table[0], table[1] - - for i, ch := range pRow { - s.pMatrix[i] = append(s.pMatrix[i], ch) - } - - for i, ch := range aRow { - s.aMatrix[i] = append(s.aMatrix[i], ch) - } - } -} - -func (s *pState) makeBarState(total int64, filler BarFiller, options ...BarOption) *bState { + _, _ = io.Copy(io.Discard, row) + } + } + + switch frame.shutdown { + case 1: + b.cancel() + if qb, ok := s.queueBars[b]; ok { + delete(s.queueBars, b) + qb.priority = b.priority + wg.Add(1) + go s.push(&wg, qb, true) + } else if s.popCompleted && !frame.noPop { + b.priority = s.popPriority + s.popPriority++ + wg.Add(1) + go s.push(&wg, b, false) + } else if !frame.rmOnComplete { + wg.Add(1) + go s.push(&wg, b, false) + } + case 2: + if s.popCompleted && !frame.noPop { + popCount += usedRows + continue + } + fallthrough + default: + wg.Add(1) + go s.push(&wg, b, false) + } + } + + for i := len(rows) - 1; i >= 0; i-- { + _, err := cw.ReadFrom(rows[i]) + if err != nil { + return err + } + } + + return cw.Flush(len(rows) - popCount) +} + +func (s *pState) push(wg *sync.WaitGroup, b *Bar, sync bool) { + s.hm.push(b, sync) + wg.Done() +} + +func (s pState) makeBarState(total int64, filler BarFiller, options ...BarOption) *bState { bs := &bState{ - id: s.idCount, - priority: s.idCount, - reqWidth: s.reqWidth, - total: total, - filler: filler, - extender: func(r io.Reader, _ int, _ decor.Statistics) (io.Reader, int) { return r, 0 }, - debugOut: s.debugOut, + id: s.idCount, + priority: s.idCount, + reqWidth: s.reqWidth, + total: total, + filler: filler, + renderReq: s.renderReq, + autoRefresh: s.autoRefresh, + extender: func(_ decor.Statistics, rows ...io.Reader) ([]io.Reader, error) { + return rows, nil + }, } if total > 0 { @@ -377,36 +455,9 @@ } } - if bs.middleware != nil { - bs.filler = bs.middleware(filler) - bs.middleware = nil - } - - if s.popCompleted && !bs.noPop { - bs.priority = -(math.MaxInt32 - s.idCount) - } - - bs.bufP = bytes.NewBuffer(make([]byte, 0, 128)) - bs.bufB = bytes.NewBuffer(make([]byte, 0, 256)) - bs.bufA = bytes.NewBuffer(make([]byte, 0, 128)) + bs.buffers[0] = bytes.NewBuffer(make([]byte, 0, 128)) // prepend + bs.buffers[1] = bytes.NewBuffer(make([]byte, 0, 128)) // append + bs.buffers[2] = bytes.NewBuffer(make([]byte, 0, 256)) // filler return bs } - -func syncWidth(matrix map[int][]chan int) { - for _, column := range matrix { - 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 aa7a27d..7892062 100644 --- a/progress_test.go +++ b/progress_test.go @@ -2,184 +2,281 @@ import ( "bytes" + "container/heap" "context" - "io/ioutil" - "math/rand" - "sync" + "errors" + "io" + "strings" "testing" "time" - "github.com/vbauerster/mpb/v6" - "github.com/vbauerster/mpb/v6/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) -func init() { - rand.Seed(time.Now().UnixNano()) -} - -func TestBarCount(t *testing.T) { - p := mpb.New(mpb.WithOutput(ioutil.Discard)) - - var wg sync.WaitGroup - wg.Add(1) +const ( + timeout = 300 * time.Millisecond +) + +func TestWithContext(t *testing.T) { + shutdown := make(chan interface{}) + ctx, cancel := context.WithCancel(context.Background()) + p := mpb.NewWithContext(ctx, + mpb.WithOutput(io.Discard), + mpb.WithShutdownNotifier(shutdown), + ) + _ = p.AddBar(0) // never complete bar + _ = p.AddBar(0) // never complete bar + go func() { + time.Sleep(10 * time.Millisecond) + cancel() + }() + + p.Wait() + + select { + case v := <-shutdown: + if l := len(v.([]*mpb.Bar)); l != 2 { + t.Errorf("Expected len of bars: %d, got: %d", 2, l) + } + case <-time.After(timeout): + t.Errorf("Progress didn't shutdown after %v", timeout) + } +} + +func TestShutdownsWithErrFiller(t *testing.T) { + var debug bytes.Buffer + shutdown := make(chan interface{}) + p := mpb.New( + mpb.WithShutdownNotifier(shutdown), + mpb.WithOutput(io.Discard), + mpb.WithDebugOutput(&debug), + mpb.WithAutoRefresh(), + ) + + var errReturnCount int + testError := errors.New("test error") + bar := p.AddBar(100, + mpb.BarFillerMiddleware(func(base mpb.BarFiller) mpb.BarFiller { + return mpb.BarFillerFunc(func(w io.Writer, st decor.Statistics) error { + if st.Current >= 22 { + errReturnCount++ + return testError + } + return base.Fill(w, st) + }) + }), + ) + + go func() { + for bar.IsRunning() { + bar.Increment() + time.Sleep(10 * time.Millisecond) + } + }() + + p.Wait() + + if errReturnCount != 1 { + t.Errorf("Expected errReturnCount: %d, got: %d\n", 1, errReturnCount) + } + + select { + case v := <-shutdown: + if l := len(v.([]*mpb.Bar)); l != 0 { + t.Errorf("Expected len of bars: %d, got: %d\n", 0, l) + } + if err := strings.TrimSpace(debug.String()); err != testError.Error() { + t.Errorf("Expected err: %q, got %q\n", testError.Error(), err) + } + case <-time.After(timeout): + t.Errorf("Progress didn't shutdown after %v", timeout) + } +} + +func TestShutdownAfterBarAbortWithDrop(t *testing.T) { + shutdown := make(chan interface{}) + p := mpb.New( + mpb.WithShutdownNotifier(shutdown), + mpb.WithOutput(io.Discard), + mpb.WithAutoRefresh(), + ) b := p.AddBar(100) + + var count int + for i := 0; !b.Aborted(); i++ { + if i >= 10 { + count++ + b.Abort(true) + } else { + b.Increment() + time.Sleep(10 * time.Millisecond) + } + } + + p.Wait() + + if count != 1 { + t.Errorf("Expected count: %d, got: %d", 1, count) + } + + select { + case v := <-shutdown: + if l := len(v.([]*mpb.Bar)); l != 0 { + t.Errorf("Expected len of bars: %d, got: %d", 0, l) + } + case <-time.After(timeout): + t.Errorf("Progress didn't shutdown after %v", timeout) + } +} + +func TestShutdownAfterBarAbortWithNoDrop(t *testing.T) { + shutdown := make(chan interface{}) + p := mpb.New( + mpb.WithShutdownNotifier(shutdown), + mpb.WithOutput(io.Discard), + mpb.WithAutoRefresh(), + ) + b := p.AddBar(100) + + var count int + for i := 0; !b.Aborted(); i++ { + if i >= 10 { + count++ + b.Abort(false) + } else { + b.Increment() + time.Sleep(10 * time.Millisecond) + } + } + + p.Wait() + + if count != 1 { + t.Errorf("Expected count: %d, got: %d", 1, count) + } + + select { + case v := <-shutdown: + if l := len(v.([]*mpb.Bar)); l != 1 { + t.Errorf("Expected len of bars: %d, got: %d", 1, l) + } + case <-time.After(timeout): + t.Errorf("Progress didn't shutdown after %v", timeout) + } +} + +func TestBarPristinePopOrder(t *testing.T) { + shutdown := make(chan interface{}) + ctx, cancel := context.WithCancel(context.Background()) + p := mpb.NewWithContext(ctx, + mpb.WithOutput(io.Discard), // auto refresh is disabled + mpb.WithShutdownNotifier(shutdown), + ) + a := p.AddBar(100, mpb.BarPriority(1), mpb.BarID(1)) + b := p.AddBar(100, mpb.BarPriority(2), mpb.BarID(2)) + c := p.AddBar(100, mpb.BarPriority(3), mpb.BarID(3)) + pristineOrder := []*mpb.Bar{c, b, a} + + go cancel() + + bars := (<-shutdown).([]*mpb.Bar) + if l := len(bars); l != 3 { + t.Fatalf("Expected len of bars: %d, got: %d", 3, l) + } + + p.Wait() + pq := mpb.PriorityQueue(bars) + + for _, b := range pristineOrder { + // higher priority pops first + if bar := heap.Pop(&pq).(*mpb.Bar); bar.ID() != b.ID() { + t.Errorf("Expected bar id: %d, got bar id: %d", b.ID(), bar.ID()) + } + } +} + +func makeUpdateBarPriorityTest(refresh, lazy bool) func(*testing.T) { + return func(t *testing.T) { + shutdown := make(chan interface{}) + refreshCh := make(chan interface{}) + ctx, cancel := context.WithCancel(context.Background()) + p := mpb.NewWithContext(ctx, + mpb.WithOutput(io.Discard), + mpb.WithManualRefresh(refreshCh), + mpb.WithShutdownNotifier(shutdown), + ) + a := p.AddBar(100, mpb.BarPriority(1), mpb.BarID(1)) + b := p.AddBar(100, mpb.BarPriority(2), mpb.BarID(2)) + c := p.AddBar(100, mpb.BarPriority(3), mpb.BarID(3)) + + p.UpdateBarPriority(c, 2, lazy) + p.UpdateBarPriority(b, 3, lazy) + checkOrder := []*mpb.Bar{b, c, a} // updated order + + if refresh { + refreshCh <- time.Now() + } else if lazy { + checkOrder = []*mpb.Bar{c, b, a} // pristine order + } + + go cancel() + + bars := (<-shutdown).([]*mpb.Bar) + if l := len(bars); l != 3 { + t.Fatalf("Expected len of bars: %d, got: %d", 3, l) + } + + p.Wait() + pq := mpb.PriorityQueue(bars) + + for _, b := range checkOrder { + // higher priority pops first + if bar := heap.Pop(&pq).(*mpb.Bar); bar.ID() != b.ID() { + t.Errorf("Expected bar id: %d, got bar id: %d", b.ID(), bar.ID()) + } + } + } +} + +func TestUpdateBarPriority(t *testing.T) { + makeUpdateBarPriorityTest(false, false)(t) + makeUpdateBarPriorityTest(true, false)(t) +} + +func TestUpdateBarPriorityLazy(t *testing.T) { + makeUpdateBarPriorityTest(false, true)(t) + makeUpdateBarPriorityTest(true, true)(t) +} + +func TestNoOutput(t *testing.T) { + var buf bytes.Buffer + p := mpb.New(mpb.WithOutput(&buf)) + bar := p.AddBar(100) + go func() { - rng := rand.New(rand.NewSource(time.Now().UnixNano())) - for i := 0; i < 100; i++ { - if i == 33 { - wg.Done() - } - b.Increment() - time.Sleep((time.Duration(rng.Intn(10)+1) * (10 * time.Millisecond)) / 2) + for !bar.Completed() { + bar.Increment() } }() - wg.Wait() - count := p.BarCount() - if count != 1 { - t.Errorf("BarCount want: %q, got: %q\n", 1, count) - } - - b.Abort(true) - p.Wait() -} - -func TestBarAbort(t *testing.T) { - p := mpb.New(mpb.WithOutput(ioutil.Discard)) - - var wg sync.WaitGroup - wg.Add(1) - bars := make([]*mpb.Bar, 3) - for i := 0; i < 3; i++ { - b := p.AddBar(100) - rng := rand.New(rand.NewSource(time.Now().UnixNano())) - go func(n int) { - for i := 0; !b.Completed(); i++ { - if n == 0 && i >= 33 { - b.Abort(true) - wg.Done() - } - b.Increment() - 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) - } - bars[1].Abort(true) - bars[2].Abort(true) - p.Wait() -} - -func TestWithContext(t *testing.T) { - 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), - ) - - total := 10000 - numBars := 3 - bars := make([]*mpb.Bar, 0, numBars) - for i := 0; i < numBars; i++ { - bar := p.AddBar(int64(total)) - bars = append(bars, bar) - go func() { - for !bar.Completed() { - bar.Increment() - time.Sleep(randomDuration(100 * time.Millisecond)) - } - }() - } - - time.Sleep(50 * time.Millisecond) - cancel() - - p.Wait() - select { - case <-shutdown: - case <-time.After(100 * time.Millisecond): - t.Error("Progress didn't stop") - } -} - -// 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 := 3 - p := mpb.New(mpb.WithOutput(ioutil.Discard)) - for i := 0; i < numBars; i++ { - bar := p.AddBar(int64(total), - mpb.BarOptional(mpb.BarRemoveOnComplete(), i == 0), - mpb.PrependDecorators( - decor.EwmaETA(decor.ET_STYLE_GO, 60, decor.WCSyncSpace), - ), - ) - go func() { - <-ready - rng := rand.New(rand.NewSource(time.Now().UnixNano())) - for i := 0; i < total; i++ { - start := time.Now() - if bar.ID() >= numBars-1 && i >= 42 { - bar.Abort(true) - } - time.Sleep((time.Duration(rng.Intn(10)+1) * (10 * time.Millisecond)) / 2) - bar.Increment() - bar.DecoratorEwmaUpdate(time.Since(start)) - } - }() - } - - go func() { - <-ready - p.Wait() - close(start) - }() - - 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") - } -} - -func getLastLine(bb []byte) []byte { - split := bytes.Split(bb, []byte("\n")) - return split[len(split)-2] -} - -func randomDuration(max time.Duration) time.Duration { - return time.Duration(rand.Intn(10)+1) * max / 10 -} + p.Wait() + + if buf.Len() != 0 { + t.Errorf("Expected buf.Len == 0, got: %d\n", buf.Len()) + } +} + +func TestAddAfterDone(t *testing.T) { + p := mpb.New(mpb.WithOutput(io.Discard)) + bar := p.AddBar(100) + bar.IncrBy(100) + + p.Wait() + + _, err := p.Add(100, nil) + + if err != mpb.DoneError { + t.Errorf("Expected %q, got: %q\n", mpb.DoneError, err) + } +} diff --git a/proxyreader.go b/proxyreader.go index 316f438..8c324f8 100644 --- a/proxyreader.go +++ b/proxyreader.go @@ -2,7 +2,6 @@ import ( "io" - "io/ioutil" "time" ) @@ -11,80 +10,64 @@ 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) - } return n, err } type proxyWriterTo struct { - io.ReadCloser // *proxyReader - wt io.WriterTo - bar *Bar + proxyReader } -func (x *proxyWriterTo) WriteTo(w io.Writer) (int64, error) { - n, err := x.wt.WriteTo(w) +func (x proxyWriterTo) WriteTo(w io.Writer) (int64, error) { + n, err := x.ReadCloser.(io.WriterTo).WriteTo(w) x.bar.IncrInt64(n) - if err == io.EOF { - go x.bar.SetTotal(0, true) - } return n, err } type ewmaProxyReader struct { - io.ReadCloser // *proxyReader - bar *Bar - iT time.Time + io.ReadCloser + bar *Bar } -func (x *ewmaProxyReader) Read(p []byte) (int, error) { +func (x ewmaProxyReader) Read(p []byte) (int, error) { + start := time.Now() n, err := x.ReadCloser.Read(p) - if n > 0 { - x.bar.DecoratorEwmaUpdate(time.Since(x.iT)) - x.iT = time.Now() - } + x.bar.EwmaIncrBy(n, time.Since(start)) return n, err } type ewmaProxyWriterTo struct { - io.ReadCloser // *ewmaProxyReader - wt io.WriterTo // *proxyWriterTo - bar *Bar - iT time.Time + ewmaProxyReader } -func (x *ewmaProxyWriterTo) WriteTo(w io.Writer) (int64, error) { - n, err := x.wt.WriteTo(w) - if n > 0 { - x.bar.DecoratorEwmaUpdate(time.Since(x.iT)) - x.iT = time.Now() - } +func (x ewmaProxyWriterTo) WriteTo(w io.Writer) (int64, error) { + start := time.Now() + n, err := x.ReadCloser.(io.WriterTo).WriteTo(w) + x.bar.EwmaIncrInt64(n, time.Since(start)) return n, err } -func newProxyReader(r io.Reader, bar *Bar) io.ReadCloser { +func newProxyReader(r io.Reader, b *Bar, hasEwma bool) 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} + if hasEwma { + epr := ewmaProxyReader{rc, b} + if _, ok := r.(io.WriterTo); ok { + return ewmaProxyWriterTo{epr} } - } else if isWriterTo { - rc = &proxyWriterTo{rc, wt, bar} + return epr } - return rc + pr := proxyReader{rc, b} + if _, ok := r.(io.WriterTo); ok { + return proxyWriterTo{pr} + } + return pr } func toReadCloser(r io.Reader) io.ReadCloser { if rc, ok := r.(io.ReadCloser); ok { return rc } - return ioutil.NopCloser(r) + return io.NopCloser(r) } diff --git a/proxyreader_test.go b/proxyreader_test.go index 71e036b..f7e9121 100644 --- a/proxyreader_test.go +++ b/proxyreader_test.go @@ -3,11 +3,10 @@ import ( "bytes" "io" - "io/ioutil" "strings" "testing" - "github.com/vbauerster/mpb/v6" + "github.com/vbauerster/mpb/v8" ) const content = `Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do @@ -29,21 +28,21 @@ } func TestProxyReader(t *testing.T) { - p := mpb.New(mpb.WithOutput(ioutil.Discard)) + p := mpb.New(mpb.WithOutput(io.Discard)) - tReader := &testReader{strings.NewReader(content), false} + tr := &testReader{strings.NewReader(content), false} - bar := p.AddBar(int64(len(content)), mpb.BarFillerTrim()) + bar := p.New(int64(len(content)), mpb.NopStyle()) var buf bytes.Buffer - _, err := io.Copy(&buf, bar.ProxyReader(tReader)) + _, err := io.Copy(&buf, bar.ProxyReader(tr)) if err != nil { - t.Errorf("Error copying from reader: %+v\n", err) + t.Errorf("io.Copy: %s\n", err.Error()) } p.Wait() - if !tReader.called { + if !tr.called { t.Error("Read not called") } @@ -52,35 +51,63 @@ } } -type testWriterTo struct { +type testReadCloser struct { io.Reader - wt io.WriterTo called bool } -func (wt *testWriterTo) WriteTo(w io.Writer) (n int64, err error) { - wt.called = true - return wt.wt.WriteTo(w) +func (r *testReadCloser) Close() error { + r.called = true + return nil } -func TestProxyWriterTo(t *testing.T) { - p := mpb.New(mpb.WithOutput(ioutil.Discard)) +func TestProxyReadCloser(t *testing.T) { + p := mpb.New(mpb.WithOutput(io.Discard)) - var reader io.Reader = strings.NewReader(content) - wt := reader.(io.WriterTo) - tReader := &testWriterTo{reader, wt, false} + tr := &testReadCloser{strings.NewReader(content), false} - bar := p.AddBar(int64(len(content)), mpb.BarFillerTrim()) + bar := p.New(int64(len(content)), mpb.NopStyle()) + + rc := bar.ProxyReader(tr) + _, err := io.Copy(io.Discard, rc) + if err != nil { + t.Errorf("io.Copy: %s\n", err.Error()) + } + _ = rc.Close() + + p.Wait() + + if !tr.called { + t.Error("Close not called") + } +} + +type testReaderWriterTo struct { + io.Reader + called bool +} + +func (r *testReaderWriterTo) WriteTo(w io.Writer) (n int64, err error) { + r.called = true + return r.Reader.(io.WriterTo).WriteTo(w) +} + +func TestProxyReaderWriterTo(t *testing.T) { + p := mpb.New(mpb.WithOutput(io.Discard)) + + tr := &testReaderWriterTo{strings.NewReader(content), false} + + bar := p.New(int64(len(content)), mpb.NopStyle()) var buf bytes.Buffer - _, err := io.Copy(&buf, bar.ProxyReader(tReader)) + _, err := io.Copy(&buf, bar.ProxyReader(tr)) if err != nil { - t.Errorf("Error copying from reader: %+v\n", err) + t.Errorf("io.Copy: %s\n", err.Error()) } p.Wait() - if !tReader.called { + if !tr.called { t.Error("WriteTo not called") } diff --git a/proxywriter.go b/proxywriter.go new file mode 100644 index 0000000..f260daf --- /dev/null +++ b/proxywriter.go @@ -0,0 +1,96 @@ +package mpb + +import ( + "io" + "time" +) + +type proxyWriter struct { + io.WriteCloser + bar *Bar +} + +func (x proxyWriter) Write(p []byte) (int, error) { + n, err := x.WriteCloser.Write(p) + x.bar.IncrBy(n) + return n, err +} + +type proxyReaderFrom struct { + proxyWriter +} + +func (x proxyReaderFrom) ReadFrom(r io.Reader) (int64, error) { + n, err := x.WriteCloser.(io.ReaderFrom).ReadFrom(r) + x.bar.IncrInt64(n) + return n, err +} + +type ewmaProxyWriter struct { + io.WriteCloser + bar *Bar +} + +func (x ewmaProxyWriter) Write(p []byte) (int, error) { + start := time.Now() + n, err := x.WriteCloser.Write(p) + x.bar.EwmaIncrBy(n, time.Since(start)) + return n, err +} + +type ewmaProxyReaderFrom struct { + ewmaProxyWriter +} + +func (x ewmaProxyReaderFrom) ReadFrom(r io.Reader) (int64, error) { + start := time.Now() + n, err := x.WriteCloser.(io.ReaderFrom).ReadFrom(r) + x.bar.EwmaIncrInt64(n, time.Since(start)) + return n, err +} + +func newProxyWriter(w io.Writer, b *Bar, hasEwma bool) io.WriteCloser { + wc := toWriteCloser(w) + if hasEwma { + epw := ewmaProxyWriter{wc, b} + if _, ok := w.(io.ReaderFrom); ok { + return ewmaProxyReaderFrom{epw} + } + return epw + } + pw := proxyWriter{wc, b} + if _, ok := w.(io.ReaderFrom); ok { + return proxyReaderFrom{pw} + } + return pw +} + +func toWriteCloser(w io.Writer) io.WriteCloser { + if wc, ok := w.(io.WriteCloser); ok { + return wc + } + return toNopWriteCloser(w) +} + +func toNopWriteCloser(w io.Writer) io.WriteCloser { + if _, ok := w.(io.ReaderFrom); ok { + return nopWriteCloserReaderFrom{w} + } + return nopWriteCloser{w} +} + +type nopWriteCloser struct { + io.Writer +} + +func (nopWriteCloser) Close() error { return nil } + +type nopWriteCloserReaderFrom struct { + io.Writer +} + +func (nopWriteCloserReaderFrom) Close() error { return nil } + +func (c nopWriteCloserReaderFrom) ReadFrom(r io.Reader) (int64, error) { + return c.Writer.(io.ReaderFrom).ReadFrom(r) +} diff --git a/proxywriter_test.go b/proxywriter_test.go new file mode 100644 index 0000000..b57357f --- /dev/null +++ b/proxywriter_test.go @@ -0,0 +1,120 @@ +package mpb_test + +import ( + "bytes" + "io" + "strings" + "testing" + + "github.com/vbauerster/mpb/v8" +) + +type testWriter struct { + io.Writer + called bool +} + +func (w *testWriter) Write(p []byte) (n int, err error) { + w.called = true + return w.Writer.Write(p) +} + +func TestProxyWriter(t *testing.T) { + p := mpb.New(mpb.WithOutput(io.Discard)) + + var buf bytes.Buffer + tw := &testWriter{&buf, false} + + bar := p.New(int64(len(content)), mpb.NopStyle()) + + _, err := io.Copy(bar.ProxyWriter(tw), strings.NewReader(content)) + if err != nil { + t.Errorf("io.Copy: %s\n", err.Error()) + } + + p.Wait() + + if !tw.called { + t.Error("Read not called") + } + + if got := buf.String(); got != content { + t.Errorf("Expected content: %s, got: %s\n", content, got) + } +} + +type testWriteCloser struct { + io.Writer + called bool +} + +func (w *testWriteCloser) Close() error { + w.called = true + return nil +} + +func TestProxyWriteCloser(t *testing.T) { + p := mpb.New(mpb.WithOutput(io.Discard)) + + var buf bytes.Buffer + tw := &testWriteCloser{&buf, false} + + bar := p.New(int64(len(content)), mpb.NopStyle()) + + wc := bar.ProxyWriter(tw) + _, err := io.Copy(wc, strings.NewReader(content)) + if err != nil { + t.Errorf("io.Copy: %s\n", err.Error()) + } + _ = wc.Close() + + p.Wait() + + if !tw.called { + t.Error("Close not called") + } +} + +type testWriterReadFrom struct { + io.Writer + called bool +} + +func (w *testWriterReadFrom) ReadFrom(r io.Reader) (n int64, err error) { + w.called = true + return w.Writer.(io.ReaderFrom).ReadFrom(r) +} + +type dumbReader struct { + r io.Reader +} + +func (r dumbReader) Read(p []byte) (int, error) { + return r.r.Read(p) +} + +func TestProxyWriterReadFrom(t *testing.T) { + p := mpb.New(mpb.WithOutput(io.Discard)) + + var buf bytes.Buffer + tw := &testWriterReadFrom{&buf, false} + + bar := p.New(int64(len(content)), mpb.NopStyle()) + + // To trigger ReadFrom, WriteTo needs to be hidden, hence a dumb wrapper + dr := dumbReader{strings.NewReader(content)} + _, err := io.Copy(bar.ProxyWriter(tw), dr) + if err != nil { + t.Errorf("io.Copy: %s\n", err.Error()) + } + + p.Wait() + + if !tw.called { + t.Error("ReadFrom not called") + } + + if got := buf.String(); got != content { + t.Errorf("Expected content: %s, got: %s\n", content, got) + } +}