@@ -133,11 +133,13 @@ type model struct {
133133 allItems []util.Model // The actual list items
134134 gapSize int // Number of empty lines between items
135135 padding []int // Padding around the list content
136+ wrapNavigation bool // Whether to wrap navigation at the ends
136137
137138 filterable bool // Whether items can be filtered
138139 filterPlaceholder string // Placeholder text for filter input
139140 filteredItems []util.Model // Filtered items based on current search
140141 input textinput.Model // Input field for filtering items
142+ inputStyle lipgloss.Style // Style for the input field
141143 hideFilterInput bool // Whether to hide the filter input field
142144 currentSearch string // Current search term for filtering
143145}
@@ -204,10 +206,26 @@ func WithFilterPlaceholder(placeholder string) listOptions {
204206 }
205207}
206208
209+ // WithInputStyle sets the style for the filter input field.
210+ func WithInputStyle (style lipgloss.Style ) listOptions {
211+ return func (m * model ) {
212+ m .inputStyle = style
213+ }
214+ }
215+
216+ // WithWrapNavigation enables wrapping navigation at the ends of the list.
217+ func WithWrapNavigation (wrap bool ) listOptions {
218+ return func (m * model ) {
219+ m .wrapNavigation = wrap
220+ }
221+ }
222+
207223// New creates a new list model with the specified options.
208224// The list starts with no items selected and requires SetItems to be called
209225// or items to be provided via WithItems option.
210226func New (opts ... listOptions ) ListModel {
227+ t := styles .CurrentTheme ()
228+
211229 m := & model {
212230 help : help .New (),
213231 keyMap : DefaultKeyMap (),
@@ -218,6 +236,7 @@ func New(opts ...listOptions) ListModel {
218236 padding : []int {},
219237 selectionState : selectionState {selectedIndex : NoSelection },
220238 filterPlaceholder : "Type to filter..." ,
239+ inputStyle : t .S ().Base .Padding (0 , 1 , 1 , 1 ),
221240 }
222241 for _ , opt := range opts {
223242 opt (m )
@@ -281,7 +300,7 @@ func (m *model) View() tea.View {
281300 if m .filterable && ! m .hideFilterInput {
282301 content = lipgloss .JoinVertical (
283302 lipgloss .Left ,
284- m .inputStyle () .Render (m .input .View ()),
303+ m .inputStyle .Render (m .input .View ()),
285304 content ,
286305 )
287306 }
@@ -400,7 +419,7 @@ func (m *model) renderVisibleForward() {
400419 renderer := & forwardRenderer {
401420 model : m ,
402421 start : 0 ,
403- cutoff : m .viewState .offset + m .listHeight (),
422+ cutoff : m .viewState .offset + m .listHeight () + m . listHeight () / 2 , // We render a bit more so we make sure we have smooth movementsd
404423 items : m .filteredItems ,
405424 realIdx : m .renderState .lastIndex ,
406425 }
@@ -420,7 +439,7 @@ func (m *model) renderVisibleReverse() {
420439 renderer := & reverseRenderer {
421440 model : m ,
422441 start : 0 ,
423- cutoff : m .viewState .offset + m .listHeight (),
442+ cutoff : m .viewState .offset + m .listHeight () + m . listHeight () / 2 ,
424443 items : m .filteredItems ,
425444 realIdx : m .renderState .lastIndex ,
426445 }
@@ -567,6 +586,10 @@ func (r *reverseRenderer) renderItemLines(item util.Model) []string {
567586// Handles focus management and ensures the selected item remains visible.
568587// Skips section headers during navigation.
569588func (m * model ) selectPreviousItem () tea.Cmd {
589+ if m .selectionState .selectedIndex == m .findFirstSelectableItem () && m .wrapNavigation {
590+ // If at the beginning and wrapping is enabled, go to the last item
591+ return m .goToBottom ()
592+ }
570593 if m .selectionState .selectedIndex <= 0 {
571594 return nil
572595 }
@@ -580,8 +603,9 @@ func (m *model) selectPreviousItem() tea.Cmd {
580603 }
581604
582605 // If we went past the beginning, stay at the first non-header item
583- if m .selectionState .selectedIndex < 0 {
584- m .selectionState .selectedIndex = m .findFirstSelectableItem ()
606+ if m .selectionState .selectedIndex <= 0 {
607+ cmds = append (cmds , m .goToTop ()) // Ensure we scroll to the top if needed
608+ return tea .Batch (cmds ... )
585609 }
586610
587611 cmds = append (cmds , m .focusSelected ())
@@ -593,6 +617,10 @@ func (m *model) selectPreviousItem() tea.Cmd {
593617// Handles focus management and ensures the selected item remains visible.
594618// Skips section headers during navigation.
595619func (m * model ) selectNextItem () tea.Cmd {
620+ if m .selectionState .selectedIndex >= m .findLastSelectableItem () && m .wrapNavigation {
621+ // If at the end and wrapping is enabled, go to the first item
622+ return m .goToTop ()
623+ }
596624 if m .selectionState .selectedIndex >= len (m .filteredItems )- 1 || m .selectionState .selectedIndex < 0 {
597625 return nil
598626 }
@@ -1008,6 +1036,9 @@ func (m *model) listHeight() int {
10081036 case 3 , 4 :
10091037 height -= m .padding [0 ] + m .padding [2 ]
10101038 }
1039+ if m .filterable && ! m .hideFilterInput {
1040+ height -= lipgloss .Height (m .inputStyle .Render ("dummy" ))
1041+ }
10111042 return max (0 , height )
10121043}
10131044
@@ -1107,10 +1138,6 @@ func (m *model) SetItems(items []util.Model) tea.Cmd {
11071138 return tea .Batch (cmds ... )
11081139}
11091140
1110- func (c * model ) inputStyle () lipgloss.Style {
1111- return styles .BaseStyle ().Padding (0 , 1 , 1 , 1 )
1112- }
1113-
11141141// section represents a group of items under a section header.
11151142type section struct {
11161143 header SectionHeader
0 commit comments