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