1package selector
2
3import (
4 "github.com/charmbracelet/bubbles/key"
5 "github.com/charmbracelet/bubbles/list"
6 tea "github.com/charmbracelet/bubbletea"
7 "github.com/charmbracelet/soft-serve/ui/common"
8)
9
10// Selector is a list of items that can be selected.
11type Selector struct {
12 list.Model
13 common common.Common
14 active int
15 filterState list.FilterState
16}
17
18// IdentifiableItem is an item that can be identified by a string. Implements
19// list.DefaultItem.
20type IdentifiableItem interface {
21 list.DefaultItem
22 ID() string
23}
24
25// ItemDelegate is a wrapper around list.ItemDelegate.
26type ItemDelegate interface {
27 list.ItemDelegate
28}
29
30// SelectMsg is a message that is sent when an item is selected.
31type SelectMsg struct{ IdentifiableItem }
32
33// ActiveMsg is a message that is sent when an item is active but not selected.
34type ActiveMsg struct{ IdentifiableItem }
35
36// New creates a new selector.
37func New(common common.Common, items []IdentifiableItem, delegate ItemDelegate) *Selector {
38 itms := make([]list.Item, len(items))
39 for i, item := range items {
40 itms[i] = item
41 }
42 l := list.New(itms, delegate, common.Width, common.Height)
43 s := &Selector{
44 Model: l,
45 common: common,
46 }
47 s.SetSize(common.Width, common.Height)
48 return s
49}
50
51// PerPage returns the number of items per page.
52func (s *Selector) PerPage() int {
53 return s.Model.Paginator.PerPage
54}
55
56// SetPage sets the current page.
57func (s *Selector) SetPage(page int) {
58 s.Model.Paginator.Page = page
59}
60
61// Page returns the current page.
62func (s *Selector) Page() int {
63 return s.Model.Paginator.Page
64}
65
66// TotalPages returns the total number of pages.
67func (s *Selector) TotalPages() int {
68 return s.Model.Paginator.TotalPages
69}
70
71// Select selects the item at the given index.
72func (s *Selector) Select(index int) {
73 s.Model.Select(index)
74}
75
76// SetShowTitle sets the show title flag.
77func (s *Selector) SetShowTitle(show bool) {
78 s.Model.SetShowTitle(show)
79}
80
81// SetShowHelp sets the show help flag.
82func (s *Selector) SetShowHelp(show bool) {
83 s.Model.SetShowHelp(show)
84}
85
86// SetShowStatusBar sets the show status bar flag.
87func (s *Selector) SetShowStatusBar(show bool) {
88 s.Model.SetShowStatusBar(show)
89}
90
91// DisableQuitKeybindings disables the quit keybindings.
92func (s *Selector) DisableQuitKeybindings() {
93 s.Model.DisableQuitKeybindings()
94}
95
96// SetShowFilter sets the show filter flag.
97func (s *Selector) SetShowFilter(show bool) {
98 s.Model.SetShowFilter(show)
99}
100
101// SetShowPagination sets the show pagination flag.
102func (s *Selector) SetShowPagination(show bool) {
103 s.Model.SetShowPagination(show)
104}
105
106// SetFilteringEnabled sets the filtering enabled flag.
107func (s *Selector) SetFilteringEnabled(enabled bool) {
108 s.Model.SetFilteringEnabled(enabled)
109}
110
111// SetSize implements common.Component.
112func (s *Selector) SetSize(width, height int) {
113 s.common.SetSize(width, height)
114 s.Model.SetSize(width, height)
115}
116
117// SetItems sets the items in the selector.
118func (s *Selector) SetItems(items []IdentifiableItem) tea.Cmd {
119 its := make([]list.Item, len(items))
120 for i, item := range items {
121 its[i] = item
122 }
123 return s.Model.SetItems(its)
124}
125
126// Index returns the index of the selected item.
127func (s *Selector) Index() int {
128 return s.Model.Index()
129}
130
131// Init implements tea.Model.
132func (s *Selector) Init() tea.Cmd {
133 return s.activeCmd
134}
135
136// Update implements tea.Model.
137func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
138 cmds := make([]tea.Cmd, 0)
139 switch msg := msg.(type) {
140 case tea.MouseMsg:
141 switch msg.Type {
142 case tea.MouseWheelUp:
143 s.Model.CursorUp()
144 case tea.MouseWheelDown:
145 s.Model.CursorDown()
146 case tea.MouseLeft:
147 curIdx := s.Model.Index()
148 for i, item := range s.Model.Items() {
149 item, _ := item.(IdentifiableItem)
150 // Check each item to see if it's in bounds.
151 if item != nil && s.common.Zone.Get(item.ID()).InBounds(msg) {
152 if i == curIdx {
153 cmds = append(cmds, s.selectCmd)
154 } else {
155 s.Model.Select(i)
156 }
157 break
158 }
159 }
160 }
161 case tea.KeyMsg:
162 filterState := s.Model.FilterState()
163 switch {
164 case key.Matches(msg, s.common.KeyMap.Help):
165 if filterState == list.Filtering {
166 return s, tea.Batch(cmds...)
167 }
168 case key.Matches(msg, s.common.KeyMap.Select):
169 if filterState != list.Filtering {
170 cmds = append(cmds, s.selectCmd)
171 }
172 }
173 case list.FilterMatchesMsg:
174 cmds = append(cmds, s.activeFilterCmd)
175 }
176 m, cmd := s.Model.Update(msg)
177 s.Model = m
178 if cmd != nil {
179 cmds = append(cmds, cmd)
180 }
181 // Track filter state and update active item when filter state changes.
182 filterState := s.Model.FilterState()
183 if s.filterState != filterState {
184 cmds = append(cmds, s.activeFilterCmd)
185 }
186 s.filterState = filterState
187 // Send ActiveMsg when index change.
188 if s.active != s.Model.Index() {
189 cmds = append(cmds, s.activeCmd)
190 }
191 s.active = s.Model.Index()
192 return s, tea.Batch(cmds...)
193}
194
195// View implements tea.Model.
196func (s *Selector) View() string {
197 return s.Model.View()
198}
199
200// SelectItem is a command that selects the currently active item.
201func (s *Selector) SelectItem() tea.Msg {
202 return s.selectCmd()
203}
204
205func (s *Selector) selectCmd() tea.Msg {
206 item := s.Model.SelectedItem()
207 i, ok := item.(IdentifiableItem)
208 if !ok {
209 return SelectMsg{}
210 }
211 return SelectMsg{i}
212}
213
214func (s *Selector) activeCmd() tea.Msg {
215 item := s.Model.SelectedItem()
216 i, ok := item.(IdentifiableItem)
217 if !ok {
218 return ActiveMsg{}
219 }
220 return ActiveMsg{i}
221}
222
223func (s *Selector) activeFilterCmd() tea.Msg {
224 // Here we use VisibleItems because when list.FilterMatchesMsg is sent,
225 // VisibleItems is the only way to get the list of filtered items. The list
226 // bubble should export something like list.FilterMatchesMsg.Items().
227 items := s.Model.VisibleItems()
228 if len(items) == 0 {
229 return nil
230 }
231 item := items[0]
232 i, ok := item.(IdentifiableItem)
233 if !ok {
234 return nil
235 }
236 return ActiveMsg{i}
237}