github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/cmd/syft/cli/ui/handle_cataloger_task.go (about) 1 package ui 2 3 import ( 4 tea "github.com/charmbracelet/bubbletea" 5 "github.com/charmbracelet/lipgloss" 6 "github.com/google/uuid" 7 "github.com/wagoodman/go-partybus" 8 "github.com/wagoodman/go-progress" 9 10 "github.com/anchore/bubbly/bubbles/taskprogress" 11 "github.com/anchore/bubbly/bubbles/tree" 12 "github.com/anchore/syft/internal/log" 13 "github.com/anchore/syft/syft/event/monitor" 14 syftEventParsers "github.com/anchore/syft/syft/event/parsers" 15 ) 16 17 // we standardize how rows are instantiated to ensure consistency in the appearance across the UI 18 type taskModelFactory func(title taskprogress.Title, opts ...taskprogress.Option) taskprogress.Model 19 20 var _ tea.Model = (*catalogerTaskModel)(nil) 21 22 type catalogerTaskModel struct { 23 model tree.Model 24 modelFactory taskModelFactory 25 } 26 27 func newCatalogerTaskTreeModel(f taskModelFactory) *catalogerTaskModel { 28 t := tree.NewModel() 29 t.Padding = " " 30 t.RootsWithoutPrefix = true 31 return &catalogerTaskModel{ 32 modelFactory: f, 33 model: t, 34 } 35 } 36 37 type newCatalogerTaskRowEvent struct { 38 info monitor.GenericTask 39 prog progress.StagedProgressable 40 } 41 42 func (cts catalogerTaskModel) Init() tea.Cmd { 43 return cts.model.Init() 44 } 45 46 func (cts catalogerTaskModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 47 event, ok := msg.(newCatalogerTaskRowEvent) 48 if !ok { 49 model, cmd := cts.model.Update(msg) 50 cts.model = model.(tree.Model) 51 52 return cts, cmd 53 } 54 55 info, prog := event.info, event.prog 56 57 tsk := cts.modelFactory( 58 taskprogress.Title{ 59 Default: info.Title.Default, 60 Running: info.Title.WhileRunning, 61 Success: info.Title.OnSuccess, 62 }, 63 taskprogress.WithStagedProgressable(prog), 64 ) 65 66 if info.Context != "" { 67 tsk.Context = []string{info.Context} 68 } 69 70 tsk.HideOnSuccess = info.HideOnSuccess 71 tsk.HideStageOnSuccess = info.HideStageOnSuccess 72 tsk.HideProgressOnSuccess = true 73 74 if info.ParentID != "" { 75 tsk.TitleStyle = lipgloss.NewStyle() 76 } 77 78 if err := cts.model.Add(info.ParentID, info.ID, tsk); err != nil { 79 log.WithFields("error", err).Error("unable to add cataloger task to tree model") 80 } 81 82 return cts, tsk.Init() 83 } 84 85 func (cts catalogerTaskModel) View() string { 86 return cts.model.View() 87 } 88 89 func (m *Handler) handleCatalogerTaskStarted(e partybus.Event) ([]tea.Model, tea.Cmd) { 90 mon, info, err := syftEventParsers.ParseCatalogerTaskStarted(e) 91 if err != nil { 92 log.WithFields("error", err).Warn("unable to parse event") 93 return nil, nil 94 } 95 96 var models []tea.Model 97 98 // only create the new cataloger task tree once to manage all cataloger task events 99 m.onNewCatalogerTask.Do(func() { 100 models = append(models, newCatalogerTaskTreeModel(m.newTaskProgress)) 101 }) 102 103 // we need to update the cataloger task model with a new row. We should never update the model outside of the 104 // bubbletea update-render event loop. Instead, we return a command that will be executed by the bubbletea runtime, 105 // producing a message that is passed to the cataloger task model. This is the prescribed way to update models 106 // in bubbletea. 107 108 if info.ID == "" { 109 // ID is optional from the consumer perspective, but required internally 110 info.ID = uuid.Must(uuid.NewRandom()).String() 111 } 112 113 cmd := func() tea.Msg { 114 // this message will cause the cataloger task model to add a new row to the output based on the given task 115 // information and progress data. 116 return newCatalogerTaskRowEvent{ 117 info: *info, 118 prog: mon, 119 } 120 } 121 122 return models, cmd 123 }