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