github.com/schollz/progressbar/v3@v3.14.2/progressbar.go (about) 1 package progressbar 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "math" 8 "os" 9 "regexp" 10 "strings" 11 "sync" 12 "time" 13 14 "github.com/mitchellh/colorstring" 15 "github.com/rivo/uniseg" 16 "golang.org/x/term" 17 ) 18 19 // ProgressBar is a thread-safe, simple 20 // progress bar 21 type ProgressBar struct { 22 state state 23 config config 24 lock sync.Mutex 25 } 26 27 // State is the basic properties of the bar 28 type State struct { 29 Max int64 30 CurrentNum int64 31 CurrentPercent float64 32 CurrentBytes float64 33 SecondsSince float64 34 SecondsLeft float64 35 KBsPerSecond float64 36 Description string 37 } 38 39 type state struct { 40 currentNum int64 41 currentPercent int 42 lastPercent int 43 currentSaucerSize int 44 isAltSaucerHead bool 45 46 lastShown time.Time 47 startTime time.Time 48 49 counterTime time.Time 50 counterNumSinceLast int64 51 counterLastTenRates []float64 52 53 maxLineWidth int 54 currentBytes float64 55 finished bool 56 exit bool // Progress bar exit halfway 57 58 rendered string 59 } 60 61 type config struct { 62 max int64 // max number of the counter 63 maxHumanized string 64 maxHumanizedSuffix string 65 width int 66 writer io.Writer 67 theme Theme 68 renderWithBlankState bool 69 description string 70 iterationString string 71 ignoreLength bool // ignoreLength if max bytes not known 72 73 // whether the output is expected to contain color codes 74 colorCodes bool 75 76 // show rate of change in kB/sec or MB/sec 77 showBytes bool 78 // show the iterations per second 79 showIterationsPerSecond bool 80 showIterationsCount bool 81 82 // whether the progress bar should show elapsed time. 83 // always enabled if predictTime is true. 84 elapsedTime bool 85 86 showElapsedTimeOnFinish bool 87 88 // whether the progress bar should attempt to predict the finishing 89 // time of the progress based on the start time and the average 90 // number of seconds between increments. 91 predictTime bool 92 93 // minimum time to wait in between updates 94 throttleDuration time.Duration 95 96 // clear bar once finished 97 clearOnFinish bool 98 99 // spinnerType should be a number between 0-75 100 spinnerType int 101 102 // spinnerTypeOptionUsed remembers if the spinnerType was changed manually 103 spinnerTypeOptionUsed bool 104 105 // spinner represents the spinner as a slice of string 106 spinner []string 107 108 // fullWidth specifies whether to measure and set the bar to a specific width 109 fullWidth bool 110 111 // invisible doesn't render the bar at all, useful for debugging 112 invisible bool 113 114 onCompletion func() 115 116 // whether the render function should make use of ANSI codes to reduce console I/O 117 useANSICodes bool 118 119 // whether to use the IEC units (e.g. MiB) instead of the default SI units (e.g. MB) 120 useIECUnits bool 121 122 // showDescriptionAtLineEnd specifies whether description should be written at line end instead of line start 123 showDescriptionAtLineEnd bool 124 } 125 126 // Theme defines the elements of the bar 127 type Theme struct { 128 Saucer string 129 AltSaucerHead string 130 SaucerHead string 131 SaucerPadding string 132 BarStart string 133 BarEnd string 134 } 135 136 // Option is the type all options need to adhere to 137 type Option func(p *ProgressBar) 138 139 // OptionSetWidth sets the width of the bar 140 func OptionSetWidth(s int) Option { 141 return func(p *ProgressBar) { 142 p.config.width = s 143 } 144 } 145 146 // OptionSpinnerType sets the type of spinner used for indeterminate bars 147 func OptionSpinnerType(spinnerType int) Option { 148 return func(p *ProgressBar) { 149 p.config.spinnerTypeOptionUsed = true 150 p.config.spinnerType = spinnerType 151 } 152 } 153 154 // OptionSpinnerCustom sets the spinner used for indeterminate bars to the passed 155 // slice of string 156 func OptionSpinnerCustom(spinner []string) Option { 157 return func(p *ProgressBar) { 158 p.config.spinner = spinner 159 } 160 } 161 162 // OptionSetTheme sets the elements the bar is constructed of 163 func OptionSetTheme(t Theme) Option { 164 return func(p *ProgressBar) { 165 p.config.theme = t 166 } 167 } 168 169 // OptionSetVisibility sets the visibility 170 func OptionSetVisibility(visibility bool) Option { 171 return func(p *ProgressBar) { 172 p.config.invisible = !visibility 173 } 174 } 175 176 // OptionFullWidth sets the bar to be full width 177 func OptionFullWidth() Option { 178 return func(p *ProgressBar) { 179 p.config.fullWidth = true 180 } 181 } 182 183 // OptionSetWriter sets the output writer (defaults to os.StdOut) 184 func OptionSetWriter(w io.Writer) Option { 185 return func(p *ProgressBar) { 186 p.config.writer = w 187 } 188 } 189 190 // OptionSetRenderBlankState sets whether or not to render a 0% bar on construction 191 func OptionSetRenderBlankState(r bool) Option { 192 return func(p *ProgressBar) { 193 p.config.renderWithBlankState = r 194 } 195 } 196 197 // OptionSetDescription sets the description of the bar to render in front of it 198 func OptionSetDescription(description string) Option { 199 return func(p *ProgressBar) { 200 p.config.description = description 201 } 202 } 203 204 // OptionEnableColorCodes enables or disables support for color codes 205 // using mitchellh/colorstring 206 func OptionEnableColorCodes(colorCodes bool) Option { 207 return func(p *ProgressBar) { 208 p.config.colorCodes = colorCodes 209 } 210 } 211 212 // OptionSetElapsedTime will enable elapsed time. Always enabled if OptionSetPredictTime is true. 213 func OptionSetElapsedTime(elapsedTime bool) Option { 214 return func(p *ProgressBar) { 215 p.config.elapsedTime = elapsedTime 216 } 217 } 218 219 // OptionSetPredictTime will also attempt to predict the time remaining. 220 func OptionSetPredictTime(predictTime bool) Option { 221 return func(p *ProgressBar) { 222 p.config.predictTime = predictTime 223 } 224 } 225 226 // OptionShowCount will also print current count out of total 227 func OptionShowCount() Option { 228 return func(p *ProgressBar) { 229 p.config.showIterationsCount = true 230 } 231 } 232 233 // OptionShowIts will also print the iterations/second 234 func OptionShowIts() Option { 235 return func(p *ProgressBar) { 236 p.config.showIterationsPerSecond = true 237 } 238 } 239 240 // OptionShowElapsedOnFinish will keep the display of elapsed time on finish 241 func OptionShowElapsedTimeOnFinish() Option { 242 return func(p *ProgressBar) { 243 p.config.showElapsedTimeOnFinish = true 244 } 245 } 246 247 // OptionSetItsString sets what's displayed for iterations a second. The default is "it" which would display: "it/s" 248 func OptionSetItsString(iterationString string) Option { 249 return func(p *ProgressBar) { 250 p.config.iterationString = iterationString 251 } 252 } 253 254 // OptionThrottle will wait the specified duration before updating again. The default 255 // duration is 0 seconds. 256 func OptionThrottle(duration time.Duration) Option { 257 return func(p *ProgressBar) { 258 p.config.throttleDuration = duration 259 } 260 } 261 262 // OptionClearOnFinish will clear the bar once its finished 263 func OptionClearOnFinish() Option { 264 return func(p *ProgressBar) { 265 p.config.clearOnFinish = true 266 } 267 } 268 269 // OptionOnCompletion will invoke cmpl function once its finished 270 func OptionOnCompletion(cmpl func()) Option { 271 return func(p *ProgressBar) { 272 p.config.onCompletion = cmpl 273 } 274 } 275 276 // OptionShowBytes will update the progress bar 277 // configuration settings to display/hide kBytes/Sec 278 func OptionShowBytes(val bool) Option { 279 return func(p *ProgressBar) { 280 p.config.showBytes = val 281 } 282 } 283 284 // OptionUseANSICodes will use more optimized terminal i/o. 285 // 286 // Only useful in environments with support for ANSI escape sequences. 287 func OptionUseANSICodes(val bool) Option { 288 return func(p *ProgressBar) { 289 p.config.useANSICodes = val 290 } 291 } 292 293 // OptionUseIECUnits will enable IEC units (e.g. MiB) instead of the default 294 // SI units (e.g. MB). 295 func OptionUseIECUnits(val bool) Option { 296 return func(p *ProgressBar) { 297 p.config.useIECUnits = val 298 } 299 } 300 301 // OptionShowDescriptionAtLineEnd defines whether description should be written at line end instead of line start 302 func OptionShowDescriptionAtLineEnd() Option { 303 return func(p *ProgressBar) { 304 p.config.showDescriptionAtLineEnd = true 305 } 306 } 307 308 var defaultTheme = Theme{Saucer: "█", SaucerPadding: " ", BarStart: "|", BarEnd: "|"} 309 310 // NewOptions constructs a new instance of ProgressBar, with any options you specify 311 func NewOptions(max int, options ...Option) *ProgressBar { 312 return NewOptions64(int64(max), options...) 313 } 314 315 // NewOptions64 constructs a new instance of ProgressBar, with any options you specify 316 func NewOptions64(max int64, options ...Option) *ProgressBar { 317 b := ProgressBar{ 318 state: getBasicState(), 319 config: config{ 320 writer: os.Stdout, 321 theme: defaultTheme, 322 iterationString: "it", 323 width: 40, 324 max: max, 325 throttleDuration: 0 * time.Nanosecond, 326 elapsedTime: true, 327 predictTime: true, 328 spinnerType: 9, 329 invisible: false, 330 }, 331 } 332 333 for _, o := range options { 334 o(&b) 335 } 336 337 if b.config.spinnerType < 0 || b.config.spinnerType > 75 { 338 panic("invalid spinner type, must be between 0 and 75") 339 } 340 341 // ignoreLength if max bytes not known 342 if b.config.max == -1 { 343 b.config.ignoreLength = true 344 b.config.max = int64(b.config.width) 345 b.config.predictTime = false 346 } 347 348 b.config.maxHumanized, b.config.maxHumanizedSuffix = humanizeBytes(float64(b.config.max), 349 b.config.useIECUnits) 350 351 if b.config.renderWithBlankState { 352 b.RenderBlank() 353 } 354 355 return &b 356 } 357 358 func getBasicState() state { 359 now := time.Now() 360 return state{ 361 startTime: now, 362 lastShown: now, 363 counterTime: now, 364 } 365 } 366 367 // New returns a new ProgressBar 368 // with the specified maximum 369 func New(max int) *ProgressBar { 370 return NewOptions(max) 371 } 372 373 // DefaultBytes provides a progressbar to measure byte 374 // throughput with recommended defaults. 375 // Set maxBytes to -1 to use as a spinner. 376 func DefaultBytes(maxBytes int64, description ...string) *ProgressBar { 377 desc := "" 378 if len(description) > 0 { 379 desc = description[0] 380 } 381 return NewOptions64( 382 maxBytes, 383 OptionSetDescription(desc), 384 OptionSetWriter(os.Stderr), 385 OptionShowBytes(true), 386 OptionSetWidth(10), 387 OptionThrottle(65*time.Millisecond), 388 OptionShowCount(), 389 OptionOnCompletion(func() { 390 fmt.Fprint(os.Stderr, "\n") 391 }), 392 OptionSpinnerType(14), 393 OptionFullWidth(), 394 OptionSetRenderBlankState(true), 395 ) 396 } 397 398 // DefaultBytesSilent is the same as DefaultBytes, but does not output anywhere. 399 // String() can be used to get the output instead. 400 func DefaultBytesSilent(maxBytes int64, description ...string) *ProgressBar { 401 // Mostly the same bar as DefaultBytes 402 403 desc := "" 404 if len(description) > 0 { 405 desc = description[0] 406 } 407 return NewOptions64( 408 maxBytes, 409 OptionSetDescription(desc), 410 OptionSetWriter(io.Discard), 411 OptionShowBytes(true), 412 OptionSetWidth(10), 413 OptionThrottle(65*time.Millisecond), 414 OptionShowCount(), 415 OptionSpinnerType(14), 416 OptionFullWidth(), 417 ) 418 } 419 420 // Default provides a progressbar with recommended defaults. 421 // Set max to -1 to use as a spinner. 422 func Default(max int64, description ...string) *ProgressBar { 423 desc := "" 424 if len(description) > 0 { 425 desc = description[0] 426 } 427 return NewOptions64( 428 max, 429 OptionSetDescription(desc), 430 OptionSetWriter(os.Stderr), 431 OptionSetWidth(10), 432 OptionThrottle(65*time.Millisecond), 433 OptionShowCount(), 434 OptionShowIts(), 435 OptionOnCompletion(func() { 436 fmt.Fprint(os.Stderr, "\n") 437 }), 438 OptionSpinnerType(14), 439 OptionFullWidth(), 440 OptionSetRenderBlankState(true), 441 ) 442 } 443 444 // DefaultSilent is the same as Default, but does not output anywhere. 445 // String() can be used to get the output instead. 446 func DefaultSilent(max int64, description ...string) *ProgressBar { 447 // Mostly the same bar as Default 448 449 desc := "" 450 if len(description) > 0 { 451 desc = description[0] 452 } 453 return NewOptions64( 454 max, 455 OptionSetDescription(desc), 456 OptionSetWriter(io.Discard), 457 OptionSetWidth(10), 458 OptionThrottle(65*time.Millisecond), 459 OptionShowCount(), 460 OptionShowIts(), 461 OptionSpinnerType(14), 462 OptionFullWidth(), 463 ) 464 } 465 466 // String returns the current rendered version of the progress bar. 467 // It will never return an empty string while the progress bar is running. 468 func (p *ProgressBar) String() string { 469 return p.state.rendered 470 } 471 472 // RenderBlank renders the current bar state, you can use this to render a 0% state 473 func (p *ProgressBar) RenderBlank() error { 474 p.lock.Lock() 475 defer p.lock.Unlock() 476 477 if p.config.invisible { 478 return nil 479 } 480 if p.state.currentNum == 0 { 481 p.state.lastShown = time.Time{} 482 } 483 return p.render() 484 } 485 486 // Reset will reset the clock that is used 487 // to calculate current time and the time left. 488 func (p *ProgressBar) Reset() { 489 p.lock.Lock() 490 defer p.lock.Unlock() 491 492 p.state = getBasicState() 493 } 494 495 // Finish will fill the bar to full 496 func (p *ProgressBar) Finish() error { 497 p.lock.Lock() 498 p.state.currentNum = p.config.max 499 p.lock.Unlock() 500 return p.Add(0) 501 } 502 503 // Exit will exit the bar to keep current state 504 func (p *ProgressBar) Exit() error { 505 p.lock.Lock() 506 defer p.lock.Unlock() 507 508 p.state.exit = true 509 if p.config.onCompletion != nil { 510 p.config.onCompletion() 511 } 512 return nil 513 } 514 515 // Add will add the specified amount to the progressbar 516 func (p *ProgressBar) Add(num int) error { 517 return p.Add64(int64(num)) 518 } 519 520 // Set will set the bar to a current number 521 func (p *ProgressBar) Set(num int) error { 522 return p.Set64(int64(num)) 523 } 524 525 // Set64 will set the bar to a current number 526 func (p *ProgressBar) Set64(num int64) error { 527 p.lock.Lock() 528 toAdd := num - int64(p.state.currentBytes) 529 p.lock.Unlock() 530 return p.Add64(toAdd) 531 } 532 533 // Add64 will add the specified amount to the progressbar 534 func (p *ProgressBar) Add64(num int64) error { 535 if p.config.invisible { 536 return nil 537 } 538 p.lock.Lock() 539 defer p.lock.Unlock() 540 541 if p.state.exit { 542 return nil 543 } 544 545 // error out since OptionSpinnerCustom will always override a manually set spinnerType 546 if p.config.spinnerTypeOptionUsed && len(p.config.spinner) > 0 { 547 return errors.New("OptionSpinnerType and OptionSpinnerCustom cannot be used together") 548 } 549 550 if p.config.max == 0 { 551 return errors.New("max must be greater than 0") 552 } 553 554 if p.state.currentNum < p.config.max { 555 if p.config.ignoreLength { 556 p.state.currentNum = (p.state.currentNum + num) % p.config.max 557 } else { 558 p.state.currentNum += num 559 } 560 } 561 562 p.state.currentBytes += float64(num) 563 564 // reset the countdown timer every second to take rolling average 565 p.state.counterNumSinceLast += num 566 if time.Since(p.state.counterTime).Seconds() > 0.5 { 567 p.state.counterLastTenRates = append(p.state.counterLastTenRates, float64(p.state.counterNumSinceLast)/time.Since(p.state.counterTime).Seconds()) 568 if len(p.state.counterLastTenRates) > 10 { 569 p.state.counterLastTenRates = p.state.counterLastTenRates[1:] 570 } 571 p.state.counterTime = time.Now() 572 p.state.counterNumSinceLast = 0 573 } 574 575 percent := float64(p.state.currentNum) / float64(p.config.max) 576 p.state.currentSaucerSize = int(percent * float64(p.config.width)) 577 p.state.currentPercent = int(percent * 100) 578 updateBar := p.state.currentPercent != p.state.lastPercent && p.state.currentPercent > 0 579 580 p.state.lastPercent = p.state.currentPercent 581 if p.state.currentNum > p.config.max { 582 return errors.New("current number exceeds max") 583 } 584 585 // always update if show bytes/second or its/second 586 if updateBar || p.config.showIterationsPerSecond || p.config.showIterationsCount { 587 return p.render() 588 } 589 590 return nil 591 } 592 593 // Clear erases the progress bar from the current line 594 func (p *ProgressBar) Clear() error { 595 return clearProgressBar(p.config, p.state) 596 } 597 598 // Describe will change the description shown before the progress, which 599 // can be changed on the fly (as for a slow running process). 600 func (p *ProgressBar) Describe(description string) { 601 p.lock.Lock() 602 defer p.lock.Unlock() 603 p.config.description = description 604 if p.config.invisible { 605 return 606 } 607 p.render() 608 } 609 610 // New64 returns a new ProgressBar 611 // with the specified maximum 612 func New64(max int64) *ProgressBar { 613 return NewOptions64(max) 614 } 615 616 // GetMax returns the max of a bar 617 func (p *ProgressBar) GetMax() int { 618 return int(p.config.max) 619 } 620 621 // GetMax64 returns the current max 622 func (p *ProgressBar) GetMax64() int64 { 623 return p.config.max 624 } 625 626 // ChangeMax takes in a int 627 // and changes the max value 628 // of the progress bar 629 func (p *ProgressBar) ChangeMax(newMax int) { 630 p.ChangeMax64(int64(newMax)) 631 } 632 633 // ChangeMax64 is basically 634 // the same as ChangeMax, 635 // but takes in a int64 636 // to avoid casting 637 func (p *ProgressBar) ChangeMax64(newMax int64) { 638 p.config.max = newMax 639 640 if p.config.showBytes { 641 p.config.maxHumanized, p.config.maxHumanizedSuffix = humanizeBytes(float64(p.config.max), 642 p.config.useIECUnits) 643 } 644 645 p.Add(0) // re-render 646 } 647 648 // IsFinished returns true if progress bar is completed 649 func (p *ProgressBar) IsFinished() bool { 650 p.lock.Lock() 651 defer p.lock.Unlock() 652 653 return p.state.finished 654 } 655 656 // render renders the progress bar, updating the maximum 657 // rendered line width. this function is not thread-safe, 658 // so it must be called with an acquired lock. 659 func (p *ProgressBar) render() error { 660 // make sure that the rendering is not happening too quickly 661 // but always show if the currentNum reaches the max 662 if time.Since(p.state.lastShown).Nanoseconds() < p.config.throttleDuration.Nanoseconds() && 663 p.state.currentNum < p.config.max { 664 return nil 665 } 666 667 if !p.config.useANSICodes { 668 // first, clear the existing progress bar 669 err := clearProgressBar(p.config, p.state) 670 if err != nil { 671 return err 672 } 673 } 674 675 // check if the progress bar is finished 676 if !p.state.finished && p.state.currentNum >= p.config.max { 677 p.state.finished = true 678 if !p.config.clearOnFinish { 679 renderProgressBar(p.config, &p.state) 680 } 681 if p.config.onCompletion != nil { 682 p.config.onCompletion() 683 } 684 } 685 if p.state.finished { 686 // when using ANSI codes we don't pre-clean the current line 687 if p.config.useANSICodes && p.config.clearOnFinish { 688 err := clearProgressBar(p.config, p.state) 689 if err != nil { 690 return err 691 } 692 } 693 return nil 694 } 695 696 // then, re-render the current progress bar 697 w, err := renderProgressBar(p.config, &p.state) 698 if err != nil { 699 return err 700 } 701 702 if w > p.state.maxLineWidth { 703 p.state.maxLineWidth = w 704 } 705 706 p.state.lastShown = time.Now() 707 708 return nil 709 } 710 711 // State returns the current state 712 func (p *ProgressBar) State() State { 713 p.lock.Lock() 714 defer p.lock.Unlock() 715 s := State{} 716 s.CurrentNum = p.state.currentNum 717 s.Max = p.config.max 718 if p.config.ignoreLength { 719 s.Max = -1 720 } 721 s.CurrentPercent = float64(p.state.currentNum) / float64(p.config.max) 722 s.CurrentBytes = p.state.currentBytes 723 s.SecondsSince = time.Since(p.state.startTime).Seconds() 724 if p.state.currentNum > 0 { 725 s.SecondsLeft = s.SecondsSince / float64(p.state.currentNum) * (float64(p.config.max) - float64(p.state.currentNum)) 726 } 727 s.KBsPerSecond = float64(p.state.currentBytes) / 1024.0 / s.SecondsSince 728 s.Description = p.config.description 729 return s 730 } 731 732 // regex matching ansi escape codes 733 var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`) 734 735 func getStringWidth(c config, str string, colorize bool) int { 736 if c.colorCodes { 737 // convert any color codes in the progress bar into the respective ANSI codes 738 str = colorstring.Color(str) 739 } 740 741 // the width of the string, if printed to the console 742 // does not include the carriage return character 743 cleanString := strings.Replace(str, "\r", "", -1) 744 745 if c.colorCodes { 746 // the ANSI codes for the colors do not take up space in the console output, 747 // so they do not count towards the output string width 748 cleanString = ansiRegex.ReplaceAllString(cleanString, "") 749 } 750 751 // get the amount of runes in the string instead of the 752 // character count of the string, as some runes span multiple characters. 753 // see https://stackoverflow.com/a/12668840/2733724 754 stringWidth := uniseg.StringWidth(cleanString) 755 return stringWidth 756 } 757 758 func renderProgressBar(c config, s *state) (int, error) { 759 var sb strings.Builder 760 761 averageRate := average(s.counterLastTenRates) 762 if len(s.counterLastTenRates) == 0 || s.finished { 763 // if no average samples, or if finished, 764 // then average rate should be the total rate 765 if t := time.Since(s.startTime).Seconds(); t > 0 { 766 averageRate = s.currentBytes / t 767 } else { 768 averageRate = 0 769 } 770 } 771 772 // show iteration count in "current/total" iterations format 773 if c.showIterationsCount { 774 if sb.Len() == 0 { 775 sb.WriteString("(") 776 } else { 777 sb.WriteString(", ") 778 } 779 if !c.ignoreLength { 780 if c.showBytes { 781 currentHumanize, currentSuffix := humanizeBytes(s.currentBytes, c.useIECUnits) 782 if currentSuffix == c.maxHumanizedSuffix { 783 sb.WriteString(fmt.Sprintf("%s/%s%s", 784 currentHumanize, c.maxHumanized, c.maxHumanizedSuffix)) 785 } else { 786 sb.WriteString(fmt.Sprintf("%s%s/%s%s", 787 currentHumanize, currentSuffix, c.maxHumanized, c.maxHumanizedSuffix)) 788 } 789 } else { 790 sb.WriteString(fmt.Sprintf("%.0f/%d", s.currentBytes, c.max)) 791 } 792 } else { 793 if c.showBytes { 794 currentHumanize, currentSuffix := humanizeBytes(s.currentBytes, c.useIECUnits) 795 sb.WriteString(fmt.Sprintf("%s%s", currentHumanize, currentSuffix)) 796 } else { 797 sb.WriteString(fmt.Sprintf("%.0f/%s", s.currentBytes, "-")) 798 } 799 } 800 } 801 802 // show rolling average rate 803 if c.showBytes && averageRate > 0 && !math.IsInf(averageRate, 1) { 804 if sb.Len() == 0 { 805 sb.WriteString("(") 806 } else { 807 sb.WriteString(", ") 808 } 809 currentHumanize, currentSuffix := humanizeBytes(averageRate, c.useIECUnits) 810 sb.WriteString(fmt.Sprintf("%s%s/s", currentHumanize, currentSuffix)) 811 } 812 813 // show iterations rate 814 if c.showIterationsPerSecond { 815 if sb.Len() == 0 { 816 sb.WriteString("(") 817 } else { 818 sb.WriteString(", ") 819 } 820 if averageRate > 1 { 821 sb.WriteString(fmt.Sprintf("%0.0f %s/s", averageRate, c.iterationString)) 822 } else if averageRate*60 > 1 { 823 sb.WriteString(fmt.Sprintf("%0.0f %s/min", 60*averageRate, c.iterationString)) 824 } else { 825 sb.WriteString(fmt.Sprintf("%0.0f %s/hr", 3600*averageRate, c.iterationString)) 826 } 827 } 828 if sb.Len() > 0 { 829 sb.WriteString(")") 830 } 831 832 leftBrac, rightBrac, saucer, saucerHead := "", "", "", "" 833 834 // show time prediction in "current/total" seconds format 835 switch { 836 case c.predictTime: 837 rightBracNum := (time.Duration((1/averageRate)*(float64(c.max)-float64(s.currentNum))) * time.Second) 838 if rightBracNum.Seconds() < 0 { 839 rightBracNum = 0 * time.Second 840 } 841 rightBrac = rightBracNum.String() 842 fallthrough 843 case c.elapsedTime: 844 leftBrac = (time.Duration(time.Since(s.startTime).Seconds()) * time.Second).String() 845 } 846 847 if c.fullWidth && !c.ignoreLength { 848 width, err := termWidth() 849 if err != nil { 850 width = 80 851 } 852 853 amend := 1 // an extra space at eol 854 switch { 855 case leftBrac != "" && rightBrac != "": 856 amend = 4 // space, square brackets and colon 857 case leftBrac != "" && rightBrac == "": 858 amend = 4 // space and square brackets and another space 859 case leftBrac == "" && rightBrac != "": 860 amend = 3 // space and square brackets 861 } 862 if c.showDescriptionAtLineEnd { 863 amend += 1 // another space 864 } 865 866 c.width = width - getStringWidth(c, c.description, true) - 10 - amend - sb.Len() - len(leftBrac) - len(rightBrac) 867 s.currentSaucerSize = int(float64(s.currentPercent) / 100.0 * float64(c.width)) 868 } 869 if s.currentSaucerSize > 0 { 870 if c.ignoreLength { 871 saucer = strings.Repeat(c.theme.SaucerPadding, s.currentSaucerSize-1) 872 } else { 873 saucer = strings.Repeat(c.theme.Saucer, s.currentSaucerSize-1) 874 } 875 876 // Check if an alternate saucer head is set for animation 877 if c.theme.AltSaucerHead != "" && s.isAltSaucerHead { 878 saucerHead = c.theme.AltSaucerHead 879 s.isAltSaucerHead = false 880 } else if c.theme.SaucerHead == "" || s.currentSaucerSize == c.width { 881 // use the saucer for the saucer head if it hasn't been set 882 // to preserve backwards compatibility 883 saucerHead = c.theme.Saucer 884 } else { 885 saucerHead = c.theme.SaucerHead 886 s.isAltSaucerHead = true 887 } 888 } 889 890 /* 891 Progress Bar format 892 Description % |------ | (kb/s) (iteration count) (iteration rate) (predict time) 893 894 or if showDescriptionAtLineEnd is enabled 895 % |------ | (kb/s) (iteration count) (iteration rate) (predict time) Description 896 */ 897 898 repeatAmount := c.width - s.currentSaucerSize 899 if repeatAmount < 0 { 900 repeatAmount = 0 901 } 902 903 str := "" 904 905 if c.ignoreLength { 906 selectedSpinner := spinners[c.spinnerType] 907 if len(c.spinner) > 0 { 908 selectedSpinner = c.spinner 909 } 910 spinner := selectedSpinner[int(math.Round(math.Mod(float64(time.Since(s.startTime).Milliseconds()/100), float64(len(selectedSpinner)))))] 911 if c.elapsedTime { 912 if c.showDescriptionAtLineEnd { 913 str = fmt.Sprintf("\r%s %s [%s] %s ", 914 spinner, 915 sb.String(), 916 leftBrac, 917 c.description) 918 } else { 919 str = fmt.Sprintf("\r%s %s %s [%s] ", 920 spinner, 921 c.description, 922 sb.String(), 923 leftBrac) 924 } 925 } else { 926 if c.showDescriptionAtLineEnd { 927 str = fmt.Sprintf("\r%s %s %s ", 928 spinner, 929 sb.String(), 930 c.description) 931 } else { 932 str = fmt.Sprintf("\r%s %s %s ", 933 spinner, 934 c.description, 935 sb.String()) 936 } 937 } 938 } else if rightBrac == "" { 939 str = fmt.Sprintf("%4d%% %s%s%s%s%s %s", 940 s.currentPercent, 941 c.theme.BarStart, 942 saucer, 943 saucerHead, 944 strings.Repeat(c.theme.SaucerPadding, repeatAmount), 945 c.theme.BarEnd, 946 sb.String()) 947 948 if s.currentPercent == 100 && c.showElapsedTimeOnFinish { 949 str = fmt.Sprintf("%s [%s]", str, leftBrac) 950 } 951 952 if c.showDescriptionAtLineEnd { 953 str = fmt.Sprintf("\r%s %s ", str, c.description) 954 } else { 955 str = fmt.Sprintf("\r%s%s ", c.description, str) 956 } 957 } else { 958 if s.currentPercent == 100 { 959 str = fmt.Sprintf("%4d%% %s%s%s%s%s %s", 960 s.currentPercent, 961 c.theme.BarStart, 962 saucer, 963 saucerHead, 964 strings.Repeat(c.theme.SaucerPadding, repeatAmount), 965 c.theme.BarEnd, 966 sb.String()) 967 968 if c.showElapsedTimeOnFinish { 969 str = fmt.Sprintf("%s [%s]", str, leftBrac) 970 } 971 972 if c.showDescriptionAtLineEnd { 973 str = fmt.Sprintf("\r%s %s", str, c.description) 974 } else { 975 str = fmt.Sprintf("\r%s%s", c.description, str) 976 } 977 } else { 978 str = fmt.Sprintf("%4d%% %s%s%s%s%s %s [%s:%s]", 979 s.currentPercent, 980 c.theme.BarStart, 981 saucer, 982 saucerHead, 983 strings.Repeat(c.theme.SaucerPadding, repeatAmount), 984 c.theme.BarEnd, 985 sb.String(), 986 leftBrac, 987 rightBrac) 988 989 if c.showDescriptionAtLineEnd { 990 str = fmt.Sprintf("\r%s %s", str, c.description) 991 } else { 992 str = fmt.Sprintf("\r%s%s", c.description, str) 993 } 994 } 995 } 996 997 if c.colorCodes { 998 // convert any color codes in the progress bar into the respective ANSI codes 999 str = colorstring.Color(str) 1000 } 1001 1002 s.rendered = str 1003 1004 return getStringWidth(c, str, false), writeString(c, str) 1005 } 1006 1007 func clearProgressBar(c config, s state) error { 1008 if s.maxLineWidth == 0 { 1009 return nil 1010 } 1011 if c.useANSICodes { 1012 // write the "clear current line" ANSI escape sequence 1013 return writeString(c, "\033[2K\r") 1014 } 1015 // fill the empty content 1016 // to overwrite the progress bar and jump 1017 // back to the beginning of the line 1018 str := fmt.Sprintf("\r%s\r", strings.Repeat(" ", s.maxLineWidth)) 1019 return writeString(c, str) 1020 // the following does not show correctly if the previous line is longer than subsequent line 1021 // return writeString(c, "\r") 1022 } 1023 1024 func writeString(c config, str string) error { 1025 if _, err := io.WriteString(c.writer, str); err != nil { 1026 return err 1027 } 1028 1029 if f, ok := c.writer.(*os.File); ok { 1030 // ignore any errors in Sync(), as stdout 1031 // can't be synced on some operating systems 1032 // like Debian 9 (Stretch) 1033 f.Sync() 1034 } 1035 1036 return nil 1037 } 1038 1039 // Reader is the progressbar io.Reader struct 1040 type Reader struct { 1041 io.Reader 1042 bar *ProgressBar 1043 } 1044 1045 // NewReader return a new Reader with a given progress bar. 1046 func NewReader(r io.Reader, bar *ProgressBar) Reader { 1047 return Reader{ 1048 Reader: r, 1049 bar: bar, 1050 } 1051 } 1052 1053 // Read will read the data and add the number of bytes to the progressbar 1054 func (r *Reader) Read(p []byte) (n int, err error) { 1055 n, err = r.Reader.Read(p) 1056 r.bar.Add(n) 1057 return 1058 } 1059 1060 // Close the reader when it implements io.Closer 1061 func (r *Reader) Close() (err error) { 1062 if closer, ok := r.Reader.(io.Closer); ok { 1063 return closer.Close() 1064 } 1065 r.bar.Finish() 1066 return 1067 } 1068 1069 // Write implement io.Writer 1070 func (p *ProgressBar) Write(b []byte) (n int, err error) { 1071 n = len(b) 1072 p.Add(n) 1073 return 1074 } 1075 1076 // Read implement io.Reader 1077 func (p *ProgressBar) Read(b []byte) (n int, err error) { 1078 n = len(b) 1079 p.Add(n) 1080 return 1081 } 1082 1083 func (p *ProgressBar) Close() (err error) { 1084 p.Finish() 1085 return 1086 } 1087 1088 func average(xs []float64) float64 { 1089 total := 0.0 1090 for _, v := range xs { 1091 total += v 1092 } 1093 return total / float64(len(xs)) 1094 } 1095 1096 func humanizeBytes(s float64, iec bool) (string, string) { 1097 sizes := []string{" B", " kB", " MB", " GB", " TB", " PB", " EB"} 1098 base := 1000.0 1099 1100 if iec { 1101 sizes = []string{" B", " KiB", " MiB", " GiB", " TiB", " PiB", " EiB"} 1102 base = 1024.0 1103 } 1104 1105 if s < 10 { 1106 return fmt.Sprintf("%2.0f", s), sizes[0] 1107 } 1108 e := math.Floor(logn(float64(s), base)) 1109 suffix := sizes[int(e)] 1110 val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10 1111 f := "%.0f" 1112 if val < 10 { 1113 f = "%.1f" 1114 } 1115 1116 return fmt.Sprintf(f, val), suffix 1117 } 1118 1119 func logn(n, b float64) float64 { 1120 return math.Log(n) / math.Log(b) 1121 } 1122 1123 // termWidth function returns the visible width of the current terminal 1124 // and can be redefined for testing 1125 var termWidth = func() (width int, err error) { 1126 width, _, err = term.GetSize(int(os.Stdout.Fd())) 1127 if err == nil { 1128 return width, nil 1129 } 1130 1131 return 0, err 1132 }