diff --git a/bar.go b/bar.go index 45cff64..61780dc 100644 --- a/bar.go +++ b/bar.go @@ -8,9 +8,9 @@ "log" "strings" "time" - "unicode/utf8" "github.com/acarl005/stripansi" + "github.com/mattn/go-runewidth" "github.com/vbauerster/mpb/v5/decor" ) @@ -387,13 +387,13 @@ func (s *bState) draw(stat decor.Statistics) io.Reader { for _, d := range s.pDecorators { str := d.Decor(stat) - stat.OccupiedWidth += utf8.RuneCountInString(stripansi.Strip(str)) + stat.OccupiedWidth += runewidth.StringWidth(stripansi.Strip(str)) s.bufP.WriteString(str) } for _, d := range s.aDecorators { str := d.Decor(stat) - stat.OccupiedWidth += utf8.RuneCountInString(stripansi.Strip(str)) + stat.OccupiedWidth += runewidth.StringWidth(stripansi.Strip(str)) s.bufA.WriteString(str) } diff --git a/bar_filler_bar.go b/bar_filler_bar.go index 4b06906..dc714f0 100644 --- a/bar_filler_bar.go +++ b/bar_filler_bar.go @@ -4,6 +4,7 @@ "io" "unicode/utf8" + "github.com/mattn/go-runewidth" "github.com/vbauerster/mpb/v5/decor" "github.com/vbauerster/mpb/v5/internal" ) @@ -12,7 +13,7 @@ rLeft = iota rFill rTip - rEmpty + rSpace rRight rRevTip rRefill @@ -27,7 +28,7 @@ // // '3rd rune' stands for tip rune // -// '4th rune' stands for empty rune +// '4th rune' stands for space rune // // '5th rune' stands for right boundary rune // @@ -39,19 +40,24 @@ type barFiller struct { format [][]byte + rwidth []int tip []byte refill int64 reverse bool - flush func(w io.Writer, bb [][]byte) + flush func(io.Writer, *space, [][]byte) +} + +type space struct { + space []byte + width int + count int } // NewBarFiller constucts mpb.BarFiller, to be used with *Progress.Add(...) *Bar method. func NewBarFiller(style string, reverse bool) BarFiller { - if style == "" { - style = DefaultBarStyle - } bf := &barFiller{ - format: make([][]byte, utf8.RuneCountInString(style)), + format: make([][]byte, len(DefaultBarStyle)), + rwidth: make([]int, len(DefaultBarStyle)), reverse: reverse, } bf.SetStyle(style) @@ -62,9 +68,15 @@ if !utf8.ValidString(style) { return } - src := make([][]byte, 0, utf8.RuneCountInString(style)) + if style == "" { + style = DefaultBarStyle + } + src := make([][]byte, utf8.RuneCountInString(style)) + i := 0 for _, r := range style { - src = append(src, []byte(string(r))) + s.rwidth[i] = runewidth.RuneWidth(r) + src[i] = []byte(string(r)) + i++ } copy(s.format, src) s.SetReverse(s.reverse) @@ -88,52 +100,77 @@ func (s *barFiller) Fill(w io.Writer, reqWidth int, stat decor.Statistics) { width := internal.CalcWidthForBarFiller(reqWidth, stat.TermWidth-stat.OccupiedWidth) - width -= 2 // don't count rLeft and rRight as progress - if width < 2 { + // don't count rLeft and rRight as progress + brackets := s.rwidth[rLeft] + s.rwidth[rRight] + width -= brackets + if width < brackets { return } w.Write(s.format[rLeft]) defer w.Write(s.format[rRight]) - bb := make([][]byte, width) - cwidth := int(internal.PercentageRound(stat.Total, stat.Current, width)) - - for i := 0; i < cwidth; i++ { - bb[i] = s.format[rFill] + bb := make([][]byte, cwidth) + space := &space{ + space: s.format[rSpace], + width: s.rwidth[rSpace], + count: width - cwidth, + } + if cwidth == 0 { + s.flush(w, space, bb) + return } + index := 0 + if space.count != 0 { + bb[index] = s.tip + cwidth -= s.rwidth[rTip] + } else { + bb[index] = s.format[rFill] + cwidth -= s.rwidth[rFill] + } + index++ + + rwidth := 0 if s.refill > 0 { - var rwidth int - if s.refill > stat.Current { - rwidth = cwidth - } else { + rwidth = cwidth + if s.refill < stat.Current { rwidth = int(internal.PercentageRound(stat.Total, int64(s.refill), width)) } - for i := 0; i < rwidth; i++ { - bb[i] = s.format[rRefill] - } + cwidth -= rwidth } - if cwidth > 0 && cwidth < width { - bb[cwidth-1] = s.tip + for cwidth > 0 { + bb[index] = s.format[rFill] + cwidth -= s.rwidth[rFill] + index++ } - for i := cwidth; i < width; i++ { - bb[i] = s.format[rEmpty] + for rwidth > 0 { + bb[index] = s.format[rRefill] + rwidth -= s.rwidth[rRefill] + index++ } - s.flush(w, bb) + s.flush(w, space, bb) } -func regularFlush(w io.Writer, bb [][]byte) { +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.width + } +} + +func reverseFlush(w io.Writer, space *space, bb [][]byte) { + for space.count > 0 { + w.Write(space.space) + space.count -= space.width + } for i := 0; i < len(bb); i++ { w.Write(bb[i]) } } - -func reverseFlush(w io.Writer, bb [][]byte) { - for i := len(bb) - 1; i >= 0; i-- { - w.Write(bb[i]) - } -} diff --git a/decor/decorator.go b/decor/decorator.go index 1903b13..a7827ae 100644 --- a/decor/decorator.go +++ b/decor/decorator.go @@ -3,9 +3,9 @@ import ( "fmt" "time" - "unicode/utf8" "github.com/acarl005/stripansi" + "github.com/mattn/go-runewidth" ) const ( @@ -127,38 +127,35 @@ // W represents width and C represents bit set of width related config. // A decorator should embed WC, to enable width synchronization. type WC struct { - W int - C int - dynFormat string - wsync chan int + W int + C int + fill func(s string, w int) string + wsync chan int } // FormatMsg formats final message according to WC.W and WC.C. // Should be called by any Decorator implementation. func (wc *WC) FormatMsg(msg string) string { - var format string - runeCount := utf8.RuneCountInString(stripansi.Strip(msg)) - ansiCount := utf8.RuneCountInString(msg) - runeCount + pureWidth := runewidth.StringWidth(msg) + stripWidth := runewidth.StringWidth(stripansi.Strip(msg)) + maxCell := wc.W if (wc.C & DSyncWidth) != 0 { + cellCount := stripWidth if (wc.C & DextraSpace) != 0 { - runeCount++ + cellCount++ } - wc.wsync <- runeCount - max := <-wc.wsync - format = fmt.Sprintf(wc.dynFormat, ansiCount+max) - } else { - format = fmt.Sprintf(wc.dynFormat, ansiCount+wc.W) + wc.wsync <- cellCount + maxCell = <-wc.wsync } - return fmt.Sprintf(format, msg) + return wc.fill(msg, maxCell+(pureWidth-stripWidth)) } // Init initializes width related config. func (wc *WC) Init() WC { - wc.dynFormat = "%%" + wc.fill = runewidth.FillLeft if (wc.C & DidentRight) != 0 { - wc.dynFormat += "-" + wc.fill = runewidth.FillRight } - wc.dynFormat += "%ds" if (wc.C & DSyncWidth) != 0 { // it's deliberate choice to override wsync on each Init() call, // this way globals like WCSyncSpace can be reused diff --git a/decor/merge.go b/decor/merge.go index d6cc004..e41406a 100644 --- a/decor/merge.go +++ b/decor/merge.go @@ -1,11 +1,10 @@ package decor import ( - "fmt" "strings" - "unicode/utf8" "github.com/acarl005/stripansi" + "github.com/mattn/go-runewidth" ) // Merge wraps its decorator argument with intention to sync width @@ -68,16 +67,16 @@ func (d *mergeDecorator) Decor(s Statistics) string { msg := d.Decorator.Decor(s) - msgLen := utf8.RuneCountInString(stripansi.Strip(msg)) + pureWidth := runewidth.StringWidth(msg) + stripWidth := runewidth.StringWidth(stripansi.Strip(msg)) + cellCount := stripWidth if (d.wc.C & DextraSpace) != 0 { - msgLen++ + cellCount++ } - var total int - max := utf8.RuneCountInString(d.placeHolders[0].FormatMsg("")) - total += max - pw := (msgLen - max) / len(d.placeHolders) - rem := (msgLen - max) % len(d.placeHolders) + total := runewidth.StringWidth(d.placeHolders[0].FormatMsg("")) + pw := (cellCount - total) / len(d.placeHolders) + rem := (cellCount - total) % len(d.placeHolders) var diff int for i := 1; i < len(d.placeHolders); i++ { @@ -89,14 +88,14 @@ width = 0 } } - max = utf8.RuneCountInString(ph.FormatMsg(strings.Repeat(" ", width))) + max := runewidth.StringWidth(ph.FormatMsg(strings.Repeat(" ", width))) total += max diff = max - pw } d.wc.wsync <- pw + rem - max = <-d.wc.wsync - return fmt.Sprintf(fmt.Sprintf(d.wc.dynFormat, max+total), msg) + max := <-d.wc.wsync + return d.wc.fill(msg, max+total+(pureWidth-stripWidth)) } type placeHolderDecorator struct { diff --git a/go.mod b/go.mod index d95571b..3800af2 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ github.com/VividCortex/ewma v1.1.1 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/mattn/go-isatty v0.0.12 + github.com/mattn/go-runewidth v0.0.9 golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f ) diff --git a/go.sum b/go.sum index 56d6d99..d1250bd 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f h1:mOhmO9WsBaJCNmaZHPtHs9wOcdqdKCjF6OPJlmDM3KI= golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=