github.com/wolfi-dev/wolfictl@v0.16.11/pkg/cli/components/interview/interview.go (about) 1 package interview 2 3 import ( 4 "errors" 5 "fmt" 6 "strings" 7 8 tea "github.com/charmbracelet/bubbletea" 9 "github.com/wolfi-dev/wolfictl/pkg/cli/components/accept" 10 "github.com/wolfi-dev/wolfictl/pkg/cli/components/picker" 11 "github.com/wolfi-dev/wolfictl/pkg/cli/components/textinput" 12 "github.com/wolfi-dev/wolfictl/pkg/cli/internal/wrapped" 13 "github.com/wolfi-dev/wolfictl/pkg/question" 14 ) 15 16 type Model[T any] struct { 17 root question.Question[T] 18 stack []question.Question[T] 19 answerComponentStack []tea.Model 20 state T 21 done bool 22 err error 23 } 24 25 // New returns a new interview model. 26 func New[T any](root question.Question[T], initialState T) (Model[T], error) { 27 m := Model[T]{ 28 root: root, 29 state: initialState, 30 } 31 32 m.stack = append(m.stack, root) 33 34 ac, err := newAnswerComponent(root) 35 if err != nil { 36 return m, fmt.Errorf("creating answer component: %w", err) 37 } 38 m.answerComponentStack = append(m.answerComponentStack, ac) 39 40 return m, nil 41 } 42 43 func newAnswerComponent[T any](q question.Question[T]) (tea.Model, error) { 44 switch a := q.Answer.(type) { 45 case question.MultipleChoice[T]: 46 opts := picker.Options[question.Choice[T]]{ 47 Items: a, 48 ItemRenderFunc: renderChoice[T], 49 } 50 return picker.New(opts), nil 51 52 case question.AcceptText[T]: 53 return textinput.New(), nil 54 55 case question.MessageOnly[T]: 56 return accept.Model{}, nil 57 } 58 59 // This should never happen. 60 return nil, fmt.Errorf("unsupported question answer type %T for question %q", q.Answer, q.Text) 61 } 62 63 func (m Model[T]) stackTopIndex() int { 64 return len(m.stack) - 1 65 } 66 67 func (m Model[T]) answerComponentStackTop() tea.Model { 68 return m.answerComponentStack[m.stackTopIndex()] 69 } 70 71 func (m Model[T]) Init() tea.Cmd { 72 return m.answerComponentStackTop().Init() 73 } 74 75 func (m Model[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 76 // Most events should be routed to the component for the current question. 77 78 switch m.answerComponentStackTop().(type) { 79 case picker.Model[question.Choice[T]]: 80 return m.updateForPicker(msg) 81 82 case textinput.Model: 83 return m.updateForTextInput(msg) 84 85 case accept.Model: 86 return m.updateForMessage(msg) 87 } 88 89 return m, nil 90 } 91 92 func (m Model[T]) updateForMessage(msg tea.Msg) (tea.Model, tea.Cmd) { 93 messageTea, cmd := m.answerComponentStackTop().Update(msg) 94 messageModel, ok := messageTea.(accept.Model) 95 if !ok { 96 // Nothing we can do here, but this shouldn't ever happen. 97 return m, nil 98 } 99 m.answerComponentStack[m.stackTopIndex()] = messageModel 100 101 if !messageModel.Accepted { 102 // The user hasn't acknowledged the message yet. 103 return m, cmd 104 } 105 106 // The user has acknowledged the message. 107 108 // Update the state. 109 var nextQuestion *question.Question[T] 110 var err error 111 m.state, nextQuestion, err = m.stack[m.stackTopIndex()].Answer.(question.MessageOnly[T])(m.state) 112 if err != nil { 113 if errors.Is(err, question.ErrTerminate) { 114 // Exit the interview without a resulting state. 115 m.done = true 116 m.err = err 117 return m, tea.Quit 118 } 119 120 // An error occurred. 121 m.err = err 122 return m, tea.Quit 123 } 124 125 if nextQuestion == nil { 126 // The line of questioning is concluded. 127 m.done = true 128 return m, tea.Quit 129 } 130 131 // The line of questioning continues. 132 // Update the stack. 133 m.stack = append(m.stack, *nextQuestion) 134 135 // Create a new answer component for the next question. 136 ac, err := newAnswerComponent(*nextQuestion) 137 if err != nil { 138 // An error occurred. 139 m.err = err 140 return m, tea.Quit 141 } 142 m.answerComponentStack = append(m.answerComponentStack, ac) 143 144 return m, ac.Init() 145 } 146 147 func (m Model[T]) updateForTextInput(msg tea.Msg) (tea.Model, tea.Cmd) { 148 switch msg := msg.(type) { 149 case tea.KeyMsg: 150 switch msg.String() { 151 case "enter": 152 // The user has submitted a text answer. 153 // Update the state. 154 val := m.answerComponentStackTop().(textinput.Model).Inner.Value() 155 156 // Update the state. 157 var nextQuestion *question.Question[T] 158 var err error 159 m.state, nextQuestion, err = m.stack[m.stackTopIndex()].Answer.(question.AcceptText[T])(m.state, val) 160 if err != nil { 161 if errors.Is(err, question.ErrTerminate) { 162 // Exit the interview without a resulting state. 163 m.done = true 164 m.err = err 165 return m, tea.Quit 166 } 167 168 // An error occurred. 169 m.err = err 170 return m, tea.Quit 171 } 172 if nextQuestion == nil { 173 // The line of questioning is concluded. 174 m.done = true 175 return m, tea.Quit 176 } 177 178 // The line of questioning continues. 179 // Update the stack. 180 m.stack = append(m.stack, *nextQuestion) 181 182 // Create a new answer component for the next question. 183 ac, err := newAnswerComponent(*nextQuestion) 184 if err != nil { 185 // An error occurred. 186 m.err = err 187 return m, tea.Quit 188 } 189 m.answerComponentStack = append(m.answerComponentStack, ac) 190 191 return m, ac.Init() 192 193 default: 194 return m.routeMsgToTextInputAtTopOfStack(msg) 195 } 196 197 default: 198 return m.routeMsgToTextInputAtTopOfStack(msg) 199 } 200 } 201 202 func (m Model[T]) routeMsgToTextInputAtTopOfStack(msg tea.Msg) (tea.Model, tea.Cmd) { 203 // Pass the message to the text input. 204 tiTea, cmd := m.answerComponentStackTop().Update(msg) 205 ti, ok := tiTea.(textinput.Model) 206 if !ok { 207 // Nothing we can do here, but this shouldn't ever happen. 208 return m, nil 209 } 210 211 m.answerComponentStack[m.stackTopIndex()] = ti 212 return m, cmd 213 } 214 215 func (m Model[T]) updateForPicker(msg tea.Msg) (tea.Model, tea.Cmd) { 216 switch msg := msg.(type) { 217 case tea.KeyMsg: 218 switch msg.String() { 219 case "enter", "up", "down": 220 pTea, cmd := m.answerComponentStackTop().Update(msg) 221 p, ok := pTea.(picker.Model[question.Choice[T]]) 222 if !ok { 223 // Nothing we can do here, but this shouldn't ever happen. 224 return m, nil 225 } 226 m.answerComponentStack[m.stackTopIndex()] = p 227 228 // Check if the user has submitted an answer. 229 230 c := p.Picked() 231 if c == nil { 232 // The user hasn't picked an answer yet. 233 return m, cmd 234 } 235 236 // The user has picked an answer. 237 238 if c.Choose == nil { 239 return m, nil 240 } 241 242 // Update the state. 243 var nextQuestion *question.Question[T] 244 var err error 245 m.state, nextQuestion, err = c.Choose(m.state) 246 if err != nil { 247 if errors.Is(err, question.ErrTerminate) { 248 // Exit the interview without a resulting state. 249 m.done = true 250 m.err = err 251 return m, tea.Quit 252 } 253 254 // An error occurred. 255 m.err = err 256 return m, tea.Quit 257 } 258 if nextQuestion == nil { 259 // The line of questioning is concluded. 260 m.done = true 261 return m, tea.Quit 262 } 263 264 // The line of questioning continues. 265 // Update the stack. 266 m.stack = append(m.stack, *nextQuestion) 267 268 // Create a new answer component for the next question. 269 ac, err := newAnswerComponent(*nextQuestion) 270 if err != nil { 271 // An error occurred. 272 m.err = err 273 return m, tea.Quit 274 } 275 m.answerComponentStack = append(m.answerComponentStack, ac) 276 277 return m, ac.Init() 278 } 279 280 default: 281 // Pass the message to the picker. 282 pTea, cmd := m.answerComponentStackTop().Update(msg) 283 p, ok := pTea.(picker.Model[question.Choice[T]]) 284 if !ok { 285 // Nothing we can do here, but this shouldn't ever happen. 286 return m, nil 287 } 288 m.answerComponentStack[m.stackTopIndex()] = p 289 return m, cmd 290 } 291 292 return m, nil 293 } 294 295 func (m Model[T]) View() string { 296 // TODO: consider rendering differently if m.done is true. 297 298 sb := strings.Builder{} 299 300 for i, q := range m.stack { 301 qText := wrapped.Sprint(q.Text) 302 sb.WriteString(qText + "\n\n") 303 304 acView := m.answerComponentStack[i].View() 305 sb.WriteString(acView) 306 } 307 308 return sb.String() 309 } 310 311 func (m Model[T]) State() (T, error) { 312 return m.state, m.err 313 } 314 315 func renderChoice[T any](c question.Choice[T]) string { 316 return c.Text 317 }