github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/tui/model/model.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 model provides the program model for the guided remediation interactive tui. 16 package model 17 18 import ( 19 "os" 20 21 "github.com/charmbracelet/bubbles/help" 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/lockfile" 26 "github.com/google/osv-scalibr/guidedremediation/internal/manifest" 27 "github.com/google/osv-scalibr/guidedremediation/internal/remediation" 28 "github.com/google/osv-scalibr/guidedremediation/internal/tui/components" 29 "github.com/google/osv-scalibr/guidedremediation/options" 30 "github.com/google/osv-scalibr/guidedremediation/result" 31 "github.com/google/osv-scalibr/log" 32 "golang.org/x/term" 33 ) 34 35 // Model is a bubbletea Model for the guided remediation interactive tui. 36 type Model struct { 37 manifestRW manifest.ReadWriter 38 lockfileRW lockfile.ReadWriter 39 options options.FixVulnsOptions 40 41 lockfileGraph remediation.ResolvedGraph 42 lockfilePatches []result.Patch 43 44 relockBaseManifest *remediation.ResolvedManifest 45 relockBaseErrors []result.ResolveError 46 47 termWidth int // width of the whole terminal 48 termHeight int // height of the whole terminal 49 50 viewWidth int // width of each of the two view panel 51 viewHeight int // height of each of the two view panel 52 viewStyle lipgloss.Style // border style to render views 53 detailsRenderer components.DetailsRenderer // renderer for markdown details. 54 55 help help.Model // help text renderer 56 57 st modelState // current state of program 58 err error // set if a fatal error occurs within the program 59 writing bool // whether the model is currently shelling out writing lockfile/manifest file 60 } 61 62 // NewModel creates a new Model for the guided remediation interactive tui. 63 func NewModel(manifestRW manifest.ReadWriter, lockfileRW lockfile.ReadWriter, opts options.FixVulnsOptions, detailsRenderer components.DetailsRenderer) (Model, error) { 64 if detailsRenderer == nil { 65 detailsRenderer = components.FallbackDetailsRenderer{} 66 } 67 m := Model{ 68 manifestRW: manifestRW, 69 lockfileRW: lockfileRW, 70 options: opts, 71 st: newStateInitialize(), 72 help: help.New(), 73 viewStyle: lipgloss.NewStyle(). 74 BorderStyle(lipgloss.RoundedBorder()). 75 Padding(components.ViewVPad, components.ViewHPad), 76 detailsRenderer: detailsRenderer, 77 } 78 79 w, h, err := term.GetSize(int(os.Stdout.Fd())) 80 if err != nil { 81 log.Errorf("Failed to get terminal size: %v", err) 82 return Model{}, err 83 } 84 m = m.setTermSize(w, h) 85 86 return m, nil 87 } 88 89 func (m Model) setTermSize(w, h int) Model { 90 m.termWidth = w 91 m.termHeight = h 92 93 // The internal rendering space of the views occupy a percentage of the terminal width 94 viewWidth := max(int(float64(w)*components.ViewWidthPct), components.ViewMinWidth) 95 // The internal height is constant 96 viewHeight := components.ViewMinHeight 97 98 // The total width/height, including the whitespace padding and border characters on each side 99 paddedWidth := viewWidth + 2*components.ViewHPad + 2 100 paddedHeight := viewHeight + 2*components.ViewVPad + 2 101 102 // resize the views to the calculated dimensions 103 m.viewWidth = viewWidth 104 m.viewHeight = viewHeight 105 m.viewStyle = m.viewStyle.Width(paddedWidth).Height(paddedHeight) 106 107 m.st = m.st.ResizeInfo(m.viewWidth, m.viewHeight) 108 109 return m 110 } 111 112 func (m Model) getBorderStyles() (lipgloss.Style, lipgloss.Style) { 113 unfocused := m.viewStyle.BorderForeground(components.ColorDisabled) 114 if m.st.IsInfoFocused() { 115 return unfocused, m.viewStyle 116 } 117 return m.viewStyle, unfocused 118 } 119 120 func errorAndExit(m Model, err error) (tea.Model, tea.Cmd) { 121 m.err = err 122 return m, tea.Quit 123 } 124 125 // Init initializes the model. 126 func (m Model) Init() tea.Cmd { 127 return m.st.Init(m) 128 } 129 130 // Update updates the model. 131 func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 132 switch msg := msg.(type) { 133 case tea.KeyMsg: 134 switch { 135 case msg.Type == tea.KeyCtrlC: // always quit on ctrl+c 136 return m, tea.Quit 137 case key.Matches(msg, components.Keys.Help): // toggle help 138 m.help.ShowAll = !m.help.ShowAll 139 } 140 case tea.WindowSizeMsg: 141 m = m.setTermSize(msg.Width, msg.Height) 142 } 143 144 return m.st.Update(m, msg) 145 } 146 147 // View returns the view of the model. 148 func (m Model) View() string { 149 // render both views side-by-side 150 mainStyle, infoStyle := m.getBorderStyles() 151 mainView := mainStyle.Render(m.st.View(m)) 152 infoView := infoStyle.Render(m.st.InfoView()) 153 view := lipgloss.JoinHorizontal(lipgloss.Top, mainView, infoView) 154 155 // If we can't fit both side-by-side, only render the focused view 156 if lipgloss.Width(view) > m.termWidth { 157 if m.st.IsInfoFocused() { 158 view = infoView 159 } else { 160 view = mainView 161 } 162 } 163 164 // add the help to the bottom 165 view = lipgloss.JoinVertical(lipgloss.Center, view, m.help.View(components.Keys)) 166 167 return lipgloss.Place(m.termWidth, m.termHeight, lipgloss.Center, lipgloss.Center, view) 168 } 169 170 // Error returns the error of the model, if any. 171 func (m Model) Error() error { 172 return m.err 173 } 174 175 type modelState interface { 176 Init(m Model) tea.Cmd 177 Update(m Model, msg tea.Msg) (tea.Model, tea.Cmd) 178 View(m Model) string 179 Resize(w, h int) modelState 180 181 InfoView() string 182 ResizeInfo(w, h int) modelState 183 IsInfoFocused() bool 184 } 185 186 type writeMsg struct { 187 err error 188 }