1package utilComponents
2
3import (
4 "github.com/charmbracelet/bubbles/key"
5 tea "github.com/charmbracelet/bubbletea"
6 "github.com/charmbracelet/lipgloss"
7 "github.com/opencode-ai/opencode/internal/tui/layout"
8 "github.com/opencode-ai/opencode/internal/tui/styles"
9 "github.com/opencode-ai/opencode/internal/tui/theme"
10)
11
12type SimpleListItem interface {
13 Render(selected bool, width int) string
14}
15
16type SimpleList[T SimpleListItem] interface {
17 tea.Model
18 layout.Bindings
19 SetMaxWidth(maxWidth int)
20 GetSelectedItem() (item T, idx int)
21 SetItems(items []T)
22 GetItems() []T
23}
24
25type simpleListCmp[T SimpleListItem] struct {
26 fallbackMsg string
27 items []T
28 selectedIdx int
29 maxWidth int
30 maxVisibleItems int
31 useAlphaNumericKeys bool
32 width int
33 height int
34}
35
36type simpleListKeyMap struct {
37 Up key.Binding
38 Down key.Binding
39 UpAlpha key.Binding
40 DownAlpha key.Binding
41}
42
43var simpleListKeys = simpleListKeyMap{
44 Up: key.NewBinding(
45 key.WithKeys("up"),
46 key.WithHelp("↑", "previous list item"),
47 ),
48 Down: key.NewBinding(
49 key.WithKeys("down"),
50 key.WithHelp("↓", "next list item"),
51 ),
52 UpAlpha: key.NewBinding(
53 key.WithKeys("k"),
54 key.WithHelp("k", "previous list item"),
55 ),
56 DownAlpha: key.NewBinding(
57 key.WithKeys("j"),
58 key.WithHelp("j", "next list item"),
59 ),
60}
61
62func (c *simpleListCmp[T]) Init() tea.Cmd {
63 return nil
64}
65
66func (c *simpleListCmp[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
67 switch msg := msg.(type) {
68 case tea.KeyMsg:
69 switch {
70 case key.Matches(msg, simpleListKeys.Up) || (c.useAlphaNumericKeys && key.Matches(msg, simpleListKeys.UpAlpha)):
71 if c.selectedIdx > 0 {
72 c.selectedIdx--
73 }
74 return c, nil
75 case key.Matches(msg, simpleListKeys.Down) || (c.useAlphaNumericKeys && key.Matches(msg, simpleListKeys.DownAlpha)):
76 if c.selectedIdx < len(c.items)-1 {
77 c.selectedIdx++
78 }
79 return c, nil
80 }
81 }
82
83 return c, nil
84}
85
86func (c *simpleListCmp[T]) BindingKeys() []key.Binding {
87 return layout.KeyMapToSlice(simpleListKeys)
88}
89
90func (c *simpleListCmp[T]) GetSelectedItem() (T, int) {
91 if len(c.items) > 0 {
92 return c.items[c.selectedIdx], c.selectedIdx
93 }
94
95 var zero T
96 return zero, -1
97}
98
99func (c *simpleListCmp[T]) SetItems(items []T) {
100 // Preserve the selected index when updating items
101 // Only reset to 0 if the list is empty or the index is out of bounds
102 if len(items) == 0 || c.selectedIdx >= len(items) {
103 c.selectedIdx = 0
104 }
105 c.items = items
106}
107
108func (c *simpleListCmp[T]) GetItems() []T {
109 return c.items
110}
111
112func (c *simpleListCmp[T]) SetMaxWidth(width int) {
113 c.maxWidth = width
114}
115
116func (c *simpleListCmp[T]) View() string {
117 t := theme.CurrentTheme()
118 baseStyle := styles.BaseStyle()
119
120 items := c.items
121 maxWidth := c.maxWidth
122 maxVisibleItems := min(c.maxVisibleItems, len(items))
123 startIdx := 0
124
125 if len(items) <= 0 {
126 return baseStyle.
127 Background(t.Background()).
128 Padding(0, 1).
129 Width(maxWidth).
130 Render(c.fallbackMsg)
131 }
132
133 if len(items) > maxVisibleItems {
134 halfVisible := maxVisibleItems / 2
135 if c.selectedIdx >= halfVisible && c.selectedIdx < len(items)-halfVisible {
136 startIdx = c.selectedIdx - halfVisible
137 } else if c.selectedIdx >= len(items)-halfVisible {
138 startIdx = len(items) - maxVisibleItems
139 }
140 }
141
142 endIdx := min(startIdx+maxVisibleItems, len(items))
143
144 listItems := make([]string, 0, maxVisibleItems)
145
146 for i := startIdx; i < endIdx; i++ {
147 item := items[i]
148 title := item.Render(i == c.selectedIdx, maxWidth)
149 listItems = append(listItems, title)
150 }
151
152 return lipgloss.JoinVertical(lipgloss.Left, listItems...)
153}
154
155func NewSimpleList[T SimpleListItem](items []T, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) SimpleList[T] {
156 return &simpleListCmp[T]{
157 fallbackMsg: fallbackMsg,
158 items: items,
159 maxVisibleItems: maxVisibleItems,
160 useAlphaNumericKeys: useAlphaNumericKeys,
161 selectedIdx: 0,
162 }
163}