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