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