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