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() string {
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 baseStyle.
124			Background(t.Background()).
125			Padding(0, 1).
126			Width(maxWidth).
127			Render(c.fallbackMsg)
128	}
129
130	if len(items) > maxVisibleItems {
131		halfVisible := maxVisibleItems / 2
132		if c.selectedIdx >= halfVisible && c.selectedIdx < len(items)-halfVisible {
133			startIdx = c.selectedIdx - halfVisible
134		} else if c.selectedIdx >= len(items)-halfVisible {
135			startIdx = len(items) - maxVisibleItems
136		}
137	}
138
139	endIdx := min(startIdx+maxVisibleItems, len(items))
140
141	listItems := make([]string, 0, maxVisibleItems)
142
143	for i := startIdx; i < endIdx; i++ {
144		item := items[i]
145		title := item.Render(i == c.selectedIdx, maxWidth)
146		listItems = append(listItems, title)
147	}
148
149	return lipgloss.JoinVertical(lipgloss.Left, listItems...)
150}
151
152func NewSimpleList[T SimpleListItem](items []T, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) SimpleList[T] {
153	return &simpleListCmp[T]{
154		fallbackMsg:         fallbackMsg,
155		items:               items,
156		maxVisibleItems:     maxVisibleItems,
157		useAlphaNumericKeys: useAlphaNumericKeys,
158		selectedIdx:         0,
159	}
160}