Skip to content

Commit 968b753

Browse files
committed
Add contextual help dialog (Ctrl+?)
- Created new help dialog that displays all currently active key bindings - Help is contextual: shows different bindings based on focused panel - Bindings are automatically categorized into General, Control Key Shortcuts, and Other - Added Ctrl+? keybinding to open the help dialog - Dialog is scrollable for when there are many bindings - Inherits from readOnlyScrollDialog for consistent behavior Assisted-By: docker-agent
1 parent ae34273 commit 968b753

2 files changed

Lines changed: 164 additions & 0 deletions

File tree

pkg/tui/dialog/help.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package dialog
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"charm.land/bubbles/v2/key"
8+
tea "charm.land/bubbletea/v2"
9+
"charm.land/lipgloss/v2"
10+
11+
"github.com/docker/docker-agent/pkg/tui/core/layout"
12+
"github.com/docker/docker-agent/pkg/tui/styles"
13+
)
14+
15+
// helpDialog displays all currently active key bindings in a scrollable dialog.
16+
type helpDialog struct {
17+
readOnlyScrollDialog
18+
bindings []key.Binding
19+
}
20+
21+
// NewHelpDialog creates a new help dialog that displays all active key bindings.
22+
func NewHelpDialog(bindings []key.Binding) Dialog {
23+
d := &helpDialog{
24+
bindings: bindings,
25+
}
26+
d.readOnlyScrollDialog = newReadOnlyScrollDialog(
27+
readOnlyScrollDialogSize{
28+
widthPercent: 70,
29+
minWidth: 60,
30+
maxWidth: 100,
31+
heightPercent: 80,
32+
heightMax: 40,
33+
},
34+
d.renderContent,
35+
)
36+
d.helpKeys = []string{"↑↓", "scroll", "Esc", "close"}
37+
return d
38+
}
39+
40+
// renderContent renders the help dialog content.
41+
func (d *helpDialog) renderContent(contentWidth, maxHeight int) []string {
42+
titleStyle := styles.DialogTitleStyle
43+
separatorStyle := styles.DialogSeparatorStyle
44+
keyStyle := styles.DialogHelpStyle.Foreground(styles.TextSecondary).Bold(true)
45+
descStyle := styles.DialogHelpStyle
46+
47+
lines := []string{
48+
titleStyle.Render("Active Key Bindings"),
49+
separatorStyle.Render(strings.Repeat("─", contentWidth)),
50+
"",
51+
}
52+
53+
// Group bindings by category for better organization
54+
// We'll do a simple categorization based on key prefixes
55+
globalBindings := []key.Binding{}
56+
ctrlBindings := []key.Binding{}
57+
otherBindings := []key.Binding{}
58+
59+
for _, binding := range d.bindings {
60+
if len(binding.Keys()) == 0 {
61+
continue
62+
}
63+
keyStr := binding.Keys()[0]
64+
if strings.HasPrefix(keyStr, "ctrl+") {
65+
ctrlBindings = append(ctrlBindings, binding)
66+
} else if keyStr == "esc" || keyStr == "enter" || keyStr == "tab" {
67+
globalBindings = append(globalBindings, binding)
68+
} else {
69+
otherBindings = append(otherBindings, binding)
70+
}
71+
}
72+
73+
// Render global bindings
74+
if len(globalBindings) > 0 {
75+
lines = append(lines, styles.DialogHelpStyle.Bold(true).Render("General"))
76+
lines = append(lines, "")
77+
for _, binding := range globalBindings {
78+
lines = append(lines, d.formatBinding(binding, keyStyle, descStyle, contentWidth))
79+
}
80+
lines = append(lines, "")
81+
}
82+
83+
// Render ctrl bindings
84+
if len(ctrlBindings) > 0 {
85+
lines = append(lines, styles.DialogHelpStyle.Bold(true).Render("Control Key Shortcuts"))
86+
lines = append(lines, "")
87+
for _, binding := range ctrlBindings {
88+
lines = append(lines, d.formatBinding(binding, keyStyle, descStyle, contentWidth))
89+
}
90+
lines = append(lines, "")
91+
}
92+
93+
// Render other bindings
94+
if len(otherBindings) > 0 {
95+
lines = append(lines, styles.DialogHelpStyle.Bold(true).Render("Other"))
96+
lines = append(lines, "")
97+
for _, binding := range otherBindings {
98+
lines = append(lines, d.formatBinding(binding, keyStyle, descStyle, contentWidth))
99+
}
100+
}
101+
102+
return lines
103+
}
104+
105+
// formatBinding formats a single key binding as " key description"
106+
func (d *helpDialog) formatBinding(binding key.Binding, keyStyle, descStyle lipgloss.Style, width int) string {
107+
helpInfo := binding.Help()
108+
helpKey := helpInfo.Key
109+
helpDesc := helpInfo.Desc
110+
111+
// Calculate spacing to align descriptions
112+
const keyWidth = 20
113+
const indent = 2
114+
115+
keyPart := keyStyle.Render(helpKey)
116+
descPart := descStyle.Render(helpDesc)
117+
118+
// Pad the key part to align descriptions
119+
keyPartWidth := lipgloss.Width(keyPart)
120+
padding := strings.Repeat(" ", max(1, keyWidth-keyPartWidth))
121+
122+
return fmt.Sprintf("%s%s%s%s",
123+
strings.Repeat(" ", indent),
124+
keyPart,
125+
padding,
126+
descPart,
127+
)
128+
}
129+
130+
func (d *helpDialog) Init() tea.Cmd {
131+
return d.readOnlyScrollDialog.Init()
132+
}
133+
134+
func (d *helpDialog) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
135+
model, cmd := d.readOnlyScrollDialog.Update(msg)
136+
if model != nil {
137+
if rod, ok := model.(*readOnlyScrollDialog); ok {
138+
d.readOnlyScrollDialog = *rod
139+
}
140+
}
141+
return d, cmd
142+
}
143+
144+
func (d *helpDialog) View() string {
145+
return d.readOnlyScrollDialog.View()
146+
}
147+
148+
func (d *helpDialog) Position() (int, int) {
149+
return d.readOnlyScrollDialog.Position()
150+
}
151+
152+
func (d *helpDialog) SetSize(width, height int) tea.Cmd {
153+
return d.readOnlyScrollDialog.SetSize(width, height)
154+
}
155+
156+
func (d *helpDialog) Bindings() []key.Binding {
157+
return []key.Binding{}
158+
}

pkg/tui/tui.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1687,6 +1687,12 @@ func (m *appModel) handleKeyPress(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
16871687

16881688
case key.Matches(msg, key.NewBinding(key.WithKeys("ctrl+x"))):
16891689
return m, core.CmdHandler(messages.ClearQueueMsg{})
1690+
1691+
case key.Matches(msg, key.NewBinding(key.WithKeys("ctrl+?"))):
1692+
// Show contextual help dialog with all currently active key bindings
1693+
return m, core.CmdHandler(dialog.OpenDialogMsg{
1694+
Model: dialog.NewHelpDialog(m.Bindings()),
1695+
})
16901696
}
16911697

16921698
// History search is a modal state — capture all remaining keys before normal routing

0 commit comments

Comments
 (0)