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}