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  }