cspell.json 🔗
@@ -1 +1 @@
  Kujtim Hoxha created
cspell.json                               |   0 
internal/csync/slices.go                  | 127 +++++++++++++++
internal/csync/slices_test.go             | 209 +++++++++++++++++++++++++
internal/tui/exp/list/filterable.go       |   2 
internal/tui/exp/list/filterable_group.go |   2 
internal/tui/exp/list/grouped.go          |   5 
internal/tui/exp/list/list.go             | 140 ++++++++++-----
internal/tui/exp/list/list_test.go        |  12 
8 files changed, 437 insertions(+), 60 deletions(-)
@@ -1 +1 @@
  @@ -2,6 +2,7 @@ package csync
 
 import (
 	"iter"
+	"slices"
 	"sync"
 )
 
@@ -34,3 +35,129 @@ func (s *LazySlice[K]) Seq() iter.Seq[K] {
 		}
 	}
 }
+
+// Slice is a thread-safe slice implementation that provides concurrent access.
+type Slice[T any] struct {
+	inner []T
+	mu    sync.RWMutex
+}
+
+// NewSlice creates a new thread-safe slice.
+func NewSlice[T any]() *Slice[T] {
+	return &Slice[T]{
+		inner: make([]T, 0),
+	}
+}
+
+// NewSliceFrom creates a new thread-safe slice from an existing slice.
+func NewSliceFrom[T any](s []T) *Slice[T] {
+	inner := make([]T, len(s))
+	copy(inner, s)
+	return &Slice[T]{
+		inner: inner,
+	}
+}
+
+// Append adds an element to the end of the slice.
+func (s *Slice[T]) Append(item T) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.inner = append(s.inner, item)
+}
+
+// Prepend adds an element to the beginning of the slice.
+func (s *Slice[T]) Prepend(item T) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.inner = append([]T{item}, s.inner...)
+}
+
+// Delete removes the element at the specified index.
+func (s *Slice[T]) Delete(index int) bool {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if index < 0 || index >= len(s.inner) {
+		return false
+	}
+	s.inner = slices.Delete(s.inner, index, index+1)
+	return true
+}
+
+// Get returns the element at the specified index.
+func (s *Slice[T]) Get(index int) (T, bool) {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+	var zero T
+	if index < 0 || index >= len(s.inner) {
+		return zero, false
+	}
+	return s.inner[index], true
+}
+
+// Set updates the element at the specified index.
+func (s *Slice[T]) Set(index int, item T) bool {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if index < 0 || index >= len(s.inner) {
+		return false
+	}
+	s.inner[index] = item
+	return true
+}
+
+// Len returns the number of elements in the slice.
+func (s *Slice[T]) Len() int {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+	return len(s.inner)
+}
+
+// Slice returns a copy of the underlying slice.
+func (s *Slice[T]) Slice() []T {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+	result := make([]T, len(s.inner))
+	copy(result, s.inner)
+	return result
+}
+
+// SetSlice replaces the entire slice with a new one.
+func (s *Slice[T]) SetSlice(items []T) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.inner = make([]T, len(items))
+	copy(s.inner, items)
+}
+
+// Clear removes all elements from the slice.
+func (s *Slice[T]) Clear() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.inner = s.inner[:0]
+}
+
+// Seq returns an iterator that yields elements from the slice.
+func (s *Slice[T]) Seq() iter.Seq[T] {
+	// Take a snapshot to avoid holding the lock during iteration
+	items := s.Slice()
+	return func(yield func(T) bool) {
+		for _, v := range items {
+			if !yield(v) {
+				return
+			}
+		}
+	}
+}
+
+// SeqWithIndex returns an iterator that yields index-value pairs from the slice.
+func (s *Slice[T]) SeqWithIndex() iter.Seq2[int, T] {
+	// Take a snapshot to avoid holding the lock during iteration
+	items := s.Slice()
+	return func(yield func(int, T) bool) {
+		for i, v := range items {
+			if !yield(i, v) {
+				return
+			}
+		}
+	}
+}
  @@ -1,11 +1,13 @@
 package csync
 
 import (
+	"sync"
 	"sync/atomic"
 	"testing"
 	"time"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestLazySlice_Seq(t *testing.T) {
@@ -85,3 +87,210 @@ func TestLazySlice_EarlyBreak(t *testing.T) {
 
 	assert.Equal(t, []string{"a", "b"}, result)
 }
+
+func TestSlice(t *testing.T) {
+	t.Run("NewSlice", func(t *testing.T) {
+		s := NewSlice[int]()
+		assert.Equal(t, 0, s.Len())
+	})
+
+	t.Run("NewSliceFrom", func(t *testing.T) {
+		original := []int{1, 2, 3}
+		s := NewSliceFrom(original)
+		assert.Equal(t, 3, s.Len())
+		
+		// Verify it's a copy, not a reference
+		original[0] = 999
+		val, ok := s.Get(0)
+		require.True(t, ok)
+		assert.Equal(t, 1, val)
+	})
+
+	t.Run("Append", func(t *testing.T) {
+		s := NewSlice[string]()
+		s.Append("hello")
+		s.Append("world")
+		
+		assert.Equal(t, 2, s.Len())
+		val, ok := s.Get(0)
+		require.True(t, ok)
+		assert.Equal(t, "hello", val)
+		
+		val, ok = s.Get(1)
+		require.True(t, ok)
+		assert.Equal(t, "world", val)
+	})
+
+	t.Run("Prepend", func(t *testing.T) {
+		s := NewSlice[string]()
+		s.Append("world")
+		s.Prepend("hello")
+		
+		assert.Equal(t, 2, s.Len())
+		val, ok := s.Get(0)
+		require.True(t, ok)
+		assert.Equal(t, "hello", val)
+		
+		val, ok = s.Get(1)
+		require.True(t, ok)
+		assert.Equal(t, "world", val)
+	})
+
+	t.Run("Delete", func(t *testing.T) {
+		s := NewSliceFrom([]int{1, 2, 3, 4, 5})
+		
+		// Delete middle element
+		ok := s.Delete(2)
+		assert.True(t, ok)
+		assert.Equal(t, 4, s.Len())
+		
+		expected := []int{1, 2, 4, 5}
+		actual := s.Slice()
+		assert.Equal(t, expected, actual)
+		
+		// Delete out of bounds
+		ok = s.Delete(10)
+		assert.False(t, ok)
+		assert.Equal(t, 4, s.Len())
+		
+		// Delete negative index
+		ok = s.Delete(-1)
+		assert.False(t, ok)
+		assert.Equal(t, 4, s.Len())
+	})
+
+	t.Run("Get", func(t *testing.T) {
+		s := NewSliceFrom([]string{"a", "b", "c"})
+		
+		val, ok := s.Get(1)
+		require.True(t, ok)
+		assert.Equal(t, "b", val)
+		
+		// Out of bounds
+		_, ok = s.Get(10)
+		assert.False(t, ok)
+		
+		// Negative index
+		_, ok = s.Get(-1)
+		assert.False(t, ok)
+	})
+
+	t.Run("Set", func(t *testing.T) {
+		s := NewSliceFrom([]string{"a", "b", "c"})
+		
+		ok := s.Set(1, "modified")
+		assert.True(t, ok)
+		
+		val, ok := s.Get(1)
+		require.True(t, ok)
+		assert.Equal(t, "modified", val)
+		
+		// Out of bounds
+		ok = s.Set(10, "invalid")
+		assert.False(t, ok)
+		
+		// Negative index
+		ok = s.Set(-1, "invalid")
+		assert.False(t, ok)
+	})
+
+	t.Run("SetSlice", func(t *testing.T) {
+		s := NewSlice[int]()
+		s.Append(1)
+		s.Append(2)
+		
+		newItems := []int{10, 20, 30}
+		s.SetSlice(newItems)
+		
+		assert.Equal(t, 3, s.Len())
+		assert.Equal(t, newItems, s.Slice())
+		
+		// Verify it's a copy
+		newItems[0] = 999
+		val, ok := s.Get(0)
+		require.True(t, ok)
+		assert.Equal(t, 10, val)
+	})
+
+	t.Run("Clear", func(t *testing.T) {
+		s := NewSliceFrom([]int{1, 2, 3})
+		assert.Equal(t, 3, s.Len())
+		
+		s.Clear()
+		assert.Equal(t, 0, s.Len())
+	})
+
+	t.Run("Slice", func(t *testing.T) {
+		original := []int{1, 2, 3}
+		s := NewSliceFrom(original)
+		
+		copy := s.Slice()
+		assert.Equal(t, original, copy)
+		
+		// Verify it's a copy
+		copy[0] = 999
+		val, ok := s.Get(0)
+		require.True(t, ok)
+		assert.Equal(t, 1, val)
+	})
+
+	t.Run("Seq", func(t *testing.T) {
+		s := NewSliceFrom([]int{1, 2, 3})
+		
+		var result []int
+		for v := range s.Seq() {
+			result = append(result, v)
+		}
+		
+		assert.Equal(t, []int{1, 2, 3}, result)
+	})
+
+	t.Run("SeqWithIndex", func(t *testing.T) {
+		s := NewSliceFrom([]string{"a", "b", "c"})
+		
+		var indices []int
+		var values []string
+		for i, v := range s.SeqWithIndex() {
+			indices = append(indices, i)
+			values = append(values, v)
+		}
+		
+		assert.Equal(t, []int{0, 1, 2}, indices)
+		assert.Equal(t, []string{"a", "b", "c"}, values)
+	})
+
+	t.Run("ConcurrentAccess", func(t *testing.T) {
+		s := NewSlice[int]()
+		const numGoroutines = 100
+		const itemsPerGoroutine = 10
+		
+		var wg sync.WaitGroup
+		
+		// Concurrent appends
+		for i := 0; i < numGoroutines; i++ {
+			wg.Add(1)
+			go func(start int) {
+				defer wg.Done()
+				for j := 0; j < itemsPerGoroutine; j++ {
+					s.Append(start*itemsPerGoroutine + j)
+				}
+			}(i)
+		}
+		
+		// Concurrent reads
+		for i := 0; i < numGoroutines; i++ {
+			wg.Add(1)
+			go func() {
+				defer wg.Done()
+				for j := 0; j < itemsPerGoroutine; j++ {
+					s.Len() // Just read the length
+				}
+			}()
+		}
+		
+		wg.Wait()
+		
+		// Should have all items
+		assert.Equal(t, numGoroutines*itemsPerGoroutine, s.Len())
+	})
+}
  @@ -97,7 +97,7 @@ func NewFilterableList[T FilterableItem](items []T, opts ...filterableListOption
 	f.list = New[T](items, f.listOptions...).(*list[T])
 
 	f.updateKeyMaps()
-	f.items = f.list.items
+	f.items = f.list.items.Slice()
 
 	if f.inputHidden {
 		return f
  @@ -179,7 +179,7 @@ func (f *filterableGroupList[T]) inputHeight() int {
 
 func (f *filterableGroupList[T]) Filter(query string) tea.Cmd {
 	var cmds []tea.Cmd
-	for _, item := range f.items {
+	for _, item := range f.items.Slice() {
 		if i, ok := any(item).(layout.Focusable); ok {
 			cmds = append(cmds, i.Blur())
 		}
  @@ -38,6 +38,7 @@ func NewGroupedList[T Item](groups []Group[T], opts ...ListOption) GroupedList[T
 			keyMap:    DefaultKeyMap(),
 			focused:   true,
 		},
+		items:         csync.NewSlice[Item](),
 		indexMap:      csync.NewMap[string, int](),
 		renderedItems: csync.NewMap[string, renderedItem](),
 	}
@@ -82,13 +83,13 @@ func (g *groupedList[T]) convertItems() {
 			items = append(items, g)
 		}
 	}
-	g.items = items
+	g.items.SetSlice(items)
 }
 
 func (g *groupedList[T]) SetGroups(groups []Group[T]) tea.Cmd {
 	g.groups = groups
 	g.convertItems()
-	return g.SetItems(g.items)
+	return g.SetItems(g.items.Slice())
 }
 
 func (g *groupedList[T]) Groups() []Group[T] {
  @@ -1,7 +1,6 @@
 package list
 
 import (
-	"slices"
 	"strings"
 
 	"github.com/charmbracelet/bubbles/v2/key"
@@ -86,7 +85,7 @@ type list[T Item] struct {
 	offset int
 
 	indexMap *csync.Map[string, int]
-	items    []T
+	items    *csync.Slice[T]
 
 	renderedItems *csync.Map[string, renderedItem]
 
@@ -164,7 +163,7 @@ func New[T Item](items []T, opts ...ListOption) List[T] {
 			keyMap:    DefaultKeyMap(),
 			focused:   true,
 		},
-		items:         items,
+		items:         csync.NewSliceFrom(items),
 		indexMap:      csync.NewMap[string, int](),
 		renderedItems: csync.NewMap[string, renderedItem](),
 	}
@@ -191,7 +190,7 @@ func (l *list[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
 	case anim.StepMsg:
 		var cmds []tea.Cmd
-		for _, item := range l.items {
+		for _, item := range l.items.Slice() {
 			if i, ok := any(item).(HasAnim); ok && i.Spinning() {
 				updated, cmd := i.Update(msg)
 				cmds = append(cmds, cmd)
@@ -267,7 +266,7 @@ func (l *list[T]) viewPosition() (int, int) {
 
 func (l *list[T]) recalculateItemPositions() {
 	currentContentHeight := 0
-	for _, item := range l.items {
+	for _, item := range l.items.Slice() {
 		rItem, ok := l.renderedItems.Get(item.ID())
 		if !ok {
 			continue
@@ -280,7 +279,7 @@ func (l *list[T]) recalculateItemPositions() {
 }
 
 func (l *list[T]) render() tea.Cmd {
-	if l.width <= 0 || l.height <= 0 || len(l.items) == 0 {
+	if l.width <= 0 || l.height <= 0 || l.items.Len() == 0 {
 		return nil
 	}
 	l.setDefaultSelected()
@@ -424,19 +423,23 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
 			if inx == ItemNotFound {
 				return nil
 			}
-			item, ok := l.renderedItems.Get(l.items[inx].ID())
+			item, ok := l.items.Get(inx)
+			if !ok {
+				continue
+			}
+			renderedItem, ok := l.renderedItems.Get(item.ID())
 			if !ok {
 				continue
 			}
 
 			// If the item is bigger than the viewport, select it
-			if item.start <= start && item.end >= end {
-				l.selectedItem = item.id
+			if renderedItem.start <= start && renderedItem.end >= end {
+				l.selectedItem = renderedItem.id
 				return l.render()
 			}
 			// item is in the view
-			if item.start >= start && item.start <= end {
-				l.selectedItem = item.id
+			if renderedItem.start >= start && renderedItem.start <= end {
+				l.selectedItem = renderedItem.id
 				return l.render()
 			}
 		}
@@ -452,19 +455,23 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
 			if inx == ItemNotFound {
 				return nil
 			}
-			item, ok := l.renderedItems.Get(l.items[inx].ID())
+			item, ok := l.items.Get(inx)
+			if !ok {
+				continue
+			}
+			renderedItem, ok := l.renderedItems.Get(item.ID())
 			if !ok {
 				continue
 			}
 
 			// If the item is bigger than the viewport, select it
-			if item.start <= start && item.end >= end {
-				l.selectedItem = item.id
+			if renderedItem.start <= start && renderedItem.end >= end {
+				l.selectedItem = renderedItem.id
 				return l.render()
 			}
 			// item is in the view
-			if item.end >= start && item.end <= end {
-				l.selectedItem = item.id
+			if renderedItem.end >= start && renderedItem.end <= end {
+				l.selectedItem = renderedItem.id
 				return l.render()
 			}
 		}
@@ -475,36 +482,51 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
 func (l *list[T]) selectFirstItem() {
 	inx := l.firstSelectableItemBelow(-1)
 	if inx != ItemNotFound {
-		l.selectedItem = l.items[inx].ID()
+		item, ok := l.items.Get(inx)
+		if ok {
+			l.selectedItem = item.ID()
+		}
 	}
 }
 
 func (l *list[T]) selectLastItem() {
-	inx := l.firstSelectableItemAbove(len(l.items))
+	inx := l.firstSelectableItemAbove(l.items.Len())
 	if inx != ItemNotFound {
-		l.selectedItem = l.items[inx].ID()
+		item, ok := l.items.Get(inx)
+		if ok {
+			l.selectedItem = item.ID()
+		}
 	}
 }
 
 func (l *list[T]) firstSelectableItemAbove(inx int) int {
 	for i := inx - 1; i >= 0; i-- {
-		if _, ok := any(l.items[i]).(layout.Focusable); ok {
+		item, ok := l.items.Get(i)
+		if !ok {
+			continue
+		}
+		if _, ok := any(item).(layout.Focusable); ok {
 			return i
 		}
 	}
 	if inx == 0 && l.wrap {
-		return l.firstSelectableItemAbove(len(l.items))
+		return l.firstSelectableItemAbove(l.items.Len())
 	}
 	return ItemNotFound
 }
 
 func (l *list[T]) firstSelectableItemBelow(inx int) int {
-	for i := inx + 1; i < len(l.items); i++ {
-		if _, ok := any(l.items[i]).(layout.Focusable); ok {
+	itemsLen := l.items.Len()
+	for i := inx + 1; i < itemsLen; i++ {
+		item, ok := l.items.Get(i)
+		if !ok {
+			continue
+		}
+		if _, ok := any(item).(layout.Focusable); ok {
 			return i
 		}
 	}
-	if inx == len(l.items)-1 && l.wrap {
+	if inx == itemsLen-1 && l.wrap {
 		return l.firstSelectableItemBelow(-1)
 	}
 	return ItemNotFound
@@ -515,7 +537,7 @@ func (l *list[T]) focusSelectedItem() tea.Cmd {
 		return nil
 	}
 	var cmds []tea.Cmd
-	for _, item := range l.items {
+	for _, item := range l.items.Slice() {
 		if f, ok := any(item).(layout.Focusable); ok {
 			if item.ID() == l.selectedItem && !f.IsFocused() {
 				cmds = append(cmds, f.Focus())
@@ -534,7 +556,7 @@ func (l *list[T]) blurSelectedItem() tea.Cmd {
 		return nil
 	}
 	var cmds []tea.Cmd
-	for _, item := range l.items {
+	for _, item := range l.items.Slice() {
 		if f, ok := any(item).(layout.Focusable); ok {
 			if item.ID() == l.selectedItem && f.IsFocused() {
 				cmds = append(cmds, f.Blur())
@@ -549,7 +571,8 @@ func (l *list[T]) blurSelectedItem() tea.Cmd {
 // returns the last index
 func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
 	currentContentHeight := lipgloss.Height(l.rendered) - 1
-	for i := startInx; i < len(l.items); i++ {
+	itemsLen := l.items.Len()
+	for i := startInx; i < itemsLen; i++ {
 		if currentContentHeight >= l.height && limitHeight {
 			return i
 		}
@@ -557,10 +580,13 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
 		inx := i
 
 		if l.direction != DirectionForward {
-			inx = (len(l.items) - 1) - i
+			inx = (itemsLen - 1) - i
 		}
 
-		item := l.items[inx]
+		item, ok := l.items.Get(inx)
+		if !ok {
+			continue
+		}
 		var rItem renderedItem
 		if cache, ok := l.renderedItems.Get(item.ID()); ok {
 			rItem = cache
@@ -571,7 +597,7 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
 			l.renderedItems.Set(item.ID(), rItem)
 		}
 		gap := l.gap + 1
-		if inx == len(l.items)-1 {
+		if inx == itemsLen-1 {
 			gap = 0
 		}
 
@@ -582,7 +608,7 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
 		}
 		currentContentHeight = rItem.end + 1 + l.gap
 	}
-	return len(l.items)
+	return itemsLen
 }
 
 func (l *list[T]) renderItem(item Item) renderedItem {
@@ -602,9 +628,9 @@ func (l *list[T]) AppendItem(item T) tea.Cmd {
 		cmds = append(cmds, cmd)
 	}
 
-	l.items = append(l.items, item)
+	l.items.Append(item)
 	l.indexMap = csync.NewMap[string, int]()
-	for inx, item := range l.items {
+	for inx, item := range l.items.Slice() {
 		l.indexMap.Set(item.ID(), inx)
 	}
 	if l.width > 0 && l.height > 0 {
@@ -627,7 +653,7 @@ func (l *list[T]) AppendItem(item T) tea.Cmd {
 			newItem, ok := l.renderedItems.Get(item.ID())
 			if ok {
 				newLines := newItem.height
-				if len(l.items) > 1 {
+				if l.items.Len() > 1 {
 					newLines += l.gap
 				}
 				l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
@@ -649,15 +675,20 @@ func (l *list[T]) DeleteItem(id string) tea.Cmd {
 	if !ok {
 		return nil
 	}
-	l.items = slices.Delete(l.items, inx, inx+1)
+	l.items.Delete(inx)
 	l.renderedItems.Del(id)
-	for inx, item := range l.items {
+	for inx, item := range l.items.Slice() {
 		l.indexMap.Set(item.ID(), inx)
 	}
 
 	if l.selectedItem == id {
 		if inx > 0 {
-			l.selectedItem = l.items[inx-1].ID()
+			item, ok := l.items.Get(inx - 1)
+			if ok {
+				l.selectedItem = item.ID()
+			} else {
+				l.selectedItem = ""
+			}
 		} else {
 			l.selectedItem = ""
 		}
@@ -711,7 +742,7 @@ func (l *list[T]) IsFocused() bool {
 
 // Items implements List.
 func (l *list[T]) Items() []T {
-	return l.items
+	return l.items.Slice()
 }
 
 func (l *list[T]) incrementOffset(n int) {
@@ -764,9 +795,9 @@ func (l *list[T]) PrependItem(item T) tea.Cmd {
 	cmds := []tea.Cmd{
 		item.Init(),
 	}
-	l.items = append([]T{item}, l.items...)
+	l.items.Prepend(item)
 	l.indexMap = csync.NewMap[string, int]()
-	for inx, item := range l.items {
+	for inx, item := range l.items.Slice() {
 		l.indexMap.Set(item.ID(), inx)
 	}
 	if l.width > 0 && l.height > 0 {
@@ -783,7 +814,7 @@ func (l *list[T]) PrependItem(item T) tea.Cmd {
 			newItem, ok := l.renderedItems.Get(item.ID())
 			if ok {
 				newLines := newItem.height
-				if len(l.items) > 1 {
+				if l.items.Len() > 1 {
 					newLines += l.gap
 				}
 				l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
@@ -817,7 +848,10 @@ func (l *list[T]) SelectItemAbove() tea.Cmd {
 		}
 
 	}
-	item := l.items[newIndex]
+	item, ok := l.items.Get(newIndex)
+	if !ok {
+		return nil
+	}
 	l.selectedItem = item.ID()
 	l.movingByItem = true
 	renderCmd := l.render()
@@ -839,7 +873,10 @@ func (l *list[T]) SelectItemBelow() tea.Cmd {
 		// no item above
 		return nil
 	}
-	item := l.items[newIndex]
+	item, ok := l.items.Get(newIndex)
+	if !ok {
+		return nil
+	}
 	l.selectedItem = item.ID()
 	l.movingByItem = true
 	return l.render()
@@ -851,18 +888,21 @@ func (l *list[T]) SelectedItem() *T {
 	if !ok {
 		return nil
 	}
-	if inx > len(l.items)-1 {
+	if inx > l.items.Len()-1 {
+		return nil
+	}
+	item, ok := l.items.Get(inx)
+	if !ok {
 		return nil
 	}
-	item := l.items[inx]
 	return &item
 }
 
 // SetItems implements List.
 func (l *list[T]) SetItems(items []T) tea.Cmd {
-	l.items = items
+	l.items.SetSlice(items)
 	var cmds []tea.Cmd
-	for inx, item := range l.items {
+	for inx, item := range l.items.Slice() {
 		if i, ok := any(item).(Indexable); ok {
 			i.SetIndex(inx)
 		}
@@ -885,7 +925,7 @@ func (l *list[T]) reset(selectedItem string) tea.Cmd {
 	l.selectedItem = selectedItem
 	l.indexMap = csync.NewMap[string, int]()
 	l.renderedItems = csync.NewMap[string, renderedItem]()
-	for inx, item := range l.items {
+	for inx, item := range l.items.Slice() {
 		l.indexMap.Set(item.ID(), inx)
 		if l.width > 0 && l.height > 0 {
 			cmds = append(cmds, item.SetSize(l.width, l.height))
@@ -911,7 +951,7 @@ func (l *list[T]) SetSize(width int, height int) tea.Cmd {
 func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
 	var cmds []tea.Cmd
 	if inx, ok := l.indexMap.Get(id); ok {
-		l.items[inx] = item
+		l.items.Set(inx, item)
 		oldItem, hasOldItem := l.renderedItems.Get(id)
 		oldPosition := l.offset
 		if l.direction == DirectionBackward {
@@ -928,7 +968,7 @@ func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
 		if hasOldItem && l.direction == DirectionBackward {
 			// if we are the last item and there is no offset
 			// make sure to go to the bottom
-			if inx == len(l.items)-1 && l.offset == 0 {
+			if inx == l.items.Len()-1 && l.offset == 0 {
 				cmd = l.GoToBottom()
 				if cmd != nil {
 					cmds = append(cmds, cmd)
  @@ -30,7 +30,7 @@ func TestList(t *testing.T) {
 		assert.Equal(t, items[0].ID(), l.selectedItem)
 		assert.Equal(t, 0, l.offset)
 		require.Equal(t, 5, l.indexMap.Len())
-		require.Len(t, l.items, 5)
+		require.Equal(t, 5, l.items.Len())
 		require.Equal(t, 5, l.renderedItems.Len())
 		assert.Equal(t, 5, lipgloss.Height(l.rendered))
 		assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
@@ -60,7 +60,7 @@ func TestList(t *testing.T) {
 		assert.Equal(t, items[4].ID(), l.selectedItem)
 		assert.Equal(t, 0, l.offset)
 		require.Equal(t, 5, l.indexMap.Len())
-		require.Len(t, l.items, 5)
+		require.Equal(t, 5, l.items.Len())
 		require.Equal(t, 5, l.renderedItems.Len())
 		assert.Equal(t, 5, lipgloss.Height(l.rendered))
 		assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
@@ -91,7 +91,7 @@ func TestList(t *testing.T) {
 		assert.Equal(t, items[0].ID(), l.selectedItem)
 		assert.Equal(t, 0, l.offset)
 		require.Equal(t, 30, l.indexMap.Len())
-		require.Len(t, l.items, 30)
+		require.Equal(t, 30, l.items.Len())
 		require.Equal(t, 30, l.renderedItems.Len())
 		assert.Equal(t, 30, lipgloss.Height(l.rendered))
 		assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
@@ -121,7 +121,7 @@ func TestList(t *testing.T) {
 		assert.Equal(t, items[29].ID(), l.selectedItem)
 		assert.Equal(t, 0, l.offset)
 		require.Equal(t, 30, l.indexMap.Len())
-		require.Len(t, l.items, 30)
+		require.Equal(t, 30, l.items.Len())
 		require.Equal(t, 30, l.renderedItems.Len())
 		assert.Equal(t, 30, lipgloss.Height(l.rendered))
 		assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
@@ -154,7 +154,7 @@ func TestList(t *testing.T) {
 		assert.Equal(t, items[0].ID(), l.selectedItem)
 		assert.Equal(t, 0, l.offset)
 		require.Equal(t, 30, l.indexMap.Len())
-		require.Len(t, l.items, 30)
+		require.Equal(t, 30, l.items.Len())
 		require.Equal(t, 30, l.renderedItems.Len())
 		expectedLines := 0
 		for i := range 30 {
@@ -192,7 +192,7 @@ func TestList(t *testing.T) {
 		assert.Equal(t, items[29].ID(), l.selectedItem)
 		assert.Equal(t, 0, l.offset)
 		require.Equal(t, 30, l.indexMap.Len())
-		require.Len(t, l.items, 30)
+		require.Equal(t, 30, l.items.Len())
 		require.Equal(t, 30, l.renderedItems.Len())
 		expectedLines := 0
 		for i := range 30 {