help.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. package dialog
  2. import (
  3. "strings"
  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/styles"
  8. "github.com/opencode-ai/opencode/internal/tui/theme"
  9. )
  10. type helpCmp struct {
  11. width int
  12. height int
  13. keys []key.Binding
  14. }
  15. func (h *helpCmp) Init() tea.Cmd {
  16. return nil
  17. }
  18. func (h *helpCmp) SetBindings(k []key.Binding) {
  19. h.keys = k
  20. }
  21. func (h *helpCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  22. switch msg := msg.(type) {
  23. case tea.WindowSizeMsg:
  24. h.width = 90
  25. h.height = msg.Height
  26. }
  27. return h, nil
  28. }
  29. func removeDuplicateBindings(bindings []key.Binding) []key.Binding {
  30. seen := make(map[string]struct{})
  31. result := make([]key.Binding, 0, len(bindings))
  32. // Process bindings in reverse order
  33. for i := len(bindings) - 1; i >= 0; i-- {
  34. b := bindings[i]
  35. k := strings.Join(b.Keys(), " ")
  36. if _, ok := seen[k]; ok {
  37. // duplicate, skip
  38. continue
  39. }
  40. seen[k] = struct{}{}
  41. // Add to the beginning of result to maintain original order
  42. result = append([]key.Binding{b}, result...)
  43. }
  44. return result
  45. }
  46. func (h *helpCmp) render() string {
  47. t := theme.CurrentTheme()
  48. baseStyle := styles.BaseStyle()
  49. helpKeyStyle := styles.Bold().
  50. Background(t.Background()).
  51. Foreground(t.Text()).
  52. Padding(0, 1, 0, 0)
  53. helpDescStyle := styles.Regular().
  54. Background(t.Background()).
  55. Foreground(t.TextMuted())
  56. // Compile list of bindings to render
  57. bindings := removeDuplicateBindings(h.keys)
  58. // Enumerate through each group of bindings, populating a series of
  59. // pairs of columns, one for keys, one for descriptions
  60. var (
  61. pairs []string
  62. width int
  63. rows = 10 - 2
  64. )
  65. for i := 0; i < len(bindings); i += rows {
  66. var (
  67. keys []string
  68. descs []string
  69. )
  70. for j := i; j < min(i+rows, len(bindings)); j++ {
  71. keys = append(keys, helpKeyStyle.Render(bindings[j].Help().Key))
  72. descs = append(descs, helpDescStyle.Render(bindings[j].Help().Desc))
  73. }
  74. // Render pair of columns; beyond the first pair, render a three space
  75. // left margin, in order to visually separate the pairs.
  76. var cols []string
  77. if len(pairs) > 0 {
  78. cols = []string{baseStyle.Render(" ")}
  79. }
  80. maxDescWidth := 0
  81. for _, desc := range descs {
  82. if maxDescWidth < lipgloss.Width(desc) {
  83. maxDescWidth = lipgloss.Width(desc)
  84. }
  85. }
  86. for i := range descs {
  87. remainingWidth := maxDescWidth - lipgloss.Width(descs[i])
  88. if remainingWidth > 0 {
  89. descs[i] = descs[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
  90. }
  91. }
  92. maxKeyWidth := 0
  93. for _, key := range keys {
  94. if maxKeyWidth < lipgloss.Width(key) {
  95. maxKeyWidth = lipgloss.Width(key)
  96. }
  97. }
  98. for i := range keys {
  99. remainingWidth := maxKeyWidth - lipgloss.Width(keys[i])
  100. if remainingWidth > 0 {
  101. keys[i] = keys[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
  102. }
  103. }
  104. cols = append(cols,
  105. strings.Join(keys, "\n"),
  106. strings.Join(descs, "\n"),
  107. )
  108. pair := baseStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, cols...))
  109. // check whether it exceeds the maximum width avail (the width of the
  110. // terminal, subtracting 2 for the borders).
  111. width += lipgloss.Width(pair)
  112. if width > h.width-2 {
  113. break
  114. }
  115. pairs = append(pairs, pair)
  116. }
  117. // https://github.com/charmbracelet/lipgloss/issues/209
  118. if len(pairs) > 1 {
  119. prefix := pairs[:len(pairs)-1]
  120. lastPair := pairs[len(pairs)-1]
  121. prefix = append(prefix, lipgloss.Place(
  122. lipgloss.Width(lastPair), // width
  123. lipgloss.Height(prefix[0]), // height
  124. lipgloss.Left, // x
  125. lipgloss.Top, // y
  126. lastPair, // content
  127. lipgloss.WithWhitespaceBackground(t.Background()),
  128. ))
  129. content := baseStyle.Width(h.width).Render(
  130. lipgloss.JoinHorizontal(
  131. lipgloss.Top,
  132. prefix...,
  133. ),
  134. )
  135. return content
  136. }
  137. // Join pairs of columns and enclose in a border
  138. content := baseStyle.Width(h.width).Render(
  139. lipgloss.JoinHorizontal(
  140. lipgloss.Top,
  141. pairs...,
  142. ),
  143. )
  144. return content
  145. }
  146. func (h *helpCmp) View() string {
  147. t := theme.CurrentTheme()
  148. baseStyle := styles.BaseStyle()
  149. content := h.render()
  150. header := baseStyle.
  151. Bold(true).
  152. Width(lipgloss.Width(content)).
  153. Foreground(t.Primary()).
  154. Render("Keyboard Shortcuts")
  155. return baseStyle.Padding(1).
  156. Border(lipgloss.RoundedBorder()).
  157. BorderForeground(t.TextMuted()).
  158. Width(h.width).
  159. BorderBackground(t.Background()).
  160. Render(
  161. lipgloss.JoinVertical(lipgloss.Center,
  162. header,
  163. baseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))),
  164. content,
  165. ),
  166. )
  167. }
  168. type HelpCmp interface {
  169. tea.Model
  170. SetBindings([]key.Binding)
  171. }
  172. func NewHelpCmp() HelpCmp {
  173. return &helpCmp{}
  174. }