github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/runbits/runtime/progress/decor.go (about)

     1  package progress
     2  
     3  import (
     4  	"fmt"
     5  	"time"
     6  
     7  	"github.com/ActiveState/cli/internal/constants"
     8  	"github.com/ActiveState/cli/internal/errs"
     9  	"github.com/ActiveState/cli/internal/locale"
    10  	"github.com/ActiveState/cli/internal/logging"
    11  	"github.com/ActiveState/cli/internal/output"
    12  	"github.com/ActiveState/cli/internal/termutils"
    13  	"github.com/go-openapi/strfmt"
    14  	"github.com/vbauerster/mpb/v7"
    15  	"github.com/vbauerster/mpb/v7/decor"
    16  )
    17  
    18  const progressBarWidth = 40
    19  
    20  var refreshRate = constants.TerminalAnimationInterval
    21  
    22  type bar struct {
    23  	*mpb.Bar
    24  	started time.Time
    25  	total   int64
    26  }
    27  
    28  // Completed reports whether the bar has reached 100%. We have our own assertion prior to the mpb one as for whatever
    29  // reason mpb reports completed even when it isn't, and I've not been able to diagnose why.
    30  func (b *bar) Completed() bool {
    31  	if b.Bar.Current() < b.total {
    32  		return false
    33  	}
    34  
    35  	return b.Bar.Completed()
    36  }
    37  
    38  // trimName ensures that the name in a progress bar is not too wide for a terminal to display
    39  func (p *ProgressDigester) trimName(name string) string {
    40  	if len(name) > p.maxNameWidth {
    41  		return name[0:p.maxNameWidth]
    42  	}
    43  	return name
    44  }
    45  
    46  // addTotalBar adds a bar counting a number of sub-events adding up to total
    47  func (p *ProgressDigester) addTotalBar(name string, total int64, options ...mpb.BarOption) *bar {
    48  	logging.Debug("Adding total bar: %s", name)
    49  	return p.addBar(name, total, false, append(options, mpb.BarFillerClearOnComplete())...)
    50  }
    51  
    52  // addArtifactBar adds a bar counting the progress in a specific artifact setup step
    53  func (p *ProgressDigester) addArtifactBar(id strfmt.UUID, step step, total int64, countsBytes bool) error {
    54  	name := locale.T("artifact_unknown_name")
    55  	if aname, ok := p.artifacts[id]; ok {
    56  		name = aname
    57  	}
    58  	logging.Debug("Adding %s artifact bar: %s", step.verb, name)
    59  
    60  	aStep := artifactStep{id, step}
    61  	if _, ok := p.artifactBars[aStep.ID()]; ok {
    62  		return errs.New("Artifact bar already exists")
    63  	}
    64  	p.artifactBars[aStep.ID()] = p.addBar(fmt.Sprintf("  - %s %s", step.verb, name), total, countsBytes, mpb.BarRemoveOnComplete(), mpb.BarPriority(step.priority+len(p.artifactBars)))
    65  	return nil
    66  }
    67  
    68  // updateArtifactBar sets the current progress of an artifact bar
    69  func (p *ProgressDigester) updateArtifactBar(id strfmt.UUID, step step, inc int) error {
    70  	aStep := artifactStep{id, step}
    71  	if _, ok := p.artifactBars[aStep.ID()]; !ok {
    72  		return errs.New("%s Artifact bar doesn't exists", step.verb)
    73  	}
    74  	p.artifactBars[aStep.ID()].IncrBy(inc)
    75  
    76  	name := locale.T("artifact_unknown_name")
    77  	if aname, ok := p.artifacts[id]; ok {
    78  		name = aname
    79  	}
    80  	if p.artifactBars[aStep.ID()].Current() >= p.artifactBars[aStep.ID()].total {
    81  		logging.Debug("%s Artifact bar reached total: %s", step.verb, name)
    82  	}
    83  
    84  	return nil
    85  }
    86  
    87  // dropArtifactBar removes an artifact bar from the progress display
    88  func (p *ProgressDigester) dropArtifactBar(id strfmt.UUID, step step) error {
    89  	name := locale.T("artifact_unknown_name")
    90  	if aname, ok := p.artifacts[id]; ok {
    91  		name = aname
    92  	}
    93  	logging.Debug("Dropping %s artifact bar: %s", step.verb, name)
    94  
    95  	aStep := artifactStep{id, step}
    96  	if _, ok := p.artifactBars[aStep.ID()]; !ok {
    97  		return errs.New("Artifact bar doesn't exists")
    98  	}
    99  	p.artifactBars[aStep.ID()].Abort(true)
   100  	return nil
   101  }
   102  
   103  func (p *ProgressDigester) addBar(name string, total int64, countsBytes bool, options ...mpb.BarOption) *bar {
   104  	name = p.trimName(name)
   105  	prependDecorators := []decor.Decorator{
   106  		decor.Name(name, decor.WC{W: p.maxNameWidth, C: decor.DidentRight}),
   107  		decor.OnComplete(
   108  			decor.Spinner(output.SpinnerFrames, decor.WCSyncSpace), "",
   109  		),
   110  	}
   111  	if countsBytes {
   112  		prependDecorators = append(prependDecorators, decor.CountersKiloByte("%.1f/%.1f", decor.WC{W: 17}))
   113  	} else {
   114  		prependDecorators = append(prependDecorators, decor.CountersNoUnit("%d/%d", decor.WC{W: 17}))
   115  	}
   116  	options = append(options,
   117  		mpb.BarFillerClearOnComplete(),
   118  		mpb.PrependDecorators(prependDecorators...),
   119  		mpb.AppendDecorators(
   120  			decor.OnComplete(decor.Percentage(decor.WC{W: 5}), ""),
   121  		),
   122  	)
   123  
   124  	return &bar{p.mainProgress.AddBar(total, options...), time.Now(), total}
   125  }
   126  
   127  // MaxNameWidth returns the maximum width to be used for a name in a progress bar
   128  func MaxNameWidth() int {
   129  	tw := termutils.GetWidth()
   130  
   131  	// calculate the maximum width for a name displayed to the left of the progress bar
   132  	maxWidth := tw - progressBarWidth - 24 // 40 is the size for the progressbar, 24 is taken by counters (up to 999.9) and percentage display
   133  	if maxWidth < 0 {
   134  		maxWidth = 4
   135  	}
   136  	// limit to 30 characters such that text is not too far away from progress bars
   137  	if maxWidth > 30 {
   138  		return 30
   139  	}
   140  	return maxWidth
   141  }