1package selector23import (4 "sync"56 "charm.land/bubbles/v2/key"7 "charm.land/bubbles/v2/list"8 tea "charm.land/bubbletea/v2"9 "github.com/charmbracelet/soft-serve/pkg/ui/common"10)1112// Selector is a list of items that can be selected.13type Selector struct {14 *list.Model15 common common.Common16 active int17 filterState list.FilterState1819 // XXX: we use a mutex to support concurrent access to the model. This is20 // needed to implement pagination for the Log component. list.Model does21 // not support item pagination so we hack it ourselves on top of22 // list.Model.23 mtx sync.RWMutex24}2526// IdentifiableItem is an item that can be identified by a string. Implements27// list.DefaultItem.28type IdentifiableItem interface {29 list.DefaultItem30 ID() string31}3233// ItemDelegate is a wrapper around list.ItemDelegate.34type ItemDelegate interface {35 list.ItemDelegate36}3738// SelectMsg is a message that is sent when an item is selected.39type SelectMsg struct{ IdentifiableItem }4041// ActiveMsg is a message that is sent when an item is active but not selected.42type ActiveMsg struct{ IdentifiableItem }4344// New creates a new selector.45func New(common common.Common, items []IdentifiableItem, delegate ItemDelegate) *Selector {46 itms := make([]list.Item, len(items))47 for i, item := range items {48 itms[i] = item49 }50 l := list.New(itms, delegate, common.Width, common.Height)51 l.Styles.NoItems = common.Styles.NoContent52 s := &Selector{53 Model: &l,54 common: common,55 }56 s.SetSize(common.Width, common.Height)57 return s58}5960// PerPage returns the number of items per page.61func (s *Selector) PerPage() int {62 s.mtx.RLock()63 defer s.mtx.RUnlock()64 return s.Model.Paginator.PerPage65}6667// SetPage sets the current page.68func (s *Selector) SetPage(page int) {69 s.mtx.Lock()70 defer s.mtx.Unlock()71 s.Model.Paginator.Page = page72}7374// Page returns the current page.75func (s *Selector) Page() int {76 s.mtx.RLock()77 defer s.mtx.RUnlock()78 return s.Model.Paginator.Page79}8081// TotalPages returns the total number of pages.82func (s *Selector) TotalPages() int {83 s.mtx.RLock()84 defer s.mtx.RUnlock()85 return s.Model.Paginator.TotalPages86}8788// SetTotalPages sets the total number of pages given the number of items.89func (s *Selector) SetTotalPages(items int) int {90 s.mtx.Lock()91 defer s.mtx.Unlock()92 return s.Model.Paginator.SetTotalPages(items)93}9495// SelectedItem returns the currently selected item.96func (s *Selector) SelectedItem() IdentifiableItem {97 s.mtx.RLock()98 defer s.mtx.RUnlock()99 item := s.Model.SelectedItem()100 i, ok := item.(IdentifiableItem)101 if !ok {102 return nil103 }104 return i105}106107// Select selects the item at the given index.108func (s *Selector) Select(index int) {109 s.mtx.RLock()110 defer s.mtx.RUnlock()111 s.Model.Select(index)112}113114// SetShowTitle sets the show title flag.115func (s *Selector) SetShowTitle(show bool) {116 s.mtx.Lock()117 defer s.mtx.Unlock()118 s.Model.SetShowTitle(show)119}120121// SetShowHelp sets the show help flag.122func (s *Selector) SetShowHelp(show bool) {123 s.mtx.Lock()124 defer s.mtx.Unlock()125 s.Model.SetShowHelp(show)126}127128// SetShowStatusBar sets the show status bar flag.129func (s *Selector) SetShowStatusBar(show bool) {130 s.mtx.Lock()131 defer s.mtx.Unlock()132 s.Model.SetShowStatusBar(show)133}134135// DisableQuitKeybindings disables the quit keybindings.136func (s *Selector) DisableQuitKeybindings() {137 s.mtx.Lock()138 defer s.mtx.Unlock()139 s.Model.DisableQuitKeybindings()140}141142// SetShowFilter sets the show filter flag.143func (s *Selector) SetShowFilter(show bool) {144 s.mtx.Lock()145 defer s.mtx.Unlock()146 s.Model.SetShowFilter(show)147}148149// SetShowPagination sets the show pagination flag.150func (s *Selector) SetShowPagination(show bool) {151 s.mtx.Lock()152 defer s.mtx.Unlock()153 s.Model.SetShowPagination(show)154}155156// SetFilteringEnabled sets the filtering enabled flag.157func (s *Selector) SetFilteringEnabled(enabled bool) {158 s.mtx.Lock()159 defer s.mtx.Unlock()160 s.Model.SetFilteringEnabled(enabled)161}162163// SetSize implements common.Component.164func (s *Selector) SetSize(width, height int) {165 s.mtx.Lock()166 defer s.mtx.Unlock()167 s.common.SetSize(width, height)168 s.Model.SetSize(width, height)169}170171// SetItems sets the items in the selector.172func (s *Selector) SetItems(items []IdentifiableItem) tea.Cmd {173 its := make([]list.Item, len(items))174 for i, item := range items {175 its[i] = item176 }177 s.mtx.Lock()178 defer s.mtx.Unlock()179 return s.Model.SetItems(its)180}181182// Index returns the index of the selected item.183func (s *Selector) Index() int {184 s.mtx.RLock()185 defer s.mtx.RUnlock()186 return s.Model.Index()187}188189// Items returns the items in the selector.190func (s *Selector) Items() []list.Item {191 s.mtx.RLock()192 defer s.mtx.RUnlock()193 return s.Model.Items()194}195196// VisibleItems returns all the visible items in the selector.197func (s *Selector) VisibleItems() []list.Item {198 s.mtx.RLock()199 defer s.mtx.RUnlock()200 return s.Model.VisibleItems()201}202203// FilterState returns the filter state.204func (s *Selector) FilterState() list.FilterState {205 s.mtx.RLock()206 defer s.mtx.RUnlock()207 return s.Model.FilterState()208}209210// CursorUp moves the cursor up.211func (s *Selector) CursorUp() {212 s.mtx.Lock()213 defer s.mtx.Unlock()214 s.Model.CursorUp()215}216217// CursorDown moves the cursor down.218func (s *Selector) CursorDown() {219 s.mtx.Lock()220 defer s.mtx.Unlock()221 s.Model.CursorDown()222}223224// Init implements tea.Model.225func (s *Selector) Init() tea.Cmd {226 return s.activeCmd227}228229// Update implements tea.Model.230func (s *Selector) Update(msg tea.Msg) (common.Model, tea.Cmd) {231 cmds := make([]tea.Cmd, 0)232 switch msg := msg.(type) {233 case tea.MouseClickMsg:234 m := msg.Mouse()235 switch m.Button {236 case tea.MouseWheelUp:237 s.CursorUp()238 case tea.MouseWheelDown:239 s.CursorDown()240 case tea.MouseLeft:241 curIdx := s.Index()242 for i, item := range s.Items() {243 item, _ := item.(IdentifiableItem)244 // Check each item to see if it's in bounds.245 if item != nil && s.common.Zone.Get(item.ID()).InBounds(msg) {246 if i == curIdx {247 cmds = append(cmds, s.SelectItemCmd)248 } else {249 s.Select(i)250 }251 break252 }253 }254 }255 case tea.KeyPressMsg:256 filterState := s.FilterState()257 switch {258 case key.Matches(msg, s.common.KeyMap.Help):259 if filterState == list.Filtering {260 return s, tea.Batch(cmds...)261 }262 case key.Matches(msg, s.common.KeyMap.Select):263 if filterState != list.Filtering {264 cmds = append(cmds, s.SelectItemCmd)265 }266 }267 case list.FilterMatchesMsg:268 cmds = append(cmds, s.activeFilterCmd)269 }270 m, cmd := s.Model.Update(msg)271 s.mtx.Lock()272 s.Model = &m273 s.mtx.Unlock()274 if cmd != nil {275 cmds = append(cmds, cmd)276 }277 // Track filter state and update active item when filter state changes.278 filterState := s.FilterState()279 if s.filterState != filterState {280 cmds = append(cmds, s.activeFilterCmd)281 }282 s.filterState = filterState283 // Send ActiveMsg when index change.284 if s.active != s.Index() {285 cmds = append(cmds, s.activeCmd)286 }287 s.active = s.Index()288 return s, tea.Batch(cmds...)289}290291// View implements tea.Model.292func (s *Selector) View() string {293 return s.Model.View()294}295296// SelectItemCmd is a command that selects the currently active item.297func (s *Selector) SelectItemCmd() tea.Msg {298 return SelectMsg{s.SelectedItem()}299}300301func (s *Selector) activeCmd() tea.Msg {302 item := s.SelectedItem()303 return ActiveMsg{item}304}305306func (s *Selector) activeFilterCmd() tea.Msg {307 // Here we use VisibleItems because when list.FilterMatchesMsg is sent,308 // VisibleItems is the only way to get the list of filtered items. The list309 // bubble should export something like list.FilterMatchesMsg.Items().310 items := s.VisibleItems()311 if len(items) == 0 {312 return nil313 }314 item := items[0]315 i, ok := item.(IdentifiableItem)316 if !ok {317 return nil318 }319 return ActiveMsg{i}320}