selector.go

  1package selector
  2
  3import (
  4	"sync"
  5
  6	"github.com/charmbracelet/bubbles/key"
  7	"github.com/charmbracelet/bubbles/list"
  8	tea "github.com/charmbracelet/bubbletea"
  9	"github.com/charmbracelet/soft-serve/ui/common"
 10)
 11
 12// Selector is a list of items that can be selected.
 13type Selector struct {
 14	list.Model
 15	common      common.Common
 16	active      int
 17	filterState list.FilterState
 18	mtx         sync.RWMutex
 19}
 20
 21// IdentifiableItem is an item that can be identified by a string. Implements
 22// list.DefaultItem.
 23type IdentifiableItem interface {
 24	list.DefaultItem
 25	ID() string
 26}
 27
 28// ItemDelegate is a wrapper around list.ItemDelegate.
 29type ItemDelegate interface {
 30	list.ItemDelegate
 31}
 32
 33// SelectMsg is a message that is sent when an item is selected.
 34type SelectMsg struct{ IdentifiableItem }
 35
 36// ActiveMsg is a message that is sent when an item is active but not selected.
 37type ActiveMsg struct{ IdentifiableItem }
 38
 39// New creates a new selector.
 40func New(common common.Common, items []IdentifiableItem, delegate ItemDelegate) *Selector {
 41	itms := make([]list.Item, len(items))
 42	for i, item := range items {
 43		itms[i] = item
 44	}
 45	l := list.New(itms, delegate, common.Width, common.Height)
 46	s := &Selector{
 47		Model:  l,
 48		common: common,
 49	}
 50	s.SetSize(common.Width, common.Height)
 51	return s
 52}
 53
 54// PerPage returns the number of items per page.
 55func (s *Selector) PerPage() int {
 56	return s.Model.Paginator.PerPage
 57}
 58
 59// SetPage sets the current page.
 60func (s *Selector) SetPage(page int) {
 61	s.Model.Paginator.Page = page
 62}
 63
 64// Page returns the current page.
 65func (s *Selector) Page() int {
 66	return s.Model.Paginator.Page
 67}
 68
 69// TotalPages returns the total number of pages.
 70func (s *Selector) TotalPages() int {
 71	return s.Model.Paginator.TotalPages
 72}
 73
 74// Select selects the item at the given index.
 75func (s *Selector) Select(index int) {
 76	s.Model.Select(index)
 77}
 78
 79// SetShowTitle sets the show title flag.
 80func (s *Selector) SetShowTitle(show bool) {
 81	s.Model.SetShowTitle(show)
 82}
 83
 84// SetShowHelp sets the show help flag.
 85func (s *Selector) SetShowHelp(show bool) {
 86	s.Model.SetShowHelp(show)
 87}
 88
 89// SetShowStatusBar sets the show status bar flag.
 90func (s *Selector) SetShowStatusBar(show bool) {
 91	s.Model.SetShowStatusBar(show)
 92}
 93
 94// DisableQuitKeybindings disables the quit keybindings.
 95func (s *Selector) DisableQuitKeybindings() {
 96	s.Model.DisableQuitKeybindings()
 97}
 98
 99// SetShowFilter sets the show filter flag.
