simple-list.go

  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}