package pb import ( "bytes" "fmt" "math" "strings" "sync" "time" ) const ( adElPlaceholder = "%_ad_el_%" adElPlaceholderLen = len(adElPlaceholder) ) var ( defaultBarEls = [5]string{"[", "-", ">", "_", "]"} ) // Element is an interface for bar elements type Element interface { ProgressElement(state *State, args ...string) string } // ElementFunc type implements Element interface and created for simplify elements type ElementFunc func(state *State, args ...string) string // ProgressElement just call self func func (e ElementFunc) ProgressElement(state *State, args ...string) string { return e(state, args...) } var elementsM sync.Mutex var elements = map[string]Element{ "percent": ElementPercent, "counters": ElementCounters, "bar": adaptiveWrap(ElementBar), "speed": ElementSpeed, "rtime": ElementRemainingTime, "etime": ElementElapsedTime, "string": ElementString, "cycle": ElementCycle, } // RegisterElement give you a chance to use custom elements func RegisterElement(name string, el Element, adaptive bool) { if adaptive { el = adaptiveWrap(el) } elementsM.Lock() elements[name] = el elementsM.Unlock() } type argsHelper []string func (args argsHelper) getOr(n int, value string) string { if len(args) > n { return args[n] } return value } func (args argsHelper) getNotEmptyOr(n int, value string) (v string) { if v = args.getOr(n, value); v == "" { return value } return } func adaptiveWrap(el Element) Element { return ElementFunc(func(state *State, args ...string) string { state.recalc = append(state.recalc, ElementFunc(func(s *State, _ ...string) (result string) { s.adaptive = true result = el.ProgressElement(s, args...) s.adaptive = false return })) return adElPlaceholder }) } // ElementPercent shows current percent of progress. // Optionally can take one or two string arguments. // First string will be used as value for format float64, default is "%.02f%%". // Second string will be used when percent can't be calculated, default is "?%" // In template use as follows: {{percent .}} or {{percent . "%.03f%%"}} or {{percent . "%.03f%%" "?"}} var ElementPercent ElementFunc = func(state *State, args ...string) string { argsh := argsHelper(args) if state.Total() > 0 { return fmt.Sprintf( argsh.getNotEmptyOr(0, "%.02f%%"), float64(state.Value())/(float64(state.Total())/float64(100)), ) } return argsh.getOr(1, "?%") } // ElementCounters shows current and total values. // Optionally can take one or two string arguments. // First string will be used as format value when Total is present (>0). Default is "%s / %s" // Second string will be used when total <= 0. Default is "%[1]s" // In template use as follows: {{counters .}} or {{counters . "%s/%s"}} or {{counters . "%s/%s" "%s/?"}} var ElementCounters ElementFunc = func(state *State, args ...string) string { var f string if state.Total() > 0 { f = argsHelper(args).getNotEmptyOr(0, "%s / %s") } else { f = argsHelper(args).getNotEmptyOr(1, "%[1]s") } return fmt.Sprintf(f, state.Format(state.Value()), state.Format(state.Total())) } type elementKey int const ( barObj elementKey = iota speedObj cycleObj ) type bar struct { eb [5][]byte // elements in bytes cc [5]int // cell counts buf *bytes.Buffer } func (p *bar) write(state *State, eln, width int) int { repeat := width / p.cc[eln] remainder := width % p.cc[eln] for i := 0; i < repeat; i++ { p.buf.Write(p.eb[eln]) } if remainder > 0 { StripStringToBuffer(string(p.eb[eln]), remainder, p.buf) } return width } func getProgressObj(state *State, args ...string) (p *bar) { var ok bool if p, ok = state.Get(barObj).(*bar); !ok { p = &bar{ buf: bytes.NewBuffer(nil), } state.Set(barObj, p) } argsH := argsHelper(args) for i := range p.eb { arg := argsH.getNotEmptyOr(i, defaultBarEls[i]) if string(p.eb[i]) != arg { p.cc[i] = CellCount(arg) p.eb[i] = []byte(arg) if p.cc[i] == 0 { p.cc[i] = 1 p.eb[i] = []byte(" ") } } } return } // ElementBar make progress bar view [-->__] // Optionally can take up to 5 string arguments. Defaults is "[", "-", ">", "_", "]" // In template use as follows: {{bar . }} or {{bar . "<" "oOo" "|" "~" ">"}} // Color args: {{bar . (red "[") (green "-") ... var ElementBar ElementFunc = func(state *State, args ...string) string { // init var p = getProgressObj(state, args...) total, value := state.Total(), state.Value() if total < 0 { total = -total } if value < 0 { value = -value } // check for overflow if total != 0 && value > total { total = value } p.buf.Reset() var widthLeft = state.AdaptiveElWidth() if widthLeft <= 0 || !state.IsAdaptiveWidth() { widthLeft = 30 } // write left border if p.cc[0] < widthLeft { widthLeft -= p.write(state, 0, p.cc[0]) } else { p.write(state, 0, widthLeft) return p.buf.String() } // check right border size if p.cc[4] < widthLeft { // write later widthLeft -= p.cc[4] } else { p.write(state, 4, widthLeft) return p.buf.String() } var curCount int if total > 0 { // calculate count of currenct space curCount = int(math.Ceil((float64(value) / float64(total)) * float64(widthLeft))) } // write bar if total == value && state.IsFinished() { widthLeft -= p.write(state, 1, curCount) } else if toWrite := curCount - p.cc[2]; toWrite > 0 { widthLeft -= p.write(state, 1, toWrite) widthLeft -= p.write(state, 2, p.cc[2]) } else if curCount > 0 { widthLeft -= p.write(state, 2, curCount) } if widthLeft > 0 { widthLeft -= p.write(state, 3, widthLeft) } // write right border p.write(state, 4, p.cc[4]) // cut result and return string return p.buf.String() } func elapsedTime(state *State) string { elapsed := state.Time().Sub(state.StartTime()) var precision time.Duration var ok bool if precision, ok = state.Get(TimeRound).(time.Duration); !ok { // default behavior: round to nearest .1s when elapsed < 10s // // we compare with 9.95s as opposed to 10s to avoid an annoying // interaction with the fixed precision display code below, // where 9.9s would be rounded to 10s but printed as 10.0s, and // then 10.0s would be rounded to 10s and printed as 10s if elapsed < 9950*time.Millisecond { precision = 100 * time.Millisecond } else { precision = time.Second } } rounded := elapsed.Round(precision) if precision < time.Second && rounded >= time.Second { // special handling to ensure string is shown with the given // precision, with trailing zeros after the decimal point if // necessary reference := (2*time.Second - time.Nanosecond).Truncate(precision).String() // reference looks like "1.9[...]9s", telling us how many // decimal digits we need neededDecimals := len(reference) - 3 s := rounded.String() dotIndex := strings.LastIndex(s, ".") if dotIndex != -1 { // s has the form "[stuff].[decimals]s" decimals := len(s) - dotIndex - 2 extraZeros := neededDecimals - decimals return fmt.Sprintf("%s%ss", s[:len(s)-1], strings.Repeat("0", extraZeros)) } else { // s has the form "[stuff]s" return fmt.Sprintf("%s.%ss", s[:len(s)-1], strings.Repeat("0", neededDecimals)) } } else { return rounded.String() } } // ElementRemainingTime calculates remaining time based on speed (EWMA) // Optionally can take one or two string arguments. // First string will be used as value for format time duration string, default is "%s". // Second string will be used when bar finished and value indicates elapsed time, default is "%s" // Third string will be used when value not available, default is "?" // In template use as follows: {{rtime .}} or {{rtime . "%s remain"}} or {{rtime . "%s remain" "%s total" "???"}} var ElementRemainingTime ElementFunc = func(state *State, args ...string) string { if state.IsFinished() { return fmt.Sprintf(argsHelper(args).getOr(1, "%s"), elapsedTime(state)) } sp := getSpeedObj(state).value(state) if sp > 0 { remain := float64(state.Total() - state.Value()) remainDur := time.Duration(remain/sp) * time.Second return fmt.Sprintf(argsHelper(args).getOr(0, "%s"), remainDur) } return argsHelper(args).getOr(2, "?") } // ElementElapsedTime shows elapsed time // Optionally can take one argument - it's format for time string. // In template use as follows: {{etime .}} or {{etime . "%s elapsed"}} var ElementElapsedTime ElementFunc = func(state *State, args ...string) string { return fmt.Sprintf(argsHelper(args).getOr(0, "%s"), elapsedTime(state)) } // ElementString get value from bar by given key and print them // bar.Set("myKey", "string to print") // In template use as follows: {{string . "myKey"}} var ElementString ElementFunc = func(state *State, args ...string) string { if len(args) == 0 { return "" } v := state.Get(args[0]) if v == nil { return "" } return fmt.Sprint(v) } // ElementCycle return next argument for every call // In template use as follows: {{cycle . "1" "2" "3"}} // Or mix width other elements: {{ bar . "" "" (cycle . "↖" "↗" "↘" "↙" )}} var ElementCycle ElementFunc = func(state *State, args ...string) string { if len(args) == 0 { return "" } n, _ := state.Get(cycleObj).(int) if n >= len(args) { n = 0 } state.Set(cycleObj, n+1) return args[n] }