blob: 3882550d350d651a3d5b97a0a8bfe97b7141990c [file] [log] [blame]
Abhay Kumara61c5222025-11-10 07:32:50 +00001package pb
2
3import (
4 "bytes"
5 "fmt"
6 "math"
7 "strings"
8 "sync"
9 "time"
10)
11
12const (
13 adElPlaceholder = "%_ad_el_%"
14 adElPlaceholderLen = len(adElPlaceholder)
15)
16
17var (
18 defaultBarEls = [5]string{"[", "-", ">", "_", "]"}
19)
20
21// Element is an interface for bar elements
22type Element interface {
23 ProgressElement(state *State, args ...string) string
24}
25
26// ElementFunc type implements Element interface and created for simplify elements
27type ElementFunc func(state *State, args ...string) string
28
29// ProgressElement just call self func
30func (e ElementFunc) ProgressElement(state *State, args ...string) string {
31 return e(state, args...)
32}
33
34var elementsM sync.Mutex
35
36var 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
48func 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
57type argsHelper []string
58
59func (args argsHelper) getOr(n int, value string) string {
60 if len(args) > n {
61 return args[n]
62 }
63 return value
64}
65
66func (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
73func 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%%" "?"}}
90var 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/?"}}
106var 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
116type elementKey int
117
118const (
119 barObj elementKey = iota
120 speedObj
121 cycleObj
122)
123
124type bar struct {
125 eb [5][]byte // elements in bytes
126 cc [5]int // cell counts
127 buf *bytes.Buffer
128}
129
130func (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
142func 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 "-") ...
169var 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
235func 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" "???"}}
283var 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"}}
299var 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"}}
306var 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 . "↖" "↗" "↘" "↙" )}}
320var 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}