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