list_test.go

  1package list
  2
  3import (
  4	"fmt"
  5	"strings"
  6	"testing"
  7
  8	tea "github.com/charmbracelet/bubbletea/v2"
  9	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 10	"github.com/charmbracelet/lipgloss/v2"
 11	"github.com/charmbracelet/x/exp/golden"
 12	"github.com/google/uuid"
 13	"github.com/stretchr/testify/assert"
 14	"github.com/stretchr/testify/require"
 15)
 16
 17func TestList(t *testing.T) {
 18	t.Parallel()
 19	t.Run("should have correct positions in list that fits the items", func(t *testing.T) {
 20		t.Parallel()
 21		items := []Item{}
 22		for i := range 5 {
 23			item := NewSelectableItem(fmt.Sprintf("Item %d", i))
 24			items = append(items, item)
 25		}
 26		l := New(items, WithDirectionForward(), WithSize(10, 20)).(*list[Item])
 27		execCmd(l, l.Init())
 28
 29		// should select the last item
 30		assert.Equal(t, items[0].ID(), l.selectedItem)
 31		assert.Equal(t, 0, l.offset)
 32		require.Equal(t, 5, l.indexMap.Len())
 33		require.Equal(t, 5, l.items.Len())
 34		require.Equal(t, 5, l.renderedItems.Len())
 35		assert.Equal(t, 5, lipgloss.Height(l.rendered))
 36		assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
 37		start, end := l.viewPosition()
 38		assert.Equal(t, 0, start)
 39		assert.Equal(t, 4, end)
 40		for i := range 5 {
 41			item, ok := l.renderedItems.Get(items[i].ID())
 42			require.True(t, ok)
 43			assert.Equal(t, i, item.start)
 44			assert.Equal(t, i, item.end)
 45		}
 46
 47		golden.RequireEqual(t, []byte(l.View()))
 48	})
 49	t.Run("should have correct positions in list that fits the items backwards", func(t *testing.T) {
 50		t.Parallel()
 51		items := []Item{}
 52		for i := range 5 {
 53			item := NewSelectableItem(fmt.Sprintf("Item %d", i))
 54			items = append(items, item)
 55		}
 56		l := New(items, WithDirectionBackward(), WithSize(10, 20)).(*list[Item])
 57		execCmd(l, l.Init())
 58
 59		// should select the last item
 60		assert.Equal(t, items[4].ID(), l.selectedItem)
 61		assert.Equal(t, 0, l.offset)
 62		require.Equal(t, 5, l.indexMap.Len())
 63		require.Equal(t, 5, l.items.Len())
 64		require.Equal(t, 5, l.renderedItems.Len())
 65		assert.Equal(t, 5, lipgloss.Height(l.rendered))
 66		assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
 67		start, end := l.viewPosition()
 68		assert.Equal(t, 0, start)
 69		assert.Equal(t, 4, end)
 70		for i := range 5 {
 71			item, ok := l.renderedItems.Get(items[i].ID())
 72			require.True(t, ok)
 73			assert.Equal(t, i, item.start)
 74			assert.Equal(t, i, item.end)
 75		}
 76
 77		golden.RequireEqual(t, []byte(l.View()))
 78	})
 79
 80	t.Run("should have correct positions in list that does not fits the items", func(t *testing.T) {
 81		t.Parallel()
 82		items := []Item{}
 83		for i := range 30 {
 84			item := NewSelectableItem(fmt.Sprintf("Item %d", i))
 85			items = append(items, item)
 86		}
 87		l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
 88		execCmd(l, l.Init())
 89
 90		// should select the last item
 91		assert.Equal(t, items[0].ID(), l.selectedItem)
 92		assert.Equal(t, 0, l.offset)
 93		require.Equal(t, 30, l.indexMap.Len())
 94		require.Equal(t, 30, l.items.Len())
 95		require.Equal(t, 30, l.renderedItems.Len())
 96		assert.Equal(t, 30, lipgloss.Height(l.rendered))
 97		assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
 98		start, end := l.viewPosition()
 99		assert.Equal(t, 0, start)
100		assert.Equal(t, 9, end)
101		for i := range 30 {
102			item, ok := l.renderedItems.Get(items[i].ID())
103			require.True(t, ok)
104			assert.Equal(t, i, item.start)
105			assert.Equal(t, i, item.end)
106		}
107
108		golden.RequireEqual(t, []byte(l.View()))
109	})
110	t.Run("should have correct positions in list that does not fits the items backwards", func(t *testing.T) {
111		t.Parallel()
112		items := []Item{}
113		for i := range 30 {
114			item := NewSelectableItem(fmt.Sprintf("Item %d", i))
115			items = append(items, item)
116		}
117		l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
118		execCmd(l, l.Init())
119
120		// should select the last item
121		assert.Equal(t, items[29].ID(), l.selectedItem)
122		assert.Equal(t, 0, l.offset)
123		require.Equal(t, 30, l.indexMap.Len())
124		require.Equal(t, 30, l.items.Len())
125		require.Equal(t, 30, l.renderedItems.Len())
126		assert.Equal(t, 30, lipgloss.Height(l.rendered))
127		assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
128		start, end := l.viewPosition()
129		assert.Equal(t, 20, start)
130		assert.Equal(t, 29, end)
131		for i := range 30 {
132			item, ok := l.renderedItems.Get(items[i].ID())
133			require.True(t, ok)
134			assert.Equal(t, i, item.start)
135			assert.Equal(t, i, item.end)
136		}
137
138		golden.RequireEqual(t, []byte(l.View()))
139	})
140
141	t.Run("should have correct positions in list that does not fits the items and has multi line items", func(t *testing.T) {
142		t.Parallel()
143		items := []Item{}
144		for i := range 30 {
145			content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
146			content = strings.TrimSuffix(content, "\n")
147			item := NewSelectableItem(content)
148			items = append(items, item)
149		}
150		l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
151		execCmd(l, l.Init())
152
153		// should select the last item
154		assert.Equal(t, items[0].ID(), l.selectedItem)
155		assert.Equal(t, 0, l.offset)
156		require.Equal(t, 30, l.indexMap.Len())
157		require.Equal(t, 30, l.items.Len())
158		require.Equal(t, 30, l.renderedItems.Len())
159		expectedLines := 0
160		for i := range 30 {
161			expectedLines += (i + 1) * 1
162		}
163		// With virtual scrolling, rendered height should be viewport height (10)
164		assert.Equal(t, 10, lipgloss.Height(l.rendered))
165		if len(l.rendered) > 0 {
166			assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
167		}
168		start, end := l.viewPosition()
169		assert.Equal(t, 0, start)
170		assert.Equal(t, 9, end)
171		currentPosition := 0
172		for i := range 30 {
173			rItem, ok := l.renderedItems.Get(items[i].ID())
174			require.True(t, ok)
175			assert.Equal(t, currentPosition, rItem.start)
176			assert.Equal(t, currentPosition+i, rItem.end)
177			currentPosition += i + 1
178		}
179
180		golden.RequireEqual(t, []byte(l.View()))
181	})
182	t.Run("should have correct positions in list that does not fits the items and has multi line items backwards", func(t *testing.T) {
183		t.Parallel()
184		items := []Item{}
185		for i := range 30 {
186			content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
187			content = strings.TrimSuffix(content, "\n")
188			item := NewSelectableItem(content)
189			items = append(items, item)
190		}
191		l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
192		execCmd(l, l.Init())
193
194		// should select the last item
195		assert.Equal(t, items[29].ID(), l.selectedItem)
196		assert.Equal(t, 0, l.offset)
197		require.Equal(t, 30, l.indexMap.Len())
198		require.Equal(t, 30, l.items.Len())
199		require.Equal(t, 30, l.renderedItems.Len())
200		expectedLines := 0
201		for i := range 30 {
202			expectedLines += (i + 1) * 1
203		}
204		// With virtual scrolling, rendered height should be viewport height (10)
205		assert.Equal(t, 10, lipgloss.Height(l.rendered))
206		if len(l.rendered) > 0 {
207			assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
208		}
209		start, end := l.viewPosition()
210		assert.Equal(t, expectedLines-10, start)
211		assert.Equal(t, expectedLines-1, end)
212		currentPosition := 0
213		for i := range 30 {
214			rItem, ok := l.renderedItems.Get(items[i].ID())
215			require.True(t, ok)
216			assert.Equal(t, currentPosition, rItem.start)
217			assert.Equal(t, currentPosition+i, rItem.end)
218			currentPosition += i + 1
219		}
220
221		golden.RequireEqual(t, []byte(l.View()))
222	})
223
224	t.Run("should go to selected item at the beginning", func(t *testing.T) {
225		t.Parallel()
226		items := []Item{}
227		for i := range 30 {
228			content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
229			content = strings.TrimSuffix(content, "\n")
230			item := NewSelectableItem(content)
231			items = append(items, item)
232		}
233		l := New(items, WithDirectionForward(), WithSize(10, 10), WithSelectedItem(items[10].ID())).(*list[Item])
234		execCmd(l, l.Init())
235
236		// should select the last item
237		assert.Equal(t, items[10].ID(), l.selectedItem)
238
239		golden.RequireEqual(t, []byte(l.View()))
240	})
241
242	t.Run("should go to selected item at the beginning backwards", func(t *testing.T) {
243		t.Parallel()
244		items := []Item{}
245		for i := range 30 {
246			content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
247			content = strings.TrimSuffix(content, "\n")
248			item := NewSelectableItem(content)
249			items = append(items, item)
250		}
251		l := New(items, WithDirectionBackward(), WithSize(10, 10), WithSelectedItem(items[10].ID())).(*list[Item])
252		execCmd(l, l.Init())
253
254		// should select the last item
255		assert.Equal(t, items[10].ID(), l.selectedItem)
256
257		golden.RequireEqual(t, []byte(l.View()))
258	})
259}
260
261func TestListMovement(t *testing.T) {
262	t.Parallel()
263	t.Run("should move viewport up", func(t *testing.T) {
264		t.Parallel()
265		items := []Item{}
266		for i := range 30 {
267			content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
268			content = strings.TrimSuffix(content, "\n")
269			item := NewSelectableItem(content)
270			items = append(items, item)
271		}
272		l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
273		execCmd(l, l.Init())
274
275		execCmd(l, l.MoveUp(25))
276
277		assert.Equal(t, 25, l.offset)
278		golden.RequireEqual(t, []byte(l.View()))
279	})
280	t.Run("should move viewport up and down", func(t *testing.T) {
281		t.Parallel()
282		items := []Item{}
283		for i := range 30 {
284			content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
285			content = strings.TrimSuffix(content, "\n")
286			item := NewSelectableItem(content)
287			items = append(items, item)
288		}
289		l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
290		execCmd(l, l.Init())
291
292		execCmd(l, l.MoveUp(25))
293		execCmd(l, l.MoveDown(25))
294
295		assert.Equal(t, 0, l.offset)
296		golden.RequireEqual(t, []byte(l.View()))
297	})
298
299	t.Run("should move viewport down", func(t *testing.T) {
300		t.Parallel()
301		items := []Item{}
302		for i := range 30 {
303			content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
304			content = strings.TrimSuffix(content, "\n")
305			item := NewSelectableItem(content)
306			items = append(items, item)
307		}
308		l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
309		execCmd(l, l.Init())
310
311		execCmd(l, l.MoveDown(25))
312
313		assert.Equal(t, 25, l.offset)
314		golden.RequireEqual(t, []byte(l.View()))
315	})
316	t.Run("should move viewport down and up", func(t *testing.T) {
317		t.Parallel()
318		items := []Item{}
319		for i := range 30 {
320			content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
321			content = strings.TrimSuffix(content, "\n")
322			item := NewSelectableItem(content)
323			items = append(items, item)
324		}
325		l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
326		execCmd(l, l.Init())
327
328		execCmd(l, l.MoveDown(25))
329		execCmd(l, l.MoveUp(25))
330
331		assert.Equal(t, 0, l.offset)
332		golden.RequireEqual(t, []byte(l.View()))
333	})
334
335	t.Run("should not change offset when new items are appended and we are at the bottom in backwards list", func(t *testing.T) {
336		t.Parallel()
337		items := []Item{}
338		for i := range 30 {
339			content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
340			content = strings.TrimSuffix(content, "\n")
341			item := NewSelectableItem(content)
342			items = append(items, item)
343		}
344		l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
345		execCmd(l, l.Init())
346		execCmd(l, l.AppendItem(NewSelectableItem("Testing")))
347
348		assert.Equal(t, 0, l.offset)
349		golden.RequireEqual(t, []byte(l.View()))
350	})
351
352	t.Run("should stay at the position it is when new items are added but we moved up in backwards list", func(t *testing.T) {
353		t.Parallel()
354		items := []Item{}
355		for i := range 30 {
356			item := NewSelectableItem(fmt.Sprintf("Item %d", i))
357			items = append(items, item)
358		}
359		l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
360		execCmd(l, l.Init())
361
362		execCmd(l, l.MoveUp(2))
363		viewBefore := l.View()
364		execCmd(l, l.AppendItem(NewSelectableItem("Testing\nHello\n")))
365		viewAfter := l.View()
366		assert.Equal(t, viewBefore, viewAfter)
367		assert.Equal(t, 5, l.offset)
368		assert.Equal(t, 33, lipgloss.Height(l.rendered))
369		golden.RequireEqual(t, []byte(l.View()))
370	})
371	t.Run("should stay at the position it is when the hight of an item below is increased in backwards list", func(t *testing.T) {
372		t.Parallel()
373		items := []Item{}
374		for i := range 30 {
375			item := NewSelectableItem(fmt.Sprintf("Item %d", i))
376			items = append(items, item)
377		}
378		l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
379		execCmd(l, l.Init())
380
381		execCmd(l, l.MoveUp(2))
382		viewBefore := l.View()
383		item := items[29]
384		execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 29\nLine 2\nLine 3")))
385		viewAfter := l.View()
386		assert.Equal(t, viewBefore, viewAfter)
387		assert.Equal(t, 4, l.offset)
388		assert.Equal(t, 32, lipgloss.Height(l.rendered))
389		golden.RequireEqual(t, []byte(l.View()))
390	})
391	t.Run("should stay at the position it is when the hight of an item below is decreases in backwards list", func(t *testing.T) {
392		t.Parallel()
393		items := []Item{}
394		for i := range 30 {
395			item := NewSelectableItem(fmt.Sprintf("Item %d", i))
396			items = append(items, item)
397		}
398		items = append(items, NewSelectableItem("Item 30\nLine 2\nLine 3"))
399		l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
400		execCmd(l, l.Init())
401
402		execCmd(l, l.MoveUp(2))
403		viewBefore := l.View()
404		item := items[30]
405		execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 30")))
406		viewAfter := l.View()
407		assert.Equal(t, viewBefore, viewAfter)
408		assert.Equal(t, 0, l.offset)
409		assert.Equal(t, 31, lipgloss.Height(l.rendered))
410		golden.RequireEqual(t, []byte(l.View()))
411	})
412	t.Run("should stay at the position it is when the hight of an item above is increased in backwards list", func(t *testing.T) {
413		t.Parallel()
414		items := []Item{}
415		for i := range 30 {
416			item := NewSelectableItem(fmt.Sprintf("Item %d", i))
417			items = append(items, item)
418		}
419		l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
420		execCmd(l, l.Init())
421
422		execCmd(l, l.MoveUp(2))
423		viewBefore := l.View()
424		item := items[1]
425		execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 1\nLine 2\nLine 3")))
426		viewAfter := l.View()
427		assert.Equal(t, viewBefore, viewAfter)
428		assert.Equal(t, 2, l.offset)
429		assert.Equal(t, 32, lipgloss.Height(l.rendered))
430		golden.RequireEqual(t, []byte(l.View()))
431	})
432	t.Run("should stay at the position it is if an item is prepended and we are in backwards list", func(t *testing.T) {
433		t.Parallel()
434		items := []Item{}
435		for i := range 30 {
436			item := NewSelectableItem(fmt.Sprintf("Item %d", i))
437			items = append(items, item)
438		}
439		l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
440		execCmd(l, l.Init())
441
442		execCmd(l, l.MoveUp(2))
443		viewBefore := l.View()
444		execCmd(l, l.PrependItem(NewSelectableItem("New")))
445		viewAfter := l.View()
446		assert.Equal(t, viewBefore, viewAfter)
447		assert.Equal(t, 2, l.offset)
448		assert.Equal(t, 31, lipgloss.Height(l.rendered))
449		golden.RequireEqual(t, []byte(l.View()))
450	})
451
452	t.Run("should not change offset when new items are prepended and we are at the top in forward list", func(t *testing.T) {
453		t.Parallel()
454		items := []Item{}
455		for i := range 30 {
456			content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
457			content = strings.TrimSuffix(content, "\n")
458			item := NewSelectableItem(content)
459			items = append(items, item)
460		}
461		l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
462		execCmd(l, l.Init())
463		execCmd(l, l.PrependItem(NewSelectableItem("Testing")))
464
465		assert.Equal(t, 0, l.offset)
466		golden.RequireEqual(t, []byte(l.View()))
467	})
468
469	t.Run("should stay at the position it is when new items are added but we moved down in forward list", func(t *testing.T) {
470		t.Parallel()
471		items := []Item{}
472		for i := range 30 {
473			item := NewSelectableItem(fmt.Sprintf("Item %d", i))
474			items = append(items, item)
475		}
476		l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
477		execCmd(l, l.Init())
478
479		execCmd(l, l.MoveDown(2))
480		viewBefore := l.View()
481		execCmd(l, l.PrependItem(NewSelectableItem("Testing\nHello\n")))
482		viewAfter := l.View()
483		assert.Equal(t, viewBefore, viewAfter)
484		assert.Equal(t, 5, l.offset)
485		assert.Equal(t, 33, lipgloss.Height(l.rendered))
486		golden.RequireEqual(t, []byte(l.View()))
487	})
488
489	t.Run("should stay at the position it is when the hight of an item above is increased in forward list", func(t *testing.T) {
490		t.Parallel()
491		items := []Item{}
492		for i := range 30 {
493			item := NewSelectableItem(fmt.Sprintf("Item %d", i))
494			items = append(items, item)
495		}
496		l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
497		execCmd(l, l.Init())
498
499		execCmd(l, l.MoveDown(2))
500		viewBefore := l.View()
501		item := items[0]
502		execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 29\nLine 2\nLine 3")))
503		viewAfter := l.View()
504		assert.Equal(t, viewBefore, viewAfter)
505		assert.Equal(t, 4, l.offset)
506		assert.Equal(t, 32, lipgloss.Height(l.rendered))
507		golden.RequireEqual(t, []byte(l.View()))
508	})
509
510	t.Run("should stay at the position it is when the hight of an item above is decreases in forward list", func(t *testing.T) {
511		t.Parallel()
512		items := []Item{}
513		items = append(items, NewSelectableItem("At top\nLine 2\nLine 3"))
514		for i := range 30 {
515			item := NewSelectableItem(fmt.Sprintf("Item %d", i))
516			items = append(items, item)
517		}
518		l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
519		execCmd(l, l.Init())
520
521		execCmd(l, l.MoveDown(3))
522		viewBefore := l.View()
523		item := items[0]
524		execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("At top")))
525		viewAfter := l.View()
526		assert.Equal(t, viewBefore, viewAfter)
527		assert.Equal(t, 1, l.offset)
528		assert.Equal(t, 31, lipgloss.Height(l.rendered))
529		golden.RequireEqual(t, []byte(l.View()))
530	})
531
532	t.Run("should stay at the position it is when the hight of an item below is increased in forward list", func(t *testing.T) {
533		t.Parallel()
534		items := []Item{}
535		for i := range 30 {
536			item := NewSelectableItem(fmt.Sprintf("Item %d", i))
537			items = append(items, item)
538		}
539		l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
540		execCmd(l, l.Init())
541
542		execCmd(l, l.MoveDown(2))
543		viewBefore := l.View()
544		item := items[29]
545		execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 29\nLine 2\nLine 3")))
546		viewAfter := l.View()
547		assert.Equal(t, viewBefore, viewAfter)
548		assert.Equal(t, 2, l.offset)
549		assert.Equal(t, 32, lipgloss.Height(l.rendered))
550		golden.RequireEqual(t, []byte(l.View()))
551	})
552	t.Run("should stay at the position it is if an item is appended and we are in forward list", func(t *testing.T) {
553		t.Parallel()
554		items := []Item{}
555		for i := range 30 {
556			item := NewSelectableItem(fmt.Sprintf("Item %d", i))
557			items = append(items, item)
558		}
559		l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
560		execCmd(l, l.Init())
561
562		execCmd(l, l.MoveDown(2))
563		viewBefore := l.View()
564		execCmd(l, l.AppendItem(NewSelectableItem("New")))
565		viewAfter := l.View()
566		assert.Equal(t, viewBefore, viewAfter)
567		assert.Equal(t, 2, l.offset)
568		assert.Equal(t, 31, lipgloss.Height(l.rendered))
569		golden.RequireEqual(t, []byte(l.View()))
570	})
571}
572
573type SelectableItem interface {
574	Item
575	layout.Focusable
576}
577
578type simpleItem struct {
579	width   int
580	content string
581	id      string
582}
583type selectableItem struct {
584	*simpleItem
585	focused bool
586}
587
588func NewSimpleItem(content string) *simpleItem {
589	return &simpleItem{
590		id:      uuid.NewString(),
591		width:   0,
592		content: content,
593	}
594}
595
596func NewSelectableItem(content string) SelectableItem {
597	return &selectableItem{
598		simpleItem: NewSimpleItem(content),
599		focused:    false,
600	}
601}
602
603func (s *simpleItem) ID() string {
604	return s.id
605}
606
607func (s *simpleItem) Init() tea.Cmd {
608	return nil
609}
610
611func (s *simpleItem) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
612	return s, nil
613}
614
615func (s *simpleItem) View() string {
616	return lipgloss.NewStyle().Width(s.width).Render(s.content)
617}
618
619func (l *simpleItem) GetSize() (int, int) {
620	return l.width, 0
621}
622
623// SetSize implements Item.
624func (s *simpleItem) SetSize(width int, height int) tea.Cmd {
625	s.width = width
626	return nil
627}
628
629func (s *selectableItem) View() string {
630	if s.focused {
631		return lipgloss.NewStyle().BorderLeft(true).BorderStyle(lipgloss.NormalBorder()).Width(s.width).Render(s.content)
632	}
633	return lipgloss.NewStyle().Width(s.width).Render(s.content)
634}
635
636// Blur implements SimpleItem.
637func (s *selectableItem) Blur() tea.Cmd {
638	s.focused = false
639	return nil
640}
641
642// Focus implements SimpleItem.
643func (s *selectableItem) Focus() tea.Cmd {
644	s.focused = true
645	return nil
646}
647
648// IsFocused implements SimpleItem.
649func (s *selectableItem) IsFocused() bool {
650	return s.focused
651}
652
653func execCmd(m tea.Model, cmd tea.Cmd) {
654	for cmd != nil {
655		msg := cmd()
656		m, cmd = m.Update(msg)
657	}
658}