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		}
147	case tea.KeyMsg:
148		filterState := s.Model.FilterState()
149		switch {
150		case key.Matches(msg, s.common.KeyMap.Help):
151			if filterState == list.Filtering {
152				return s, tea.Batch(cmds...)
153			}
154		case key.Matches(msg, s.common.KeyMap.Select):
155			if filterState != list.Filtering {
156				cmds = append(cmds, s.selectCmd)
157			}
158		}
159	case list.FilterMatchesMsg:
160		cmds = append(cmds, s.activeFilterCmd)
161	}
162	m, cmd := s.Model.Update(msg)
163	s.Model = m
164	if cmd != nil {
165		cmds = append(cmds, cmd)
166	}
167	// Track filter state and update active item when filter state changes.
168	filterState := s.Model.FilterState()
169	if s.filterState != filterState {
170		cmds = append(cmds, s.activeFilterCmd)
171	}
172	s.filterState = filterState
173	// Send ActiveMsg when index change.
174	if s.active != s.Model.Index() {
175		cmds = append(cmds, s.activeCmd)
176	}
177	s.active = s.Model.Index()
178	return s, tea.Batch(cmds...)
179}
180
181// View implements tea.Model.
182func (s *Selector) View() string {
183	return s.Model.View()
184}
185
186// SelectItem is a command that selects the currently active item.
187func (s *Selector) SelectItem() tea.Msg {
188	return s.selectCmd()
189}
190
191func (s *Selector) selectCmd() tea.Msg {
192	item := s.Model.SelectedItem()
193	i, ok := item.(IdentifiableItem)
194	if !ok {
195		return SelectMsg{}
196	}
197	return SelectMsg{i}
198}
199
200func (s *Selector) activeCmd() tea.Msg {
201	item := s.Model.SelectedItem()
202	i, ok := item.(IdentifiableItem)
203	if !ok {
204		return ActiveMsg{}
205	}
206	return ActiveMsg{i}
207}
208
209func (s *Selector) activeFilterCmd() tea.Msg {
210	// Here we use VisibleItems because when list.FilterMatchesMsg is sent,
211	// VisibleItems is the only way to get the list of filtered items. The list
212	// bubble should export something like list.FilterMatchesMsg.Items().
213	items := s.Model.VisibleItems()
214	if len(items) == 0 {
215		return nil
216	}
217	item := items[0]
218	i, ok := item.(IdentifiableItem)
219	if !ok {
220		return nil
221	}
222	return ActiveMsg{i}
223}