@@ -1,7 +1,6 @@
11package list
22
33import (
4− "slices"
54 "strings"
65
76 "github.com/charmbracelet/bubbles/v2/key"
@@ -86,7 +85,7 @@ type list[T Item] struct {
8685 offset int
8786
8887 indexMap *csync.Map[string, int]
89− items []T
88+ items *csync.Slice[T]
9089
9190 renderedItems *csync.Map[string, renderedItem]
9291
@@ -164,7 +163,7 @@ func New[T Item](items []T, opts ...ListOption) List[T] {
164163 keyMap: DefaultKeyMap(),
165164 focused: true,
166165 },
167− items: items,
166+ items: csync.NewSliceFrom(items),
168167 indexMap: csync.NewMap[string, int](),
169168 renderedItems: csync.NewMap[string, renderedItem](),
170169 }
@@ -191,7 +190,7 @@ func (l *list[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
191190 switch msg := msg.(type) {
192191 case anim.StepMsg:
193192 var cmds []tea.Cmd
194− for _, item := range l.items {
193+ for _, item := range l.items.Slice() {
195194 if i, ok := any(item).(HasAnim); ok && i.Spinning() {
196195 updated, cmd := i.Update(msg)
197196 cmds = append(cmds, cmd)
@@ -267,7 +266,7 @@ func (l *list[T]) viewPosition() (int, int) {
267266
268267func (l *list[T]) recalculateItemPositions() {
269268 currentContentHeight := 0
270− for _, item := range l.items {
269+ for _, item := range l.items.Slice() {
271270 rItem, ok := l.renderedItems.Get(item.ID())
272271 if !ok {
273272 continue
@@ -280,7 +279,7 @@ func (l *list[T]) recalculateItemPositions() {
280279}
281280
282281func (l *list[T]) render() tea.Cmd {
283− if l.width <= 0 || l.height <= 0 || len(l.items) == 0 {
282+ if l.width <= 0 || l.height <= 0 || l.items.Len() == 0 {
284283 return nil
285284 }
286285 l.setDefaultSelected()
@@ -424,19 +423,23 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
424423 if inx == ItemNotFound {
425424 return nil
426425 }
427− item, ok := l.renderedItems.Get(l.items[inx].ID())
426+ item, ok := l.items.Get(inx)
427+ if !ok {
428+ continue
429+ }
430+ renderedItem, ok := l.renderedItems.Get(item.ID())
428431 if !ok {
429432 continue
430433 }
431434
432435 // If the item is bigger than the viewport, select it
433− if item.start <= start && item.end >= end {
434− l.selectedItem = item.id
436+ if renderedItem.start <= start && renderedItem.end >= end {
437+ l.selectedItem = renderedItem.id
435438 return l.render()
436439 }
437440 // item is in the view
438− if item.start >= start && item.start <= end {
439− l.selectedItem = item.id
441+ if renderedItem.start >= start && renderedItem.start <= end {
442+ l.selectedItem = renderedItem.id
440443 return l.render()
441444 }
442445 }
@@ -452,19 +455,23 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
452455 if inx == ItemNotFound {
453456 return nil
454457 }
455− item, ok := l.renderedItems.Get(l.items[inx].ID())
458+ item, ok := l.items.Get(inx)
459+ if !ok {
460+ continue
461+ }
462+ renderedItem, ok := l.renderedItems.Get(item.ID())
456463 if !ok {
457464 continue
458465 }
459466
460467 // If the item is bigger than the viewport, select it
461− if item.start <= start && item.end >= end {
462− l.selectedItem = item.id
468+ if renderedItem.start <= start && renderedItem.end >= end {
469+ l.selectedItem = renderedItem.id
463470 return l.render()
464471 }
465472 // item is in the view
466− if item.end >= start && item.end <= end {
467− l.selectedItem = item.id
473+ if renderedItem.end >= start && renderedItem.end <= end {
474+ l.selectedItem = renderedItem.id
468475 return l.render()
469476 }
470477 }
@@ -475,36 +482,51 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
475482func (l *list[T]) selectFirstItem() {
476483 inx := l.firstSelectableItemBelow(-1)
477484 if inx != ItemNotFound {
478− l.selectedItem = l.items[inx].ID()
485+ item, ok := l.items.Get(inx)
486+ if ok {
487+ l.selectedItem = item.ID()
488+ }
479489 }
480490}
481491
482492func (l *list[T]) selectLastItem() {
483− inx := l.firstSelectableItemAbove(len(l.items))
493+ inx := l.firstSelectableItemAbove(l.items.Len())
484494 if inx != ItemNotFound {
485− l.selectedItem = l.items[inx].ID()
495+ item, ok := l.items.Get(inx)
496+ if ok {
497+ l.selectedItem = item.ID()
498+ }
486499 }
487500}
488501
489502func (l *list[T]) firstSelectableItemAbove(inx int) int {
490503 for i := inx - 1; i >= 0; i-- {
491− if _, ok := any(l.items[i]).(layout.Focusable); ok {
504+ item, ok := l.items.Get(i)
505+ if !ok {
506+ continue
507+ }
508+ if _, ok := any(item).(layout.Focusable); ok {
492509 return i
493510 }
494511 }
495512 if inx == 0 && l.wrap {
496− return l.firstSelectableItemAbove(len(l.items))
513+ return l.firstSelectableItemAbove(l.items.Len())
497514 }
498515 return ItemNotFound
499516}
500517
501518func (l *list[T]) firstSelectableItemBelow(inx int) int {
502− for i := inx + 1; i < len(l.items); i++ {
503− if _, ok := any(l.items[i]).(layout.Focusable); ok {
519+ itemsLen := l.items.Len()
520+ for i := inx + 1; i < itemsLen; i++ {
521+ item, ok := l.items.Get(i)
522+ if !ok {
523+ continue
524+ }
525+ if _, ok := any(item).(layout.Focusable); ok {
504526 return i
505527 }
506528 }
507− if inx == len(l.items)-1 && l.wrap {
529+ if inx == itemsLen-1 && l.wrap {
508530 return l.firstSelectableItemBelow(-1)
509531 }
510532 return ItemNotFound
@@ -515,7 +537,7 @@ func (l *list[T]) focusSelectedItem() tea.Cmd {
515537 return nil
516538 }
517539 var cmds []tea.Cmd
518− for _, item := range l.items {
540+ for _, item := range l.items.Slice() {
519541 if f, ok := any(item).(layout.Focusable); ok {
520542 if item.ID() == l.selectedItem && !f.IsFocused() {
521543 cmds = append(cmds, f.Focus())
@@ -534,7 +556,7 @@ func (l *list[T]) blurSelectedItem() tea.Cmd {
534556 return nil
535557 }
536558 var cmds []tea.Cmd
537− for _, item := range l.items {
559+ for _, item := range l.items.Slice() {
538560 if f, ok := any(item).(layout.Focusable); ok {
539561 if item.ID() == l.selectedItem && f.IsFocused() {
540562 cmds = append(cmds, f.Blur())
@@ -549,7 +571,8 @@ func (l *list[T]) blurSelectedItem() tea.Cmd {
549571// returns the last index
550572func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
551573 currentContentHeight := lipgloss.Height(l.rendered) - 1
552− for i := startInx; i < len(l.items); i++ {
574+ itemsLen := l.items.Len()
575+ for i := startInx; i < itemsLen; i++ {
553576 if currentContentHeight >= l.height && limitHeight {
554577 return i
555578 }
@@ -557,10 +580,13 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
557580 inx := i
558581
559582 if l.direction != DirectionForward {
560− inx = (len(l.items) - 1) - i
583+ inx = (itemsLen - 1) - i
561584 }
562585
563− item := l.items[inx]
586+ item, ok := l.items.Get(inx)
587+ if !ok {
588+ continue
589+ }
564590 var rItem renderedItem
565591 if cache, ok := l.renderedItems.Get(item.ID()); ok {
566592 rItem = cache
@@ -571,7 +597,7 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
571597 l.renderedItems.Set(item.ID(), rItem)
572598 }
573599 gap := l.gap + 1
574− if inx == len(l.items)-1 {
600+ if inx == itemsLen-1 {
575601 gap = 0
576602 }
577603
@@ -582,7 +608,7 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
582608 }
583609 currentContentHeight = rItem.end + 1 + l.gap
584610 }
585− return len(l.items)
611+ return itemsLen
586612}
587613
588614func (l *list[T]) renderItem(item Item) renderedItem {
@@ -602,9 +628,9 @@ func (l *list[T]) AppendItem(item T) tea.Cmd {
602628 cmds = append(cmds, cmd)
603629 }
604630
605− l.items = append(l.items, item)
631+ l.items.Append(item)
606632 l.indexMap = csync.NewMap[string, int]()
607− for inx, item := range l.items {
633+ for inx, item := range l.items.Slice() {
608634 l.indexMap.Set(item.ID(), inx)
609635 }
610636 if l.width > 0 && l.height > 0 {
@@ -627,7 +653,7 @@ func (l *list[T]) AppendItem(item T) tea.Cmd {
627653 newItem, ok := l.renderedItems.Get(item.ID())
628654 if ok {
629655 newLines := newItem.height
630− if len(l.items) > 1 {
656+ if l.items.Len() > 1 {
631657 newLines += l.gap
632658 }
633659 l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
@@ -649,15 +675,20 @@ func (l *list[T]) DeleteItem(id string) tea.Cmd {
649675 if !ok {
650676 return nil
651677 }
652− l.items = slices.Delete(l.items, inx, inx+1)
678+ l.items.Delete(inx)
653679 l.renderedItems.Del(id)
654− for inx, item := range l.items {
680+ for inx, item := range l.items.Slice() {
655681 l.indexMap.Set(item.ID(), inx)
656682 }
657683
658684 if l.selectedItem == id {
659685 if inx > 0 {
660− l.selectedItem = l.items[inx-1].ID()
686+ item, ok := l.items.Get(inx - 1)
687+ if ok {
688+ l.selectedItem = item.ID()
689+ } else {
690+ l.selectedItem = ""
691+ }
661692 } else {
662693 l.selectedItem = ""
663694 }
@@ -711,7 +742,7 @@ func (l *list[T]) IsFocused() bool {
711742
712743// Items implements List.
713744func (l *list[T]) Items() []T {
714− return l.items
745+ return l.items.Slice()
715746}
716747
717748func (l *list[T]) incrementOffset(n int) {
@@ -764,9 +795,9 @@ func (l *list[T]) PrependItem(item T) tea.Cmd {
764795 cmds := []tea.Cmd{
765796 item.Init(),
766797 }
767− l.items = append([]T{item}, l.items...)
798+ l.items.Prepend(item)
768799 l.indexMap = csync.NewMap[string, int]()
769− for inx, item := range l.items {
800+ for inx, item := range l.items.Slice() {
770801 l.indexMap.Set(item.ID(), inx)
771802 }
772803 if l.width > 0 && l.height > 0 {
@@ -783,7 +814,7 @@ func (l *list[T]) PrependItem(item T) tea.Cmd {
783814 newItem, ok := l.renderedItems.Get(item.ID())
784815 if ok {
785816 newLines := newItem.height
786− if len(l.items) > 1 {
817+ if l.items.Len() > 1 {
787818 newLines += l.gap
788819 }
789820 l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
@@ -817,7 +848,10 @@ func (l *list[T]) SelectItemAbove() tea.Cmd {
817848 }
818849
819850 }
820− item := l.items[newIndex]
851+ item, ok := l.items.Get(newIndex)
852+ if !ok {
853+ return nil
854+ }
821855 l.selectedItem = item.ID()
822856 l.movingByItem = true
823857 renderCmd := l.render()
@@ -839,7 +873,10 @@ func (l *list[T]) SelectItemBelow() tea.Cmd {
839873 // no item above
840874 return nil
841875 }
842− item := l.items[newIndex]
876+ item, ok := l.items.Get(newIndex)
877+ if !ok {
878+ return nil
879+ }
843880 l.selectedItem = item.ID()
844881 l.movingByItem = true
845882 return l.render()
@@ -851,18 +888,21 @@ func (l *list[T]) SelectedItem() *T {
851888 if !ok {
852889 return nil
853890 }
854− if inx > len(l.items)-1 {
891+ if inx > l.items.Len()-1 {
892+ return nil
893+ }
894+ item, ok := l.items.Get(inx)
895+ if !ok {
855896 return nil
856897 }
857− item := l.items[inx]
858898 return &item
859899}
860900
861901// SetItems implements List.
862902func (l *list[T]) SetItems(items []T) tea.Cmd {
863− l.items = items
903+ l.items.SetSlice(items)
864904 var cmds []tea.Cmd
865− for inx, item := range l.items {
905+ for inx, item := range l.items.Slice() {
866906 if i, ok := any(item).(Indexable); ok {
867907 i.SetIndex(inx)
868908 }
@@ -885,7 +925,7 @@ func (l *list[T]) reset(selectedItem string) tea.Cmd {
885925 l.selectedItem = selectedItem
886926 l.indexMap = csync.NewMap[string, int]()
887927 l.renderedItems = csync.NewMap[string, renderedItem]()
888− for inx, item := range l.items {
928+ for inx, item := range l.items.Slice() {
889929 l.indexMap.Set(item.ID(), inx)
890930 if l.width > 0 && l.height > 0 {
891931 cmds = append(cmds, item.SetSize(l.width, l.height))
@@ -911,7 +951,7 @@ func (l *list[T]) SetSize(width int, height int) tea.Cmd {
911951func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
912952 var cmds []tea.Cmd
913953 if inx, ok := l.indexMap.Get(id); ok {
914− l.items[inx] = item
954+ l.items.Set(inx, item)
915955 oldItem, hasOldItem := l.renderedItems.Get(id)
916956 oldPosition := l.offset
917957 if l.direction == DirectionBackward {
@@ -928,7 +968,7 @@ func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
928968 if hasOldItem && l.direction == DirectionBackward {
929969 // if we are the last item and there is no offset
930970 // make sure to go to the bottom
931− if inx == len(l.items)-1 && l.offset == 0 {
971+ if inx == l.items.Len()-1 && l.offset == 0 {
932972 cmd = l.GoToBottom()
933973 if cmd != nil {
934974 cmds = append(cmds, cmd)