detached 4000758

fix(vim): follow theme accent for headings

commit
40007580f55bd08de0aa4bb8e61f6442a2807d58
author
brice · · 2026-08-14 12:09 UTC
parent
a11aeca
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +54 -19
M internal/application/port/webview.go +4 -3
@@ -465,9 +465,10 @@ const (
465465 // SemanticNavigationRequest describes one Vim structural movement. Count uses
466466 // Vim semantics: adapters treat values less than one as one.
467467 type SemanticNavigationRequest struct {
468- Target SemanticNavigationTarget
469- Direction SemanticNavigationDirection
470- Count int
468+ Target SemanticNavigationTarget
469+ Direction SemanticNavigationDirection
470+ Count int
471+ HighlightColor string
471472 }
472473
473474 // SemanticNavigable is an optional WebView capability for visible structural
@@ -465,9 +465,10 @@ const (
465 // SemanticNavigationRequest describes one Vim structural movement. Count uses465 // SemanticNavigationRequest describes one Vim structural movement. Count uses
466 // Vim semantics: adapters treat values less than one as one.466 // Vim semantics: adapters treat values less than one as one.
467 type SemanticNavigationRequest struct {467 type SemanticNavigationRequest struct {
468- Target SemanticNavigationTarget468+ Target SemanticNavigationTarget
469- Direction SemanticNavigationDirection469+ Direction SemanticNavigationDirection
470- Count int470+ Count int
471+ HighlightColor string
471 }472 }
472 473
473 // SemanticNavigable is an optional WebView capability for visible structural474 // SemanticNavigable is an optional WebView capability for visible structural
M internal/infrastructure/cef/semantic_navigation.go +12 -5
@@ -26,14 +26,18 @@ func (wv *WebView) NavigateSemantic(ctx context.Context, request port.SemanticNa
2626 if count < 1 {
2727 count = 1
2828 }
29- wv.RunJavaScript(ctx, headingNavigationScript(int(request.Direction), count))
29+ wv.RunJavaScript(ctx, headingNavigationScript(int(request.Direction), count, request.HighlightColor))
3030 return nil
3131 }
3232
33-func headingNavigationScript(direction, count int) string {
33+func headingNavigationScript(direction, count int, highlightColor string) string {
34+ if highlightColor == "" {
35+ highlightColor = "#fbbf24"
36+ }
3437 return `(() => {
3538 const stateKey = "__dumberVimHeadingTarget";
3639 const className = "dumber-vim-heading-target";
40+ const highlightColor = ` + strconv.Quote(highlightColor) + `;
3741 const headings = Array.from(document.querySelectorAll("h1,h2,h3,h4,h5,h6"))
3842 .filter((heading) => heading.getClientRects().length > 0);
3943 if (headings.length === 0) return;
@@ -57,14 +61,17 @@ func headingNavigationScript(direction, count int) string {
5761
5862 const previous = document.querySelector("." + className);
5963 if (previous && previous !== target) previous.classList.remove(className);
60- if (!document.getElementById("dumber-vim-heading-target-style")) {
64+ let style = document.getElementById("dumber-vim-heading-target-style");
65+ if (!style) {
6166 // This visual cue is best-effort: strict page CSP can reject a DOM style
6267 // element, but target selection and scrolling must stay independent of it.
63- const style = document.createElement("style");
68+ style = document.createElement("style");
6469 style.id = "dumber-vim-heading-target-style";
65- style.textContent = "." + className + " { outline: 3px solid #fbbf24 !important; outline-offset: 5px !important; border-radius: 3px !important; }";
6670 document.documentElement.appendChild(style);
6771 }
72+ // Refresh the outline on every navigation so an in-session accent change is
73+ // reflected even when the target style element already exists.
74+ style.textContent = "." + className + " { outline: 3px solid " + highlightColor + " !important; outline-offset: 5px !important; border-radius: 3px !important; }";
6875 window[stateKey] = target;
6976 target.classList.add(className);
7077 target.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth" });
@@ -26,14 +26,18 @@ func (wv *WebView) NavigateSemantic(ctx context.Context, request port.SemanticNa
26 if count < 1 {26 if count < 1 {
27 count = 127 count = 1
28 }28 }
29- wv.RunJavaScript(ctx, headingNavigationScript(int(request.Direction), count))29+ wv.RunJavaScript(ctx, headingNavigationScript(int(request.Direction), count, request.HighlightColor))
30 return nil30 return nil
31 }31 }
32 32
33-func headingNavigationScript(direction, count int) string {33+func headingNavigationScript(direction, count int, highlightColor string) string {
34+ if highlightColor == "" {
35+ highlightColor = "#fbbf24"
36+ }
34 return `(() => {37 return `(() => {
35 const stateKey = "__dumberVimHeadingTarget";38 const stateKey = "__dumberVimHeadingTarget";
36 const className = "dumber-vim-heading-target";39 const className = "dumber-vim-heading-target";
40+ const highlightColor = ` + strconv.Quote(highlightColor) + `;
37 const headings = Array.from(document.querySelectorAll("h1,h2,h3,h4,h5,h6"))41 const headings = Array.from(document.querySelectorAll("h1,h2,h3,h4,h5,h6"))
38 .filter((heading) => heading.getClientRects().length > 0);42 .filter((heading) => heading.getClientRects().length > 0);
39 if (headings.length === 0) return;43 if (headings.length === 0) return;
@@ -57,14 +61,17 @@ func headingNavigationScript(direction, count int) string {
57 61
58 const previous = document.querySelector("." + className);62 const previous = document.querySelector("." + className);
59 if (previous && previous !== target) previous.classList.remove(className);63 if (previous && previous !== target) previous.classList.remove(className);
60- if (!document.getElementById("dumber-vim-heading-target-style")) {64+ let style = document.getElementById("dumber-vim-heading-target-style");
65+ if (!style) {
61 // This visual cue is best-effort: strict page CSP can reject a DOM style66 // This visual cue is best-effort: strict page CSP can reject a DOM style
62 // element, but target selection and scrolling must stay independent of it.67 // element, but target selection and scrolling must stay independent of it.
63- const style = document.createElement("style");68+ style = document.createElement("style");
64 style.id = "dumber-vim-heading-target-style";69 style.id = "dumber-vim-heading-target-style";
65- style.textContent = "." + className + " { outline: 3px solid #fbbf24 !important; outline-offset: 5px !important; border-radius: 3px !important; }";
66 document.documentElement.appendChild(style);70 document.documentElement.appendChild(style);
67 }71 }
72+ // Refresh the outline on every navigation so an in-session accent change is
73+ // reflected even when the target style element already exists.
74+ style.textContent = "." + className + " { outline: 3px solid " + highlightColor + " !important; outline-offset: 5px !important; border-radius: 3px !important; }";
68 window[stateKey] = target;75 window[stateKey] = target;
69 target.classList.add(className);76 target.classList.add(className);
70 target.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth" });77 target.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth" });
M internal/infrastructure/cef/semantic_navigation_test.go +11 -5
@@ -23,25 +23,31 @@ func TestWebViewNavigateSemantic_HeadingExecutesStrictNavigationScript(t *testin
2323 "previous && previous !== target",
2424 "scrollIntoView",
2525 "window[stateKey] = target",
26+ "const highlightColor = \"#22c55e\"",
27+ "let style = document.getElementById(\"dumber-vim-heading-target-style\")",
28+ "style.textContent = \".\" + className",
2629 "1 * 3",
2730 )), "", int32(0)).Once()
2831
2932 wv := &WebView{browser: browser}
3033 err := wv.NavigateSemantic(context.Background(), port.SemanticNavigationRequest{
31- Target: port.SemanticNavigationTargetHeading,
32- Direction: port.SemanticNavigationForward,
33- Count: 3,
34+ Target: port.SemanticNavigationTargetHeading,
35+ Direction: port.SemanticNavigationForward,
36+ Count: 3,
37+ HighlightColor: "#22c55e",
3438 })
3539 require.NoError(t, err)
3640 }
3741
3842 func TestWebViewNavigateSemantic_NormalizesCountAndRejectsUnsupportedRequests(t *testing.T) {
39- forward := headingNavigationScript(1, 1)
40- backward := headingNavigationScript(-1, 1)
43+ forward := headingNavigationScript(1, 1, "")
44+ backward := headingNavigationScript(-1, 1, "#4ade80")
4145 assert.Contains(t, forward, "index = firstAtOrAfterViewportTop - 1")
4246 assert.Contains(t, backward, "index = firstAtOrAfterViewportTop >= 0 ? firstAtOrAfterViewportTop : headings.length")
4347 assert.Contains(t, backward, "-1 * 1")
4448 assert.NotContains(t, backward, "window.innerHeight")
49+ assert.Contains(t, forward, "const highlightColor = \"#fbbf24\"")
50+ assert.Contains(t, backward, "const highlightColor = \"#4ade80\"")
4551
4652 wv := &WebView{}
4753 err := wv.NavigateSemantic(context.Background(), port.SemanticNavigationRequest{
@@ -23,25 +23,31 @@ func TestWebViewNavigateSemantic_HeadingExecutesStrictNavigationScript(t *testin
23 "previous && previous !== target",23 "previous && previous !== target",
24 "scrollIntoView",24 "scrollIntoView",
25 "window[stateKey] = target",25 "window[stateKey] = target",
26+ "const highlightColor = \"#22c55e\"",
27+ "let style = document.getElementById(\"dumber-vim-heading-target-style\")",
28+ "style.textContent = \".\" + className",
26 "1 * 3",29 "1 * 3",
27 )), "", int32(0)).Once()30 )), "", int32(0)).Once()
28 31
29 wv := &WebView{browser: browser}32 wv := &WebView{browser: browser}
30 err := wv.NavigateSemantic(context.Background(), port.SemanticNavigationRequest{33 err := wv.NavigateSemantic(context.Background(), port.SemanticNavigationRequest{
31- Target: port.SemanticNavigationTargetHeading,34+ Target: port.SemanticNavigationTargetHeading,
32- Direction: port.SemanticNavigationForward,35+ Direction: port.SemanticNavigationForward,
33- Count: 3,36+ Count: 3,
37+ HighlightColor: "#22c55e",
34 })38 })
35 require.NoError(t, err)39 require.NoError(t, err)
36 }40 }
37 41
38 func TestWebViewNavigateSemantic_NormalizesCountAndRejectsUnsupportedRequests(t *testing.T) {42 func TestWebViewNavigateSemantic_NormalizesCountAndRejectsUnsupportedRequests(t *testing.T) {
39- forward := headingNavigationScript(1, 1)43+ forward := headingNavigationScript(1, 1, "")
40- backward := headingNavigationScript(-1, 1)44+ backward := headingNavigationScript(-1, 1, "#4ade80")
41 assert.Contains(t, forward, "index = firstAtOrAfterViewportTop - 1")45 assert.Contains(t, forward, "index = firstAtOrAfterViewportTop - 1")
42 assert.Contains(t, backward, "index = firstAtOrAfterViewportTop >= 0 ? firstAtOrAfterViewportTop : headings.length")46 assert.Contains(t, backward, "index = firstAtOrAfterViewportTop >= 0 ? firstAtOrAfterViewportTop : headings.length")
43 assert.Contains(t, backward, "-1 * 1")47 assert.Contains(t, backward, "-1 * 1")
44 assert.NotContains(t, backward, "window.innerHeight")48 assert.NotContains(t, backward, "window.innerHeight")
49+ assert.Contains(t, forward, "const highlightColor = \"#fbbf24\"")
50+ assert.Contains(t, backward, "const highlightColor = \"#4ade80\"")
45 51
46 wv := &WebView{}52 wv := &WebView{}
47 err := wv.NavigateSemantic(context.Background(), port.SemanticNavigationRequest{53 err := wv.NavigateSemantic(context.Background(), port.SemanticNavigationRequest{
M internal/ui/app.go +15 -6
@@ -2168,15 +2168,17 @@ func (a *App) navigateVimSequenceAction(ctx context.Context, bw *browserWindow,
21682168 switch action {
21692169 case "heading-next":
21702170 request = port.SemanticNavigationRequest{
2171- Target: port.SemanticNavigationTargetHeading,
2172- Direction: port.SemanticNavigationForward,
2173- Count: count,
2171+ Target: port.SemanticNavigationTargetHeading,
2172+ Direction: port.SemanticNavigationForward,
2173+ Count: count,
2174+ HighlightColor: a.vimNavigationHighlightColor(),
21742175 }
21752176 case "heading-prev":
21762177 request = port.SemanticNavigationRequest{
2177- Target: port.SemanticNavigationTargetHeading,
2178- Direction: port.SemanticNavigationBackward,
2179- Count: count,
2178+ Target: port.SemanticNavigationTargetHeading,
2179+ Direction: port.SemanticNavigationBackward,
2180+ Count: count,
2181+ HighlightColor: a.vimNavigationHighlightColor(),
21802182 }
21812183 default:
21822184 return
@@ -2196,6 +2198,13 @@ func (a *App) navigateVimSequenceAction(ctx context.Context, bw *browserWindow,
21962198 }
21972199 }
21982200
2201+func (a *App) vimNavigationHighlightColor() string {
2202+ if a == nil || a.deps == nil || a.deps.Theme == nil {
2203+ return ""
2204+ }
2205+ return a.deps.Theme.GetCurrentPalette().Accent
2206+}
2207+
21992208 func (a *App) enableAccessibilityForVimMode(ctx context.Context, bw *browserWindow) {
22002209 paneID, wv := a.activeWebViewForBrowserWindow(bw)
22012210 if wv == nil {
@@ -2168,15 +2168,17 @@ func (a *App) navigateVimSequenceAction(ctx context.Context, bw *browserWindow,
2168 switch action {2168 switch action {
2169 case "heading-next":2169 case "heading-next":
2170 request = port.SemanticNavigationRequest{2170 request = port.SemanticNavigationRequest{
2171- Target: port.SemanticNavigationTargetHeading,2171+ Target: port.SemanticNavigationTargetHeading,
2172- Direction: port.SemanticNavigationForward,2172+ Direction: port.SemanticNavigationForward,
2173- Count: count,2173+ Count: count,
2174+ HighlightColor: a.vimNavigationHighlightColor(),
2174 }2175 }
2175 case "heading-prev":2176 case "heading-prev":
2176 request = port.SemanticNavigationRequest{2177 request = port.SemanticNavigationRequest{
2177- Target: port.SemanticNavigationTargetHeading,2178+ Target: port.SemanticNavigationTargetHeading,
2178- Direction: port.SemanticNavigationBackward,2179+ Direction: port.SemanticNavigationBackward,
2179- Count: count,2180+ Count: count,
2181+ HighlightColor: a.vimNavigationHighlightColor(),
2180 }2182 }
2181 default:2183 default:
2182 return2184 return
@@ -2196,6 +2198,13 @@ func (a *App) navigateVimSequenceAction(ctx context.Context, bw *browserWindow,
2196 }2198 }
2197 }2199 }
2198 2200
2201+func (a *App) vimNavigationHighlightColor() string {
2202+ if a == nil || a.deps == nil || a.deps.Theme == nil {
2203+ return ""
2204+ }
2205+ return a.deps.Theme.GetCurrentPalette().Accent
2206+}
2207+
2199 func (a *App) enableAccessibilityForVimMode(ctx context.Context, bw *browserWindow) {2208 func (a *App) enableAccessibilityForVimMode(ctx context.Context, bw *browserWindow) {
2200 paneID, wv := a.activeWebViewForBrowserWindow(bw)2209 paneID, wv := a.activeWebViewForBrowserWindow(bw)
2201 if wv == nil {2210 if wv == nil {
M internal/ui/app_vim_mode_test.go +12 -0
@@ -433,6 +433,18 @@ func TestVimMode_Enter_AccessibilityNoopsWhenActiveWebViewMissing(t *testing.T)
433433 app.handleModeChange(context.Background(), bw, input.ModeNormal, input.ModeVim)
434434 }
435435
436+func TestVimNavigationHighlightColorUsesCurrentThemeAccent(t *testing.T) {
437+ resolved := entity.ResolvedTheme{
438+ PrefersDark: true,
439+ DarkPalette: entity.ColorPalette{
440+ Accent: "#22c55e",
441+ },
442+ }
443+ app := &App{deps: &Dependencies{Theme: theme.NewManager(context.Background(), resolved)}}
444+
445+ assert.Equal(t, "#22c55e", app.vimNavigationHighlightColor())
446+}
447+
436448 func TestVimMode_SequenceActionNavigatesActiveWebViewHeading(t *testing.T) {
437449 app, bw, paneID := newVimModeAccessibilityFixture(t)
438450 wv := newSemanticNavigatingWebView(t, 4)
@@ -433,6 +433,18 @@ func TestVimMode_Enter_AccessibilityNoopsWhenActiveWebViewMissing(t *testing.T)
433 app.handleModeChange(context.Background(), bw, input.ModeNormal, input.ModeVim)433 app.handleModeChange(context.Background(), bw, input.ModeNormal, input.ModeVim)
434 }434 }
435 435
436+func TestVimNavigationHighlightColorUsesCurrentThemeAccent(t *testing.T) {
437+ resolved := entity.ResolvedTheme{
438+ PrefersDark: true,
439+ DarkPalette: entity.ColorPalette{
440+ Accent: "#22c55e",
441+ },
442+ }
443+ app := &App{deps: &Dependencies{Theme: theme.NewManager(context.Background(), resolved)}}
444+
445+ assert.Equal(t, "#22c55e", app.vimNavigationHighlightColor())
446+}
447+
436 func TestVimMode_SequenceActionNavigatesActiveWebViewHeading(t *testing.T) {448 func TestVimMode_SequenceActionNavigatesActiveWebViewHeading(t *testing.T) {
437 app, bw, paneID := newVimModeAccessibilityFixture(t)449 app, bw, paneID := newVimModeAccessibilityFixture(t)
438 wv := newSemanticNavigatingWebView(t, 4)450 wv := newSemanticNavigatingWebView(t, 4)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close