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/pkg/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
 19	// XXX: we use a mutex to support concurrent access to the model. This is
 20	// needed to implement pagination for the Log component. list.Model does
 21	// not support item pagination so we hack it ourselves on top of
 22	// list.Model.
 23	mtx sync.RWMutex
 24}
 25
 26// IdentifiableItem is an item that can be identified by a string. Implements
 27// list.DefaultItem.
 28type IdentifiableItem interface {
 29	list.DefaultItem
 30	ID() string
 31}
 32
 33// ItemDelegate is a wrapper around list.ItemDelegate.
 34type ItemDelegate interface {
 35	list.ItemDelegate
 36}
 37
 38// SelectMsg is a message that is sent when an item is selected.
 39type SelectMsg struct{ IdentifiableItem }
 40
 41// ActiveMsg is a message that is sent when an item is active but not selected.
 42type ActiveMsg struct{ IdentifiableItem }
 43
 44// New creates a new selector.
 45func New(common common.Common, items []IdentifiableItem, delegate ItemDelegate) *Selector {
 46	itms := make([]list.Item, len(items))
 47	for i, item := range items {
 48		itms[i] = item
 49	}
 50	l := list.New(itms, delegate, common.Width, common.Height)
 51	l.Styles.NoItems = common.Styles.NoContent
 52	s := &Selector{
 53		Model:  &l,
 54		common: common,
 55	}
 56	s.SetSize(common.Width, common.Height)
 57	return s
 58}
 59
 60// PerPage returns the number of items per page.
 61func (s *Selector) PerPage() int {
 62	s.mtx.RLock()
 63	defer s.mtx.RUnlock()
 64	return s.Model.Paginator.PerPage
 65}
 66
 67// SetPage sets the current page.
 68func (s *Selector) SetPage(page int) {
 69	s.mtx.Lock()
 70	defer s.mtx.Unlock()
 71	s.Model.Paginator.Page = page
 72}
 73
 74// Page returns the current page.
 75func (s *Selector) Page() int {
 76	s.mtx.RLock()
 77	defer s.mtx.RUnlock()
 78	return s.Model.Paginator.Page
 79}
 80
 81// TotalPages returns the total number of pages.
 82func (s *Selector) TotalPages() int {
 83	s.mtx.RLock()
 84	defer s.mtx.RUnlock()
 85	return s.Model.Paginator.TotalPages
 86}
 87
 88// SetTotalPages sets the total number of pages given the number of items.
 89func (s *Selector) SetTotalPages(items int) int {
 90	s.mtx.Lock()
 91	defer s.mtx.Unlock()
 92	return s.Model.Paginator.SetTotalPages(items)
 93}
 94
 95// SelectedItem returns the currently selected item.
 96func (s *Selector) SelectedItem() IdentifiableItem {
 97	s.mtx.RLock()
 98	defer s.mtx.RUnlock()
 99	item := s.Model.SelectedItem()
100	i, ok := item.(IdentifiableItem)
101	if !ok {
102		return nil
103	}
104	return i
105}
106
107// Select selects the item at the given index.
108func (s *Selector) Select(index int) {
109	s.mtx.RLock()
110	defer s.mtx.RUnlock()
111	s.Model.Select(index)
112}
113
114// SetShowTitle sets the show title flag.
115func (s *Selector) SetShowTitle(show bool) {
116	s.mtx.Lock()
117	defer s.mtx.Unlock()
118	s.Model.SetShowTitle(show)
119}
120
121// SetShowHelp sets the show help flag.
122func (s *Selector) SetShowHelp(show bool) {
123	s.mtx.Lock()
124	defer s.mtx.Unlock()
125	s.Model.SetShowHelp(show)
126}
127
128// SetShowStatusBar sets the show status bar flag.
129func (s *Selector) SetShowStatusBar(show bool) {
130	s.mtx.Lock()
131	defer s.mtx.Unlock()
132	s.Model.SetShowStatusBar(show)
133}
134
135// DisableQuitKeybindings disables the quit keybindings.
136func (s *Selector) DisableQuitKeybindings() {
137	s.mtx.Lock()
138	defer s.mtx.Unlock()
139	s.Model.DisableQuitKeybindings()
140}
141
142// SetShowFilter sets the show filter flag.
143func (s *Selector) SetShowFilter(show bool) {
144	s.mtx.Lock()
145	defer s.mtx.Unlock()
146	s.Model.SetShowFilter(show)
147}
148
149// SetShowPagination sets the show pagination flag.
150func (s *Selector) SetShowPagination(show bool) {
151	s.mtx.Lock()
152	defer s.mtx.Unlock()
153	s.Model.SetShowPagination(show)
154}
155
156// SetFilteringEnabled sets the filtering enabled flag.
157func (s *Selector) SetFilteringEnabled(enabled bool) {
158	s.mtx.Lock()
159	defer s.mtx.Unlock()
160	s.Model.SetFilteringEnabled(enabled)
161}
162
163// SetSize implements common.Component.
164func (s *Selector) SetSize(width, height int) {
165	s.mtx.Lock()
166	defer s.mtx.Unlock()
167	s.common.SetSize(width, height)
168	s.Model.SetSize(width, height)
169}
170
171// SetItems sets the items in the selector.
172func (s *Selector) SetItems(items []IdentifiableItem) tea.Cmd {
173	its := make([]list.Item, len(items))
174	for i, item := range items {
175		its[i] = item
176	}
177	s.mtx.Lock()
178	defer s.mtx.Unlock()
179	return s.Model.SetItems(its)
180}
181
182// Index returns the index of the selected item.
183func (s *Selector) Index() int {
184	s.mtx.RLock()
185	defer s.mtx.RUnlock()
186	return s.Model.Index()
187}
188
189// Items returns the items in the selector.
190func (s *Selector) Items() []list.Item {
191	s.mtx.RLock()
192	defer s.mtx.RUnlock()
193	return s.Model.Items()
194}
195
196// VisibleItems returns all the visible items in the selector.
197func (s *Selector) VisibleItems() []list.Item {
198	s.mtx.RLock()
199	defer s.mtx.RUnlock()
200	return s.Model.VisibleItems()
201}
202
203// FilterState returns the filter state.
204func (s *Selector) FilterState() list.FilterState {
205	s.mtx.RLock()
206	defer s.mtx.RUnlock()
207	return s.Model.FilterState()
208}
209
210// CursorUp moves the cursor up.
211func (s *Selector) CursorUp() {
212	s.mtx.Lock()
213	defer s.mtx.Unlock()
214	s.Model.CursorUp()
215}
216
217// CursorDown moves the cursor down.
218func (s *Selector) CursorDown() {
219	s.mtx.Lock()
220	defer s.mtx.Unlock()
221	s.Model.CursorDown()
222}
223
224// Init implements tea.Model.
225func (s *Selector) Init() tea.Cmd {
226	return s.activeCmd
227}
228
229// Update implements tea.Model.
230func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
231	cmds := make([]tea.Cmd, 0)
232	switch msg := msg.(type) {
233	case tea.MouseMsg:
234		switch msg.Type {
235		case tea.MouseWheelUp:
236			s.CursorUp()
237		case tea.MouseWheelDown:
238			s.CursorDown()
239		case tea.MouseLeft:
240			curIdx := s.Index()
241			for i, item := range s.Items() {
242				item, _ := item.(IdentifiableItem)
243				// Check each item to see if it's in bounds.
244				if item != nil && s.common.Zone.Get(item.ID()).InBounds(msg) {
245					if i == curIdx {
246						cmds = append(cmds, s.SelectItemCmd)
247					} else {
248						s.Select(i)
249					}
250					break
251				}
252			}
253		}
254	case tea.KeyMsg:
255		filterState := s.FilterState()
256		switch {
257		case key.Matches(msg, s.common.KeyMap.Help):
258			if filterState == list.Filtering {
259				return s, tea.Batch(cmds...)
260			}
261		case key.Matches(msg, s.common.KeyMap.Select):
262			if filterState != list.Filtering {
263				cmds = append(cmds, s.SelectItemCmd)
264			}
265		}
266	case list.FilterMatchesMsg:
267		cmds = append(cmds, s.activeFilterCmd)
268	}
269	m, cmd := s.Model.Update(msg)
270	s.mtx.Lock()
271	s.Model = &m
272	s.mtx.Unlock()
273	if cmd != nil {
274		cmds = append(cmds, cmd)
275	}
276	// Track filter state and update active item when filter state changes.
277	filterState := s.FilterState()
278	if s.filterState != filterState {
279		cmds = append(cmds, s.activeFilterCmd)
280	}
281	s.filterState = filterState
282	// Send ActiveMsg when index change.
283	if s.active != s.Index() {
284		cmds = append(cmds, s.activeCmd)
285	}
286	s.active = s.Index()
287	return s, tea.Batch(cmds...)
288}
289
290// View implements tea.Model.
291func (s *Selector) View() string {
292	return s.Model.View()
293}
294
295// SelectItemCmd is a command that selects the currently active item.
296func (s *Selector) SelectItemCmd() tea.Msg {
297	return SelectMsg{s.SelectedItem()}
298}
299
300func (s *Selector) activeCmd() tea.Msg {
301	item := s.SelectedItem()
302	return ActiveMsg{item}
303}
304
305func (s *Selector) activeFilterCmd() tea.Msg {
306	// Here we use VisibleItems because when list.FilterMatchesMsg is sent,
307	// VisibleItems is the only way to get the list of filtered items. The list
308	// bubble should export something like list.FilterMatchesMsg.Items().
309	items := s.VisibleItems()
310	if len(items) == 0 {
311		return nil
312	}
313	item := items[0]
314	i, ok := item.(IdentifiableItem)
315	if !ok {
316		return nil
317	}
318	return ActiveMsg{i}
319}