detached c2c7597

chore(cef): satisfy lint for scale probe

commit
c2c7597304f830516c3c361eccba482e20fd9363
author
brice · · 2026-05-27 18:40 UTC
committer
brice · · 2026-05-27 18:47 UTC
parent
206243c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +90 -45
M internal/application/port/engine.go +13 -13
@@ -142,22 +142,22 @@ type Engine interface {
142142 SetHandlerContext(ctx context.Context)
143143 }
144144
145+// EngineSettingsPayload is the engine-facing boundary view of runtime config.
146+// Add fields here only when an engine needs to react to them at runtime.
147+type EngineSettingsPayload struct {
148+ DefaultUIScale float64
149+}
150+
145151 // EngineSettingsUpdate carries a runtime config change to the engine.
146152 //
147-// The Raw field holds the implementation-specific config object; each engine
148-// implementation type-asserts it to its concrete config type (e.g. the WebKit
149-// engine asserts to *infrastructure/config.Config).
150-//
151-// This is the only intentional use of `any` in the Engine port. A fully typed
152-// approach would require either moving the entire config schema into the domain
153-// layer (a large refactor) or defining a SettingsProvider interface — which
154-// would itself reduce to `any` because callers would embed engine-specific data
155-// behind an empty interface. Until the config schema is promoted to domain,
156-// the type assertion in the engine implementation is the least-invasive option.
153+// Settings exposes the typed boundary payload that engine adapters should prefer.
154+// Raw remains for legacy adapters that still consume implementation-specific
155+// config until their settings managers are narrowed to boundary fields.
157156 type EngineSettingsUpdate struct {
158- // Raw holds the implementation-specific config.
159- // WebKit engine expects *infrastructure/config.Config.
160- Raw any //nolint:iface // intentional: see type comment above
157+ Settings EngineSettingsPayload
158+ // Raw holds the implementation-specific config for legacy adapters.
159+ // WebKit engine currently expects *infrastructure/config.Config.
160+ Raw any //nolint:iface // intentional legacy bridge; prefer Settings for new code
161161 }
162162
163163 // WebUIMessageHandler handles a decoded message payload from the JS bridge.
@@ -142,22 +142,22 @@ type Engine interface {
142 SetHandlerContext(ctx context.Context)142 SetHandlerContext(ctx context.Context)
143 }143 }
144 144
145+// EngineSettingsPayload is the engine-facing boundary view of runtime config.
146+// Add fields here only when an engine needs to react to them at runtime.
147+type EngineSettingsPayload struct {
148+ DefaultUIScale float64
149+}
150+
145 // EngineSettingsUpdate carries a runtime config change to the engine.151 // EngineSettingsUpdate carries a runtime config change to the engine.
146 //152 //
147-// The Raw field holds the implementation-specific config object; each engine153+// Settings exposes the typed boundary payload that engine adapters should prefer.
148-// implementation type-asserts it to its concrete config type (e.g. the WebKit154+// Raw remains for legacy adapters that still consume implementation-specific
149-// engine asserts to *infrastructure/config.Config).155+// config until their settings managers are narrowed to boundary fields.
150-//
151-// This is the only intentional use of `any` in the Engine port. A fully typed
152-// approach would require either moving the entire config schema into the domain
153-// layer (a large refactor) or defining a SettingsProvider interface — which
154-// would itself reduce to `any` because callers would embed engine-specific data
155-// behind an empty interface. Until the config schema is promoted to domain,
156-// the type assertion in the engine implementation is the least-invasive option.
157 type EngineSettingsUpdate struct {156 type EngineSettingsUpdate struct {
158- // Raw holds the implementation-specific config.157+ Settings EngineSettingsPayload
159- // WebKit engine expects *infrastructure/config.Config.158+ // Raw holds the implementation-specific config for legacy adapters.
160- Raw any //nolint:iface // intentional: see type comment above159+ // WebKit engine currently expects *infrastructure/config.Config.
160+ Raw any //nolint:iface // intentional legacy bridge; prefer Settings for new code
161 }161 }
162 162
163 // WebUIMessageHandler handles a decoded message payload from the JS bridge.163 // WebUIMessageHandler handles a decoded message payload from the JS bridge.
M internal/infrastructure/cef/engine.go +26 -16
@@ -10,8 +10,6 @@ import (
1010 "time"
1111 "unicode/utf8"
1212
13- "github.com/bnema/dumber/internal/infrastructure/config"
14-
1513 purecef "github.com/bnema/purego-cef/cef"
1614 cef2gtk "github.com/bnema/purego-cef2gtk"
1715
@@ -27,13 +25,14 @@ var _ port.AlreadyRunningAppRelaunchHandlerSetter = (*Engine)(nil)
2725 // Engine implements port.Engine for the CEF browser backend.
2826 // It manages the CEF lifecycle and provides access to all engine subsystems.
2927 type Engine struct {
30- ctx context.Context
31- factory *WebViewFactory
32- pool *WebViewPool
33- profileLogDir string
34- runtimeCEFDir string
35- renderStackPlan cef2gtk.RenderStackPlan
36- applicationScale float64
28+ ctx context.Context
29+ factory *WebViewFactory
30+ pool *WebViewPool
31+ profileLogDir string
32+ runtimeCEFDir string
33+ renderStackPlan cef2gtk.RenderStackPlan
34+ applicationScaleMu sync.RWMutex
35+ applicationScale float64
3736
3837 messageRouter *MessageRouter
3938 schemeHandler *dumbSchemeHandler
@@ -357,21 +356,32 @@ func (e *Engine) UpdateAppearance(_ context.Context, r, g, b, alpha float64) err
357356 // a new engine lifetime. Browser zoom remains independently adjustable at
358357 // runtime through SetZoomLevel.
359358 func (e *Engine) UpdateSettings(ctx context.Context, update port.EngineSettingsUpdate) error {
360- cfg, ok := update.Raw.(*config.Config)
361- if !ok {
362- return fmt.Errorf("UpdateSettings: expected *config.Config, got %T", update.Raw)
359+ newScale := normalizedApplicationScale(update.Settings.DefaultUIScale)
360+ e.applicationScaleMu.Lock()
361+ oldScale := e.applicationScale
362+ if oldScale != newScale {
363+ e.applicationScale = newScale
363364 }
364- newScale := normalizedApplicationScale(cfg.DefaultUIScale)
365- if e.applicationScale != newScale {
365+ e.applicationScaleMu.Unlock()
366+
367+ if oldScale != newScale {
366368 logging.FromContext(ctx).Info().
367- Float64("old_application_scale", e.applicationScale).
369+ Float64("old_application_scale", oldScale).
368370 Float64("new_application_scale", newScale).
369371 Msg("cef: application scale update will apply to newly-created CEF views")
370- e.applicationScale = newScale
371372 }
372373 return nil
373374 }
374375
376+func (e *Engine) currentApplicationScale() float64 {
377+ if e == nil {
378+ return 1
379+ }
380+ e.applicationScaleMu.RLock()
381+ defer e.applicationScaleMu.RUnlock()
382+ return normalizedApplicationScale(e.applicationScale)
383+}
384+
375385 func (e *Engine) handleExplicitClipboardBridgeText(viewID port.WebViewID, action, text string) {
376386 if e == nil || text == "" {
377387 return
@@ -10,8 +10,6 @@ import (
10 "time"10 "time"
11 "unicode/utf8"11 "unicode/utf8"
12 12
13- "github.com/bnema/dumber/internal/infrastructure/config"
14-
15 purecef "github.com/bnema/purego-cef/cef"13 purecef "github.com/bnema/purego-cef/cef"
16 cef2gtk "github.com/bnema/purego-cef2gtk"14 cef2gtk "github.com/bnema/purego-cef2gtk"
17 15
@@ -27,13 +25,14 @@ var _ port.AlreadyRunningAppRelaunchHandlerSetter = (*Engine)(nil)
27 // Engine implements port.Engine for the CEF browser backend.25 // Engine implements port.Engine for the CEF browser backend.
28 // It manages the CEF lifecycle and provides access to all engine subsystems.26 // It manages the CEF lifecycle and provides access to all engine subsystems.
29 type Engine struct {27 type Engine struct {
30- ctx context.Context28+ ctx context.Context
31- factory *WebViewFactory29+ factory *WebViewFactory
32- pool *WebViewPool30+ pool *WebViewPool
33- profileLogDir string31+ profileLogDir string
34- runtimeCEFDir string32+ runtimeCEFDir string
35- renderStackPlan cef2gtk.RenderStackPlan33+ renderStackPlan cef2gtk.RenderStackPlan
36- applicationScale float6434+ applicationScaleMu sync.RWMutex
35+ applicationScale float64
37 36
38 messageRouter *MessageRouter37 messageRouter *MessageRouter
39 schemeHandler *dumbSchemeHandler38 schemeHandler *dumbSchemeHandler
@@ -357,21 +356,32 @@ func (e *Engine) UpdateAppearance(_ context.Context, r, g, b, alpha float64) err
357 // a new engine lifetime. Browser zoom remains independently adjustable at356 // a new engine lifetime. Browser zoom remains independently adjustable at
358 // runtime through SetZoomLevel.357 // runtime through SetZoomLevel.
359 func (e *Engine) UpdateSettings(ctx context.Context, update port.EngineSettingsUpdate) error {358 func (e *Engine) UpdateSettings(ctx context.Context, update port.EngineSettingsUpdate) error {
360- cfg, ok := update.Raw.(*config.Config)359+ newScale := normalizedApplicationScale(update.Settings.DefaultUIScale)
361- if !ok {360+ e.applicationScaleMu.Lock()
362- return fmt.Errorf("UpdateSettings: expected *config.Config, got %T", update.Raw)361+ oldScale := e.applicationScale
362+ if oldScale != newScale {
363+ e.applicationScale = newScale
363 }364 }
364- newScale := normalizedApplicationScale(cfg.DefaultUIScale)365+ e.applicationScaleMu.Unlock()
365- if e.applicationScale != newScale {366+
367+ if oldScale != newScale {
366 logging.FromContext(ctx).Info().368 logging.FromContext(ctx).Info().
367- Float64("old_application_scale", e.applicationScale).369+ Float64("old_application_scale", oldScale).
368 Float64("new_application_scale", newScale).370 Float64("new_application_scale", newScale).
369 Msg("cef: application scale update will apply to newly-created CEF views")371 Msg("cef: application scale update will apply to newly-created CEF views")
370- e.applicationScale = newScale
371 }372 }
372 return nil373 return nil
373 }374 }
374 375
376+func (e *Engine) currentApplicationScale() float64 {
377+ if e == nil {
378+ return 1
379+ }
380+ e.applicationScaleMu.RLock()
381+ defer e.applicationScaleMu.RUnlock()
382+ return normalizedApplicationScale(e.applicationScale)
383+}
384+
375 func (e *Engine) handleExplicitClipboardBridgeText(viewID port.WebViewID, action, text string) {385 func (e *Engine) handleExplicitClipboardBridgeText(viewID port.WebViewID, action, text string) {
376 if e == nil || text == "" {386 if e == nil || text == "" {
377 return387 return
M internal/infrastructure/cef/engine_init.go +1 -1
@@ -88,7 +88,7 @@ func NewEngine(
8888 Bool("external_begin_frame", externalBeginFrameEnabled()).
8989 Bool("trace_handlers", cfg.TraceHandlers).
9090 Bool("enable_audio_handler", cfg.EnableAudioHandler).
91- Float64("application_scale", eng.applicationScale).
91+ Float64("application_scale", eng.currentApplicationScale()).
9292 Msg("cef: configured engine")
9393
9494 if err := initializeCEF(eng, settings, logger); err != nil {
@@ -88,7 +88,7 @@ func NewEngine(
88 Bool("external_begin_frame", externalBeginFrameEnabled()).88 Bool("external_begin_frame", externalBeginFrameEnabled()).
89 Bool("trace_handlers", cfg.TraceHandlers).89 Bool("trace_handlers", cfg.TraceHandlers).
90 Bool("enable_audio_handler", cfg.EnableAudioHandler).90 Bool("enable_audio_handler", cfg.EnableAudioHandler).
91- Float64("application_scale", eng.applicationScale).91+ Float64("application_scale", eng.currentApplicationScale()).
92 Msg("cef: configured engine")92 Msg("cef: configured engine")
93 93
94 if err := initializeCEF(eng, settings, logger); err != nil {94 if err := initializeCEF(eng, settings, logger); err != nil {
M internal/infrastructure/cef/engine_settings_test.go +34 -10
@@ -2,27 +2,51 @@ package cef
22
33 import (
44 "context"
5+ "sync"
56 "testing"
67
78 "github.com/bnema/dumber/internal/application/port"
8- "github.com/bnema/dumber/internal/infrastructure/config"
99 )
1010
11-func TestEngineUpdateSettingsUpdatesApplicationScaleForNewViews(t *testing.T) {
11+func TestEngineUpdateSettingsUpdatesApplicationScaleFromBoundaryPayload(t *testing.T) {
1212 e := &Engine{applicationScale: 1}
13- cfg := &config.Config{DefaultUIScale: 1.25}
1413
15- if err := e.UpdateSettings(context.Background(), port.EngineSettingsUpdate{Raw: cfg}); err != nil {
14+ if err := e.UpdateSettings(context.Background(), port.EngineSettingsUpdate{
15+ Settings: port.EngineSettingsPayload{DefaultUIScale: 1.25},
16+ }); err != nil {
1617 t.Fatalf("UpdateSettings returned error: %v", err)
1718 }
18- if e.applicationScale != 1.25 {
19- t.Fatalf("applicationScale=%v, want 1.25", e.applicationScale)
19+ if got := e.currentApplicationScale(); got != 1.25 {
20+ t.Fatalf("applicationScale=%v, want 1.25", got)
2021 }
2122 }
2223
23-func TestEngineUpdateSettingsRejectsUnexpectedConfig(t *testing.T) {
24- e := &Engine{}
25- if err := e.UpdateSettings(context.Background(), port.EngineSettingsUpdate{Raw: "wrong"}); err == nil {
26- t.Fatal("expected error for unexpected config type")
24+func TestEngineUpdateSettingsIgnoresLegacyRawForApplicationScale(t *testing.T) {
25+ e := &Engine{applicationScale: 1}
26+
27+ if err := e.UpdateSettings(context.Background(), port.EngineSettingsUpdate{Raw: "wrong"}); err != nil {
28+ t.Fatalf("UpdateSettings returned error: %v", err)
29+ }
30+ if got := e.currentApplicationScale(); got != 1 {
31+ t.Fatalf("applicationScale=%v, want default 1", got)
32+ }
33+}
34+
35+func TestEngineApplicationScaleConcurrentAccess(_ *testing.T) {
36+ e := &Engine{applicationScale: 1}
37+ var wg sync.WaitGroup
38+ for i := 0; i < 16; i++ {
39+ wg.Add(2)
40+ go func() {
41+ defer wg.Done()
42+ _ = e.currentApplicationScale()
43+ }()
44+ go func(scale float64) {
45+ defer wg.Done()
46+ _ = e.UpdateSettings(context.Background(), port.EngineSettingsUpdate{
47+ Settings: port.EngineSettingsPayload{DefaultUIScale: scale},
48+ })
49+ }(1 + float64(i)/10)
2750 }
51+ wg.Wait()
2852 }
@@ -2,27 +2,51 @@ package cef
2 2
3 import (3 import (
4 "context"4 "context"
5+ "sync"
5 "testing"6 "testing"
6 7
7 "github.com/bnema/dumber/internal/application/port"8 "github.com/bnema/dumber/internal/application/port"
8- "github.com/bnema/dumber/internal/infrastructure/config"
9 )9 )
10 10
11-func TestEngineUpdateSettingsUpdatesApplicationScaleForNewViews(t *testing.T) {11+func TestEngineUpdateSettingsUpdatesApplicationScaleFromBoundaryPayload(t *testing.T) {
12 e := &Engine{applicationScale: 1}12 e := &Engine{applicationScale: 1}
13- cfg := &config.Config{DefaultUIScale: 1.25}
14 13
15- if err := e.UpdateSettings(context.Background(), port.EngineSettingsUpdate{Raw: cfg}); err != nil {14+ if err := e.UpdateSettings(context.Background(), port.EngineSettingsUpdate{
15+ Settings: port.EngineSettingsPayload{DefaultUIScale: 1.25},
16+ }); err != nil {
16 t.Fatalf("UpdateSettings returned error: %v", err)17 t.Fatalf("UpdateSettings returned error: %v", err)
17 }18 }
18- if e.applicationScale != 1.25 {19+ if got := e.currentApplicationScale(); got != 1.25 {
19- t.Fatalf("applicationScale=%v, want 1.25", e.applicationScale)20+ t.Fatalf("applicationScale=%v, want 1.25", got)
20 }21 }
21 }22 }
22 23
23-func TestEngineUpdateSettingsRejectsUnexpectedConfig(t *testing.T) {24+func TestEngineUpdateSettingsIgnoresLegacyRawForApplicationScale(t *testing.T) {
24- e := &Engine{}25+ e := &Engine{applicationScale: 1}
25- if err := e.UpdateSettings(context.Background(), port.EngineSettingsUpdate{Raw: "wrong"}); err == nil {26+
26- t.Fatal("expected error for unexpected config type")27+ if err := e.UpdateSettings(context.Background(), port.EngineSettingsUpdate{Raw: "wrong"}); err != nil {
28+ t.Fatalf("UpdateSettings returned error: %v", err)
29+ }
30+ if got := e.currentApplicationScale(); got != 1 {
31+ t.Fatalf("applicationScale=%v, want default 1", got)
32+ }
33+}
34+
35+func TestEngineApplicationScaleConcurrentAccess(_ *testing.T) {
36+ e := &Engine{applicationScale: 1}
37+ var wg sync.WaitGroup
38+ for i := 0; i < 16; i++ {
39+ wg.Add(2)
40+ go func() {
41+ defer wg.Done()
42+ _ = e.currentApplicationScale()
43+ }()
44+ go func(scale float64) {
45+ defer wg.Done()
46+ _ = e.UpdateSettings(context.Background(), port.EngineSettingsUpdate{
47+ Settings: port.EngineSettingsPayload{DefaultUIScale: scale},
48+ })
49+ }(1 + float64(i)/10)
27 }50 }
51+ wg.Wait()
28 }52 }
M internal/infrastructure/cef/factory.go +3 -2
@@ -106,7 +106,8 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) {
106106
107107 func (f *WebViewFactory) newWebView(ctx context.Context) (*WebView, error) {
108108 id := port.WebViewID(f.nextID.Add(1))
109- viewBridge := NewCef2gtkAdapter(f.engine.renderStackPlan, f.engine.applicationScale)
109+ applicationScale := f.engine.currentApplicationScale()
110+ viewBridge := NewCef2gtkAdapter(f.engine.renderStackPlan, applicationScale)
110111 if viewBridge == nil {
111112 return nil, fmt.Errorf("create cef2gtk view")
112113 }
@@ -125,7 +126,7 @@ func (f *WebViewFactory) newWebView(ctx context.Context) (*WebView, error) {
125126 }
126127 wv.runOnGTKSync(func() {
127128 nativeWidget := viewBridge.Widget()
128- popupSurface := newPopupBridgeSurface(ctx, nativeWidget, f.engine.renderStackPlan, f.engine.applicationScale)
129+ popupSurface := newPopupBridgeSurface(ctx, nativeWidget, f.engine.renderStackPlan, applicationScale)
129130 wv.nativeWidget = nativeWidget
130131 if popupSurface != nil {
131132 wv.popupSurface = popupSurface
@@ -106,7 +106,8 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) {
106 106
107 func (f *WebViewFactory) newWebView(ctx context.Context) (*WebView, error) {107 func (f *WebViewFactory) newWebView(ctx context.Context) (*WebView, error) {
108 id := port.WebViewID(f.nextID.Add(1))108 id := port.WebViewID(f.nextID.Add(1))
109- viewBridge := NewCef2gtkAdapter(f.engine.renderStackPlan, f.engine.applicationScale)109+ applicationScale := f.engine.currentApplicationScale()
110+ viewBridge := NewCef2gtkAdapter(f.engine.renderStackPlan, applicationScale)
110 if viewBridge == nil {111 if viewBridge == nil {
111 return nil, fmt.Errorf("create cef2gtk view")112 return nil, fmt.Errorf("create cef2gtk view")
112 }113 }
@@ -125,7 +126,7 @@ func (f *WebViewFactory) newWebView(ctx context.Context) (*WebView, error) {
125 }126 }
126 wv.runOnGTKSync(func() {127 wv.runOnGTKSync(func() {
127 nativeWidget := viewBridge.Widget()128 nativeWidget := viewBridge.Widget()
128- popupSurface := newPopupBridgeSurface(ctx, nativeWidget, f.engine.renderStackPlan, f.engine.applicationScale)129+ popupSurface := newPopupBridgeSurface(ctx, nativeWidget, f.engine.renderStackPlan, applicationScale)
129 wv.nativeWidget = nativeWidget130 wv.nativeWidget = nativeWidget
130 if popupSurface != nil {131 if popupSurface != nil {
131 wv.popupSurface = popupSurface132 wv.popupSurface = popupSurface
M internal/infrastructure/cef/popup_surface.go +6 -1
@@ -23,7 +23,12 @@ type popupBridgeSurface struct {
2323
2424 // newPopupBridgeSurface must be called on the GTK thread because it creates
2525 // and packs GTK widgets around the primary CEF widget.
26-func newPopupBridgeSurface(ctx context.Context, mainWidget *gtk.Widget, plan cef2gtk.RenderStackPlan, applicationScale float64) *popupBridgeSurface {
26+func newPopupBridgeSurface(
27+ ctx context.Context,
28+ mainWidget *gtk.Widget,
29+ plan cef2gtk.RenderStackPlan,
30+ applicationScale float64,
31+) *popupBridgeSurface {
2732 mainContext := glib.MainContextDefault()
2833 if mainContext == nil || !mainContext.IsOwner() {
2934 logging.FromContext(popupSurfaceLogContext(ctx)).Error().Msg("cef: newPopupBridgeSurface called off GTK thread")
@@ -23,7 +23,12 @@ type popupBridgeSurface struct {
23 23
24 // newPopupBridgeSurface must be called on the GTK thread because it creates24 // newPopupBridgeSurface must be called on the GTK thread because it creates
25 // and packs GTK widgets around the primary CEF widget.25 // and packs GTK widgets around the primary CEF widget.
26-func newPopupBridgeSurface(ctx context.Context, mainWidget *gtk.Widget, plan cef2gtk.RenderStackPlan, applicationScale float64) *popupBridgeSurface {26+func newPopupBridgeSurface(
27+ ctx context.Context,
28+ mainWidget *gtk.Widget,
29+ plan cef2gtk.RenderStackPlan,
30+ applicationScale float64,
31+) *popupBridgeSurface {
27 mainContext := glib.MainContextDefault()32 mainContext := glib.MainContextDefault()
28 if mainContext == nil || !mainContext.IsOwner() {33 if mainContext == nil || !mainContext.IsOwner() {
29 logging.FromContext(popupSurfaceLogContext(ctx)).Error().Msg("cef: newPopupBridgeSurface called off GTK thread")34 logging.FromContext(popupSurfaceLogContext(ctx)).Error().Msg("cef: newPopupBridgeSurface called off GTK thread")
M internal/infrastructure/cef/scale_probe.go +3 -1
@@ -5,6 +5,8 @@ import (
55 "strings"
66 )
77
8+const scaleProbeRoundFactor = 1_000_000
9+
810 type cefScaleProbeMetrics struct {
911 SurfaceWidth int32
1012 SurfaceHeight int32
@@ -62,7 +64,7 @@ func roundedScale(v float64) float64 {
6264 if math.IsNaN(v) || math.IsInf(v, 0) {
6365 return 1
6466 }
65- return math.Round(v*1_000_000) / 1_000_000
67+ return math.Round(v*scaleProbeRoundFactor) / scaleProbeRoundFactor
6668 }
6769
6870 const cefScaleProbeScript = `(function(){
@@ -5,6 +5,8 @@ import (
5 "strings"5 "strings"
6 )6 )
7 7
8+const scaleProbeRoundFactor = 1_000_000
9+
8 type cefScaleProbeMetrics struct {10 type cefScaleProbeMetrics struct {
9 SurfaceWidth int3211 SurfaceWidth int32
10 SurfaceHeight int3212 SurfaceHeight int32
@@ -62,7 +64,7 @@ func roundedScale(v float64) float64 {
62 if math.IsNaN(v) || math.IsInf(v, 0) {64 if math.IsNaN(v) || math.IsInf(v, 0) {
63 return 165 return 1
64 }66 }
65- return math.Round(v*1_000_000) / 1_000_00067+ return math.Round(v*scaleProbeRoundFactor) / scaleProbeRoundFactor
66 }68 }
67 69
68 const cefScaleProbeScript = `(function(){70 const cefScaleProbeScript = `(function(){
M internal/ui/app.go +4 -1
@@ -4827,7 +4827,10 @@ func (a *App) applyAppearanceConfig(ctx context.Context) {
48274827 cfg := a.deps.Config
48284828
48294829 if a.engine != nil {
4830- if err := a.engine.UpdateSettings(ctx, port.EngineSettingsUpdate{Raw: cfg}); err != nil {
4830+ if err := a.engine.UpdateSettings(ctx, port.EngineSettingsUpdate{
4831+ Settings: port.EngineSettingsPayload{DefaultUIScale: cfg.DefaultUIScale},
4832+ Raw: cfg,
4833+ }); err != nil {
48314834 log.Warn().Err(err).Msg("failed to apply engine settings update")
48324835 }
48334836 }
@@ -4827,7 +4827,10 @@ func (a *App) applyAppearanceConfig(ctx context.Context) {
4827 cfg := a.deps.Config4827 cfg := a.deps.Config
4828 4828
4829 if a.engine != nil {4829 if a.engine != nil {
4830- if err := a.engine.UpdateSettings(ctx, port.EngineSettingsUpdate{Raw: cfg}); err != nil {4830+ if err := a.engine.UpdateSettings(ctx, port.EngineSettingsUpdate{
4831+ Settings: port.EngineSettingsPayload{DefaultUIScale: cfg.DefaultUIScale},
4832+ Raw: cfg,
4833+ }); err != nil {
4831 log.Warn().Err(err).Msg("failed to apply engine settings update")4834 log.Warn().Err(err).Msg("failed to apply engine settings update")
4832 }4835 }
4833 }4836 }

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