simple-list.go

  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}