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