element.go 9.1 KB

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