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