44package list
55
66import (
7+ "cmp"
78 "fmt"
89 "io"
910 "sort"
@@ -22,6 +23,13 @@ import (
2223 "github.com/charmbracelet/bubbles/textinput"
2324)
2425
26+ func clamp [T cmp.Ordered ](v , low , high T ) T {
27+ if low > high {
28+ low , high = high , low
29+ }
30+ return min (high , max (low , v ))
31+ }
32+
2533// Item is an item that appears in the list.
2634type Item interface {
2735 // FilterValue is the value we use when filtering against this item when
@@ -282,17 +290,15 @@ func (m *Model) SetFilterText(filter string) {
282290 fmm , _ := msg .(FilterMatchesMsg )
283291 m .filteredItems = filteredItems (fmm )
284292 m .filterState = FilterApplied
285- m .Paginator .Page = 0
286- m .cursor = 0
293+ m .GoToStart ()
287294 m .FilterInput .CursorEnd ()
288295 m .updatePagination ()
289296 m .updateKeybindings ()
290297}
291298
292299// SetFilterState allows setting the filtering state manually.
293300func (m * Model ) SetFilterState (state FilterState ) {
294- m .Paginator .Page = 0
295- m .cursor = 0
301+ m .GoToStart ()
296302 m .filterState = state
297303 m .FilterInput .CursorEnd ()
298304 m .FilterInput .Focus ()
@@ -516,14 +522,12 @@ func (m *Model) CursorUp() {
516522 m .cursor --
517523
518524 // If we're at the start, stop
519- if m .cursor < 0 && m .Paginator .Page == 0 {
525+ if m .cursor < 0 && m .Paginator .OnFirstPage () {
520526 // if infinite scrolling is enabled, go to the last item
521527 if m .InfiniteScrolling {
522- m .Paginator .Page = m .Paginator .TotalPages - 1
523- m .cursor = m .Paginator .ItemsOnPage (len (m .VisibleItems ())) - 1
528+ m .GoToEnd ()
524529 return
525530 }
526-
527531 m .cursor = 0
528532 return
529533 }
@@ -535,18 +539,18 @@ func (m *Model) CursorUp() {
535539
536540 // Go to the previous page
537541 m .Paginator .PrevPage ()
538- m .cursor = m .Paginator . ItemsOnPage ( len ( m . VisibleItems ())) - 1
542+ m .cursor = m .maxCursorIndex ()
539543}
540544
541545// CursorDown moves the cursor down. This can also advance the state to the
542546// next page.
543547func (m * Model ) CursorDown () {
544- itemsOnPage := m .Paginator . ItemsOnPage ( len ( m . VisibleItems ()) )
548+ maxCursorIndex := m .maxCursorIndex ( )
545549
546550 m .cursor ++
547551
548- // If we 're at the end, stop
549- if m .cursor < itemsOnPage {
552+ // We 're still within bounds of the current page, so no need to do anything.
553+ if m .cursor <= maxCursorIndex {
550554 return
551555 }
552556
@@ -557,31 +561,40 @@ func (m *Model) CursorDown() {
557561 return
558562 }
559563
560- // During filtering the cursor position can exceed the number of
561- // itemsOnPage. It's more intuitive to start the cursor at the
562- // topmost position when moving it down in this scenario.
563- if m .cursor > itemsOnPage {
564- m .cursor = 0
565- return
566- }
564+ m .cursor = max (0 , maxCursorIndex )
567565
568- m .cursor = itemsOnPage - 1
569-
570- // if infinite scrolling is enabled, go to the first item
566+ // if infinite scrolling is enabled, go to the first item.
571567 if m .InfiniteScrolling {
572- m .Paginator .Page = 0
573- m .cursor = 0
568+ m .GoToStart ()
574569 }
575570}
576571
572+ // GoToStart moves to the first page, and first item on the first page.
573+ func (m * Model ) GoToStart () {
574+ m .Paginator .Page = 0
575+ m .cursor = 0
576+ }
577+
578+ // GoToEnd moves to the last page, and last item on the last page.
579+ func (m * Model ) GoToEnd () {
580+ m .Paginator .Page = max (0 , m .Paginator .TotalPages - 1 )
581+ m .cursor = m .maxCursorIndex ()
582+ }
583+
577584// PrevPage moves to the previous page, if available.
578585func (m * Model ) PrevPage () {
579586 m .Paginator .PrevPage ()
587+ m .cursor = clamp (m .cursor , 0 , m .maxCursorIndex ())
580588}
581589
582590// NextPage moves to the next page, if available.
583591func (m * Model ) NextPage () {
584592 m .Paginator .NextPage ()
593+ m .cursor = clamp (m .cursor , 0 , m .maxCursorIndex ())
594+ }
595+
596+ func (m * Model ) maxCursorIndex () int {
597+ return max (0 , m .Paginator .ItemsOnPage (len (m .VisibleItems ()))- 1 )
585598}
586599
587600// FilterState returns the current filter state.
@@ -673,22 +686,18 @@ func (m *Model) NewStatusMessage(s string) tea.Cmd {
673686 }
674687}
675688
676- // SetSize sets the width and height of this component.
677- func (m * Model ) SetSize (width , height int ) {
678- m .setSize (width , height )
679- }
680-
681689// SetWidth sets the width of this component.
682690func (m * Model ) SetWidth (v int ) {
683- m .setSize (v , m .height )
691+ m .SetSize (v , m .height )
684692}
685693
686694// SetHeight sets the height of this component.
687695func (m * Model ) SetHeight (v int ) {
688- m .setSize (m .width , v )
696+ m .SetSize (m .width , v )
689697}
690698
691- func (m * Model ) setSize (width , height int ) {
699+ // SetSize sets the width and height of this component.
700+ func (m * Model ) SetSize (width , height int ) {
692701 promptWidth := lipgloss .Width (m .Styles .Title .Render (m .FilterInput .Prompt ))
693702
694703 m .width = width
@@ -848,7 +857,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
848857// Updates for when a user is browsing the list.
849858func (m * Model ) handleBrowsing (msg tea.Msg ) tea.Cmd {
850859 var cmds []tea.Cmd
851- numItems := len (m .VisibleItems ())
852860
853861 switch msg := msg .(type ) {
854862 case tea.KeyMsg :
@@ -874,21 +882,18 @@ func (m *Model) handleBrowsing(msg tea.Msg) tea.Cmd {
874882 m .Paginator .NextPage ()
875883
876884 case key .Matches (msg , m .KeyMap .GoToStart ):
877- m .Paginator .Page = 0
878- m .cursor = 0
885+ m .GoToStart ()
879886
880887 case key .Matches (msg , m .KeyMap .GoToEnd ):
881- m .Paginator .Page = m .Paginator .TotalPages - 1
882- m .cursor = m .Paginator .ItemsOnPage (numItems ) - 1
888+ m .GoToEnd ()
883889
884890 case key .Matches (msg , m .KeyMap .Filter ):
885891 m .hideStatusMessage ()
886892 if m .FilterInput .Value () == "" {
887893 // Populate filter with all items only if the filter is empty.
888894 m .filteredItems = m .itemsAsFilterItems ()
889895 }
890- m .Paginator .Page = 0
891- m .cursor = 0
896+ m .GoToStart ()
892897 m .filterState = Filtering
893898 m .FilterInput .CursorEnd ()
894899 m .FilterInput .Focus ()
@@ -906,11 +911,7 @@ func (m *Model) handleBrowsing(msg tea.Msg) tea.Cmd {
906911 cmd := m .delegate .Update (msg , m )
907912 cmds = append (cmds , cmd )
908913
909- // Keep the index in bounds when paginating
910- itemsOnPage := m .Paginator .ItemsOnPage (len (m .VisibleItems ()))
911- if m .cursor > itemsOnPage - 1 {
912- m .cursor = max (0 , itemsOnPage - 1 )
913- }
914+ m .cursor = clamp (m .cursor , 0 , m .maxCursorIndex ())
914915
915916 return tea .Batch (cmds ... )
916917}
0 commit comments