100func (s *Selector) SetShowFilter(show bool) {
101	s.Model.SetShowFilter(show)
102}
103
104// SetShowPagination sets the show pagination flag.
105func (s *Selector) SetShowPagination(show bool) {
106	s.Model.SetShowPagination(show)
107}
108
109// SetFilteringEnabled sets the filtering enabled flag.
110func (s *Selector) SetFilteringEnabled(enabled bool) {
111	s.Model.SetFilteringEnabled(enabled)
112}
113
114// SetSize implements common.Component.
115func (s *Selector) SetSize(width, height int) {
116	s.mtx.Lock()
117	defer s.mtx.Unlock()
118	s.common.SetSize(width, height)
119	s.Model.SetSize(width, height)
120}
121
122// SetItems sets the items in the selector.
123func (s *Selector) SetItems(items []IdentifiableItem) tea.Cmd {
124	its := make([]list.Item, len(items))
125	for i, item := range items {
126		its[i] = item
127	}
128	return s.Model.SetItems(its)
129}
130
131// Index returns the index of the selected item.
132func (s *Selector) Index() int {
133	return s.Model.Index()
134}
135
136// Init implements tea.Model.
137func (s *Selector) Init() tea.Cmd {
138	return s.activeCmd
139}
140
141// Update implements tea.Model.
142func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
143	cmds := make([]tea.Cmd, 0)
144	switch msg := msg.(type) {
145	case tea.MouseMsg:
146		switch msg.Type {
147		case tea.MouseWheelUp:
148			s.Model.CursorUp()
149		case tea.MouseWheelDown:
150			s.Model.CursorDown()
151		case tea.MouseLeft:
152			curIdx := s.Model.Index()
153			for i, item := range s.Model.Items() {
154				item, _ := item.(IdentifiableItem)
155				// Check each item to see if it's in bounds.
156				if item != nil && s.common.Zone.Get(item.ID()).InBounds(msg) {
157					if i == curIdx {
158						cmds = append(cmds, s.selectCmd)
159					} else {
160						s.Model.Select(i)
161					}
162					break
163				}
164			}
165		}
166	case tea.KeyMsg:
167		filterState := s.Model.FilterState()
168		switch {
169		case key.Matches(msg, s.common.KeyMap.Help):
170			if filterState == list.Filtering {
171				return s, tea.Batch(cmds...)
172			}
173		case key.Matches(msg, s.common.KeyMap.Select):
174			if filterState != list.Filtering {
175				cmds = append(cmds, s.selectCmd)
176			}
177		}
178	case list.FilterMatchesMsg:
179		cmds = append(cmds, s.activeFilterCmd)
180	}
181	m, cmd := s.Model.Update(msg)
182	s.mtx.Lock()
183	s.Model = m
184	s.mtx.Unlock()
185	if cmd != nil {
186		cmds = append(cmds, cmd)
187	}
188	// Track filter state and update active item when filter state changes.
189	filterState := s.Model.FilterState()
190	if s.filterState != filterState {
191		cmds = append(cmds, s.activeFilterCmd)
192	}
193	s.filterState = filterState
194	// Send ActiveMsg when index change.
195	if s.active != s.Model.Index() {
196		cmds = append(cmds, s.activeCmd)
197	}
198	s.active = s.Model.Index()
199	return s, tea.Batch(cmds...)
200}
201
202// View implements tea.Model.
203func (s *Selector) View() string {
204	return s.Model.View()
205}
206
207// SelectItem is a command that selects the currently active item.
208func (s *Selector) SelectItem() tea.Msg {
209	return s.selectCmd()
210}
211
212func (s *Selector) selectCmd() tea.Msg {
213	item := s.Model.SelectedItem()
214	i, ok := item.(IdentifiableItem)
215	if !ok {
216		return SelectMsg{}
217	}
218	return SelectMsg{i}
219}
220
221func (s *Selector) activeCmd() tea.Msg {
222	s.mtx.RLock()
223	item := s.Model.SelectedItem()
224	s.mtx.RUnlock()
225	i, ok := item.(IdentifiableItem)
226	if !ok {
227		return ActiveMsg{}
228	}
229	return ActiveMsg{i}
230}
231
232func (s *Selector) activeFilterCmd() tea.Msg {
233	// Here we use VisibleItems because when list.FilterMatchesMsg is sent,
234	// VisibleItems is the only way to get the list of filtered items. The list
235	// bubble should export something like list.FilterMatchesMsg.Items().
236	items := s.Model.VisibleItems()
237	if len(items) == 0 {
238		return nil
239	}
240	item := items[0]
241	i, ok := item.(IdentifiableItem)
242	if !ok {
243		return nil
244	}
245	return ActiveMsg{i}
246}