selector.go

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