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}