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	KeyMap      list.KeyMap
 13	list        list.Model
 14	common      common.Common
 15	active      int
 16	filterState list.FilterState
 17}
 18
 19// IdentifiableItem is an item that can be identified by a string. Implements list.DefaultItem.
 20type IdentifiableItem interface {
 21	list.DefaultItem
 22	ID() string
 23}
 24
 25// SelectMsg is a message that is sent when an item is selected.
 26type SelectMsg string
 27
 28// ActiveMsg is a message that is sent when an item is active but not selected.
 29type ActiveMsg string
 30
 31// New creates a new selector.
 32func New(common common.Common, items []IdentifiableItem, delegate list.ItemDelegate) *Selector {
 33	itms := make([]list.Item, len(items))
 34	for i, item := range items {
 35		itms[i] = item
 36	}
 37	l := list.New(itms, delegate, common.Width, common.Height)
 38	s := &Selector{
 39		list:   l,
 40		common: common,
 41	}
 42	s.KeyMap = list.DefaultKeyMap()
 43	s.SetSize(common.Width, common.Height)
 44	return s
 45}
 46
 47// SetShowTitle sets the show title flag.
 48func (s *Selector) SetShowTitle(show bool) {
 49	s.list.SetShowTitle(show)
 50}
 51
 52// SetShowHelp sets the show help flag.
 53func (s *Selector) SetShowHelp(show bool) {
 54	s.list.SetShowHelp(show)
 55}
 56
 57// SetShowStatusBar sets the show status bar flag.
 58func (s *Selector) SetShowStatusBar(show bool) {
 59	s.list.SetShowStatusBar(show)
 60}
 61
 62// DisableQuitKeybindings disables the quit keybindings.
 63func (s *Selector) DisableQuitKeybindings() {
 64	s.list.DisableQuitKeybindings()
 65}
 66
 67// SetShowFilter sets the show filter flag.
 68func (s *Selector) SetShowFilter(show bool) {
 69	s.list.SetShowFilter(show)
 70}
 71
 72// SetShowPagination sets the show pagination flag.
 73func (s *Selector) SetShowPagination(show bool) {
 74	s.list.SetShowPagination(show)
 75}
 76
 77// SetFilteringEnabled sets the filtering enabled flag.
 78func (s *Selector) SetFilteringEnabled(enabled bool) {
 79	s.list.SetFilteringEnabled(enabled)
 80}
 81
 82// SetSize implements common.Component.
 83func (s *Selector) SetSize(width, height int) {
 84	s.common.SetSize(width, height)
 85	s.list.SetSize(width, height)
 86}
 87
 88// SetItems sets the items in the selector.
 89func (s *Selector) SetItems(items []list.Item) tea.Cmd {
 90	return s.list.SetItems(items)
 91}
 92
 93// Index returns the index of the selected item.
 94func (s *Selector) Index() int {
 95	return s.list.Index()
 96}
 97
 98// Init implements tea.Model.
 99func (s *Selector) Init() tea.Cmd {
100	return s.activeCmd
101}
102
103// Update implements tea.Model.
104func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
105	cmds := make([]tea.Cmd, 0)
106	switch msg := msg.(type) {
107	case tea.MouseMsg:
108		switch msg.Type {
109		case tea.MouseWheelUp:
110			s.list.CursorUp()
111		case tea.MouseWheelDown:
112			s.list.CursorDown()
113		}
114	case tea.KeyMsg:
115		switch {
116		case key.Matches(msg, s.common.Keymap.Select):
117			cmds = append(cmds, s.selectCmd)
118		}
119	case list.FilterMatchesMsg:
120		cmds = append(cmds, s.activeFilterCmd)
121	}
122	m, cmd := s.list.Update(msg)
123	s.list = m
124	if cmd != nil {
125		cmds = append(cmds, cmd)
126	}
127	// Track filter state and update active item when filter state changes.
128	filterState := s.list.FilterState()
129	if s.filterState != filterState {
130		cmds = append(cmds, s.activeFilterCmd)
131	}
132	s.filterState = filterState
133	// Send ActiveMsg when index change.
134	if s.active != s.list.Index() {
135		cmds = append(cmds, s.activeCmd)
136	}
137	s.active = s.list.Index()
138	return s, tea.Batch(cmds...)
139}
140
141// View implements tea.Model.
142func (s *Selector) View() string {
143	return s.list.View()
144}
145
146func (s *Selector) selectCmd() tea.Msg {
147	item := s.list.SelectedItem()
148	i, ok := item.(IdentifiableItem)
149	if !ok {
150		return SelectMsg("")
151	}
152	return SelectMsg(i.ID())
153}
154
155func (s *Selector) activeCmd() tea.Msg {
156	item := s.list.SelectedItem()
157	i, ok := item.(IdentifiableItem)
158	if !ok {
159		return ActiveMsg("")
160	}
161	return ActiveMsg(i.ID())
162}
163
164func (s *Selector) activeFilterCmd() tea.Msg {
165	// Here we use VisibleItems because when list.FilterMatchesMsg is sent,
166	// VisibleItems is the only way to get the list of filtered items. The list
167	// bubble should export something like list.FilterMatchesMsg.Items().
168	items := s.list.VisibleItems()
169	if len(items) == 0 {
170		return nil
171	}
172	item := items[0]
173	i, ok := item.(IdentifiableItem)
174	if !ok {
175		return nil
176	}
177	return ActiveMsg(i.ID())
178}