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