feat(ui): list: expose filterable items source type and return values for selection methods

Ayman Bagabas created

Change summary

internal/ui/list/filterable.go | 12 ++++++++----
internal/ui/list/item.go       | 19 +++++++++++++++++++
internal/ui/list/list.go       | 21 +++++++++++++++++----
3 files changed, 44 insertions(+), 8 deletions(-)

Detailed changes

internal/ui/list/filterable.go 🔗

@@ -70,13 +70,17 @@ func (f *FilterableList) SetFilter(q string) {
 	f.query = q
 }
 
-type filterableItems []FilterableItem
+// FilterableItemsSource is a type that implements [fuzzy.Source] for filtering
+// [FilterableItem]s.
+type FilterableItemsSource []FilterableItem
 
-func (f filterableItems) Len() int {
+// Len returns the length of the source.
+func (f FilterableItemsSource) Len() int {
 	return len(f)
 }
 
-func (f filterableItems) String(i int) string {
+// String returns the string representation of the item at index i.
+func (f FilterableItemsSource) String(i int) string {
 	return f[i].Filter()
 }
 
@@ -94,7 +98,7 @@ func (f *FilterableList) VisibleItems() []Item {
 		return items
 	}
 
-	items := filterableItems(f.items)
+	items := FilterableItemsSource(f.items)
 	matches := fuzzy.FindFrom(f.query, items)
 	matchedItems := []Item{}
 	resultSize := len(matches)

internal/ui/list/item.go 🔗

@@ -1,6 +1,8 @@
 package list
 
 import (
+	"strings"
+
 	"github.com/charmbracelet/x/ansi"
 )
 
@@ -30,3 +32,20 @@ type MouseClickable interface {
 	// It returns true if the event was handled, false otherwise.
 	HandleMouseClick(btn ansi.MouseButton, x, y int) bool
 }
+
+// SpacerItem is a spacer item that adds vertical space in the list.
+type SpacerItem struct {
+	Height int
+}
+
+// NewSpacerItem creates a new [SpacerItem] with the specified height.
+func NewSpacerItem(height int) *SpacerItem {
+	return &SpacerItem{
+		Height: max(0, height-1),
+	}
+}
+
+// Render implements the Item interface for [SpacerItem].
+func (s *SpacerItem) Render(width int) string {
+	return strings.Repeat("\n", s.Height)
+}

internal/ui/list/list.go 🔗

@@ -390,6 +390,7 @@ func (l *List) SelectedItemInView() bool {
 }
 
 // SetSelected sets the selected item index in the list.
+// It returns -1 if the index is out of bounds.
 func (l *List) SetSelected(index int) {
 	if index < 0 || index >= len(l.items) {
 		l.selectedIdx = -1
@@ -415,31 +416,43 @@ func (l *List) IsSelectedLast() bool {
 }
 
 // SelectPrev selects the previous item in the list.
-func (l *List) SelectPrev() {
+// It returns whether the selection changed.
+func (l *List) SelectPrev() bool {
 	if l.selectedIdx > 0 {
 		l.selectedIdx--
+		return true
 	}
+	return false
 }
 
 // SelectNext selects the next item in the list.
-func (l *List) SelectNext() {
+// It returns whether the selection changed.
+func (l *List) SelectNext() bool {
 	if l.selectedIdx < len(l.items)-1 {
 		l.selectedIdx++
+		return true
 	}
+	return false
 }
 
 // SelectFirst selects the first item in the list.
-func (l *List) SelectFirst() {
+// It returns whether the selection changed.
+func (l *List) SelectFirst() bool {
 	if len(l.items) > 0 {
 		l.selectedIdx = 0
+		return true
 	}
+	return false
 }
 
 // SelectLast selects the last item in the list.
-func (l *List) SelectLast() {
+// It returns whether the selection changed.
+func (l *List) SelectLast() bool {
 	if len(l.items) > 0 {
 		l.selectedIdx = len(l.items) - 1
+		return true
 	}
+	return false
 }
 
 // SelectedItem returns the currently selected item. It may be nil if no item