diff --git a/.travis.yml b/.travis.yml index a7fd776..c982d1f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: go sudo: false go: - - 1.8.x - - 1.9.x + - 1.10.x + - tip before_install: - go get -t -v ./... diff --git a/README.md b/README.md index 9b76064..f96857c 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,6 @@ p := mpb.New( // override default (80) width mpb.WithWidth(64), - // override default "[=>-]" format - mpb.WithFormat("╢▌▌░╟"), // override default 120ms refresh rate mpb.WithRefreshRate(180*time.Millisecond), ) @@ -41,6 +39,8 @@ name := "Single Bar:" // adding a single bar bar := p.AddBar(int64(total), + // override default "[=>-]" style + mpb.BarStyle("╢▌▌░╟"), mpb.PrependDecorators( // display our name with one space on the right decor.Name(name, decor.WC{W: len(name) + 1, C: decor.DidentRight}), diff --git a/bar.go b/bar.go index 5a506fc..a304a87 100644 --- a/bar.go +++ b/bar.go @@ -2,6 +2,7 @@ import ( "bytes" + "context" "fmt" "io" "io/ioutil" @@ -11,20 +12,7 @@ "unicode/utf8" "github.com/vbauerster/mpb/decor" - "github.com/vbauerster/mpb/internal" ) - -const ( - rLeft = iota - rFill - rTip - rEmpty - rRight -) - -const formatLen = 5 - -type barRunes [formatLen]rune // Bar represents a progress Bar type Bar struct { @@ -45,15 +33,30 @@ shutdown chan struct{} } +// Filler interface. +// Bar renders by calling Filler's Fill method. You can literally have +// any bar kind, by implementing this interface and passing it to the +// Add method. +type Filler interface { + Fill(w io.Writer, width int, s *decor.Statistics) +} + +// FillerFunc is function type adapter to convert function into Filler. +type FillerFunc func(w io.Writer, width int, stat *decor.Statistics) + +func (f FillerFunc) Fill(w io.Writer, width int, stat *decor.Statistics) { + f(w, width, stat) +} + type ( bState struct { + filler Filler id int width int + alignment int total int64 current int64 - runes barRunes - trimLeftSpace bool - trimRightSpace bool + trimSpace bool toComplete bool removeOnComplete bool barClearOnComplete bool @@ -73,8 +76,8 @@ runningBar *Bar } refill struct { - char rune - till int64 + r rune + limit int64 } frameReader struct { io.Reader @@ -84,14 +87,20 @@ } ) -func newBar(wg *sync.WaitGroup, id int, total int64, cancel <-chan struct{}, options ...BarOption) *Bar { - if total <= 0 { - total = time.Now().Unix() - } +func newBar( + ctx context.Context, + wg *sync.WaitGroup, + filler Filler, + id, width int, + total int64, + options ...BarOption, +) *Bar { s := &bState{ + filler: filler, id: id, priority: id, + width: width, total: total, } @@ -104,6 +113,9 @@ s.bufP = bytes.NewBuffer(make([]byte, 0, s.width)) s.bufB = bytes.NewBuffer(make([]byte, 0, s.width)) s.bufA = bytes.NewBuffer(make([]byte, 0, s.width)) + if s.newLineExtendFn != nil { + s.bufNL = bytes.NewBuffer(make([]byte, 0, s.width)) + } b := &Bar{ priority: s.priority, @@ -121,11 +133,7 @@ b.priority = b.runningBar.priority } - if s.newLineExtendFn != nil { - s.bufNL = bytes.NewBuffer(make([]byte, 0, s.width)) - } - - go b.serve(wg, s, cancel) + go b.serve(ctx, wg, s) return b } @@ -178,37 +186,27 @@ } // SetTotal sets total dynamically. -// Set final to true, when total is known, it will trigger bar complete event. -func (b *Bar) SetTotal(total int64, final bool) bool { +// Set complete to true, to trigger bar complete event now. +func (b *Bar) SetTotal(total int64, complete bool) { select { case b.operateState <- func(s *bState) { - if total > 0 { - s.total = total - } - if final { + s.total = total + if complete && !s.toComplete { s.current = s.total s.toComplete = true } }: - return true - case <-b.done: - return false - } -} - -// SetRefill sets fill rune to r, up until n. -func (b *Bar) SetRefill(n int, r rune) { - if n <= 0 { - return - } + case <-b.done: + } +} + +// SetRefill sets refill, if supported by underlying Filler. +func (b *Bar) SetRefill(amount int64) { b.operateState <- func(s *bState) { - s.refill = &refill{r, int64(n)} - } -} - -// RefillBy is deprecated, use SetRefill -func (b *Bar) RefillBy(n int, r rune) { - b.SetRefill(n, r) + if f, ok := s.filler.(interface{ SetRefill(int64) }); ok { + f.SetRefill(amount) + } + } } // Increment is a shorthand for b.IncrBy(1). @@ -217,13 +215,13 @@ } // IncrBy increments progress bar by amount of n. -// wdd is optional work duration i.e. time.Since(start), -// which expected to be provided, if any ewma based decorator is used. +// wdd is optional work duration i.e. time.Since(start), which expected +// to be provided, if any ewma based decorator is used. func (b *Bar) IncrBy(n int, wdd ...time.Duration) { select { case b.operateState <- func(s *bState) { s.current += int64(n) - if s.current >= s.total { + if s.total > 0 && s.current >= s.total { s.current = s.total s.toComplete = true } @@ -238,9 +236,9 @@ // Completed reports whether the bar is in completed state. func (b *Bar) Completed() bool { // omit select here, because primary usage of the method is for loop - // condition, like for !bar.Completed() {...} - // so when toComplete=true it is called once (at which time, the bar is still alive), - // then quits the loop and never suppose to be called afterwards. + // condition, like for !bar.Completed() {...} so when toComplete=true + // it is called once (at which time, the bar is still alive), then + // quits the loop and never suppose to be called afterwards. return <-b.boolCh } @@ -253,8 +251,9 @@ } } -func (b *Bar) serve(wg *sync.WaitGroup, s *bState, cancel <-chan struct{}) { +func (b *Bar) serve(ctx context.Context, wg *sync.WaitGroup, s *bState) { defer wg.Done() + cancel := ctx.Done() for { select { case op := <-b.operateState: @@ -322,8 +321,6 @@ } func (s *bState) draw(termWidth int) io.Reader { - defer s.bufA.WriteByte('\n') - if s.panicMsg != "" { return strings.NewReader(fmt.Sprintf(fmt.Sprintf("%%.%ds\n", termWidth), s.panicMsg)) } @@ -338,77 +335,32 @@ s.bufA.WriteString(d.Decor(stat)) } + if s.barClearOnComplete && s.completeFlushed { + s.bufA.WriteByte('\n') + return io.MultiReader(s.bufP, s.bufA) + } + prependCount := utf8.RuneCount(s.bufP.Bytes()) appendCount := utf8.RuneCount(s.bufA.Bytes()) - if s.barClearOnComplete && s.completeFlushed { - return io.MultiReader(s.bufP, s.bufA) - } - - s.fillBar(s.width) - barCount := utf8.RuneCount(s.bufB.Bytes()) - totalCount := prependCount + barCount + appendCount - if spaceCount := 0; totalCount > termWidth { - if !s.trimLeftSpace { - spaceCount++ - } - if !s.trimRightSpace { - spaceCount++ - } - s.fillBar(termWidth - prependCount - appendCount - spaceCount) - } - + if !s.trimSpace { + // reserve space for edge spaces + termWidth -= 2 + s.bufB.WriteByte(' ') + } + + if prependCount+s.width+appendCount > termWidth { + s.filler.Fill(s.bufB, termWidth-prependCount-appendCount, stat) + } else { + s.filler.Fill(s.bufB, s.width, stat) + } + + if !s.trimSpace { + s.bufB.WriteByte(' ') + } + + s.bufA.WriteByte('\n') return io.MultiReader(s.bufP, s.bufB, s.bufA) -} - -func (s *bState) fillBar(width int) { - defer func() { - s.bufB.WriteRune(s.runes[rRight]) - if !s.trimRightSpace { - s.bufB.WriteByte(' ') - } - }() - - s.bufB.Reset() - if !s.trimLeftSpace { - s.bufB.WriteByte(' ') - } - s.bufB.WriteRune(s.runes[rLeft]) - if width <= 2 { - return - } - - // bar s.width without leftEnd and rightEnd runes - barWidth := width - 2 - - completedWidth := internal.Percentage(s.total, s.current, int64(barWidth)) - - if s.refill != nil { - till := internal.Percentage(s.total, s.refill.till, int64(barWidth)) - // append refill rune - var i int64 - for i = 0; i < till; i++ { - s.bufB.WriteRune(s.refill.char) - } - for i = till; i < completedWidth; i++ { - s.bufB.WriteRune(s.runes[rFill]) - } - } else { - var i int64 - for i = 0; i < completedWidth; i++ { - s.bufB.WriteRune(s.runes[rFill]) - } - } - - if completedWidth < int64(barWidth) && completedWidth > 0 { - _, size := utf8.DecodeLastRune(s.bufB.Bytes()) - s.bufB.Truncate(s.bufB.Len() - size) - s.bufB.WriteRune(s.runes[rTip]) - } - - for i := completedWidth; i < int64(barWidth); i++ { - s.bufB.WriteRune(s.runes[rEmpty]) - } } func (s *bState) wSyncTable() [][]chan int { @@ -442,14 +394,6 @@ } } -func strToBarRunes(format string) (array barRunes) { - for i, n := 0, 0; len(format) > 0; i++ { - array[i], n = utf8.DecodeRuneInString(format) - format = format[n:] - } - return -} - func countLines(b []byte) int { return bytes.Count(b, []byte("\n")) } diff --git a/bar_filler.go b/bar_filler.go new file mode 100644 index 0000000..4e9285c --- /dev/null +++ b/bar_filler.go @@ -0,0 +1,111 @@ +package mpb + +import ( + "io" + "unicode/utf8" + + "github.com/vbauerster/mpb/decor" + "github.com/vbauerster/mpb/internal" +) + +const ( + rLeft = iota + rFill + rTip + rEmpty + rRight + rRevTip + rRefill +) + +var defaultBarStyle = "[=>-]<+" + +type barFiller struct { + format [][]byte + refillAmount int64 + reverse bool +} + +func newDefaultBarFiller() Filler { + bf := &barFiller{ + format: make([][]byte, utf8.RuneCountInString(defaultBarStyle)), + } + bf.setStyle(defaultBarStyle) + return bf +} + +func (s *barFiller) setStyle(style string) { + if !utf8.ValidString(style) { + return + } + src := make([][]byte, 0, utf8.RuneCountInString(style)) + for _, r := range style { + src = append(src, []byte(string(r))) + } + copy(s.format, src) +} + +func (s *barFiller) setReverse() { + s.reverse = true +} + +func (s *barFiller) SetRefill(amount int64) { + s.refillAmount = amount +} + +func (s *barFiller) Fill(w io.Writer, width int, stat *decor.Statistics) { + + // don't count rLeft and rRight [brackets] + width -= 2 + if width < 2 { + return + } + + w.Write(s.format[rLeft]) + if width == 2 { + w.Write(s.format[rRight]) + return + } + + bb := make([][]byte, width) + + cwidth := int(internal.Percentage(stat.Total, stat.Current, int64(width))) + + for i := 0; i < cwidth; i++ { + bb[i] = s.format[rFill] + } + + if s.refillAmount > 0 { + var rwidth int + if s.refillAmount > stat.Current { + rwidth = cwidth + } else { + rwidth = int(internal.Percentage(stat.Total, int64(s.refillAmount), int64(width))) + } + for i := 0; i < rwidth; i++ { + bb[i] = s.format[rRefill] + } + } + + if cwidth > 0 && cwidth < width { + bb[cwidth-1] = s.format[rTip] + } + + for i := cwidth; i < width; i++ { + bb[i] = s.format[rEmpty] + } + + if s.reverse { + if cwidth > 0 && cwidth < width { + bb[cwidth-1] = s.format[rRevTip] + } + for i := len(bb) - 1; i >= 0; i-- { + w.Write(bb[i]) + } + } else { + for i := 0; i < len(bb); i++ { + w.Write(bb[i]) + } + } + w.Write(s.format[rRight]) +} diff --git a/bar_option.go b/bar_option.go index e33bce4..e9a4bd2 100644 --- a/bar_option.go +++ b/bar_option.go @@ -6,11 +6,10 @@ "github.com/vbauerster/mpb/decor" ) -// BarOption is a function option which changes the default behavior of a bar, -// if passed to p.AddBar(int64, ...BarOption) +// BarOption is a function option which changes the default behavior of a bar. type BarOption func(*bState) -// AppendDecorators let you inject decorators to the bar's right side +// AppendDecorators let you inject decorators to the bar's right side. func AppendDecorators(appenders ...decor.Decorator) BarOption { return func(s *bState) { for _, decorator := range appenders { @@ -25,7 +24,7 @@ } } -// PrependDecorators let you inject decorators to the bar's left side +// PrependDecorators let you inject decorators to the bar's left side. func PrependDecorators(prependers ...decor.Decorator) BarOption { return func(s *bState) { for _, decorator := range prependers { @@ -40,85 +39,155 @@ } } -// BarTrimLeft trims left side space of the bar -func BarTrimLeft() BarOption { - return func(s *bState) { - s.trimLeftSpace = true - } -} - -// BarTrimRight trims right space of the bar -func BarTrimRight() BarOption { - return func(s *bState) { - s.trimRightSpace = true - } -} - -// BarTrim trims both left and right spaces of the bar -func BarTrim() BarOption { - return func(s *bState) { - s.trimLeftSpace = true - s.trimRightSpace = true - } -} - -// BarID overwrites internal bar id +// BarID sets bar id. func BarID(id int) BarOption { return func(s *bState) { s.id = id } } -// BarRemoveOnComplete is a flag, if set whole bar line will be removed on complete event. -// If both BarRemoveOnComplete and BarClearOnComplete are set, first bar section gets cleared -// and then whole bar line gets removed completely. +// BarWidth sets bar width independent of the container. +func BarWidth(width int) BarOption { + return func(s *bState) { + s.width = width + } +} + +// BarRemoveOnComplete is a flag, if set whole bar line will be removed +// on complete event. If both BarRemoveOnComplete and BarClearOnComplete +// are set, first bar section gets cleared and then whole bar line +// gets removed completely. func BarRemoveOnComplete() BarOption { return func(s *bState) { s.removeOnComplete = true } } -// BarReplaceOnComplete is indicator for delayed bar start, after the `runningBar` is complete. -// To achieve bar replacement effect, `runningBar` should has its `BarRemoveOnComplete` option set. +// BarReplaceOnComplete is indicator for delayed bar start, after the +// `runningBar` is complete. To achieve bar replacement effect, +// `runningBar` should has its `BarRemoveOnComplete` option set. func BarReplaceOnComplete(runningBar *Bar) BarOption { + return BarParkTo(runningBar) +} + +// BarParkTo same as BarReplaceOnComplete +func BarParkTo(runningBar *Bar) BarOption { return func(s *bState) { s.runningBar = runningBar } } -// BarClearOnComplete is a flag, if set will clear bar section on complete event. -// If you need to remove a whole bar line, refer to BarRemoveOnComplete. +// BarClearOnComplete is a flag, if set will clear bar section on +// complete event. If you need to remove a whole bar line, refer to +// BarRemoveOnComplete. func BarClearOnComplete() BarOption { return func(s *bState) { s.barClearOnComplete = true } } -// BarPriority sets bar's priority. -// Zero is highest priority, i.e. bar will be on top. -// If `BarReplaceOnComplete` option is supplied, this option is ignored. +// BarPriority sets bar's priority. Zero is highest priority, i.e. bar +// will be on top. If `BarReplaceOnComplete` option is supplied, this +// option is ignored. func BarPriority(priority int) BarOption { return func(s *bState) { s.priority = priority } } -// BarNewLineExtend takes user defined efn, which gets called each render cycle. -// Any write to provided writer of efn, will appear on new line of respective bar. +// BarNewLineExtend takes user defined efn, which gets called each +// render cycle. Any write to provided writer of efn, will appear on +// new line of respective bar. func BarNewLineExtend(efn func(io.Writer, *decor.Statistics)) BarOption { return func(s *bState) { s.newLineExtendFn = efn } } -func barWidth(w int) BarOption { +// TrimSpace trims bar's edge spaces. +func TrimSpace() BarOption { return func(s *bState) { - s.width = w + s.trimSpace = true } } -func barFormat(format string) BarOption { +// BarStyle sets custom bar style, default one is "[=>-]<+". +// +// '[' left bracket rune +// +// '=' fill rune +// +// '>' tip rune +// +// '-' empty rune +// +// ']' right bracket rune +// +// '<' reverse tip rune, used when BarReverse option is set +// +// '+' refill rune, used when *Bar.SetRefill(int64) is called +// +// It's ok to provide first five runes only, for example mpb.BarStyle("╢▌▌░╟") +func BarStyle(style string) BarOption { + chk := func(filler Filler) (interface{}, bool) { + if style == "" { + return nil, false + } + t, ok := filler.(*barFiller) + return t, ok + } + cb := func(t interface{}) { + t.(*barFiller).setStyle(style) + } + return MakeFillerTypeSpecificBarOption(chk, cb) +} + +// BarReverse reverse mode, bar will progress from right to left. +func BarReverse() BarOption { + chk := func(filler Filler) (interface{}, bool) { + t, ok := filler.(*barFiller) + return t, ok + } + cb := func(t interface{}) { + t.(*barFiller).setReverse() + } + return MakeFillerTypeSpecificBarOption(chk, cb) +} + +// SpinnerStyle sets custom spinner style. +// Effective when Filler type is spinner. +func SpinnerStyle(frames []string) BarOption { + chk := func(filler Filler) (interface{}, bool) { + if len(frames) == 0 { + return nil, false + } + t, ok := filler.(*spinnerFiller) + return t, ok + } + cb := func(t interface{}) { + t.(*spinnerFiller).frames = frames + } + return MakeFillerTypeSpecificBarOption(chk, cb) +} + +// MakeFillerTypeSpecificBarOption makes BarOption specific to Filler's +// actual type. If you implement your own Filler, so most probably +// you'll need this. See BarStyle or SpinnerStyle for example. +func MakeFillerTypeSpecificBarOption( + typeChecker func(Filler) (interface{}, bool), + cb func(interface{}), +) BarOption { return func(s *bState) { - s.runes = strToBarRunes(format) + if t, ok := typeChecker(s.filler); ok { + cb(t) + } } } + +// OptionOnCondition returns option when condition evaluates to true. +func OptionOnCondition(option BarOption, condition func() bool) BarOption { + if condition() { + return option + } + return nil +} diff --git a/bar_test.go b/bar_test.go index f97d5c7..51a8924 100644 --- a/bar_test.go +++ b/bar_test.go @@ -7,6 +7,7 @@ "strings" "testing" "time" + "unicode/utf8" . "github.com/vbauerster/mpb" "github.com/vbauerster/mpb/decor" @@ -60,11 +61,11 @@ total := 100 till := 30 - refillRune := '+' - - bar := p.AddBar(int64(total), BarTrim()) - - bar.SetRefill(till, refillRune) + refillRune, _ := utf8.DecodeLastRuneInString(DefaultBarStyle) + + bar := p.AddBar(int64(total), TrimSpace()) + + bar.SetRefill(int64(till)) bar.IncrBy(till) for i := 0; i < total-till; i++ { @@ -76,10 +77,93 @@ wantBar := fmt.Sprintf("[%s%s]", strings.Repeat(string(refillRune), till-1), - strings.Repeat("=", total-till-1)) - - if !strings.Contains(buf.String(), wantBar) { - t.Errorf("Want bar: %s, got bar: %s\n", wantBar, buf.String()) + strings.Repeat("=", total-till-1), + ) + + got := string(getLastLine(buf.Bytes())) + + 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 := New(WithOutput(&buf)) + + total := 50 + + bar := p.AddBar(int64(total), + 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) + } +} + +func TestBarHas100PercentWithBarRemoveOnComplete(t *testing.T) { + var buf bytes.Buffer + + p := New(WithOutput(&buf)) + + total := 50 + + bar := p.AddBar(int64(total), + BarRemoveOnComplete(), + AppendDecorators(decor.Percentage()), + ) + + 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) + } +} + +func TestBarStyle(t *testing.T) { + var buf bytes.Buffer + customFormat := "╢▌▌░╟" + p := New(WithOutput(&buf)) + total := 80 + bar := p.AddBar(int64(total), BarStyle(customFormat), TrimSpace()) + + for i := 0; i < total; i++ { + bar.Increment() + time.Sleep(10 * time.Millisecond) + } + + p.Wait() + + runes := []rune(customFormat) + wantBar := fmt.Sprintf("%s%s%s", + string(runes[0]), + strings.Repeat(string(runes[1]), total-2), + string(runes[len(runes)-1]), + ) + got := string(getLastLine(buf.Bytes())) + + if got != wantBar { + t.Errorf("Want bar: %q:%d, got bar: %q:%d\n", wantBar, utf8.RuneCountInString(wantBar), got, utf8.RuneCountInString(got)) } } diff --git a/cwriter/writer.go b/cwriter/writer.go index 0b1470d..638237c 100644 --- a/cwriter/writer.go +++ b/cwriter/writer.go @@ -22,8 +22,8 @@ clearCursorAndLine = cursorUp + clearLine ) -// Writer is a buffered the writer that updates the terminal. -// The contents of writer will be flushed when Flush is called. +// 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 @@ -64,11 +64,13 @@ return w.buf.WriteString(s) } -// ReadFrom reads from the provided io.Reader and writes to the underlying buffer. +// 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 { tw, _, err := terminal.GetSize(w.fd) diff --git a/cwriter/writer_windows.go b/cwriter/writer_windows.go index dad7f50..747a634 100644 --- a/cwriter/writer_windows.go +++ b/cwriter/writer_windows.go @@ -8,7 +8,7 @@ "syscall" "unsafe" - "github.com/mattn/go-isatty" + isatty "github.com/mattn/go-isatty" ) var kernel32 = syscall.NewLazyDLL("kernel32.dll") diff --git a/decor/counters.go b/decor/counters.go index e4161dc..7d581ee 100644 --- a/decor/counters.go +++ b/decor/counters.go @@ -141,12 +141,14 @@ return Counters(0, pairFormat, wcc...) } -// CountersKibiByte is a wrapper around Counters with predefined unit UnitKiB (bytes/1024). +// CountersKibiByte is a wrapper around Counters with predefined unit +// UnitKiB (bytes/1024). func CountersKibiByte(pairFormat string, wcc ...WC) Decorator { return Counters(UnitKiB, pairFormat, wcc...) } -// CountersKiloByte is a wrapper around Counters with predefined unit UnitKB (bytes/1000). +// CountersKiloByte is a wrapper around Counters with predefined unit +// UnitKB (bytes/1000). func CountersKiloByte(pairFormat string, wcc ...WC) Decorator { return Counters(UnitKB, pairFormat, wcc...) } diff --git a/decor/decorator.go b/decor/decorator.go index 6aaf6c8..2fe40ae 100644 --- a/decor/decorator.go +++ b/decor/decorator.go @@ -31,8 +31,12 @@ DSyncSpaceR = DSyncWidth | DextraSpace | DidentRight ) +// TimeStyle enum. +type TimeStyle int + +// TimeStyle kinds. const ( - ET_STYLE_GO = iota + ET_STYLE_GO TimeStyle = iota ET_STYLE_HHMMSS ET_STYLE_HHMM ET_STYLE_MMSS @@ -47,35 +51,37 @@ } // Decorator interface. -// A decorator must implement this interface, in order to be used with mpb library. +// A decorator must implement this interface, in order to be used with +// mpb library. type Decorator interface { Decor(*Statistics) string Syncable } // Syncable interface. -// All decorators implement this interface implicitly. -// Its Syncable method exposes width sync channel, if sync is enabled. +// All decorators implement this interface implicitly. Its Syncable +// method exposes width sync channel, if sync is enabled. type Syncable interface { Syncable() (bool, chan int) } // OnCompleteMessenger interface. -// Decorators implementing this interface suppose to return provided string on complete event. +// Decorators implementing this interface suppose to return provided +// string on complete event. type OnCompleteMessenger interface { OnCompleteMessage(string) } // AmountReceiver interface. -// If decorator needs to receive increment amount, -// so this is the right interface to implement. +// If decorator needs to receive increment amount, so this is the right +// interface to implement. type AmountReceiver interface { NextAmount(int, ...time.Duration) } // ShutdownListener interface. -// If decorator needs to be notified once upon bar shutdown event, -// so this is the right interface to implement. +// If decorator needs to be notified once upon bar shutdown event, so +// this is the right interface to implement. type ShutdownListener interface { Shutdown() } @@ -90,6 +96,7 @@ // WC is a struct with two public fields W and C, both of int type. // W represents width and C represents bit set of width related config. +// A decorator should embed WC, in order to become Syncable. type WC struct { W int C int @@ -126,12 +133,13 @@ } } +// Syncable is implementation of Syncable interface. func (wc *WC) Syncable() (bool, chan int) { return (wc.C & DSyncWidth) != 0, wc.wsync } -// OnComplete returns decorator, which wraps provided decorator, with sole -// purpose to display provided message on complete event. +// OnComplete returns decorator, which wraps provided decorator, with +// sole purpose to display provided message on complete event. // // `decorator` Decorator to wrap // diff --git a/decor/elapsed.go b/decor/elapsed.go index 649d40a..b2e7585 100644 --- a/decor/elapsed.go +++ b/decor/elapsed.go @@ -10,7 +10,7 @@ // `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS] // // `wcc` optional WC config -func Elapsed(style int, wcc ...WC) Decorator { +func Elapsed(style TimeStyle, wcc ...WC) Decorator { var wc WC for _, widthConf := range wcc { wc = widthConf @@ -26,7 +26,7 @@ type elapsedDecorator struct { WC - style int + style TimeStyle startTime time.Time msg string completeMsg *string diff --git a/decor/eta.go b/decor/eta.go index 44a1f03..e8dc979 100644 --- a/decor/eta.go +++ b/decor/eta.go @@ -6,7 +6,6 @@ "time" "github.com/VividCortex/ewma" - "github.com/vbauerster/mpb/internal" ) type TimeNormalizer func(time.Duration) time.Duration @@ -18,7 +17,7 @@ // `age` is the previous N samples to average over. // // `wcc` optional WC config -func EwmaETA(style int, age float64, wcc ...WC) Decorator { +func EwmaETA(style TimeStyle, age float64, wcc ...WC) Decorator { return MovingAverageETA(style, ewma.NewMovingAverage(age), NopNormalizer(), wcc...) } @@ -31,7 +30,7 @@ // `normalizer` available implementations are [NopNormalizer|FixedIntervalTimeNormalizer|MaxTolerateTimeNormalizer] // // `wcc` optional WC config -func MovingAverageETA(style int, average MovingAverage, normalizer TimeNormalizer, wcc ...WC) Decorator { +func MovingAverageETA(style TimeStyle, average MovingAverage, normalizer TimeNormalizer, wcc ...WC) Decorator { var wc WC for _, widthConf := range wcc { wc = widthConf @@ -48,7 +47,7 @@ type movingAverageETA struct { WC - style int + style TimeStyle average ewma.MovingAverage completeMsg *string normalizer TimeNormalizer @@ -59,7 +58,7 @@ return d.FormatMsg(*d.completeMsg) } - v := internal.Round(d.average.Value()) + v := math.Round(d.average.Value()) remaining := d.normalizer(time.Duration((st.Total - st.Current) * int64(v))) hours := int64((remaining / time.Hour) % 60) minutes := int64((remaining / time.Minute) % 60) @@ -105,7 +104,7 @@ // `style` one of [ET_STYLE_GO|ET_STYLE_HHMMSS|ET_STYLE_HHMM|ET_STYLE_MMSS] // // `wcc` optional WC config -func AverageETA(style int, wcc ...WC) Decorator { +func AverageETA(style TimeStyle, wcc ...WC) Decorator { var wc WC for _, widthConf := range wcc { wc = widthConf @@ -121,7 +120,7 @@ type averageETA struct { WC - style int + style TimeStyle startTime time.Time completeMsg *string } @@ -133,7 +132,7 @@ var str string timeElapsed := time.Since(d.startTime) - v := internal.Round(float64(timeElapsed) / float64(st.Current)) + v := math.Round(float64(timeElapsed) / float64(st.Current)) if math.IsInf(v, 0) || math.IsNaN(v) { v = 0 } diff --git a/decor/moving-average.go b/decor/moving-average.go index f9596a2..fcd2689 100644 --- a/decor/moving-average.go +++ b/decor/moving-average.go @@ -6,9 +6,9 @@ "github.com/VividCortex/ewma" ) -// MovingAverage is the interface that computes a moving average over a time- -// series stream of numbers. The average may be over a window or exponentially -// decaying. +// MovingAverage is the interface that computes a moving average over +// a time-series stream of numbers. The average may be over a window +// or exponentially decaying. type MovingAverage interface { Add(float64) Value() float64 @@ -57,7 +57,8 @@ s.count++ } -// NewMedianEwma is ewma based MovingAverage, which gets its values from median MovingAverage. +// NewMedianEwma is ewma based MovingAverage, which gets its values +// from median MovingAverage. func NewMedianEwma(age ...float64) MovingAverage { return &medianEwma{ MovingAverage: ewma.NewMovingAverage(age...), diff --git a/decor/speed.go b/decor/speed.go index 395e5d0..74658ce 100644 --- a/decor/speed.go +++ b/decor/speed.go @@ -137,7 +137,8 @@ return MovingAverageSpeed(unit, unitFormat, ewma.NewMovingAverage(age), wcc...) } -// MovingAverageSpeed decorator relies on MovingAverage implementation to calculate its average. +// MovingAverageSpeed decorator relies on MovingAverage implementation +// to calculate its average. // // `unit` one of [0|UnitKiB|UnitKB] zero for no unit // diff --git a/draw_test.go b/draw_test.go index 77bf654..9374528 100644 --- a/draw_test.go +++ b/draw_test.go @@ -3,197 +3,293 @@ import ( "bytes" "testing" + "unicode/utf8" ) func TestDraw(t *testing.T) { // key is termWidth - testSuite := map[int]map[string]struct { + testSuite := map[int][]struct { + name string total, current int64 barWidth int - barRefill *refill + trimSpace bool + rup int64 want string }{ + 2: { + { + name: "t,c,bw{60,20,80}", + total: 60, + current: 20, + barWidth: 80, + want: " ", + }, + { + name: "t,c,bw,trim{60,20,80,true}", + total: 60, + current: 20, + barWidth: 80, + trimSpace: true, + want: "", + }, + }, + 3: { + { + name: "t,c,bw{60,20,80}", + total: 60, + current: 20, + barWidth: 80, + want: " ", + }, + { + name: "t,c,bw,trim{60,20,80,true}", + total: 60, + current: 20, + barWidth: 80, + trimSpace: true, + want: "", + }, + }, + 4: { + { + name: "t,c,bw{60,20,80}", + total: 60, + current: 20, + barWidth: 80, + want: " ", + }, + { + name: "t,c,bw,trim{60,20,80,true}", + total: 60, + current: 20, + barWidth: 80, + trimSpace: true, + want: "[]", + }, + }, + 5: { + { + name: "t,c,bw{60,20,80}", + total: 60, + current: 20, + barWidth: 80, + want: " ", + }, + { + name: "t,c,bw,trim{60,20,80,true}", + total: 60, + current: 20, + barWidth: 80, + trimSpace: true, + want: "[>--]", + }, + }, + 6: { + { + name: "t,c,bw{60,20,80}", + total: 60, + current: 20, + barWidth: 80, + want: " [] ", + }, + { + name: "t,c,bw,trim{60,20,80,true}", + total: 60, + current: 20, + barWidth: 80, + trimSpace: true, + want: "[>---]", + }, + }, + 7: { + { + name: "t,c,bw{60,20,80}", + total: 60, + current: 20, + barWidth: 80, + want: " [>--] ", + }, + { + name: "t,c,bw,trim{60,20,80,true}", + total: 60, + current: 20, + barWidth: 80, + trimSpace: true, + want: "[=>---]", + }, + }, + 8: { + { + name: "t,c,bw{60,20,80}", + total: 60, + current: 20, + barWidth: 80, + want: " [>---] ", + }, + { + name: "t,c,bw,trim{60,20,80,true}", + total: 60, + current: 20, + barWidth: 80, + trimSpace: true, + want: "[=>----]", + }, + }, + 80: { + { + name: "t,c,bw{60,20,80}", + total: 60, + current: 20, + barWidth: 80, + want: " [========================>---------------------------------------------------] ", + }, + { + name: "t,c,bw,trim{60,20,80,true}", + total: 60, + current: 20, + barWidth: 80, + trimSpace: true, + want: "[=========================>----------------------------------------------------]", + }, + }, 100: { - "t,c,bw{100,100,0}": { + { + name: "t,c,bw{100,100,0}", total: 100, current: 0, barWidth: 100, - want: "[--------------------------------------------------------------------------------------------------]", - }, - "t,c,bw{100,1,100}": { + want: " [------------------------------------------------------------------------------------------------] ", + }, + { + name: "t,c,bw,trim{100,100,0,true}", + total: 100, + current: 0, + barWidth: 100, + trimSpace: true, + want: "[--------------------------------------------------------------------------------------------------]", + }, + { + name: "t,c,bw{100,1,100}", total: 100, current: 1, barWidth: 100, - want: "[>-------------------------------------------------------------------------------------------------]", - }, - "t,c,bw{100,40,100}": { + want: " [>-----------------------------------------------------------------------------------------------] ", + }, + { + name: "t,c,bw,trim{100,1,100,true}", + total: 100, + current: 1, + barWidth: 100, + trimSpace: true, + want: "[>-------------------------------------------------------------------------------------------------]", + }, + { + name: "t,c,bw{100,33,100}", + total: 100, + current: 33, + barWidth: 100, + want: " [===============================>----------------------------------------------------------------] ", + }, + { + name: "t,c,bw,trim{100,33,100,true}", + total: 100, + current: 33, + barWidth: 100, + trimSpace: true, + want: "[===============================>------------------------------------------------------------------]", + }, + { + name: "t,c,bw,rup{100,33,100,33}", + total: 100, + current: 33, + barWidth: 100, + rup: 33, + want: " [+++++++++++++++++++++++++++++++>----------------------------------------------------------------] ", + }, + { + name: "t,c,bw,rup,trim{100,33,100,33,true}", + total: 100, + current: 33, + barWidth: 100, + rup: 33, + trimSpace: true, + want: "[+++++++++++++++++++++++++++++++>------------------------------------------------------------------]", + }, + { + name: "t,c,bw,rup{100,40,100,32}", total: 100, current: 40, barWidth: 100, - want: "[======================================>-----------------------------------------------------------]", - }, - "t,c,bw{100,40,100}refill{'+', 32}": { + rup: 33, + want: " [++++++++++++++++++++++++++++++++=====>----------------------------------------------------------] ", + }, + { + name: "t,c,bw,rup,trim{100,40,100,32,true}", total: 100, current: 40, barWidth: 100, - barRefill: &refill{'+', 32}, - want: "[+++++++++++++++++++++++++++++++=======>-----------------------------------------------------------]", - }, - "t,c,bw{100,99,100}": { + rup: 33, + trimSpace: true, + want: "[++++++++++++++++++++++++++++++++======>-----------------------------------------------------------]", + }, + { + name: "t,c,bw{100,99,100}", total: 100, current: 99, barWidth: 100, - want: "[================================================================================================>-]", - }, - "t,c,bw{100,100,100}": { + want: " [==============================================================================================>-] ", + }, + { + name: "t,c,bw,trim{100,99,100,true}", + total: 100, + current: 99, + barWidth: 100, + trimSpace: true, + want: "[================================================================================================>-]", + }, + { + name: "t,c,bw{100,100,100}", total: 100, current: 100, barWidth: 100, - want: "[==================================================================================================]", - }, - }, - 2: { - "t,c,bw{0,0,100}": { - barWidth: 100, - want: "[]", - }, - "t,c,bw{60,20,80}": { - total: 60, - current: 20, - barWidth: 80, - want: "[]", - }, - }, - 3: { - "t,c,bw{100,20,100}": { - total: 100, - current: 20, - barWidth: 100, - want: "[-]", - }, - "t,c,bw{100,98,100}": { - total: 100, - current: 98, - barWidth: 100, - want: "[=]", - }, - "t,c,bw{100,100,100}": { - total: 100, - current: 100, - barWidth: 100, - want: "[=]", - }, - }, - 5: { - "t,c,bw{100,20,100}": { - total: 100, - current: 20, - barWidth: 100, - want: "[>--]", - }, - "t,c,bw{100,98,100}": { - total: 100, - current: 98, - barWidth: 100, - want: "[===]", - }, - "t,c,bw{100,100,100}": { - total: 100, - current: 100, - barWidth: 100, - want: "[===]", - }, - }, - 6: { - "t,c,bw{100,20,100}": { - total: 100, - current: 20, - barWidth: 100, - want: "[>---]", - }, - "t,c,bw{100,98,100}": { - total: 100, - current: 98, - barWidth: 100, - want: "[====]", - }, - "t,c,bw{100,100,100}": { - total: 100, - current: 100, - barWidth: 100, - want: "[====]", - }, - }, - 20: { - "t,c,bw{100,20,100}": { - total: 100, - current: 20, - barWidth: 100, - want: "[===>--------------]", - }, - "t,c,bw{100,60,100}": { - total: 100, - current: 60, - barWidth: 100, - want: "[==========>-------]", - }, - "t,c,bw{100,98,100}": { - total: 100, - current: 98, - barWidth: 100, - want: "[==================]", - }, - "t,c,bw{100,100,100}": { - total: 100, - current: 100, - barWidth: 100, - want: "[==================]", - }, - }, - 50: { - "t,c,bw{100,20,100}": { - total: 100, - current: 20, - barWidth: 100, - want: "[=========>--------------------------------------]", - }, - "t,c,bw{100,60,100}": { - total: 100, - current: 60, - barWidth: 100, - want: "[============================>-------------------]", - }, - "t,c,bw{100,98,100}": { - total: 100, - current: 98, - barWidth: 100, - want: "[==============================================>-]", - }, - "t,c,bw{100,100,100}": { - total: 100, - current: 100, - barWidth: 100, - want: "[================================================]", + want: " [================================================================================================] ", + }, + { + name: "t,c,bw,trim{100,100,100,true}", + total: 100, + current: 100, + barWidth: 100, + trimSpace: true, + want: "[==================================================================================================]", }, }, } var tmpBuf bytes.Buffer for termWidth, cases := range testSuite { - for name, tc := range cases { + for _, tc := range cases { s := newTestState() s.width = tc.barWidth s.total = tc.total s.current = tc.current - if tc.barRefill != nil { - s.refill = tc.barRefill + s.trimSpace = tc.trimSpace + if tc.rup > 0 { + if f, ok := s.filler.(interface{ SetRefill(int64) }); ok { + f.SetRefill(tc.rup) + } } tmpBuf.Reset() tmpBuf.ReadFrom(s.draw(termWidth)) - got := tmpBuf.String() - want := tc.want + "\n" - if got != want { - t.Errorf("termWidth %d; %s: want: %s %d, got: %s %d\n", termWidth, name, want, len(want), got, len(got)) + by := tmpBuf.Bytes() + by = by[:len(by)-1] + + if utf8.RuneCount(by) > termWidth { + t.Errorf("termWidth:%d %q barWidth:%d overflow termWidth\n", termWidth, tc.name, utf8.RuneCount(by)) + } + + got := string(by) + if got != tc.want { + t.Errorf("termWidth:%d %q want: %q %d, got: %q %d\n", termWidth, tc.name, tc.want, len(tc.want), got, len(got)) } } } @@ -201,12 +297,10 @@ func newTestState() *bState { s := &bState{ - trimLeftSpace: true, - trimRightSpace: true, - bufP: new(bytes.Buffer), - bufB: new(bytes.Buffer), - bufA: new(bytes.Buffer), + filler: newDefaultBarFiller(), + bufP: new(bytes.Buffer), + bufB: new(bytes.Buffer), + bufA: new(bytes.Buffer), } - s.runes = strToBarRunes(pformat) return s } diff --git a/example_test.go b/example_test.go index 97f1ded..2f4f9d3 100644 --- a/example_test.go +++ b/example_test.go @@ -15,8 +15,6 @@ p := mpb.New( // override default (80) width mpb.WithWidth(64), - // override default "[=>-]" format - mpb.WithFormat("╢▌▌░╟"), // override default 120ms refresh rate mpb.WithRefreshRate(180*time.Millisecond), ) @@ -25,6 +23,8 @@ name := "Single Bar:" // adding a single bar bar := p.AddBar(int64(total), + // override default "[=>-]" style + mpb.BarStyle("╢▌▌░╟"), mpb.PrependDecorators( // display our name with one space on the right decor.Name(name, decor.WC{W: len(name) + 1, C: decor.DidentRight}), diff --git a/examples/cancel/main.go b/examples/cancel/main.go index 001e201..9da8ed3 100644 --- a/examples/cancel/main.go +++ b/examples/cancel/main.go @@ -1,5 +1,3 @@ -//+build go1.7 - package main import ( diff --git a/examples/differentWidth/main.go b/examples/differentWidth/main.go new file mode 100644 index 0000000..c327b34 --- /dev/null +++ b/examples/differentWidth/main.go @@ -0,0 +1,60 @@ +package main + +import ( + "fmt" + "math/rand" + "sync" + "time" + + "github.com/vbauerster/mpb" + "github.com/vbauerster/mpb/decor" +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +func main() { + var wg sync.WaitGroup + p := mpb.New( + mpb.WithWaitGroup(&wg), + // container's width. + mpb.WithWidth(60), + ) + total, numBars := 100, 3 + wg.Add(numBars) + + for i := 0; i < numBars; i++ { + name := fmt.Sprintf("Bar#%d:", i) + bar := p.AddBar(int64(total), + // set BarWidth 40 for bar 1 and 2 + mpb.OptionOnCondition(mpb.BarWidth(40), func() bool { return i > 0 }), + mpb.PrependDecorators( + // simple name decorator + decor.Name(name), + // decor.DSyncWidth bit enables column width synchronization + decor.Percentage(decor.WCSyncSpace), + ), + mpb.AppendDecorators( + // replace ETA decorator with "done" message, OnComplete event + decor.OnComplete( + // ETA decorator with ewma age of 60 + decor.EwmaETA(decor.ET_STYLE_GO, 60), "done", + ), + ), + ) + // simulating some work + go func() { + defer wg.Done() + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + start := time.Now() + time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) + // ewma based decorators require work duration measurement + bar.IncrBy(1, time.Since(start)) + } + }() + } + // wait for all bars to complete and flush + p.Wait() +} diff --git a/examples/io/single/main.go b/examples/io/single/main.go index a5eb2e4..09d6c11 100644 --- a/examples/io/single/main.go +++ b/examples/io/single/main.go @@ -39,11 +39,10 @@ p := mpb.New( mpb.WithWidth(60), - mpb.WithFormat("[=>-|"), mpb.WithRefreshRate(180*time.Millisecond), ) - bar := p.AddBar(size, + bar := p.AddBar(size, mpb.BarStyle("[=>-|"), mpb.PrependDecorators( decor.CountersKibiByte("% 6.1f / % 6.1f"), ), diff --git a/examples/remove/main.go b/examples/remove/main.go index 9bfd4f3..308978f 100644 --- a/examples/remove/main.go +++ b/examples/remove/main.go @@ -23,14 +23,8 @@ for i := 0; i < numBars; i++ { name := fmt.Sprintf("Bar#%d:", i) - - var bOption mpb.BarOption - if i == 0 { - bOption = mpb.BarRemoveOnComplete() - } - b := p.AddBar(int64(total), mpb.BarID(i), - bOption, + mpb.OptionOnCondition(mpb.BarRemoveOnComplete(), func() bool { return i == 0 }), mpb.PrependDecorators( decor.Name(name), decor.EwmaETA(decor.ET_STYLE_GO, 60, decor.WCSyncSpace), diff --git a/examples/singleBar/main.go b/examples/singleBar/main.go index 7fe249f..c712afb 100644 --- a/examples/singleBar/main.go +++ b/examples/singleBar/main.go @@ -12,8 +12,6 @@ p := mpb.New( // override default (80) width mpb.WithWidth(64), - // override default "[=>-]" format - mpb.WithFormat("╢▌▌░╟"), // override default 120ms refresh rate mpb.WithRefreshRate(180*time.Millisecond), ) @@ -22,6 +20,8 @@ name := "Single Bar:" // adding a single bar bar := p.AddBar(int64(total), + // override default "[=>-]" style + mpb.BarStyle("╢▌▌░╟"), mpb.PrependDecorators( // display our name with one space on the right decor.Name(name, decor.WC{W: len(name) + 1, C: decor.DidentRight}), diff --git a/examples/spinner/main.go b/examples/spinner/main.go new file mode 100644 index 0000000..1f81897 --- /dev/null +++ b/examples/spinner/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "fmt" + "math/rand" + "sync" + "time" + + "github.com/vbauerster/mpb" + "github.com/vbauerster/mpb/decor" +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +func main() { + var wg sync.WaitGroup + p := mpb.New( + mpb.WithWaitGroup(&wg), + mpb.WithWidth(13), + ) + total, numBars := 101, 3 + wg.Add(numBars) + + for i := 0; i < numBars; i++ { + name := fmt.Sprintf("Bar#%d:", i) + var bar *mpb.Bar + if i == 0 { + bar = p.AddBar(int64(total), + // override default "[=>-]" style + mpb.BarStyle("╢▌▌░╟"), + 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", + ), + ), + ) + } else { + bar = p.AddSpinner(int64(total), mpb.SpinnerOnMiddle, + // override default {"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} style + mpb.SpinnerStyle([]string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"}), + mpb.PrependDecorators( + // simple name decorator + decor.Name(name), + ), + mpb.AppendDecorators( + // replace ETA decorator with "done" message, OnComplete event + decor.OnComplete( + // ETA decorator with ewma age of 60 + decor.EwmaETA(decor.ET_STYLE_GO, 60), "done", + ), + ), + ) + } + + // simulating some work + go func() { + defer wg.Done() + max := 100 * time.Millisecond + for i := 0; i < total; i++ { + start := time.Now() + time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10) + // ewma based decorators require work duration measurement + bar.IncrBy(1, time.Since(start)) + } + }() + } + // wait for all bars to complete and flush + p.Wait() +} diff --git a/examples/stress/main.go b/examples/stress/main.go index b1e99fb..09d4e55 100644 --- a/examples/stress/main.go +++ b/examples/stress/main.go @@ -20,7 +20,10 @@ func main() { var wg sync.WaitGroup - p := mpb.New(mpb.WithWaitGroup(&wg)) + p := mpb.New( + mpb.WithWaitGroup(&wg), + mpb.WithRefreshRate(50*time.Millisecond), + ) wg.Add(totalBars) for i := 0; i < totalBars; i++ { diff --git a/export_test.go b/export_test.go index 0bc28fe..1d05eda 100644 --- a/export_test.go +++ b/export_test.go @@ -1,3 +1,6 @@ package mpb -var SyncWidth = syncWidth +var ( + SyncWidth = syncWidth + DefaultBarStyle = defaultBarStyle +) diff --git a/internal/percentage.go b/internal/percentage.go index 3c8defb..0483d25 100644 --- a/internal/percentage.go +++ b/internal/percentage.go @@ -1,4 +1,6 @@ package internal + +import "math" // Percentage is a helper function, to calculate percentage. func Percentage(total, current, width int64) int64 { @@ -6,5 +8,5 @@ return 0 } p := float64(width*current) / float64(total) - return int64(Round(p)) + return int64(math.Round(p)) } diff --git a/internal/percentage_test.go b/internal/percentage_test.go index ce5a25e..4a649ce 100644 --- a/internal/percentage_test.go +++ b/internal/percentage_test.go @@ -1,73 +1,72 @@ package internal -import ( - "testing" -) +import "testing" func TestPercentage(t *testing.T) { // key is barWidth - testSuite := map[int64]map[string]struct { + testSuite := map[int64][]struct { + name string total, current, expected int64 }{ 100: { - "t,c,e{-1,-1,0}": {-1, -1, 0}, - "t,c,e{0,-1,0}": {0, -1, 0}, - "t,c,e{0,0,0}": {0, 0, 0}, - "t,c,e{0,1,0}": {0, 1, 0}, - "t,c,e{100,0,0}": {100, 0, 0}, - "t,c,e{100,10,10}": {100, 10, 10}, - "t,c,e{100,15,15}": {100, 15, 15}, - "t,c,e{100,50,50}": {100, 50, 50}, - "t,c,e{100,99,99}": {100, 99, 99}, - "t,c,e{100,100,100}": {100, 100, 100}, - "t,c,e{100,101,101}": {100, 101, 101}, - "t,c,e{100,102,101}": {100, 102, 102}, - "t,c,e{120,0,0}": {120, 0, 0}, - "t,c,e{120,10,8}": {120, 10, 8}, - "t,c,e{120,15,13}": {120, 15, 13}, - "t,c,e{120,50,42}": {120, 50, 42}, - "t,c,e{120,60,50}": {120, 60, 50}, - "t,c,e{120,99,83}": {120, 99, 83}, - "t,c,e{120,101,84}": {120, 101, 84}, - "t,c,e{120,118,98}": {120, 118, 98}, - "t,c,e{120,119,99}": {120, 119, 99}, - "t,c,e{120,120,100}": {120, 120, 100}, - "t,c,e{120,121,101}": {120, 121, 101}, - "t,c,e{120,122,101}": {120, 122, 102}, + {"t,c,e{-1,-1,0}", -1, -1, 0}, + {"t,c,e{0,-1,0}", 0, -1, 0}, + {"t,c,e{0,0,0}", 0, 0, 0}, + {"t,c,e{0,1,0}", 0, 1, 0}, + {"t,c,e{100,0,0}", 100, 0, 0}, + {"t,c,e{100,10,10}", 100, 10, 10}, + {"t,c,e{100,15,15}", 100, 15, 15}, + {"t,c,e{100,50,50}", 100, 50, 50}, + {"t,c,e{100,99,99}", 100, 99, 99}, + {"t,c,e{100,100,100}", 100, 100, 100}, + {"t,c,e{100,101,101}", 100, 101, 101}, + {"t,c,e{100,102,101}", 100, 102, 102}, + {"t,c,e{120,0,0}", 120, 0, 0}, + {"t,c,e{120,10,8}", 120, 10, 8}, + {"t,c,e{120,15,13}", 120, 15, 13}, + {"t,c,e{120,50,42}", 120, 50, 42}, + {"t,c,e{120,60,50}", 120, 60, 50}, + {"t,c,e{120,99,83}", 120, 99, 83}, + {"t,c,e{120,101,84}", 120, 101, 84}, + {"t,c,e{120,118,98}", 120, 118, 98}, + {"t,c,e{120,119,99}", 120, 119, 99}, + {"t,c,e{120,120,100}", 120, 120, 100}, + {"t,c,e{120,121,101}", 120, 121, 101}, + {"t,c,e{120,122,101}", 120, 122, 102}, }, 80: { - "t,c,e{-1,-1,0}": {-1, -1, 0}, - "t,c,e{0,-1,0}": {0, -1, 0}, - "t,c,e{0,0,0}": {0, 0, 0}, - "t,c,e{0,1,0}": {0, 1, 0}, - "t,c,e{100,0,0}": {100, 0, 0}, - "t,c,e{100,10,8}": {100, 10, 8}, - "t,c,e{100,15,12}": {100, 15, 12}, - "t,c,e{100,50,40}": {100, 50, 40}, - "t,c,e{100,99,79}": {100, 99, 79}, - "t,c,e{100,100,80}": {100, 100, 80}, - "t,c,e{100,101,81}": {100, 101, 81}, - "t,c,e{100,102,82}": {100, 102, 82}, - "t,c,e{120,0,0}": {120, 0, 0}, - "t,c,e{120,10,7}": {120, 10, 7}, - "t,c,e{120,15,10}": {120, 15, 10}, - "t,c,e{120,50,33}": {120, 50, 33}, - "t,c,e{120,60,40}": {120, 60, 40}, - "t,c,e{120,99,66}": {120, 99, 66}, - "t,c,e{120,101,67}": {120, 101, 67}, - "t,c,e{120,118,79}": {120, 118, 79}, - "t,c,e{120,119,79}": {120, 119, 79}, - "t,c,e{120,120,80}": {120, 120, 80}, - "t,c,e{120,121,81}": {120, 121, 81}, - "t,c,e{120,122,81}": {120, 122, 81}, + {"t,c,e{-1,-1,0}", -1, -1, 0}, + {"t,c,e{0,-1,0}", 0, -1, 0}, + {"t,c,e{0,0,0}", 0, 0, 0}, + {"t,c,e{0,1,0}", 0, 1, 0}, + {"t,c,e{100,0,0}", 100, 0, 0}, + {"t,c,e{100,10,8}", 100, 10, 8}, + {"t,c,e{100,15,12}", 100, 15, 12}, + {"t,c,e{100,50,40}", 100, 50, 40}, + {"t,c,e{100,99,79}", 100, 99, 79}, + {"t,c,e{100,100,80}", 100, 100, 80}, + {"t,c,e{100,101,81}", 100, 101, 81}, + {"t,c,e{100,102,82}", 100, 102, 82}, + {"t,c,e{120,0,0}", 120, 0, 0}, + {"t,c,e{120,10,7}", 120, 10, 7}, + {"t,c,e{120,15,10}", 120, 15, 10}, + {"t,c,e{120,50,33}", 120, 50, 33}, + {"t,c,e{120,60,40}", 120, 60, 40}, + {"t,c,e{120,99,66}", 120, 99, 66}, + {"t,c,e{120,101,67}", 120, 101, 67}, + {"t,c,e{120,118,79}", 120, 118, 79}, + {"t,c,e{120,119,79}", 120, 119, 79}, + {"t,c,e{120,120,80}", 120, 120, 80}, + {"t,c,e{120,121,81}", 120, 121, 81}, + {"t,c,e{120,122,81}", 120, 122, 81}, }, } for width, cases := range testSuite { - for name, tc := range cases { + for _, tc := range cases { got := Percentage(tc.total, tc.current, width) if got != tc.expected { - t.Errorf("width %d; %s: Expected: %d, got: %d\n", width, name, tc.expected, got) + t.Errorf("width %d; %s: Expected: %d, got: %d\n", width, tc.name, tc.expected, got) } } } diff --git a/internal/round.go b/internal/round.go deleted file mode 100644 index c54a789..0000000 --- a/internal/round.go +++ /dev/null @@ -1,49 +0,0 @@ -package internal - -import "math" - -const ( - uvone = 0x3FF0000000000000 - mask = 0x7FF - shift = 64 - 11 - 1 - bias = 1023 - signMask = 1 << 63 - fracMask = 1<= 0.5 { - // return t + Copysign(1, x) - // } - // return t - // } - bits := math.Float64bits(x) - e := uint(bits>>shift) & mask - if e < bias { - // Round abs(x) < 1 including denormals. - bits &= signMask // +-0 - if e == bias-1 { - bits |= uvone // +-1 - } - } else if e < bias+shift { - // Round any abs(x) >= 1 containing a fractional component [0,1). - // - // Numbers with larger exponents are returned unchanged since they - // must be either an integer, infinity, or NaN. - const half = 1 << (shift - 1) - e -= bias - bits += half >> e - bits &^= fracMask >> e - } - return math.Float64frombits(bits) -} diff --git a/options.go b/options.go index 05d2ecf..44a6ee3 100644 --- a/options.go +++ b/options.go @@ -1,29 +1,30 @@ package mpb import ( + "context" "io" "sync" "time" - "unicode/utf8" "github.com/vbauerster/mpb/cwriter" ) -// ProgressOption is a function option which changes the default behavior of -// progress pool, if passed to mpb.New(...ProgressOption) +// ProgressOption is a function option which changes the default +// behavior of progress pool, if passed to mpb.New(...ProgressOption). type ProgressOption func(*pState) -// WithWaitGroup provides means to have a single joint point. -// If *sync.WaitGroup is provided, you can safely call just p.Wait() -// without calling Wait() on provided *sync.WaitGroup. -// Makes sense when there are more than one bar to render. +// WithWaitGroup provides means to have a single joint point. If +// *sync.WaitGroup is provided, you can safely call just p.Wait() +// without calling Wait() on provided *sync.WaitGroup. Makes sense +// when there are more than one bar to render. func WithWaitGroup(wg *sync.WaitGroup) ProgressOption { return func(s *pState) { s.uwg = wg } } -// WithWidth overrides default width 80 +// WithWidth sets container width. Default is 80. Bars inherit this +// width, as long as no BarWidth is applied. func WithWidth(w int) ProgressOption { return func(s *pState) { if w >= 0 { @@ -32,16 +33,7 @@ } } -// WithFormat overrides default bar format "[=>-]" -func WithFormat(format string) ProgressOption { - return func(s *pState) { - if utf8.RuneCountInString(format) == formatLen { - s.format = format - } - } -} - -// WithRefreshRate overrides default 120ms refresh rate +// WithRefreshRate overrides default 120ms refresh rate. func WithRefreshRate(d time.Duration) ProgressOption { return func(s *pState) { if d < 10*time.Millisecond { @@ -59,22 +51,25 @@ } } -// WithCancel provide your cancel channel, -// which you plan to close at some point. -func WithCancel(ch <-chan struct{}) ProgressOption { +// WithContext provided context will be used for cancellation purposes. +func WithContext(ctx context.Context) ProgressOption { return func(s *pState) { - s.cancel = ch + if ctx == nil { + return + } + s.ctx = ctx } } -// WithShutdownNotifier provided chanel will be closed, after all bars have been rendered. +// WithShutdownNotifier provided chanel will be closed, after all bars +// have been rendered. func WithShutdownNotifier(ch chan struct{}) ProgressOption { return func(s *pState) { s.shutdownNotifier = ch } } -// WithOutput overrides default output os.Stdout +// WithOutput overrides default output os.Stdout. func WithOutput(w io.Writer) ProgressOption { return func(s *pState) { if w == nil { diff --git a/options_go1.7.go b/options_go1.7.go deleted file mode 100644 index ca9a5ba..0000000 --- a/options_go1.7.go +++ /dev/null @@ -1,15 +0,0 @@ -//+build go1.7 - -package mpb - -import "context" - -// WithContext provided context will be used for cancellation purposes -func WithContext(ctx context.Context) ProgressOption { - return func(s *pState) { - if ctx == nil { - panic("ctx must not be nil") - } - s.cancel = ctx.Done() - } -} diff --git a/progress.go b/progress.go index d95fe45..f9e25af 100644 --- a/progress.go +++ b/progress.go @@ -2,6 +2,7 @@ import ( "container/heap" + "context" "fmt" "io" "io/ioutil" @@ -17,8 +18,6 @@ prr = 120 * time.Millisecond // default width pwidth = 80 - // default format - pformat = "[=>-]" ) // Progress represents the container that renders Progress bars @@ -42,24 +41,24 @@ pMatrix map[int][]chan int aMatrix map[int][]chan int - // following are provided by user + // following are provided/overrided by user + ctx context.Context uwg *sync.WaitGroup manualRefreshCh <-chan time.Time - cancel <-chan struct{} shutdownNotifier chan struct{} waitBars map[*Bar]*Bar debugOut io.Writer } -// New creates new Progress instance, which orchestrates bars rendering process. -// Accepts mpb.ProgressOption funcs for customization. +// New creates new Progress instance, which orchestrates bars rendering +// process. Accepts mpb.ProgressOption funcs for customization. func New(options ...ProgressOption) *Progress { pq := make(priorityQueue, 0) heap.Init(&pq) s := &pState{ + ctx: context.Background(), bHeap: &pq, width: pwidth, - format: pformat, cw: cwriter.New(os.Stdout), rr: prr, waitBars: make(map[*Bar]*Bar), @@ -84,12 +83,28 @@ // AddBar creates a new progress bar and adds to the container. func (p *Progress) AddBar(total int64, options ...BarOption) *Bar { + return p.Add(total, newDefaultBarFiller(), options...) +} + +// AddSpinner creates a new spinner bar and adds to the container. +func (p *Progress) AddSpinner(total int64, alignment SpinnerAlignment, options ...BarOption) *Bar { + filler := &spinnerFiller{ + frames: defaultSpinnerStyle, + alignment: alignment, + } + return p.Add(total, filler, options...) +} + +// Add creates a bar which renders itself by provided filler. +func (p *Progress) Add(total int64, filler Filler, options ...BarOption) *Bar { + if filler == nil { + filler = newDefaultBarFiller() + } p.wg.Add(1) result := make(chan *Bar) select { case p.operateState <- func(s *pState) { - options = append(options, barWidth(s.width), barFormat(s.format)) - b := newBar(p.wg, s.idCounter, total, s.cancel, options...) + b := newBar(s.ctx, p.wg, filler, s.idCounter, s.width, total, options...) if b.runningBar != nil { s.waitBars[b.runningBar] = b } else { @@ -106,10 +121,10 @@ } } -// Abort is only effective while bar progress is running, -// it means remove bar now without waiting for its completion. -// If bar is already completed, there is nothing to abort. -// If you need to remove bar after completion, use BarRemoveOnComplete BarOption. +// Abort is only effective while bar progress is running, it means +// remove bar now without waiting for its completion. If bar is already +// completed, there is nothing to abort. If you need to remove bar +// after completion, use BarRemoveOnComplete BarOption. func (p *Progress) Abort(b *Bar, remove bool) { select { case p.operateState <- func(s *pState) { @@ -145,9 +160,10 @@ } } -// Wait first waits for user provided *sync.WaitGroup, if any, -// then waits far all bars to complete and finally shutdowns master goroutine. -// After this method has been called, there is no way to reuse *Progress instance. +// Wait first waits for user provided *sync.WaitGroup, if any, then +// waits far all bars to complete and finally shutdowns master goroutine. +// After this method has been called, there is no way to reuse *Progress +// instance. func (p *Progress) Wait() { if p.uwg != nil { p.uwg.Wait() @@ -205,8 +221,8 @@ defer func() { if frameReader.toShutdown { // shutdown at next flush, in other words decrement underlying WaitGroup - // only after the bar with completed state has been flushed. - // this ensures no bar ends up with less than 100% rendered. + // only after the bar with completed state has been flushed. this + // ensures no bar ends up with less than 100% rendered. s.shutdownPending = append(s.shutdownPending, bar) if replacementBar, ok := s.waitBars[bar]; ok { heap.Push(s.bHeap, replacementBar) diff --git a/progress_go1.7_test.go b/progress_go1.7_test.go deleted file mode 100644 index 6b4adda..0000000 --- a/progress_go1.7_test.go +++ /dev/null @@ -1,50 +0,0 @@ -//+build go1.7 - -package mpb_test - -import ( - "context" - "io/ioutil" - "testing" - "time" - - "github.com/vbauerster/mpb" -) - -func TestWithContext(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - shutdown := make(chan struct{}) - p := mpb.New( - mpb.WithOutput(ioutil.Discard), - mpb.WithContext(ctx), - mpb.WithShutdownNotifier(shutdown), - ) - - total := 1000 - numBars := 3 - bars := make([]*mpb.Bar, 0, numBars) - for i := 0; i < numBars; i++ { - bar := p.AddBar(int64(total)) - bars = append(bars, bar) - go func() { - for !bar.Completed() { - time.Sleep(randomDuration(40 * time.Millisecond)) - bar.Increment() - } - }() - } - - time.AfterFunc(100*time.Millisecond, cancel) - - p.Wait() - for _, bar := range bars { - if bar.Current() >= int64(total) { - t.Errorf("bar %d: total = %d, current = %d\n", bar.ID(), total, bar.Current()) - } - } - select { - case <-shutdown: - case <-time.After(100 * time.Millisecond): - t.Error("Progress didn't stop") - } -} diff --git a/progress_test.go b/progress_test.go index 0c4388c..de34785 100644 --- a/progress_test.go +++ b/progress_test.go @@ -2,6 +2,7 @@ import ( "bytes" + "context" "fmt" "io/ioutil" "math/rand" @@ -9,6 +10,7 @@ "testing" "time" + "github.com/vbauerster/mpb" . "github.com/vbauerster/mpb" "github.com/vbauerster/mpb/cwriter" ) @@ -80,61 +82,38 @@ p.Wait() } -func TestWithCancel(t *testing.T) { - cancel := make(chan struct{}) +func TestWithContext(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) shutdown := make(chan struct{}) - p := New( - WithOutput(ioutil.Discard), - WithCancel(cancel), - WithShutdownNotifier(shutdown), + p := mpb.New( + mpb.WithOutput(ioutil.Discard), + mpb.WithContext(ctx), + mpb.WithRefreshRate(50*time.Millisecond), + mpb.WithShutdownNotifier(shutdown), ) - for i := 0; i < 2; i++ { - bar := p.AddBar(int64(1000), BarID(i)) + 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)) - bar.Increment() } }() } - time.AfterFunc(100*time.Millisecond, func() { - close(cancel) - }) + time.Sleep(50 * time.Millisecond) + cancel() p.Wait() - select { case <-shutdown: - case <-time.After(200 * time.Millisecond): - t.FailNow() - } -} - -func TestWithFormat(t *testing.T) { - var buf bytes.Buffer - customFormat := "╢▌▌░╟" - p := New(WithOutput(&buf), WithFormat(customFormat)) - bar := p.AddBar(100, BarTrim()) - - for i := 0; i < 100; i++ { - if i == 33 { - p.Abort(bar, true) - break - } - time.Sleep(randomDuration(100 * time.Millisecond)) - bar.Increment() - } - - p.Wait() - - lastLine := getLastLine(buf.Bytes()) - - for _, r := range customFormat { - if !bytes.ContainsRune(lastLine, r) { - t.Errorf("Rune %#U not found in bar\n", r) - } + case <-time.After(100 * time.Millisecond): + t.Error("Progress didn't stop") } } diff --git a/proxyreader_test.go b/proxyreader_test.go index d1c0862..7006674 100644 --- a/proxyreader_test.go +++ b/proxyreader_test.go @@ -1,7 +1,6 @@ package mpb_test import ( - "bytes" "io" "io/ioutil" "strings" @@ -18,27 +17,36 @@ cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.` +type testReader struct { + io.Reader + called bool +} + +func (r *testReader) Read(p []byte) (n int, err error) { + r.called = true + return r.Reader.Read(p) +} + func TestProxyReader(t *testing.T) { - var buf bytes.Buffer - p := mpb.New(mpb.WithOutput(&buf)) - reader := strings.NewReader(content) + p := mpb.New(mpb.WithOutput(ioutil.Discard)) + + reader := &testReader{Reader: strings.NewReader(content)} total := len(content) - bar := p.AddBar(100, mpb.BarTrim()) - preader := bar.ProxyReader(reader) + bar := p.AddBar(100, mpb.TrimSpace()) - if _, ok := preader.(io.Closer); !ok { - t.Error("type assertion to io.Closer is not ok") - } - - written, err := io.Copy(ioutil.Discard, preader) + written, err := io.Copy(ioutil.Discard, bar.ProxyReader(reader)) if err != nil { t.Errorf("Error copying from reader: %+v\n", err) } p.Wait() + if !reader.called { + t.Error("Read not called") + } + if written != int64(total) { t.Errorf("Expected written: %d, got: %d\n", total, written) } diff --git a/spinner_filler.go b/spinner_filler.go new file mode 100644 index 0000000..36299fe --- /dev/null +++ b/spinner_filler.go @@ -0,0 +1,48 @@ +package mpb + +import ( + "io" + "strings" + "unicode/utf8" + + "github.com/vbauerster/mpb/decor" +) + +// SpinnerAlignment enum. +type SpinnerAlignment int + +// SpinnerAlignment kinds. +const ( + SpinnerOnLeft SpinnerAlignment = iota + SpinnerOnMiddle + SpinnerOnRight +) + +var defaultSpinnerStyle = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} + +type spinnerFiller struct { + frames []string + count uint + alignment SpinnerAlignment +} + +func (s *spinnerFiller) Fill(w io.Writer, width int, stat *decor.Statistics) { + + frame := s.frames[s.count%uint(len(s.frames))] + frameWidth := utf8.RuneCountInString(frame) + + if width < frameWidth { + return + } + + switch rest := width - frameWidth; s.alignment { + case SpinnerOnLeft: + io.WriteString(w, frame+strings.Repeat(" ", rest)) + case SpinnerOnMiddle: + str := strings.Repeat(" ", rest/2) + frame + strings.Repeat(" ", rest/2+rest%2) + io.WriteString(w, str) + case SpinnerOnRight: + io.WriteString(w, strings.Repeat(" ", rest)+frame) + } + s.count++ +}