github.com/jfrog/jfrog-cli-core/v2@v2.51.0/utils/progressbar/progressbarmng.go (about) 1 package progressbar 2 3 import ( 4 golangLog "log" 5 "math" 6 "os" 7 "strconv" 8 "strings" 9 "sync" 10 "time" 11 12 "github.com/gookit/color" 13 artifactoryutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" 14 "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" 15 corelog "github.com/jfrog/jfrog-cli-core/v2/utils/log" 16 "github.com/jfrog/jfrog-client-go/utils" 17 "github.com/jfrog/jfrog-client-go/utils/errorutils" 18 "github.com/jfrog/jfrog-client-go/utils/log" 19 "github.com/vbauerster/mpb/v7" 20 "github.com/vbauerster/mpb/v7/decor" 21 "golang.org/x/term" 22 ) 23 24 const ( 25 ProgressBarWidth = 20 26 longProgressBarWidth = 100 27 ProgressRefreshRate = 200 * time.Millisecond 28 ) 29 30 type Color int64 31 32 const ( 33 WHITE Color = iota 34 GREEN = 1 35 ) 36 37 var terminalWidth int 38 39 type ProgressBarMng struct { 40 // A container of all external mpb bar objects to be displayed. 41 container *mpb.Progress 42 // A synchronization lock object. 43 barsRWMutex sync.RWMutex 44 // A wait group for all progress bars. 45 barsWg *sync.WaitGroup 46 // The log file 47 logFile *os.File 48 } 49 50 func NewBarsMng() (mng *ProgressBarMng, shouldInit bool, err error) { 51 // Determine whether the progress bar should be displayed or not 52 shouldInit, err = ShouldInitProgressBar() 53 if !shouldInit || err != nil { 54 return 55 } 56 mng = &ProgressBarMng{} 57 // Init log file 58 mng.logFile, err = corelog.CreateLogFile() 59 if err != nil { 60 return 61 } 62 log.Info("Log path:", mng.logFile.Name()) 63 log.SetLogger(log.NewLoggerWithFlags(corelog.GetCliLogLevel(), mng.logFile, golangLog.Ldate|golangLog.Ltime|golangLog.Lmsgprefix)) 64 65 mng.barsWg = new(sync.WaitGroup) 66 mng.container = mpb.New( 67 mpb.WithOutput(os.Stderr), 68 mpb.WithWidth(longProgressBarWidth), 69 mpb.WithWaitGroup(mng.barsWg), 70 mpb.WithRefreshRate(ProgressRefreshRate)) 71 return 72 } 73 74 // Initializing a new Tasks with headline progress bar 75 // Initialize a progress bar that can show the status of two different values, and a headline above it 76 func (bm *ProgressBarMng) newDoubleHeadLineProgressBar(headline, val1HeadLine, val2HeadLine string, getVal func() (firstNumerator, firstDenominator, secondNumerator, secondDenominator *int64, err error)) *TasksWithHeadlineProg { 77 bm.barsWg.Add(1) 78 prog := TasksWithHeadlineProg{} 79 prog.headlineBar = bm.NewHeadlineBar(headline) 80 prog.tasksProgressBar = bm.newDoubleValueProgressBar(getVal, val1HeadLine, val2HeadLine) 81 prog.emptyLine = bm.NewHeadlineBar("") 82 83 return &prog 84 } 85 86 // Initialize a progress bar that can show the status of two different values 87 func (bm *ProgressBarMng) newDoubleValueProgressBar(getVal func() (firstNumerator, firstDenominator, secondNumerator, secondDenominator *int64, err error), firstValueLine, secondValueLine string) *TasksProgressBar { 88 pb := TasksProgressBar{} 89 windows := coreutils.IsWindows() 90 padding, filler := paddingAndFiller(windows) 91 pb.bar = bm.container.New(0, 92 mpb.BarStyle().Lbound("|").Tip(filler).Padding(padding).Filler(filler).Refiller("").Rbound("|"), 93 mpb.BarRemoveOnComplete(), 94 mpb.AppendDecorators( 95 decor.Name(" "+firstValueLine+": "), 96 decor.Any(func(statistics decor.Statistics) string { 97 firstNumerator, firstDenominator, _, _, err := getVal() 98 if err != nil { 99 log.Error(err) 100 } 101 s1 := artifactoryutils.ConvertIntToStorageSizeString(*firstNumerator) 102 s2 := artifactoryutils.ConvertIntToStorageSizeString(*firstDenominator) 103 return color.Green.Render(s1 + "/" + s2) 104 }), decor.Name(" "+secondValueLine+": "), decor.Any(func(statistics decor.Statistics) string { 105 _, _, secondNumerator, secondDenominator, err := getVal() 106 if err != nil { 107 log.Error(err) 108 } 109 s1 := strconv.Itoa(int(*secondNumerator)) 110 s2 := strconv.Itoa(int(*secondDenominator)) 111 return color.Green.Render(s1 + "/" + s2) 112 }), 113 ), 114 ) 115 return &pb 116 } 117 118 // Initialize a regular tasks progress bar, with a headline above it 119 func (bm *ProgressBarMng) newHeadlineTaskProgressBar(getVal func() (numerator, denominator *int64), headLine, valHeadLine string) *TasksWithHeadlineProg { 120 bm.barsWg.Add(1) 121 prog := TasksWithHeadlineProg{} 122 prog.headlineBar = bm.NewHeadlineBar(headLine) 123 prog.tasksProgressBar = bm.newTasksProgressBar(getVal, valHeadLine) 124 prog.emptyLine = bm.NewHeadlineBar("") 125 return &prog 126 } 127 128 // Initialize a regular tasks progress bar, with a headline above it 129 func (bm *ProgressBarMng) NewTasksWithHeadlineProgressBar(totalTasks int64, headline string, spinner bool, windows bool, taskType string) *TasksWithHeadlineProg { 130 bm.barsWg.Add(1) 131 prog := TasksWithHeadlineProg{} 132 if spinner { 133 prog.headlineBar = bm.NewHeadlineBarWithSpinner(headline) 134 } else { 135 prog.headlineBar = bm.NewHeadlineBar(headline) 136 } 137 // If totalTasks is 0 - phase is already finished in previous run. 138 if totalTasks == 0 { 139 prog.tasksProgressBar = bm.newDoneTasksProgressBar() 140 } else { 141 prog.tasksProgressBar = bm.NewTasksProgressBar(totalTasks, windows, taskType) 142 } 143 prog.emptyLine = bm.NewHeadlineBar("") 144 return &prog 145 } 146 147 func (bm *ProgressBarMng) QuitTasksWithHeadlineProgressBar(prog *TasksWithHeadlineProg) { 148 prog.headlineBar.Abort(true) 149 prog.headlineBar = nil 150 prog.tasksProgressBar.bar.Abort(true) 151 prog.tasksProgressBar = nil 152 prog.emptyLine.Abort(true) 153 prog.emptyLine = nil 154 bm.barsWg.Done() 155 } 156 157 // NewHeadlineBar Initializes a new progress bar for headline, with an optional spinner 158 func (bm *ProgressBarMng) NewHeadlineBarWithSpinner(msg string) *mpb.Bar { 159 return bm.container.New(1, 160 mpb.SpinnerStyle("∙∙∙∙∙∙", "●∙∙∙∙∙", "∙●∙∙∙∙", "∙∙●∙∙∙", "∙∙∙●∙∙", "∙∙∙∙●∙", "∙∙∙∙∙●", "∙∙∙∙∙∙").PositionLeft(), 161 mpb.BarRemoveOnComplete(), 162 mpb.PrependDecorators( 163 decor.Name(msg), 164 ), 165 ) 166 } 167 168 func (bm *ProgressBarMng) NewUpdatableHeadlineBarWithSpinner(updateFn func() string) *mpb.Bar { 169 return bm.container.New(1, 170 mpb.SpinnerStyle("∙∙∙∙∙∙", "●∙∙∙∙∙", "∙●∙∙∙∙", "∙∙●∙∙∙", "∙∙∙●∙∙", "∙∙∙∙●∙", "∙∙∙∙∙●", "∙∙∙∙∙∙").PositionLeft(), 171 mpb.BarRemoveOnComplete(), 172 mpb.PrependDecorators( 173 decor.Any(func(statistics decor.Statistics) string { 174 return updateFn() 175 }), 176 ), 177 ) 178 } 179 180 func (bm *ProgressBarMng) NewHeadlineBar(msg string) *mpb.Bar { 181 return bm.container.Add(1, 182 nil, 183 mpb.BarRemoveOnComplete(), 184 mpb.PrependDecorators( 185 decor.Name(msg), 186 ), 187 ) 188 } 189 190 // Increment increments completed tasks count by 1. 191 func (bm *ProgressBarMng) Increment(prog *TasksWithHeadlineProg) { 192 bm.barsRWMutex.RLock() 193 defer bm.barsRWMutex.RUnlock() 194 prog.tasksProgressBar.bar.Increment() 195 prog.tasksProgressBar.tasksCount++ 196 } 197 198 // Increment increments completed tasks count by n. 199 func (bm *ProgressBarMng) IncBy(n int, prog *TasksWithHeadlineProg) { 200 bm.barsRWMutex.RLock() 201 defer bm.barsRWMutex.RUnlock() 202 prog.tasksProgressBar.bar.IncrBy(n) 203 prog.tasksProgressBar.tasksCount += int64(n) 204 } 205 206 // DoneTask increase tasks counter to the number of totalTasks. 207 func (bm *ProgressBarMng) DoneTask(prog *TasksWithHeadlineProg) { 208 bm.barsRWMutex.RLock() 209 defer bm.barsRWMutex.RUnlock() 210 diff := prog.tasksProgressBar.total - prog.tasksProgressBar.tasksCount 211 // diff is int64, but we can increase the progress up to math.MaxInt in a time 212 for ; diff > math.MaxInt; diff -= math.MaxInt { 213 prog.tasksProgressBar.bar.IncrBy(math.MaxInt) 214 } 215 prog.tasksProgressBar.bar.IncrBy(int(diff)) 216 } 217 218 func (bm *ProgressBarMng) NewTasksProgressBar(totalTasks int64, windows bool, taskType string) *TasksProgressBar { 219 padding, filler := paddingAndFiller(windows) 220 pb := &TasksProgressBar{} 221 if taskType == "" { 222 taskType = "Tasks" 223 } 224 pb.bar = bm.container.New(0, 225 mpb.BarStyle().Lbound("|").Tip(filler).Padding(padding).Filler(filler).Refiller("").Rbound("|"), 226 mpb.BarRemoveOnComplete(), 227 mpb.AppendDecorators( 228 decor.Name(" "+taskType+": "), 229 decor.CountersNoUnit(getRenderedFormattedCounters("%d")), 230 ), 231 ) 232 pb.IncGeneralProgressTotalBy(totalTasks) 233 return pb 234 } 235 236 func (bm *ProgressBarMng) newTasksProgressBar(getVal func() (numerator, denominator *int64), headLine string) *TasksProgressBar { 237 padding, filler := paddingAndFiller(coreutils.IsWindows()) 238 pb := &TasksProgressBar{} 239 numerator, denominator := getVal() 240 pb.bar = bm.container.New(0, 241 mpb.BarStyle().Lbound("|").Tip(filler).Padding(padding).Filler(filler).Refiller("").Rbound("|"), 242 mpb.BarRemoveOnComplete(), 243 mpb.AppendDecorators( 244 decor.Name(" "+headLine+": "), 245 decor.Any(func(statistics decor.Statistics) string { 246 numeratorString := strconv.Itoa(int(*numerator)) 247 denominatorString := strconv.Itoa(int(*denominator)) 248 return color.Green.Render(numeratorString + "/" + denominatorString) 249 }), 250 ), 251 ) 252 return pb 253 } 254 255 // Initializing a counter progress bar 256 func (bm *ProgressBarMng) newCounterProgressBar(getVal func() (value int, err error), headLine string, counterDescription decor.Decorator) *TasksProgressBar { 257 pb := &TasksProgressBar{} 258 pb.bar = bm.container.Add(0, 259 nil, 260 mpb.BarRemoveOnComplete(), 261 mpb.PrependDecorators( 262 decor.Name(headLine), 263 decor.Any(func(decor.Statistics) string { 264 value, err := getVal() 265 if err != nil { 266 log.Error(err) 267 } 268 s1 := strconv.Itoa(value) 269 return color.Green.Render(s1) 270 }), 271 ), 272 mpb.AppendDecorators(counterDescription), 273 ) 274 return pb 275 } 276 277 // Initializing a progress bar that shows Done status 278 func (bm *ProgressBarMng) newDoneTasksProgressBar() *TasksProgressBar { 279 pb := &TasksProgressBar{} 280 pb.bar = bm.container.Add(1, 281 nil, 282 mpb.BarRemoveOnComplete(), 283 mpb.PrependDecorators( 284 decor.Name("Done ✅"), 285 ), 286 ) 287 return pb 288 } 289 290 func (bm *ProgressBarMng) NewStringProgressBar(headline string, updateFn func() string) *TasksProgressBar { 291 pb := &TasksProgressBar{} 292 pb.bar = bm.container.Add(1, 293 nil, 294 mpb.BarRemoveOnComplete(), 295 mpb.PrependDecorators( 296 decor.Name(headline), decor.Any(func(statistics decor.Statistics) string { 297 return updateFn() 298 }), 299 ), 300 ) 301 return pb 302 } 303 304 func getRenderedFormattedCounters(formatDirective string) string { 305 return color.Green.Render(strings.Join([]string{formatDirective, formatDirective}, "/")) 306 } 307 308 func (bm *ProgressBarMng) GetBarsWg() *sync.WaitGroup { 309 return bm.barsWg 310 } 311 312 func (bm *ProgressBarMng) GetLogFile() *os.File { 313 return bm.logFile 314 } 315 316 func paddingAndFiller(windows bool) (padding, filler string) { 317 padding = ".." 318 filler = "●" 319 if !windows { 320 padding = "⬛" 321 filler = "🟩" 322 } 323 return padding, filler 324 } 325 326 // The ShouldInitProgressBar func is used to determine whether the progress bar should be displayed. 327 // This default implementation will init the progress bar if the following conditions are met: 328 // CI == false (or unset) and Stderr is a terminal. 329 var ShouldInitProgressBar = func() (bool, error) { 330 ci, err := utils.GetBoolEnvValue(coreutils.CI, false) 331 if ci || err != nil { 332 return false, err 333 } 334 if !log.IsStdErrTerminal() { 335 return false, err 336 } 337 err = setTerminalWidth() 338 if err != nil { 339 return false, err 340 } 341 return true, nil 342 } 343 344 func setTerminalWidth() error { 345 width, _, err := term.GetSize(int(os.Stderr.Fd())) 346 if err != nil { 347 return errorutils.CheckError(err) 348 } 349 // -5 to avoid edges 350 terminalWidth = width - 5 351 if terminalWidth <= 0 { 352 terminalWidth = 5 353 } 354 return err 355 } 356 357 func GetTerminalWidth() int { 358 return terminalWidth 359 }