1package dialog
2
3import (
4 "slices"
5
6 "github.com/charmbracelet/crush/internal/ui/list"
7 "github.com/charmbracelet/crush/internal/ui/styles"
8 "github.com/sahilm/fuzzy"
9)
10
11// ModelsList is a list specifically for model items and groups.
12type ModelsList struct {
13 *list.List
14 groups []ModelGroup
15 query string
16 t *styles.Styles
17}
18
19// NewModelsList creates a new list suitable for model items and groups.
20func NewModelsList(sty *styles.Styles, groups ...ModelGroup) *ModelsList {
21 f := &ModelsList{
22 List: list.NewList(),
23 groups: groups,
24 t: sty,
25 }
26 return f
27}
28
29// Len returns the number of model items across all groups.
30func (f *ModelsList) Len() int {
31 n := 0
32 for _, g := range f.groups {
33 n += len(g.Items)
34 }
35 return n
36}
37
38// SetGroups sets the model groups and updates the list items.
39func (f *ModelsList) SetGroups(groups ...ModelGroup) {
40 f.groups = groups
41 items := []list.Item{}
42 for _, g := range f.groups {
43 items = append(items, &g)
44 for _, item := range g.Items {
45 items = append(items, item)
46 }
47 // Add a space separator after each provider section
48 items = append(items, list.NewSpacerItem(1))
49 }
50 f.SetItems(items...)
51}
52
53// SetFilter sets the filter query and updates the list items.
54func (f *ModelsList) SetFilter(q string) {
55 f.query = q
56}
57
58// SetSelected sets the selected item index. It overrides the base method to
59// skip non-model items.
60func (f *ModelsList) SetSelected(index int) {
61 if index < 0 || index >= f.Len() {
62 f.List.SetSelected(index)
63 return
64 }
65
66 f.List.SetSelected(index)
67 for {
68 selectedItem := f.List.SelectedItem()
69 if _, ok := selectedItem.(*ModelItem); ok {
70 return
71 }
72 f.List.SetSelected(index + 1)
73 index++
74 if index >= f.Len() {
75 return
76 }
77 }
78}
79
80// SetSelectedItem sets the selected item in the list by item ID.
81func (f *ModelsList) SetSelectedItem(itemID string) {
82 if itemID == "" {
83 f.SetSelected(0)
84 return
85 }
86
87 count := 0
88 for _, g := range f.groups {
89 for _, item := range g.Items {
90 if item.ID() == itemID {
91 f.SetSelected(count)
92 return
93 }
94 count++
95 }
96 }
97}
98
99// SelectNext selects the next model item, skipping any non-focusable items
100// like group headers and spacers.
101func (f *ModelsList) SelectNext() (v bool) {
102 for {
103 v = f.List.SelectNext()
104 selectedItem := f.List.SelectedItem()
105 if _, ok := selectedItem.(*ModelItem); ok {
106 return v
107 }
108 }
109}
110
111// SelectPrev selects the previous model item, skipping any non-focusable items
112// like group headers and spacers.
113func (f *ModelsList) SelectPrev() (v bool) {
114 for {
115 v = f.List.SelectPrev()
116 selectedItem := f.List.SelectedItem()
117 if _, ok := selectedItem.(*ModelItem); ok {
118 return v
119 }
120 }
121}
122
123// SelectFirst selects the first model item in the list.
124func (f *ModelsList) SelectFirst() (v bool) {
125 v = f.List.SelectFirst()
126 for {
127 selectedItem := f.List.SelectedItem()
128 if _, ok := selectedItem.(*ModelItem); ok {
129 return v
130 }
131 v = f.List.SelectNext()
132 }
133}
134
135// SelectLast selects the last model item in the list.
136func (f *ModelsList) SelectLast() (v bool) {
137 v = f.List.SelectLast()
138 for {
139 selectedItem := f.List.SelectedItem()
140 if _, ok := selectedItem.(*ModelItem); ok {
141 return v
142 }
143 v = f.List.SelectPrev()
144 }
145}
146
147// VisibleItems returns the visible items after filtering.
148func (f *ModelsList) VisibleItems() []list.Item {
149 if f.query == "" {
150 // No filter, return all items with group headers
151 items := []list.Item{}
152 for _, g := range f.groups {
153 items = append(items, &g)
154 for _, item := range g.Items {
155 item.SetMatch(fuzzy.Match{})
156 items = append(items, item)
157 }
158 // Add a space separator after each provider section
159 items = append(items, list.NewSpacerItem(1))
160 }
161 return items
162 }
163
164 filterableItems := make([]list.FilterableItem, 0, f.Len())
165 for _, g := range f.groups {
166 for _, item := range g.Items {
167 filterableItems = append(filterableItems, item)
168 }
169 }
170
171 matches := fuzzy.FindFrom(f.query, list.FilterableItemsSource(filterableItems))
172 for _, match := range matches {
173 item := filterableItems[match.Index]
174 if ms, ok := item.(list.MatchSettable); ok {
175 ms.SetMatch(match)
176 item = ms.(list.FilterableItem)
177 }
178 filterableItems = append(filterableItems, item)
179 }
180
181 items := []list.Item{}
182 visitedGroups := map[int]bool{}
183
184 // Reconstruct groups with matched items
185 // Find which group this item belongs to
186 for gi, g := range f.groups {
187 addedCount := 0
188 for _, match := range matches {
189 item := filterableItems[match.Index]
190 if slices.Contains(g.Items, item.(*ModelItem)) {
191 if !visitedGroups[gi] {
192 // Add section header
193 items = append(items, &g)
194 visitedGroups[gi] = true
195 }
196 // Add the matched item
197 if ms, ok := item.(list.MatchSettable); ok {
198 ms.SetMatch(match)
199 item = ms.(list.FilterableItem)
200 }
201 items = append(items, item)
202 addedCount++
203 }
204 }
205 if addedCount > 0 {
206 // Add a space separator after each provider section
207 items = append(items, list.NewSpacerItem(1))
208 }
209 }
210
211 return items
212}
213
214// Render renders the filterable list.
215func (f *ModelsList) Render() string {
216 f.SetItems(f.VisibleItems()...)
217 return f.List.Render()
218}
219
220type modelGroups []ModelGroup
221
222func (m modelGroups) Len() int {
223 n := 0
224 for _, g := range m {
225 n += len(g.Items)
226 }
227 return n
228}
229
230func (m modelGroups) String(i int) string {
231 count := 0
232 for _, g := range m {
233 if i < count+len(g.Items) {
234 return g.Items[i-count].Filter()
235 }
236 count += len(g.Items)
237 }
238 return ""
239}