Skip to content

Commit 0c5a60c

Browse files
gitcoder89431claude
andcommitted
feat: scrollable Help page with j/k navigation
When content overflows the terminal height, shows a page of lines plus a "↓ j/k to scroll" / "↑ j/k to scroll" indicator at the bottom. Theme changes preserve scroll position via WithTheme pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent acbb17a commit 0c5a60c

2 files changed

Lines changed: 84 additions & 32 deletions

File tree

internal/app/update.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,11 @@ func (m *Model) updateDerivedScreens() {
352352
DeviceName: m.deviceName,
353353
DeviceID: m.deviceID,
354354
}, m.theme)
355-
m.screens["help"] = screens.NewHelp(m.keys.FullHelp(), m.theme)
355+
if s, ok := m.screens["help"].(screens.Help); ok {
356+
m.screens["help"] = s.WithTheme(m.theme)
357+
} else {
358+
m.screens["help"] = screens.NewHelp(m.keys.FullHelp(), m.theme)
359+
}
356360
if s, ok := m.screens["activity"].(screens.Activity); ok {
357361
m.screens["activity"] = s.WithTheme(m.theme)
358362
} else {

internal/screens/help.go

Lines changed: 79 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,33 @@ import (
1010
)
1111

1212
type Help struct {
13-
bindings [][]key.Binding
14-
theme theme.Theme
13+
bindings [][]key.Binding
14+
theme theme.Theme
15+
scrollOffset int
1516
}
1617

1718
func NewHelp(bindings [][]key.Binding, t theme.Theme) Help {
1819
return Help{bindings: bindings, theme: t}
1920
}
2021

22+
func (h Help) WithTheme(t theme.Theme) Help {
23+
h.theme = t
24+
return h
25+
}
26+
2127
func (h Help) Init() tea.Cmd { return nil }
2228

23-
func (h Help) Update(msg tea.Msg) (Screen, tea.Cmd) { return h, nil }
29+
func (h Help) Update(msg tea.Msg) (Screen, tea.Cmd) {
30+
if msg, ok := msg.(tea.KeyPressMsg); ok {
31+
switch msg.String() {
32+
case "up", "k":
33+
h.scrollOffset = max(0, h.scrollOffset-1)
34+
case "down", "j":
35+
h.scrollOffset++
36+
}
37+
}
38+
return h, nil
39+
}
2440

2541
func (h Help) View(width, height int) string {
2642
rule := func(label string) string {
@@ -29,42 +45,74 @@ func (h Help) View(width, height int) string {
2945
return h.theme.Title.Render(label) + h.theme.PaletteAccent.Render(" "+fill)
3046
}
3147
binding := func(k, desc string) string {
32-
return " " + h.theme.Accent.Render(k) + " " + h.theme.Muted.Render(desc) + "\n"
48+
return " " + h.theme.Accent.Render(k) + " " + h.theme.Muted.Render(desc)
3349
}
3450

35-
var b strings.Builder
36-
b.WriteString(rule("Global keys") + "\n\n")
51+
var lines []string
52+
add := func(s string) { lines = append(lines, s) }
53+
54+
add(rule("Global keys"))
55+
add("")
3756
for _, group := range h.bindings {
3857
for _, kb := range group {
3958
help := kb.Help()
40-
b.WriteString(binding(help.Key, help.Desc))
59+
add(binding(help.Key, help.Desc))
4160
}
4261
}
4362

44-
b.WriteString("\n" + rule("Dashboard") + "\n\n")
45-
b.WriteString(binding("j/k", "navigate files"))
46-
b.WriteString(binding("enter", "open selected file/folder in terminal"))
47-
b.WriteString(binding("r", "refresh + trigger rescan"))
48-
49-
b.WriteString("\n" + rule("Connect") + "\n\n")
50-
b.WriteString(binding("j/k", "navigate devices"))
51-
b.WriteString(binding("p", "pair a new device by pasting its ID"))
52-
b.WriteString(binding("a", "accept a pending device"))
53-
b.WriteString(binding("i", "ignore a pending device"))
54-
b.WriteString(binding("d", "remove an approved device"))
55-
b.WriteString(binding("r", "rename a device"))
56-
57-
b.WriteString("\n" + rule("Settings") + "\n\n")
58-
b.WriteString(binding("c", "copy this device's ID to clipboard"))
59-
60-
b.WriteString("\n" + rule("Command Palette") + "\n\n")
61-
b.WriteString(binding("ctrl+k", "open palette"))
62-
b.WriteString(binding("ctrl+t", "cycle to next theme"))
63-
b.WriteString(binding("type", "filter commands"))
64-
b.WriteString(binding("enter", "run selected command"))
65-
b.WriteString(binding("esc", "close palette"))
66-
67-
return lipgloss.NewStyle().Width(width).Height(height).Render(b.String())
63+
add("")
64+
add(rule("Dashboard"))
65+
add("")
66+
add(binding("j/k", "navigate files"))
67+
add(binding("enter", "open selected file/folder in terminal"))
68+
add(binding("r", "refresh + trigger rescan"))
69+
70+
add("")
71+
add(rule("Connect"))
72+
add("")
73+
add(binding("j/k", "navigate devices"))
74+
add(binding("p", "pair a new device by pasting its ID"))
75+
add(binding("a", "accept a pending device"))
76+
add(binding("i", "ignore a pending device"))
77+
add(binding("d", "remove an approved device"))
78+
add(binding("r", "rename a device"))
79+
80+
add("")
81+
add(rule("Settings"))
82+
add("")
83+
add(binding("c", "copy this device's ID to clipboard"))
84+
85+
add("")
86+
add(rule("Command Palette"))
87+
add("")
88+
add(binding("ctrl+k", "open palette"))
89+
add(binding("ctrl+t", "cycle to next theme"))
90+
add(binding("type", "filter commands"))
91+
add(binding("enter", "run selected command"))
92+
add(binding("esc", "close palette"))
93+
94+
total := len(lines)
95+
if total <= height {
96+
// no scrolling needed
97+
return lipgloss.NewStyle().Width(width).Height(height).Render(strings.Join(lines, "\n"))
98+
}
99+
100+
// clamp scroll so we never go past the last page
101+
maxOffset := total - height + 1 // +1 reserves space for the indicator
102+
h.scrollOffset = min(h.scrollOffset, maxOffset)
103+
104+
visible := lines[h.scrollOffset : h.scrollOffset+height-1]
105+
106+
// indicator line
107+
var indicator string
108+
if h.scrollOffset < maxOffset {
109+
indicator = h.theme.Muted.Render("↓ j/k to scroll")
110+
} else {
111+
indicator = h.theme.Muted.Render("↑ j/k to scroll")
112+
}
113+
114+
content := strings.Join(append(visible, indicator), "\n")
115+
return lipgloss.NewStyle().Width(width).Height(height).Render(content)
68116
}
69117

70118
func (h Help) Title() string { return "Help" }

0 commit comments

Comments
 (0)