go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/cli/progress/progress.go (about) 1 // Copyright (c) Mondoo, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package progress 5 6 import ( 7 "fmt" 8 "os" 9 "strings" 10 "sync" 11 "time" 12 13 tea "github.com/charmbracelet/bubbletea" 14 "github.com/mattn/go-isatty" 15 "github.com/muesli/termenv" 16 "go.mondoo.com/cnquery/logger" 17 "go.mondoo.com/cnquery/utils/multierr" 18 ) 19 20 type Progress interface { 21 Open() error 22 OnProgress(current int, total int) 23 Score(score string) 24 Errored() 25 NotApplicable() 26 Completed() 27 Close() 28 } 29 30 type Noop struct{} 31 32 func (n Noop) Open() error { return nil } 33 func (n Noop) OnProgress(int, int) {} 34 func (n Noop) Score(score string) {} 35 func (n Noop) Errored() {} 36 func (n Noop) NotApplicable() {} 37 func (n Noop) Completed() {} 38 func (n Noop) Close() {} 39 40 type progressbar struct { 41 id string 42 maxNameWidth int 43 padding int 44 Data progressData 45 lock sync.Mutex 46 bar *renderer 47 isTTY bool 48 wg sync.WaitGroup 49 } 50 51 type progressData struct { 52 Names []string 53 Completion []float32 54 complete bool 55 } 56 57 func New(id string, name string) *progressbar { 58 return NewMultiBar(id, progressData{ 59 Names: []string{name}, 60 Completion: []float32{0}, 61 complete: false, 62 }) 63 } 64 65 func NewMultiBar(id string, data progressData) *progressbar { 66 maxNameWidth := 0 67 for _, v := range data.Names { 68 l := len(v) 69 if l > maxNameWidth { 70 maxNameWidth = l 71 } 72 } 73 74 return &progressbar{ 75 id: id, 76 maxNameWidth: maxNameWidth, 77 Data: data, 78 isTTY: isatty.IsTerminal(os.Stdout.Fd()), 79 } 80 } 81 82 func (p *progressbar) Errored() {} 83 func (p *progressbar) NotApplicable() {} 84 func (p *progressbar) Score(string) {} 85 func (p *progressbar) Completed() {} 86 87 func (p *progressbar) Open() error { 88 var err error 89 p.bar, err = newRenderer() 90 if err != nil { 91 return multierr.Wrap(err, "failed to initialize progressbar renderer") 92 } 93 94 p.wg.Add(1) 95 if p.isTTY { 96 go func() { 97 defer p.wg.Done() 98 (logger.LogOutputWriter.(*logger.BufferedWriter)).Pause() 99 defer (logger.LogOutputWriter.(*logger.BufferedWriter)).Resume() 100 if _, err := tea.NewProgram(p).Run(); err != nil { 101 fmt.Println(err.Error()) 102 panic(err) 103 } 104 }() 105 } else { 106 go func() { 107 defer p.wg.Done() 108 o := termenv.NewOutput(os.Stdout) 109 for { 110 time.Sleep(time.Second / progressPipedFps) 111 o.ClearLines(2) 112 o.WriteString(p.View()) 113 p.lock.Lock() 114 complete := p.Data.complete 115 p.lock.Unlock() 116 if complete { 117 break 118 } 119 } 120 }() 121 } 122 123 return nil 124 } 125 126 func (p *progressbar) OnProgress(current int, total int) { 127 p.lock.Lock() 128 p.Data.Completion[0] = float32(current) / float32(total) 129 p.lock.Unlock() 130 } 131 132 func (p *progressbar) Close() { 133 p.lock.Lock() 134 p.Data.complete = true 135 p.lock.Unlock() 136 p.wg.Wait() 137 } 138 139 const ( 140 progressDefaultFps = 60 141 progressDefaultWidth = 80 142 progressPipedFps = 1 143 ) 144 145 type tickMsg time.Time 146 147 // Init is a required interface method for the underlying renderer 148 func (p *progressbar) Init() tea.Cmd { 149 return tickCmd() 150 } 151 152 // Update is a required interface method for the underlying renderer 153 func (p *progressbar) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 154 switch msg := msg.(type) { 155 case tea.KeyMsg: 156 switch msg.String() { 157 case "q", "ctrl+c": 158 return p, tea.Quit 159 default: 160 return p, nil 161 } 162 163 case tea.WindowSizeMsg: 164 p.bar.Width = msg.Width - p.padding*2 - 4 - p.maxNameWidth 165 if p.bar.Width > progressDefaultWidth { 166 p.bar.Width = progressDefaultWidth 167 } 168 return p, nil 169 170 case tickMsg: 171 p.lock.Lock() 172 complete := p.Data.complete 173 p.lock.Unlock() 174 if complete { 175 return p, tea.Quit 176 } 177 return p, tickCmd() 178 179 default: 180 return p, nil 181 } 182 } 183 184 // View is a required interface method for the underlying renderer 185 func (p *progressbar) View() string { 186 pad := strings.Repeat(" ", p.padding) 187 out := "" 188 for i := range p.Data.Names { 189 name := p.Data.Names[i] 190 value := p.Data.Completion[i] 191 out += "\n" + pad + p.bar.View(value) + " " + name 192 } 193 194 out += "\n" 195 return out 196 } 197 198 func tickCmd() tea.Cmd { 199 return tea.Tick(time.Second/progressDefaultFps, func(t time.Time) tea.Msg { 200 return tickMsg(t) 201 }) 202 }