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/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, 0, l.selectedItemIdx)
 32		assert.Equal(t, 0, l.offset)
 33		require.Equal(t, 5, len(l.indexMap))
 34		require.Equal(t, 5, len(l.items))
 35		require.Equal(t, 5, len(l.renderedItems))
 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[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, 4, l.selectedItemIdx)
 62		assert.Equal(t, 0, l.offset)
 63		require.Equal(t, 5, len(l.indexMap))
 64		require.Equal(t, 5, len(l.items))
 65		require.Equal(t, 5, len(l.renderedItems))
 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[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, 0, l.selectedItemIdx)
 93		assert.Equal(t, 0, l.offset)
 94		require.Equal(t, 30, len(l.indexMap))
 95		require.Equal(t, 30, len(l.items))
 96		require.Equal(t, 30, len(l.renderedItems))
 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[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, 29, l.selectedItemIdx)
123		assert.Equal(t, 0, l.offset)
124		require.Equal(t, 30, len(l.indexMap))
125		require.Equal(t, 30, len(l.items))
126		require.Equal(t, 30, len(l.renderedItems))
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[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, 0, l.selectedItemIdx)
156		assert.Equal(t, 0, l.offset)
157		require.Equal(t, 30, len(l.indexMap))
158		require.Equal(t, 30, len(l.items))
159		require.Equal(t, 30, len(l.renderedItems))
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[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, 29, l.selectedItemIdx)
194		assert.Equal(t, 0, l.offset)
195		require.Equal(t, 30, len(l.indexMap))
196		require.Equal(t, 30, len(l.items))
197		require.Equal(t, 30, len(l.renderedItems))
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[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, 10, l.selectedItemIdx)
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, 10, l.selectedItemIdx)
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}