help.go 4.5 KB

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