selector.go

  1package selector
  2
  3import (
  4	"sync"
  5
  6	"github.com/charmbracelet/bubbles/v2/key"
  7	"github.com/charmbracelet/bubbles/v2/list"
  8	tea "github.com/charmbracelet/bubbletea/v2"
  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.MouseClickMsg:
234		m := msg.Mouse()
235		switch m.Button {
236		case tea.MouseWheelUp:
237			s.CursorUp()
238		case tea.MouseWheelDown:
239			s.CursorDown()
240		case tea.MouseLeft:
241			curIdx := s.Index()
242			for i, item := range s.Items() {
243				item, _ := item.(IdentifiableItem)
244				// Check each item to see if it's in bounds.
245				if item != nil && s.common.Zone.Get(item.ID()).InBounds(msg) {
246					if i == curIdx {
247						cmds = append(cmds, s.SelectItemCmd)
248					} else {
249						s.Select(i)
250					}
251					break
252				}
253			}
254		}
255	case tea.KeyMsg:
256		filterState := s.FilterState()
257		switch {
258		case key.Matches(msg, s.common.KeyMap.Help):
259			if filterState == list.Filtering {
260				return s, tea.Batch(cmds...)
261			}
262		case key.Matches(msg, s.common.KeyMap.Select):
263			if filterState != list.Filtering {
264				cmds = append(cmds, s.SelectItemCmd)
265			}
266		}
267	case list.FilterMatchesMsg:
268		cmds = append(cmds, s.activeFilterCmd)
269	}
270	m, cmd := s.Model.Update(msg)
271	s.mtx.Lock()
272	s.Model = &m
273	s.mtx.Unlock()
274	if cmd != nil {
275		cmds = append(cmds, cmd)
276	}
277	// Track filter state and update active item when filter state changes.
278	filterState := s.FilterState()
279	if s.filterState != filterState {
280		cmds = append(cmds, s.activeFilterCmd)
281	}
282	s.filterState = filterState
283	// Send ActiveMsg when index change.
284	if s.active != s.Index() {
285		cmds = append(cmds, s.activeCmd)
286	}
287	s.active = s.Index()
288	return s, tea.Batch(cmds...)
289}
290
291// View implements tea.Model.
292func (s *Selector) View() string {
293	return s.Model.View()
294}
295
296// SelectItemCmd is a command that selects the currently active item.
297func (s *Selector) SelectItemCmd() tea.Msg {
298	return SelectMsg{s.SelectedItem()}
299}
300
301func (s *Selector) activeCmd() tea.Msg {
302	item := s.SelectedItem()
303	return ActiveMsg{item}
304}
305
306func (s *Selector) activeFilterCmd() tea.Msg {
307	// Here we use VisibleItems because when list.FilterMatchesMsg is sent,
308	// VisibleItems is the only way to get the list of filtered items. The list
309	// bubble should export something like list.FilterMatchesMsg.Items().
310	items := s.VisibleItems()
311	if len(items) == 0 {
312		return nil
313	}
314	item := items[0]
315	i, ok := item.(IdentifiableItem)
316	if !ok {
317		return nil
318	}
319	return ActiveMsg{i}
320}