pkg.re/essentialkaos/ek.v11@v12.41.0+incompatible/progress/progress.go (about) 1 // Package progress provides methods and structs for creating terminal progress bar 2 package progress 3 4 // ////////////////////////////////////////////////////////////////////////////////// // 5 // // 6 // Copyright (c) 2022 ESSENTIAL KAOS // 7 // Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0> // 8 // // 9 // ////////////////////////////////////////////////////////////////////////////////// // 10 11 import ( 12 "fmt" 13 "io" 14 "strconv" 15 "strings" 16 "sync" 17 "sync/atomic" 18 "time" 19 20 "pkg.re/essentialkaos/ek.v12/fmtc" 21 "pkg.re/essentialkaos/ek.v12/fmtutil" 22 "pkg.re/essentialkaos/ek.v12/mathutil" 23 ) 24 25 // ////////////////////////////////////////////////////////////////////////////////// // 26 27 // MIN_WIDTH is minimal progress bar width 28 const MIN_WIDTH = 80 29 30 // PROGRESS_BAR_SYMBOL is symbol for creating progress bar 31 const PROGRESS_BAR_SYMBOL = "—" 32 33 // ////////////////////////////////////////////////////////////////////////////////// // 34 35 // Bar is progress bar struct 36 type Bar struct { 37 settings Settings 38 39 startTime time.Time 40 started bool 41 finished bool 42 43 finishChan chan bool 44 45 current int64 46 total int64 47 name string 48 49 buffer string 50 51 ticker *time.Ticker 52 passThruCalc *PassThruCalc 53 phCounter int 54 55 reader *passThruReader 56 writer *passThruWriter 57 58 mu *sync.RWMutex 59 } 60 61 // Settings contains progress bar settings 62 type Settings struct { 63 RefreshRate time.Duration 64 65 NameColorTag string 66 BarFgColorTag string 67 BarBgColorTag string 68 PercentColorTag string 69 ProgressColorTag string 70 SpeedColorTag string 71 RemainingColorTag string 72 73 ShowSpeed bool 74 ShowName bool 75 ShowPercentage bool 76 ShowProgress bool 77 ShowRemaining bool 78 79 Width int 80 NameSize int 81 82 IsSize bool 83 } 84 85 // ////////////////////////////////////////////////////////////////////////////////// // 86 87 type passThruReader struct { 88 io.Reader 89 bar *Bar 90 } 91 92 type passThruWriter struct { 93 io.Writer 94 bar *Bar 95 } 96 97 // ////////////////////////////////////////////////////////////////////////////////// // 98 99 // DefaultSettings is default progress bar settings 100 var DefaultSettings = Settings{ 101 RefreshRate: 100 * time.Millisecond, 102 NameColorTag: "{b}", 103 BarFgColorTag: "{r}", 104 BarBgColorTag: "{s-}", 105 PercentColorTag: "{m}", 106 SpeedColorTag: "{r}", 107 ProgressColorTag: "{g}", 108 RemainingColorTag: "{c}", 109 ShowName: true, 110 ShowPercentage: true, 111 ShowProgress: true, 112 ShowSpeed: true, 113 ShowRemaining: true, 114 IsSize: true, 115 Width: 88, 116 } 117 118 // ////////////////////////////////////////////////////////////////////////////////// // 119 120 // New creates new progress bar struct 121 func New(total int64, name string) *Bar { 122 return &Bar{ 123 settings: DefaultSettings, 124 name: name, 125 total: total, 126 mu: &sync.RWMutex{}, 127 } 128 } 129 130 // ////////////////////////////////////////////////////////////////////////////////// // 131 132 // Start starts progress processing 133 func (b *Bar) Start() { 134 if b.IsStarted() && !b.IsFinished() { 135 return 136 } 137 138 b.phCounter = 0 139 b.current = 0 140 b.started = true 141 b.finished = false 142 b.startTime = time.Now() 143 b.ticker = time.NewTicker(b.settings.RefreshRate) 144 b.finishChan = make(chan bool) 145 146 if b.total > 0 { 147 b.passThruCalc = NewPassThruCalc(b.total, 10.0) 148 } 149 150 go b.renderer() 151 } 152 153 // Finish finishes progress processing 154 func (b *Bar) Finish() { 155 b.mu.Lock() 156 defer b.mu.Unlock() 157 158 if b.finished || !b.started { 159 return 160 } 161 162 fmtc.TPrintf(b.renderElements()) 163 fmtc.NewLine() 164 165 b.finishChan <- true 166 } 167 168 // UpdateSettings updates progress settings 169 func (b *Bar) UpdateSettings(s Settings) { 170 b.mu.Lock() 171 b.settings = s 172 b.mu.Unlock() 173 } 174 175 // SetName sets progress bar name 176 func (b *Bar) SetName(name string) { 177 b.mu.Lock() 178 b.name = name 179 b.mu.Unlock() 180 } 181 182 // Name returns progress bar name 183 func (b *Bar) Name() string { 184 b.mu.RLock() 185 defer b.mu.RUnlock() 186 return b.name 187 } 188 189 // SetTotal sets total progress bar value 190 func (b *Bar) SetTotal(v int64) { 191 b.mu.Lock() 192 193 if b.passThruCalc == nil { 194 b.passThruCalc = NewPassThruCalc(v, 10.0) 195 } else { 196 b.passThruCalc.SetTotal(v) 197 } 198 199 b.mu.Unlock() 200 201 atomic.StoreInt64(&b.total, v) 202 } 203 204 // Total returns total progress bar value 205 func (b *Bar) Total() int64 { 206 return atomic.LoadInt64(&b.total) 207 } 208 209 // SetCurrent sets current progress bar value 210 func (b *Bar) SetCurrent(v int64) { 211 atomic.StoreInt64(&b.current, v) 212 } 213 214 // Current returns current progress bar value 215 func (b *Bar) Current() int64 { 216 return atomic.LoadInt64(&b.current) 217 } 218 219 func (b *Bar) Add(v int) { 220 atomic.AddInt64(&b.current, int64(v)) 221 } 222 223 // Add64 adds given value ti 224 func (b *Bar) Add64(v int64) { 225 atomic.AddInt64(&b.current, v) 226 } 227 228 // IsFinished returns true if progress proccesing is finished 229 func (b *Bar) IsFinished() bool { 230 b.mu.RLock() 231 defer b.mu.RUnlock() 232 return b.finished 233 } 234 235 // IsStarted returns true if progress proccesing is started 236 func (b *Bar) IsStarted() bool { 237 b.mu.RLock() 238 defer b.mu.RUnlock() 239 return b.started 240 } 241 242 // Reader creates and returns pass thru proxy reader 243 func (b *Bar) Reader(r io.Reader) io.Reader { 244 if b.reader != nil { 245 b.reader.Reader = r 246 } else { 247 b.reader = &passThruReader{ 248 Reader: r, 249 bar: b, 250 } 251 } 252 253 return b.reader 254 } 255 256 // Writer creates and returns pass thru proxy reader 257 func (b *Bar) Writer(w io.Writer) io.Writer { 258 if b.writer != nil { 259 b.writer.Writer = w 260 } else { 261 b.writer = &passThruWriter{ 262 Writer: w, 263 bar: b, 264 } 265 } 266 267 return b.writer 268 } 269 270 // ////////////////////////////////////////////////////////////////////////////////// // 271 272 // renderer is rendering loop func 273 func (b *Bar) renderer() { 274 for { 275 select { 276 case <-b.finishChan: 277 b.finished = true 278 b.ticker.Stop() 279 b.render() 280 return 281 case <-b.ticker.C: 282 b.render() 283 } 284 } 285 } 286 287 // render renders current progress bar state 288 func (b *Bar) render() { 289 b.mu.RLock() 290 defer b.mu.RUnlock() 291 292 if !b.finished && b.total > 0 && b.current >= b.total { 293 b.finished = true 294 b.ticker.Stop() 295 } 296 297 result := b.renderElements() 298 299 // render text only if changed 300 if b.buffer != result { 301 fmtc.TPrintf(result) 302 } 303 304 if b.total > 0 { 305 b.buffer = result 306 } 307 308 if b.finished { 309 fmtc.NewLine() 310 } 311 } 312 313 // renderElements returns text with all progress bar graphics and text 314 func (b *Bar) renderElements() string { 315 var size, totalSize int 316 var name, percentage, bar, progress, speed, remaining string 317 var statSpeed float64 318 var statRemaining time.Duration 319 320 if b.passThruCalc != nil && (b.settings.ShowSpeed || b.settings.ShowRemaining) { 321 if b.finished { 322 statRemaining = time.Since(b.startTime) 323 statSpeed = float64(b.current) / statRemaining.Seconds() 324 } else { 325 statSpeed, statRemaining = b.passThruCalc.Calculate(b.current) 326 } 327 } 328 329 if b.settings.ShowName && b.name != "" { 330 name, size = b.renderName() 331 totalSize += size + 1 332 } 333 334 if b.total > 0 { 335 if b.settings.ShowPercentage { 336 percentage, size = b.renderPercentage() 337 totalSize += size + 1 338 } 339 340 if b.settings.ShowProgress { 341 progress, size = b.renderProgress() 342 totalSize += size + 3 343 } 344 345 if b.settings.ShowSpeed { 346 speed, size = b.renderSpeed(statSpeed) 347 totalSize += size + 3 348 } 349 350 if b.settings.ShowRemaining { 351 remaining, size = b.renderRemaining(statRemaining) 352 totalSize += size + 3 353 } 354 } 355 356 bar = b.renderBar(totalSize) 357 358 var result string 359 360 if b.settings.ShowName && name != "" { 361 result += name + " " 362 } 363 364 result += bar + " " 365 366 if b.total > 0 { 367 if b.settings.ShowPercentage { 368 result += percentage 369 } 370 371 if b.settings.ShowProgress { 372 result += " {s-}•{!} " + progress 373 } 374 375 if b.settings.ShowSpeed { 376 result += " {s-}•{!} " + speed 377 } 378 379 if b.settings.ShowRemaining { 380 result += " {s-}•{!} " + remaining 381 } 382 } 383 384 return result 385 } 386 387 // renderName returns name text 388 func (b *Bar) renderName() (string, int) { 389 var result string 390 391 if b.settings.NameSize > 0 && len(b.name) < b.settings.NameSize { 392 result = fmt.Sprintf("%"+strconv.Itoa(b.settings.NameSize)+"s", b.name) 393 } else { 394 result = b.name 395 } 396 397 if fmtc.DisableColors || b.settings.NameColorTag == "" { 398 return result, len(result) 399 } 400 401 return b.settings.NameColorTag + result + "{!}", len(result) 402 } 403 404 // renderPercentage returns parcentage text 405 func (b *Bar) renderPercentage() (string, int) { 406 var perc float64 407 var result string 408 409 switch { 410 case b.total <= 0: 411 perc = 0.0 412 case b.current > b.total: 413 perc = 100.0 414 default: 415 perc = (float64(b.current) / float64(b.total)) * 100.0 416 } 417 418 if perc == 100.0 { 419 result = "100%%" 420 } else { 421 result = fmt.Sprintf("%5.1f", perc) + "%%" 422 } 423 424 if fmtc.DisableColors || b.settings.PercentColorTag == "" { 425 return result, len(result) 426 } 427 428 return b.settings.PercentColorTag + result + "{!}", len(result) 429 } 430 431 // renderProgress returns progress text 432 func (b *Bar) renderProgress() (string, int) { 433 var result, curText, totText, label string 434 var size int 435 436 if b.settings.IsSize { 437 curText, totText, label = getPrettyCTSize(b.current, b.total) 438 } else { 439 curText, totText, label = getPrettyCTNum(b.current, b.total) 440 } 441 442 size = (len(totText) * 2) + len(label) + 1 443 444 if label == "" { 445 result = fmt.Sprintf("%"+strconv.Itoa(size)+"s", curText+"/"+totText) 446 } else { 447 result = fmt.Sprintf("%"+strconv.Itoa(size)+"s", curText+"/"+totText+label) 448 } 449 450 if fmtc.DisableColors || b.settings.ProgressColorTag == "" { 451 return result, size 452 } 453 454 return b.settings.ProgressColorTag + result + "{!}", size 455 } 456 457 // renderSpeed returns speed text 458 func (b *Bar) renderSpeed(speed float64) (string, int) { 459 var result string 460 461 if b.settings.IsSize { 462 result = fmt.Sprintf("%9s/s", fmtutil.PrettySize(speed, " ")) 463 } else { 464 result = formatSpeedNum(speed) 465 } 466 467 if fmtc.DisableColors || b.settings.SpeedColorTag == "" { 468 return result, len(result) 469 } 470 471 return b.settings.SpeedColorTag + result + "{!}", len(result) 472 } 473 474 // renderRemaining returns remaining text 475 func (b *Bar) renderRemaining(remaining time.Duration) (string, int) { 476 var result string 477 var min, sec int 478 479 d := int(remaining.Seconds()) 480 481 if d >= 60 { 482 min = d / 60 483 sec = d % 60 484 } else { 485 sec = d 486 } 487 488 result = fmt.Sprintf("%2d:%02d", min, sec) 489 490 if fmtc.DisableColors || b.settings.RemainingColorTag == "" { 491 return result, len(result) 492 } 493 494 return b.settings.RemainingColorTag + result + "{!}", len(result) 495 } 496 497 // renderBar returns bar graphics 498 func (b *Bar) renderBar(dataSize int) string { 499 size := mathutil.Max(5, mathutil.Max(MIN_WIDTH, b.settings.Width)-dataSize) 500 501 if b.total <= 0 { 502 return b.renderPlaceholder(size) 503 } 504 505 if b.current >= b.total { 506 switch fmtc.DisableColors || b.settings.BarFgColorTag == "" { 507 case true: 508 return strings.Repeat(PROGRESS_BAR_SYMBOL, size) 509 case false: 510 return b.settings.BarFgColorTag + strings.Repeat(PROGRESS_BAR_SYMBOL, size) + "{!}" 511 } 512 } 513 514 cur := int((float64(b.current) / float64(b.total)) * float64(size)) 515 516 if fmtc.DisableColors || b.settings.BarFgColorTag == "" { 517 return strings.Repeat(PROGRESS_BAR_SYMBOL, cur) + strings.Repeat(" ", size-cur) 518 } 519 520 return b.settings.BarFgColorTag + strings.Repeat(PROGRESS_BAR_SYMBOL, cur) + b.settings.BarBgColorTag + strings.Repeat(PROGRESS_BAR_SYMBOL, size-cur) + "{!}" 521 } 522 523 // renderPlaceholder returns placeholder bar graphics 524 func (b *Bar) renderPlaceholder(size int) string { 525 var result string 526 527 disableColors := fmtc.DisableColors || b.settings.BarFgColorTag == "" 528 529 for i := 0; i < size; i++ { 530 if disableColors { 531 if i%3 == b.phCounter { 532 result += PROGRESS_BAR_SYMBOL 533 } else { 534 result += " " 535 } 536 } else { 537 if i%3 == b.phCounter { 538 result += b.settings.BarFgColorTag 539 } else { 540 result += b.settings.BarBgColorTag 541 } 542 543 result += PROGRESS_BAR_SYMBOL 544 } 545 } 546 547 b.phCounter++ 548 549 if b.phCounter == 3 { 550 b.phCounter = 0 551 } 552 553 if disableColors { 554 return result 555 } 556 557 return result + "{!}" 558 } 559 560 // ////////////////////////////////////////////////////////////////////////////////// // 561 562 // Read reads data and updates progress bar 563 func (r *passThruReader) Read(p []byte) (int, error) { 564 n, err := r.Reader.Read(p) 565 566 if n > 0 { 567 r.bar.Add(n) 568 } 569 570 return n, err 571 } 572 573 // Write writes data and updates progress bar 574 func (w *passThruWriter) Write(p []byte) (int, error) { 575 n, err := w.Writer.Write(p) 576 577 if n > 0 { 578 w.bar.Add(n) 579 } 580 581 return n, err 582 } 583 584 // ////////////////////////////////////////////////////////////////////////////////// // 585 586 // getPrettyCTSize returns formatted current/total size text 587 func getPrettyCTSize(current, total int64) (string, string, string) { 588 var mod float64 589 var label string 590 591 switch { 592 case total > 1024*1024*1024: 593 mod = 1024 * 1024 * 1024 594 label = " GB" 595 case total > 1024*1024: 596 mod = 1024 * 1024 597 label = " MB" 598 case total > 1024: 599 mod = 1024 600 label = " KB" 601 default: 602 mod = 1 603 label = " B" 604 } 605 606 curText := fmt.Sprintf("%.1f", float64(current)/mod) 607 totText := fmt.Sprintf("%.1f", float64(total)/mod) 608 609 return curText, totText, label 610 } 611 612 // getPrettyCTNum returns formatted current/total number text 613 func getPrettyCTNum(current, total int64) (string, string, string) { 614 var mod float64 615 var label, curText, totText string 616 617 switch { 618 case total > 1000*1000*1000: 619 mod = 1000 * 1000 * 1000 620 label = "B" 621 case total > 1000*1000: 622 mod = 1000 * 1000 623 label = "M" 624 case total > 1000: 625 mod = 1000 626 label = "K" 627 default: 628 mod = 1 629 } 630 631 if total > 1000 { 632 curText = fmt.Sprintf("%.1f", float64(current)/mod) 633 totText = fmt.Sprintf("%.1f", float64(total)/mod) 634 } else { 635 curText = fmt.Sprintf("%d", int(float64(current)/mod)) 636 totText = fmt.Sprintf("%d", int(float64(total)/mod)) 637 } 638 639 return curText, totText, label 640 } 641 642 // formatSpeedNum formats speed number 643 func formatSpeedNum(s float64) string { 644 var mod float64 645 var label string 646 647 switch { 648 case s > 1000.0*1000.0*1000.0: 649 mod = 1000.0 * 1000.0 * 1000.0 650 label = "B" 651 case s > 1000.0*1000.0: 652 mod = 1000.0 * 1000.0 653 label = "M" 654 case s > 1000.0: 655 mod = 1000.0 656 label = "K" 657 default: 658 mod = 1 659 } 660 661 return fmt.Sprintf("%6g%s/s", fmtutil.Float(s/mod), label) 662 }