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