github.com/wolfi-dev/wolfictl@v0.16.11/pkg/cli/components/picker/picker.go (about) 1 package picker 2 3 import ( 4 "fmt" 5 "strings" 6 7 tea "github.com/charmbracelet/bubbletea" 8 "github.com/wolfi-dev/wolfictl/pkg/cli/components/breather" 9 "github.com/wolfi-dev/wolfictl/pkg/cli/components/ctrlcwrapper" 10 "github.com/wolfi-dev/wolfictl/pkg/cli/styles" 11 ) 12 13 type Model[T any] struct { 14 items []T 15 itemRendererFunc func(T) string 16 customActions []CustomAction[T] 17 selected int 18 picked bool 19 aboutToExit bool 20 breather breather.Model 21 messageForZeroItems string 22 23 // Error is the error that occurred during the picker's lifecycle, if any. 24 Error error 25 } 26 27 type Options[T any] struct { 28 // Items is the list of items to display, from which the user can pick. 29 Items []T 30 31 // MessageForZeroItems is the message to display when there are no items to 32 // list. 33 MessageForZeroItems string 34 35 // ItemRenderFunc is the function to use to render each item in the list. If 36 // nil, the item will be rendered via fmt.Sprintf("%v", item). 37 ItemRenderFunc func(T) string 38 39 // CustomActions is a list of custom actions that the user can take. 40 CustomActions []CustomAction[T] 41 } 42 43 // New returns a new picker model. The render function is used to render each 44 // item in the list. 45 func New[T any](opts Options[T]) Model[T] { 46 return Model[T]{ 47 items: opts.Items, 48 itemRendererFunc: opts.ItemRenderFunc, 49 customActions: opts.CustomActions, 50 messageForZeroItems: opts.MessageForZeroItems, 51 breather: breather.New(">"), 52 } 53 } 54 55 type CustomAction[T any] struct { 56 // Key is the key that the user should press to select this action. 57 Key string 58 59 // Description is a short description of what this action does, used in help 60 // text. 61 // 62 // For example, in the help text "o to open.", "to open" is the description. 63 Description string 64 65 // Do is the function to call when the user presses the key for this action. 66 Do func(selected T) tea.Cmd 67 } 68 69 // ErrCmd returns a command that will cause the picker to display an error 70 // message and exit. 71 func ErrCmd(err error) tea.Cmd { 72 return func() tea.Msg { 73 return ErrMsg(err) 74 } 75 } 76 77 // ErrMsg is a tea.Msg that indicates an error occurred during the picker's 78 // lifecycle. 79 type ErrMsg error 80 81 func (m Model[T]) Init() tea.Cmd { 82 return m.breather.Init() 83 } 84 85 func (m Model[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 86 switch msg := msg.(type) { 87 case ctrlcwrapper.AboutToExitMsg: 88 m.aboutToExit = true 89 return m, ctrlcwrapper.InnerIsReady 90 91 case ErrMsg: 92 m.Error = msg 93 return m, tea.Quit 94 95 case tea.KeyMsg: 96 switch msg.String() { 97 case "up": 98 if m.selected > 0 { 99 m.selected-- 100 } 101 102 case "down": 103 if m.selected < len(m.items)-1 { 104 m.selected++ 105 } 106 107 case "enter": 108 m.picked = true 109 m.aboutToExit = true 110 return m, tea.Quit 111 } 112 113 for _, action := range m.customActions { 114 if msg.String() == action.Key { 115 if action.Do == nil { 116 continue 117 } 118 119 var selected T 120 if len(m.items) > 0 { 121 selected = m.items[m.selected] 122 } 123 return m, action.Do(selected) 124 } 125 } 126 127 case breather.TickMsg: 128 var cmd tea.Cmd 129 m.breather, cmd = m.breather.Update(msg) 130 return m, cmd 131 } 132 133 return m, nil 134 } 135 136 func (m Model[T]) View() string { 137 sb := new(strings.Builder) 138 139 if len(m.items) == 0 { 140 sb.WriteString(m.messageForZeroItems + "\n") 141 } 142 143 var breatherView string 144 if m.aboutToExit { 145 // Don't animate, picking and/or exiting has happened. 146 breatherView = m.breather.ViewStatic() 147 } else { 148 breatherView = m.breather.View() 149 } 150 151 for i, item := range m.items { 152 if i == m.selected { 153 sb.WriteString(fmt.Sprintf("%s %s\n", breatherView, m.itemRendererFunc(item))) 154 continue 155 } 156 157 if !m.aboutToExit { 158 sb.WriteString(fmt.Sprintf(" %s\n", m.itemRendererFunc(item))) 159 } 160 } 161 162 sb.WriteString("\n") 163 164 if !m.aboutToExit { 165 if count := len(m.items); count > 0 { 166 if count > 1 { 167 sb.WriteString(helpChangeSelection + " ") 168 } 169 170 sb.WriteString(helpConfirm + "\n") 171 } 172 173 for _, action := range m.customActions { 174 sb.WriteString(fmt.Sprintf( 175 "%s %s\n", 176 styleHelpKey.Render(action.Key), 177 styleHelpExplanation.Render(action.Description+"."), 178 )) 179 } 180 } 181 182 return sb.String() 183 } 184 185 // Picked returns the item that was picked by the user, if any. If no item has 186 // been picked, this returns nil. 187 func (m Model[T]) Picked() *T { 188 if !m.picked { 189 // The user hasn't picked anything yet. 190 return nil 191 } 192 193 picked := m.items[m.selected] 194 return &picked 195 } 196 197 var ( 198 helpChangeSelection = fmt.Sprintf( 199 "%s %s", 200 styleHelpKey.Render("↑/↓"), 201 styleHelpExplanation.Render("to change selection."), 202 ) 203 204 helpConfirm = fmt.Sprintf( 205 "%s %s", 206 styleHelpKey.Render("enter"), 207 styleHelpExplanation.Render("to confirm."), 208 ) 209 ) 210 211 var ( 212 styleHelpKey = styles.FaintAccent().Copy().Bold(true) 213 styleHelpExplanation = styles.Faint().Copy() 214 )