@@ -6,6 +6,7 @@ import (
66
77 "github.com/charmbracelet/bubbles/v2/key"
88 tea "github.com/charmbracelet/bubbletea/v2"
9+ "github.com/charmbracelet/crush/internal/csync"
910 "github.com/charmbracelet/crush/internal/tui/components/anim"
1011 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
1112 "github.com/charmbracelet/crush/internal/tui/styles"
@@ -84,10 +85,10 @@ type list[T Item] struct {
8485
8586 offset int
8687
87− indexMap map[string]int
88+ indexMap *csync.Map[string, int]
8889 items []T
8990
90− renderedItems map[string]renderedItem
91+ renderedItems *csync.Map[string, renderedItem]
9192
9293 rendered string
9394
@@ -164,8 +165,8 @@ func New[T Item](items []T, opts ...ListOption) List[T] {
164165 focused: true,
165166 },
166167 items: items,
167− indexMap: make(map[string]int),
168− renderedItems: map[string]renderedItem{},
168+ indexMap: csync.NewMap[string, int](),
169+ renderedItems: csync.NewMap[string, renderedItem](),
169170 }
170171 for _, opt := range opts {
171172 opt(list.confOptions)
@@ -175,7 +176,7 @@ func New[T Item](items []T, opts ...ListOption) List[T] {
175176 if i, ok := any(item).(Indexable); ok {
176177 i.SetIndex(inx)
177178 }
178− list.indexMap[item.ID()] = inx
179+ list.indexMap.Set(item.ID(), inx)
179180 }
180181 return list
181182}
@@ -267,13 +268,13 @@ func (l *list[T]) viewPosition() (int, int) {
267268func (l *list[T]) recalculateItemPositions() {
268269 currentContentHeight := 0
269270 for _, item := range l.items {
270− rItem, ok := l.renderedItems[item.ID()]
271+ rItem, ok := l.renderedItems.Get(item.ID())
271272 if !ok {
272273 continue
273274 }
274275 rItem.start = currentContentHeight
275276 rItem.end = currentContentHeight + rItem.height - 1
276− l.renderedItems[item.ID()] = rItem
277+ l.renderedItems.Set(item.ID(), rItem)
277278 currentContentHeight = rItem.end + 1 + l.gap
278279 }
279280}
@@ -337,7 +338,7 @@ func (l *list[T]) setDefaultSelected() {
337338}
338339
339340func (l *list[T]) scrollToSelection() {
340− rItem, ok := l.renderedItems[l.selectedItem]
341+ rItem, ok := l.renderedItems.Get(l.selectedItem)
341342 if !ok {
342343 l.selectedItem = ""
343344 l.setDefaultSelected()
@@ -395,7 +396,7 @@ func (l *list[T]) scrollToSelection() {
395396}
396397
397398func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
398− rItem, ok := l.renderedItems[l.selectedItem]
399+ rItem, ok := l.renderedItems.Get(l.selectedItem)
399400 if !ok {
400401 return nil
401402 }
@@ -414,13 +415,16 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
414415 if itemMiddle < start {
415416 // select the first item in the viewport
416417 // the item is most likely an item coming after this item
417− inx := l.indexMap[rItem.id]
418+ inx, ok := l.indexMap.Get(rItem.id)
419+ if !ok {
420+ return nil
421+ }
418422 for {
419423 inx = l.firstSelectableItemBelow(inx)
420424 if inx == ItemNotFound {
421425 return nil
422426 }
423− item, ok := l.renderedItems[l.items[inx].ID()]
427+ item, ok := l.renderedItems.Get(l.items[inx].ID())
424428 if !ok {
425429 continue
426430 }
@@ -439,13 +443,16 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
439443 } else if itemMiddle > end {
440444 // select the first item in the viewport
441445 // the item is most likely an item coming after this item
442− inx := l.indexMap[rItem.id]
446+ inx, ok := l.indexMap.Get(rItem.id)
447+ if !ok {
448+ return nil
449+ }
443450 for {
444451 inx = l.firstSelectableItemAbove(inx)
445452 if inx == ItemNotFound {
446453 return nil
447454 }
448− item, ok := l.renderedItems[l.items[inx].ID()]
455+ item, ok := l.renderedItems.Get(l.items[inx].ID())
449456 if !ok {
450457 continue
451458 }
@@ -512,10 +519,10 @@ func (l *list[T]) focusSelectedItem() tea.Cmd {
512519 if f, ok := any(item).(layout.Focusable); ok {
513520 if item.ID() == l.selectedItem && !f.IsFocused() {
514521 cmds = append(cmds, f.Focus())
515− delete(l.renderedItems, item.ID())
522+ l.renderedItems.Del(item.ID())
516523 } else if item.ID() != l.selectedItem && f.IsFocused() {
517524 cmds = append(cmds, f.Blur())
518− delete(l.renderedItems, item.ID())
525+ l.renderedItems.Del(item.ID())
519526 }
520527 }
521528 }
@@ -531,7 +538,7 @@ func (l *list[T]) blurSelectedItem() tea.Cmd {
531538 if f, ok := any(item).(layout.Focusable); ok {
532539 if item.ID() == l.selectedItem && f.IsFocused() {
533540 cmds = append(cmds, f.Blur())
534− delete(l.renderedItems, item.ID())
541+ l.renderedItems.Del(item.ID())
535542 }
536543 }
537544 }
@@ -555,13 +562,13 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
555562
556563 item := l.items[inx]
557564 var rItem renderedItem
558− if cache, ok := l.renderedItems[item.ID()]; ok {
565+ if cache, ok := l.renderedItems.Get(item.ID()); ok {
559566 rItem = cache
560567 } else {
561568 rItem = l.renderItem(item)
562569 rItem.start = currentContentHeight
563570 rItem.end = currentContentHeight + rItem.height - 1
564− l.renderedItems[item.ID()] = rItem
571+ l.renderedItems.Set(item.ID(), rItem)
565572 }
566573 gap := l.gap + 1
567574 if inx == len(l.items)-1 {
@@ -596,9 +603,9 @@ func (l *list[T]) AppendItem(item T) tea.Cmd {
596603 }
597604
598605 l.items = append(l.items, item)
599− l.indexMap = make(map[string]int)
606+ l.indexMap = csync.NewMap[string, int]()
600607 for inx, item := range l.items {
601− l.indexMap[item.ID()] = inx
608+ l.indexMap.Set(item.ID(), inx)
602609 }
603610 if l.width > 0 && l.height > 0 {
604611 cmd = item.SetSize(l.width, l.height)
@@ -617,12 +624,14 @@ func (l *list[T]) AppendItem(item T) tea.Cmd {
617624 cmds = append(cmds, cmd)
618625 }
619626 } else {
620− newItem := l.renderedItems[item.ID()]
621− newLines := newItem.height
622− if len(l.items) > 1 {
623− newLines += l.gap
627+ newItem, ok := l.renderedItems.Get(item.ID())
628+ if ok {
629+ newLines := newItem.height
630+ if len(l.items) > 1 {
631+ newLines += l.gap
632+ }
633+ l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
624634 }
625− l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
626635 }
627636 }
628637 return tea.Sequence(cmds...)
@@ -636,11 +645,14 @@ func (l *list[T]) Blur() tea.Cmd {
636645
637646// DeleteItem implements List.
638647func (l *list[T]) DeleteItem(id string) tea.Cmd {
639− inx := l.indexMap[id]
648+ inx, ok := l.indexMap.Get(id)
649+ if !ok {
650+ return nil
651+ }
640652 l.items = slices.Delete(l.items, inx, inx+1)
641− delete(l.renderedItems, id)
653+ l.renderedItems.Del(id)
642654 for inx, item := range l.items {
643− l.indexMap[item.ID()] = inx
655+ l.indexMap.Set(item.ID(), inx)
644656 }
645657
646658 if l.selectedItem == id {
@@ -753,9 +765,9 @@ func (l *list[T]) PrependItem(item T) tea.Cmd {
753765 item.Init(),
754766 }
755767 l.items = append([]T{item}, l.items...)
756− l.indexMap = make(map[string]int)
768+ l.indexMap = csync.NewMap[string, int]()
757769 for inx, item := range l.items {
758− l.indexMap[item.ID()] = inx
770+ l.indexMap.Set(item.ID(), inx)
759771 }
760772 if l.width > 0 && l.height > 0 {
761773 cmds = append(cmds, item.SetSize(l.width, l.height))
@@ -768,12 +780,14 @@ func (l *list[T]) PrependItem(item T) tea.Cmd {
768780 cmds = append(cmds, cmd)
769781 }
770782 } else {
771− newItem := l.renderedItems[item.ID()]
772− newLines := newItem.height
773− if len(l.items) > 1 {
774− newLines += l.gap
783+ newItem, ok := l.renderedItems.Get(item.ID())
784+ if ok {
785+ newLines := newItem.height
786+ if len(l.items) > 1 {
787+ newLines += l.gap
788+ }
789+ l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
775790 }
776− l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
777791 }
778792 }
779793 return tea.Batch(cmds...)
@@ -781,7 +795,7 @@ func (l *list[T]) PrependItem(item T) tea.Cmd {
781795
782796// SelectItemAbove implements List.
783797func (l *list[T]) SelectItemAbove() tea.Cmd {
784− inx, ok := l.indexMap[l.selectedItem]
798+ inx, ok := l.indexMap.Get(l.selectedItem)
785799 if !ok {
786800 return nil
787801 }
@@ -815,7 +829,7 @@ func (l *list[T]) SelectItemAbove() tea.Cmd {
815829
816830// SelectItemBelow implements List.
817831func (l *list[T]) SelectItemBelow() tea.Cmd {
818− inx, ok := l.indexMap[l.selectedItem]
832+ inx, ok := l.indexMap.Get(l.selectedItem)
819833 if !ok {
820834 return nil
821835 }
@@ -833,7 +847,7 @@ func (l *list[T]) SelectItemBelow() tea.Cmd {
833847
834848// SelectedItem implements List.
835849func (l *list[T]) SelectedItem() *T {
836− inx, ok := l.indexMap[l.selectedItem]
850+ inx, ok := l.indexMap.Get(l.selectedItem)
837851 if !ok {
838852 return nil
839853 }
@@ -869,10 +883,10 @@ func (l *list[T]) reset(selectedItem string) tea.Cmd {
869883 l.rendered = ""
870884 l.offset = 0
871885 l.selectedItem = selectedItem
872− l.indexMap = make(map[string]int)
873− l.renderedItems = make(map[string]renderedItem)
886+ l.indexMap = csync.NewMap[string, int]()
887+ l.renderedItems = csync.NewMap[string, renderedItem]()
874888 for inx, item := range l.items {
875− l.indexMap[item.ID()] = inx
889+ l.indexMap.Set(item.ID(), inx)
876890 if l.width > 0 && l.height > 0 {
877891 cmds = append(cmds, item.SetSize(l.width, l.height))
878892 }
@@ -896,22 +910,22 @@ func (l *list[T]) SetSize(width int, height int) tea.Cmd {
896910// UpdateItem implements List.
897911func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
898912 var cmds []tea.Cmd
899− if inx, ok := l.indexMap[id]; ok {
913+ if inx, ok := l.indexMap.Get(id); ok {
900914 l.items[inx] = item
901− oldItem := l.renderedItems[id]
915+ oldItem, hasOldItem := l.renderedItems.Get(id)
902916 oldPosition := l.offset
903917 if l.direction == DirectionBackward {
904918 oldPosition = (lipgloss.Height(l.rendered) - 1) - l.offset
905919 }
906920
907− delete(l.renderedItems, id)
921+ l.renderedItems.Del(id)
908922 cmd := l.render()
909923
910924 // need to check for nil because of sequence not handling nil
911925 if cmd != nil {
912926 cmds = append(cmds, cmd)
913927 }
914− if l.direction == DirectionBackward {
928+ if hasOldItem && l.direction == DirectionBackward {
915929 // if we are the last item and there is no offset
916930 // make sure to go to the bottom
917931 if inx == len(l.items)-1 && l.offset == 0 {
@@ -921,14 +935,18 @@ func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
921935 }
922936 // if the item is at least partially below the viewport
923937 } else if oldPosition < oldItem.end {
924− newItem := l.renderedItems[item.ID()]
938+ newItem, ok := l.renderedItems.Get(item.ID())
939+ if ok {
940+ newLines := newItem.height - oldItem.height
941+ l.offset = util.Clamp(l.offset+newLines, 0, lipgloss.Height(l.rendered)-1)
942+ }
943+ }
944+ } else if hasOldItem && l.offset > oldItem.start {
945+ newItem, ok := l.renderedItems.Get(item.ID())
946+ if ok {
925947 newLines := newItem.height - oldItem.height
926948 l.offset = util.Clamp(l.offset+newLines, 0, lipgloss.Height(l.rendered)-1)
927949 }
928− } else if l.offset > oldItem.start {
929− newItem := l.renderedItems[item.ID()]
930− newLines := newItem.height - oldItem.height
931− l.offset = util.Clamp(l.offset+newLines, 0, lipgloss.Height(l.rendered)-1)
932950 }
933951 }
934952 return tea.Sequence(cmds...)