list.go 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. package list
  2. import (
  3. "github.com/charmbracelet/bubbles/v2/key"
  4. tea "github.com/charmbracelet/bubbletea/v2"
  5. "github.com/charmbracelet/lipgloss/v2"
  6. "github.com/sst/opencode/internal/layout"
  7. )
  8. type ListItem interface {
  9. Render(selected bool, width int) string
  10. }
  11. type List[T ListItem] interface {
  12. layout.ModelWithView
  13. SetMaxWidth(maxWidth int)
  14. GetSelectedItem() (item T, idx int)
  15. SetItems(items []T)
  16. GetItems() []T
  17. SetSelectedIndex(idx int)
  18. IsEmpty() bool
  19. }
  20. type listComponent[T ListItem] struct {
  21. fallbackMsg string
  22. items []T
  23. selectedIdx int
  24. maxWidth int
  25. maxVisibleItems int
  26. useAlphaNumericKeys bool
  27. width int
  28. height int
  29. }
  30. type listKeyMap struct {
  31. Up key.Binding
  32. Down key.Binding
  33. UpAlpha key.Binding
  34. DownAlpha key.Binding
  35. }
  36. var simpleListKeys = listKeyMap{
  37. Up: key.NewBinding(
  38. key.WithKeys("up"),
  39. key.WithHelp("↑", "previous list item"),
  40. ),
  41. Down: key.NewBinding(
  42. key.WithKeys("down"),
  43. key.WithHelp("↓", "next list item"),
  44. ),
  45. UpAlpha: key.NewBinding(
  46. key.WithKeys("k"),
  47. key.WithHelp("k", "previous list item"),
  48. ),
  49. DownAlpha: key.NewBinding(
  50. key.WithKeys("j"),
  51. key.WithHelp("j", "next list item"),
  52. ),
  53. }
  54. func (c *listComponent[T]) Init() tea.Cmd {
  55. return nil
  56. }
  57. func (c *listComponent[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  58. switch msg := msg.(type) {
  59. case tea.KeyMsg:
  60. switch {
  61. case key.Matches(msg, simpleListKeys.Up) || (c.useAlphaNumericKeys && key.Matches(msg, simpleListKeys.UpAlpha)):
  62. if c.selectedIdx > 0 {
  63. c.selectedIdx--
  64. }
  65. return c, nil
  66. case key.Matches(msg, simpleListKeys.Down) || (c.useAlphaNumericKeys && key.Matches(msg, simpleListKeys.DownAlpha)):
  67. if c.selectedIdx < len(c.items)-1 {
  68. c.selectedIdx++
  69. }
  70. return c, nil
  71. }
  72. }
  73. return c, nil
  74. }
  75. func (c *listComponent[T]) GetSelectedItem() (T, int) {
  76. if len(c.items) > 0 {
  77. return c.items[c.selectedIdx], c.selectedIdx
  78. }
  79. var zero T
  80. return zero, -1
  81. }
  82. func (c *listComponent[T]) SetItems(items []T) {
  83. c.selectedIdx = 0
  84. c.items = items
  85. }
  86. func (c *listComponent[T]) GetItems() []T {
  87. return c.items
  88. }
  89. func (c *listComponent[T]) IsEmpty() bool {
  90. return len(c.items) == 0
  91. }
  92. func (c *listComponent[T]) SetMaxWidth(width int) {
  93. c.maxWidth = width
  94. }
  95. func (c *listComponent[T]) SetSelectedIndex(idx int) {
  96. if idx >= 0 && idx < len(c.items) {
  97. c.selectedIdx = idx
  98. }
  99. }
  100. func (c *listComponent[T]) View() string {
  101. items := c.items
  102. maxWidth := c.maxWidth
  103. maxVisibleItems := min(c.maxVisibleItems, len(items))
  104. startIdx := 0
  105. if len(items) <= 0 {
  106. return c.fallbackMsg
  107. }
  108. if len(items) > maxVisibleItems {
  109. halfVisible := maxVisibleItems / 2
  110. if c.selectedIdx >= halfVisible && c.selectedIdx < len(items)-halfVisible {
  111. startIdx = c.selectedIdx - halfVisible
  112. } else if c.selectedIdx >= len(items)-halfVisible {
  113. startIdx = len(items) - maxVisibleItems
  114. }
  115. }
  116. endIdx := min(startIdx+maxVisibleItems, len(items))
  117. listItems := make([]string, 0, maxVisibleItems)
  118. for i := startIdx; i < endIdx; i++ {
  119. item := items[i]
  120. title := item.Render(i == c.selectedIdx, maxWidth)
  121. listItems = append(listItems, title)
  122. }
  123. return lipgloss.JoinVertical(lipgloss.Left, listItems...)
  124. }
  125. func NewListComponent[T ListItem](items []T, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) List[T] {
  126. return &listComponent[T]{
  127. fallbackMsg: fallbackMsg,
  128. items: items,
  129. maxVisibleItems: maxVisibleItems,
  130. useAlphaNumericKeys: useAlphaNumericKeys,
  131. selectedIdx: 0,
  132. }
  133. }