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}