| Abhay Kumar | a61c522 | 2025-11-10 07:32:50 +0000 | [diff] [blame^] | 1 | package pb |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "fmt" |
| 6 | "math" |
| 7 | "strings" |
| 8 | "sync" |
| 9 | "time" |
| 10 | ) |
| 11 | |
| 12 | const ( |
| 13 | adElPlaceholder = "%_ad_el_%" |
| 14 | adElPlaceholderLen = len(adElPlaceholder) |
| 15 | ) |
| 16 | |
| 17 | var ( |
| 18 | defaultBarEls = [5]string{"[", "-", ">", "_", "]"} |
| 19 | ) |
| 20 | |
| 21 | // Element is an interface for bar elements |
| 22 | type Element interface { |
| 23 | ProgressElement(state *State, args ...string) string |
| 24 | } |
| 25 | |
| 26 | // ElementFunc type implements Element interface and created for simplify elements |
| 27 | type ElementFunc func(state *State, args ...string) string |
| 28 | |
| 29 | // ProgressElement just call self func |
| 30 | func (e ElementFunc) ProgressElement(state *State, args ...string) string { |
| 31 | return e(state, args...) |
| 32 | } |
| 33 | |
| 34 | var elementsM sync.Mutex |
| 35 | |
| 36 | var elements = map[string]Element{ |
| 37 | "percent": ElementPercent, |
| 38 | "counters": ElementCounters, |
| 39 | "bar": adaptiveWrap(ElementBar), |
| 40 | "speed": ElementSpeed, |
| 41 | "rtime": ElementRemainingTime, |
| 42 | "etime": ElementElapsedTime, |
| 43 | "string": ElementString, |
| 44 | "cycle": ElementCycle, |
| 45 | } |
| 46 | |
| 47 | // RegisterElement give you a chance to use custom elements |
| 48 | func RegisterElement(name string, el Element, adaptive bool) { |
| 49 | if adaptive { |
| 50 | el = adaptiveWrap(el) |
| 51 | } |
| 52 | elementsM.Lock() |
| 53 | elements[name] = el |
| 54 | elementsM.Unlock() |
| 55 | } |
| 56 | |
| 57 | type argsHelper []string |
| 58 | |
| 59 | func (args argsHelper) getOr(n int, value string) string { |
| 60 | if len(args) > n { |
| 61 | return args[n] |
| 62 | } |
| 63 | return value |
| 64 | } |
| 65 | |
| 66 | func (args argsHelper) getNotEmptyOr(n int, value string) (v string) { |
| 67 | if v = args.getOr(n, value); v == "" { |
| 68 | return value |
| 69 | } |
| 70 | return |
| 71 | } |
| 72 | |
| 73 | func adaptiveWrap(el Element) Element { |
| 74 | return ElementFunc(func(state *State, args ...string) string { |
| 75 | state.recalc = append(state.recalc, ElementFunc(func(s *State, _ ...string) (result string) { |
| 76 | s.adaptive = true |
| 77 | result = el.ProgressElement(s, args...) |
| 78 | s.adaptive = false |
| 79 | return |
| 80 | })) |
| 81 | return adElPlaceholder |
| 82 | }) |
| 83 | } |
| 84 | |
| 85 | // ElementPercent shows current percent of progress. |
| 86 | // Optionally can take one or two string arguments. |
| 87 | // First string will be used as value for format float64, default is "%.02f%%". |
| 88 | // Second string will be used when percent can't be calculated, default is "?%" |
| 89 | // In template use as follows: {{percent .}} or {{percent . "%.03f%%"}} or {{percent . "%.03f%%" "?"}} |
| 90 | var ElementPercent ElementFunc = func(state *State, args ...string) string { |
| 91 | argsh := argsHelper(args) |
| 92 | if state.Total() > 0 { |
| 93 | return fmt.Sprintf( |
| 94 | argsh.getNotEmptyOr(0, "%.02f%%"), |
| 95 | float64(state.Value())/(float64(state.Total())/float64(100)), |
| 96 | ) |
| 97 | } |
| 98 | return argsh.getOr(1, "?%") |
| 99 | } |
| 100 | |
| 101 | // ElementCounters shows current and total values. |
| 102 | // Optionally can take one or two string arguments. |
| 103 | // First string will be used as format value when Total is present (>0). Default is "%s / %s" |
| 104 | // Second string will be used when total <= 0. Default is "%[1]s" |
| 105 | // In template use as follows: {{counters .}} or {{counters . "%s/%s"}} or {{counters . "%s/%s" "%s/?"}} |
| 106 | var ElementCounters ElementFunc = func(state *State, args ...string) string { |
| 107 | var f string |
| 108 | if state.Total() > 0 { |
| 109 | f = argsHelper(args).getNotEmptyOr(0, "%s / %s") |
| 110 | } else { |
| 111 | f = argsHelper(args).getNotEmptyOr(1, "%[1]s") |
| 112 | } |
| 113 | return fmt.Sprintf(f, state.Format(state.Value()), state.Format(state.Total())) |
| 114 | } |
| 115 | |
| 116 | type elementKey int |
| 117 | |
| 118 | const ( |
| 119 | barObj elementKey = iota |
| 120 | speedObj |
| 121 | cycleObj |
| 122 | ) |
| 123 | |
| 124 | type bar struct { |
| 125 | eb [5][]byte // elements in bytes |
| 126 | cc [5]int // cell counts |
| 127 | buf *bytes.Buffer |
| 128 | } |
| 129 | |
| 130 | func (p *bar) write(state *State, eln, width int) int { |
| 131 | repeat := width / p.cc[eln] |
| 132 | remainder := width % p.cc[eln] |
| 133 | for i := 0; i < repeat; i++ { |
| 134 | p.buf.Write(p.eb[eln]) |
| 135 | } |
| 136 | if remainder > 0 { |
| 137 | StripStringToBuffer(string(p.eb[eln]), remainder, p.buf) |
| 138 | } |
| 139 | return width |
| 140 | } |
| 141 | |
| 142 | func getProgressObj(state *State, args ...string) (p *bar) { |
| 143 | var ok bool |
| 144 | if p, ok = state.Get(barObj).(*bar); !ok { |
| 145 | p = &bar{ |
| 146 | buf: bytes.NewBuffer(nil), |
| 147 | } |
| 148 | state.Set(barObj, p) |
| 149 | } |
| 150 | argsH := argsHelper(args) |
| 151 | for i := range p.eb { |
| 152 | arg := argsH.getNotEmptyOr(i, defaultBarEls[i]) |
| 153 | if string(p.eb[i]) != arg { |
| 154 | p.cc[i] = CellCount(arg) |
| 155 | p.eb[i] = []byte(arg) |
| 156 | if p.cc[i] == 0 { |
| 157 | p.cc[i] = 1 |
| 158 | p.eb[i] = []byte(" ") |
| 159 | } |
| 160 | } |
| 161 | } |
| 162 | return |
| 163 | } |
| 164 | |
| 165 | // ElementBar make progress bar view [-->__] |
| 166 | // Optionally can take up to 5 string arguments. Defaults is "[", "-", ">", "_", "]" |
| 167 | // In template use as follows: {{bar . }} or {{bar . "<" "oOo" "|" "~" ">"}} |
| 168 | // Color args: {{bar . (red "[") (green "-") ... |
| 169 | var ElementBar ElementFunc = func(state *State, args ...string) string { |
| 170 | // init |
| 171 | var p = getProgressObj(state, args...) |
| 172 | |
| 173 | total, value := state.Total(), state.Value() |
| 174 | if total < 0 { |
| 175 | total = -total |
| 176 | } |
| 177 | if value < 0 { |
| 178 | value = -value |
| 179 | } |
| 180 | |
| 181 | // check for overflow |
| 182 | if total != 0 && value > total { |
| 183 | total = value |
| 184 | } |
| 185 | |
| 186 | p.buf.Reset() |
| 187 | |
| 188 | var widthLeft = state.AdaptiveElWidth() |
| 189 | if widthLeft <= 0 || !state.IsAdaptiveWidth() { |
| 190 | widthLeft = 30 |
| 191 | } |
| 192 | |
| 193 | // write left border |
| 194 | if p.cc[0] < widthLeft { |
| 195 | widthLeft -= p.write(state, 0, p.cc[0]) |
| 196 | } else { |
| 197 | p.write(state, 0, widthLeft) |
| 198 | return p.buf.String() |
| 199 | } |
| 200 | |
| 201 | // check right border size |
| 202 | if p.cc[4] < widthLeft { |
| 203 | // write later |
| 204 | widthLeft -= p.cc[4] |
| 205 | } else { |
| 206 | p.write(state, 4, widthLeft) |
| 207 | return p.buf.String() |
| 208 | } |
| 209 | |
| 210 | var curCount int |
| 211 | |
| 212 | if total > 0 { |
| 213 | // calculate count of currenct space |
| 214 | curCount = int(math.Ceil((float64(value) / float64(total)) * float64(widthLeft))) |
| 215 | } |
| 216 | |
| 217 | // write bar |
| 218 | if total == value && state.IsFinished() { |
| 219 | widthLeft -= p.write(state, 1, curCount) |
| 220 | } else if toWrite := curCount - p.cc[2]; toWrite > 0 { |
| 221 | widthLeft -= p.write(state, 1, toWrite) |
| 222 | widthLeft -= p.write(state, 2, p.cc[2]) |
| 223 | } else if curCount > 0 { |
| 224 | widthLeft -= p.write(state, 2, curCount) |
| 225 | } |
| 226 | if widthLeft > 0 { |
| 227 | widthLeft -= p.write(state, 3, widthLeft) |
| 228 | } |
| 229 | // write right border |
| 230 | p.write(state, 4, p.cc[4]) |
| 231 | // cut result and return string |
| 232 | return p.buf.String() |
| 233 | } |
| 234 | |
| 235 | func elapsedTime(state *State) string { |
| 236 | elapsed := state.Time().Sub(state.StartTime()) |
| 237 | var precision time.Duration |
| 238 | var ok bool |
| 239 | if precision, ok = state.Get(TimeRound).(time.Duration); !ok { |
| 240 | // default behavior: round to nearest .1s when elapsed < 10s |
| 241 | // |
| 242 | // we compare with 9.95s as opposed to 10s to avoid an annoying |
| 243 | // interaction with the fixed precision display code below, |
| 244 | // where 9.9s would be rounded to 10s but printed as 10.0s, and |
| 245 | // then 10.0s would be rounded to 10s and printed as 10s |
| 246 | if elapsed < 9950*time.Millisecond { |
| 247 | precision = 100 * time.Millisecond |
| 248 | } else { |
| 249 | precision = time.Second |
| 250 | } |
| 251 | } |
| 252 | rounded := elapsed.Round(precision) |
| 253 | if precision < time.Second && rounded >= time.Second { |
| 254 | // special handling to ensure string is shown with the given |
| 255 | // precision, with trailing zeros after the decimal point if |
| 256 | // necessary |
| 257 | reference := (2*time.Second - time.Nanosecond).Truncate(precision).String() |
| 258 | // reference looks like "1.9[...]9s", telling us how many |
| 259 | // decimal digits we need |
| 260 | neededDecimals := len(reference) - 3 |
| 261 | s := rounded.String() |
| 262 | dotIndex := strings.LastIndex(s, ".") |
| 263 | if dotIndex != -1 { |
| 264 | // s has the form "[stuff].[decimals]s" |
| 265 | decimals := len(s) - dotIndex - 2 |
| 266 | extraZeros := neededDecimals - decimals |
| 267 | return fmt.Sprintf("%s%ss", s[:len(s)-1], strings.Repeat("0", extraZeros)) |
| 268 | } else { |
| 269 | // s has the form "[stuff]s" |
| 270 | return fmt.Sprintf("%s.%ss", s[:len(s)-1], strings.Repeat("0", neededDecimals)) |
| 271 | } |
| 272 | } else { |
| 273 | return rounded.String() |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | // ElementRemainingTime calculates remaining time based on speed (EWMA) |
| 278 | // Optionally can take one or two string arguments. |
| 279 | // First string will be used as value for format time duration string, default is "%s". |
| 280 | // Second string will be used when bar finished and value indicates elapsed time, default is "%s" |
| 281 | // Third string will be used when value not available, default is "?" |
| 282 | // In template use as follows: {{rtime .}} or {{rtime . "%s remain"}} or {{rtime . "%s remain" "%s total" "???"}} |
| 283 | var ElementRemainingTime ElementFunc = func(state *State, args ...string) string { |
| 284 | if state.IsFinished() { |
| 285 | return fmt.Sprintf(argsHelper(args).getOr(1, "%s"), elapsedTime(state)) |
| 286 | } |
| 287 | sp := getSpeedObj(state).value(state) |
| 288 | if sp > 0 { |
| 289 | remain := float64(state.Total() - state.Value()) |
| 290 | remainDur := time.Duration(remain/sp) * time.Second |
| 291 | return fmt.Sprintf(argsHelper(args).getOr(0, "%s"), remainDur) |
| 292 | } |
| 293 | return argsHelper(args).getOr(2, "?") |
| 294 | } |
| 295 | |
| 296 | // ElementElapsedTime shows elapsed time |
| 297 | // Optionally can take one argument - it's format for time string. |
| 298 | // In template use as follows: {{etime .}} or {{etime . "%s elapsed"}} |
| 299 | var ElementElapsedTime ElementFunc = func(state *State, args ...string) string { |
| 300 | return fmt.Sprintf(argsHelper(args).getOr(0, "%s"), elapsedTime(state)) |
| 301 | } |
| 302 | |
| 303 | // ElementString get value from bar by given key and print them |
| 304 | // bar.Set("myKey", "string to print") |
| 305 | // In template use as follows: {{string . "myKey"}} |
| 306 | var ElementString ElementFunc = func(state *State, args ...string) string { |
| 307 | if len(args) == 0 { |
| 308 | return "" |
| 309 | } |
| 310 | v := state.Get(args[0]) |
| 311 | if v == nil { |
| 312 | return "" |
| 313 | } |
| 314 | return fmt.Sprint(v) |
| 315 | } |
| 316 | |
| 317 | // ElementCycle return next argument for every call |
| 318 | // In template use as follows: {{cycle . "1" "2" "3"}} |
| 319 | // Or mix width other elements: {{ bar . "" "" (cycle . "↖" "↗" "↘" "↙" )}} |
| 320 | var ElementCycle ElementFunc = func(state *State, args ...string) string { |
| 321 | if len(args) == 0 { |
| 322 | return "" |
| 323 | } |
| 324 | n, _ := state.Get(cycleObj).(int) |
| 325 | if n >= len(args) { |
| 326 | n = 0 |
| 327 | } |
| 328 | state.Set(cycleObj, n+1) |
| 329 | return args[n] |
| 330 | } |