@@ -1,9 +1,9 @@
1package list
1package list
6+ "github.com/charmbracelet/bubbles/v2/key"
7 tea "github.com/charmbracelet/bubbletea/v2"
7 tea "github.com/charmbracelet/bubbletea/v2"
8 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
8 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
9 "github.com/charmbracelet/crush/internal/tui/util"
9 "github.com/charmbracelet/crush/internal/tui/util"
@@ -16,11 +16,19 @@ type Item interface {
16 ID() string
16 ID() string
19−type List interface {
19+type List[T Item] interface {
20 util.Model
20 util.Model
21 layout.Sizeable
21 layout.Sizeable
22 layout.Focusable
22 layout.Focusable
24+ MoveDown(int) tea.Cmd
27+ SelectItemAbove() tea.Cmd
28+ SelectItemBelow() tea.Cmd
23− SetItems(items []Item) tea.Cmd
29+ SetItems([]T) tea.Cmd
30+ SetSelected(string) tea.Cmd
26type direction int
34type direction int
@@ -31,41 +39,45 @@ const (
34− NotFound = -1
42+ NotFound = -1
43+ DefaultScrollSize = 2
46+type setSelectedMsg struct {
47+ selectedItemID string
37type renderedItem struct {
50type renderedItem struct {
39 view string
52 view string
40 height int
53 height int
43−type list struct {
56+type confOptions struct {
44 width, height int
57 width, height int
59+ // if you are at the last item and go down it will wrap to the top
47− direction direction
62+ direction direction
48− selectedItem string
63+ selectedItem string
65+type list[T Item] struct {
51− items []Item
70+ items []T
52 renderedItems []renderedItem
71 renderedItems []renderedItem
53 rendered string
72 rendered string
54 isReady bool
73 isReady bool
57−type listOption func(*list)
76+type listOption func(*confOptions)
59−// WithItems sets the initial items for the list.
60−func WithItems(items ...Item) listOption {
61− return func(l *list) {
66// WithSize sets the size of the list.
78// WithSize sets the size of the list.
67func WithSize(width, height int) listOption {
79func WithSize(width, height int) listOption {
68− return func(l *list) {
80+ return func(l *confOptions) {
69 l.width = width
81 l.width = width
70 l.height = height
82 l.height = height
@@ -73,44 +85,53 @@ func WithSize(width, height int) listOption {
74// WithGap sets the gap between items in the list.
86// WithGap sets the gap between items in the list.
75func WithGap(gap int) listOption {
87func WithGap(gap int) listOption {
76− return func(l *list) {
88+ return func(l *confOptions) {
77 l.gap = gap
89 l.gap = gap
81// WithDirection sets the direction of the list.
93// WithDirection sets the direction of the list.
82func WithDirection(dir direction) listOption {
94func WithDirection(dir direction) listOption {
83− return func(l *list) {
95+ return func(l *confOptions) {
84 l.direction = dir
96 l.direction = dir
88// WithSelectedItem sets the initially selected item in the list.
100// WithSelectedItem sets the initially selected item in the list.
89func WithSelectedItem(id string) listOption {
101func WithSelectedItem(id string) listOption {
90− return func(l *list) {
102+ return func(l *confOptions) {
91 l.selectedItem = id
103 l.selectedItem = id
107+func WithKeyMap(keyMap KeyMap) listOption {
108+ return func(l *confOptions) {
113+func WithWrapNavigation() listOption {
114+ return func(l *confOptions) {
95−func New(opts ...listOption) List {
119+func New[T Item](items []T, opts ...listOption) List[T] {
96− list := &list{
120+ list := &list[T]{
97− items: make([]Item, 0),
121+ confOptions: confOptions{
98− direction: Forward,
122+ direction: Forward,
123+ keyMap: DefaultKeyMap(),
100 for _, opt := range opts {
127 for _, opt := range opts {
101− opt(list)
128+ opt(&list.confOptions)
103 return list
130 return list
106// Init implements List.
133// Init implements List.
107−func (l *list) Init() tea.Cmd {
134+func (l *list[T]) Init() tea.Cmd {
108− if l.height <= 0 || l.width <= 0 {
111− if len(l.items) == 0 {
114 var cmds []tea.Cmd
135 var cmds []tea.Cmd
115 for _, item := range l.items {
136 for _, item := range l.items {
116 cmd := item.Init()
137 cmd := item.Init()
@@ -121,12 +142,41 @@ func (l *list) Init() tea.Cmd {
123// Update implements List.
144// Update implements List.
124−func (l *list) Update(tea.Msg) (tea.Model, tea.Cmd) {
145+func (l *list[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
146+ switch msg := msg.(type) {
147+ case setSelectedMsg:
148+ return l, l.SetSelected(msg.selectedItemID)
149+ case tea.KeyPressMsg:
152+ case key.Matches(msg, l.keyMap.Down):
153+ return l, l.MoveDown(DefaultScrollSize)
154+ case key.Matches(msg, l.keyMap.Up):
155+ return l, l.MoveUp(DefaultScrollSize)
156+ case key.Matches(msg, l.keyMap.DownOneItem):
157+ return l, l.SelectItemBelow()
158+ case key.Matches(msg, l.keyMap.UpOneItem):
159+ return l, l.SelectItemAbove()
160+ case key.Matches(msg, l.keyMap.HalfPageDown):
161+ return l, l.MoveDown(l.listHeight() / 2)
162+ case key.Matches(msg, l.keyMap.HalfPageUp):
163+ return l, l.MoveUp(l.listHeight() / 2)
164+ case key.Matches(msg, l.keyMap.PageDown):
165+ return l, l.MoveDown(l.listHeight())
166+ case key.Matches(msg, l.keyMap.PageUp):
167+ return l, l.MoveUp(l.listHeight())
168+ case key.Matches(msg, l.keyMap.End):
169+ return l, l.GoToBottom()
170+ case key.Matches(msg, l.keyMap.Home):
171+ return l, l.GoToTop()
125 return l, nil
175 return l, nil
128// View implements List.
178// View implements List.
129−func (l *list) View() string {
179+func (l *list[T]) View() string {
130 if l.height <= 0 || l.width <= 0 {
180 if l.height <= 0 || l.width <= 0 {
131 return ""
181 return ""
@@ -138,7 +188,7 @@ func (l *list) View() string {
138 return strings.Join(lines, "\n")
188 return strings.Join(lines, "\n")
141−func (l *list) viewPosition() (int, int) {
191+func (l *list[T]) viewPosition() (int, int) {
142 start, end := 0, 0
192 start, end := 0, 0
143 renderedLines := lipgloss.Height(l.rendered) - 1
193 renderedLines := lipgloss.Height(l.rendered) - 1
144 if l.direction == Forward {
194 if l.direction == Forward {
@@ -151,7 +201,7 @@ func (l *list) viewPosition() (int, int) {
151 return start, end
201 return start, end
154−func (l *list) renderItem(item Item) renderedItem {
204+func (l *list[T]) renderItem(item Item) renderedItem {
155 view := item.View()
205 view := item.View()
156 return renderedItem{
206 return renderedItem{
157 id: item.ID(),
207 id: item.ID(),
@@ -160,7 +210,7 @@ func (l *list) renderItem(item Item) renderedItem {
163−func (l *list) renderView() {
213+func (l *list[T]) renderView() {
164 var sb strings.Builder
214 var sb strings.Builder
165 for i, rendered := range l.renderedItems {
215 for i, rendered := range l.renderedItems {
166 sb.WriteString(rendered.view)
216 sb.WriteString(rendered.view)
@@ -171,7 +221,7 @@ func (l *list) renderView() {
171 l.rendered = sb.String()
221 l.rendered = sb.String()
174−func (l *list) incrementOffset(n int) {
224+func (l *list[T]) incrementOffset(n int) {
175 if !l.isReady {
225 if !l.isReady {
@@ -188,7 +238,7 @@ func (l *list) incrementOffset(n int) {
188 l.offset += n
238 l.offset += n
191−func (l *list) decrementOffset(n int) {
241+func (l *list[T]) decrementOffset(n int) {
192 if !l.isReady {
242 if !l.isReady {
@@ -203,7 +253,7 @@ func (l *list) decrementOffset(n int) {
205// changeSelectedWhenNotVisible is called so we make sure we move to the next available selected that is visible
255// changeSelectedWhenNotVisible is called so we make sure we move to the next available selected that is visible
206−func (l *list) changeSelectedWhenNotVisible() tea.Cmd {
256+func (l *list[T]) changeSelectedWhenNotVisible() tea.Cmd {
207 var cmds []tea.Cmd
257 var cmds []tea.Cmd
208 start, end := l.viewPosition()
258 start, end := l.viewPosition()
209 currentPosition := 0
259 currentPosition := 0
@@ -228,7 +278,7 @@ func (l *list) changeSelectedWhenNotVisible() tea.Cmd {
228 needsMove = true
278 needsMove = true
230 if needsMove {
280 if needsMove {
231− if focusable, ok := item.(layout.Focusable); ok {
281+ if focusable, ok := any(item).(layout.Focusable); ok {
232 cmds = append(cmds, focusable.Blur())
282 cmds = append(cmds, focusable.Blur())
234 l.renderedItems[i] = l.renderItem(item)
284 l.renderedItems[i] = l.renderItem(item)
@@ -239,7 +289,7 @@ func (l *list) changeSelectedWhenNotVisible() tea.Cmd {
239 if itemWithinView != NotFound && needsMove {
289 if itemWithinView != NotFound && needsMove {
240 newSelection := l.items[itemWithinView]
290 newSelection := l.items[itemWithinView]
241 l.selectedItem = newSelection.ID()
291 l.selectedItem = newSelection.ID()
242− if focusable, ok := newSelection.(layout.Focusable); ok {
292+ if focusable, ok := any(newSelection).(layout.Focusable); ok {
243 cmds = append(cmds, focusable.Focus())
293 cmds = append(cmds, focusable.Focus())
245 l.renderedItems[itemWithinView] = l.renderItem(newSelection)
295 l.renderedItems[itemWithinView] = l.renderItem(newSelection)
@@ -251,7 +301,7 @@ func (l *list) changeSelectedWhenNotVisible() tea.Cmd {
251 return tea.Batch(cmds...)
301 return tea.Batch(cmds...)
254−func (l *list) MoveUp(n int) tea.Cmd {
304+func (l *list[T]) MoveUp(n int) tea.Cmd {
255 if l.direction == Forward {
305 if l.direction == Forward {
256 l.decrementOffset(n)
306 l.decrementOffset(n)
@@ -260,7 +310,7 @@ func (l *list) MoveUp(n int) tea.Cmd {
260 return l.changeSelectedWhenNotVisible()
310 return l.changeSelectedWhenNotVisible()
263−func (l *list) MoveDown(n int) tea.Cmd {
313+func (l *list[T]) MoveDown(n int) tea.Cmd {
264 if l.direction == Forward {
314 if l.direction == Forward {
265 l.incrementOffset(n)
315 l.incrementOffset(n)
@@ -269,49 +319,80 @@ func (l *list) MoveDown(n int) tea.Cmd {
269 return l.changeSelectedWhenNotVisible()
319 return l.changeSelectedWhenNotVisible()
272−func (l *list) firstSelectableItemBefore(inx int) int {
322+func (l *list[T]) firstSelectableItemBefore(inx int) int {
273 for i := inx - 1; i >= 0; i-- {
323 for i := inx - 1; i >= 0; i-- {
274− if _, ok := l.items[i].(layout.Focusable); ok {
324+ if _, ok := any(l.items[i]).(layout.Focusable); ok {
328+ if inx == 0 && l.wrap {
329+ return l.firstSelectableItemBefore(len(l.items))
278 return NotFound
331 return NotFound
281−func (l *list) firstSelectableItemAfter(inx int) int {
334+func (l *list[T]) firstSelectableItemAfter(inx int) int {
282 for i := inx + 1; i < len(l.items); i++ {
335 for i := inx + 1; i < len(l.items); i++ {
283− if _, ok := l.items[i].(layout.Focusable); ok {
336+ if _, ok := any(l.items[i]).(layout.Focusable); ok {
340+ if inx == len(l.items)-1 && l.wrap {
341+ return l.firstSelectableItemAfter(-1)
287 return NotFound
343 return NotFound
290−func (l *list) moveToSelected() {
346+func (l *list[T]) moveToSelected(center bool) tea.Cmd {
291 if l.selectedItem == "" || !l.isReady {
348 if l.selectedItem == "" || !l.isReady {
292− return
349+ return nil
294 currentPosition := 0
351 currentPosition := 0
295 start, end := l.viewPosition()
352 start, end := l.viewPosition()
296 for _, item := range l.renderedItems {
353 for _, item := range l.renderedItems {
297 if item.id == l.selectedItem {
354 if item.id == l.selectedItem {
355+ itemStart := currentPosition
298− if start <= currentPosition && (currentPosition+item.height) <= end {
356+ itemEnd := currentPosition + item.height - 1
358+ if start <= itemStart && itemEnd <= end {
299− return
359+ return nil
302− if currentPosition < start {
303− l.MoveUp(start - currentPosition)
305− // we need to go down
361+
363+ viewportCenter := l.listHeight() / 2
364+ itemCenter := itemStart + item.height/2
365+ targetOffset := itemCenter - viewportCenter
366+ if l.direction == Forward {
367+ if targetOffset > l.offset {
368+ cmds = append(cmds, l.MoveDown(targetOffset-l.offset))
369+ } else if targetOffset < l.offset {
370+ cmds = append(cmds, l.MoveUp(l.offset-targetOffset))
373+ renderedHeight := lipgloss.Height(l.rendered)
374+ backwardTargetOffset := renderedHeight - targetOffset - l.listHeight()
375+ if backwardTargetOffset > l.offset {
376+ cmds = append(cmds, l.MoveUp(backwardTargetOffset-l.offset))
377+ } else if backwardTargetOffset < l.offset {
378+ cmds = append(cmds, l.MoveDown(l.offset-backwardTargetOffset))
306− if currentPosition > end {
382+ if currentPosition < start {
383+ cmds = append(cmds, l.MoveUp(start-currentPosition))
385+ if currentPosition > end {
307− l.MoveDown(currentPosition - end)
386+ cmds = append(cmds, l.MoveDown(currentPosition-end))
310 currentPosition += item.height + l.gap
390 currentPosition += item.height + l.gap
392+ return tea.Batch(cmds...)
314−func (l *list) SelectItemAbove() tea.Cmd {
395+func (l *list[T]) SelectItemAbove() tea.Cmd {
315 if !l.isReady {
396 if !l.isReady {
316 return nil
397 return nil
@@ -324,14 +405,14 @@ func (l *list) SelectItemAbove() tea.Cmd {
324 return nil
405 return nil
326 // blur the current item
407 // blur the current item
327− if focusable, ok := item.(layout.Focusable); ok {
408+ if focusable, ok := any(item).(layout.Focusable); ok {
328 cmds = append(cmds, focusable.Blur())
409 cmds = append(cmds, focusable.Blur())
330 // rerender the item
411 // rerender the item
331 l.renderedItems[i] = l.renderItem(item)
412 l.renderedItems[i] = l.renderItem(item)
332 // focus the item above
413 // focus the item above
333 above := l.items[inx]
414 above := l.items[inx]
334− if focusable, ok := above.(layout.Focusable); ok {
415+ if focusable, ok := any(above).(layout.Focusable); ok {
335 cmds = append(cmds, focusable.Focus())
416 cmds = append(cmds, focusable.Focus())
337 // rerender the item
418 // rerender the item
@@ -340,12 +421,12 @@ func (l *list) SelectItemAbove() tea.Cmd {
424+ l.moveToSelected(false)
343 l.renderView()
425 l.renderView()
345 return tea.Batch(cmds...)
426 return tea.Batch(cmds...)
348−func (l *list) SelectItemBelow() tea.Cmd {
429+func (l *list[T]) SelectItemBelow() tea.Cmd {
349 if !l.isReady {
430 if !l.isReady {
350 return nil
431 return nil
@@ -358,7 +439,7 @@ func (l *list) SelectItemBelow() tea.Cmd {
358 return nil
439 return nil
360 // blur the current item
441 // blur the current item
361− if focusable, ok := item.(layout.Focusable); ok {
442+ if focusable, ok := any(item).(layout.Focusable); ok {
362 cmds = append(cmds, focusable.Blur())
443 cmds = append(cmds, focusable.Blur())
364 // rerender the item
445 // rerender the item
@@ -366,7 +447,7 @@ func (l *list) SelectItemBelow() tea.Cmd {
367 // focus the item below
448 // focus the item below
368 below := l.items[inx]
449 below := l.items[inx]
369− if focusable, ok := below.(layout.Focusable); ok {
450+ if focusable, ok := any(below).(layout.Focusable); ok {
370 cmds = append(cmds, focusable.Focus())
451 cmds = append(cmds, focusable.Focus())
372 // rerender the item
453 // rerender the item
@@ -376,12 +457,12 @@ func (l *list) SelectItemBelow() tea.Cmd {
460+ l.moveToSelected(false)
379 l.renderView()
461 l.renderView()
381 return tea.Batch(cmds...)
462 return tea.Batch(cmds...)
384−func (l *list) GoToTop() tea.Cmd {
465+func (l *list[T]) GoToTop() tea.Cmd {
385 if !l.isReady {
466 if !l.isReady {
386 return nil
467 return nil
@@ -390,7 +471,7 @@ func (l *list) GoToTop() tea.Cmd {
390 return tea.Batch(l.selectFirstItem(), l.renderForward())
471 return tea.Batch(l.selectFirstItem(), l.renderForward())
393−func (l *list) GoToBottom() tea.Cmd {
474+func (l *list[T]) GoToBottom() tea.Cmd {
394 if !l.isReady {
475 if !l.isReady {
395 return nil
476 return nil
@@ -400,7 +481,7 @@ func (l *list) GoToBottom() tea.Cmd {
400 return tea.Batch(l.selectLastItem(), l.renderBackward())
481 return tea.Batch(l.selectLastItem(), l.renderBackward())
403−func (l *list) renderForward() tea.Cmd {
484+func (l *list[T]) renderForward() tea.Cmd {
404 // TODO: figure out a way to preserve items that did not change
485 // TODO: figure out a way to preserve items that did not change
405 l.renderedItems = make([]renderedItem, 0)
486 l.renderedItems = make([]renderedItem, 0)
406 currentHeight := 0
487 currentHeight := 0
@@ -434,13 +515,12 @@ func (l *list) renderForward() tea.Cmd {
437−func (l *list) renderBackward() tea.Cmd {
518+func (l *list[T]) renderBackward() tea.Cmd {
438 // TODO: figure out a way to preserve items that did not change
519 // TODO: figure out a way to preserve items that did not change
439 l.renderedItems = make([]renderedItem, 0)
520 l.renderedItems = make([]renderedItem, 0)
440 currentHeight := 0
521 currentHeight := 0
441 currentIndex := 0
522 currentIndex := 0
442 for i := len(l.items) - 1; i >= 0; i-- {
523 for i := len(l.items) - 1; i >= 0; i-- {
443− fmt.Printf("rendering item %d\n", i)
444 currentIndex = i
524 currentIndex = i
445 if currentHeight > l.listHeight() {
525 if currentHeight > l.listHeight() {
@@ -457,7 +537,6 @@ func (l *list) renderBackward() tea.Cmd {
458 return func() tea.Msg {
538 return func() tea.Msg {
459 for i := currentIndex; i >= 0; i-- {
539 for i := currentIndex; i >= 0; i-- {
460− fmt.Printf("rendering item after %d\n", i)
461 rendered := l.renderItem(l.items[i])
540 rendered := l.renderItem(l.items[i])
462 l.renderedItems = append([]renderedItem{rendered}, l.renderedItems...)
541 l.renderedItems = append([]renderedItem{rendered}, l.renderedItems...)
@@ -467,31 +546,31 @@ func (l *list) renderBackward() tea.Cmd {
470−func (l *list) selectFirstItem() tea.Cmd {
549+func (l *list[T]) selectFirstItem() tea.Cmd {
471 var cmd tea.Cmd
550 var cmd tea.Cmd
472 inx := l.firstSelectableItemAfter(-1)
551 inx := l.firstSelectableItemAfter(-1)
473 if inx != NotFound {
552 if inx != NotFound {
474 l.selectedItem = l.items[inx].ID()
553 l.selectedItem = l.items[inx].ID()
475− if focusable, ok := l.items[inx].(layout.Focusable); ok {
554+ if focusable, ok := any(l.items[inx]).(layout.Focusable); ok {
476 cmd = focusable.Focus()
555 cmd = focusable.Focus()
479 return cmd
558 return cmd
482−func (l *list) selectLastItem() tea.Cmd {
561+func (l *list[T]) selectLastItem() tea.Cmd {
483 var cmd tea.Cmd
562 var cmd tea.Cmd
484 inx := l.firstSelectableItemBefore(len(l.items))
563 inx := l.firstSelectableItemBefore(len(l.items))
485 if inx != NotFound {
564 if inx != NotFound {
486 l.selectedItem = l.items[inx].ID()
565 l.selectedItem = l.items[inx].ID()
487− if focusable, ok := l.items[inx].(layout.Focusable); ok {
566+ if focusable, ok := any(l.items[inx]).(layout.Focusable); ok {
488 cmd = focusable.Focus()
567 cmd = focusable.Focus()
491 return cmd
570 return cmd
494−func (l *list) renderItems() tea.Cmd {
573+func (l *list[T]) renderItems() tea.Cmd {
495 if l.height <= 0 || l.width <= 0 {
574 if l.height <= 0 || l.width <= 0 {
496 return nil
575 return nil
@@ -512,12 +591,12 @@ func (l *list) renderItems() tea.Cmd {
512 return l.renderBackward()
591 return l.renderBackward()
515−func (l *list) listHeight() int {
594+func (l *list[T]) listHeight() int {
516 // for the moment its the same
595 // for the moment its the same
517 return l.height
596 return l.height
520−func (l *list) SetItems(items []Item) tea.Cmd {
599+func (l *list[T]) SetItems(items []T) tea.Cmd {
521 l.items = items
600 l.items = items
522 var cmds []tea.Cmd
601 var cmds []tea.Cmd
523 for _, item := range l.items {
602 for _, item := range l.items {
@@ -525,36 +604,41 @@ func (l *list) SetItems(items []Item) tea.Cmd {
525 // Set height to 0 to let the item calculate its own height
604 // Set height to 0 to let the item calculate its own height
526 cmds = append(cmds, item.SetSize(l.width, 0))
605 cmds = append(cmds, item.SetSize(l.width, 0))
608+ if l.selectedItem != "" {
609+ cmds = append(cmds, l.moveToSelected(true))
528 cmds = append(cmds, l.renderItems())
611 cmds = append(cmds, l.renderItems())
529 return tea.Batch(cmds...)
612 return tea.Batch(cmds...)
532// GetSize implements List.
615// GetSize implements List.
533−func (l *list) GetSize() (int, int) {
616+func (l *list[T]) GetSize() (int, int) {
534 return l.width, l.height
617 return l.width, l.height
537// SetSize implements List.
620// SetSize implements List.
538−func (l *list) SetSize(width int, height int) tea.Cmd {
621+func (l *list[T]) SetSize(width int, height int) tea.Cmd {
539 l.width = width
622 l.width = width
540 l.height = height
623 l.height = height
541 var cmds []tea.Cmd
624 var cmds []tea.Cmd
542 for _, item := range l.items {
625 for _, item := range l.items {
543 cmds = append(cmds, item.SetSize(width, height))
626 cmds = append(cmds, item.SetSize(width, height))
545 cmds = append(cmds, l.renderItems())
629 cmds = append(cmds, l.renderItems())
546 return tea.Batch(cmds...)
630 return tea.Batch(cmds...)
549// Blur implements List.
633// Blur implements List.
550−func (l *list) Blur() tea.Cmd {
634+func (l *list[T]) Blur() tea.Cmd {
551 var cmd tea.Cmd
635 var cmd tea.Cmd
552 l.focused = false
636 l.focused = false
553 for i, item := range l.items {
637 for i, item := range l.items {
554 if item.ID() != l.selectedItem {
638 if item.ID() != l.selectedItem {
557− if focusable, ok := item.(layout.Focusable); ok {
641+ if focusable, ok := any(item).(layout.Focusable); ok {
558 cmd = focusable.Blur()
642 cmd = focusable.Blur()
560 l.renderedItems[i] = l.renderItem(item)
644 l.renderedItems[i] = l.renderItem(item)
@@ -564,23 +648,64 @@ func (l *list) Blur() tea.Cmd {
566// Focus implements List.
650// Focus implements List.
567−func (l *list) Focus() tea.Cmd {
651+func (l *list[T]) Focus() tea.Cmd {
568 var cmd tea.Cmd
652 var cmd tea.Cmd
569 l.focused = true
653 l.focused = true
654+ if l.selectedItem != "" {
570− for i, item := range l.items {
655+ for i, item := range l.items {
571− if item.ID() != l.selectedItem {
656+ if item.ID() != l.selectedItem {
572− continue
657+ continue
659+ if focusable, ok := any(item).(layout.Focusable); ok {
660+ cmd = focusable.Focus()
662+ if len(l.renderedItems) > i {
663+ l.renderedItems[i] = l.renderItem(item)
574− if focusable, ok := item.(layout.Focusable); ok {
666+ l.renderView()
671+func (l *list[T]) SetSelected(id string) tea.Cmd {
672+ if l.selectedItem == id {
676+ for i, item := range l.items {
677+ if item.ID() == l.selectedItem {
678+ if focusable, ok := any(item).(layout.Focusable); ok {
679+ cmds = append(cmds, focusable.Blur())
681+ if len(l.renderedItems) > i {
682+ l.renderedItems[i] = l.renderItem(item)
684+ } else if item.ID() == id {
685+ if focusable, ok := any(item).(layout.Focusable); ok {
575− cmd = focusable.Focus()
686+ cmds = append(cmds, focusable.Focus())
688+ if len(l.renderedItems) > i {
689+ l.renderedItems[i] = l.renderItem(item)
577− l.renderedItems[i] = l.renderItem(item)
694+ cmds = append(cmds, l.moveToSelected(true))
579 l.renderView()
695 l.renderView()
580− return cmd
696+ return tea.Batch(cmds...)
699+func (l *list[T]) SelectedItem() *T {
700+ for _, item := range l.items {
701+ if item.ID() == l.selectedItem {
583// IsFocused implements List.
708// IsFocused implements List.
584−func (l *list) IsFocused() bool {
709+func (l *list[T]) IsFocused() bool {
585 return l.focused
710 return l.focused