detached 86449d3

refactor(config): add read boundary for systemviews

commit
86449d3bf4c6096b3648a832b0383a68e197204c
author
brice · · 2026-04-11 17:57 UTC
committer
brice · · 2026-04-12 04:52 UTC
parent
8cb9bde
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
9 files · +338 -123
M internal/application/port/webui_config.go +54 -0
@@ -11,6 +11,54 @@ type WebUIConfig struct {
1111 SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"`
1212 }
1313
14+// SystemviewCustomPerformancePayload is the editable custom performance profile shape.
15+type SystemviewCustomPerformancePayload struct {
16+ SkiaCPUThreads int `json:"skia_cpu_threads"`
17+ SkiaGPUThreads int `json:"skia_gpu_threads"`
18+ WebProcessMemoryMB int `json:"web_process_memory_mb"`
19+ NetworkProcessMemoryMB int `json:"network_process_memory_mb"`
20+ WebViewPoolPrewarm int `json:"webview_pool_prewarm"`
21+}
22+
23+// SystemviewResolvedPerformancePayload is the resolved effective performance shape.
24+type SystemviewResolvedPerformancePayload struct {
25+ SkiaCPUThreads int `json:"skia_cpu_threads"`
26+ SkiaGPUThreads int `json:"skia_gpu_threads"`
27+ WebProcessMemoryMB int `json:"web_process_memory_mb"`
28+ NetworkProcessMemoryMB int `json:"network_process_memory_mb"`
29+ WebViewPoolPrewarm int `json:"webview_pool_prewarm"`
30+ ConservativeThreshold float64 `json:"conservative_threshold"`
31+ StrictThreshold float64 `json:"strict_threshold"`
32+}
33+
34+// SystemviewHardwarePayload exposes detected hardware to the settings UI.
35+type SystemviewHardwarePayload struct {
36+ CPUCores int `json:"cpu_cores"`
37+ CPUThreads int `json:"cpu_threads"`
38+ TotalRAMMB int `json:"total_ram_mb"`
39+ GPUVendor string `json:"gpu_vendor"`
40+ GPUName string `json:"gpu_name"`
41+ VRAMMB int `json:"vram_mb"`
42+}
43+
44+// SystemviewPerformancePayload is the performance tab payload expected by the UI.
45+type SystemviewPerformancePayload struct {
46+ Profile string `json:"profile"`
47+ Custom SystemviewCustomPerformancePayload `json:"custom"`
48+ Resolved SystemviewResolvedPerformancePayload `json:"resolved"`
49+ Hardware SystemviewHardwarePayload `json:"hardware"`
50+}
51+
52+// SystemviewConfigPayload is the full JSON shape expected by dumb://config.
53+type SystemviewConfigPayload struct {
54+ Appearance WebUIAppearanceConfig `json:"appearance"`
55+ Performance SystemviewPerformancePayload `json:"performance"`
56+ DefaultUIScale float64 `json:"default_ui_scale"`
57+ DefaultSearchEngine string `json:"default_search_engine"`
58+ SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"`
59+ EngineType string `json:"engine_type"`
60+}
61+
1462 // WebUIPerformanceConfig represents performance settings editable in dumb://config.
1563 type WebUIPerformanceConfig struct {
1664 Profile string `json:"profile"`
@@ -51,6 +99,12 @@ type SearchShortcut struct {
5199 Description string `json:"description"`
52100 }
53101
102+// SystemviewConfigReader reads the current and default systemview config payloads.
103+type SystemviewConfigReader interface {
104+ Current(ctx context.Context) (SystemviewConfigPayload, error)
105+ Default(ctx context.Context) (SystemviewConfigPayload, error)
106+}
107+
54108 // WebUIConfigSaver persists WebUI configuration changes.
55109 type WebUIConfigSaver interface {
56110 SaveWebUIConfig(ctx context.Context, cfg WebUIConfig) error
@@ -11,6 +11,54 @@ type WebUIConfig struct {
11 SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"`11 SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"`
12 }12 }
13 13
14+// SystemviewCustomPerformancePayload is the editable custom performance profile shape.
15+type SystemviewCustomPerformancePayload struct {
16+ SkiaCPUThreads int `json:"skia_cpu_threads"`
17+ SkiaGPUThreads int `json:"skia_gpu_threads"`
18+ WebProcessMemoryMB int `json:"web_process_memory_mb"`
19+ NetworkProcessMemoryMB int `json:"network_process_memory_mb"`
20+ WebViewPoolPrewarm int `json:"webview_pool_prewarm"`
21+}
22+
23+// SystemviewResolvedPerformancePayload is the resolved effective performance shape.
24+type SystemviewResolvedPerformancePayload struct {
25+ SkiaCPUThreads int `json:"skia_cpu_threads"`
26+ SkiaGPUThreads int `json:"skia_gpu_threads"`
27+ WebProcessMemoryMB int `json:"web_process_memory_mb"`
28+ NetworkProcessMemoryMB int `json:"network_process_memory_mb"`
29+ WebViewPoolPrewarm int `json:"webview_pool_prewarm"`
30+ ConservativeThreshold float64 `json:"conservative_threshold"`
31+ StrictThreshold float64 `json:"strict_threshold"`
32+}
33+
34+// SystemviewHardwarePayload exposes detected hardware to the settings UI.
35+type SystemviewHardwarePayload struct {
36+ CPUCores int `json:"cpu_cores"`
37+ CPUThreads int `json:"cpu_threads"`
38+ TotalRAMMB int `json:"total_ram_mb"`
39+ GPUVendor string `json:"gpu_vendor"`
40+ GPUName string `json:"gpu_name"`
41+ VRAMMB int `json:"vram_mb"`
42+}
43+
44+// SystemviewPerformancePayload is the performance tab payload expected by the UI.
45+type SystemviewPerformancePayload struct {
46+ Profile string `json:"profile"`
47+ Custom SystemviewCustomPerformancePayload `json:"custom"`
48+ Resolved SystemviewResolvedPerformancePayload `json:"resolved"`
49+ Hardware SystemviewHardwarePayload `json:"hardware"`
50+}
51+
52+// SystemviewConfigPayload is the full JSON shape expected by dumb://config.
53+type SystemviewConfigPayload struct {
54+ Appearance WebUIAppearanceConfig `json:"appearance"`
55+ Performance SystemviewPerformancePayload `json:"performance"`
56+ DefaultUIScale float64 `json:"default_ui_scale"`
57+ DefaultSearchEngine string `json:"default_search_engine"`
58+ SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"`
59+ EngineType string `json:"engine_type"`
60+}
61+
14 // WebUIPerformanceConfig represents performance settings editable in dumb://config.62 // WebUIPerformanceConfig represents performance settings editable in dumb://config.
15 type WebUIPerformanceConfig struct {63 type WebUIPerformanceConfig struct {
16 Profile string `json:"profile"`64 Profile string `json:"profile"`
@@ -51,6 +99,12 @@ type SearchShortcut struct {
51 Description string `json:"description"`99 Description string `json:"description"`
52 }100 }
53 101
102+// SystemviewConfigReader reads the current and default systemview config payloads.
103+type SystemviewConfigReader interface {
104+ Current(ctx context.Context) (SystemviewConfigPayload, error)
105+ Default(ctx context.Context) (SystemviewConfigPayload, error)
106+}
107+
54 // WebUIConfigSaver persists WebUI configuration changes.108 // WebUIConfigSaver persists WebUI configuration changes.
55 type WebUIConfigSaver interface {109 type WebUIConfigSaver interface {
56 SaveWebUIConfig(ctx context.Context, cfg WebUIConfig) error110 SaveWebUIConfig(ctx context.Context, cfg WebUIConfig) error
A internal/application/usecase/read_systemview_config.go +34 -0
@@ -0,0 +1,34 @@
1+package usecase
2+
3+import (
4+ "context"
5+ "fmt"
6+
7+ "github.com/bnema/dumber/internal/application/port"
8+)
9+
10+// ReadSystemviewConfigUseCase exposes read-side systemview config payloads.
11+type ReadSystemviewConfigUseCase struct {
12+ reader port.SystemviewConfigReader
13+}
14+
15+// NewReadSystemviewConfigUseCase creates a new read systemview config use case.
16+func NewReadSystemviewConfigUseCase(reader port.SystemviewConfigReader) *ReadSystemviewConfigUseCase {
17+ return &ReadSystemviewConfigUseCase{reader: reader}
18+}
19+
20+// Current returns the current systemview config payload.
21+func (uc *ReadSystemviewConfigUseCase) Current(ctx context.Context) (port.SystemviewConfigPayload, error) {
22+ if uc == nil || uc.reader == nil {
23+ return port.SystemviewConfigPayload{}, fmt.Errorf("systemview config reader is nil")
24+ }
25+ return uc.reader.Current(ctx)
26+}
27+
28+// Default returns the default systemview config payload.
29+func (uc *ReadSystemviewConfigUseCase) Default(ctx context.Context) (port.SystemviewConfigPayload, error) {
30+ if uc == nil || uc.reader == nil {
31+ return port.SystemviewConfigPayload{}, fmt.Errorf("systemview config reader is nil")
32+ }
33+ return uc.reader.Default(ctx)
34+}
@@ -0,0 +1,34 @@
1+package usecase
2+
3+import (
4+ "context"
5+ "fmt"
6+
7+ "github.com/bnema/dumber/internal/application/port"
8+)
9+
10+// ReadSystemviewConfigUseCase exposes read-side systemview config payloads.
11+type ReadSystemviewConfigUseCase struct {
12+ reader port.SystemviewConfigReader
13+}
14+
15+// NewReadSystemviewConfigUseCase creates a new read systemview config use case.
16+func NewReadSystemviewConfigUseCase(reader port.SystemviewConfigReader) *ReadSystemviewConfigUseCase {
17+ return &ReadSystemviewConfigUseCase{reader: reader}
18+}
19+
20+// Current returns the current systemview config payload.
21+func (uc *ReadSystemviewConfigUseCase) Current(ctx context.Context) (port.SystemviewConfigPayload, error) {
22+ if uc == nil || uc.reader == nil {
23+ return port.SystemviewConfigPayload{}, fmt.Errorf("systemview config reader is nil")
24+ }
25+ return uc.reader.Current(ctx)
26+}
27+
28+// Default returns the default systemview config payload.
29+func (uc *ReadSystemviewConfigUseCase) Default(ctx context.Context) (port.SystemviewConfigPayload, error) {
30+ if uc == nil || uc.reader == nil {
31+ return port.SystemviewConfigPayload{}, fmt.Errorf("systemview config reader is nil")
32+ }
33+ return uc.reader.Default(ctx)
34+}
A internal/application/usecase/read_systemview_config_test.go +51 -0
@@ -0,0 +1,51 @@
1+package usecase
2+
3+import (
4+ "context"
5+ "testing"
6+
7+ "github.com/bnema/dumber/internal/application/port"
8+ "github.com/stretchr/testify/require"
9+)
10+
11+func TestReadSystemviewConfigUseCase_Current(t *testing.T) {
12+ reader := &stubSystemviewConfigReader{
13+ current: port.SystemviewConfigPayload{EngineType: "webkit"},
14+ }
15+
16+ uc := NewReadSystemviewConfigUseCase(reader)
17+
18+ got, err := uc.Current(context.Background())
19+ require.NoError(t, err)
20+ require.Equal(t, reader.current, got)
21+ require.Equal(t, []string{"current"}, reader.calls)
22+}
23+
24+func TestReadSystemviewConfigUseCase_Default(t *testing.T) {
25+ reader := &stubSystemviewConfigReader{
26+ defaultPayload: port.SystemviewConfigPayload{EngineType: "webkit"},
27+ }
28+
29+ uc := NewReadSystemviewConfigUseCase(reader)
30+
31+ got, err := uc.Default(context.Background())
32+ require.NoError(t, err)
33+ require.Equal(t, reader.defaultPayload, got)
34+ require.Equal(t, []string{"default"}, reader.calls)
35+}
36+
37+type stubSystemviewConfigReader struct {
38+ current port.SystemviewConfigPayload
39+ defaultPayload port.SystemviewConfigPayload
40+ calls []string
41+}
42+
43+func (r *stubSystemviewConfigReader) Current(context.Context) (port.SystemviewConfigPayload, error) {
44+ r.calls = append(r.calls, "current")
45+ return r.current, nil
46+}
47+
48+func (r *stubSystemviewConfigReader) Default(context.Context) (port.SystemviewConfigPayload, error) {
49+ r.calls = append(r.calls, "default")
50+ return r.defaultPayload, nil
51+}
@@ -0,0 +1,51 @@
1+package usecase
2+
3+import (
4+ "context"
5+ "testing"
6+
7+ "github.com/bnema/dumber/internal/application/port"
8+ "github.com/stretchr/testify/require"
9+)
10+
11+func TestReadSystemviewConfigUseCase_Current(t *testing.T) {
12+ reader := &stubSystemviewConfigReader{
13+ current: port.SystemviewConfigPayload{EngineType: "webkit"},
14+ }
15+
16+ uc := NewReadSystemviewConfigUseCase(reader)
17+
18+ got, err := uc.Current(context.Background())
19+ require.NoError(t, err)
20+ require.Equal(t, reader.current, got)
21+ require.Equal(t, []string{"current"}, reader.calls)
22+}
23+
24+func TestReadSystemviewConfigUseCase_Default(t *testing.T) {
25+ reader := &stubSystemviewConfigReader{
26+ defaultPayload: port.SystemviewConfigPayload{EngineType: "webkit"},
27+ }
28+
29+ uc := NewReadSystemviewConfigUseCase(reader)
30+
31+ got, err := uc.Default(context.Background())
32+ require.NoError(t, err)
33+ require.Equal(t, reader.defaultPayload, got)
34+ require.Equal(t, []string{"default"}, reader.calls)
35+}
36+
37+type stubSystemviewConfigReader struct {
38+ current port.SystemviewConfigPayload
39+ defaultPayload port.SystemviewConfigPayload
40+ calls []string
41+}
42+
43+func (r *stubSystemviewConfigReader) Current(context.Context) (port.SystemviewConfigPayload, error) {
44+ r.calls = append(r.calls, "current")
45+ return r.current, nil
46+}
47+
48+func (r *stubSystemviewConfigReader) Default(context.Context) (port.SystemviewConfigPayload, error) {
49+ r.calls = append(r.calls, "default")
50+ return r.defaultPayload, nil
51+}
M internal/bootstrap/engine.go +30 -14
@@ -6,6 +6,7 @@ import (
66 "fmt"
77
88 "github.com/bnema/dumber/internal/application/port"
9+ "github.com/bnema/dumber/internal/application/usecase"
910 audiofactory "github.com/bnema/dumber/internal/infrastructure/audio/factory"
1011 "github.com/bnema/dumber/internal/infrastructure/cef"
1112 "github.com/bnema/dumber/internal/infrastructure/config"
@@ -31,6 +32,19 @@ type EngineInput struct {
3132 // BuildEngine constructs a port.Engine for the engine type specified in cfg.Engine.Type.
3233 func BuildEngine(input EngineInput) (port.Engine, error) {
3334 cfg := input.Config
35+ systemviewReader := config.NewSystemviewConfigReader(env.NewHardwareSurveyor())
36+ systemviewUC := usecase.NewReadSystemviewConfigUseCase(systemviewReader)
37+ buildConfigPayload := func(read func(context.Context) (port.SystemviewConfigPayload, error)) func() ([]byte, error) {
38+ return func() ([]byte, error) {
39+ payload, err := read(input.Ctx)
40+ if err != nil {
41+ return nil, err
42+ }
43+ return json.Marshal(payload)
44+ }
45+ }
46+ currentConfigPayload := buildConfigPayload(systemviewUC.Current)
47+ defaultConfigPayload := buildConfigPayload(systemviewUC.Default)
3448 engineType := cfg.Engine.ResolveEngineType()
3549 switch engineType {
3650 case config.EngineTypeWebKit:
@@ -40,9 +54,23 @@ func BuildEngine(input EngineInput) (port.Engine, error) {
4054 CookiePolicy: port.CookiePolicy(cfg.Engine.CookiePolicy),
4155 }
4256 wkCfg := webkit.EngineConfigFromConfig(cfg.Engine.WebKit)
57+ systemviewReader := config.NewSystemviewConfigReader(env.NewHardwareSurveyor())
58+ systemviewUC := usecase.NewReadSystemviewConfigUseCase(systemviewReader)
59+ buildConfigPayload := func(read func(context.Context) (port.SystemviewConfigPayload, error)) func() ([]byte, error) {
60+ return func() ([]byte, error) {
61+ payload, err := read(input.Ctx)
62+ if err != nil {
63+ return nil, err
64+ }
65+ return json.Marshal(payload)
66+ }
67+ }
68+ currentConfigPayload := buildConfigPayload(systemviewUC.Current)
69+ defaultConfigPayload := buildConfigPayload(systemviewUC.Default)
4370
4471 return webkit.NewEngine(
4572 input.Ctx, cfg, opts, wkCfg,
73+ currentConfigPayload, defaultConfigPayload,
4674 input.ThemeManager, input.ColorResolver, input.Logger,
4775 )
4876 case config.EngineTypeCEF:
@@ -66,23 +94,11 @@ func BuildEngine(input EngineInput) (port.Engine, error) {
6694 MaxConcurrent: cfg.Transcoding.MaxConcurrent,
6795 Quality: cfg.Transcoding.Quality,
6896 }
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- }
8197 deps := cef.EngineDependencies{
8298 RegisterHandlers: handlers.RegisterAll,
8399 RegisterAccentHandlers: handlers.RegisterAccentHandlers,
84- CurrentConfigPayload: buildConfigPayload(config.Get),
85- DefaultConfigPayload: buildConfigPayload(config.DefaultConfig),
100+ CurrentConfigPayload: currentConfigPayload,
101+ DefaultConfigPayload: defaultConfigPayload,
86102 MediaClassifier: cef.MediaClassifier{
87103 IsProprietaryVideoMIME: transcoder.IsProprietaryVideoMIME,
88104 IsOpenVideoMIME: transcoder.IsOpenVideoMIME,
@@ -6,6 +6,7 @@ import (
6 "fmt"6 "fmt"
7 7
8 "github.com/bnema/dumber/internal/application/port"8 "github.com/bnema/dumber/internal/application/port"
9+ "github.com/bnema/dumber/internal/application/usecase"
9 audiofactory "github.com/bnema/dumber/internal/infrastructure/audio/factory"10 audiofactory "github.com/bnema/dumber/internal/infrastructure/audio/factory"
10 "github.com/bnema/dumber/internal/infrastructure/cef"11 "github.com/bnema/dumber/internal/infrastructure/cef"
11 "github.com/bnema/dumber/internal/infrastructure/config"12 "github.com/bnema/dumber/internal/infrastructure/config"
@@ -31,6 +32,19 @@ type EngineInput struct {
31 // BuildEngine constructs a port.Engine for the engine type specified in cfg.Engine.Type.32 // BuildEngine constructs a port.Engine for the engine type specified in cfg.Engine.Type.
32 func BuildEngine(input EngineInput) (port.Engine, error) {33 func BuildEngine(input EngineInput) (port.Engine, error) {
33 cfg := input.Config34 cfg := input.Config
35+ systemviewReader := config.NewSystemviewConfigReader(env.NewHardwareSurveyor())
36+ systemviewUC := usecase.NewReadSystemviewConfigUseCase(systemviewReader)
37+ buildConfigPayload := func(read func(context.Context) (port.SystemviewConfigPayload, error)) func() ([]byte, error) {
38+ return func() ([]byte, error) {
39+ payload, err := read(input.Ctx)
40+ if err != nil {
41+ return nil, err
42+ }
43+ return json.Marshal(payload)
44+ }
45+ }
46+ currentConfigPayload := buildConfigPayload(systemviewUC.Current)
47+ defaultConfigPayload := buildConfigPayload(systemviewUC.Default)
34 engineType := cfg.Engine.ResolveEngineType()48 engineType := cfg.Engine.ResolveEngineType()
35 switch engineType {49 switch engineType {
36 case config.EngineTypeWebKit:50 case config.EngineTypeWebKit:
@@ -40,9 +54,23 @@ func BuildEngine(input EngineInput) (port.Engine, error) {
40 CookiePolicy: port.CookiePolicy(cfg.Engine.CookiePolicy),54 CookiePolicy: port.CookiePolicy(cfg.Engine.CookiePolicy),
41 }55 }
42 wkCfg := webkit.EngineConfigFromConfig(cfg.Engine.WebKit)56 wkCfg := webkit.EngineConfigFromConfig(cfg.Engine.WebKit)
57+ systemviewReader := config.NewSystemviewConfigReader(env.NewHardwareSurveyor())
58+ systemviewUC := usecase.NewReadSystemviewConfigUseCase(systemviewReader)
59+ buildConfigPayload := func(read func(context.Context) (port.SystemviewConfigPayload, error)) func() ([]byte, error) {
60+ return func() ([]byte, error) {
61+ payload, err := read(input.Ctx)
62+ if err != nil {
63+ return nil, err
64+ }
65+ return json.Marshal(payload)
66+ }
67+ }
68+ currentConfigPayload := buildConfigPayload(systemviewUC.Current)
69+ defaultConfigPayload := buildConfigPayload(systemviewUC.Default)
43 70
44 return webkit.NewEngine(71 return webkit.NewEngine(
45 input.Ctx, cfg, opts, wkCfg,72 input.Ctx, cfg, opts, wkCfg,
73+ currentConfigPayload, defaultConfigPayload,
46 input.ThemeManager, input.ColorResolver, input.Logger,74 input.ThemeManager, input.ColorResolver, input.Logger,
47 )75 )
48 case config.EngineTypeCEF:76 case config.EngineTypeCEF:
@@ -66,23 +94,11 @@ func BuildEngine(input EngineInput) (port.Engine, error) {
66 MaxConcurrent: cfg.Transcoding.MaxConcurrent,94 MaxConcurrent: cfg.Transcoding.MaxConcurrent,
67 Quality: cfg.Transcoding.Quality,95 Quality: cfg.Transcoding.Quality,
68 }96 }
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{97 deps := cef.EngineDependencies{
82 RegisterHandlers: handlers.RegisterAll,98 RegisterHandlers: handlers.RegisterAll,
83 RegisterAccentHandlers: handlers.RegisterAccentHandlers,99 RegisterAccentHandlers: handlers.RegisterAccentHandlers,
84- CurrentConfigPayload: buildConfigPayload(config.Get),100+ CurrentConfigPayload: currentConfigPayload,
85- DefaultConfigPayload: buildConfigPayload(config.DefaultConfig),101+ DefaultConfigPayload: defaultConfigPayload,
86 MediaClassifier: cef.MediaClassifier{102 MediaClassifier: cef.MediaClassifier{
87 IsProprietaryVideoMIME: transcoder.IsProprietaryVideoMIME,103 IsProprietaryVideoMIME: transcoder.IsProprietaryVideoMIME,
88 IsOpenVideoMIME: transcoder.IsOpenVideoMIME,104 IsOpenVideoMIME: transcoder.IsOpenVideoMIME,
A internal/infrastructure/config/systemview_reader.go +40 -0
@@ -0,0 +1,40 @@
1+package config
2+
3+import (
4+ "context"
5+
6+ "github.com/bnema/dumber/internal/application/port"
7+)
8+
9+type systemviewConfigReader struct {
10+ hwSurveyor port.HardwareSurveyor
11+}
12+
13+var _ port.SystemviewConfigReader = (*systemviewConfigReader)(nil)
14+
15+// NewSystemviewConfigReader creates a read-side config payload reader.
16+func NewSystemviewConfigReader(hwSurveyor port.HardwareSurveyor) port.SystemviewConfigReader {
17+ return &systemviewConfigReader{hwSurveyor: hwSurveyor}
18+}
19+
20+func (r *systemviewConfigReader) Current(ctx context.Context) (port.SystemviewConfigPayload, error) {
21+ return r.build(ctx, Get())
22+}
23+
24+func (r *systemviewConfigReader) Default(ctx context.Context) (port.SystemviewConfigPayload, error) {
25+ return r.build(ctx, DefaultConfig())
26+}
27+
28+func (r *systemviewConfigReader) build(ctx context.Context, cfg *Config) (port.SystemviewConfigPayload, error) {
29+ if cfg == nil {
30+ return port.SystemviewConfigPayload{}, nil
31+ }
32+
33+ var hw *port.HardwareInfo
34+ if r != nil && r.hwSurveyor != nil {
35+ survey := r.hwSurveyor.Survey(ctx)
36+ hw = &survey
37+ }
38+
39+ return BuildSystemviewConfigPayload(cfg, hw), nil
40+}
@@ -0,0 +1,40 @@
1+package config
2+
3+import (
4+ "context"
5+
6+ "github.com/bnema/dumber/internal/application/port"
7+)
8+
9+type systemviewConfigReader struct {
10+ hwSurveyor port.HardwareSurveyor
11+}
12+
13+var _ port.SystemviewConfigReader = (*systemviewConfigReader)(nil)
14+
15+// NewSystemviewConfigReader creates a read-side config payload reader.
16+func NewSystemviewConfigReader(hwSurveyor port.HardwareSurveyor) port.SystemviewConfigReader {
17+ return &systemviewConfigReader{hwSurveyor: hwSurveyor}
18+}
19+
20+func (r *systemviewConfigReader) Current(ctx context.Context) (port.SystemviewConfigPayload, error) {
21+ return r.build(ctx, Get())
22+}
23+
24+func (r *systemviewConfigReader) Default(ctx context.Context) (port.SystemviewConfigPayload, error) {
25+ return r.build(ctx, DefaultConfig())
26+}
27+
28+func (r *systemviewConfigReader) build(ctx context.Context, cfg *Config) (port.SystemviewConfigPayload, error) {
29+ if cfg == nil {
30+ return port.SystemviewConfigPayload{}, nil
31+ }
32+
33+ var hw *port.HardwareInfo
34+ if r != nil && r.hwSurveyor != nil {
35+ survey := r.hwSurveyor.Survey(ctx)
36+ hw = &survey
37+ }
38+
39+ return BuildSystemviewConfigPayload(cfg, hw), nil
40+}
M internal/infrastructure/config/webui_payload.go +53 -80
@@ -2,99 +2,32 @@ package config
22
33 import "github.com/bnema/dumber/internal/application/port"
44
5-// WebUIConfigAppearancePayload is the JSON shape expected by dumb://config.
6-type WebUIConfigAppearancePayload struct {
7- SansFont string `json:"sans_font"`
8- SerifFont string `json:"serif_font"`
9- MonospaceFont string `json:"monospace_font"`
10- DefaultFontSize int `json:"default_font_size"`
11- ColorScheme string `json:"color_scheme"`
12- LightPalette ColorPalette `json:"light_palette"`
13- DarkPalette ColorPalette `json:"dark_palette"`
14-}
15-
16-// WebUIConfigHardwarePayload exposes detected hardware to the settings UI.
17-type WebUIConfigHardwarePayload struct {
18- CPUCores int `json:"cpu_cores"`
19- CPUThreads int `json:"cpu_threads"`
20- TotalRAMMB int `json:"total_ram_mb"`
21- GPUVendor string `json:"gpu_vendor"`
22- GPUName string `json:"gpu_name"`
23- VRAMMB int `json:"vram_mb"`
24-}
25-
26-// WebUIConfigCustomPerformancePayload holds user-editable custom profile values.
27-type WebUIConfigCustomPerformancePayload struct {
28- SkiaCPUThreads int `json:"skia_cpu_threads"`
29- SkiaGPUThreads int `json:"skia_gpu_threads"`
30- WebProcessMemoryMB int `json:"web_process_memory_mb"`
31- NetworkProcessMemoryMB int `json:"network_process_memory_mb"`
32- WebViewPoolPrewarm int `json:"webview_pool_prewarm"`
33-}
34-
35-// WebUIConfigResolvedPerformancePayload holds the resolved effective values.
36-type WebUIConfigResolvedPerformancePayload struct {
37- SkiaCPUThreads int `json:"skia_cpu_threads"`
38- SkiaGPUThreads int `json:"skia_gpu_threads"`
39- WebProcessMemoryMB int `json:"web_process_memory_mb"`
40- NetworkProcessMemoryMB int `json:"network_process_memory_mb"`
41- WebViewPoolPrewarm int `json:"webview_pool_prewarm"`
42- ConservativeThreshold float64 `json:"conservative_threshold"`
43- StrictThreshold float64 `json:"strict_threshold"`
44-}
45-
46-// WebUIConfigPerformancePayload is the performance tab payload expected by the UI.
47-type WebUIConfigPerformancePayload struct {
48- Profile string `json:"profile"`
49- Custom WebUIConfigCustomPerformancePayload `json:"custom"`
50- Resolved WebUIConfigResolvedPerformancePayload `json:"resolved"`
51- Hardware WebUIConfigHardwarePayload `json:"hardware"`
52-}
53-
54-// WebUIConfigPayload is the full JSON shape expected by dumb://config.
55-type WebUIConfigPayload struct {
56- Appearance WebUIConfigAppearancePayload `json:"appearance"`
57- Performance WebUIConfigPerformancePayload `json:"performance"`
58- DefaultUIScale float64 `json:"default_ui_scale"`
59- DefaultSearchEngine string `json:"default_search_engine"`
60- SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"`
61- EngineType string `json:"engine_type"`
62-}
63-
64-// BuildWebUIConfigPayload projects the full config into the stable DTO used by
5+// BuildSystemviewConfigPayload projects the full config into the stable DTO used by
656 // the settings page across engines.
66-func BuildWebUIConfigPayload(cfg *Config, hw *port.HardwareInfo) WebUIConfigPayload {
7+func BuildSystemviewConfigPayload(cfg *Config, hw *port.HardwareInfo) port.SystemviewConfigPayload {
678 if cfg == nil {
68- return WebUIConfigPayload{}
9+ return port.SystemviewConfigPayload{}
6910 }
7011
7112 perfCfg := PerformanceConfigFromEngine(&cfg.Engine)
7213 resolved := ResolvePerformanceProfile(&perfCfg, hw)
7314
74- return WebUIConfigPayload{
15+ return port.SystemviewConfigPayload{
16+ Appearance: buildSystemviewAppearancePayload(cfg.Appearance),
7517 DefaultUIScale: cfg.DefaultUIScale,
7618 DefaultSearchEngine: cfg.DefaultSearchEngine,
77- SearchShortcuts: cfg.SearchShortcuts,
19+ SearchShortcuts: buildSystemviewSearchShortcutsPayload(cfg.SearchShortcuts),
7820 EngineType: cfg.Engine.ResolveEngineType(),
79- Appearance: WebUIConfigAppearancePayload{
80- SansFont: cfg.Appearance.SansFont,
81- SerifFont: cfg.Appearance.SerifFont,
82- MonospaceFont: cfg.Appearance.MonospaceFont,
83- DefaultFontSize: cfg.Appearance.DefaultFontSize,
84- ColorScheme: cfg.Appearance.ColorScheme,
85- LightPalette: cfg.Appearance.LightPalette,
86- DarkPalette: cfg.Appearance.DarkPalette,
87- },
88- Performance: WebUIConfigPerformancePayload{
21+ Performance: port.SystemviewPerformancePayload{
8922 Profile: string(cfg.Engine.Profile),
90- Custom: WebUIConfigCustomPerformancePayload{
23+ Custom: port.SystemviewCustomPerformancePayload{
9124 SkiaCPUThreads: cfg.Engine.WebKit.SkiaCPUPaintingThreads,
9225 SkiaGPUThreads: cfg.Engine.WebKit.SkiaGPUPaintingThreads,
9326 WebProcessMemoryMB: cfg.Engine.WebKit.WebProcessMemoryLimitMB,
9427 NetworkProcessMemoryMB: cfg.Engine.WebKit.NetworkProcessMemoryLimitMB,
9528 WebViewPoolPrewarm: cfg.Engine.PoolPrewarmCount,
9629 },
97- Resolved: WebUIConfigResolvedPerformancePayload{
30+ Resolved: port.SystemviewResolvedPerformancePayload{
9831 SkiaCPUThreads: resolved.SkiaCPUPaintingThreads,
9932 SkiaGPUThreads: resolved.SkiaGPUPaintingThreads,
10033 WebProcessMemoryMB: resolved.WebProcessMemoryLimitMB,
@@ -103,16 +36,56 @@ func BuildWebUIConfigPayload(cfg *Config, hw *port.HardwareInfo) WebUIConfigPayl
10336 ConservativeThreshold: resolved.WebProcessMemoryConservativeThreshold,
10437 StrictThreshold: resolved.WebProcessMemoryStrictThreshold,
10538 },
106- Hardware: buildWebUIHardwarePayload(hw),
39+ Hardware: buildSystemviewHardwarePayload(hw),
40+ },
41+ }
42+}
43+
44+func buildSystemviewAppearancePayload(appearance AppearanceConfig) port.WebUIAppearanceConfig {
45+ return port.WebUIAppearanceConfig{
46+ SansFont: appearance.SansFont,
47+ SerifFont: appearance.SerifFont,
48+ MonospaceFont: appearance.MonospaceFont,
49+ DefaultFontSize: appearance.DefaultFontSize,
50+ ColorScheme: appearance.ColorScheme,
51+ LightPalette: port.ColorPalette{
52+ Background: appearance.LightPalette.Background,
53+ Surface: appearance.LightPalette.Surface,
54+ SurfaceVariant: appearance.LightPalette.SurfaceVariant,
55+ Text: appearance.LightPalette.Text,
56+ Muted: appearance.LightPalette.Muted,
57+ Accent: appearance.LightPalette.Accent,
58+ Border: appearance.LightPalette.Border,
59+ },
60+ DarkPalette: port.ColorPalette{
61+ Background: appearance.DarkPalette.Background,
62+ Surface: appearance.DarkPalette.Surface,
63+ SurfaceVariant: appearance.DarkPalette.SurfaceVariant,
64+ Text: appearance.DarkPalette.Text,
65+ Muted: appearance.DarkPalette.Muted,
66+ Accent: appearance.DarkPalette.Accent,
67+ Border: appearance.DarkPalette.Border,
10768 },
10869 }
10970 }
11071
111-func buildWebUIHardwarePayload(hw *port.HardwareInfo) WebUIConfigHardwarePayload {
72+func buildSystemviewSearchShortcutsPayload(shortcuts map[string]SearchShortcut) map[string]port.SearchShortcut {
73+ if len(shortcuts) == 0 {
74+ return nil
75+ }
76+
77+ result := make(map[string]port.SearchShortcut, len(shortcuts))
78+ for key, shortcut := range shortcuts {
79+ result[key] = port.SearchShortcut{URL: shortcut.URL, Description: shortcut.Description}
80+ }
81+ return result
82+}
83+
84+func buildSystemviewHardwarePayload(hw *port.HardwareInfo) port.SystemviewHardwarePayload {
11285 if hw == nil {
113- return WebUIConfigHardwarePayload{}
86+ return port.SystemviewHardwarePayload{}
11487 }
115- return WebUIConfigHardwarePayload{
88+ return port.SystemviewHardwarePayload{
11689 CPUCores: hw.CPUCores,
11790 CPUThreads: hw.CPUThreads,
11891 TotalRAMMB: hw.TotalRAMMB(),
@@ -2,99 +2,32 @@ package config
2 2
3 import "github.com/bnema/dumber/internal/application/port"3 import "github.com/bnema/dumber/internal/application/port"
4 4
5-// WebUIConfigAppearancePayload is the JSON shape expected by dumb://config.5+// BuildSystemviewConfigPayload projects the full config into the stable DTO used by
6-type WebUIConfigAppearancePayload struct {
7- SansFont string `json:"sans_font"`
8- SerifFont string `json:"serif_font"`
9- MonospaceFont string `json:"monospace_font"`
10- DefaultFontSize int `json:"default_font_size"`
11- ColorScheme string `json:"color_scheme"`
12- LightPalette ColorPalette `json:"light_palette"`
13- DarkPalette ColorPalette `json:"dark_palette"`
14-}
15-
16-// WebUIConfigHardwarePayload exposes detected hardware to the settings UI.
17-type WebUIConfigHardwarePayload struct {
18- CPUCores int `json:"cpu_cores"`
19- CPUThreads int `json:"cpu_threads"`
20- TotalRAMMB int `json:"total_ram_mb"`
21- GPUVendor string `json:"gpu_vendor"`
22- GPUName string `json:"gpu_name"`
23- VRAMMB int `json:"vram_mb"`
24-}
25-
26-// WebUIConfigCustomPerformancePayload holds user-editable custom profile values.
27-type WebUIConfigCustomPerformancePayload struct {
28- SkiaCPUThreads int `json:"skia_cpu_threads"`
29- SkiaGPUThreads int `json:"skia_gpu_threads"`
30- WebProcessMemoryMB int `json:"web_process_memory_mb"`
31- NetworkProcessMemoryMB int `json:"network_process_memory_mb"`
32- WebViewPoolPrewarm int `json:"webview_pool_prewarm"`
33-}
34-
35-// WebUIConfigResolvedPerformancePayload holds the resolved effective values.
36-type WebUIConfigResolvedPerformancePayload struct {
37- SkiaCPUThreads int `json:"skia_cpu_threads"`
38- SkiaGPUThreads int `json:"skia_gpu_threads"`
39- WebProcessMemoryMB int `json:"web_process_memory_mb"`
40- NetworkProcessMemoryMB int `json:"network_process_memory_mb"`
41- WebViewPoolPrewarm int `json:"webview_pool_prewarm"`
42- ConservativeThreshold float64 `json:"conservative_threshold"`
43- StrictThreshold float64 `json:"strict_threshold"`
44-}
45-
46-// WebUIConfigPerformancePayload is the performance tab payload expected by the UI.
47-type WebUIConfigPerformancePayload struct {
48- Profile string `json:"profile"`
49- Custom WebUIConfigCustomPerformancePayload `json:"custom"`
50- Resolved WebUIConfigResolvedPerformancePayload `json:"resolved"`
51- Hardware WebUIConfigHardwarePayload `json:"hardware"`
52-}
53-
54-// WebUIConfigPayload is the full JSON shape expected by dumb://config.
55-type WebUIConfigPayload struct {
56- Appearance WebUIConfigAppearancePayload `json:"appearance"`
57- Performance WebUIConfigPerformancePayload `json:"performance"`
58- DefaultUIScale float64 `json:"default_ui_scale"`
59- DefaultSearchEngine string `json:"default_search_engine"`
60- SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"`
61- EngineType string `json:"engine_type"`
62-}
63-
64-// BuildWebUIConfigPayload projects the full config into the stable DTO used by
65 // the settings page across engines.6 // the settings page across engines.
66-func BuildWebUIConfigPayload(cfg *Config, hw *port.HardwareInfo) WebUIConfigPayload {7+func BuildSystemviewConfigPayload(cfg *Config, hw *port.HardwareInfo) port.SystemviewConfigPayload {
67 if cfg == nil {8 if cfg == nil {
68- return WebUIConfigPayload{}9+ return port.SystemviewConfigPayload{}
69 }10 }
70 11
71 perfCfg := PerformanceConfigFromEngine(&cfg.Engine)12 perfCfg := PerformanceConfigFromEngine(&cfg.Engine)
72 resolved := ResolvePerformanceProfile(&perfCfg, hw)13 resolved := ResolvePerformanceProfile(&perfCfg, hw)
73 14
74- return WebUIConfigPayload{15+ return port.SystemviewConfigPayload{
16+ Appearance: buildSystemviewAppearancePayload(cfg.Appearance),
75 DefaultUIScale: cfg.DefaultUIScale,17 DefaultUIScale: cfg.DefaultUIScale,
76 DefaultSearchEngine: cfg.DefaultSearchEngine,18 DefaultSearchEngine: cfg.DefaultSearchEngine,
77- SearchShortcuts: cfg.SearchShortcuts,19+ SearchShortcuts: buildSystemviewSearchShortcutsPayload(cfg.SearchShortcuts),
78 EngineType: cfg.Engine.ResolveEngineType(),20 EngineType: cfg.Engine.ResolveEngineType(),
79- Appearance: WebUIConfigAppearancePayload{21+ Performance: port.SystemviewPerformancePayload{
80- SansFont: cfg.Appearance.SansFont,
81- SerifFont: cfg.Appearance.SerifFont,
82- MonospaceFont: cfg.Appearance.MonospaceFont,
83- DefaultFontSize: cfg.Appearance.DefaultFontSize,
84- ColorScheme: cfg.Appearance.ColorScheme,
85- LightPalette: cfg.Appearance.LightPalette,
86- DarkPalette: cfg.Appearance.DarkPalette,
87- },
88- Performance: WebUIConfigPerformancePayload{
89 Profile: string(cfg.Engine.Profile),22 Profile: string(cfg.Engine.Profile),
90- Custom: WebUIConfigCustomPerformancePayload{23+ Custom: port.SystemviewCustomPerformancePayload{
91 SkiaCPUThreads: cfg.Engine.WebKit.SkiaCPUPaintingThreads,24 SkiaCPUThreads: cfg.Engine.WebKit.SkiaCPUPaintingThreads,
92 SkiaGPUThreads: cfg.Engine.WebKit.SkiaGPUPaintingThreads,25 SkiaGPUThreads: cfg.Engine.WebKit.SkiaGPUPaintingThreads,
93 WebProcessMemoryMB: cfg.Engine.WebKit.WebProcessMemoryLimitMB,26 WebProcessMemoryMB: cfg.Engine.WebKit.WebProcessMemoryLimitMB,
94 NetworkProcessMemoryMB: cfg.Engine.WebKit.NetworkProcessMemoryLimitMB,27 NetworkProcessMemoryMB: cfg.Engine.WebKit.NetworkProcessMemoryLimitMB,
95 WebViewPoolPrewarm: cfg.Engine.PoolPrewarmCount,28 WebViewPoolPrewarm: cfg.Engine.PoolPrewarmCount,
96 },29 },
97- Resolved: WebUIConfigResolvedPerformancePayload{30+ Resolved: port.SystemviewResolvedPerformancePayload{
98 SkiaCPUThreads: resolved.SkiaCPUPaintingThreads,31 SkiaCPUThreads: resolved.SkiaCPUPaintingThreads,
99 SkiaGPUThreads: resolved.SkiaGPUPaintingThreads,32 SkiaGPUThreads: resolved.SkiaGPUPaintingThreads,
100 WebProcessMemoryMB: resolved.WebProcessMemoryLimitMB,33 WebProcessMemoryMB: resolved.WebProcessMemoryLimitMB,
@@ -103,16 +36,56 @@ func BuildWebUIConfigPayload(cfg *Config, hw *port.HardwareInfo) WebUIConfigPayl
103 ConservativeThreshold: resolved.WebProcessMemoryConservativeThreshold,36 ConservativeThreshold: resolved.WebProcessMemoryConservativeThreshold,
104 StrictThreshold: resolved.WebProcessMemoryStrictThreshold,37 StrictThreshold: resolved.WebProcessMemoryStrictThreshold,
105 },38 },
106- Hardware: buildWebUIHardwarePayload(hw),39+ Hardware: buildSystemviewHardwarePayload(hw),
40+ },
41+ }
42+}
43+
44+func buildSystemviewAppearancePayload(appearance AppearanceConfig) port.WebUIAppearanceConfig {
45+ return port.WebUIAppearanceConfig{
46+ SansFont: appearance.SansFont,
47+ SerifFont: appearance.SerifFont,
48+ MonospaceFont: appearance.MonospaceFont,
49+ DefaultFontSize: appearance.DefaultFontSize,
50+ ColorScheme: appearance.ColorScheme,
51+ LightPalette: port.ColorPalette{
52+ Background: appearance.LightPalette.Background,
53+ Surface: appearance.LightPalette.Surface,
54+ SurfaceVariant: appearance.LightPalette.SurfaceVariant,
55+ Text: appearance.LightPalette.Text,
56+ Muted: appearance.LightPalette.Muted,
57+ Accent: appearance.LightPalette.Accent,
58+ Border: appearance.LightPalette.Border,
59+ },
60+ DarkPalette: port.ColorPalette{
61+ Background: appearance.DarkPalette.Background,
62+ Surface: appearance.DarkPalette.Surface,
63+ SurfaceVariant: appearance.DarkPalette.SurfaceVariant,
64+ Text: appearance.DarkPalette.Text,
65+ Muted: appearance.DarkPalette.Muted,
66+ Accent: appearance.DarkPalette.Accent,
67+ Border: appearance.DarkPalette.Border,
107 },68 },
108 }69 }
109 }70 }
110 71
111-func buildWebUIHardwarePayload(hw *port.HardwareInfo) WebUIConfigHardwarePayload {72+func buildSystemviewSearchShortcutsPayload(shortcuts map[string]SearchShortcut) map[string]port.SearchShortcut {
73+ if len(shortcuts) == 0 {
74+ return nil
75+ }
76+
77+ result := make(map[string]port.SearchShortcut, len(shortcuts))
78+ for key, shortcut := range shortcuts {
79+ result[key] = port.SearchShortcut{URL: shortcut.URL, Description: shortcut.Description}
80+ }
81+ return result
82+}
83+
84+func buildSystemviewHardwarePayload(hw *port.HardwareInfo) port.SystemviewHardwarePayload {
112 if hw == nil {85 if hw == nil {
113- return WebUIConfigHardwarePayload{}86+ return port.SystemviewHardwarePayload{}
114 }87 }
115- return WebUIConfigHardwarePayload{88+ return port.SystemviewHardwarePayload{
116 CPUCores: hw.CPUCores,89 CPUCores: hw.CPUCores,
117 CPUThreads: hw.CPUThreads,90 CPUThreads: hw.CPUThreads,
118 TotalRAMMB: hw.TotalRAMMB(),91 TotalRAMMB: hw.TotalRAMMB(),
M internal/infrastructure/webkit/engine_init.go +3 -0
@@ -23,6 +23,8 @@ func NewEngine(
2323 cfg *config.Config,
2424 opts port.EngineOptions,
2525 wkCfg WebKitEngineConfig,
26+ currentConfigPayload func() ([]byte, error),
27+ defaultConfigPayload func() ([]byte, error),
2628 themeManager *theme.Manager,
2729 colorResolver port.ColorSchemeResolver,
2830 logger zerolog.Logger,
@@ -52,6 +54,7 @@ func NewEngine(
5254
5355 // --- Scheme handler ---
5456 schemeHandler := NewDumbSchemeHandler(ctx)
57+ schemeHandler.SetConfigPayloadBuilders(currentConfigPayload, defaultConfigPayload)
5558 schemeHandler.SetAssets(assets.WebUIAssets)
5659 schemeHandler.RegisterWithContext(wkCtx)
5760
@@ -23,6 +23,8 @@ func NewEngine(
23 cfg *config.Config,23 cfg *config.Config,
24 opts port.EngineOptions,24 opts port.EngineOptions,
25 wkCfg WebKitEngineConfig,25 wkCfg WebKitEngineConfig,
26+ currentConfigPayload func() ([]byte, error),
27+ defaultConfigPayload func() ([]byte, error),
26 themeManager *theme.Manager,28 themeManager *theme.Manager,
27 colorResolver port.ColorSchemeResolver,29 colorResolver port.ColorSchemeResolver,
28 logger zerolog.Logger,30 logger zerolog.Logger,
@@ -52,6 +54,7 @@ func NewEngine(
52 54
53 // --- Scheme handler ---55 // --- Scheme handler ---
54 schemeHandler := NewDumbSchemeHandler(ctx)56 schemeHandler := NewDumbSchemeHandler(ctx)
57+ schemeHandler.SetConfigPayloadBuilders(currentConfigPayload, defaultConfigPayload)
55 schemeHandler.SetAssets(assets.WebUIAssets)58 schemeHandler.SetAssets(assets.WebUIAssets)
56 schemeHandler.RegisterWithContext(wkCtx)59 schemeHandler.RegisterWithContext(wkCtx)
57 60
M internal/infrastructure/webkit/scheme_handler.go +35 -29
@@ -3,7 +3,6 @@ package webkit
33 import (
44 "context"
55 "embed"
6- "encoding/json"
76 "fmt"
87 "io/fs"
98 "net/http"
@@ -12,9 +11,6 @@ import (
1211 "strings"
1312 "sync"
1413
15- "github.com/bnema/dumber/internal/application/port"
16- "github.com/bnema/dumber/internal/infrastructure/config"
17- "github.com/bnema/dumber/internal/infrastructure/env"
1814 "github.com/bnema/dumber/internal/infrastructure/webutil"
1915 "github.com/bnema/dumber/internal/logging"
2016 "github.com/bnema/puregotk-webkit/soup"
@@ -64,13 +60,13 @@ func (f PageHandlerFunc) Handle(req *SchemeRequest) *SchemeResponse {
6460
6561 // DumbSchemeHandler handles dumb:// URI scheme requests.
6662 type DumbSchemeHandler struct {
67- handlers map[string]PageHandler
68- assets embed.FS
69- assetDir string // subdirectory within embed.FS (e.g., "assets/webui")
70- logger zerolog.Logger
71- mu sync.RWMutex
72- hwSurveyor *env.HardwareSurveyor
73- ctx context.Context
63+ handlers map[string]PageHandler
64+ assets embed.FS
65+ assetDir string // subdirectory within embed.FS (e.g., "assets/webui")
66+ logger zerolog.Logger
67+ mu sync.RWMutex
68+ currentConfigPayload func() ([]byte, error)
69+ defaultConfigPayload func() ([]byte, error)
7470 }
7571
7672 // NewDumbSchemeHandler creates a new handler for the dumb:// scheme.
@@ -78,11 +74,9 @@ func NewDumbSchemeHandler(ctx context.Context) *DumbSchemeHandler {
7874 log := logging.FromContext(ctx)
7975
8076 h := &DumbSchemeHandler{
81- handlers: make(map[string]PageHandler),
82- assetDir: "webui",
83- logger: log.With().Str("component", "scheme-handler").Logger(),
84- hwSurveyor: env.NewHardwareSurveyor(),
85- ctx: ctx,
77+ handlers: make(map[string]PageHandler),
78+ assetDir: "webui",
79+ logger: log.With().Str("component", "scheme-handler").Logger(),
8680 }
8781
8882 // Register default pages
@@ -99,6 +93,15 @@ func (h *DumbSchemeHandler) SetAssets(assets embed.FS) {
9993 h.logger.Debug().Msg("assets filesystem configured")
10094 }
10195
96+// SetConfigPayloadBuilders wires the config payload builders used by /api/config.
97+func (h *DumbSchemeHandler) SetConfigPayloadBuilders(current, defaultPayload func() ([]byte, error)) {
98+ h.mu.Lock()
99+ defer h.mu.Unlock()
100+ h.currentConfigPayload = current
101+ h.defaultConfigPayload = defaultPayload
102+ h.logger.Debug().Msg("config payload builders configured")
103+}
104+
102105 // registerDefaults sets up default page handlers.
103106 func (h *DumbSchemeHandler) registerDefaults() {
104107 // Error page (static fallback)
@@ -129,7 +132,10 @@ func (h *DumbSchemeHandler) registerDefaults() {
129132 return nil
130133 }
131134
132- return h.buildConfigResponse(config.Get())
135+ h.mu.RLock()
136+ build := h.currentConfigPayload
137+ h.mu.RUnlock()
138+ return h.buildConfigResponse(build)
133139 }))
134140
135141 // API: Get default config (used by Reset Defaults in dumb://config)
@@ -138,7 +144,10 @@ func (h *DumbSchemeHandler) registerDefaults() {
138144 return nil
139145 }
140146
141- return h.buildConfigResponse(config.DefaultConfig())
147+ h.mu.RLock()
148+ build := h.defaultConfigPayload
149+ h.mu.RUnlock()
150+ return h.buildConfigResponse(build)
142151 }))
143152 }
144153
@@ -161,19 +170,16 @@ func buildCrashPageHTML(originalURI string) string {
161170 return webutil.BuildCrashPageHTML(originalURI)
162171 }
163172
164-func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeResponse {
165- // Get hardware info for display and profile resolution
166- // Use background context since survey results are cached and we don't want
167- // request context cancellation to affect this
168- var hw *port.HardwareInfo
169- if h.hwSurveyor != nil {
170- hwInfo := h.hwSurveyor.Survey(context.Background())
171- hw = &hwInfo
173+func (h *DumbSchemeHandler) buildConfigResponse(build func() ([]byte, error)) *SchemeResponse {
174+ if build == nil {
175+ return &SchemeResponse{
176+ Data: []byte(`{"error": "config payload builder not configured"}`),
177+ ContentType: "application/json",
178+ StatusCode: http.StatusInternalServerError,
179+ }
172180 }
173181
174- resp := config.BuildWebUIConfigPayload(cfg, hw)
175-
176- data, err := json.Marshal(resp)
182+ data, err := build()
177183 if err != nil {
178184 return &SchemeResponse{
179185 Data: []byte(fmt.Sprintf(`{"error": %q}`, err)),
@@ -3,7 +3,6 @@ package webkit
3 import (3 import (
4 "context"4 "context"
5 "embed"5 "embed"
6- "encoding/json"
7 "fmt"6 "fmt"
8 "io/fs"7 "io/fs"
9 "net/http"8 "net/http"
@@ -12,9 +11,6 @@ import (
12 "strings"11 "strings"
13 "sync"12 "sync"
14 13
15- "github.com/bnema/dumber/internal/application/port"
16- "github.com/bnema/dumber/internal/infrastructure/config"
17- "github.com/bnema/dumber/internal/infrastructure/env"
18 "github.com/bnema/dumber/internal/infrastructure/webutil"14 "github.com/bnema/dumber/internal/infrastructure/webutil"
19 "github.com/bnema/dumber/internal/logging"15 "github.com/bnema/dumber/internal/logging"
20 "github.com/bnema/puregotk-webkit/soup"16 "github.com/bnema/puregotk-webkit/soup"
@@ -64,13 +60,13 @@ func (f PageHandlerFunc) Handle(req *SchemeRequest) *SchemeResponse {
64 60
65 // DumbSchemeHandler handles dumb:// URI scheme requests.61 // DumbSchemeHandler handles dumb:// URI scheme requests.
66 type DumbSchemeHandler struct {62 type DumbSchemeHandler struct {
67- handlers map[string]PageHandler63+ handlers map[string]PageHandler
68- assets embed.FS64+ assets embed.FS
69- assetDir string // subdirectory within embed.FS (e.g., "assets/webui")65+ assetDir string // subdirectory within embed.FS (e.g., "assets/webui")
70- logger zerolog.Logger66+ logger zerolog.Logger
71- mu sync.RWMutex67+ mu sync.RWMutex
72- hwSurveyor *env.HardwareSurveyor68+ currentConfigPayload func() ([]byte, error)
73- ctx context.Context69+ defaultConfigPayload func() ([]byte, error)
74 }70 }
75 71
76 // NewDumbSchemeHandler creates a new handler for the dumb:// scheme.72 // NewDumbSchemeHandler creates a new handler for the dumb:// scheme.
@@ -78,11 +74,9 @@ func NewDumbSchemeHandler(ctx context.Context) *DumbSchemeHandler {
78 log := logging.FromContext(ctx)74 log := logging.FromContext(ctx)
79 75
80 h := &DumbSchemeHandler{76 h := &DumbSchemeHandler{
81- handlers: make(map[string]PageHandler),77+ handlers: make(map[string]PageHandler),
82- assetDir: "webui",78+ assetDir: "webui",
83- logger: log.With().Str("component", "scheme-handler").Logger(),79+ logger: log.With().Str("component", "scheme-handler").Logger(),
84- hwSurveyor: env.NewHardwareSurveyor(),
85- ctx: ctx,
86 }80 }
87 81
88 // Register default pages82 // Register default pages
@@ -99,6 +93,15 @@ func (h *DumbSchemeHandler) SetAssets(assets embed.FS) {
99 h.logger.Debug().Msg("assets filesystem configured")93 h.logger.Debug().Msg("assets filesystem configured")
100 }94 }
101 95
96+// SetConfigPayloadBuilders wires the config payload builders used by /api/config.
97+func (h *DumbSchemeHandler) SetConfigPayloadBuilders(current, defaultPayload func() ([]byte, error)) {
98+ h.mu.Lock()
99+ defer h.mu.Unlock()
100+ h.currentConfigPayload = current
101+ h.defaultConfigPayload = defaultPayload
102+ h.logger.Debug().Msg("config payload builders configured")
103+}
104+
102 // registerDefaults sets up default page handlers.105 // registerDefaults sets up default page handlers.
103 func (h *DumbSchemeHandler) registerDefaults() {106 func (h *DumbSchemeHandler) registerDefaults() {
104 // Error page (static fallback)107 // Error page (static fallback)
@@ -129,7 +132,10 @@ func (h *DumbSchemeHandler) registerDefaults() {
129 return nil132 return nil
130 }133 }
131 134
132- return h.buildConfigResponse(config.Get())135+ h.mu.RLock()
136+ build := h.currentConfigPayload
137+ h.mu.RUnlock()
138+ return h.buildConfigResponse(build)
133 }))139 }))
134 140
135 // API: Get default config (used by Reset Defaults in dumb://config)141 // API: Get default config (used by Reset Defaults in dumb://config)
@@ -138,7 +144,10 @@ func (h *DumbSchemeHandler) registerDefaults() {
138 return nil144 return nil
139 }145 }
140 146
141- return h.buildConfigResponse(config.DefaultConfig())147+ h.mu.RLock()
148+ build := h.defaultConfigPayload
149+ h.mu.RUnlock()
150+ return h.buildConfigResponse(build)
142 }))151 }))
143 }152 }
144 153
@@ -161,19 +170,16 @@ func buildCrashPageHTML(originalURI string) string {
161 return webutil.BuildCrashPageHTML(originalURI)170 return webutil.BuildCrashPageHTML(originalURI)
162 }171 }
163 172
164-func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeResponse {173+func (h *DumbSchemeHandler) buildConfigResponse(build func() ([]byte, error)) *SchemeResponse {
165- // Get hardware info for display and profile resolution174+ if build == nil {
166- // Use background context since survey results are cached and we don't want175+ return &SchemeResponse{
167- // request context cancellation to affect this176+ Data: []byte(`{"error": "config payload builder not configured"}`),
168- var hw *port.HardwareInfo177+ ContentType: "application/json",
169- if h.hwSurveyor != nil {178+ StatusCode: http.StatusInternalServerError,
170- hwInfo := h.hwSurveyor.Survey(context.Background())179+ }
171- hw = &hwInfo
172 }180 }
173 181
174- resp := config.BuildWebUIConfigPayload(cfg, hw)182+ data, err := build()
175-
176- data, err := json.Marshal(resp)
177 if err != nil {183 if err != nil {
178 return &SchemeResponse{184 return &SchemeResponse{
179 Data: []byte(fmt.Sprintf(`{"error": %q}`, err)),185 Data: []byte(fmt.Sprintf(`{"error": %q}`, err)),
M internal/infrastructure/webkit/scheme_handler_test.go +38 -0
@@ -2,6 +2,7 @@ package webkit
22
33 import (
44 "context"
5+ "net/http"
56 "net/url"
67 "testing"
78
@@ -128,3 +129,40 @@ func TestHandleAsset_ConfigOpaqueFormServesConfigPage(t *testing.T) {
128129 assert.Equal(t, "text/html; charset=utf-8", resp.ContentType)
129130 assert.Contains(t, string(resp.Data), "config")
130131 }
132+
133+func TestConfigHandlersUseInjectedPayloadBuilders(t *testing.T) {
134+ h := NewDumbSchemeHandler(context.Background())
135+ h.SetConfigPayloadBuilders(
136+ func() ([]byte, error) { return []byte(`{"current":true}`), nil },
137+ func() ([]byte, error) { return []byte(`{"default":true}`), nil },
138+ )
139+
140+ h.mu.RLock()
141+ currentHandler := h.handlers["/api/config"]
142+ defaultHandler := h.handlers["/api/config/default"]
143+ h.mu.RUnlock()
144+ require.NotNil(t, currentHandler)
145+ require.NotNil(t, defaultHandler)
146+
147+ currentResp := currentHandler.Handle(&SchemeRequest{
148+ URI: "dumb://config/api/config",
149+ Path: "/api/config",
150+ Method: http.MethodGet,
151+ Scheme: "dumb",
152+ })
153+ require.NotNil(t, currentResp)
154+ assert.Equal(t, http.StatusOK, currentResp.StatusCode)
155+ assert.Equal(t, "application/json", currentResp.ContentType)
156+ assert.Equal(t, []byte(`{"current":true}`), currentResp.Data)
157+
158+ defaultResp := defaultHandler.Handle(&SchemeRequest{
159+ URI: "dumb://config/api/config/default",
160+ Path: "/api/config/default",
161+ Method: http.MethodGet,
162+ Scheme: "dumb",
163+ })
164+ require.NotNil(t, defaultResp)
165+ assert.Equal(t, http.StatusOK, defaultResp.StatusCode)
166+ assert.Equal(t, "application/json", defaultResp.ContentType)
167+ assert.Equal(t, []byte(`{"default":true}`), defaultResp.Data)
168+}
@@ -2,6 +2,7 @@ package webkit
2 2
3 import (3 import (
4 "context"4 "context"
5+ "net/http"
5 "net/url"6 "net/url"
6 "testing"7 "testing"
7 8
@@ -128,3 +129,40 @@ func TestHandleAsset_ConfigOpaqueFormServesConfigPage(t *testing.T) {
128 assert.Equal(t, "text/html; charset=utf-8", resp.ContentType)129 assert.Equal(t, "text/html; charset=utf-8", resp.ContentType)
129 assert.Contains(t, string(resp.Data), "config")130 assert.Contains(t, string(resp.Data), "config")
130 }131 }
132+
133+func TestConfigHandlersUseInjectedPayloadBuilders(t *testing.T) {
134+ h := NewDumbSchemeHandler(context.Background())
135+ h.SetConfigPayloadBuilders(
136+ func() ([]byte, error) { return []byte(`{"current":true}`), nil },
137+ func() ([]byte, error) { return []byte(`{"default":true}`), nil },
138+ )
139+
140+ h.mu.RLock()
141+ currentHandler := h.handlers["/api/config"]
142+ defaultHandler := h.handlers["/api/config/default"]
143+ h.mu.RUnlock()
144+ require.NotNil(t, currentHandler)
145+ require.NotNil(t, defaultHandler)
146+
147+ currentResp := currentHandler.Handle(&SchemeRequest{
148+ URI: "dumb://config/api/config",
149+ Path: "/api/config",
150+ Method: http.MethodGet,
151+ Scheme: "dumb",
152+ })
153+ require.NotNil(t, currentResp)
154+ assert.Equal(t, http.StatusOK, currentResp.StatusCode)
155+ assert.Equal(t, "application/json", currentResp.ContentType)
156+ assert.Equal(t, []byte(`{"current":true}`), currentResp.Data)
157+
158+ defaultResp := defaultHandler.Handle(&SchemeRequest{
159+ URI: "dumb://config/api/config/default",
160+ Path: "/api/config/default",
161+ Method: http.MethodGet,
162+ Scheme: "dumb",
163+ })
164+ require.NotNil(t, defaultResp)
165+ assert.Equal(t, http.StatusOK, defaultResp.StatusCode)
166+ assert.Equal(t, "application/json", defaultResp.ContentType)
167+ assert.Equal(t, []byte(`{"default":true}`), defaultResp.Data)
168+}

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