2021-02-01 00:10:16 +01:00
|
|
|
package pb
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
"math"
|
2021-04-24 00:34:07 +02:00
|
|
|
"strings"
|
2021-02-01 00:10:16 +01:00
|
|
|
"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]
|
2021-02-16 21:24:44 +01:00
|
|
|
remainder := width % p.cc[eln]
|
2021-02-01 00:10:16 +01:00
|
|
|
for i := 0; i < repeat; i++ {
|
|
|
|
p.buf.Write(p.eb[eln])
|
|
|
|
}
|
2021-02-16 21:24:44 +01:00
|
|
|
if remainder > 0 {
|
|
|
|
StripStringToBuffer(string(p.eb[eln]), remainder, p.buf)
|
|
|
|
}
|
2021-02-01 00:10:16 +01:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2021-04-24 00:34:07 +02:00
|
|
|
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()
|
|
|
|
}
|
2021-03-25 16:56:10 +01:00
|
|
|
}
|
|
|
|
|
2021-02-01 00:10:16 +01:00
|
|
|
// 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 {
|
2021-03-25 16:56:10 +01:00
|
|
|
if state.IsFinished() {
|
|
|
|
return fmt.Sprintf(argsHelper(args).getOr(1, "%s"), elapsedTime(state))
|
|
|
|
}
|
2021-02-01 00:10:16 +01:00
|
|
|
sp := getSpeedObj(state).value(state)
|
2021-03-25 16:56:10 +01:00
|
|
|
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)
|
2021-02-01 00:10:16 +01:00
|
|
|
}
|
2021-03-25 16:56:10 +01:00
|
|
|
return argsHelper(args).getOr(2, "?")
|
2021-02-01 00:10:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// ElementElapsedTime shows elapsed time
|
2021-04-24 00:34:07 +02:00
|
|
|
// Optionally can take one argument - it's format for time string.
|
2021-02-01 00:10:16 +01:00
|
|
|
// In template use as follows: {{etime .}} or {{etime . "%s elapsed"}}
|
|
|
|
var ElementElapsedTime ElementFunc = func(state *State, args ...string) string {
|
2021-03-25 16:56:10 +01:00
|
|
|
return fmt.Sprintf(argsHelper(args).getOr(0, "%s"), elapsedTime(state))
|
2021-02-01 00:10:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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]
|
|
|
|
}
|