refactoring: BarStyleComposer
Vladimir Bauer
5 years ago
| 2 | 2 | import ( |
| 3 | 3 | "bytes" |
| 4 | 4 | "io" |
| 5 | "unicode/utf8" | |
| 6 | ||
| 5 | ||
| 6 | "github.com/acarl005/stripansi" | |
| 7 | 7 | "github.com/mattn/go-runewidth" |
| 8 | "github.com/rivo/uniseg" | |
| 9 | 8 | "github.com/vbauerster/mpb/v6/decor" |
| 10 | 9 | "github.com/vbauerster/mpb/v6/internal" |
| 11 | 10 | ) |
| 12 | 11 | |
| 13 | 12 | const ( |
| 14 | rLeft = iota | |
| 15 | rFill | |
| 16 | rTip | |
| 17 | rSpace | |
| 18 | rRight | |
| 19 | rRevTip | |
| 20 | rRefill | |
| 13 | iLbound = iota | |
| 14 | iRbound | |
| 15 | iFiller | |
| 16 | iRefiller | |
| 17 | iPadding | |
| 18 | components | |
| 21 | 19 | ) |
| 22 | 20 | |
| 23 | // BarDefaultStyle is a style for rendering a progress bar. | |
| 24 | // It consist of 7 ordered runes: | |
| 25 | // | |
| 26 | // '1st rune' stands for left boundary rune | |
| 27 | // | |
| 28 | // '2nd rune' stands for fill rune | |
| 29 | // | |
| 30 | // '3rd rune' stands for tip rune | |
| 31 | // | |
| 32 | // '4th rune' stands for space rune | |
| 33 | // | |
| 34 | // '5th rune' stands for right boundary rune | |
| 35 | // | |
| 36 | // '6th rune' stands for reverse tip rune | |
| 37 | // | |
| 38 | // '7th rune' stands for refill rune | |
| 39 | // | |
| 40 | const BarDefaultStyle string = "[=>-]<+" | |
| 41 | ||
| 42 | type barFiller struct { | |
| 43 | format [][]byte | |
| 44 | rwidth []int | |
| 45 | tip []byte | |
| 46 | refill int64 | |
| 47 | reverse bool | |
| 48 | flush func(io.Writer, *space, [][]byte) | |
| 49 | } | |
| 50 | ||
| 51 | type space struct { | |
| 52 | space []byte | |
| 53 | rwidth int | |
| 54 | count int | |
| 55 | } | |
| 56 | ||
| 57 | // NewBarFiller returns a BarFiller implementation which renders a | |
| 58 | // progress bar in regular direction. If style is empty string, | |
| 59 | // BarDefaultStyle is applied. To be used with `*Progress.Add(...) | |
| 60 | // *Bar` method. | |
| 61 | func NewBarFiller(style string) BarFiller { | |
| 62 | return newBarFiller(style, false) | |
| 63 | } | |
| 64 | ||
| 65 | // NewBarFillerRev returns a BarFiller implementation which renders a | |
| 66 | // progress bar in reverse direction. If style is empty string, | |
| 67 | // BarDefaultStyle is applied. To be used with `*Progress.Add(...) | |
| 68 | // *Bar` method. | |
| 69 | func NewBarFillerRev(style string) BarFiller { | |
| 70 | return newBarFiller(style, true) | |
| 71 | } | |
| 72 | ||
| 73 | // NewBarFillerPick pick between regular and reverse BarFiller implementation | |
| 74 | // based on rev param. To be used with `*Progress.Add(...) *Bar` method. | |
| 75 | func NewBarFillerPick(style string, rev bool) BarFiller { | |
| 76 | return newBarFiller(style, rev) | |
| 77 | } | |
| 78 | ||
| 79 | func newBarFiller(style string, rev bool) BarFiller { | |
| 80 | bf := &barFiller{ | |
| 81 | format: make([][]byte, len(BarDefaultStyle)), | |
| 82 | rwidth: make([]int, len(BarDefaultStyle)), | |
| 83 | reverse: rev, | |
| 84 | } | |
| 85 | bf.parse(BarDefaultStyle) | |
| 86 | if style != "" && style != BarDefaultStyle { | |
| 87 | bf.parse(style) | |
| 21 | // BarStyleComposer interface. | |
| 22 | type BarStyleComposer interface { | |
| 23 | BarFillerBuilder | |
| 24 | Lbound(string) BarStyleComposer | |
| 25 | Rbound(string) BarStyleComposer | |
| 26 | Filler(string) BarStyleComposer | |
| 27 | Refiller(string) BarStyleComposer | |
| 28 | Padding(string) BarStyleComposer | |
| 29 | Tip(...string) BarStyleComposer | |
| 30 | Reverse() BarStyleComposer | |
| 31 | } | |
| 32 | ||
| 33 | type bFiller struct { | |
| 34 | components [components]*component | |
| 35 | tip struct { | |
| 36 | count uint | |
| 37 | frames []*component | |
| 38 | } | |
| 39 | flush func(dst io.Writer, src [][]byte, pad *component, padWidth int) | |
| 40 | } | |
| 41 | ||
| 42 | type component struct { | |
| 43 | width int | |
| 44 | bytes []byte | |
| 45 | } | |
| 46 | ||
| 47 | type barStyle struct { | |
| 48 | lbound string | |
| 49 | rbound string | |
| 50 | filler string | |
| 51 | refiller string | |
| 52 | padding string | |
| 53 | tip []string | |
| 54 | rev bool | |
| 55 | } | |
| 56 | ||
| 57 | // BarStyle constructs default bar style which can be altered via | |
| 58 | // BarStyleComposer interface. | |
| 59 | func BarStyle() BarStyleComposer { | |
| 60 | return &barStyle{ | |
| 61 | lbound: "[", | |
| 62 | rbound: "]", | |
| 63 | filler: "=", | |
| 64 | refiller: "+", | |
| 65 | padding: "-", | |
| 66 | tip: []string{">"}, | |
| 67 | } | |
| 68 | } | |
| 69 | ||
| 70 | func (s *barStyle) Lbound(bound string) BarStyleComposer { | |
| 71 | s.lbound = bound | |
| 72 | return s | |
| 73 | } | |
| 74 | ||
| 75 | func (s *barStyle) Rbound(bound string) BarStyleComposer { | |
| 76 | s.rbound = bound | |
| 77 | return s | |
| 78 | } | |
| 79 | ||
| 80 | func (s *barStyle) Filler(filler string) BarStyleComposer { | |
| 81 | s.filler = filler | |
| 82 | return s | |
| 83 | } | |
| 84 | ||
| 85 | func (s *barStyle) Refiller(refiller string) BarStyleComposer { | |
| 86 | s.refiller = refiller | |
| 87 | return s | |
| 88 | } | |
| 89 | ||
| 90 | func (s *barStyle) Padding(padding string) BarStyleComposer { | |
| 91 | s.padding = padding | |
| 92 | return s | |
| 93 | } | |
| 94 | ||
| 95 | func (s *barStyle) Tip(tip ...string) BarStyleComposer { | |
| 96 | if len(tip) != 0 { | |
| 97 | s.tip = append(s.tip[:0], tip...) | |
| 98 | } | |
| 99 | return s | |
| 100 | } | |
| 101 | ||
| 102 | func (s *barStyle) Reverse() BarStyleComposer { | |
| 103 | s.rev = true | |
| 104 | return s | |
| 105 | } | |
| 106 | ||
| 107 | func (s *barStyle) Build() BarFiller { | |
| 108 | bf := &bFiller{ | |
| 109 | flush: regFlush, | |
| 110 | } | |
| 111 | if s.rev { | |
| 112 | bf.flush = revFlush | |
| 113 | } | |
| 114 | bf.components[iLbound] = &component{ | |
| 115 | width: runewidth.StringWidth(stripansi.Strip(s.lbound)), | |
| 116 | bytes: []byte(s.lbound), | |
| 117 | } | |
| 118 | bf.components[iRbound] = &component{ | |
| 119 | width: runewidth.StringWidth(stripansi.Strip(s.rbound)), | |
| 120 | bytes: []byte(s.rbound), | |
| 121 | } | |
| 122 | bf.components[iFiller] = &component{ | |
| 123 | width: runewidth.StringWidth(stripansi.Strip(s.filler)), | |
| 124 | bytes: []byte(s.filler), | |
| 125 | } | |
| 126 | bf.components[iRefiller] = &component{ | |
| 127 | width: runewidth.StringWidth(stripansi.Strip(s.refiller)), | |
| 128 | bytes: []byte(s.refiller), | |
| 129 | } | |
| 130 | bf.components[iPadding] = &component{ | |
| 131 | width: runewidth.StringWidth(stripansi.Strip(s.padding)), | |
| 132 | bytes: []byte(s.padding), | |
| 133 | } | |
| 134 | bf.tip.frames = make([]*component, len(s.tip)) | |
| 135 | for i, t := range s.tip { | |
| 136 | bf.tip.frames[i] = &component{ | |
| 137 | width: runewidth.StringWidth(stripansi.Strip(t)), | |
| 138 | bytes: []byte(t), | |
| 139 | } | |
| 88 | 140 | } |
| 89 | 141 | return bf |
| 90 | 142 | } |
| 91 | 143 | |
| 92 | func (s *barFiller) parse(style string) { | |
| 93 | if !utf8.ValidString(style) { | |
| 94 | panic("invalid bar style") | |
| 95 | } | |
| 96 | srcFormat := make([][]byte, len(BarDefaultStyle)) | |
| 97 | srcRwidth := make([]int, len(BarDefaultStyle)) | |
| 98 | i := 0 | |
| 99 | for gr := uniseg.NewGraphemes(style); i < len(BarDefaultStyle) && gr.Next(); i++ { | |
| 100 | srcFormat[i] = gr.Bytes() | |
| 101 | srcRwidth[i] = runewidth.StringWidth(gr.Str()) | |
| 102 | } | |
| 103 | copy(s.format, srcFormat[:i]) | |
| 104 | copy(s.rwidth, srcRwidth[:i]) | |
| 105 | if s.reverse { | |
| 106 | s.tip = s.format[rRevTip] | |
| 107 | s.flush = reverseFlush | |
| 108 | } else { | |
| 109 | s.tip = s.format[rTip] | |
| 110 | s.flush = regularFlush | |
| 111 | } | |
| 112 | } | |
| 113 | ||
| 114 | func (s *barFiller) Fill(w io.Writer, reqWidth int, stat decor.Statistics) { | |
| 115 | width := internal.CheckRequestedWidth(reqWidth, stat.AvailableWidth) | |
| 116 | brackets := s.rwidth[rLeft] + s.rwidth[rRight] | |
| 144 | func (s *bFiller) Fill(w io.Writer, width int, stat decor.Statistics) { | |
| 145 | width = internal.CheckRequestedWidth(width, stat.AvailableWidth) | |
| 146 | brackets := s.components[iLbound].width + s.components[iRbound].width | |
| 117 | 147 | if width < brackets { |
| 118 | 148 | return |
| 119 | 149 | } |
| 120 | 150 | // don't count brackets as progress |
| 121 | 151 | width -= brackets |
| 122 | 152 | |
| 123 | w.Write(s.format[rLeft]) | |
| 124 | defer w.Write(s.format[rRight]) | |
| 125 | ||
| 126 | cwidth := int(internal.PercentageRound(stat.Total, stat.Current, width)) | |
| 127 | space := &space{ | |
| 128 | space: s.format[rSpace], | |
| 129 | rwidth: s.rwidth[rSpace], | |
| 130 | count: width - cwidth, | |
| 131 | } | |
| 132 | ||
| 153 | w.Write(s.components[iLbound].bytes) | |
| 154 | defer w.Write(s.components[iRbound].bytes) | |
| 155 | ||
| 156 | curWidth := int(internal.PercentageRound(stat.Total, stat.Current, width)) | |
| 157 | padWidth := width - curWidth | |
| 133 | 158 | index, refill := 0, 0 |
| 134 | bb := make([][]byte, cwidth) | |
| 135 | ||
| 136 | if cwidth > 0 && cwidth != width { | |
| 137 | bb[index] = s.tip | |
| 138 | cwidth -= s.rwidth[rTip] | |
| 159 | bb := make([][]byte, curWidth) | |
| 160 | ||
| 161 | if curWidth > 0 && curWidth != width { | |
| 162 | tipFrame := s.tip.frames[s.tip.count%uint(len(s.tip.frames))] | |
| 163 | bb[index] = tipFrame.bytes | |
| 164 | curWidth -= tipFrame.width | |
| 165 | s.tip.count++ | |
| 139 | 166 | index++ |
| 140 | 167 | } |
| 141 | 168 | |
| 142 | 169 | if stat.Refill > 0 { |
| 143 | 170 | refill = int(internal.PercentageRound(stat.Total, int64(stat.Refill), width)) |
| 144 | if refill > cwidth { | |
| 145 | refill = cwidth | |
| 171 | if refill > curWidth { | |
| 172 | refill = curWidth | |
| 146 | 173 | } |
| 147 | cwidth -= refill | |
| 148 | } | |
| 149 | ||
| 150 | for cwidth > 0 { | |
| 151 | bb[index] = s.format[rFill] | |
| 152 | cwidth -= s.rwidth[rFill] | |
| 174 | curWidth -= refill | |
| 175 | } | |
| 176 | ||
| 177 | for curWidth > 0 { | |
| 178 | bb[index] = s.components[iFiller].bytes | |
| 179 | curWidth -= s.components[iFiller].width | |
| 153 | 180 | index++ |
| 154 | 181 | } |
| 155 | 182 | |
| 156 | 183 | for refill > 0 { |
| 157 | bb[index] = s.format[rRefill] | |
| 158 | refill -= s.rwidth[rRefill] | |
| 184 | bb[index] = s.components[iRefiller].bytes | |
| 185 | refill -= s.components[iRefiller].width | |
| 159 | 186 | index++ |
| 160 | 187 | } |
| 161 | 188 | |
| 162 | if cwidth+refill < 0 || space.rwidth > 1 { | |
| 189 | if curWidth+refill < 0 || s.components[iPadding].width > 1 { | |
| 163 | 190 | buf := new(bytes.Buffer) |
| 164 | s.flush(buf, space, bb[:index]) | |
| 191 | s.flush(buf, bb[:index], s.components[iPadding], padWidth) | |
| 165 | 192 | io.WriteString(w, runewidth.Truncate(buf.String(), width, "…")) |
| 166 | 193 | return |
| 167 | 194 | } |
| 168 | 195 | |
| 169 | s.flush(w, space, bb) | |
| 170 | } | |
| 171 | ||
| 172 | func regularFlush(w io.Writer, space *space, bb [][]byte) { | |
| 173 | for i := len(bb) - 1; i >= 0; i-- { | |
| 174 | w.Write(bb[i]) | |
| 175 | } | |
| 176 | for space.count > 0 { | |
| 177 | w.Write(space.space) | |
| 178 | space.count -= space.rwidth | |
| 179 | } | |
| 180 | } | |
| 181 | ||
| 182 | func reverseFlush(w io.Writer, space *space, bb [][]byte) { | |
| 183 | for space.count > 0 { | |
| 184 | w.Write(space.space) | |
| 185 | space.count -= space.rwidth | |
| 186 | } | |
| 187 | for i := 0; i < len(bb); i++ { | |
| 188 | w.Write(bb[i]) | |
| 189 | } | |
| 190 | } | |
| 196 | s.flush(w, bb, s.components[iPadding], padWidth) | |
| 197 | } | |
| 198 | ||
| 199 | func regFlush(dst io.Writer, src [][]byte, pad *component, padWidth int) { | |
| 200 | for i := len(src) - 1; i >= 0; i-- { | |
| 201 | dst.Write(src[i]) | |
| 202 | } | |
| 203 | for padWidth > 0 { | |
| 204 | dst.Write(pad.bytes) | |
| 205 | padWidth -= pad.width | |
| 206 | } | |
| 207 | } | |
| 208 | ||
| 209 | func revFlush(dst io.Writer, src [][]byte, pad *component, padWidth int) { | |
| 210 | for padWidth > 0 { | |
| 211 | dst.Write(pad.bytes) | |
| 212 | padWidth -= pad.width | |
| 213 | } | |
| 214 | for i := 0; i < len(src); i++ { | |
| 215 | dst.Write(src[i]) | |
| 216 | } | |
| 217 | } |