github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/tui/components/relock_info.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package components 16 17 import ( 18 "fmt" 19 "slices" 20 "strings" 21 22 "github.com/charmbracelet/bubbles/key" 23 tea "github.com/charmbracelet/bubbletea" 24 "github.com/charmbracelet/lipgloss" 25 "github.com/google/osv-scalibr/guidedremediation/internal/resolution" 26 "github.com/google/osv-scalibr/guidedremediation/result" 27 ) 28 29 // relockInfo is a ViewModel showing the dependency changes, the removed, and added vulnerabilities 30 // resulting from a proposed relock patch 31 type relockInfo struct { 32 fixedHeight float64 33 fixedList *vulnList 34 addedList *vulnList 35 addedFocused bool 36 } 37 38 // NewRelockInfo creates a ViewModel showing the dependency changes, the removed, and added vulnerabilities 39 // resulting from a proposed relock patch. 40 // allVulns must contain all vulnerabilities, both before and after patch application. 41 func NewRelockInfo(patch result.Patch, allVulns []resolution.Vulnerability, detailsRenderer DetailsRenderer) ViewModel { 42 info := relockInfo{fixedHeight: 1} 43 preamble := strings.Builder{} 44 preamble.WriteString("The following upgrades:\n") 45 for _, pkg := range patch.PackageUpdates { 46 preamble.WriteString(fmt.Sprintf(" %s@%s → @%s\n", pkg.Name, pkg.VersionFrom, pkg.VersionTo)) 47 } 48 preamble.WriteString("Will resolve the following:") 49 fixedVulns := make([]resolution.Vulnerability, 0, len(patch.Fixed)) 50 for _, fixed := range patch.Fixed { 51 idx := slices.IndexFunc(allVulns, func(v resolution.Vulnerability) bool { return v.OSV.Id == fixed.ID }) 52 if idx >= 0 { 53 fixedVulns = append(fixedVulns, allVulns[idx]) 54 } 55 // else, something went wrong, just ignore this. 56 } 57 info.fixedList = NewVulnList(fixedVulns, preamble.String(), detailsRenderer).(*vulnList) 58 59 if len(patch.Introduced) == 0 { 60 return &info 61 } 62 63 // Create a second list showing introduced vulns 64 newVulns := make([]resolution.Vulnerability, 0, len(patch.Introduced)) 65 for i := range patch.Introduced { 66 idx := slices.IndexFunc(allVulns, func(v resolution.Vulnerability) bool { return v.OSV.Id == patch.Introduced[i].ID }) 67 if idx >= 0 { 68 newVulns = append(newVulns, allVulns[idx]) 69 } 70 // else, something went wrong, just ignore this. 71 } 72 info.addedList = NewVulnList(newVulns, "But will introduce the following new vulns:", detailsRenderer).(*vulnList) 73 info.addedList.Blur() 74 75 // divide two lists by roughly how many lines each would have 76 const fixedMinHeight = 0.5 77 const fixedMaxHeight = 0.8 78 fixed := float64(len(patch.PackageUpdates) + len(fixedVulns)) 79 added := float64(len(newVulns)) 80 info.fixedHeight = fixed / (fixed + added) 81 if info.fixedHeight < fixedMinHeight { 82 info.fixedHeight = fixedMinHeight 83 } 84 if info.fixedHeight > fixedMaxHeight { 85 info.fixedHeight = fixedMaxHeight 86 } 87 88 return &info 89 } 90 91 func (r *relockInfo) Resize(w, h int) ViewModel { 92 fixedHeight := int(r.fixedHeight * float64(h)) 93 r.fixedList = r.fixedList.Resize(w, fixedHeight).(*vulnList) 94 if r.addedList != nil { 95 r.addedList = r.addedList.Resize(w, h-fixedHeight).(*vulnList) 96 } 97 98 return r 99 } 100 101 func (r *relockInfo) Update(msg tea.Msg) (ViewModel, tea.Cmd) { 102 var cmds []tea.Cmd 103 104 // check if we're trying to scroll past the end of one of the lists 105 if msg, ok := msg.(tea.KeyMsg); ok && r.addedList != nil { 106 // scrolling up out of the added list 107 if r.addedFocused && 108 r.addedList.Index() == 0 && 109 key.Matches(msg, Keys.Up) { 110 r.addedFocused = false 111 r.addedList.Blur() 112 r.fixedList.Focus() 113 114 return r, nil 115 } 116 // scrolling down out of fixed list 117 if !r.addedFocused && 118 r.fixedList.Index() == len(r.fixedList.Items())-1 && 119 key.Matches(msg, Keys.Down) { 120 r.addedFocused = true 121 r.addedList.Focus() 122 r.fixedList.Blur() 123 124 return r, nil 125 } 126 } 127 128 // do normal updates 129 l, cmd := r.fixedList.Update(msg) 130 r.fixedList = l.(*vulnList) 131 cmds = append(cmds, cmd) 132 133 if r.addedList != nil { 134 l, cmd := r.addedList.Update(msg) 135 r.addedList = l.(*vulnList) 136 cmds = append(cmds, cmd) 137 } 138 139 return r, tea.Batch(cmds...) 140 } 141 142 func (r *relockInfo) View() string { 143 if r.addedList == nil || r.fixedList.currVulnInfo != nil { 144 return r.fixedList.View() 145 } 146 if r.addedList.currVulnInfo != nil { 147 return r.addedList.View() 148 } 149 150 return lipgloss.JoinVertical(lipgloss.Center, r.fixedList.View(), r.addedList.View()) 151 }