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