@@ -1,7 +1,6 @@
1package list
1package list
7 "github.com/charmbracelet/bubbles/v2/key"
6 "github.com/charmbracelet/bubbles/v2/key"
@@ -86,7 +85,7 @@ type list[T Item] struct {
86 offset int
85 offset int
88 indexMap *csync.Map[string, int]
87 indexMap *csync.Map[string, int]
89− items []T
88+ items *csync.Slice[T]
91 renderedItems *csync.Map[string, renderedItem]
90 renderedItems *csync.Map[string, renderedItem]
@@ -164,7 +163,7 @@ func New[T Item](items []T, opts ...ListOption) List[T] {
164 keyMap: DefaultKeyMap(),
163 keyMap: DefaultKeyMap(),
165 focused: true,
164 focused: true,
167− items: items,
166+ items: csync.NewSliceFrom(items),
168 indexMap: csync.NewMap[string, int](),
167 indexMap: csync.NewMap[string, int](),
169 renderedItems: csync.NewMap[string, renderedItem](),
168 renderedItems: csync.NewMap[string, renderedItem](),
@@ -191,7 +190,7 @@ func (l *list[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
191 switch msg := msg.(type) {
190 switch msg := msg.(type) {
192 case anim.StepMsg:
191 case anim.StepMsg:
193 var cmds []tea.Cmd
192 var cmds []tea.Cmd
194− for _, item := range l.items {
193+ for _, item := range l.items.Slice() {
195 if i, ok := any(item).(HasAnim); ok && i.Spinning() {
194 if i, ok := any(item).(HasAnim); ok && i.Spinning() {
196 updated, cmd := i.Update(msg)
195 updated, cmd := i.Update(msg)
197 cmds = append(cmds, cmd)
196 cmds = append(cmds, cmd)
@@ -267,7 +266,7 @@ func (l *list[T]) viewPosition() (int, int) {
268func (l *list[T]) recalculateItemPositions() {
267func (l *list[T]) recalculateItemPositions() {
269 currentContentHeight := 0
268 currentContentHeight := 0
270− for _, item := range l.items {
269+ for _, item := range l.items.Slice() {
271 rItem, ok := l.renderedItems.Get(item.ID())
270 rItem, ok := l.renderedItems.Get(item.ID())
@@ -280,7 +279,7 @@ func (l *list[T]) recalculateItemPositions() {
282func (l *list[T]) render() tea.Cmd {
281func (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 {
284 return nil
283 return nil
286 l.setDefaultSelected()
285 l.setDefaultSelected()
@@ -424,19 +423,23 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
424 if inx == ItemNotFound {
423 if inx == ItemNotFound {
425 return nil
424 return nil
427− item, ok := l.renderedItems.Get(l.items[inx].ID())
426+ item, ok := l.items.Get(inx)
430+ renderedItem, ok := l.renderedItems.Get(item.ID())
432 // If the item is bigger than the viewport, select it
435 // If the item is bigger than the viewport, select it
433− if item.start <= start && item.end >= end {
436+ if renderedItem.start <= start && renderedItem.end >= end {
434− l.selectedItem = item.id
437+ l.selectedItem = renderedItem.id
435 return l.render()
438 return l.render()
437 // item is in the view
440 // item is in the view
438− if item.start >= start && item.start <= end {
441+ if renderedItem.start >= start && renderedItem.start <= end {
439− l.selectedItem = item.id
442+ l.selectedItem = renderedItem.id
440 return l.render()
443 return l.render()
@@ -452,19 +455,23 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
452 if inx == ItemNotFound {
455 if inx == ItemNotFound {
453 return nil
456 return nil
455− item, ok := l.renderedItems.Get(l.items[inx].ID())
458+ item, ok := l.items.Get(inx)
462+ renderedItem, ok := l.renderedItems.Get(item.ID())
460 // If the item is bigger than the viewport, select it
467 // If the item is bigger than the viewport, select it
461− if item.start <= start && item.end >= end {
468+ if renderedItem.start <= start && renderedItem.end >= end {
462− l.selectedItem = item.id
469+ l.selectedItem = renderedItem.id
463 return l.render()
470 return l.render()
465 // item is in the view
472 // item is in the view
466− if item.end >= start && item.end <= end {
473+ if renderedItem.end >= start && renderedItem.end <= end {
467− l.selectedItem = item.id
474+ l.selectedItem = renderedItem.id
468 return l.render()
475 return l.render()
@@ -475,36 +482,51 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
475func (l *list[T]) selectFirstItem() {
482func (l *list[T]) selectFirstItem() {
476 inx := l.firstSelectableItemBelow(-1)
483 inx := l.firstSelectableItemBelow(-1)
477 if inx != ItemNotFound {
484 if inx != ItemNotFound {
485+ item, ok := l.items.Get(inx)
478− l.selectedItem = l.items[inx].ID()
487+ l.selectedItem = item.ID()
482func (l *list[T]) selectLastItem() {
492func (l *list[T]) selectLastItem() {
483− inx := l.firstSelectableItemAbove(len(l.items))
493+ inx := l.firstSelectableItemAbove(l.items.Len())
484 if inx != ItemNotFound {
494 if inx != ItemNotFound {
495+ item, ok := l.items.Get(inx)
485− l.selectedItem = l.items[inx].ID()
497+ l.selectedItem = item.ID()
489func (l *list[T]) firstSelectableItemAbove(inx int) int {
502func (l *list[T]) firstSelectableItemAbove(inx int) int {
490 for i := inx - 1; i >= 0; i-- {
503 for i := inx - 1; i >= 0; i-- {
504+ item, ok := l.items.Get(i)
491− if _, ok := any(l.items[i]).(layout.Focusable); ok {
508+ if _, ok := any(item).(layout.Focusable); ok {
495 if inx == 0 && l.wrap {
512 if inx == 0 && l.wrap {
496− return l.firstSelectableItemAbove(len(l.items))
513+ return l.firstSelectableItemAbove(l.items.Len())
498 return ItemNotFound
515 return ItemNotFound
501func (l *list[T]) firstSelectableItemBelow(inx int) int {
518func (l *list[T]) firstSelectableItemBelow(inx int) int {
519+ itemsLen := l.items.Len()
502− for i := inx + 1; i < len(l.items); i++ {
520+ for i := inx + 1; i < itemsLen; i++ {
521+ item, ok := l.items.Get(i)
503− if _, ok := any(l.items[i]).(layout.Focusable); ok {
525+ if _, ok := any(item).(layout.Focusable); ok {
507− if inx == len(l.items)-1 && l.wrap {
529+ if inx == itemsLen-1 && l.wrap {
508 return l.firstSelectableItemBelow(-1)
530 return l.firstSelectableItemBelow(-1)
510 return ItemNotFound
532 return ItemNotFound
@@ -515,7 +537,7 @@ func (l *list[T]) focusSelectedItem() tea.Cmd {
515 return nil
537 return nil
517 var cmds []tea.Cmd
539 var cmds []tea.Cmd
518− for _, item := range l.items {
540+ for _, item := range l.items.Slice() {
519 if f, ok := any(item).(layout.Focusable); ok {
541 if f, ok := any(item).(layout.Focusable); ok {
520 if item.ID() == l.selectedItem && !f.IsFocused() {
542 if item.ID() == l.selectedItem && !f.IsFocused() {
521 cmds = append(cmds, f.Focus())
543 cmds = append(cmds, f.Focus())
@@ -534,7 +556,7 @@ func (l *list[T]) blurSelectedItem() tea.Cmd {
534 return nil
556 return nil
536 var cmds []tea.Cmd
558 var cmds []tea.Cmd
537− for _, item := range l.items {
559+ for _, item := range l.items.Slice() {
538 if f, ok := any(item).(layout.Focusable); ok {
560 if f, ok := any(item).(layout.Focusable); ok {
539 if item.ID() == l.selectedItem && f.IsFocused() {
561 if item.ID() == l.selectedItem && f.IsFocused() {
540 cmds = append(cmds, f.Blur())
562 cmds = append(cmds, f.Blur())
@@ -549,7 +571,8 @@ func (l *list[T]) blurSelectedItem() tea.Cmd {
549// returns the last index
571// returns the last index
550func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
572func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
551 currentContentHeight := lipgloss.Height(l.rendered) - 1
573 currentContentHeight := lipgloss.Height(l.rendered) - 1
574+ itemsLen := l.items.Len()
552− for i := startInx; i < len(l.items); i++ {
575+ for i := startInx; i < itemsLen; i++ {
553 if currentContentHeight >= l.height && limitHeight {
576 if currentContentHeight >= l.height && limitHeight {
@@ -557,10 +580,13 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
559 if l.direction != DirectionForward {
582 if l.direction != DirectionForward {
560− inx = (len(l.items) - 1) - i
583+ inx = (itemsLen - 1) - i
563− item := l.items[inx]
586+ item, ok := l.items.Get(inx)
564 var rItem renderedItem
590 var rItem renderedItem
565 if cache, ok := l.renderedItems.Get(item.ID()); ok {
591 if cache, ok := l.renderedItems.Get(item.ID()); ok {
566 rItem = cache
592 rItem = cache
@@ -571,7 +597,7 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
571 l.renderedItems.Set(item.ID(), rItem)
597 l.renderedItems.Set(item.ID(), rItem)
573 gap := l.gap + 1
599 gap := l.gap + 1
574− if inx == len(l.items)-1 {
600+ if inx == itemsLen-1 {
@@ -582,7 +608,7 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
583 currentContentHeight = rItem.end + 1 + l.gap
609 currentContentHeight = rItem.end + 1 + l.gap
585− return len(l.items)
611+ return itemsLen
588func (l *list[T]) renderItem(item Item) renderedItem {
614func (l *list[T]) renderItem(item Item) renderedItem {
@@ -602,9 +628,9 @@ func (l *list[T]) AppendItem(item T) tea.Cmd {
602 cmds = append(cmds, cmd)
628 cmds = append(cmds, cmd)
605− l.items = append(l.items, item)
631+ l.items.Append(item)
606 l.indexMap = csync.NewMap[string, int]()
632 l.indexMap = csync.NewMap[string, int]()
607− for inx, item := range l.items {
633+ for inx, item := range l.items.Slice() {
608 l.indexMap.Set(item.ID(), inx)
634 l.indexMap.Set(item.ID(), inx)
610 if l.width > 0 && l.height > 0 {
636 if l.width > 0 && l.height > 0 {
@@ -627,7 +653,7 @@ func (l *list[T]) AppendItem(item T) tea.Cmd {
627 newItem, ok := l.renderedItems.Get(item.ID())
653 newItem, ok := l.renderedItems.Get(item.ID())
629 newLines := newItem.height
655 newLines := newItem.height
630− if len(l.items) > 1 {
656+ if l.items.Len() > 1 {
631 newLines += l.gap
657 newLines += l.gap
633 l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
659 l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
@@ -649,15 +675,20 @@ func (l *list[T]) DeleteItem(id string) tea.Cmd {
650 return nil
676 return nil
652− l.items = slices.Delete(l.items, inx, inx+1)
678+ l.items.Delete(inx)
653 l.renderedItems.Del(id)
679 l.renderedItems.Del(id)
654− for inx, item := range l.items {
680+ for inx, item := range l.items.Slice() {
655 l.indexMap.Set(item.ID(), inx)
681 l.indexMap.Set(item.ID(), inx)
658 if l.selectedItem == id {
684 if l.selectedItem == id {
659 if inx > 0 {
685 if inx > 0 {
660− l.selectedItem = l.items[inx-1].ID()
686+ item, ok := l.items.Get(inx - 1)
688+ l.selectedItem = item.ID()
662 l.selectedItem = ""
693 l.selectedItem = ""
@@ -711,7 +742,7 @@ func (l *list[T]) IsFocused() bool {
712// Items implements List.
743// Items implements List.
713func (l *list[T]) Items() []T {
744func (l *list[T]) Items() []T {
714− return l.items
745+ return l.items.Slice()
717func (l *list[T]) incrementOffset(n int) {
748func (l *list[T]) incrementOffset(n int) {
@@ -764,9 +795,9 @@ func (l *list[T]) PrependItem(item T) tea.Cmd {
764 cmds := []tea.Cmd{
795 cmds := []tea.Cmd{
765 item.Init(),
796 item.Init(),
767− l.items = append([]T{item}, l.items...)
798+ l.items.Prepend(item)
768 l.indexMap = csync.NewMap[string, int]()
799 l.indexMap = csync.NewMap[string, int]()
769− for inx, item := range l.items {
800+ for inx, item := range l.items.Slice() {
770 l.indexMap.Set(item.ID(), inx)
801 l.indexMap.Set(item.ID(), inx)
772 if l.width > 0 && l.height > 0 {
803 if l.width > 0 && l.height > 0 {
@@ -783,7 +814,7 @@ func (l *list[T]) PrependItem(item T) tea.Cmd {
783 newItem, ok := l.renderedItems.Get(item.ID())
814 newItem, ok := l.renderedItems.Get(item.ID())
785 newLines := newItem.height
816 newLines := newItem.height
786− if len(l.items) > 1 {
817+ if l.items.Len() > 1 {
787 newLines += l.gap
818 newLines += l.gap
789 l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
820 l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
@@ -817,7 +848,10 @@ func (l *list[T]) SelectItemAbove() tea.Cmd {
820− item := l.items[newIndex]
851+ item, ok := l.items.Get(newIndex)
821 l.selectedItem = item.ID()
855 l.selectedItem = item.ID()
822 l.movingByItem = true
856 l.movingByItem = true
823 renderCmd := l.render()
857 renderCmd := l.render()
@@ -839,7 +873,10 @@ func (l *list[T]) SelectItemBelow() tea.Cmd {
839 // no item above
873 // no item above
840 return nil
874 return nil
842− item := l.items[newIndex]
876+ item, ok := l.items.Get(newIndex)
843 l.selectedItem = item.ID()
880 l.selectedItem = item.ID()
844 l.movingByItem = true
881 l.movingByItem = true
845 return l.render()
882 return l.render()
@@ -851,18 +888,21 @@ func (l *list[T]) SelectedItem() *T {
852 return nil
889 return nil
854− if inx > len(l.items)-1 {
891+ if inx > l.items.Len()-1 {
894+ item, ok := l.items.Get(inx)
855 return nil
896 return nil
857− item := l.items[inx]
858 return &item
898 return &item
861// SetItems implements List.
901// SetItems implements List.
862func (l *list[T]) SetItems(items []T) tea.Cmd {
902func (l *list[T]) SetItems(items []T) tea.Cmd {
863− l.items = items
903+ l.items.SetSlice(items)
864 var cmds []tea.Cmd
904 var cmds []tea.Cmd
865− for inx, item := range l.items {
905+ for inx, item := range l.items.Slice() {
866 if i, ok := any(item).(Indexable); ok {
906 if i, ok := any(item).(Indexable); ok {
867 i.SetIndex(inx)
907 i.SetIndex(inx)
@@ -885,7 +925,7 @@ func (l *list[T]) reset(selectedItem string) tea.Cmd {
885 l.selectedItem = selectedItem
925 l.selectedItem = selectedItem
886 l.indexMap = csync.NewMap[string, int]()
926 l.indexMap = csync.NewMap[string, int]()
887 l.renderedItems = csync.NewMap[string, renderedItem]()
927 l.renderedItems = csync.NewMap[string, renderedItem]()
888− for inx, item := range l.items {
928+ for inx, item := range l.items.Slice() {
889 l.indexMap.Set(item.ID(), inx)
929 l.indexMap.Set(item.ID(), inx)
890 if l.width > 0 && l.height > 0 {
930 if l.width > 0 && l.height > 0 {
891 cmds = append(cmds, item.SetSize(l.width, l.height))
931 cmds = append(cmds, item.SetSize(l.width, l.height))
@@ -911,7 +951,7 @@ func (l *list[T]) SetSize(width int, height int) tea.Cmd {
911func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
951func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
912 var cmds []tea.Cmd
952 var cmds []tea.Cmd
913 if inx, ok := l.indexMap.Get(id); ok {
953 if inx, ok := l.indexMap.Get(id); ok {
914− l.items[inx] = item
954+ l.items.Set(inx, item)
915 oldItem, hasOldItem := l.renderedItems.Get(id)
955 oldItem, hasOldItem := l.renderedItems.Get(id)
916 oldPosition := l.offset
956 oldPosition := l.offset
917 if l.direction == DirectionBackward {
957 if l.direction == DirectionBackward {
@@ -928,7 +968,7 @@ func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
928 if hasOldItem && l.direction == DirectionBackward {
968 if hasOldItem && l.direction == DirectionBackward {
929 // if we are the last item and there is no offset
969 // if we are the last item and there is no offset
930 // make sure to go to the bottom
970 // 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 {
932 cmd = l.GoToBottom()
972 cmd = l.GoToBottom()
933 if cmd != nil {
973 if cmd != nil {
934 cmds = append(cmds, cmd)
974 cmds = append(cmds, cmd)