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