diff --git a/bar.go b/bar.go index b801e7f..c711197 100644 --- a/bar.go +++ b/bar.go @@ -11,20 +11,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,16 +32,19 @@ shutdown chan struct{} } +type filler interface { + fill(w io.Writer, width int, s *decor.Statistics) +} + type ( bState struct { + filler filler id int width int + alignment int total int64 current int64 - runes barRunes - spinner []rune - trimLeftSpace bool - trimRightSpace bool + trimSpace bool toComplete bool removeOnComplete bool barClearOnComplete bool @@ -74,8 +64,8 @@ runningBar *Bar } refill struct { - char rune - till int64 + r rune + limit int64 } frameReader struct { io.Reader @@ -85,7 +75,7 @@ } ) -func newBar(wg *sync.WaitGroup, id int, total int64, cancel <-chan struct{}, options ...BarOption) *Bar { +func newBar(wg *sync.WaitGroup, id, width int, total int64, cancel <-chan struct{}, options ...BarOption) *Bar { if total <= 0 { total = time.Now().Unix() } @@ -93,6 +83,7 @@ s := &bState{ id: id, priority: id, + width: width, total: total, } @@ -102,9 +93,12 @@ } } - 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)) + s.bufP = bytes.NewBuffer(make([]byte, 0, width)) + s.bufB = bytes.NewBuffer(make([]byte, 0, width)) + s.bufA = bytes.NewBuffer(make([]byte, 0, width)) + if s.newLineExtendFn != nil { + s.bufNL = bytes.NewBuffer(make([]byte, 0, width)) + } b := &Bar{ priority: s.priority, @@ -122,10 +116,6 @@ b.priority = b.runningBar.priority } - if s.newLineExtendFn != nil { - s.bufNL = bytes.NewBuffer(make([]byte, 0, s.width)) - } - go b.serve(wg, s, cancel) return b } @@ -203,13 +193,10 @@ return } 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 bf, ok := s.filler.(*barFiller); ok { + bf.refill = &refill{r, int64(n)} + } + } } // Increment is a shorthand for b.IncrBy(1). @@ -323,8 +310,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)) } @@ -339,103 +324,107 @@ 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.fill(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.fill(termWidth - prependCount - appendCount - spaceCount) - } - + // s.bufB.Reset() + 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) fill(width int) { - if len(s.spinner) != 0 { - s.fillSpinner(width) - } else { - s.fillBar(width) - } -} - -func (s *bState) fillSpinner(width int) { - s.bufB.Reset() - - if !s.trimLeftSpace { - s.bufB.WriteByte(' ') - } - - spin := []byte(string(s.spinner[s.current%int64(len(s.spinner))])) - for _, b := range spin { - s.bufB.WriteByte(b) - } - - for i := len(spin); i < width; i++ { - s.bufB.WriteRune(' ') - } -} - -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) fillSpinner(width int) { +// s.bufB.Reset() +// s.bufB.WriteByte(' ') + +// if width <= 2 { +// s.bufB.WriteByte(' ') +// return +// } + +// r := s.bType.format[s.current%int64(len(s.bType.format))] + +// switch s.alignment { +// case alignLeft: +// s.bufB.WriteRune(r) +// s.bufB.Write(bytes.Repeat([]byte(" "), width-1)) +// case alignMiddle: +// mid := width / 2 +// mod := width % 2 +// s.bufB.Write(bytes.Repeat([]byte(" "), mid-1+mod)) +// s.bufB.WriteRune(r) +// s.bufB.Write(bytes.Repeat([]byte(" "), mid)) +// case alignRight: +// s.bufB.Write(bytes.Repeat([]byte(" "), width-1)) +// s.bufB.WriteRune(r) +// } + +// s.bufB.WriteByte(' ') +// } + +// func (s *bState) fillBar(width int) { +// s.bufB.Reset() +// s.bufB.WriteByte(' ') + +// // don't count rLeft and rRight [brackets] with trailing spaces +// width -= 4 + +// if width <= 2 { +// s.bufB.WriteByte(' ') +// return +// } + +// s.bufB.WriteRune(s.bType.format[rLeft]) +// completedWidth := internal.Percentage(s.total, s.current, int64(width)) + +// if s.refill != nil { +// till := internal.Percentage(s.total, s.refill.till, int64(width)) +// // append refill rune +// for i := int64(0); i < till; i++ { +// s.bufB.WriteRune(s.refill.char) +// } +// for i := till; i < completedWidth; i++ { +// s.bufB.WriteRune(s.bType.format[rFill]) +// } +// } else { +// for i := int64(0); i < completedWidth; i++ { +// s.bufB.WriteRune(s.bType.format[rFill]) +// } +// } + +// if completedWidth < int64(width) && completedWidth > 0 { +// _, size := utf8.DecodeLastRune(s.bufB.Bytes()) +// s.bufB.Truncate(s.bufB.Len() - size) +// s.bufB.WriteRune(s.bType.format[rTip]) +// } + +// for i := completedWidth; i < int64(width); i++ { +// s.bufB.WriteRune(s.bType.format[rEmpty]) +// } + +// s.bufB.WriteRune(s.bType.format[rRight]) +// s.bufB.WriteByte(' ') +// } func (s *bState) wSyncTable() [][]chan int { columns := make([]chan int, 0, len(s.pDecorators)+len(s.aDecorators)) @@ -468,14 +457,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..7263805 --- /dev/null +++ b/bar_filler.go @@ -0,0 +1,67 @@ +package mpb + +import ( + "bytes" + "io" + + "github.com/vbauerster/mpb/decor" + "github.com/vbauerster/mpb/internal" +) + +const ( + rLeft = iota + rFill + rTip + rEmpty + rRight +) + +var defaultBarStyle = []rune("[=>-]") + +type barFiller struct { + format []rune + refill *refill +} + +func (s *barFiller) fill(w io.Writer, width int, stat *decor.Statistics) { + + w.Write([]byte(string(s.format[rLeft]))) + + // don't count rLeft and rRight [brackets] + width -= 2 + + if width <= 2 { + w.Write([]byte(string(s.format[rRight]))) + return + } + + progressWidth := internal.Percentage(stat.Total, stat.Current, int64(width)) + needTip := progressWidth < int64(width) && progressWidth > 0 + + if needTip { + progressWidth-- + } + + if s.refill != nil { + // append refill rune + times := internal.Percentage(stat.Total, s.refill.limit, int64(width)) + w.Write(s.repeat(s.refill.r, int(times))) + rest := progressWidth - times + w.Write(s.repeat(s.format[rFill], int(rest))) + } else { + w.Write(s.repeat(s.format[rFill], int(progressWidth))) + } + + if needTip { + w.Write([]byte(string(s.format[rTip]))) + progressWidth++ + } + + rest := int64(width) - progressWidth + w.Write(s.repeat(s.format[rEmpty], int(rest))) + w.Write([]byte(string(s.format[rRight]))) +} + +func (s *barFiller) repeat(r rune, count int) []byte { + return bytes.Repeat([]byte(string(r)), count) +} diff --git a/bar_option.go b/bar_option.go index 800210d..343644a 100644 --- a/bar_option.go +++ b/bar_option.go @@ -2,15 +2,15 @@ import ( "io" + "unicode/utf8" "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 +25,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,29 +40,7 @@ } } -// 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 @@ -111,20 +89,69 @@ } } -func barSpinner(spinner string) BarOption { +// BarStyle sets custom bar style. +func BarStyle(style string) BarOption { return func(s *bState) { - s.spinner = []rune(spinner) + if style == "" { + return + } + if bf, ok := s.filler.(*barFiller); ok { + if !utf8.ValidString(style) { + panic("invalid style string") + } + defaultFormat := bf.format + bf.format = []rune(style) + if len(bf.format) < 5 { + bf.format = defaultFormat + } + } } } -func barWidth(w int) BarOption { +// SpinnerStyle sets custom Spinner style. +func SpinnerStyle(frames []string) BarOption { return func(s *bState) { - s.width = w + if len(frames) == 0 { + return + } + if bf, ok := s.filler.(*spinnerFiller); ok { + bf.frames = frames + } } } -func barFormat(format string) BarOption { +// AlignLeft align spinner on left, default. +// Applicable fo spinner bar type only. +// func AlignLeft() BarOption { +// return func(s *bState) { +// if bf, ok := s.filler.(*spinnerFiller); ok { +// bf.alignment = alignLeft +// } +// } +// } + +// AlignMiddle align spinner on the middle. +// Applicable fo spinner bar type only. +// func AlignMiddle() BarOption { +// return func(s *bState) { +// if bf, ok := s.filler.(*spinnerFiller); ok { +// bf.alignment = alignMiddle +// } +// } +// } + +// AlignRight align spinner on right. +// Applicable fo spinner bar type only. +// func AlignRight() BarOption { +// return func(s *bState) { +// if bf, ok := s.filler.(*spinnerFiller); ok { +// bf.alignment = alignRight +// } +// } +// } + +func TrimSpace() BarOption { return func(s *bState) { - s.runes = strToBarRunes(format) + s.trimSpace = true } } diff --git a/examples/spinner/main.go b/examples/spinner/main.go index e21163e..2422744 100644 --- a/examples/spinner/main.go +++ b/examples/spinner/main.go @@ -18,26 +18,46 @@ var wg sync.WaitGroup p := mpb.New( mpb.WithWaitGroup(&wg), - mpb.WithSpinner("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"), + mpb.WithWidth(13), ) - total, numBars := 100, 3 + total, numBars := 101, 3 wg.Add(numBars) for i := 0; i < numBars; i++ { name := fmt.Sprintf("Bar#%d:", i) - bar := p.AddBar(int64(total), - mpb.PrependDecorators( - // simple name decorator - decor.Name(name), - ), - 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", + var bar *mpb.Bar + if i == 0 { + bar = p.AddBar(int64(total), + 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, + // 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() diff --git a/options.go b/options.go index 9240519..a477948 100644 --- a/options.go +++ b/options.go @@ -4,7 +4,6 @@ "io" "sync" "time" - "unicode/utf8" "github.com/vbauerster/mpb/cwriter" ) @@ -28,22 +27,6 @@ return func(s *pState) { if w >= 0 { s.width = w - } - } -} - -// WithSpinner overrides default bar format to use a spinner -func WithSpinner(spinner string) ProgressOption { - return func(s *pState) { - s.spinner = spinner - } -} - -// WithFormat overrides default bar format "[=>-]" -func WithFormat(format string) ProgressOption { - return func(s *pState) { - if utf8.RuneCountInString(format) == formatLen { - s.format = format } } } diff --git a/progress.go b/progress.go index 7387974..da8d206 100644 --- a/progress.go +++ b/progress.go @@ -17,8 +17,6 @@ prr = 120 * time.Millisecond // default width pwidth = 80 - // default format - pformat = "[=>-]" ) // Progress represents the container that renders Progress bars @@ -37,7 +35,6 @@ idCounter int width int format string - spinner string rr time.Duration cw *cwriter.Writer pMatrix map[int][]chan int @@ -60,7 +57,6 @@ s := &pState{ bHeap: &pq, width: pwidth, - format: pformat, cw: cwriter.New(os.Stdout), rr: prr, waitBars: make(map[*Bar]*Bar), @@ -85,15 +81,38 @@ // AddBar creates a new progress bar and adds to the container. func (p *Progress) AddBar(total int64, options ...BarOption) *Bar { + // make sure filler is initialized first + args := []BarOption{ + func(s *bState) { + s.filler = &barFiller{ + format: defaultBarStyle, + } + }, + } + args = append(args, options...) + return p.add(total, args...) +} + +func (p *Progress) AddSpinner(total int64, alignment SpinnerAlignment, options ...BarOption) *Bar { + // make sure filler is initialized first + args := []BarOption{ + func(s *bState) { + s.filler = &spinnerFiller{ + frames: defaultSpinnerStyle, + alignment: alignment, + } + }, + } + args = append(args, options...) + return p.add(total, args...) +} + +func (p *Progress) add(total int64, options ...BarOption) *Bar { p.wg.Add(1) result := make(chan *Bar) select { case p.operateState <- func(s *pState) { - options = append(options, barWidth(s.width), barFormat(s.format)) - if s.spinner != "" { - options = append(options, barSpinner(s.spinner)) - } - b := newBar(p.wg, s.idCounter, total, s.cancel, options...) + b := newBar(p.wg, s.idCounter, s.width, total, s.cancel, options...) if b.runningBar != nil { s.waitBars[b.runningBar] = b } else { diff --git a/spinner_filler.go b/spinner_filler.go new file mode 100644 index 0000000..2ba70c4 --- /dev/null +++ b/spinner_filler.go @@ -0,0 +1,44 @@ +package mpb + +import ( + "io" + "strings" + "unicode/utf8" + + "github.com/vbauerster/mpb/decor" +) + +type SpinnerAlignment int + +const ( + SpinnerOnLeft SpinnerAlignment = iota + SpinnerOnMiddle + SpinnerOnRight +) + +var defaultSpinnerStyle = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} + +type spinnerFiller struct { + frames []string + alignment SpinnerAlignment +} + +func (s *spinnerFiller) fill(w io.Writer, width int, stat *decor.Statistics) { + + frame := s.frames[stat.Current%int64(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) + } +}