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