help.go

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