detached aebb5c1

refactor(config): add runtime config snapshot

commit
aebb5c187c6bcfe76e51334e2598b6812a4ed5e6
author
brice · · 2026-06-23 13:59 UTC
committer
brice · · 2026-06-23 20:01 UTC
parent
84747b2
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +142 -0
A internal/application/port/runtime_config.go +53 -0
@@ -0,0 +1,53 @@
1+package port
2+
3+import "github.com/bnema/dumber/internal/domain/entity"
4+
5+type RuntimeConfigProvider interface {
6+ Current() RuntimeConfigSnapshot
7+ Watch() error
8+ OnChange(func(RuntimeConfigSnapshot))
9+}
10+
11+type RuntimeConfigSnapshot struct {
12+ EngineSettings EngineSettingsPayload
13+ UI RuntimeUIConfig
14+}
15+
16+type RuntimeUIConfig struct {
17+ DefaultUIScale float64
18+ SidebarWidth int
19+ Appearance entity.AppearanceConfig
20+ Workspace entity.WorkspaceConfig
21+ Session entity.SessionConfig
22+ Clipboard RuntimeClipboardConfig
23+ SearchShortcuts map[string]RuntimeSearchShortcut
24+ DefaultSearchEngine string
25+ Omnibox RuntimeOmniboxConfig
26+ Update RuntimeUpdateConfig
27+ Downloads RuntimeDownloadsConfig
28+}
29+
30+type RuntimeClipboardConfig struct {
31+ AutoCopyOnSelection bool
32+}
33+
34+type RuntimeSearchShortcut struct {
35+ URL string
36+ Description string
37+}
38+
39+type RuntimeOmniboxConfig struct {
40+ InitialBehavior entity.OmniboxInitialBehavior
41+ MostVisitedDays int
42+ AutoOpenOnNewPane bool
43+}
44+
45+type RuntimeUpdateConfig struct {
46+ EnableOnStartup bool
47+ AutoDownload bool
48+ NotifyOnNewSettings bool
49+}
50+
51+type RuntimeDownloadsConfig struct {
52+ Path string
53+}
@@ -0,0 +1,53 @@
1+package port
2+
3+import "github.com/bnema/dumber/internal/domain/entity"
4+
5+type RuntimeConfigProvider interface {
6+ Current() RuntimeConfigSnapshot
7+ Watch() error
8+ OnChange(func(RuntimeConfigSnapshot))
9+}
10+
11+type RuntimeConfigSnapshot struct {
12+ EngineSettings EngineSettingsPayload
13+ UI RuntimeUIConfig
14+}
15+
16+type RuntimeUIConfig struct {
17+ DefaultUIScale float64
18+ SidebarWidth int
19+ Appearance entity.AppearanceConfig
20+ Workspace entity.WorkspaceConfig
21+ Session entity.SessionConfig
22+ Clipboard RuntimeClipboardConfig
23+ SearchShortcuts map[string]RuntimeSearchShortcut
24+ DefaultSearchEngine string
25+ Omnibox RuntimeOmniboxConfig
26+ Update RuntimeUpdateConfig
27+ Downloads RuntimeDownloadsConfig
28+}
29+
30+type RuntimeClipboardConfig struct {
31+ AutoCopyOnSelection bool
32+}
33+
34+type RuntimeSearchShortcut struct {
35+ URL string
36+ Description string
37+}
38+
39+type RuntimeOmniboxConfig struct {
40+ InitialBehavior entity.OmniboxInitialBehavior
41+ MostVisitedDays int
42+ AutoOpenOnNewPane bool
43+}
44+
45+type RuntimeUpdateConfig struct {
46+ EnableOnStartup bool
47+ AutoDownload bool
48+ NotifyOnNewSettings bool
49+}
50+
51+type RuntimeDownloadsConfig struct {
52+ Path string
53+}
M internal/bootstrap/runtime_config.go +41 -0
@@ -24,6 +24,47 @@ func EngineSettingsPayloadFromConfig(cfg *config.Config) port.EngineSettingsPayl
2424 }
2525 }
2626
27+func RuntimeConfigSnapshotFromConfig(cfg *config.Config) port.RuntimeConfigSnapshot {
28+ if cfg == nil {
29+ return port.RuntimeConfigSnapshot{}
30+ }
31+ return port.RuntimeConfigSnapshot{
32+ EngineSettings: EngineSettingsPayloadFromConfig(cfg),
33+ UI: port.RuntimeUIConfig{
34+ DefaultUIScale: cfg.DefaultUIScale,
35+ SidebarWidth: cfg.SidebarWidth,
36+ Appearance: cfg.Appearance,
37+ Workspace: cfg.Workspace,
38+ Session: cfg.Session,
39+ Clipboard: port.RuntimeClipboardConfig{AutoCopyOnSelection: cfg.Clipboard.AutoCopyOnSelection},
40+ SearchShortcuts: runtimeSearchShortcutsFromConfig(cfg.SearchShortcuts),
41+ DefaultSearchEngine: cfg.DefaultSearchEngine,
42+ Omnibox: port.RuntimeOmniboxConfig{
43+ InitialBehavior: cfg.Omnibox.InitialBehavior,
44+ MostVisitedDays: cfg.Omnibox.MostVisitedDays,
45+ AutoOpenOnNewPane: cfg.Omnibox.AutoOpenOnNewPane,
46+ },
47+ Update: port.RuntimeUpdateConfig{
48+ EnableOnStartup: cfg.Update.EnableOnStartup,
49+ AutoDownload: cfg.Update.AutoDownload,
50+ NotifyOnNewSettings: cfg.Update.NotifyOnNewSettings,
51+ },
52+ Downloads: port.RuntimeDownloadsConfig{Path: cfg.Downloads.Path},
53+ },
54+ }
55+}
56+
57+func runtimeSearchShortcutsFromConfig(in map[string]config.SearchShortcut) map[string]port.RuntimeSearchShortcut {
58+ out := make(map[string]port.RuntimeSearchShortcut, len(in))
59+ for key, shortcut := range in {
60+ out[key] = port.RuntimeSearchShortcut{
61+ URL: shortcut.URL,
62+ Description: shortcut.Description,
63+ }
64+ }
65+ return out
66+}
67+
2768 func engineHardwareDecodingModeFromConfig(mode config.HardwareDecodingMode) port.EngineHardwareDecodingMode {
2869 switch mode {
2970 case config.HardwareDecodingForce:
@@ -24,6 +24,47 @@ func EngineSettingsPayloadFromConfig(cfg *config.Config) port.EngineSettingsPayl
24 }24 }
25 }25 }
26 26
27+func RuntimeConfigSnapshotFromConfig(cfg *config.Config) port.RuntimeConfigSnapshot {
28+ if cfg == nil {
29+ return port.RuntimeConfigSnapshot{}
30+ }
31+ return port.RuntimeConfigSnapshot{
32+ EngineSettings: EngineSettingsPayloadFromConfig(cfg),
33+ UI: port.RuntimeUIConfig{
34+ DefaultUIScale: cfg.DefaultUIScale,
35+ SidebarWidth: cfg.SidebarWidth,
36+ Appearance: cfg.Appearance,
37+ Workspace: cfg.Workspace,
38+ Session: cfg.Session,
39+ Clipboard: port.RuntimeClipboardConfig{AutoCopyOnSelection: cfg.Clipboard.AutoCopyOnSelection},
40+ SearchShortcuts: runtimeSearchShortcutsFromConfig(cfg.SearchShortcuts),
41+ DefaultSearchEngine: cfg.DefaultSearchEngine,
42+ Omnibox: port.RuntimeOmniboxConfig{
43+ InitialBehavior: cfg.Omnibox.InitialBehavior,
44+ MostVisitedDays: cfg.Omnibox.MostVisitedDays,
45+ AutoOpenOnNewPane: cfg.Omnibox.AutoOpenOnNewPane,
46+ },
47+ Update: port.RuntimeUpdateConfig{
48+ EnableOnStartup: cfg.Update.EnableOnStartup,
49+ AutoDownload: cfg.Update.AutoDownload,
50+ NotifyOnNewSettings: cfg.Update.NotifyOnNewSettings,
51+ },
52+ Downloads: port.RuntimeDownloadsConfig{Path: cfg.Downloads.Path},
53+ },
54+ }
55+}
56+
57+func runtimeSearchShortcutsFromConfig(in map[string]config.SearchShortcut) map[string]port.RuntimeSearchShortcut {
58+ out := make(map[string]port.RuntimeSearchShortcut, len(in))
59+ for key, shortcut := range in {
60+ out[key] = port.RuntimeSearchShortcut{
61+ URL: shortcut.URL,
62+ Description: shortcut.Description,
63+ }
64+ }
65+ return out
66+}
67+
27 func engineHardwareDecodingModeFromConfig(mode config.HardwareDecodingMode) port.EngineHardwareDecodingMode {68 func engineHardwareDecodingModeFromConfig(mode config.HardwareDecodingMode) port.EngineHardwareDecodingMode {
28 switch mode {69 switch mode {
29 case config.HardwareDecodingForce:70 case config.HardwareDecodingForce:
M internal/bootstrap/runtime_config_test.go +48 -0
@@ -1,6 +1,7 @@
11 package bootstrap
22
33 import (
4+ "reflect"
45 "testing"
56
67 "github.com/bnema/dumber/internal/application/port"
@@ -47,6 +48,53 @@ func TestEngineSettingsPayloadFromNilConfigReturnsZeroPayload(t *testing.T) {
4748 }
4849 }
4950
51+func TestRuntimeConfigSnapshotFromConfigMapsUIRuntimeFields(t *testing.T) {
52+ cfg := config.DefaultConfig()
53+ cfg.DefaultUIScale = 1.4
54+ cfg.SidebarWidth = 333
55+ cfg.Downloads.Path = "/tmp/downloads"
56+ cfg.DefaultSearchEngine = "https://search.example/?q=%s"
57+ cfg.SearchShortcuts = map[string]config.SearchShortcut{
58+ "gh": {URL: "https://github.com/search?q=%s", Description: "GitHub"},
59+ }
60+ cfg.Workspace.NewPaneURL = "about:blank"
61+ cfg.Session.SnapshotIntervalMs = 7000
62+ cfg.Clipboard.AutoCopyOnSelection = true
63+ cfg.Omnibox.AutoOpenOnNewPane = true
64+ cfg.Update.NotifyOnNewSettings = true
65+
66+ got := RuntimeConfigSnapshotFromConfig(cfg)
67+
68+ if got.EngineSettings != EngineSettingsPayloadFromConfig(cfg) {
69+ t.Fatalf("EngineSettings=%#v, want %#v", got.EngineSettings, EngineSettingsPayloadFromConfig(cfg))
70+ }
71+ if got.UI.DefaultUIScale != 1.4 ||
72+ got.UI.SidebarWidth != 333 ||
73+ got.UI.Downloads.Path != "/tmp/downloads" ||
74+ got.UI.DefaultSearchEngine != "https://search.example/?q=%s" ||
75+ got.UI.Workspace.NewPaneURL != "about:blank" ||
76+ got.UI.Session.SnapshotIntervalMs != 7000 ||
77+ !got.UI.Clipboard.AutoCopyOnSelection ||
78+ !got.UI.Omnibox.AutoOpenOnNewPane ||
79+ !got.UI.Update.NotifyOnNewSettings {
80+ t.Fatalf("snapshot not mapped: %#v", got.UI)
81+ }
82+ if got.UI.SearchShortcuts["gh"].URL != "https://github.com/search?q=%s" {
83+ t.Fatalf("search shortcut not mapped: %#v", got.UI.SearchShortcuts)
84+ }
85+ got.UI.SearchShortcuts["gh"] = port.RuntimeSearchShortcut{URL: "mutated"}
86+ if cfg.SearchShortcuts["gh"].URL == "mutated" {
87+ t.Fatal("snapshot must deep-copy search shortcut map")
88+ }
89+}
90+
91+func TestRuntimeConfigSnapshotFromNilConfigReturnsZeroSnapshot(t *testing.T) {
92+ got := RuntimeConfigSnapshotFromConfig(nil)
93+ if !reflect.DeepEqual(got, port.RuntimeConfigSnapshot{}) {
94+ t.Fatalf("snapshot=%#v, want zero value", got)
95+ }
96+}
97+
5098 func TestEngineSettingsPayloadFromConfigMapsHardwareDecodingModes(t *testing.T) {
5199 tests := []struct {
52100 name string
@@ -1,6 +1,7 @@
1 package bootstrap1 package bootstrap
2 2
3 import (3 import (
4+ "reflect"
4 "testing"5 "testing"
5 6
6 "github.com/bnema/dumber/internal/application/port"7 "github.com/bnema/dumber/internal/application/port"
@@ -47,6 +48,53 @@ func TestEngineSettingsPayloadFromNilConfigReturnsZeroPayload(t *testing.T) {
47 }48 }
48 }49 }
49 50
51+func TestRuntimeConfigSnapshotFromConfigMapsUIRuntimeFields(t *testing.T) {
52+ cfg := config.DefaultConfig()
53+ cfg.DefaultUIScale = 1.4
54+ cfg.SidebarWidth = 333
55+ cfg.Downloads.Path = "/tmp/downloads"
56+ cfg.DefaultSearchEngine = "https://search.example/?q=%s"
57+ cfg.SearchShortcuts = map[string]config.SearchShortcut{
58+ "gh": {URL: "https://github.com/search?q=%s", Description: "GitHub"},
59+ }
60+ cfg.Workspace.NewPaneURL = "about:blank"
61+ cfg.Session.SnapshotIntervalMs = 7000
62+ cfg.Clipboard.AutoCopyOnSelection = true
63+ cfg.Omnibox.AutoOpenOnNewPane = true
64+ cfg.Update.NotifyOnNewSettings = true
65+
66+ got := RuntimeConfigSnapshotFromConfig(cfg)
67+
68+ if got.EngineSettings != EngineSettingsPayloadFromConfig(cfg) {
69+ t.Fatalf("EngineSettings=%#v, want %#v", got.EngineSettings, EngineSettingsPayloadFromConfig(cfg))
70+ }
71+ if got.UI.DefaultUIScale != 1.4 ||
72+ got.UI.SidebarWidth != 333 ||
73+ got.UI.Downloads.Path != "/tmp/downloads" ||
74+ got.UI.DefaultSearchEngine != "https://search.example/?q=%s" ||
75+ got.UI.Workspace.NewPaneURL != "about:blank" ||
76+ got.UI.Session.SnapshotIntervalMs != 7000 ||
77+ !got.UI.Clipboard.AutoCopyOnSelection ||
78+ !got.UI.Omnibox.AutoOpenOnNewPane ||
79+ !got.UI.Update.NotifyOnNewSettings {
80+ t.Fatalf("snapshot not mapped: %#v", got.UI)
81+ }
82+ if got.UI.SearchShortcuts["gh"].URL != "https://github.com/search?q=%s" {
83+ t.Fatalf("search shortcut not mapped: %#v", got.UI.SearchShortcuts)
84+ }
85+ got.UI.SearchShortcuts["gh"] = port.RuntimeSearchShortcut{URL: "mutated"}
86+ if cfg.SearchShortcuts["gh"].URL == "mutated" {
87+ t.Fatal("snapshot must deep-copy search shortcut map")
88+ }
89+}
90+
91+func TestRuntimeConfigSnapshotFromNilConfigReturnsZeroSnapshot(t *testing.T) {
92+ got := RuntimeConfigSnapshotFromConfig(nil)
93+ if !reflect.DeepEqual(got, port.RuntimeConfigSnapshot{}) {
94+ t.Fatalf("snapshot=%#v, want zero value", got)
95+ }
96+}
97+
50 func TestEngineSettingsPayloadFromConfigMapsHardwareDecodingModes(t *testing.T) {98 func TestEngineSettingsPayloadFromConfigMapsHardwareDecodingModes(t *testing.T) {
51 tests := []struct {99 tests := []struct {
52 name string100 name string

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