detached f150f41

fix: address final review findings

commit
f150f417d949c6593e960b81089c1ac5450369c5
author
brice · · 2026-06-26 10:11 UTC
parent
f82f76a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
19 files · +243 -66
M cmd/dumber/main.go +3 -1
@@ -883,6 +883,7 @@ func buildUIDependencies(
883883 focusProvider := textinput.NewFocusProvider()
884884 browserLauncher := desktop.NewBrowserLauncher(browserLaunchRelay)
885885 runtimeConfig := bootstrap.NewRuntimeConfigProvider(cfg, config.GetManager())
886+ localPaths := filesystem.New()
886887
887888 uiDeps := &ui.Dependencies{
888889 Ctx: ctx,
@@ -936,7 +937,8 @@ func buildUIDependencies(
936937 CheckUpdateUC: uc.checkUpdate,
937938 ApplyUpdateUC: uc.applyUpdate,
938939 SessionSpawner: bootstrap.NewSessionSpawner(ctx, runtimeProfile),
939- FileSystem: filesystem.New(),
940+ FileSystem: localPaths,
941+ LocalPathResolver: localPaths,
940942 AccentFocusProvider: focusProvider,
941943 NewGTKEntryTarget: func(entry *gtk.SearchEntry) port.TextInputTarget {
942944 return textinput.NewGTKEntryTarget(entry)
@@ -883,6 +883,7 @@ func buildUIDependencies(
883 focusProvider := textinput.NewFocusProvider()883 focusProvider := textinput.NewFocusProvider()
884 browserLauncher := desktop.NewBrowserLauncher(browserLaunchRelay)884 browserLauncher := desktop.NewBrowserLauncher(browserLaunchRelay)
885 runtimeConfig := bootstrap.NewRuntimeConfigProvider(cfg, config.GetManager())885 runtimeConfig := bootstrap.NewRuntimeConfigProvider(cfg, config.GetManager())
886+ localPaths := filesystem.New()
886 887
887 uiDeps := &ui.Dependencies{888 uiDeps := &ui.Dependencies{
888 Ctx: ctx,889 Ctx: ctx,
@@ -936,7 +937,8 @@ func buildUIDependencies(
936 CheckUpdateUC: uc.checkUpdate,937 CheckUpdateUC: uc.checkUpdate,
937 ApplyUpdateUC: uc.applyUpdate,938 ApplyUpdateUC: uc.applyUpdate,
938 SessionSpawner: bootstrap.NewSessionSpawner(ctx, runtimeProfile),939 SessionSpawner: bootstrap.NewSessionSpawner(ctx, runtimeProfile),
939- FileSystem: filesystem.New(),940+ FileSystem: localPaths,
941+ LocalPathResolver: localPaths,
940 AccentFocusProvider: focusProvider,942 AccentFocusProvider: focusProvider,
941 NewGTKEntryTarget: func(entry *gtk.SearchEntry) port.TextInputTarget {943 NewGTKEntryTarget: func(entry *gtk.SearchEntry) port.TextInputTarget {
942 return textinput.NewGTKEntryTarget(entry)944 return textinput.NewGTKEntryTarget(entry)
M docs/config/index.md +14 -2
@@ -245,6 +245,18 @@ When Noctalia rewrites the watched file, Dumber reapplies the same resolved them
245245 | Key | Type | Default | Description |
246246 |-----|------|---------|-------------|
247247 | `debug.enable_devtools` | bool | `true` | Enable browser developer tools (F12, Inspect Element) |
248+| `engine.cef.log_file` | string | `""` | CEF runtime log path |
249+| `engine.cef.log_severity` | int32 | `0` | CEF log severity (`0`, `1`, `2`, `3`, `4`, `99`) |
250+| `engine.cef.trace_handlers` | bool | `false` | Log CEF handler dispatch details |
251+| `engine.cef.enable_audio_handler` | bool | `true` | Enable the experimental CEF audio handler |
252+
253+## Privacy
254+
255+| Key | Type | Default | Valid Values | Description |
256+|-----|------|---------|--------------|-------------|
257+| `engine.cookie_policy` | string | `"always"` | `always`, `no_third_party`, `never` | Cookie acceptance policy |
258+| `engine.webkit.itp_enabled` | bool | `true` | - | Enable WebKit fallback Intelligent Tracking Prevention |
259+
248260
249261 ## Rendering, UI Scale & Zoom
250262
@@ -263,7 +275,7 @@ CEF is the default browser engine. WebKitGTK remains available as a fallback via
263275 | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | `> 0` | Vertical scroll sensitivity multiplier; combines with `scroll_precise_multiplier` for touchpads |
264276 | `engine.cef.input.scroll_max_delta` | int32 | `0` | `>= 0` | Maximum absolute scroll delta after scaling; 0 disables clamping |
265277 | `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | - | Enable two-finger touchpad swipe back/forward navigation |
266-| `engine.cef.input.touchpad_navigation_min_delta` | float | `200.0` | `> 0` | Minimum accumulated horizontal swipe delta required for navigation |
278+| `engine.cef.input.touchpad_navigation_min_delta` | float | `320.0` | `> 0` | Minimum accumulated horizontal swipe delta required for navigation |
267279 | `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | `> 0` | Maximum vertical-to-horizontal delta ratio allowed for navigation swipes |
268280 | `engine.webkit.gsk_renderer` | string | `"auto"` | `auto`, `opengl`, `vulkan`, `cairo` | WebKit fallback GTK renderer selection (`GSK_RENDERER`) |
269281 | `engine.webkit.disable_dmabuf_renderer` | bool | `false` | - | Disable WebKit fallback DMA-BUF renderer |
@@ -283,7 +295,7 @@ CEF is the default browser engine. WebKitGTK remains available as a fallback via
283295
284296 `engine.cef.input.scroll_precise_multiplier` controls touchpad/high-resolution wheel scroll speed, and `engine.cef.input.scroll_vertical_multiplier` applies an additional vertical-only scale. `engine.cef.input.touchpad_navigation_max_vertical_ratio` only filters horizontal back/forward swipe recognition; it does not tune vertical scroll speed.
285297
286-`engine.cef.input.touchpad_navigation_min_delta` uses raw GTK touchpad surface units for back/forward gestures. The default `200.0` matches WebKit-style commit distance to reduce accidental navigation; raise or lower it in `config.toml` to tune gesture sensitivity.
298+`engine.cef.input.touchpad_navigation_min_delta` uses raw GTK touchpad surface units for back/forward gestures. The default `320.0` matches WebKit-style commit distance to reduce accidental navigation; raise or lower it in `config.toml` to tune gesture sensitivity.
287299
288300 ### Legacy key migration
289301
@@ -245,6 +245,18 @@ When Noctalia rewrites the watched file, Dumber reapplies the same resolved them
245 | Key | Type | Default | Description |245 | Key | Type | Default | Description |
246 |-----|------|---------|-------------|246 |-----|------|---------|-------------|
247 | `debug.enable_devtools` | bool | `true` | Enable browser developer tools (F12, Inspect Element) |247 | `debug.enable_devtools` | bool | `true` | Enable browser developer tools (F12, Inspect Element) |
248+| `engine.cef.log_file` | string | `""` | CEF runtime log path |
249+| `engine.cef.log_severity` | int32 | `0` | CEF log severity (`0`, `1`, `2`, `3`, `4`, `99`) |
250+| `engine.cef.trace_handlers` | bool | `false` | Log CEF handler dispatch details |
251+| `engine.cef.enable_audio_handler` | bool | `true` | Enable the experimental CEF audio handler |
252+
253+## Privacy
254+
255+| Key | Type | Default | Valid Values | Description |
256+|-----|------|---------|--------------|-------------|
257+| `engine.cookie_policy` | string | `"always"` | `always`, `no_third_party`, `never` | Cookie acceptance policy |
258+| `engine.webkit.itp_enabled` | bool | `true` | - | Enable WebKit fallback Intelligent Tracking Prevention |
259+
248 260
249 ## Rendering, UI Scale & Zoom261 ## Rendering, UI Scale & Zoom
250 262
@@ -263,7 +275,7 @@ CEF is the default browser engine. WebKitGTK remains available as a fallback via
263 | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | `> 0` | Vertical scroll sensitivity multiplier; combines with `scroll_precise_multiplier` for touchpads |275 | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | `> 0` | Vertical scroll sensitivity multiplier; combines with `scroll_precise_multiplier` for touchpads |
264 | `engine.cef.input.scroll_max_delta` | int32 | `0` | `>= 0` | Maximum absolute scroll delta after scaling; 0 disables clamping |276 | `engine.cef.input.scroll_max_delta` | int32 | `0` | `>= 0` | Maximum absolute scroll delta after scaling; 0 disables clamping |
265 | `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | - | Enable two-finger touchpad swipe back/forward navigation |277 | `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | - | Enable two-finger touchpad swipe back/forward navigation |
266-| `engine.cef.input.touchpad_navigation_min_delta` | float | `200.0` | `> 0` | Minimum accumulated horizontal swipe delta required for navigation |278+| `engine.cef.input.touchpad_navigation_min_delta` | float | `320.0` | `> 0` | Minimum accumulated horizontal swipe delta required for navigation |
267 | `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | `> 0` | Maximum vertical-to-horizontal delta ratio allowed for navigation swipes |279 | `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | `> 0` | Maximum vertical-to-horizontal delta ratio allowed for navigation swipes |
268 | `engine.webkit.gsk_renderer` | string | `"auto"` | `auto`, `opengl`, `vulkan`, `cairo` | WebKit fallback GTK renderer selection (`GSK_RENDERER`) |280 | `engine.webkit.gsk_renderer` | string | `"auto"` | `auto`, `opengl`, `vulkan`, `cairo` | WebKit fallback GTK renderer selection (`GSK_RENDERER`) |
269 | `engine.webkit.disable_dmabuf_renderer` | bool | `false` | - | Disable WebKit fallback DMA-BUF renderer |281 | `engine.webkit.disable_dmabuf_renderer` | bool | `false` | - | Disable WebKit fallback DMA-BUF renderer |
@@ -283,7 +295,7 @@ CEF is the default browser engine. WebKitGTK remains available as a fallback via
283 295
284 `engine.cef.input.scroll_precise_multiplier` controls touchpad/high-resolution wheel scroll speed, and `engine.cef.input.scroll_vertical_multiplier` applies an additional vertical-only scale. `engine.cef.input.touchpad_navigation_max_vertical_ratio` only filters horizontal back/forward swipe recognition; it does not tune vertical scroll speed.296 `engine.cef.input.scroll_precise_multiplier` controls touchpad/high-resolution wheel scroll speed, and `engine.cef.input.scroll_vertical_multiplier` applies an additional vertical-only scale. `engine.cef.input.touchpad_navigation_max_vertical_ratio` only filters horizontal back/forward swipe recognition; it does not tune vertical scroll speed.
285 297
286-`engine.cef.input.touchpad_navigation_min_delta` uses raw GTK touchpad surface units for back/forward gestures. The default `200.0` matches WebKit-style commit distance to reduce accidental navigation; raise or lower it in `config.toml` to tune gesture sensitivity.298+`engine.cef.input.touchpad_navigation_min_delta` uses raw GTK touchpad surface units for back/forward gestures. The default `320.0` matches WebKit-style commit distance to reduce accidental navigation; raise or lower it in `config.toml` to tune gesture sensitivity.
287 299
288 ### Legacy key migration300 ### Legacy key migration
289 301
M docs/reference/configuration.md +2 -2
@@ -70,7 +70,7 @@
7070 | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | > 0 |
7171 | `engine.cef.input.scroll_max_delta` | int32 | `0` | >= 0 |
7272 | `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | |
73-| `engine.cef.input.touchpad_navigation_min_delta` | float | `200.0` | > 0 |
73+| `engine.cef.input.touchpad_navigation_min_delta` | float | `320.0` | > 0 |
7474 | `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | > 0 |
7575 | `engine.webkit.gsk_renderer` | string | `auto` | `auto`, `opengl`, `vulkan`, `cairo` (WebKit fallback only) |
7676 | `engine.webkit.disable_dmabuf_renderer` | bool | `false` | WebKit fallback only |
@@ -163,7 +163,7 @@
163163
164164 Touchpad vertical scroll speed is controlled by `engine.cef.input.scroll_precise_multiplier` and the additional axis-specific `engine.cef.input.scroll_vertical_multiplier`. `engine.cef.input.touchpad_navigation_max_vertical_ratio` only filters horizontal back/forward swipe recognition; it does not tune vertical scroll speed.
165165
166-`engine.cef.input.touchpad_navigation_min_delta` is measured in raw GTK touchpad surface units. The default `200.0` matches WebKit-style commit distance to reduce accidental back/forward navigation and can be overridden in `config.toml`.
166+`engine.cef.input.touchpad_navigation_min_delta` is measured in raw GTK touchpad surface units. The default `320.0` matches WebKit-style commit distance to reduce accidental back/forward navigation and can be overridden in `config.toml`.
167167
168168 CEF is the default engine. WebKitGTK is a fallback selected with `engine.type = "webkit"`; `engine.webkit.*` keys are WebKit-specific.
169169
@@ -70,7 +70,7 @@
70 | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | > 0 |70 | `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | > 0 |
71 | `engine.cef.input.scroll_max_delta` | int32 | `0` | >= 0 |71 | `engine.cef.input.scroll_max_delta` | int32 | `0` | >= 0 |
72 | `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | |72 | `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | |
73-| `engine.cef.input.touchpad_navigation_min_delta` | float | `200.0` | > 0 |73+| `engine.cef.input.touchpad_navigation_min_delta` | float | `320.0` | > 0 |
74 | `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | > 0 |74 | `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | > 0 |
75 | `engine.webkit.gsk_renderer` | string | `auto` | `auto`, `opengl`, `vulkan`, `cairo` (WebKit fallback only) |75 | `engine.webkit.gsk_renderer` | string | `auto` | `auto`, `opengl`, `vulkan`, `cairo` (WebKit fallback only) |
76 | `engine.webkit.disable_dmabuf_renderer` | bool | `false` | WebKit fallback only |76 | `engine.webkit.disable_dmabuf_renderer` | bool | `false` | WebKit fallback only |
@@ -163,7 +163,7 @@
163 163
164 Touchpad vertical scroll speed is controlled by `engine.cef.input.scroll_precise_multiplier` and the additional axis-specific `engine.cef.input.scroll_vertical_multiplier`. `engine.cef.input.touchpad_navigation_max_vertical_ratio` only filters horizontal back/forward swipe recognition; it does not tune vertical scroll speed.164 Touchpad vertical scroll speed is controlled by `engine.cef.input.scroll_precise_multiplier` and the additional axis-specific `engine.cef.input.scroll_vertical_multiplier`. `engine.cef.input.touchpad_navigation_max_vertical_ratio` only filters horizontal back/forward swipe recognition; it does not tune vertical scroll speed.
165 165
166-`engine.cef.input.touchpad_navigation_min_delta` is measured in raw GTK touchpad surface units. The default `200.0` matches WebKit-style commit distance to reduce accidental back/forward navigation and can be overridden in `config.toml`.166+`engine.cef.input.touchpad_navigation_min_delta` is measured in raw GTK touchpad surface units. The default `320.0` matches WebKit-style commit distance to reduce accidental back/forward navigation and can be overridden in `config.toml`.
167 167
168 CEF is the default engine. WebKitGTK is a fallback selected with `engine.type = "webkit"`; `engine.webkit.*` keys are WebKit-specific.168 CEF is the default engine. WebKitGTK is a fallback selected with `engine.type = "webkit"`; `engine.webkit.*` keys are WebKit-specific.
169 169
M internal/application/usecase/navigation_url_normalizer.go +27 -0
@@ -34,6 +34,33 @@ func (n *NavigationURLNormalizer) Normalize(ctx context.Context, input string) s
3434 return domainurl.Normalize(input)
3535 }
3636
37+// BuildNavigationURL resolves local paths before applying bang shortcuts and search fallback.
38+func (n *NavigationURLNormalizer) BuildNavigationURL(
39+ ctx context.Context,
40+ input string,
41+ shortcutURLs map[string]string,
42+ defaultSearch string,
43+) string {
44+ return BuildNavigationURL(ctx, input, n.normalize, shortcutURLs, defaultSearch)
45+}
46+
47+// BuildNavigationURL resolves a user navigation string using the shared CLI/UI policy.
48+func BuildNavigationURL(
49+ ctx context.Context,
50+ input string,
51+ normalize func(context.Context, string) string,
52+ shortcutURLs map[string]string,
53+ defaultSearch string,
54+) string {
55+ if _, _, found := domainurl.ParseBangShortcut(input); !found && normalize != nil {
56+ normalized := normalize(ctx, input)
57+ if normalized != input {
58+ return normalized
59+ }
60+ }
61+ return domainurl.BuildSearchURL(input, shortcutURLs, defaultSearch)
62+}
63+
3764 func (n *NavigationURLNormalizer) normalize(ctx context.Context, input string) string {
3865 if n == nil {
3966 return domainurl.Normalize(input)
@@ -34,6 +34,33 @@ func (n *NavigationURLNormalizer) Normalize(ctx context.Context, input string) s
34 return domainurl.Normalize(input)34 return domainurl.Normalize(input)
35 }35 }
36 36
37+// BuildNavigationURL resolves local paths before applying bang shortcuts and search fallback.
38+func (n *NavigationURLNormalizer) BuildNavigationURL(
39+ ctx context.Context,
40+ input string,
41+ shortcutURLs map[string]string,
42+ defaultSearch string,
43+) string {
44+ return BuildNavigationURL(ctx, input, n.normalize, shortcutURLs, defaultSearch)
45+}
46+
47+// BuildNavigationURL resolves a user navigation string using the shared CLI/UI policy.
48+func BuildNavigationURL(
49+ ctx context.Context,
50+ input string,
51+ normalize func(context.Context, string) string,
52+ shortcutURLs map[string]string,
53+ defaultSearch string,
54+) string {
55+ if _, _, found := domainurl.ParseBangShortcut(input); !found && normalize != nil {
56+ normalized := normalize(ctx, input)
57+ if normalized != input {
58+ return normalized
59+ }
60+ }
61+ return domainurl.BuildSearchURL(input, shortcutURLs, defaultSearch)
62+}
63+
37 func (n *NavigationURLNormalizer) normalize(ctx context.Context, input string) string {64 func (n *NavigationURLNormalizer) normalize(ctx context.Context, input string) string {
38 if n == nil {65 if n == nil {
39 return domainurl.Normalize(input)66 return domainurl.Normalize(input)
M internal/application/usecase/navigation_url_normalizer_test.go +15 -0
@@ -51,6 +51,21 @@ func TestNavigationURLNormalizerFallsBackToDomainNormalize(t *testing.T) {
5151 t.Fatalf("Normalize(./missing.html) = %q", got)
5252 }
5353 }
54+func TestNavigationURLNormalizerBuildNavigationURLCentralizesSearchFallback(t *testing.T) {
55+ ctx := context.Background()
56+ absFile := filepath.Join(string(filepath.Separator), "tmp", "page.html")
57+ normalizer := NewNavigationURLNormalizer(fakeLocalPathResolver{paths: map[string]string{
58+ "page.html": absFile,
59+ }})
60+ shortcuts := map[string]string{"g": "https://google.com/search?q=%s"}
61+
62+ if got := normalizer.BuildNavigationURL(ctx, "page.html", shortcuts, "https://search.example/?q=%s"); got != "file://"+absFile {
63+ t.Fatalf("BuildNavigationURL(local file) = %q, want file URL", got)
64+ }
65+ if got := normalizer.BuildNavigationURL(ctx, "!g cats", shortcuts, "https://search.example/?q=%s"); got != "https://google.com/search?q=cats" {
66+ t.Fatalf("BuildNavigationURL(shortcut) = %q, want shortcut search", got)
67+ }
68+}
5469
5570 func TestNavigationURLNormalizerDoesNotProbeSchemeBearingInputs(t *testing.T) {
5671 ctx := context.Background()
@@ -51,6 +51,21 @@ func TestNavigationURLNormalizerFallsBackToDomainNormalize(t *testing.T) {
51 t.Fatalf("Normalize(./missing.html) = %q", got)51 t.Fatalf("Normalize(./missing.html) = %q", got)
52 }52 }
53 }53 }
54+func TestNavigationURLNormalizerBuildNavigationURLCentralizesSearchFallback(t *testing.T) {
55+ ctx := context.Background()
56+ absFile := filepath.Join(string(filepath.Separator), "tmp", "page.html")
57+ normalizer := NewNavigationURLNormalizer(fakeLocalPathResolver{paths: map[string]string{
58+ "page.html": absFile,
59+ }})
60+ shortcuts := map[string]string{"g": "https://google.com/search?q=%s"}
61+
62+ if got := normalizer.BuildNavigationURL(ctx, "page.html", shortcuts, "https://search.example/?q=%s"); got != "file://"+absFile {
63+ t.Fatalf("BuildNavigationURL(local file) = %q, want file URL", got)
64+ }
65+ if got := normalizer.BuildNavigationURL(ctx, "!g cats", shortcuts, "https://search.example/?q=%s"); got != "https://google.com/search?q=cats" {
66+ t.Fatalf("BuildNavigationURL(shortcut) = %q, want shortcut search", got)
67+ }
68+}
54 69
55 func TestNavigationURLNormalizerDoesNotProbeSchemeBearingInputs(t *testing.T) {70 func TestNavigationURLNormalizerDoesNotProbeSchemeBearingInputs(t *testing.T) {
56 ctx := context.Background()71 ctx := context.Background()
M internal/cli/cmd/dmenu.go +9 -11
@@ -218,22 +218,20 @@ func parseSelection(selection string) string {
218218 return ""
219219 }
220220
221- // Use BuildSearchURL to handle bang shortcuts, URLs, and search queries.
222221 // Existing local paths are resolved before URL/search fallback so dmenu keeps
223222 // the same file:// behavior as the in-app omnibox.
224223 app := GetApp()
225224 if app != nil {
226- if _, _, found := domainurl.ParseBangShortcut(selection); !found {
227- normalizer := app.NavigationURLNormalizer
228- if normalizer == nil {
229- normalizer = usecase.NewNavigationURLNormalizer(app.LocalPaths)
230- }
231- normalized := normalizer.Normalize(context.Background(), selection)
232- if normalized != selection {
233- return normalized
234- }
225+ normalizer := app.NavigationURLNormalizer
226+ if normalizer == nil {
227+ normalizer = usecase.NewNavigationURLNormalizer(app.LocalPaths)
235228 }
236- return domainurl.BuildSearchURL(selection, app.Config.ShortcutURLs(), app.Config.DefaultSearchEngine)
229+ return normalizer.BuildNavigationURL(
230+ context.Background(),
231+ selection,
232+ app.Config.ShortcutURLs(),
233+ app.Config.DefaultSearchEngine,
234+ )
237235 }
238236
239237 // Fallback: add https:// scheme if no app context
@@ -218,22 +218,20 @@ func parseSelection(selection string) string {
218 return ""218 return ""
219 }219 }
220 220
221- // Use BuildSearchURL to handle bang shortcuts, URLs, and search queries.
222 // Existing local paths are resolved before URL/search fallback so dmenu keeps221 // Existing local paths are resolved before URL/search fallback so dmenu keeps
223 // the same file:// behavior as the in-app omnibox.222 // the same file:// behavior as the in-app omnibox.
224 app := GetApp()223 app := GetApp()
225 if app != nil {224 if app != nil {
226- if _, _, found := domainurl.ParseBangShortcut(selection); !found {225+ normalizer := app.NavigationURLNormalizer
227- normalizer := app.NavigationURLNormalizer226+ if normalizer == nil {
228- if normalizer == nil {227+ normalizer = usecase.NewNavigationURLNormalizer(app.LocalPaths)
229- normalizer = usecase.NewNavigationURLNormalizer(app.LocalPaths)
230- }
231- normalized := normalizer.Normalize(context.Background(), selection)
232- if normalized != selection {
233- return normalized
234- }
235 }228 }
236- return domainurl.BuildSearchURL(selection, app.Config.ShortcutURLs(), app.Config.DefaultSearchEngine)229+ return normalizer.BuildNavigationURL(
230+ context.Background(),
231+ selection,
232+ app.Config.ShortcutURLs(),
233+ app.Config.DefaultSearchEngine,
234+ )
237 }235 }
238 236
239 // Fallback: add https:// scheme if no app context237 // Fallback: add https:// scheme if no app context
M internal/infrastructure/cef/scale_probe.go +2 -2
@@ -41,10 +41,10 @@ func cefScaleProbeSnapshot(wv *WebView) cefScaleProbeMetrics {
4141 }
4242 m.UserZoom = normalizeScale(wv.GetZoomLevel())
4343 m.OSRBackingScale = normalizeScale(wv.osrBackingScaleFactor())
44- m.InternalCEFFactor = m.UserZoom * m.OSRBackingScale
44+ m.SurfaceScale = normalizeScale(wv.viewBridgeScale())
45+ m.InternalCEFFactor = m.UserZoom * zoomScaleRatio(m.SurfaceScale, m.OSRBackingScale)
4546 if wv.viewBridge != nil {
4647 m.SurfaceWidth, m.SurfaceHeight = wv.viewBridge.Size()
47- m.SurfaceScale = normalizeScale(float64(wv.viewBridge.DeviceScaleFactor()))
4848 }
4949 return m
5050 }
@@ -41,10 +41,10 @@ func cefScaleProbeSnapshot(wv *WebView) cefScaleProbeMetrics {
41 }41 }
42 m.UserZoom = normalizeScale(wv.GetZoomLevel())42 m.UserZoom = normalizeScale(wv.GetZoomLevel())
43 m.OSRBackingScale = normalizeScale(wv.osrBackingScaleFactor())43 m.OSRBackingScale = normalizeScale(wv.osrBackingScaleFactor())
44- m.InternalCEFFactor = m.UserZoom * m.OSRBackingScale44+ m.SurfaceScale = normalizeScale(wv.viewBridgeScale())
45+ m.InternalCEFFactor = m.UserZoom * zoomScaleRatio(m.SurfaceScale, m.OSRBackingScale)
45 if wv.viewBridge != nil {46 if wv.viewBridge != nil {
46 m.SurfaceWidth, m.SurfaceHeight = wv.viewBridge.Size()47 m.SurfaceWidth, m.SurfaceHeight = wv.viewBridge.Size()
47- m.SurfaceScale = normalizeScale(float64(wv.viewBridge.DeviceScaleFactor()))
48 }48 }
49 return m49 return m
50 }50 }
M internal/infrastructure/cef/webview.go +37 -23
@@ -214,12 +214,12 @@ type WebView struct {
214214 loadDiagLastLoadStateAt time.Time
215215
216216 // Atomic state.
217- destroyed atomic.Bool
218- fullscreen atomic.Bool
219- generation atomic.Uint64
220- audioPlaying atomic.Bool
221- zoomFactor atomic.Value // float64, initialized to 1.0
222- lastAppliedOSRBackingScaleBits atomic.Uint64
217+ destroyed atomic.Bool
218+ fullscreen atomic.Bool
219+ generation atomic.Uint64
220+ audioPlaying atomic.Bool
221+ zoomFactor atomic.Value // float64, initialized to 1.0
222+ lastAppliedZoomScaleRatioBits atomic.Uint64
223223
224224 // Browser creation defaults copied from the factory so native popup shells
225225 // can apply the same settings in OnBeforePopup.
@@ -491,15 +491,23 @@ func cefZoomFromFactor(factor float64) float64 {
491491 return math.Log(factor) / math.Log(chromiumZoomBase)
492492 }
493493
494-func cefZoomFromPageAndBackingFactor(pageZoom, backingScale float64) float64 {
495- return cefZoomFromFactor(pageZoom / normalizeScale(backingScale))
494+func zoomScaleRatio(surfaceScale, backingScale float64) float64 {
495+ backingScale = normalizeScale(backingScale)
496+ if backingScale <= 1 {
497+ return 1
498+ }
499+ return normalizeScale(surfaceScale) / backingScale
500+}
501+
502+func cefZoomFromPageAndScaleFactors(pageZoom, surfaceScale, backingScale float64) float64 {
503+ return cefZoomFromFactor(pageZoom * zoomScaleRatio(surfaceScale, backingScale))
496504 }
497505
498-func pageZoomFromCEFAndBackingLevel(level, backingScale float64) float64 {
499- return factorFromCEFZoom(level) * normalizeScale(backingScale)
506+func pageZoomFromCEFAndScaleLevel(level, surfaceScale, backingScale float64) float64 {
507+ return factorFromCEFZoom(level) / zoomScaleRatio(surfaceScale, backingScale)
500508 }
501509
502-func (wv *WebView) applyCEFZoomLevel(host purecef.BrowserHost, factor, cefLevel, backingScale float64) {
510+func (wv *WebView) applyCEFZoomLevel(host purecef.BrowserHost, factor, cefLevel, surfaceScale, backingScale float64) {
503511 host.SetZoomLevel(cefLevel)
504512 // Force CEF to produce a new frame at the new zoom level. In OSR mode,
505513 // SetZoomLevel changes the Blink layout zoom but doesn't guarantee a
@@ -508,7 +516,7 @@ func (wv *WebView) applyCEFZoomLevel(host purecef.BrowserHost, factor, cefLevel,
508516 // SynchronizeVisualProperties cycle, which makes the renderer produce
509517 // a new compositor frame at the new zoom level.
510518 host.NotifyScreenInfoChanged()
511- wv.recordAppliedOSRBackingScale(backingScale)
519+ wv.recordAppliedZoomScaleRatio(surfaceScale, backingScale)
512520 // Zoom is applied asynchronously in the renderer process. Request a couple
513521 // of follow-up refreshes on the CEF UI thread so OSR captures the updated
514522 // compositor frame after the zoom IPC has been processed.
@@ -541,14 +549,16 @@ func (wv *WebView) SetZoomLevel(_ context.Context, factor float64) error {
541549 if host == nil {
542550 return errNoBrowser
543551 }
552+ surfaceScale := wv.viewBridgeScale()
544553 backingScale := wv.osrBackingScaleFactor()
545- cefLevel := cefZoomFromPageAndBackingFactor(factor, backingScale)
554+ cefLevel := cefZoomFromPageAndScaleFactors(factor, surfaceScale, backingScale)
546555 logging.FromContext(wv.ctx).Debug().
547556 Float64("factor", factor).
548557 Float64("cef_level", cefLevel).
549558 Float64("osr_backing_scale", backingScale).
559+ Float64("surface_scale", surfaceScale).
550560 Msg("cef: SetZoomLevel")
551- wv.applyCEFZoomLevel(host, factor, cefLevel, backingScale)
561+ wv.applyCEFZoomLevel(host, factor, cefLevel, surfaceScale, backingScale)
552562 wv.zoomFactor.Store(factor)
553563 return nil
554564 }
@@ -1665,23 +1675,23 @@ func (wv *WebView) osrBackingScaleFactor() float64 {
16651675 return normalizeScale(wv.viewBridge.OSRBackingScaleFactor())
16661676 }
16671677
1668-func (wv *WebView) recordAppliedOSRBackingScale(scale float64) {
1678+func (wv *WebView) recordAppliedZoomScaleRatio(surfaceScale, backingScale float64) {
16691679 if wv == nil {
16701680 return
16711681 }
1672- wv.lastAppliedOSRBackingScaleBits.Store(math.Float64bits(normalizeScale(scale)))
1682+ wv.lastAppliedZoomScaleRatioBits.Store(math.Float64bits(zoomScaleRatio(surfaceScale, backingScale)))
16731683 }
16741684
1675-func (wv *WebView) shouldReapplyZoomForBackingScale(scale float64) bool {
1685+func (wv *WebView) shouldReapplyZoomForScaleRatio(surfaceScale, backingScale float64) bool {
16761686 if wv == nil {
16771687 return false
16781688 }
1679- normalized := normalizeScale(scale)
1680- bits := wv.lastAppliedOSRBackingScaleBits.Load()
1689+ ratio := zoomScaleRatio(surfaceScale, backingScale)
1690+ bits := wv.lastAppliedZoomScaleRatioBits.Load()
16811691 if bits == 0 {
16821692 return true
16831693 }
1684- return math.Float64frombits(bits) != normalized
1694+ return math.Float64frombits(bits) != ratio
16851695 }
16861696
16871697 func (wv *WebView) handleMiddleClickFromBridge() bool {
@@ -1982,16 +1992,18 @@ func (wv *WebView) scheduleZoomReadback(expectedFactor, expectedLevel float64) {
19821992 if host == nil {
19831993 return
19841994 }
1995+ surfaceScale := wv.viewBridgeScale()
19851996 backingScale := wv.osrBackingScaleFactor()
19861997 actualLevel := host.GetZoomLevel()
19871998 logging.FromContext(wv.ctx).Debug().
19881999 Int64("delay_ms", delayMs).
19892000 Float64("expected_factor", expectedFactor).
19902001 Float64("expected_cef_level", expectedLevel).
1991- Float64("actual_factor", pageZoomFromCEFAndBackingLevel(actualLevel, backingScale)).
2002+ Float64("actual_factor", pageZoomFromCEFAndScaleLevel(actualLevel, surfaceScale, backingScale)).
19922003 Float64("actual_cef_factor", factorFromCEFZoom(actualLevel)).
19932004 Float64("actual_cef_level", actualLevel).
19942005 Float64("osr_backing_scale", backingScale).
2006+ Float64("surface_scale", surfaceScale).
19952007 Msg("cef: zoom level readback")
19962008 }))
19972009 if task == nil {
@@ -2012,14 +2024,16 @@ func (wv *WebView) reapplyCurrentZoomForBackingScale(reason string) {
20122024 return
20132025 }
20142026 factor := wv.GetZoomLevel()
2027+ surfaceScale := wv.viewBridgeScale()
20152028 backingScale := wv.osrBackingScaleFactor()
2016- cefLevel := cefZoomFromPageAndBackingFactor(factor, backingScale)
2017- wv.applyCEFZoomLevel(host, factor, cefLevel, backingScale)
2029+ cefLevel := cefZoomFromPageAndScaleFactors(factor, surfaceScale, backingScale)
2030+ wv.applyCEFZoomLevel(host, factor, cefLevel, surfaceScale, backingScale)
20182031 logging.FromContext(wv.ctx).Debug().
20192032 Str("reason", reason).
20202033 Float64("factor", factor).
20212034 Float64("cef_level", cefLevel).
20222035 Float64("osr_backing_scale", backingScale).
2036+ Float64("surface_scale", surfaceScale).
20232037 Msg("cef: reapplied zoom after OSR backing scale change")
20242038 }
20252039
@@ -214,12 +214,12 @@ type WebView struct {
214 loadDiagLastLoadStateAt time.Time214 loadDiagLastLoadStateAt time.Time
215 215
216 // Atomic state.216 // Atomic state.
217- destroyed atomic.Bool217+ destroyed atomic.Bool
218- fullscreen atomic.Bool218+ fullscreen atomic.Bool
219- generation atomic.Uint64219+ generation atomic.Uint64
220- audioPlaying atomic.Bool220+ audioPlaying atomic.Bool
221- zoomFactor atomic.Value // float64, initialized to 1.0221+ zoomFactor atomic.Value // float64, initialized to 1.0
222- lastAppliedOSRBackingScaleBits atomic.Uint64222+ lastAppliedZoomScaleRatioBits atomic.Uint64
223 223
224 // Browser creation defaults copied from the factory so native popup shells224 // Browser creation defaults copied from the factory so native popup shells
225 // can apply the same settings in OnBeforePopup.225 // can apply the same settings in OnBeforePopup.
@@ -491,15 +491,23 @@ func cefZoomFromFactor(factor float64) float64 {
491 return math.Log(factor) / math.Log(chromiumZoomBase)491 return math.Log(factor) / math.Log(chromiumZoomBase)
492 }492 }
493 493
494-func cefZoomFromPageAndBackingFactor(pageZoom, backingScale float64) float64 {494+func zoomScaleRatio(surfaceScale, backingScale float64) float64 {
495- return cefZoomFromFactor(pageZoom / normalizeScale(backingScale))495+ backingScale = normalizeScale(backingScale)
496+ if backingScale <= 1 {
497+ return 1
498+ }
499+ return normalizeScale(surfaceScale) / backingScale
500+}
501+
502+func cefZoomFromPageAndScaleFactors(pageZoom, surfaceScale, backingScale float64) float64 {
503+ return cefZoomFromFactor(pageZoom * zoomScaleRatio(surfaceScale, backingScale))
496 }504 }
497 505
498-func pageZoomFromCEFAndBackingLevel(level, backingScale float64) float64 {506+func pageZoomFromCEFAndScaleLevel(level, surfaceScale, backingScale float64) float64 {
499- return factorFromCEFZoom(level) * normalizeScale(backingScale)507+ return factorFromCEFZoom(level) / zoomScaleRatio(surfaceScale, backingScale)
500 }508 }
501 509
502-func (wv *WebView) applyCEFZoomLevel(host purecef.BrowserHost, factor, cefLevel, backingScale float64) {510+func (wv *WebView) applyCEFZoomLevel(host purecef.BrowserHost, factor, cefLevel, surfaceScale, backingScale float64) {
503 host.SetZoomLevel(cefLevel)511 host.SetZoomLevel(cefLevel)
504 // Force CEF to produce a new frame at the new zoom level. In OSR mode,512 // Force CEF to produce a new frame at the new zoom level. In OSR mode,
505 // SetZoomLevel changes the Blink layout zoom but doesn't guarantee a513 // SetZoomLevel changes the Blink layout zoom but doesn't guarantee a
@@ -508,7 +516,7 @@ func (wv *WebView) applyCEFZoomLevel(host purecef.BrowserHost, factor, cefLevel,
508 // SynchronizeVisualProperties cycle, which makes the renderer produce516 // SynchronizeVisualProperties cycle, which makes the renderer produce
509 // a new compositor frame at the new zoom level.517 // a new compositor frame at the new zoom level.
510 host.NotifyScreenInfoChanged()518 host.NotifyScreenInfoChanged()
511- wv.recordAppliedOSRBackingScale(backingScale)519+ wv.recordAppliedZoomScaleRatio(surfaceScale, backingScale)
512 // Zoom is applied asynchronously in the renderer process. Request a couple520 // Zoom is applied asynchronously in the renderer process. Request a couple
513 // of follow-up refreshes on the CEF UI thread so OSR captures the updated521 // of follow-up refreshes on the CEF UI thread so OSR captures the updated
514 // compositor frame after the zoom IPC has been processed.522 // compositor frame after the zoom IPC has been processed.
@@ -541,14 +549,16 @@ func (wv *WebView) SetZoomLevel(_ context.Context, factor float64) error {
541 if host == nil {549 if host == nil {
542 return errNoBrowser550 return errNoBrowser
543 }551 }
552+ surfaceScale := wv.viewBridgeScale()
544 backingScale := wv.osrBackingScaleFactor()553 backingScale := wv.osrBackingScaleFactor()
545- cefLevel := cefZoomFromPageAndBackingFactor(factor, backingScale)554+ cefLevel := cefZoomFromPageAndScaleFactors(factor, surfaceScale, backingScale)
546 logging.FromContext(wv.ctx).Debug().555 logging.FromContext(wv.ctx).Debug().
547 Float64("factor", factor).556 Float64("factor", factor).
548 Float64("cef_level", cefLevel).557 Float64("cef_level", cefLevel).
549 Float64("osr_backing_scale", backingScale).558 Float64("osr_backing_scale", backingScale).
559+ Float64("surface_scale", surfaceScale).
550 Msg("cef: SetZoomLevel")560 Msg("cef: SetZoomLevel")
551- wv.applyCEFZoomLevel(host, factor, cefLevel, backingScale)561+ wv.applyCEFZoomLevel(host, factor, cefLevel, surfaceScale, backingScale)
552 wv.zoomFactor.Store(factor)562 wv.zoomFactor.Store(factor)
553 return nil563 return nil
554 }564 }
@@ -1665,23 +1675,23 @@ func (wv *WebView) osrBackingScaleFactor() float64 {
1665 return normalizeScale(wv.viewBridge.OSRBackingScaleFactor())1675 return normalizeScale(wv.viewBridge.OSRBackingScaleFactor())
1666 }1676 }
1667 1677
1668-func (wv *WebView) recordAppliedOSRBackingScale(scale float64) {1678+func (wv *WebView) recordAppliedZoomScaleRatio(surfaceScale, backingScale float64) {
1669 if wv == nil {1679 if wv == nil {
1670 return1680 return
1671 }1681 }
1672- wv.lastAppliedOSRBackingScaleBits.Store(math.Float64bits(normalizeScale(scale)))1682+ wv.lastAppliedZoomScaleRatioBits.Store(math.Float64bits(zoomScaleRatio(surfaceScale, backingScale)))
1673 }1683 }
1674 1684
1675-func (wv *WebView) shouldReapplyZoomForBackingScale(scale float64) bool {1685+func (wv *WebView) shouldReapplyZoomForScaleRatio(surfaceScale, backingScale float64) bool {
1676 if wv == nil {1686 if wv == nil {
1677 return false1687 return false
1678 }1688 }
1679- normalized := normalizeScale(scale)1689+ ratio := zoomScaleRatio(surfaceScale, backingScale)
1680- bits := wv.lastAppliedOSRBackingScaleBits.Load()1690+ bits := wv.lastAppliedZoomScaleRatioBits.Load()
1681 if bits == 0 {1691 if bits == 0 {
1682 return true1692 return true
1683 }1693 }
1684- return math.Float64frombits(bits) != normalized1694+ return math.Float64frombits(bits) != ratio
1685 }1695 }
1686 1696
1687 func (wv *WebView) handleMiddleClickFromBridge() bool {1697 func (wv *WebView) handleMiddleClickFromBridge() bool {
@@ -1982,16 +1992,18 @@ func (wv *WebView) scheduleZoomReadback(expectedFactor, expectedLevel float64) {
1982 if host == nil {1992 if host == nil {
1983 return1993 return
1984 }1994 }
1995+ surfaceScale := wv.viewBridgeScale()
1985 backingScale := wv.osrBackingScaleFactor()1996 backingScale := wv.osrBackingScaleFactor()
1986 actualLevel := host.GetZoomLevel()1997 actualLevel := host.GetZoomLevel()
1987 logging.FromContext(wv.ctx).Debug().1998 logging.FromContext(wv.ctx).Debug().
1988 Int64("delay_ms", delayMs).1999 Int64("delay_ms", delayMs).
1989 Float64("expected_factor", expectedFactor).2000 Float64("expected_factor", expectedFactor).
1990 Float64("expected_cef_level", expectedLevel).2001 Float64("expected_cef_level", expectedLevel).
1991- Float64("actual_factor", pageZoomFromCEFAndBackingLevel(actualLevel, backingScale)).2002+ Float64("actual_factor", pageZoomFromCEFAndScaleLevel(actualLevel, surfaceScale, backingScale)).
1992 Float64("actual_cef_factor", factorFromCEFZoom(actualLevel)).2003 Float64("actual_cef_factor", factorFromCEFZoom(actualLevel)).
1993 Float64("actual_cef_level", actualLevel).2004 Float64("actual_cef_level", actualLevel).
1994 Float64("osr_backing_scale", backingScale).2005 Float64("osr_backing_scale", backingScale).
2006+ Float64("surface_scale", surfaceScale).
1995 Msg("cef: zoom level readback")2007 Msg("cef: zoom level readback")
1996 }))2008 }))
1997 if task == nil {2009 if task == nil {
@@ -2012,14 +2024,16 @@ func (wv *WebView) reapplyCurrentZoomForBackingScale(reason string) {
2012 return2024 return
2013 }2025 }
2014 factor := wv.GetZoomLevel()2026 factor := wv.GetZoomLevel()
2027+ surfaceScale := wv.viewBridgeScale()
2015 backingScale := wv.osrBackingScaleFactor()2028 backingScale := wv.osrBackingScaleFactor()
2016- cefLevel := cefZoomFromPageAndBackingFactor(factor, backingScale)2029+ cefLevel := cefZoomFromPageAndScaleFactors(factor, surfaceScale, backingScale)
2017- wv.applyCEFZoomLevel(host, factor, cefLevel, backingScale)2030+ wv.applyCEFZoomLevel(host, factor, cefLevel, surfaceScale, backingScale)
2018 logging.FromContext(wv.ctx).Debug().2031 logging.FromContext(wv.ctx).Debug().
2019 Str("reason", reason).2032 Str("reason", reason).
2020 Float64("factor", factor).2033 Float64("factor", factor).
2021 Float64("cef_level", cefLevel).2034 Float64("cef_level", cefLevel).
2022 Float64("osr_backing_scale", backingScale).2035 Float64("osr_backing_scale", backingScale).
2036+ Float64("surface_scale", surfaceScale).
2023 Msg("cef: reapplied zoom after OSR backing scale change")2037 Msg("cef: reapplied zoom after OSR backing scale change")
2024 }2038 }
2025 2039
M internal/infrastructure/cef/webview_handlers.go +2 -2
@@ -783,7 +783,7 @@ func (h *handlerSet) attachAfterCreatedBrowser(
783783
784784 h.wv.browser = browser
785785 h.wv.host = host
786- h.wv.lastAppliedOSRBackingScaleBits.Store(0)
786+ h.wv.lastAppliedZoomScaleRatioBits.Store(0)
787787 h.wv.pendingCreate = nil
788788 bridge := h.wv.viewBridge
789789 h.wv.inputAttached = bridge == nil
@@ -901,7 +901,7 @@ func (h *handlerSet) OnBeforeClose(browser purecef.Browser) {
901901 h.wv.mu.Lock()
902902 h.wv.browser = nil
903903 h.wv.host = nil
904- h.wv.lastAppliedOSRBackingScaleBits.Store(0)
904+ h.wv.lastAppliedZoomScaleRatioBits.Store(0)
905905 h.wv.inputAttached = false
906906 if h.wv.findCtrl != nil {
907907 h.wv.findCtrl.setHost(nil)
@@ -783,7 +783,7 @@ func (h *handlerSet) attachAfterCreatedBrowser(
783 783
784 h.wv.browser = browser784 h.wv.browser = browser
785 h.wv.host = host785 h.wv.host = host
786- h.wv.lastAppliedOSRBackingScaleBits.Store(0)786+ h.wv.lastAppliedZoomScaleRatioBits.Store(0)
787 h.wv.pendingCreate = nil787 h.wv.pendingCreate = nil
788 bridge := h.wv.viewBridge788 bridge := h.wv.viewBridge
789 h.wv.inputAttached = bridge == nil789 h.wv.inputAttached = bridge == nil
@@ -901,7 +901,7 @@ func (h *handlerSet) OnBeforeClose(browser purecef.Browser) {
901 h.wv.mu.Lock()901 h.wv.mu.Lock()
902 h.wv.browser = nil902 h.wv.browser = nil
903 h.wv.host = nil903 h.wv.host = nil
904- h.wv.lastAppliedOSRBackingScaleBits.Store(0)904+ h.wv.lastAppliedZoomScaleRatioBits.Store(0)
905 h.wv.inputAttached = false905 h.wv.inputAttached = false
906 if h.wv.findCtrl != nil {906 if h.wv.findCtrl != nil {
907 h.wv.findCtrl.setHost(nil)907 h.wv.findCtrl.setHost(nil)
M internal/infrastructure/cef/webview_viewport.go +3 -2
@@ -500,7 +500,7 @@ func (wv *WebView) syncZoomForBackingScaleOnCEFUIThread(host purecef.BrowserHost
500500 if host == nil || wv == nil {
501501 return
502502 }
503- if !wv.shouldReapplyZoomForBackingScale(wv.osrBackingScaleFactor()) {
503+ if !wv.shouldReapplyZoomForScaleRatio(wv.viewBridgeScale(), wv.osrBackingScaleFactor()) {
504504 return
505505 }
506506 task := cefNewTask(cefTaskFunc(func() {
@@ -513,8 +513,9 @@ func (wv *WebView) syncZoomForBackingScaleOnCEFUIThread(host purecef.BrowserHost
513513 if currentHost != host {
514514 return
515515 }
516+ surfaceScale := wv.viewBridgeScale()
516517 backingScale := wv.osrBackingScaleFactor()
517- if !wv.shouldReapplyZoomForBackingScale(backingScale) {
518+ if !wv.shouldReapplyZoomForScaleRatio(surfaceScale, backingScale) {
518519 return
519520 }
520521 wv.reapplyCurrentZoomForBackingScale(reason)
@@ -500,7 +500,7 @@ func (wv *WebView) syncZoomForBackingScaleOnCEFUIThread(host purecef.BrowserHost
500 if host == nil || wv == nil {500 if host == nil || wv == nil {
501 return501 return
502 }502 }
503- if !wv.shouldReapplyZoomForBackingScale(wv.osrBackingScaleFactor()) {503+ if !wv.shouldReapplyZoomForScaleRatio(wv.viewBridgeScale(), wv.osrBackingScaleFactor()) {
504 return504 return
505 }505 }
506 task := cefNewTask(cefTaskFunc(func() {506 task := cefNewTask(cefTaskFunc(func() {
@@ -513,8 +513,9 @@ func (wv *WebView) syncZoomForBackingScaleOnCEFUIThread(host purecef.BrowserHost
513 if currentHost != host {513 if currentHost != host {
514 return514 return
515 }515 }
516+ surfaceScale := wv.viewBridgeScale()
516 backingScale := wv.osrBackingScaleFactor()517 backingScale := wv.osrBackingScaleFactor()
517- if !wv.shouldReapplyZoomForBackingScale(backingScale) {518+ if !wv.shouldReapplyZoomForScaleRatio(surfaceScale, backingScale) {
518 return519 return
519 }520 }
520 wv.reapplyCurrentZoomForBackingScale(reason)521 wv.reapplyCurrentZoomForBackingScale(reason)
M internal/infrastructure/cef/webview_zoom_test.go +53 -9
@@ -18,14 +18,58 @@ func TestZoomConversionsRoundTrip(t *testing.T) {
1818 }
1919 }
2020
21-func TestZoomConversionsCompensateDeviceSizedOSRBackingInternally(t *testing.T) {
22- for _, backing := range []float64{1.0, 1.25, 1.44, 2.0} {
23- for _, pageZoom := range []float64{1.0, 1.75} {
24- t.Run(fmt.Sprintf("backing_%.2f_page_%.2f", backing, pageZoom), func(t *testing.T) {
25- level := cefZoomFromPageAndBackingFactor(pageZoom, backing)
26- assert.InDelta(t, pageZoom/backing, factorFromCEFZoom(level), 1e-9)
27- assert.InDelta(t, pageZoom, pageZoomFromCEFAndBackingLevel(level, backing), 1e-9)
28- })
29- }
21+func TestZoomConversionsCompensateDeviceSizedOSRBackingAndSurfaceScale(t *testing.T) {
22+ tests := []struct {
23+ name string
24+ surfaceScale float64
25+ backingScale float64
26+ pageZoom float64
27+ wantInternal float64
28+ }{
29+ {
30+ name: "fractional_hidpi_keeps_100_percent_internal_zoom",
31+ surfaceScale: 1.2,
32+ backingScale: 1.2,
33+ pageZoom: 1.0,
34+ wantInternal: 1.0,
35+ },
36+ {
37+ name: "integer_hidpi_keeps_requested_user_zoom",
38+ surfaceScale: 2.0,
39+ backingScale: 2.0,
40+ pageZoom: 1.2,
41+ wantInternal: 1.2,
42+ },
43+ {
44+ name: "normal_osr_hidpi_keeps_requested_user_zoom",
45+ surfaceScale: 1.2,
46+ backingScale: 1.0,
47+ pageZoom: 1.0,
48+ wantInternal: 1.0,
49+ },
50+ {
51+ name: "device_sized_backing_remains_compensated",
52+ surfaceScale: 1.25,
53+ backingScale: 2.0,
54+ pageZoom: 1.2,
55+ wantInternal: 0.75,
56+ },
3057 }
58+
59+ for _, tt := range tests {
60+ t.Run(tt.name, func(t *testing.T) {
61+ level := cefZoomFromPageAndScaleFactors(tt.pageZoom, tt.surfaceScale, tt.backingScale)
62+ assert.InDelta(t, tt.wantInternal, factorFromCEFZoom(level), 1e-9)
63+ assert.InDelta(t, tt.pageZoom, pageZoomFromCEFAndScaleLevel(level, tt.surfaceScale, tt.backingScale), 1e-9)
64+ })
65+ }
66+}
67+
68+func TestZoomReapplyGuardTracksEffectiveScaleRatio(t *testing.T) {
69+ wv := &WebView{}
70+
71+ wv.recordAppliedZoomScaleRatio(1.2, 1.2)
72+
73+ assert.False(t, wv.shouldReapplyZoomForScaleRatio(2.0, 2.0), "same effective ratio should not reapply")
74+ assert.True(t, wv.shouldReapplyZoomForScaleRatio(1.2, 2.0), "changed active-backing ratio should reapply")
3175 }
@@ -18,14 +18,58 @@ func TestZoomConversionsRoundTrip(t *testing.T) {
18 }18 }
19 }19 }
20 20
21-func TestZoomConversionsCompensateDeviceSizedOSRBackingInternally(t *testing.T) {21+func TestZoomConversionsCompensateDeviceSizedOSRBackingAndSurfaceScale(t *testing.T) {
22- for _, backing := range []float64{1.0, 1.25, 1.44, 2.0} {22+ tests := []struct {
23- for _, pageZoom := range []float64{1.0, 1.75} {23+ name string
24- t.Run(fmt.Sprintf("backing_%.2f_page_%.2f", backing, pageZoom), func(t *testing.T) {24+ surfaceScale float64
25- level := cefZoomFromPageAndBackingFactor(pageZoom, backing)25+ backingScale float64
26- assert.InDelta(t, pageZoom/backing, factorFromCEFZoom(level), 1e-9)26+ pageZoom float64
27- assert.InDelta(t, pageZoom, pageZoomFromCEFAndBackingLevel(level, backing), 1e-9)27+ wantInternal float64
28- })28+ }{
29- }29+ {
30+ name: "fractional_hidpi_keeps_100_percent_internal_zoom",
31+ surfaceScale: 1.2,
32+ backingScale: 1.2,
33+ pageZoom: 1.0,
34+ wantInternal: 1.0,
35+ },
36+ {
37+ name: "integer_hidpi_keeps_requested_user_zoom",
38+ surfaceScale: 2.0,
39+ backingScale: 2.0,
40+ pageZoom: 1.2,
41+ wantInternal: 1.2,
42+ },
43+ {
44+ name: "normal_osr_hidpi_keeps_requested_user_zoom",
45+ surfaceScale: 1.2,
46+ backingScale: 1.0,
47+ pageZoom: 1.0,
48+ wantInternal: 1.0,
49+ },
50+ {
51+ name: "device_sized_backing_remains_compensated",
52+ surfaceScale: 1.25,
53+ backingScale: 2.0,
54+ pageZoom: 1.2,
55+ wantInternal: 0.75,
56+ },
30 }57 }
58+
59+ for _, tt := range tests {
60+ t.Run(tt.name, func(t *testing.T) {
61+ level := cefZoomFromPageAndScaleFactors(tt.pageZoom, tt.surfaceScale, tt.backingScale)
62+ assert.InDelta(t, tt.wantInternal, factorFromCEFZoom(level), 1e-9)
63+ assert.InDelta(t, tt.pageZoom, pageZoomFromCEFAndScaleLevel(level, tt.surfaceScale, tt.backingScale), 1e-9)
64+ })
65+ }
66+}
67+
68+func TestZoomReapplyGuardTracksEffectiveScaleRatio(t *testing.T) {
69+ wv := &WebView{}
70+
71+ wv.recordAppliedZoomScaleRatio(1.2, 1.2)
72+
73+ assert.False(t, wv.shouldReapplyZoomForScaleRatio(2.0, 2.0), "same effective ratio should not reapply")
74+ assert.True(t, wv.shouldReapplyZoomForScaleRatio(1.2, 2.0), "changed active-backing ratio should reapply")
31 }75 }
M internal/infrastructure/config/loader.go +20 -2
@@ -321,9 +321,16 @@ func (m *Manager) checkLegacyFormat() error {
321321 }
322322 }
323323
324- hasEngineSection := m.viper.InConfig("engine.type")
324+ hasEngineSection := m.hasEngineSectionInConfig()
325325
326- if hasOldSections && !hasEngineSection {
326+ if hasOldSections && hasEngineSection {
327+ return fmt.Errorf(
328+ "mixed old/new config: [engine] coexists with legacy engine keys in %s; "+
329+ "remove legacy keys or migrate from a purely legacy config",
330+ legacyEngineInputSectionsMessage(),
331+ )
332+ }
333+ if hasOldSections {
327334 return fmt.Errorf(
328335 "config format outdated: legacy engine keys in %s have moved to "+
329336 "[engine]/[engine.webkit] — run \"dumber config migrate\" "+
@@ -334,6 +341,17 @@ func (m *Manager) checkLegacyFormat() error {
334341 return nil
335342 }
336343
344+func (m *Manager) hasEngineSectionInConfig() bool {
345+ if configFile := m.viper.ConfigFileUsed(); configFile != "" {
346+ raw, err := readRawTOML(configFile)
347+ if err == nil {
348+ _, exists := raw["engine"]
349+ return exists
350+ }
351+ }
352+ return m.viper.InConfig("engine.type")
353+}
354+
337355 func (m *Manager) unmarshalConfig() (*Config, error) {
338356 config := &Config{}
339357 if err := m.viper.Unmarshal(config); err != nil {
@@ -321,9 +321,16 @@ func (m *Manager) checkLegacyFormat() error {
321 }321 }
322 }322 }
323 323
324- hasEngineSection := m.viper.InConfig("engine.type")324+ hasEngineSection := m.hasEngineSectionInConfig()
325 325
326- if hasOldSections && !hasEngineSection {326+ if hasOldSections && hasEngineSection {
327+ return fmt.Errorf(
328+ "mixed old/new config: [engine] coexists with legacy engine keys in %s; "+
329+ "remove legacy keys or migrate from a purely legacy config",
330+ legacyEngineInputSectionsMessage(),
331+ )
332+ }
333+ if hasOldSections {
327 return fmt.Errorf(334 return fmt.Errorf(
328 "config format outdated: legacy engine keys in %s have moved to "+335 "config format outdated: legacy engine keys in %s have moved to "+
329 "[engine]/[engine.webkit] — run \"dumber config migrate\" "+336 "[engine]/[engine.webkit] — run \"dumber config migrate\" "+
@@ -334,6 +341,17 @@ func (m *Manager) checkLegacyFormat() error {
334 return nil341 return nil
335 }342 }
336 343
344+func (m *Manager) hasEngineSectionInConfig() bool {
345+ if configFile := m.viper.ConfigFileUsed(); configFile != "" {
346+ raw, err := readRawTOML(configFile)
347+ if err == nil {
348+ _, exists := raw["engine"]
349+ return exists
350+ }
351+ }
352+ return m.viper.InConfig("engine.type")
353+}
354+
337 func (m *Manager) unmarshalConfig() (*Config, error) {355 func (m *Manager) unmarshalConfig() (*Config, error) {
338 config := &Config{}356 config := &Config{}
339 if err := m.viper.Unmarshal(config); err != nil {357 if err := m.viper.Unmarshal(config); err != nil {
M internal/infrastructure/config/loader_legacy_test.go +16 -0
@@ -29,6 +29,22 @@ func TestCheckLegacyFormat_OldSectionsWithEngine_ReturnsError(t *testing.T) {
2929 require.Error(t, err)
3030 require.Contains(t, err.Error(), "dumber config migrate")
3131 }
32+func TestCheckLegacyFormat_MixedEngineSectionReturnsMixedConfigError(t *testing.T) {
33+ dir := t.TempDir()
34+ configPath := filepath.Join(dir, "config.toml")
35+ require.NoError(t, os.WriteFile(configPath, []byte("[engine]\nprofile = \"balanced\"\n\n[rendering]\ngsk_renderer = \"auto\"\n"), 0o600))
36+
37+ m := &Manager{viper: viper.New()}
38+ m.viper.SetConfigFile(configPath)
39+ m.viper.SetConfigType("toml")
40+ require.NoError(t, m.viper.ReadInConfig())
41+
42+ err := m.checkLegacyFormat()
43+
44+ require.Error(t, err)
45+ require.Contains(t, err.Error(), "mixed old/new config")
46+ require.NotContains(t, err.Error(), "dumber config migrate")
47+}
3248
3349 func TestCheckLegacyFormat_FreshConfig_ReturnsNoError(t *testing.T) {
3450 m := &Manager{viper: viper.New()}
@@ -29,6 +29,22 @@ func TestCheckLegacyFormat_OldSectionsWithEngine_ReturnsError(t *testing.T) {
29 require.Error(t, err)29 require.Error(t, err)
30 require.Contains(t, err.Error(), "dumber config migrate")30 require.Contains(t, err.Error(), "dumber config migrate")
31 }31 }
32+func TestCheckLegacyFormat_MixedEngineSectionReturnsMixedConfigError(t *testing.T) {
33+ dir := t.TempDir()
34+ configPath := filepath.Join(dir, "config.toml")
35+ require.NoError(t, os.WriteFile(configPath, []byte("[engine]\nprofile = \"balanced\"\n\n[rendering]\ngsk_renderer = \"auto\"\n"), 0o600))
36+
37+ m := &Manager{viper: viper.New()}
38+ m.viper.SetConfigFile(configPath)
39+ m.viper.SetConfigType("toml")
40+ require.NoError(t, m.viper.ReadInConfig())
41+
42+ err := m.checkLegacyFormat()
43+
44+ require.Error(t, err)
45+ require.Contains(t, err.Error(), "mixed old/new config")
46+ require.NotContains(t, err.Error(), "dumber config migrate")
47+}
32 48
33 func TestCheckLegacyFormat_FreshConfig_ReturnsNoError(t *testing.T) {49 func TestCheckLegacyFormat_FreshConfig_ReturnsNoError(t *testing.T) {
34 m := &Manager{viper: viper.New()}50 m := &Manager{viper: viper.New()}
M internal/infrastructure/config/migrate_engine.go +10 -0
@@ -156,6 +156,15 @@ func hasLegacyEngineAlias(raw map[string]any, alias legacyEngineAlias) bool {
156156 return exists
157157 }
158158
159+func canonicalLegacyEngineValue(alias legacyEngineAlias, val any) any {
160+ if alias.targetSection == engineTargetSection && alias.targetField == "cookie_policy" {
161+ if cookiePolicy, ok := val.(string); ok && cookiePolicy == "accept_all" {
162+ return string(CookiePolicyAlways)
163+ }
164+ }
165+ return val
166+}
167+
159168 // MigrateToEngineConfig restructures old top-level config sections into [engine]/[engine.webkit].
160169 // Returns true if migration was performed, false if already migrated or no old sections found.
161170 func MigrateToEngineConfig(configFile string) (bool, error) {
@@ -245,6 +254,7 @@ func applyEngineMappings(raw map[string]any) {
245254 if val == nil {
246255 continue
247256 }
257+ val = canonicalLegacyEngineValue(alias, val)
248258 switch alias.targetSection {
249259 case engineTargetSection:
250260 engineMap[alias.targetField] = val
@@ -156,6 +156,15 @@ func hasLegacyEngineAlias(raw map[string]any, alias legacyEngineAlias) bool {
156 return exists156 return exists
157 }157 }
158 158
159+func canonicalLegacyEngineValue(alias legacyEngineAlias, val any) any {
160+ if alias.targetSection == engineTargetSection && alias.targetField == "cookie_policy" {
161+ if cookiePolicy, ok := val.(string); ok && cookiePolicy == "accept_all" {
162+ return string(CookiePolicyAlways)
163+ }
164+ }
165+ return val
166+}
167+
159 // MigrateToEngineConfig restructures old top-level config sections into [engine]/[engine.webkit].168 // MigrateToEngineConfig restructures old top-level config sections into [engine]/[engine.webkit].
160 // Returns true if migration was performed, false if already migrated or no old sections found.169 // Returns true if migration was performed, false if already migrated or no old sections found.
161 func MigrateToEngineConfig(configFile string) (bool, error) {170 func MigrateToEngineConfig(configFile string) (bool, error) {
@@ -245,6 +254,7 @@ func applyEngineMappings(raw map[string]any) {
245 if val == nil {254 if val == nil {
246 continue255 continue
247 }256 }
257+ val = canonicalLegacyEngineValue(alias, val)
248 switch alias.targetSection {258 switch alias.targetSection {
249 case engineTargetSection:259 case engineTargetSection:
250 engineMap[alias.targetField] = val260 engineMap[alias.targetField] = val
M internal/infrastructure/config/migrate_engine_test.go +2 -2
@@ -89,8 +89,8 @@ func TestMigrateToEngineConfig_OldFormat(t *testing.T) {
8989 assert.Equal(t, int64(50), engine["zoom_cache_size"])
9090 assert.Equal(t, int64(2), engine["pool_prewarm_count"])
9191
92- // [privacy] -> [engine] universal field
93- assert.Equal(t, "accept_all", engine["cookie_policy"])
92+ // [privacy] -> [engine] universal field, translated to canonical schema value
93+ assert.Equal(t, "always", engine["cookie_policy"])
9494
9595 // [engine.webkit] section
9696 webkitAny, ok := engine["webkit"]
@@ -89,8 +89,8 @@ func TestMigrateToEngineConfig_OldFormat(t *testing.T) {
89 assert.Equal(t, int64(50), engine["zoom_cache_size"])89 assert.Equal(t, int64(50), engine["zoom_cache_size"])
90 assert.Equal(t, int64(2), engine["pool_prewarm_count"])90 assert.Equal(t, int64(2), engine["pool_prewarm_count"])
91 91
92- // [privacy] -> [engine] universal field92+ // [privacy] -> [engine] universal field, translated to canonical schema value
93- assert.Equal(t, "accept_all", engine["cookie_policy"])93+ assert.Equal(t, "always", engine["cookie_policy"])
94 94
95 // [engine.webkit] section95 // [engine.webkit] section
96 webkitAny, ok := engine["webkit"]96 webkitAny, ok := engine["webkit"]
M internal/ui/app.go +1 -1
@@ -1252,7 +1252,7 @@ func NewStandaloneOmniboxRuntime(
12521252 }
12531253 var localPathResolver port.LocalPathResolver
12541254 if deps != nil {
1255- localPathResolver, _ = deps.FileSystem.(port.LocalPathResolver)
1255+ localPathResolver = deps.LocalPathResolver
12561256 }
12571257 navigationURLNormalizer := usecase.NewNavigationURLNormalizer(localPathResolver)
12581258 normalizeNavigationURL := func(navCtx context.Context, input string) string {
@@ -1252,7 +1252,7 @@ func NewStandaloneOmniboxRuntime(
1252 }1252 }
1253 var localPathResolver port.LocalPathResolver1253 var localPathResolver port.LocalPathResolver
1254 if deps != nil {1254 if deps != nil {
1255- localPathResolver, _ = deps.FileSystem.(port.LocalPathResolver)1255+ localPathResolver = deps.LocalPathResolver
1256 }1256 }
1257 navigationURLNormalizer := usecase.NewNavigationURLNormalizer(localPathResolver)1257 navigationURLNormalizer := usecase.NewNavigationURLNormalizer(localPathResolver)
1258 normalizeNavigationURL := func(navCtx context.Context, input string) string {1258 normalizeNavigationURL := func(navCtx context.Context, input string) string {
M internal/ui/component/omnibox.go +1 -7
@@ -2698,13 +2698,7 @@ func (o *Omnibox) buildURL(text string) string {
26982698 if o.shortcutsUC != nil {
26992699 shortcutURLs = o.shortcutsUC.ShortcutURLs()
27002700 }
2701- if _, _, found := url.ParseBangShortcut(text); !found && o.normalizeNavigationURL != nil {
2702- normalized := o.normalizeNavigationURL(o.ctx, text)
2703- if normalized != text {
2704- return normalized
2705- }
2706- }
2707- return url.BuildSearchURL(text, shortcutURLs, o.defaultSearch)
2701+ return usecase.BuildNavigationURL(o.ctx, text, o.normalizeNavigationURL, shortcutURLs, o.defaultSearch)
27082702 }
27092703
27102704 // toggleViewMode switches between history and favorites.
@@ -2698,13 +2698,7 @@ func (o *Omnibox) buildURL(text string) string {
2698 if o.shortcutsUC != nil {2698 if o.shortcutsUC != nil {
2699 shortcutURLs = o.shortcutsUC.ShortcutURLs()2699 shortcutURLs = o.shortcutsUC.ShortcutURLs()
2700 }2700 }
2701- if _, _, found := url.ParseBangShortcut(text); !found && o.normalizeNavigationURL != nil {2701+ return usecase.BuildNavigationURL(o.ctx, text, o.normalizeNavigationURL, shortcutURLs, o.defaultSearch)
2702- normalized := o.normalizeNavigationURL(o.ctx, text)
2703- if normalized != text {
2704- return normalized
2705- }
2706- }
2707- return url.BuildSearchURL(text, shortcutURLs, o.defaultSearch)
2708 }2702 }
2709 2703
2710 // toggleViewMode switches between history and favorites.2704 // toggleViewMode switches between history and favorites.
M internal/ui/deps.go +2 -0
@@ -89,6 +89,8 @@ type Dependencies struct {
8989 SessionSpawner port.SessionSpawner
9090 // FileSystem provides file operations (e.g., for download deduplication).
9191 FileSystem port.FileSystem
92+ // LocalPathResolver resolves user-entered local filesystem paths for navigation.
93+ LocalPathResolver port.LocalPathResolver
9294
9395 // Update management
9496 CheckUpdateUC *usecase.CheckUpdateUseCase
@@ -89,6 +89,8 @@ type Dependencies struct {
89 SessionSpawner port.SessionSpawner89 SessionSpawner port.SessionSpawner
90 // FileSystem provides file operations (e.g., for download deduplication).90 // FileSystem provides file operations (e.g., for download deduplication).
91 FileSystem port.FileSystem91 FileSystem port.FileSystem
92+ // LocalPathResolver resolves user-entered local filesystem paths for navigation.
93+ LocalPathResolver port.LocalPathResolver
92 94
93 // Update management95 // Update management
94 CheckUpdateUC *usecase.CheckUpdateUseCase96 CheckUpdateUC *usecase.CheckUpdateUseCase
M internal/ui/standalone_runtime_test.go +24 -0
@@ -3,11 +3,35 @@ package ui
33 import (
44 "context"
55 "errors"
6+ stdurl "net/url"
7+ "path/filepath"
68 "testing"
79
810 urlutil "github.com/bnema/dumber/internal/domain/url"
911 )
1012
13+type standaloneTestLocalPathResolver struct {
14+ paths map[string]string
15+}
16+
17+func (r standaloneTestLocalPathResolver) ResolveExistingPath(_ context.Context, input string) (string, bool, error) {
18+ absPath, ok := r.paths[input]
19+ return absPath, ok, nil
20+}
21+
22+func TestNewStandaloneOmniboxRuntimeUsesExplicitLocalPathResolver(t *testing.T) {
23+ absFile := filepath.Join(string(filepath.Separator), "tmp", "page.html")
24+ runtime := NewStandaloneOmniboxRuntime(context.Background(), &Dependencies{
25+ LocalPathResolver: standaloneTestLocalPathResolver{paths: map[string]string{"page.html": absFile}},
26+ }, nil)
27+
28+ got := runtime.OmniboxCfg.NormalizeNavigationURL(context.Background(), "page.html")
29+ want := (&stdurl.URL{Scheme: "file", Path: absFile}).String()
30+ if got != want {
31+ t.Fatalf("NormalizeNavigationURL(page.html) = %q, want %q", got, want)
32+ }
33+}
34+
1135 func TestHandleStandaloneOmniboxNavigation_UsesFreshBrowserWindowForHTTP(t *testing.T) {
1236 browserURL := ""
1337 externalURL := ""
@@ -3,11 +3,35 @@ package ui
3 import (3 import (
4 "context"4 "context"
5 "errors"5 "errors"
6+ stdurl "net/url"
7+ "path/filepath"
6 "testing"8 "testing"
7 9
8 urlutil "github.com/bnema/dumber/internal/domain/url"10 urlutil "github.com/bnema/dumber/internal/domain/url"
9 )11 )
10 12
13+type standaloneTestLocalPathResolver struct {
14+ paths map[string]string
15+}
16+
17+func (r standaloneTestLocalPathResolver) ResolveExistingPath(_ context.Context, input string) (string, bool, error) {
18+ absPath, ok := r.paths[input]
19+ return absPath, ok, nil
20+}
21+
22+func TestNewStandaloneOmniboxRuntimeUsesExplicitLocalPathResolver(t *testing.T) {
23+ absFile := filepath.Join(string(filepath.Separator), "tmp", "page.html")
24+ runtime := NewStandaloneOmniboxRuntime(context.Background(), &Dependencies{
25+ LocalPathResolver: standaloneTestLocalPathResolver{paths: map[string]string{"page.html": absFile}},
26+ }, nil)
27+
28+ got := runtime.OmniboxCfg.NormalizeNavigationURL(context.Background(), "page.html")
29+ want := (&stdurl.URL{Scheme: "file", Path: absFile}).String()
30+ if got != want {
31+ t.Fatalf("NormalizeNavigationURL(page.html) = %q, want %q", got, want)
32+ }
33+}
34+
11 func TestHandleStandaloneOmniboxNavigation_UsesFreshBrowserWindowForHTTP(t *testing.T) {35 func TestHandleStandaloneOmniboxNavigation_UsesFreshBrowserWindowForHTTP(t *testing.T) {
12 browserURL := ""36 browserURL := ""
13 externalURL := ""37 externalURL := ""

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