| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 |
- 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]
- }
|