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. Implements list.DefaultItem.
 19type IdentifiableItem interface {
 20	list.DefaultItem
 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.MouseMsg:
 80		switch msg.Type {
 81		case tea.MouseWheelUp:
 82			s.list.CursorUp()
 83		case tea.MouseWheelDown:
 84			s.list.CursorDown()
 85		}
 86	case tea.KeyMsg:
 87		switch {
 88		case key.Matches(msg, s.common.Keymap.Select):
 89			cmds = append(cmds, s.selectCmd)
 90		}
 91	case list.FilterMatchesMsg:
 92		cmds = append(cmds, s.activeFilterCmd)
 93	}
 94	m, cmd := s.list.Update(msg)
 95	s.list = m
 96	if cmd != nil {
 97		cmds = append(cmds, cmd)
 98	}
 99	// Track filter state and update active item when filter state changes.
100	filterState := s.list.FilterState()
101	if s.filterState != filterState {
102		cmds = append(cmds, s.activeFilterCmd)
103	}
104	s.filterState = filterState
105	// Send ActiveMsg when index change.
106	if s.active != s.list.Index() {
107		cmds = append(cmds, s.activeCmd)
108	}
109	s.active = s.list.Index()
110	return s, tea.Batch(cmds...)
111}
112
113// View implements tea.Model.
114func (s *Selector) View() string {
115	return s.list.View()
116}
117
118func (s *Selector) selectCmd() tea.Msg {
119	item := s.list.SelectedItem()
120	i, ok := item.(IdentifiableItem)
121	if !ok {
122		return SelectMsg("")
123	}
124	return SelectMsg(i.ID())
125}
126
127func (s *Selector) activeCmd() tea.Msg {
128	item := s.list.SelectedItem()
129	i, ok := item.(IdentifiableItem)
130	if !ok {
131		return ActiveMsg("")
132	}
133	return ActiveMsg(i.ID())
134}
135
136func (s *Selector) activeFilterCmd() tea.Msg {
137	// Here we use VisibleItems because when list.FilterMatchesMsg is sent,
138	// VisibleItems is the only way to get the list of filtered items. The list
139	// bubble should export something like list.FilterMatchesMsg.Items().
140	items := s.list.VisibleItems()
141	if len(items) == 0 {
142		return nil
143	}
144	item := items[0]
145	i, ok := item.(IdentifiableItem)
146	if !ok {
147		return nil
148	}
149	return ActiveMsg(i.ID())
150}