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	// 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}