@@ -39,6 +39,7 @@ type ListModel interface {
39 ResetView() // Clear rendering cache and reset scroll position
39 ResetView() // Clear rendering cache and reset scroll position
40 Items() []util.Model // Get all items in the list
40 Items() []util.Model // Get all items in the list
41 SelectedIndex() int // Get the index of the currently selected item
41 SelectedIndex() int // Get the index of the currently selected item
42+ SetSelected(int) tea.Cmd // Set the selected item by index and scroll to it
42 Filter(string) tea.Cmd // Filter items based on a search term
43 Filter(string) tea.Cmd // Filter items based on a search term
@@ -133,11 +134,12 @@ type model struct {
133 gapSize int // Number of empty lines between items
134 gapSize int // Number of empty lines between items
134 padding []int // Padding around the list content
135 padding []int // Padding around the list content
136− filterable bool // Whether items can be filtered
137+ filterable bool // Whether items can be filtered
138+ filterPlaceholder string // Placeholder text for filter input
137− filteredItems []util.Model // Filtered items based on current search
139+ filteredItems []util.Model // Filtered items based on current search
138− input textinput.Model // Input field for filtering items
140+ input textinput.Model // Input field for filtering items
139− hideFilterInput bool // Whether to hide the filter input field
141+ hideFilterInput bool // Whether to hide the filter input field
140− currentSearch string // Current search term for filtering
142+ currentSearch string // Current search term for filtering
143// listOptions is a function type for configuring list options.
145// listOptions is a function type for configuring list options.
@@ -195,29 +197,39 @@ func WithHideFilterInput(hide bool) listOptions {
200+// WithFilterPlaceholder sets the placeholder text for the filter input field.
201+func WithFilterPlaceholder(placeholder string) listOptions {
202+ return func(m *model) {
203+ m.filterPlaceholder = placeholder
198// New creates a new list model with the specified options.
207// New creates a new list model with the specified options.
199// The list starts with no items selected and requires SetItems to be called
208// The list starts with no items selected and requires SetItems to be called
200// or items to be provided via WithItems option.
209// or items to be provided via WithItems option.
201func New(opts ...listOptions) ListModel {
210func New(opts ...listOptions) ListModel {
202 m := &model{
211 m := &model{
203− help: help.New(),
212+ help: help.New(),
204− keyMap: DefaultKeyMap(),
213+ keyMap: DefaultKeyMap(),
205− allItems: []util.Model{},
214+ allItems: []util.Model{},
206− filteredItems: []util.Model{},
215+ filteredItems: []util.Model{},
207− renderState: newRenderState(),
216+ renderState: newRenderState(),
208− gapSize: DefaultGapSize,
217+ gapSize: DefaultGapSize,
209− padding: []int{},
218+ padding: []int{},
210− selectionState: selectionState{selectedIndex: NoSelection},
219+ selectionState: selectionState{selectedIndex: NoSelection},
220+ filterPlaceholder: "Type to filter...",
212 for _, opt := range opts {
222 for _, opt := range opts {
216 if m.filterable && !m.hideFilterInput {
226 if m.filterable && !m.hideFilterInput {
227+ t := styles.CurrentTheme()
217 ti := textinput.New()
228 ti := textinput.New()
218− ti.Placeholder = "Type to filter..."
229+ ti.Placeholder = m.filterPlaceholder
219 ti.SetVirtualCursor(false)
230 ti.SetVirtualCursor(false)
220 ti.Focus()
231 ti.Focus()
232+ ti.SetStyles(t.S().TextInput)
221 m.input = ti
233 m.input = ti
223 // disable j,k movements
235 // disable j,k movements
@@ -616,7 +628,7 @@ func (m *model) isSectionHeader(index int) bool {
617// findFirstSelectableItem finds the first item that is not a section header.
629// findFirstSelectableItem finds the first item that is not a section header.
618func (m *model) findFirstSelectableItem() int {
630func (m *model) findFirstSelectableItem() int {
619− for i := 0; i < len(m.filteredItems); i++ {
631+ for i := range m.filteredItems {
620 if !m.isSectionHeader(i) {
632 if !m.isSectionHeader(i) {
@@ -944,7 +956,7 @@ func (m *model) SetSize(width int, height int) tea.Cmd {
944 m.viewState.width = width
956 m.viewState.width = width
945 m.ResetView()
957 m.ResetView()
946 if m.filterable && !m.hideFilterInput {
958 if m.filterable && !m.hideFilterInput {
947− m.input.SetWidth(m.getItemWidth() - 3)
959+ m.input.SetWidth(m.getItemWidth() - 5)
949 return m.setAllItemsSize()
961 return m.setAllItemsSize()
@@ -1096,7 +1108,7 @@ func (m *model) SetItems(items []util.Model) tea.Cmd {
1098func (c *model) inputStyle() lipgloss.Style {
1110func (c *model) inputStyle() lipgloss.Style {
1099− return styles.BaseStyle()
1111+ return styles.BaseStyle().Padding(0, 1, 1, 1)
1102// section represents a group of items under a section header.
1114// section represents a group of items under a section header.
@@ -1275,3 +1287,22 @@ func (m *model) SelectedIndex() int {
1276 return m.selectionState.selectedIndex
1288 return m.selectionState.selectedIndex
1291+// SetSelected sets the selected item by index and automatically scrolls to make it visible.
1292+// If the index is invalid or points to a section header, it finds the nearest selectable item.
1293+func (m *model) SetSelected(index int) tea.Cmd {
1294+ changeNeeded := m.selectionState.selectedIndex - index
1295+ cmds := []tea.Cmd{}
1296+ if changeNeeded < 0 {
1297+ for range -changeNeeded {
1298+ cmds = append(cmds, m.selectNextItem())
1301+ } else if changeNeeded > 0 {
1302+ for range changeNeeded {
1303+ cmds = append(cmds, m.selectPreviousItem())
1307+ return tea.Batch(cmds...)