detached feb43a6

refactor(cef): inject engine dependencies from bootstrap

commit
feb43a6499399b3bcc8587b4caca1a4418615bc5
author
brice · · 2026-04-11 12:58 UTC
parent
b6125b8
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +101 -7
M internal/bootstrap/engine.go +31 -1
@@ -2,12 +2,16 @@ package bootstrap
22
33 import (
44 "context"
5+ "encoding/json"
56 "fmt"
67
78 "github.com/bnema/dumber/internal/application/port"
89 audiofactory "github.com/bnema/dumber/internal/infrastructure/audio/factory"
910 "github.com/bnema/dumber/internal/infrastructure/cef"
1011 "github.com/bnema/dumber/internal/infrastructure/config"
12+ "github.com/bnema/dumber/internal/infrastructure/env"
13+ "github.com/bnema/dumber/internal/infrastructure/handlers"
14+ "github.com/bnema/dumber/internal/infrastructure/transcoder"
1115 "github.com/bnema/dumber/internal/infrastructure/webkit"
1216 "github.com/bnema/dumber/internal/ui/theme"
1317 "github.com/rs/zerolog"
@@ -62,8 +66,34 @@ func BuildEngine(input EngineInput) (port.Engine, error) {
6266 MaxConcurrent: cfg.Transcoding.MaxConcurrent,
6367 Quality: cfg.Transcoding.Quality,
6468 }
69+ surveyor := env.NewHardwareSurveyor()
70+ buildConfigPayload := func(cfgf func() *config.Config) func() ([]byte, error) {
71+ return func() ([]byte, error) {
72+ cfg := cfgf()
73+ var hw *port.HardwareInfo
74+ if surveyor != nil {
75+ survey := surveyor.Survey(context.Background())
76+ hw = &survey
77+ }
78+ return json.Marshal(config.BuildWebUIConfigPayload(cfg, hw))
79+ }
80+ }
81+ deps := cef.EngineDependencies{
82+ RegisterHandlers: handlers.RegisterAll,
83+ RegisterAccentHandlers: handlers.RegisterAccentHandlers,
84+ CurrentConfigPayload: buildConfigPayload(config.Get),
85+ DefaultConfigPayload: buildConfigPayload(config.DefaultConfig),
86+ MediaClassifier: cef.MediaClassifier{
87+ IsProprietaryVideoMIME: transcoder.IsProprietaryVideoMIME,
88+ IsOpenVideoMIME: transcoder.IsOpenVideoMIME,
89+ IsStreamingManifestMIME: transcoder.IsStreamingManifestMIME,
90+ IsStreamingManifestURL: transcoder.IsStreamingManifestURL,
91+ IsEagerTranscodeURL: transcoder.IsEagerTranscodeURL,
92+ ParseSyntheticTranscodeURL: transcoder.ParseSyntheticTranscodeURL,
93+ },
94+ }
6595 audioFactory := audiofactory.NewAudioOutputFactory()
66- return cef.NewEngine(input.Ctx, opts, cefCfg, transcodingCfg, audioFactory, cef.EngineDependencies{})
96+ return cef.NewEngine(input.Ctx, opts, cefCfg, transcodingCfg, audioFactory, deps)
6797 default:
6898 return nil, fmt.Errorf("unknown engine type: %q", engineType)
6999 }
@@ -2,12 +2,16 @@ package bootstrap
2 2
3 import (3 import (
4 "context"4 "context"
5+ "encoding/json"
5 "fmt"6 "fmt"
6 7
7 "github.com/bnema/dumber/internal/application/port"8 "github.com/bnema/dumber/internal/application/port"
8 audiofactory "github.com/bnema/dumber/internal/infrastructure/audio/factory"9 audiofactory "github.com/bnema/dumber/internal/infrastructure/audio/factory"
9 "github.com/bnema/dumber/internal/infrastructure/cef"10 "github.com/bnema/dumber/internal/infrastructure/cef"
10 "github.com/bnema/dumber/internal/infrastructure/config"11 "github.com/bnema/dumber/internal/infrastructure/config"
12+ "github.com/bnema/dumber/internal/infrastructure/env"
13+ "github.com/bnema/dumber/internal/infrastructure/handlers"
14+ "github.com/bnema/dumber/internal/infrastructure/transcoder"
11 "github.com/bnema/dumber/internal/infrastructure/webkit"15 "github.com/bnema/dumber/internal/infrastructure/webkit"
12 "github.com/bnema/dumber/internal/ui/theme"16 "github.com/bnema/dumber/internal/ui/theme"
13 "github.com/rs/zerolog"17 "github.com/rs/zerolog"
@@ -62,8 +66,34 @@ func BuildEngine(input EngineInput) (port.Engine, error) {
62 MaxConcurrent: cfg.Transcoding.MaxConcurrent,66 MaxConcurrent: cfg.Transcoding.MaxConcurrent,
63 Quality: cfg.Transcoding.Quality,67 Quality: cfg.Transcoding.Quality,
64 }68 }
69+ surveyor := env.NewHardwareSurveyor()
70+ buildConfigPayload := func(cfgf func() *config.Config) func() ([]byte, error) {
71+ return func() ([]byte, error) {
72+ cfg := cfgf()
73+ var hw *port.HardwareInfo
74+ if surveyor != nil {
75+ survey := surveyor.Survey(context.Background())
76+ hw = &survey
77+ }
78+ return json.Marshal(config.BuildWebUIConfigPayload(cfg, hw))
79+ }
80+ }
81+ deps := cef.EngineDependencies{
82+ RegisterHandlers: handlers.RegisterAll,
83+ RegisterAccentHandlers: handlers.RegisterAccentHandlers,
84+ CurrentConfigPayload: buildConfigPayload(config.Get),
85+ DefaultConfigPayload: buildConfigPayload(config.DefaultConfig),
86+ MediaClassifier: cef.MediaClassifier{
87+ IsProprietaryVideoMIME: transcoder.IsProprietaryVideoMIME,
88+ IsOpenVideoMIME: transcoder.IsOpenVideoMIME,
89+ IsStreamingManifestMIME: transcoder.IsStreamingManifestMIME,
90+ IsStreamingManifestURL: transcoder.IsStreamingManifestURL,
91+ IsEagerTranscodeURL: transcoder.IsEagerTranscodeURL,
92+ ParseSyntheticTranscodeURL: transcoder.ParseSyntheticTranscodeURL,
93+ },
94+ }
65 audioFactory := audiofactory.NewAudioOutputFactory()95 audioFactory := audiofactory.NewAudioOutputFactory()
66- return cef.NewEngine(input.Ctx, opts, cefCfg, transcodingCfg, audioFactory, cef.EngineDependencies{})96+ return cef.NewEngine(input.Ctx, opts, cefCfg, transcodingCfg, audioFactory, deps)
67 default:97 default:
68 return nil, fmt.Errorf("unknown engine type: %q", engineType)98 return nil, fmt.Errorf("unknown engine type: %q", engineType)
69 }99 }
M internal/infrastructure/cef/engine.go +10 -5
@@ -9,7 +9,6 @@ import (
99 purecef "github.com/bnema/purego-cef/cef"
1010
1111 "github.com/bnema/dumber/internal/application/port"
12- "github.com/bnema/dumber/internal/infrastructure/handlers"
1312 "github.com/bnema/dumber/internal/logging"
1413 )
1514
@@ -28,6 +27,12 @@ type Engine struct {
2827 schemeHandler *dumbSchemeHandler
2928 contentInj *contentInjector
3029
30+ registerHandlers HandlerRegistrar
31+ registerAccentHandlers AccentHandlerRegistrar
32+ currentConfigPayload func() ([]byte, error)
33+ defaultConfigPayload func() ([]byte, error)
34+ mediaClassifier MediaClassifier
35+
3136 // activeWebViews tracks all live webviews for CSS broadcast.
3237 activeWebViews sync.Map // map[port.WebViewID]*WebView
3338 activeCount atomic.Int32
@@ -210,17 +215,17 @@ func (e *Engine) closeActiveWebViews() {
210215 // each handler is registered by message type with the router, which dispatches
211216 // incoming /api/message POSTs to the correct handler based on Message.Type.
212217 func (e *Engine) RegisterHandlers(ctx context.Context, deps port.HandlerDependencies) error {
213- if e.messageRouter == nil {
218+ if e.messageRouter == nil || e.registerHandlers == nil {
214219 return nil
215220 }
216- return handlers.RegisterAll(ctx, e.messageRouter, deps)
221+ return e.registerHandlers(ctx, e.messageRouter, deps)
217222 }
218223
219224 func (e *Engine) RegisterAccentHandlers(ctx context.Context, handler port.AccentKeyHandler) error {
220- if e.messageRouter == nil || handler == nil {
225+ if e.messageRouter == nil || handler == nil || e.registerAccentHandlers == nil {
221226 return nil
222227 }
223- return handlers.RegisterAccentHandlers(ctx, e.messageRouter, handler)
228+ return e.registerAccentHandlers(ctx, e.messageRouter, handler)
224229 }
225230
226231 // ConfigureDownloads sets up download handling (Phase 1 stub).
@@ -9,7 +9,6 @@ import (
9 purecef "github.com/bnema/purego-cef/cef"9 purecef "github.com/bnema/purego-cef/cef"
10 10
11 "github.com/bnema/dumber/internal/application/port"11 "github.com/bnema/dumber/internal/application/port"
12- "github.com/bnema/dumber/internal/infrastructure/handlers"
13 "github.com/bnema/dumber/internal/logging"12 "github.com/bnema/dumber/internal/logging"
14 )13 )
15 14
@@ -28,6 +27,12 @@ type Engine struct {
28 schemeHandler *dumbSchemeHandler27 schemeHandler *dumbSchemeHandler
29 contentInj *contentInjector28 contentInj *contentInjector
30 29
30+ registerHandlers HandlerRegistrar
31+ registerAccentHandlers AccentHandlerRegistrar
32+ currentConfigPayload func() ([]byte, error)
33+ defaultConfigPayload func() ([]byte, error)
34+ mediaClassifier MediaClassifier
35+
31 // activeWebViews tracks all live webviews for CSS broadcast.36 // activeWebViews tracks all live webviews for CSS broadcast.
32 activeWebViews sync.Map // map[port.WebViewID]*WebView37 activeWebViews sync.Map // map[port.WebViewID]*WebView
33 activeCount atomic.Int3238 activeCount atomic.Int32
@@ -210,17 +215,17 @@ func (e *Engine) closeActiveWebViews() {
210 // each handler is registered by message type with the router, which dispatches215 // each handler is registered by message type with the router, which dispatches
211 // incoming /api/message POSTs to the correct handler based on Message.Type.216 // incoming /api/message POSTs to the correct handler based on Message.Type.
212 func (e *Engine) RegisterHandlers(ctx context.Context, deps port.HandlerDependencies) error {217 func (e *Engine) RegisterHandlers(ctx context.Context, deps port.HandlerDependencies) error {
213- if e.messageRouter == nil {218+ if e.messageRouter == nil || e.registerHandlers == nil {
214 return nil219 return nil
215 }220 }
216- return handlers.RegisterAll(ctx, e.messageRouter, deps)221+ return e.registerHandlers(ctx, e.messageRouter, deps)
217 }222 }
218 223
219 func (e *Engine) RegisterAccentHandlers(ctx context.Context, handler port.AccentKeyHandler) error {224 func (e *Engine) RegisterAccentHandlers(ctx context.Context, handler port.AccentKeyHandler) error {
220- if e.messageRouter == nil || handler == nil {225+ if e.messageRouter == nil || handler == nil || e.registerAccentHandlers == nil {
221 return nil226 return nil
222 }227 }
223- return handlers.RegisterAccentHandlers(ctx, e.messageRouter, handler)228+ return e.registerAccentHandlers(ctx, e.messageRouter, handler)
224 }229 }
225 230
226 // ConfigureDownloads sets up download handling (Phase 1 stub).231 // ConfigureDownloads sets up download handling (Phase 1 stub).
M internal/infrastructure/cef/engine_init.go +7 -1
@@ -33,6 +33,7 @@ func NewEngine(
3333 deps EngineDependencies,
3434 ) (*Engine, error) {
3535 logger := logging.FromContext(ctx)
36+ deps.MediaClassifier = deps.MediaClassifier.normalize()
3637 stateRoot := resolvedStateRoot(opts)
3738 cleanStaleSingletonLocks(logger, stateRoot)
3839 windowlessFrameRate := config.CEFEngineConfig{WindowlessFrameRate: cfg.WindowlessFrameRate}.CEFWindowlessFrameRate()
@@ -48,7 +49,12 @@ func NewEngine(
4849 os.Args = appendIfMissing(os.Args, "--no-zygote")
4950
5051 eng := &Engine{
51- ctx: ctx,
52+ ctx: ctx,
53+ registerHandlers: deps.RegisterHandlers,
54+ registerAccentHandlers: deps.RegisterAccentHandlers,
55+ currentConfigPayload: deps.CurrentConfigPayload,
56+ defaultConfigPayload: deps.DefaultConfigPayload,
57+ mediaClassifier: deps.MediaClassifier,
5258 }
5359
5460 logger.Info().
@@ -33,6 +33,7 @@ func NewEngine(
33 deps EngineDependencies,33 deps EngineDependencies,
34 ) (*Engine, error) {34 ) (*Engine, error) {
35 logger := logging.FromContext(ctx)35 logger := logging.FromContext(ctx)
36+ deps.MediaClassifier = deps.MediaClassifier.normalize()
36 stateRoot := resolvedStateRoot(opts)37 stateRoot := resolvedStateRoot(opts)
37 cleanStaleSingletonLocks(logger, stateRoot)38 cleanStaleSingletonLocks(logger, stateRoot)
38 windowlessFrameRate := config.CEFEngineConfig{WindowlessFrameRate: cfg.WindowlessFrameRate}.CEFWindowlessFrameRate()39 windowlessFrameRate := config.CEFEngineConfig{WindowlessFrameRate: cfg.WindowlessFrameRate}.CEFWindowlessFrameRate()
@@ -48,7 +49,12 @@ func NewEngine(
48 os.Args = appendIfMissing(os.Args, "--no-zygote")49 os.Args = appendIfMissing(os.Args, "--no-zygote")
49 50
50 eng := &Engine{51 eng := &Engine{
51- ctx: ctx,52+ ctx: ctx,
53+ registerHandlers: deps.RegisterHandlers,
54+ registerAccentHandlers: deps.RegisterAccentHandlers,
55+ currentConfigPayload: deps.CurrentConfigPayload,
56+ defaultConfigPayload: deps.DefaultConfigPayload,
57+ mediaClassifier: deps.MediaClassifier,
52 }58 }
53 59
54 logger.Info().60 logger.Info().
A internal/infrastructure/cef/engine_runtime_test.go +53 -0
@@ -0,0 +1,53 @@
1+package cef
2+
3+import (
4+ "context"
5+ "errors"
6+ "testing"
7+
8+ "github.com/bnema/dumber/internal/application/port"
9+ "github.com/stretchr/testify/require"
10+)
11+
12+type stubAccentHandler struct{}
13+
14+func (stubAccentHandler) OnKeyPressed(context.Context, rune, bool) bool { return true }
15+func (stubAccentHandler) OnKeyReleased(context.Context, rune) {}
16+
17+func TestEngineRegisterHandlers_UsesInjectedRegistrar(t *testing.T) {
18+ called := false
19+ eng := &Engine{
20+ messageRouter: NewMessageRouter(context.Background()),
21+ registerHandlers: func(context.Context, port.WebUIHandlerRouter, port.HandlerDependencies) error {
22+ called = true
23+ return nil
24+ },
25+ }
26+ require.NoError(t, eng.RegisterHandlers(context.Background(), port.HandlerDependencies{}))
27+ require.True(t, called)
28+}
29+
30+func TestEngineRegisterAccentHandlers_UsesInjectedRegistrar(t *testing.T) {
31+ called := false
32+ eng := &Engine{
33+ messageRouter: NewMessageRouter(context.Background()),
34+ registerAccentHandlers: func(context.Context, port.WebUIHandlerRouter, port.AccentKeyHandler) error {
35+ called = true
36+ return nil
37+ },
38+ }
39+ require.NoError(t, eng.RegisterAccentHandlers(context.Background(), stubAccentHandler{}))
40+ require.True(t, called)
41+}
42+
43+func TestEngineRegisterHandlers_PropagatesRegistrarError(t *testing.T) {
44+ want := errors.New("boom")
45+ eng := &Engine{
46+ messageRouter: NewMessageRouter(context.Background()),
47+ registerHandlers: func(context.Context, port.WebUIHandlerRouter, port.HandlerDependencies) error {
48+ return want
49+ },
50+ }
51+ err := eng.RegisterHandlers(context.Background(), port.HandlerDependencies{})
52+ require.ErrorIs(t, err, want)
53+}
@@ -0,0 +1,53 @@
1+package cef
2+
3+import (
4+ "context"
5+ "errors"
6+ "testing"
7+
8+ "github.com/bnema/dumber/internal/application/port"
9+ "github.com/stretchr/testify/require"
10+)
11+
12+type stubAccentHandler struct{}
13+
14+func (stubAccentHandler) OnKeyPressed(context.Context, rune, bool) bool { return true }
15+func (stubAccentHandler) OnKeyReleased(context.Context, rune) {}
16+
17+func TestEngineRegisterHandlers_UsesInjectedRegistrar(t *testing.T) {
18+ called := false
19+ eng := &Engine{
20+ messageRouter: NewMessageRouter(context.Background()),
21+ registerHandlers: func(context.Context, port.WebUIHandlerRouter, port.HandlerDependencies) error {
22+ called = true
23+ return nil
24+ },
25+ }
26+ require.NoError(t, eng.RegisterHandlers(context.Background(), port.HandlerDependencies{}))
27+ require.True(t, called)
28+}
29+
30+func TestEngineRegisterAccentHandlers_UsesInjectedRegistrar(t *testing.T) {
31+ called := false
32+ eng := &Engine{
33+ messageRouter: NewMessageRouter(context.Background()),
34+ registerAccentHandlers: func(context.Context, port.WebUIHandlerRouter, port.AccentKeyHandler) error {
35+ called = true
36+ return nil
37+ },
38+ }
39+ require.NoError(t, eng.RegisterAccentHandlers(context.Background(), stubAccentHandler{}))
40+ require.True(t, called)
41+}
42+
43+func TestEngineRegisterHandlers_PropagatesRegistrarError(t *testing.T) {
44+ want := errors.New("boom")
45+ eng := &Engine{
46+ messageRouter: NewMessageRouter(context.Background()),
47+ registerHandlers: func(context.Context, port.WebUIHandlerRouter, port.HandlerDependencies) error {
48+ return want
49+ },
50+ }
51+ err := eng.RegisterHandlers(context.Background(), port.HandlerDependencies{})
52+ require.ErrorIs(t, err, want)
53+}

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