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