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