list.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. package list
  2. import (
  3. "strings"
  4. "github.com/charmbracelet/bubbles/v2/key"
  5. tea "github.com/charmbracelet/bubbletea/v2"
  6. "github.com/muesli/reflow/truncate"
  7. "github.com/sst/opencode/internal/styles"
  8. "github.com/sst/opencode/internal/theme"
  9. )
  10. type ListItem interface {
  11. Render(selected bool, width int) string
  12. }
  13. type List[T ListItem] interface {
  14. tea.Model
  15. tea.ViewModel
  16. SetMaxWidth(maxWidth int)
  17. GetSelectedItem() (item T, idx int)
  18. SetItems(items []T)
  19. GetItems() []T
  20. SetSelectedIndex(idx int)
  21. SetEmptyMessage(msg string)
  22. IsEmpty() bool
  23. }
  24. type listComponent[T ListItem] struct {
  25. fallbackMsg string
  26. items []T
  27. selectedIdx int
  28. maxWidth int
  29. maxVisibleItems int
  30. useAlphaNumericKeys bool
  31. width int
  32. height int
  33. }
  34. type listKeyMap struct {
  35. Up key.Binding
  36. Down key.Binding
  37. UpAlpha key.Binding
  38. DownAlpha key.Binding
  39. }
  40. var simpleListKeys = listKeyMap{
  41. Up: key.NewBinding(
  42. key.WithKeys("up"),
  43. key.WithHelp("↑", "previous list item"),
  44. ),
  45. Down: key.NewBinding(
  46. key.WithKeys("down"),
  47. key.WithHelp("↓", "next list item"),
  48. ),
  49. UpAlpha: key.NewBinding(
  50. key.WithKeys("k"),
  51. key.WithHelp("k", "previous list item"),
  52. ),
  53. DownAlpha: key.NewBinding(
  54. key.WithKeys("j"),
  55. key.WithHelp("j", "next list item"),
  56. ),
  57. }
  58. func (c *listComponent[T]) Init() tea.Cmd {
  59. return nil
  60. }
  61. func (c *listComponent[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  62. switch msg := msg.(type) {
  63. case tea.KeyMsg:
  64. switch {
  65. case key.Matches(msg, simpleListKeys.Up) || (c.useAlphaNumericKeys && key.Matches(msg, simpleListKeys.UpAlpha)):
  66. if c.selectedIdx > 0 {
  67. c.selectedIdx--
  68. }
  69. return c, nil
  70. case key.Matches(msg, simpleListKeys.Down) || (c.useAlphaNumericKeys && key.Matches(msg, simpleListKeys.DownAlpha)):
  71. if c.selectedIdx < len(c.items)-1 {
  72. c.selectedIdx++
  73. }
  74. return c, nil
  75. }
  76. }
  77. return c, nil
  78. }
  79. func (c *listComponent[T]) GetSelectedItem() (T, int) {
  80. if len(c.items) > 0 {
  81. return c.items[c.selectedIdx], c.selectedIdx
  82. }
  83. var zero T
  84. return zero, -1
  85. }
  86. func (c *listComponent[T]) SetItems(items []T) {
  87. c.selectedIdx = 0
  88. c.items = items
  89. }
  90. func (c *listComponent[T]) GetItems() []T {
  91. return c.items
  92. }
  93. func (c *listComponent[T]) SetEmptyMessage(msg string) {
  94. c.fallbackMsg = msg
  95. }
  96. func (c *listComponent[T]) IsEmpty() bool {
  97. return len(c.items) == 0
  98. }
  99. func (c *listComponent[T]) SetMaxWidth(width int) {
  100. c.maxWidth = width
  101. }
  102. func (c *listComponent[T]) SetSelectedIndex(idx int) {
  103. if idx >= 0 && idx < len(c.items) {
  104. c.selectedIdx = idx
  105. }
  106. }
  107. func (c *listComponent[T]) View() string {
  108. items := c.items
  109. maxWidth := c.maxWidth
  110. if maxWidth == 0 {
  111. maxWidth = 80 // Default width if not set
  112. }
  113. maxVisibleItems := min(c.maxVisibleItems, len(items))
  114. startIdx := 0
  115. if len(items) <= 0 {
  116. return c.fallbackMsg
  117. }
  118. if len(items) > maxVisibleItems {
  119. halfVisible := maxVisibleItems / 2
  120. if c.selectedIdx >= halfVisible && c.selectedIdx < len(items)-halfVisible {
  121. startIdx = c.selectedIdx - halfVisible
  122. } else if c.selectedIdx >= len(items)-halfVisible {
  123. startIdx = len(items) - maxVisibleItems
  124. }
  125. }
  126. endIdx := min(startIdx+maxVisibleItems, len(items))
  127. listItems := make([]string, 0, maxVisibleItems)
  128. for i := startIdx; i < endIdx; i++ {
  129. item := items[i]
  130. title := item.Render(i == c.selectedIdx, maxWidth)
  131. listItems = append(listItems, title)
  132. }
  133. return strings.Join(listItems, "\n")
  134. }
  135. func NewListComponent[T ListItem](items []T, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) List[T] {
  136. return &listComponent[T]{
  137. fallbackMsg: fallbackMsg,
  138. items: items,
  139. maxVisibleItems: maxVisibleItems,
  140. useAlphaNumericKeys: useAlphaNumericKeys,
  141. selectedIdx: 0,
  142. }
  143. }
  144. // StringItem is a simple implementation of ListItem for string values
  145. type StringItem string
  146. func (s StringItem) Render(selected bool, width int) string {
  147. t := theme.CurrentTheme()
  148. baseStyle := styles.NewStyle()
  149. truncatedStr := truncate.StringWithTail(string(s), uint(width-1), "...")
  150. var itemStyle styles.Style
  151. if selected {
  152. itemStyle = baseStyle.
  153. Background(t.Primary()).
  154. Foreground(t.BackgroundElement()).
  155. Width(width).
  156. PaddingLeft(1)
  157. } else {
  158. itemStyle = baseStyle.
  159. Foreground(t.TextMuted()).
  160. PaddingLeft(1)
  161. }
  162. return itemStyle.Render(truncatedStr)
  163. }
  164. // NewStringList creates a new list component with string items
  165. func NewStringList(items []string, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) List[StringItem] {
  166. stringItems := make([]StringItem, len(items))
  167. for i, item := range items {
  168. stringItems[i] = StringItem(item)
  169. }
  170. return NewListComponent(stringItems, maxVisibleItems, fallbackMsg, useAlphaNumericKeys)
  171. }