detached d24a1f0

refactor(cef): inject live config payload builders

commit
d24a1f0c652592b5478a0e4326dd05793872b2c7
author
brice · · 2026-04-11 13:06 UTC
parent
feb43a6
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +74 -29
M internal/infrastructure/cef/engine_init.go +9 -2
@@ -71,7 +71,7 @@ func NewEngine(
7171 }
7272 os.Args = savedArgs
7373
74- return wireEngine(ctx, eng, cfg, transcodingCfg, windowlessFrameRate, audioFactory, logger)
74+ return wireEngine(ctx, eng, cfg, transcodingCfg, windowlessFrameRate, audioFactory, logger, deps.CurrentConfigPayload, deps.DefaultConfigPayload)
7575 }
7676
7777 func resolvedStateRoot(opts port.EngineOptions) string {
@@ -153,6 +153,7 @@ func initializeCEF(eng *Engine, settings purecef.Settings, logger *zerolog.Logge
153153 func wireEngine(
154154 ctx context.Context, eng *Engine, cfg RuntimeConfig, transcodingCfg TranscodingRuntimeConfig,
155155 windowlessFrameRate int32, audioFactory port.AudioOutputFactory, logger *zerolog.Logger,
156+ currentConfigPayload func() ([]byte, error), defaultConfigPayload func() ([]byte, error),
156157 ) (*Engine, error) {
157158 gl, err := newGLLoader()
158159 if err != nil {
@@ -206,7 +207,13 @@ func wireEngine(
206207 eng.transcoderState = transcoderState
207208
208209 messageRouter := NewMessageRouter(ctx)
209- schemeHandler := newDumbSchemeHandler(ctx, messageRouter, mediaTranscoder)
210+ schemeHandler := newDumbSchemeHandler(
211+ ctx,
212+ messageRouter,
213+ mediaTranscoder,
214+ currentConfigPayload,
215+ defaultConfigPayload,
216+ )
210217 schemeHandler.setAssets(assets.WebUIAssets)
211218
212219 // Bridge clipboard writes from CEF JS → GDK system clipboard.
@@ -71,7 +71,7 @@ func NewEngine(
71 }71 }
72 os.Args = savedArgs72 os.Args = savedArgs
73 73
74- return wireEngine(ctx, eng, cfg, transcodingCfg, windowlessFrameRate, audioFactory, logger)74+ return wireEngine(ctx, eng, cfg, transcodingCfg, windowlessFrameRate, audioFactory, logger, deps.CurrentConfigPayload, deps.DefaultConfigPayload)
75 }75 }
76 76
77 func resolvedStateRoot(opts port.EngineOptions) string {77 func resolvedStateRoot(opts port.EngineOptions) string {
@@ -153,6 +153,7 @@ func initializeCEF(eng *Engine, settings purecef.Settings, logger *zerolog.Logge
153 func wireEngine(153 func wireEngine(
154 ctx context.Context, eng *Engine, cfg RuntimeConfig, transcodingCfg TranscodingRuntimeConfig,154 ctx context.Context, eng *Engine, cfg RuntimeConfig, transcodingCfg TranscodingRuntimeConfig,
155 windowlessFrameRate int32, audioFactory port.AudioOutputFactory, logger *zerolog.Logger,155 windowlessFrameRate int32, audioFactory port.AudioOutputFactory, logger *zerolog.Logger,
156+ currentConfigPayload func() ([]byte, error), defaultConfigPayload func() ([]byte, error),
156 ) (*Engine, error) {157 ) (*Engine, error) {
157 gl, err := newGLLoader()158 gl, err := newGLLoader()
158 if err != nil {159 if err != nil {
@@ -206,7 +207,13 @@ func wireEngine(
206 eng.transcoderState = transcoderState207 eng.transcoderState = transcoderState
207 208
208 messageRouter := NewMessageRouter(ctx)209 messageRouter := NewMessageRouter(ctx)
209- schemeHandler := newDumbSchemeHandler(ctx, messageRouter, mediaTranscoder)210+ schemeHandler := newDumbSchemeHandler(
211+ ctx,
212+ messageRouter,
213+ mediaTranscoder,
214+ currentConfigPayload,
215+ defaultConfigPayload,
216+ )
210 schemeHandler.setAssets(assets.WebUIAssets)217 schemeHandler.setAssets(assets.WebUIAssets)
211 218
212 // Bridge clipboard writes from CEF JS → GDK system clipboard.219 // Bridge clipboard writes from CEF JS → GDK system clipboard.
M internal/infrastructure/cef/scheme_handler.go +35 -27
@@ -19,8 +19,6 @@ import (
1919 purecef "github.com/bnema/purego-cef/cef"
2020
2121 "github.com/bnema/dumber/internal/application/port"
22- "github.com/bnema/dumber/internal/infrastructure/config"
23- "github.com/bnema/dumber/internal/infrastructure/env"
2422 "github.com/bnema/dumber/internal/infrastructure/webutil"
2523 "github.com/bnema/dumber/internal/logging"
2624 "github.com/rs/zerolog"
@@ -47,15 +45,16 @@ var pageRootFiles = map[string]string{
4745 // dumbSchemeHandler serves both the conceptual dumb:// URLs and the actual
4846 // internal https://dumber.invalid origin used by CEF.
4947 type dumbSchemeHandler struct {
50- ctx context.Context
51- messageRouter *MessageRouter
52- transcoder port.MediaTranscoder
53- assets embed.FS
54- assetsSet bool
55- assetDir string
56- logger zerolog.Logger
57- hwSurveyor *env.HardwareSurveyor
58- mu sync.RWMutex
48+ ctx context.Context
49+ messageRouter *MessageRouter
50+ transcoder port.MediaTranscoder
51+ assets embed.FS
52+ assetsSet bool
53+ assetDir string
54+ logger zerolog.Logger
55+ currentConfigPayload func() ([]byte, error)
56+ defaultConfigPayload func() ([]byte, error)
57+ mu sync.RWMutex
5958
6059 // onClipboardSet is called when JS sends copied text via /api/clipboard-set.
6160 // Set by the engine to write to the GDK system clipboard.
@@ -63,15 +62,22 @@ type dumbSchemeHandler struct {
6362 }
6463
6564 // newDumbSchemeHandler creates a handler for internal CEF pages.
66-func newDumbSchemeHandler(ctx context.Context, router *MessageRouter, transcoder port.MediaTranscoder) *dumbSchemeHandler {
65+func newDumbSchemeHandler(
66+ ctx context.Context,
67+ router *MessageRouter,
68+ transcoder port.MediaTranscoder,
69+ currentConfigPayload func() ([]byte, error),
70+ defaultConfigPayload func() ([]byte, error),
71+) *dumbSchemeHandler {
6772 log := logging.FromContext(ctx)
6873 return &dumbSchemeHandler{
69- ctx: ctx,
70- messageRouter: router,
71- transcoder: transcoder,
72- assetDir: "webui",
73- logger: log.With().Str("component", "scheme-handler").Logger(),
74- hwSurveyor: env.NewHardwareSurveyor(),
74+ ctx: ctx,
75+ messageRouter: router,
76+ transcoder: transcoder,
77+ assetDir: "webui",
78+ logger: log.With().Str("component", "scheme-handler").Logger(),
79+ currentConfigPayload: currentConfigPayload,
80+ defaultConfigPayload: defaultConfigPayload,
7581 }
7682 }
7783
@@ -141,10 +147,10 @@ func (h *dumbSchemeHandler) handleAPI(method, path string, request purecef.Reque
141147 return h.newRawResourceHandler(http.StatusOK, "application/json", resp)
142148
143149 case path == "/api/config" && (method == "" || strings.EqualFold(method, "GET")):
144- return h.handleConfigAPI(config.Get())
150+ return h.handleConfigAPI(h.currentConfigPayload)
145151
146152 case path == "/api/config/default" && (method == "" || strings.EqualFold(method, "GET")):
147- return h.handleConfigAPI(config.DefaultConfig())
153+ return h.handleConfigAPI(h.defaultConfigPayload)
148154
149155 case path == "/api/transcode" && (method == "" || strings.EqualFold(method, "GET")):
150156 return h.handleTranscodeAPI(request)
@@ -209,14 +215,16 @@ func (h *dumbSchemeHandler) handleTranscodeAPI(request purecef.Request) purecef.
209215 })
210216 }
211217
212-// handleConfigAPI returns a JSON response with the given config.
213-func (h *dumbSchemeHandler) handleConfigAPI(cfg *config.Config) purecef.ResourceHandler {
214- var hwInfo *port.HardwareInfo
215- if h.hwSurveyor != nil {
216- survey := h.hwSurveyor.Survey(context.Background())
217- hwInfo = &survey
218+func resolveConfigPayload(build func() ([]byte, error)) ([]byte, error) {
219+ if build == nil {
220+ return nil, fmt.Errorf("config payload builder not configured")
218221 }
219- data, err := json.Marshal(config.BuildWebUIConfigPayload(cfg, hwInfo))
222+ return build()
223+}
224+
225+// handleConfigAPI returns a JSON response with the given config payload builder.
226+func (h *dumbSchemeHandler) handleConfigAPI(build func() ([]byte, error)) purecef.ResourceHandler {
227+ data, err := resolveConfigPayload(build)
220228 if err != nil {
221229 return h.newJSONResourceHandler(http.StatusInternalServerError, map[string]string{"error": err.Error()})
222230 }
@@ -19,8 +19,6 @@ import (
19 purecef "github.com/bnema/purego-cef/cef"19 purecef "github.com/bnema/purego-cef/cef"
20 20
21 "github.com/bnema/dumber/internal/application/port"21 "github.com/bnema/dumber/internal/application/port"
22- "github.com/bnema/dumber/internal/infrastructure/config"
23- "github.com/bnema/dumber/internal/infrastructure/env"
24 "github.com/bnema/dumber/internal/infrastructure/webutil"22 "github.com/bnema/dumber/internal/infrastructure/webutil"
25 "github.com/bnema/dumber/internal/logging"23 "github.com/bnema/dumber/internal/logging"
26 "github.com/rs/zerolog"24 "github.com/rs/zerolog"
@@ -47,15 +45,16 @@ var pageRootFiles = map[string]string{
47 // dumbSchemeHandler serves both the conceptual dumb:// URLs and the actual45 // dumbSchemeHandler serves both the conceptual dumb:// URLs and the actual
48 // internal https://dumber.invalid origin used by CEF.46 // internal https://dumber.invalid origin used by CEF.
49 type dumbSchemeHandler struct {47 type dumbSchemeHandler struct {
50- ctx context.Context48+ ctx context.Context
51- messageRouter *MessageRouter49+ messageRouter *MessageRouter
52- transcoder port.MediaTranscoder50+ transcoder port.MediaTranscoder
53- assets embed.FS51+ assets embed.FS
54- assetsSet bool52+ assetsSet bool
55- assetDir string53+ assetDir string
56- logger zerolog.Logger54+ logger zerolog.Logger
57- hwSurveyor *env.HardwareSurveyor55+ currentConfigPayload func() ([]byte, error)
58- mu sync.RWMutex56+ defaultConfigPayload func() ([]byte, error)
57+ mu sync.RWMutex
59 58
60 // onClipboardSet is called when JS sends copied text via /api/clipboard-set.59 // onClipboardSet is called when JS sends copied text via /api/clipboard-set.
61 // Set by the engine to write to the GDK system clipboard.60 // Set by the engine to write to the GDK system clipboard.
@@ -63,15 +62,22 @@ type dumbSchemeHandler struct {
63 }62 }
64 63
65 // newDumbSchemeHandler creates a handler for internal CEF pages.64 // newDumbSchemeHandler creates a handler for internal CEF pages.
66-func newDumbSchemeHandler(ctx context.Context, router *MessageRouter, transcoder port.MediaTranscoder) *dumbSchemeHandler {65+func newDumbSchemeHandler(
66+ ctx context.Context,
67+ router *MessageRouter,
68+ transcoder port.MediaTranscoder,
69+ currentConfigPayload func() ([]byte, error),
70+ defaultConfigPayload func() ([]byte, error),
71+) *dumbSchemeHandler {
67 log := logging.FromContext(ctx)72 log := logging.FromContext(ctx)
68 return &dumbSchemeHandler{73 return &dumbSchemeHandler{
69- ctx: ctx,74+ ctx: ctx,
70- messageRouter: router,75+ messageRouter: router,
71- transcoder: transcoder,76+ transcoder: transcoder,
72- assetDir: "webui",77+ assetDir: "webui",
73- logger: log.With().Str("component", "scheme-handler").Logger(),78+ logger: log.With().Str("component", "scheme-handler").Logger(),
74- hwSurveyor: env.NewHardwareSurveyor(),79+ currentConfigPayload: currentConfigPayload,
80+ defaultConfigPayload: defaultConfigPayload,
75 }81 }
76 }82 }
77 83
@@ -141,10 +147,10 @@ func (h *dumbSchemeHandler) handleAPI(method, path string, request purecef.Reque
141 return h.newRawResourceHandler(http.StatusOK, "application/json", resp)147 return h.newRawResourceHandler(http.StatusOK, "application/json", resp)
142 148
143 case path == "/api/config" && (method == "" || strings.EqualFold(method, "GET")):149 case path == "/api/config" && (method == "" || strings.EqualFold(method, "GET")):
144- return h.handleConfigAPI(config.Get())150+ return h.handleConfigAPI(h.currentConfigPayload)
145 151
146 case path == "/api/config/default" && (method == "" || strings.EqualFold(method, "GET")):152 case path == "/api/config/default" && (method == "" || strings.EqualFold(method, "GET")):
147- return h.handleConfigAPI(config.DefaultConfig())153+ return h.handleConfigAPI(h.defaultConfigPayload)
148 154
149 case path == "/api/transcode" && (method == "" || strings.EqualFold(method, "GET")):155 case path == "/api/transcode" && (method == "" || strings.EqualFold(method, "GET")):
150 return h.handleTranscodeAPI(request)156 return h.handleTranscodeAPI(request)
@@ -209,14 +215,16 @@ func (h *dumbSchemeHandler) handleTranscodeAPI(request purecef.Request) purecef.
209 })215 })
210 }216 }
211 217
212-// handleConfigAPI returns a JSON response with the given config.218+func resolveConfigPayload(build func() ([]byte, error)) ([]byte, error) {
213-func (h *dumbSchemeHandler) handleConfigAPI(cfg *config.Config) purecef.ResourceHandler {219+ if build == nil {
214- var hwInfo *port.HardwareInfo220+ return nil, fmt.Errorf("config payload builder not configured")
215- if h.hwSurveyor != nil {
216- survey := h.hwSurveyor.Survey(context.Background())
217- hwInfo = &survey
218 }221 }
219- data, err := json.Marshal(config.BuildWebUIConfigPayload(cfg, hwInfo))222+ return build()
223+}
224+
225+// handleConfigAPI returns a JSON response with the given config payload builder.
226+func (h *dumbSchemeHandler) handleConfigAPI(build func() ([]byte, error)) purecef.ResourceHandler {
227+ data, err := resolveConfigPayload(build)
220 if err != nil {228 if err != nil {
221 return h.newJSONResourceHandler(http.StatusInternalServerError, map[string]string{"error": err.Error()})229 return h.newJSONResourceHandler(http.StatusInternalServerError, map[string]string{"error": err.Error()})
222 }230 }
A internal/infrastructure/cef/scheme_handler_test.go +30 -0
@@ -0,0 +1,30 @@
1+package cef
2+
3+import (
4+ "errors"
5+ "testing"
6+
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestResolveConfigPayload_UsesInjectedBuilder(t *testing.T) {
11+ data, err := resolveConfigPayload(func() ([]byte, error) {
12+ return []byte(`{"engine_type":"cef"}`), nil
13+ })
14+ require.NoError(t, err)
15+ require.JSONEq(t, `{"engine_type":"cef"}`, string(data))
16+}
17+
18+func TestResolveConfigPayload_PropagatesBuilderError(t *testing.T) {
19+ want := errors.New("boom")
20+ _, err := resolveConfigPayload(func() ([]byte, error) {
21+ return nil, want
22+ })
23+ require.ErrorIs(t, err, want)
24+}
25+
26+func TestResolveConfigPayload_NilBuilderFails(t *testing.T) {
27+ _, err := resolveConfigPayload(nil)
28+ require.Error(t, err)
29+ require.Contains(t, err.Error(), "config payload builder")
30+}
@@ -0,0 +1,30 @@
1+package cef
2+
3+import (
4+ "errors"
5+ "testing"
6+
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestResolveConfigPayload_UsesInjectedBuilder(t *testing.T) {
11+ data, err := resolveConfigPayload(func() ([]byte, error) {
12+ return []byte(`{"engine_type":"cef"}`), nil
13+ })
14+ require.NoError(t, err)
15+ require.JSONEq(t, `{"engine_type":"cef"}`, string(data))
16+}
17+
18+func TestResolveConfigPayload_PropagatesBuilderError(t *testing.T) {
19+ want := errors.New("boom")
20+ _, err := resolveConfigPayload(func() ([]byte, error) {
21+ return nil, want
22+ })
23+ require.ErrorIs(t, err, want)
24+}
25+
26+func TestResolveConfigPayload_NilBuilderFails(t *testing.T) {
27+ _, err := resolveConfigPayload(nil)
28+ require.Error(t, err)
29+ require.Contains(t, err.Error(), "config payload builder")
30+}

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