models_list.go

  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}