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, items[0].ID(), l.selectedItem)
32 assert.Equal(t, 0, l.offset)
33 require.Equal(t, 5, l.indexMap.Len())
34 require.Equal(t, 5, l.items.Len())
35 require.Equal(t, 5, l.renderedItems.Len())
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.Get(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, items[4].ID(), l.selectedItem)
62 assert.Equal(t, 0, l.offset)
63 require.Equal(t, 5, l.indexMap.Len())
64 require.Equal(t, 5, l.items.Len())
65 require.Equal(t, 5, l.renderedItems.Len())
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.Get(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, items[0].ID(), l.selectedItem)
93 assert.Equal(t, 0, l.offset)
94 require.Equal(t, 30, l.indexMap.Len())
95 require.Equal(t, 30, l.items.Len())
96 require.Equal(t, 30, l.renderedItems.Len())
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.Get(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, items[29].ID(), l.selectedItem)
123 assert.Equal(t, 0, l.offset)
124 require.Equal(t, 30, l.indexMap.Len())
125 require.Equal(t, 30, l.items.Len())
126 require.Equal(t, 30, l.renderedItems.Len())
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.Get(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, items[0].ID(), l.selectedItem)
156 assert.Equal(t, 0, l.offset)
157 require.Equal(t, 30, l.indexMap.Len())
158 require.Equal(t, 30, l.items.Len())
159 require.Equal(t, 30, l.renderedItems.Len())
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.Get(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, items[29].ID(), l.selectedItem)
194 assert.Equal(t, 0, l.offset)
195 require.Equal(t, 30, l.indexMap.Len())
196 require.Equal(t, 30, l.items.Len())
197 require.Equal(t, 30, l.renderedItems.Len())
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.Get(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, items[10].ID(), l.selectedItem)
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, items[10].ID(), l.selectedItem)
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}