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