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