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        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 and extends list.Item.
 19type IdentifiableItem interface {
 20	list.Item
 21	ID() string
 22}
 23
 24// SelectMsg is a message that is sent when an item is selected.
 25type SelectMsg string
 26
 27// ActiveMsg is a message that is sent when an item is active but not selected.
 28type ActiveMsg string
 29
 30// New creates a new selector.
 31func New(common common.Common, items []IdentifiableItem, delegate list.ItemDelegate) *Selector {
 32	itms := make([]list.Item, len(items))
 33	for i, item := range items {
 34		itms[i] = item
 35	}
 36	l := list.New(itms, delegate, common.Width, common.Height)
 37	l.SetShowTitle(false)
 38	l.SetShowHelp(false)
 39	l.SetShowStatusBar(false)
 40	l.DisableQuitKeybindings()
 41	s := &Selector{
 42		list:   l,
 43		common: common,
 44	}
 45	s.SetSize(common.Width, common.Height)
 46	return s
 47}
 48
 49// KeyMap returns the underlying list's keymap.
 50func (s *Selector) KeyMap() list.KeyMap {
 51	return s.list.KeyMap
 52}
 53
 54// SetSize implements common.Component.
 55func (s *Selector) SetSize(width, height int) {
 56	s.common.SetSize(width, height)
 57	s.list.SetSize(width, height)
 58}
 59
 60// SetItems sets the items in the selector.
 61func (s *Selector) SetItems(items []list.Item) tea.Cmd {
 62	return s.list.SetItems(items)
 63}
 64
 65// Index returns the index of the selected item.
 66func (s *Selector) Index() int {
 67	return s.list.Index()
 68}
 69
 70// Init implements tea.Model.
 71func (s *Selector) Init() tea.Cmd {
 72	return s.activeCmd
 73}
 74
 75// Update implements tea.Model.
 76func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 77	cmds := make([]tea.Cmd, 0)
 78	switch msg := msg.(type) {
 79	case tea.KeyMsg:
 80		switch {
 81		case key.Matches(msg, s.common.Keymap.Select):
 82			cmds = append(cmds, s.selectCmd)
 83		}
 84	case list.FilterMatchesMsg:
 85		cmds = append(cmds, s.activeFilterCmd)
 86	}
 87	m, cmd := s.list.Update(msg)
 88	s.list = m
 89	if cmd != nil {
 90		cmds = append(cmds, cmd)
 91	}
 92	// Track filter state and update active item when filter state changes.
 93	filterState := s.list.FilterState()
 94	if s.filterState != filterState {
 95		cmds = append(cmds, s.activeFilterCmd)
 96	}
 97	s.filterState = filterState
 98	// Send ActiveMsg when index change.
 99	if s.active != s.list.Index() {
100		cmds = append(cmds, s.activeCmd)
101	}
102	s.active = s.list.Index()
103	return s, tea.Batch(cmds...)
104}
105
106// View implements tea.Model.
107func (s *Selector) View() string {
108	return s.list.View()
109}
110
111func (s *Selector) selectCmd() tea.Msg {
112	item := s.list.SelectedItem()
113	i, ok := item.(IdentifiableItem)
114	if !ok {
115		return SelectMsg("")
116	}
117	return SelectMsg(i.ID())
118}
119
120func (s *Selector) activeCmd() tea.Msg {
121	item := s.list.SelectedItem()
122	i, ok := item.(IdentifiableItem)
123	if !ok {
124		return ActiveMsg("")
125	}
126	return ActiveMsg(i.ID())
127}
128
129func (s *Selector) activeFilterCmd() tea.Msg {
130	// Here we use VisibleItems because when list.FilterMatchesMsg is sent,
131	// VisibleItems is the only way to get the list of filtered items. The list
132	// bubble should export something like list.FilterMatchesMsg.Items().
133	items := s.list.VisibleItems()
134	if len(items) == 0 {
135		return nil
136	}
137	item := items[0]
138	i, ok := item.(IdentifiableItem)
139	if !ok {
140		return nil
141	}
142	return ActiveMsg(i.ID())
143}