use go-runewidth
Vladimir Bauer
6 years ago
| 7 | 7 | "log" |
| 8 | 8 | "strings" |
| 9 | 9 | "time" |
| 10 | "unicode/utf8" | |
| 11 | 10 | |
| 12 | 11 | "github.com/acarl005/stripansi" |
| 12 | "github.com/mattn/go-runewidth" | |
| 13 | 13 | "github.com/vbauerster/mpb/v5/decor" |
| 14 | 14 | ) |
| 15 | 15 | |
| 386 | 386 | func (s *bState) draw(stat decor.Statistics) io.Reader { |
| 387 | 387 | for _, d := range s.pDecorators { |
| 388 | 388 | str := d.Decor(stat) |
| 389 | stat.OccupiedWidth += utf8.RuneCountInString(stripansi.Strip(str)) | |
| 389 | stat.OccupiedWidth += runewidth.StringWidth(stripansi.Strip(str)) | |
| 390 | 390 | s.bufP.WriteString(str) |
| 391 | 391 | } |
| 392 | 392 | |
| 393 | 393 | for _, d := range s.aDecorators { |
| 394 | 394 | str := d.Decor(stat) |
| 395 | stat.OccupiedWidth += utf8.RuneCountInString(stripansi.Strip(str)) | |
| 395 | stat.OccupiedWidth += runewidth.StringWidth(stripansi.Strip(str)) | |
| 396 | 396 | s.bufA.WriteString(str) |
| 397 | 397 | } |
| 398 | 398 | |
| 3 | 3 | "io" |
| 4 | 4 | "unicode/utf8" |
| 5 | 5 | |
| 6 | "github.com/mattn/go-runewidth" | |
| 6 | 7 | "github.com/vbauerster/mpb/v5/decor" |
| 7 | 8 | "github.com/vbauerster/mpb/v5/internal" |
| 8 | 9 | ) |
| 11 | 12 | rLeft = iota |
| 12 | 13 | rFill |
| 13 | 14 | rTip |
| 14 | rEmpty | |
| 15 | rSpace | |
| 15 | 16 | rRight |
| 16 | 17 | rRevTip |
| 17 | 18 | rRefill |
| 26 | 27 | // |
| 27 | 28 | // '3rd rune' stands for tip rune |
| 28 | 29 | // |
| 29 | // '4th rune' stands for empty rune | |
| 30 | // '4th rune' stands for space rune | |
| 30 | 31 | // |
| 31 | 32 | // '5th rune' stands for right boundary rune |
| 32 | 33 | // |
| 38 | 39 | |
| 39 | 40 | type barFiller struct { |
| 40 | 41 | format [][]byte |
| 42 | rwidth []int | |
| 41 | 43 | tip []byte |
| 42 | 44 | refill int64 |
| 43 | 45 | reverse bool |
| 44 | flush func(w io.Writer, bb [][]byte) | |
| 46 | flush func(io.Writer, *space, [][]byte) | |
| 47 | } | |
| 48 | ||
| 49 | type space struct { | |
| 50 | space []byte | |
| 51 | width int | |
| 52 | count int | |
| 45 | 53 | } |
| 46 | 54 | |
| 47 | 55 | // NewBarFiller constucts mpb.BarFiller, to be used with *Progress.Add(...) *Bar method. |
| 48 | 56 | func NewBarFiller(style string, reverse bool) BarFiller { |
| 49 | if style == "" { | |
| 50 | style = DefaultBarStyle | |
| 51 | } | |
| 52 | 57 | bf := &barFiller{ |
| 53 | format: make([][]byte, utf8.RuneCountInString(style)), | |
| 58 | format: make([][]byte, len(DefaultBarStyle)), | |
| 59 | rwidth: make([]int, len(DefaultBarStyle)), | |
| 54 | 60 | reverse: reverse, |
| 55 | 61 | } |
| 56 | 62 | bf.SetStyle(style) |
| 61 | 67 | if !utf8.ValidString(style) { |
| 62 | 68 | return |
| 63 | 69 | } |
| 64 | src := make([][]byte, 0, utf8.RuneCountInString(style)) | |
| 70 | if style == "" { | |
| 71 | style = DefaultBarStyle | |
| 72 | } | |
| 73 | src := make([][]byte, utf8.RuneCountInString(style)) | |
| 74 | i := 0 | |
| 65 | 75 | for _, r := range style { |
| 66 | src = append(src, []byte(string(r))) | |
| 76 | s.rwidth[i] = runewidth.RuneWidth(r) | |
| 77 | src[i] = []byte(string(r)) | |
| 78 | i++ | |
| 67 | 79 | } |
| 68 | 80 | copy(s.format, src) |
| 69 | 81 | s.SetReverse(s.reverse) |
| 87 | 99 | func (s *barFiller) Fill(w io.Writer, reqWidth int, stat decor.Statistics) { |
| 88 | 100 | width := internal.CalcWidthForBarFiller(reqWidth, stat.TermWidth-stat.OccupiedWidth) |
| 89 | 101 | |
| 90 | width -= 2 // don't count rLeft and rRight as progress | |
| 91 | if width < 2 { | |
| 102 | // don't count rLeft and rRight as progress | |
| 103 | brackets := s.rwidth[rLeft] + s.rwidth[rRight] | |
| 104 | width -= brackets | |
| 105 | if width < brackets { | |
| 92 | 106 | return |
| 93 | 107 | } |
| 94 | 108 | w.Write(s.format[rLeft]) |
| 95 | 109 | defer w.Write(s.format[rRight]) |
| 96 | 110 | |
| 97 | bb := make([][]byte, width) | |
| 98 | ||
| 99 | 111 | cwidth := int(internal.PercentageRound(stat.Total, stat.Current, width)) |
| 100 | ||
| 101 | for i := 0; i < cwidth; i++ { | |
| 102 | bb[i] = s.format[rFill] | |
| 112 | bb := make([][]byte, cwidth) | |
| 113 | space := &space{ | |
| 114 | space: s.format[rSpace], | |
| 115 | width: s.rwidth[rSpace], | |
| 116 | count: width - cwidth, | |
| 117 | } | |
| 118 | if cwidth == 0 { | |
| 119 | s.flush(w, space, bb) | |
| 120 | return | |
| 103 | 121 | } |
| 104 | 122 | |
| 123 | index := 0 | |
| 124 | if space.count != 0 { | |
| 125 | bb[index] = s.tip | |
| 126 | cwidth -= s.rwidth[rTip] | |
| 127 | } else { | |
| 128 | bb[index] = s.format[rFill] | |
| 129 | cwidth -= s.rwidth[rFill] | |
| 130 | } | |
| 131 | index++ | |
| 132 | ||
| 133 | rwidth := 0 | |
| 105 | 134 | if s.refill > 0 { |
| 106 | var rwidth int | |
| 107 | if s.refill > stat.Current { | |
| 108 | rwidth = cwidth | |
| 109 | } else { | |
| 135 | rwidth = cwidth | |
| 136 | if s.refill < stat.Current { | |
| 110 | 137 | rwidth = int(internal.PercentageRound(stat.Total, int64(s.refill), width)) |
| 111 | 138 | } |
| 112 | for i := 0; i < rwidth; i++ { | |
| 113 | bb[i] = s.format[rRefill] | |
| 114 | } | |
| 139 | cwidth -= rwidth | |
| 115 | 140 | } |
| 116 | 141 | |
| 117 | if cwidth > 0 && cwidth < width { | |
| 118 | bb[cwidth-1] = s.tip | |
| 142 | for cwidth > 0 { | |
| 143 | bb[index] = s.format[rFill] | |
| 144 | cwidth -= s.rwidth[rFill] | |
| 145 | index++ | |
| 119 | 146 | } |
| 120 | 147 | |
| 121 | for i := cwidth; i < width; i++ { | |
| 122 | bb[i] = s.format[rEmpty] | |
| 148 | for rwidth > 0 { | |
| 149 | bb[index] = s.format[rRefill] | |
| 150 | rwidth -= s.rwidth[rRefill] | |
| 151 | index++ | |
| 123 | 152 | } |
| 124 | 153 | |
| 125 | s.flush(w, bb) | |
| 154 | s.flush(w, space, bb) | |
| 126 | 155 | } |
| 127 | 156 | |
| 128 | func regularFlush(w io.Writer, bb [][]byte) { | |
| 157 | func regularFlush(w io.Writer, space *space, bb [][]byte) { | |
| 158 | for i := len(bb) - 1; i >= 0; i-- { | |
| 159 | w.Write(bb[i]) | |
| 160 | } | |
| 161 | for space.count > 0 { | |
| 162 | w.Write(space.space) | |
| 163 | space.count -= space.width | |
| 164 | } | |
| 165 | } | |
| 166 | ||
| 167 | func reverseFlush(w io.Writer, space *space, bb [][]byte) { | |
| 168 | for space.count > 0 { | |
| 169 | w.Write(space.space) | |
| 170 | space.count -= space.width | |
| 171 | } | |
| 129 | 172 | for i := 0; i < len(bb); i++ { |
| 130 | 173 | w.Write(bb[i]) |
| 131 | 174 | } |
| 132 | 175 | } |
| 133 | ||
| 134 | func reverseFlush(w io.Writer, bb [][]byte) { | |
| 135 | for i := len(bb) - 1; i >= 0; i-- { | |
| 136 | w.Write(bb[i]) | |
| 137 | } | |
| 138 | } | |
| 2 | 2 | import ( |
| 3 | 3 | "fmt" |
| 4 | 4 | "time" |
| 5 | "unicode/utf8" | |
| 6 | 5 | |
| 7 | 6 | "github.com/acarl005/stripansi" |
| 7 | "github.com/mattn/go-runewidth" | |
| 8 | 8 | ) |
| 9 | 9 | |
| 10 | 10 | const ( |
| 126 | 126 | // W represents width and C represents bit set of width related config. |
| 127 | 127 | // A decorator should embed WC, to enable width synchronization. |
| 128 | 128 | type WC struct { |
| 129 | W int | |
| 130 | C int | |
| 131 | dynFormat string | |
| 132 | wsync chan int | |
| 129 | W int | |
| 130 | C int | |
| 131 | fill func(s string, w int) string | |
| 132 | wsync chan int | |
| 133 | 133 | } |
| 134 | 134 | |
| 135 | 135 | // FormatMsg formats final message according to WC.W and WC.C. |
| 136 | 136 | // Should be called by any Decorator implementation. |
| 137 | 137 | func (wc *WC) FormatMsg(msg string) string { |
| 138 | var format string | |
| 139 | runeCount := utf8.RuneCountInString(stripansi.Strip(msg)) | |
| 140 | ansiCount := utf8.RuneCountInString(msg) - runeCount | |
| 138 | pureWidth := runewidth.StringWidth(msg) | |
| 139 | stripWidth := runewidth.StringWidth(stripansi.Strip(msg)) | |
| 140 | maxCell := wc.W | |
| 141 | 141 | if (wc.C & DSyncWidth) != 0 { |
| 142 | cellCount := stripWidth | |
| 142 | 143 | if (wc.C & DextraSpace) != 0 { |
| 143 | runeCount++ | |
| 144 | cellCount++ | |
| 144 | 145 | } |
| 145 | wc.wsync <- runeCount | |
| 146 | max := <-wc.wsync | |
| 147 | format = fmt.Sprintf(wc.dynFormat, ansiCount+max) | |
| 148 | } else { | |
| 149 | format = fmt.Sprintf(wc.dynFormat, ansiCount+wc.W) | |
| 146 | wc.wsync <- cellCount | |
| 147 | maxCell = <-wc.wsync | |
| 150 | 148 | } |
| 151 | return fmt.Sprintf(format, msg) | |
| 149 | return wc.fill(msg, maxCell+(pureWidth-stripWidth)) | |
| 152 | 150 | } |
| 153 | 151 | |
| 154 | 152 | // Init initializes width related config. |
| 155 | 153 | func (wc *WC) Init() WC { |
| 156 | wc.dynFormat = "%%" | |
| 154 | wc.fill = runewidth.FillLeft | |
| 157 | 155 | if (wc.C & DidentRight) != 0 { |
| 158 | wc.dynFormat += "-" | |
| 156 | wc.fill = runewidth.FillRight | |
| 159 | 157 | } |
| 160 | wc.dynFormat += "%ds" | |
| 161 | 158 | if (wc.C & DSyncWidth) != 0 { |
| 162 | 159 | // it's deliberate choice to override wsync on each Init() call, |
| 163 | 160 | // this way globals like WCSyncSpace can be reused |
| 0 | 0 | package decor |
| 1 | 1 | |
| 2 | 2 | import ( |
| 3 | "fmt" | |
| 4 | 3 | "strings" |
| 5 | "unicode/utf8" | |
| 6 | 4 | |
| 7 | 5 | "github.com/acarl005/stripansi" |
| 6 | "github.com/mattn/go-runewidth" | |
| 8 | 7 | ) |
| 9 | 8 | |
| 10 | 9 | // Merge wraps its decorator argument with intention to sync width |
| 67 | 66 | |
| 68 | 67 | func (d *mergeDecorator) Decor(s Statistics) string { |
| 69 | 68 | msg := d.Decorator.Decor(s) |
| 70 | msgLen := utf8.RuneCountInString(stripansi.Strip(msg)) | |
| 69 | pureWidth := runewidth.StringWidth(msg) | |
| 70 | stripWidth := runewidth.StringWidth(stripansi.Strip(msg)) | |
| 71 | cellCount := stripWidth | |
| 71 | 72 | if (d.wc.C & DextraSpace) != 0 { |
| 72 | msgLen++ | |
| 73 | cellCount++ | |
| 73 | 74 | } |
| 74 | 75 | |
| 75 | var total int | |
| 76 | max := utf8.RuneCountInString(d.placeHolders[0].FormatMsg("")) | |
| 77 | total += max | |
| 78 | pw := (msgLen - max) / len(d.placeHolders) | |
| 79 | rem := (msgLen - max) % len(d.placeHolders) | |
| 76 | total := runewidth.StringWidth(d.placeHolders[0].FormatMsg("")) | |
| 77 | pw := (cellCount - total) / len(d.placeHolders) | |
| 78 | rem := (cellCount - total) % len(d.placeHolders) | |
| 80 | 79 | |
| 81 | 80 | var diff int |
| 82 | 81 | for i := 1; i < len(d.placeHolders); i++ { |
| 88 | 87 | width = 0 |
| 89 | 88 | } |
| 90 | 89 | } |
| 91 | max = utf8.RuneCountInString(ph.FormatMsg(strings.Repeat(" ", width))) | |
| 90 | max := runewidth.StringWidth(ph.FormatMsg(strings.Repeat(" ", width))) | |
| 92 | 91 | total += max |
| 93 | 92 | diff = max - pw |
| 94 | 93 | } |
| 95 | 94 | |
| 96 | 95 | d.wc.wsync <- pw + rem |
| 97 | max = <-d.wc.wsync | |
| 98 | return fmt.Sprintf(fmt.Sprintf(d.wc.dynFormat, max+total), msg) | |
| 96 | max := <-d.wc.wsync | |
| 97 | return d.wc.fill(msg, max+total+(pureWidth-stripWidth)) | |
| 99 | 98 | } |
| 100 | 99 | |
| 101 | 100 | type placeHolderDecorator struct { |
| 3 | 3 | github.com/VividCortex/ewma v1.1.1 |
| 4 | 4 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d |
| 5 | 5 | github.com/mattn/go-isatty v0.0.12 |
| 6 | github.com/mattn/go-runewidth v0.0.9 | |
| 6 | 7 | golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f |
| 7 | 8 | ) |
| 8 | 9 |
| 3 | 3 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= |
| 4 | 4 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= |
| 5 | 5 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= |
| 6 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= | |
| 7 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= | |
| 6 | 8 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
| 7 | 9 | golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f h1:mOhmO9WsBaJCNmaZHPtHs9wOcdqdKCjF6OPJlmDM3KI= |
| 8 | 10 | golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |