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