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 c.selectedIdx = 0
101 c.items = items
102}
103
104func (c *simpleListCmp[T]) GetItems() []T {
105 return c.items
106}
107
108func (c *simpleListCmp[T]) SetMaxWidth(width int) {
109 c.maxWidth = width
110}
111
112func (c *simpleListCmp[T]) View() string {
113 t := theme.CurrentTheme()
114 baseStyle := styles.BaseStyle()
115
116 items := c.items
117 maxWidth := c.maxWidth
118 maxVisibleItems := min(c.maxVisibleItems, len(items))
119 startIdx := 0
120
121 if len(items) <= 0 {
122 return baseStyle.
123 Background(t.Background()).
124 Padding(0, 1).
125 Width(maxWidth).
126 Render(c.fallbackMsg)
127 }
128
129 if len(items) > maxVisibleItems {
130 halfVisible := maxVisibleItems / 2
131 if c.selectedIdx >= halfVisible && c.selectedIdx < len(items)-halfVisible {
132 startIdx = c.selectedIdx - halfVisible
133 } else if c.selectedIdx >= len(items)-halfVisible {
134 startIdx = len(items) - maxVisibleItems
135 }
136 }
137
138 endIdx := min(startIdx+maxVisibleItems, len(items))
139
140 listItems := make([]string, 0, maxVisibleItems)
141
142 for i := startIdx; i < endIdx; i++ {
143 item := items[i]
144 title := item.Render(i == c.selectedIdx, maxWidth)
145 listItems = append(listItems, title)
146 }
147
148 return lipgloss.JoinVertical(lipgloss.Left, listItems...)
149}
150
151func NewSimpleList[T SimpleListItem](items []T, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) SimpleList[T] {
152 return &simpleListCmp[T]{
153 fallbackMsg: fallbackMsg,
154 items: items,
155 maxVisibleItems: maxVisibleItems,
156 useAlphaNumericKeys: useAlphaNumericKeys,
157 selectedIdx: 0,
158 }
159}