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