detached 3a44e8a

refactor(ui): add runtime config snapshot helpers

commit
3a44e8a40c385aea8f103b87e09d05e636c2fcc2
author
brice · · 2026-06-23 14:39 UTC
committer
brice · · 2026-06-23 20:01 UTC
parent
82561bc
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +315 -16
M .mockery.yaml +3 -0
@@ -75,6 +75,9 @@ packages:
7575 NativeWidgetProvider: {}
7676 Notification: {}
7777 RenderingEnvManager: {}
78+ RuntimeConfigProvider:
79+ config:
80+ filename: runtime_config_provider.go
7881 RuntimeVersionProbe: {}
7982 ScriptRefresher: {}
8083 SnapshotService: {}
@@ -75,6 +75,9 @@ packages:
75 NativeWidgetProvider: {}75 NativeWidgetProvider: {}
76 Notification: {}76 Notification: {}
77 RenderingEnvManager: {}77 RenderingEnvManager: {}
78+ RuntimeConfigProvider:
79+ config:
80+ filename: runtime_config_provider.go
78 RuntimeVersionProbe: {}81 RuntimeVersionProbe: {}
79 ScriptRefresher: {}82 ScriptRefresher: {}
80 SnapshotService: {}83 SnapshotService: {}
M cmd/dumber/main.go +4 -2
@@ -865,10 +865,12 @@ func buildUIDependencies(
865865
866866 focusProvider := textinput.NewFocusProvider()
867867 browserLauncher := desktop.NewBrowserLauncher(browserLaunchRelay)
868+ runtimeConfig := bootstrap.NewRuntimeConfigProvider(cfg, config.GetManager())
868869
869870 uiDeps := &ui.Dependencies{
870- Ctx: ctx,
871- Config: cfg,
871+ Ctx: ctx,
872+ Config: cfg,
873+ RuntimeConfig: runtimeConfig,
872874 EngineSettingsPayload: func() port.EngineSettingsPayload {
873875 return bootstrap.EngineSettingsPayloadFromConfig(cfg)
874876 },
@@ -865,10 +865,12 @@ func buildUIDependencies(
865 865
866 focusProvider := textinput.NewFocusProvider()866 focusProvider := textinput.NewFocusProvider()
867 browserLauncher := desktop.NewBrowserLauncher(browserLaunchRelay)867 browserLauncher := desktop.NewBrowserLauncher(browserLaunchRelay)
868+ runtimeConfig := bootstrap.NewRuntimeConfigProvider(cfg, config.GetManager())
868 869
869 uiDeps := &ui.Dependencies{870 uiDeps := &ui.Dependencies{
870- Ctx: ctx,871+ Ctx: ctx,
871- Config: cfg,872+ Config: cfg,
873+ RuntimeConfig: runtimeConfig,
872 EngineSettingsPayload: func() port.EngineSettingsPayload {874 EngineSettingsPayload: func() port.EngineSettingsPayload {
873 return bootstrap.EngineSettingsPayloadFromConfig(cfg)875 return bootstrap.EngineSettingsPayloadFromConfig(cfg)
874 },876 },
A internal/application/port/mocks/runtime_config_provider.go +165 -0
@@ -0,0 +1,165 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "github.com/bnema/dumber/internal/application/port"
9+ mock "github.com/stretchr/testify/mock"
10+)
11+
12+// NewMockRuntimeConfigProvider creates a new instance of MockRuntimeConfigProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
13+// The first argument is typically a *testing.T value.
14+func NewMockRuntimeConfigProvider(t interface {
15+ mock.TestingT
16+ Cleanup(func())
17+}) *MockRuntimeConfigProvider {
18+ mock := &MockRuntimeConfigProvider{}
19+ mock.Mock.Test(t)
20+
21+ t.Cleanup(func() { mock.AssertExpectations(t) })
22+
23+ return mock
24+}
25+
26+// MockRuntimeConfigProvider is an autogenerated mock type for the RuntimeConfigProvider type
27+type MockRuntimeConfigProvider struct {
28+ mock.Mock
29+}
30+
31+type MockRuntimeConfigProvider_Expecter struct {
32+ mock *mock.Mock
33+}
34+
35+func (_m *MockRuntimeConfigProvider) EXPECT() *MockRuntimeConfigProvider_Expecter {
36+ return &MockRuntimeConfigProvider_Expecter{mock: &_m.Mock}
37+}
38+
39+// Current provides a mock function for the type MockRuntimeConfigProvider
40+func (_mock *MockRuntimeConfigProvider) Current() port.RuntimeConfigSnapshot {
41+ ret := _mock.Called()
42+
43+ if len(ret) == 0 {
44+ panic("no return value specified for Current")
45+ }
46+
47+ var r0 port.RuntimeConfigSnapshot
48+ if returnFunc, ok := ret.Get(0).(func() port.RuntimeConfigSnapshot); ok {
49+ r0 = returnFunc()
50+ } else {
51+ r0 = ret.Get(0).(port.RuntimeConfigSnapshot)
52+ }
53+ return r0
54+}
55+
56+// MockRuntimeConfigProvider_Current_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Current'
57+type MockRuntimeConfigProvider_Current_Call struct {
58+ *mock.Call
59+}
60+
61+// Current is a helper method to define mock.On call
62+func (_e *MockRuntimeConfigProvider_Expecter) Current() *MockRuntimeConfigProvider_Current_Call {
63+ return &MockRuntimeConfigProvider_Current_Call{Call: _e.mock.On("Current")}
64+}
65+
66+func (_c *MockRuntimeConfigProvider_Current_Call) Run(run func()) *MockRuntimeConfigProvider_Current_Call {
67+ _c.Call.Run(func(args mock.Arguments) {
68+ run()
69+ })
70+ return _c
71+}
72+
73+func (_c *MockRuntimeConfigProvider_Current_Call) Return(runtimeConfigSnapshot port.RuntimeConfigSnapshot) *MockRuntimeConfigProvider_Current_Call {
74+ _c.Call.Return(runtimeConfigSnapshot)
75+ return _c
76+}
77+
78+func (_c *MockRuntimeConfigProvider_Current_Call) RunAndReturn(run func() port.RuntimeConfigSnapshot) *MockRuntimeConfigProvider_Current_Call {
79+ _c.Call.Return(run)
80+ return _c
81+}
82+
83+// OnChange provides a mock function for the type MockRuntimeConfigProvider
84+func (_mock *MockRuntimeConfigProvider) OnChange(fn func(port.RuntimeConfigSnapshot)) {
85+ _mock.Called(fn)
86+ return
87+}
88+
89+// MockRuntimeConfigProvider_OnChange_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnChange'
90+type MockRuntimeConfigProvider_OnChange_Call struct {
91+ *mock.Call
92+}
93+
94+// OnChange is a helper method to define mock.On call
95+// - fn func(port.RuntimeConfigSnapshot)
96+func (_e *MockRuntimeConfigProvider_Expecter) OnChange(fn any) *MockRuntimeConfigProvider_OnChange_Call {
97+ return &MockRuntimeConfigProvider_OnChange_Call{Call: _e.mock.On("OnChange", fn)}
98+}
99+
100+func (_c *MockRuntimeConfigProvider_OnChange_Call) Run(run func(fn func(port.RuntimeConfigSnapshot))) *MockRuntimeConfigProvider_OnChange_Call {
101+ _c.Call.Run(func(args mock.Arguments) {
102+ var arg0 func(port.RuntimeConfigSnapshot)
103+ if args[0] != nil {
104+ arg0 = args[0].(func(port.RuntimeConfigSnapshot))
105+ }
106+ run(
107+ arg0,
108+ )
109+ })
110+ return _c
111+}
112+
113+func (_c *MockRuntimeConfigProvider_OnChange_Call) Return() *MockRuntimeConfigProvider_OnChange_Call {
114+ _c.Call.Return()
115+ return _c
116+}
117+
118+func (_c *MockRuntimeConfigProvider_OnChange_Call) RunAndReturn(run func(fn func(port.RuntimeConfigSnapshot))) *MockRuntimeConfigProvider_OnChange_Call {
119+ _c.Run(run)
120+ return _c
121+}
122+
123+// Watch provides a mock function for the type MockRuntimeConfigProvider
124+func (_mock *MockRuntimeConfigProvider) Watch() error {
125+ ret := _mock.Called()
126+
127+ if len(ret) == 0 {
128+ panic("no return value specified for Watch")
129+ }
130+
131+ var r0 error
132+ if returnFunc, ok := ret.Get(0).(func() error); ok {
133+ r0 = returnFunc()
134+ } else {
135+ r0 = ret.Error(0)
136+ }
137+ return r0
138+}
139+
140+// MockRuntimeConfigProvider_Watch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Watch'
141+type MockRuntimeConfigProvider_Watch_Call struct {
142+ *mock.Call
143+}
144+
145+// Watch is a helper method to define mock.On call
146+func (_e *MockRuntimeConfigProvider_Expecter) Watch() *MockRuntimeConfigProvider_Watch_Call {
147+ return &MockRuntimeConfigProvider_Watch_Call{Call: _e.mock.On("Watch")}
148+}
149+
150+func (_c *MockRuntimeConfigProvider_Watch_Call) Run(run func()) *MockRuntimeConfigProvider_Watch_Call {
151+ _c.Call.Run(func(args mock.Arguments) {
152+ run()
153+ })
154+ return _c
155+}
156+
157+func (_c *MockRuntimeConfigProvider_Watch_Call) Return(err error) *MockRuntimeConfigProvider_Watch_Call {
158+ _c.Call.Return(err)
159+ return _c
160+}
161+
162+func (_c *MockRuntimeConfigProvider_Watch_Call) RunAndReturn(run func() error) *MockRuntimeConfigProvider_Watch_Call {
163+ _c.Call.Return(run)
164+ return _c
165+}
@@ -0,0 +1,165 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "github.com/bnema/dumber/internal/application/port"
9+ mock "github.com/stretchr/testify/mock"
10+)
11+
12+// NewMockRuntimeConfigProvider creates a new instance of MockRuntimeConfigProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
13+// The first argument is typically a *testing.T value.
14+func NewMockRuntimeConfigProvider(t interface {
15+ mock.TestingT
16+ Cleanup(func())
17+}) *MockRuntimeConfigProvider {
18+ mock := &MockRuntimeConfigProvider{}
19+ mock.Mock.Test(t)
20+
21+ t.Cleanup(func() { mock.AssertExpectations(t) })
22+
23+ return mock
24+}
25+
26+// MockRuntimeConfigProvider is an autogenerated mock type for the RuntimeConfigProvider type
27+type MockRuntimeConfigProvider struct {
28+ mock.Mock
29+}
30+
31+type MockRuntimeConfigProvider_Expecter struct {
32+ mock *mock.Mock
33+}
34+
35+func (_m *MockRuntimeConfigProvider) EXPECT() *MockRuntimeConfigProvider_Expecter {
36+ return &MockRuntimeConfigProvider_Expecter{mock: &_m.Mock}
37+}
38+
39+// Current provides a mock function for the type MockRuntimeConfigProvider
40+func (_mock *MockRuntimeConfigProvider) Current() port.RuntimeConfigSnapshot {
41+ ret := _mock.Called()
42+
43+ if len(ret) == 0 {
44+ panic("no return value specified for Current")
45+ }
46+
47+ var r0 port.RuntimeConfigSnapshot
48+ if returnFunc, ok := ret.Get(0).(func() port.RuntimeConfigSnapshot); ok {
49+ r0 = returnFunc()
50+ } else {
51+ r0 = ret.Get(0).(port.RuntimeConfigSnapshot)
52+ }
53+ return r0
54+}
55+
56+// MockRuntimeConfigProvider_Current_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Current'
57+type MockRuntimeConfigProvider_Current_Call struct {
58+ *mock.Call
59+}
60+
61+// Current is a helper method to define mock.On call
62+func (_e *MockRuntimeConfigProvider_Expecter) Current() *MockRuntimeConfigProvider_Current_Call {
63+ return &MockRuntimeConfigProvider_Current_Call{Call: _e.mock.On("Current")}
64+}
65+
66+func (_c *MockRuntimeConfigProvider_Current_Call) Run(run func()) *MockRuntimeConfigProvider_Current_Call {
67+ _c.Call.Run(func(args mock.Arguments) {
68+ run()
69+ })
70+ return _c
71+}
72+
73+func (_c *MockRuntimeConfigProvider_Current_Call) Return(runtimeConfigSnapshot port.RuntimeConfigSnapshot) *MockRuntimeConfigProvider_Current_Call {
74+ _c.Call.Return(runtimeConfigSnapshot)
75+ return _c
76+}
77+
78+func (_c *MockRuntimeConfigProvider_Current_Call) RunAndReturn(run func() port.RuntimeConfigSnapshot) *MockRuntimeConfigProvider_Current_Call {
79+ _c.Call.Return(run)
80+ return _c
81+}
82+
83+// OnChange provides a mock function for the type MockRuntimeConfigProvider
84+func (_mock *MockRuntimeConfigProvider) OnChange(fn func(port.RuntimeConfigSnapshot)) {
85+ _mock.Called(fn)
86+ return
87+}
88+
89+// MockRuntimeConfigProvider_OnChange_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnChange'
90+type MockRuntimeConfigProvider_OnChange_Call struct {
91+ *mock.Call
92+}
93+
94+// OnChange is a helper method to define mock.On call
95+// - fn func(port.RuntimeConfigSnapshot)
96+func (_e *MockRuntimeConfigProvider_Expecter) OnChange(fn any) *MockRuntimeConfigProvider_OnChange_Call {
97+ return &MockRuntimeConfigProvider_OnChange_Call{Call: _e.mock.On("OnChange", fn)}
98+}
99+
100+func (_c *MockRuntimeConfigProvider_OnChange_Call) Run(run func(fn func(port.RuntimeConfigSnapshot))) *MockRuntimeConfigProvider_OnChange_Call {
101+ _c.Call.Run(func(args mock.Arguments) {
102+ var arg0 func(port.RuntimeConfigSnapshot)
103+ if args[0] != nil {
104+ arg0 = args[0].(func(port.RuntimeConfigSnapshot))
105+ }
106+ run(
107+ arg0,
108+ )
109+ })
110+ return _c
111+}
112+
113+func (_c *MockRuntimeConfigProvider_OnChange_Call) Return() *MockRuntimeConfigProvider_OnChange_Call {
114+ _c.Call.Return()
115+ return _c
116+}
117+
118+func (_c *MockRuntimeConfigProvider_OnChange_Call) RunAndReturn(run func(fn func(port.RuntimeConfigSnapshot))) *MockRuntimeConfigProvider_OnChange_Call {
119+ _c.Run(run)
120+ return _c
121+}
122+
123+// Watch provides a mock function for the type MockRuntimeConfigProvider
124+func (_mock *MockRuntimeConfigProvider) Watch() error {
125+ ret := _mock.Called()
126+
127+ if len(ret) == 0 {
128+ panic("no return value specified for Watch")
129+ }
130+
131+ var r0 error
132+ if returnFunc, ok := ret.Get(0).(func() error); ok {
133+ r0 = returnFunc()
134+ } else {
135+ r0 = ret.Error(0)
136+ }
137+ return r0
138+}
139+
140+// MockRuntimeConfigProvider_Watch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Watch'
141+type MockRuntimeConfigProvider_Watch_Call struct {
142+ *mock.Call
143+}
144+
145+// Watch is a helper method to define mock.On call
146+func (_e *MockRuntimeConfigProvider_Expecter) Watch() *MockRuntimeConfigProvider_Watch_Call {
147+ return &MockRuntimeConfigProvider_Watch_Call{Call: _e.mock.On("Watch")}
148+}
149+
150+func (_c *MockRuntimeConfigProvider_Watch_Call) Run(run func()) *MockRuntimeConfigProvider_Watch_Call {
151+ _c.Call.Run(func(args mock.Arguments) {
152+ run()
153+ })
154+ return _c
155+}
156+
157+func (_c *MockRuntimeConfigProvider_Watch_Call) Return(err error) *MockRuntimeConfigProvider_Watch_Call {
158+ _c.Call.Return(err)
159+ return _c
160+}
161+
162+func (_c *MockRuntimeConfigProvider_Watch_Call) RunAndReturn(run func() error) *MockRuntimeConfigProvider_Watch_Call {
163+ _c.Call.Return(run)
164+ return _c
165+}
M internal/ui/app.go +53 -8
@@ -78,6 +78,10 @@ type App struct {
7878 gtkApp *gtk.Application
7979 mainWindow *window.MainWindow
8080
81+ runtimeConfig port.RuntimeConfigSnapshot
82+ runtimeConfigLoaded bool
83+ popupBrowsingContexts entity.BrowsingContextConfig
84+
8185 browserWindows map[string]*browserWindow
8286 browserWindowOrder []string // registration order of window IDs
8387 lastFocusedWindowID string
@@ -181,16 +185,20 @@ func New(deps *Dependencies) (*App, error) {
181185 return nil, err
182186 }
183187 ctx, cancel := context.WithCancelCause(deps.Ctx)
188+ initialRuntimeConfig := deps.RuntimeConfig.Current()
184189
185190 app := &App{
186- deps: deps,
187- tabs: entity.NewTabList(),
188- tabsUC: deps.TabsUC,
189- panesUC: deps.PanesUC,
190- workspaceViews: make(map[entity.TabID]*component.WorkspaceView),
191- windowForTab: make(map[entity.TabID]*browserWindow),
192- floatingSessions: make(map[floatingSessionKey]*floatingWorkspaceSession),
193- browserWindows: make(map[string]*browserWindow),
191+ deps: deps,
192+ runtimeConfig: initialRuntimeConfig,
193+ runtimeConfigLoaded: true,
194+ popupBrowsingContexts: initialRuntimeConfig.UI.Workspace.BrowsingContexts,
195+ tabs: entity.NewTabList(),
196+ tabsUC: deps.TabsUC,
197+ panesUC: deps.PanesUC,
198+ workspaceViews: make(map[entity.TabID]*component.WorkspaceView),
199+ windowForTab: make(map[entity.TabID]*browserWindow),
200+ floatingSessions: make(map[floatingSessionKey]*floatingWorkspaceSession),
201+ browserWindows: make(map[string]*browserWindow),
194202 dispatchOnMainThread: func(label string, fn func()) syncdispatch.SyncDispatchResult {
195203 if fn != nil {
196204 fn()
@@ -255,6 +263,43 @@ func New(deps *Dependencies) (*App, error) {
255263 return app, nil
256264 }
257265
266+func (a *App) runtimeConfigSnapshot() port.RuntimeConfigSnapshot {
267+ if a == nil {
268+ return port.RuntimeConfigSnapshot{}
269+ }
270+ if a.runtimeConfigLoaded {
271+ return a.runtimeConfig
272+ }
273+ if a.deps != nil && a.deps.RuntimeConfig != nil {
274+ return a.deps.RuntimeConfig.Current()
275+ }
276+ return port.RuntimeConfigSnapshot{}
277+}
278+
279+func (a *App) updateRuntimeConfig(snapshot port.RuntimeConfigSnapshot) {
280+ if a != nil {
281+ a.runtimeConfig = snapshot
282+ a.runtimeConfigLoaded = true
283+ a.updatePopupRuntimeConfig(snapshot.UI.Workspace.BrowsingContexts)
284+ }
285+}
286+
287+func (a *App) updatePopupRuntimeConfig(cfg entity.BrowsingContextConfig) {
288+ if a != nil {
289+ a.popupBrowsingContexts = cfg
290+ }
291+}
292+
293+func (a *App) popupBrowsingContextConfig() *entity.BrowsingContextConfig {
294+ if a == nil {
295+ return nil
296+ }
297+ if !a.runtimeConfigLoaded {
298+ a.updatePopupRuntimeConfig(a.runtimeConfigSnapshot().UI.Workspace.BrowsingContexts)
299+ }
300+ return &a.popupBrowsingContexts
301+}
302+
258303 // Run starts the GTK application and blocks until it exits.
259304 // Returns the exit code.
260305 func (a *App) Run(ctx context.Context, args []string) int {
@@ -78,6 +78,10 @@ type App struct {
78 gtkApp *gtk.Application78 gtkApp *gtk.Application
79 mainWindow *window.MainWindow79 mainWindow *window.MainWindow
80 80
81+ runtimeConfig port.RuntimeConfigSnapshot
82+ runtimeConfigLoaded bool
83+ popupBrowsingContexts entity.BrowsingContextConfig
84+
81 browserWindows map[string]*browserWindow85 browserWindows map[string]*browserWindow
82 browserWindowOrder []string // registration order of window IDs86 browserWindowOrder []string // registration order of window IDs
83 lastFocusedWindowID string87 lastFocusedWindowID string
@@ -181,16 +185,20 @@ func New(deps *Dependencies) (*App, error) {
181 return nil, err185 return nil, err
182 }186 }
183 ctx, cancel := context.WithCancelCause(deps.Ctx)187 ctx, cancel := context.WithCancelCause(deps.Ctx)
188+ initialRuntimeConfig := deps.RuntimeConfig.Current()
184 189
185 app := &App{190 app := &App{
186- deps: deps,191+ deps: deps,
187- tabs: entity.NewTabList(),192+ runtimeConfig: initialRuntimeConfig,
188- tabsUC: deps.TabsUC,193+ runtimeConfigLoaded: true,
189- panesUC: deps.PanesUC,194+ popupBrowsingContexts: initialRuntimeConfig.UI.Workspace.BrowsingContexts,
190- workspaceViews: make(map[entity.TabID]*component.WorkspaceView),195+ tabs: entity.NewTabList(),
191- windowForTab: make(map[entity.TabID]*browserWindow),196+ tabsUC: deps.TabsUC,
192- floatingSessions: make(map[floatingSessionKey]*floatingWorkspaceSession),197+ panesUC: deps.PanesUC,
193- browserWindows: make(map[string]*browserWindow),198+ workspaceViews: make(map[entity.TabID]*component.WorkspaceView),
199+ windowForTab: make(map[entity.TabID]*browserWindow),
200+ floatingSessions: make(map[floatingSessionKey]*floatingWorkspaceSession),
201+ browserWindows: make(map[string]*browserWindow),
194 dispatchOnMainThread: func(label string, fn func()) syncdispatch.SyncDispatchResult {202 dispatchOnMainThread: func(label string, fn func()) syncdispatch.SyncDispatchResult {
195 if fn != nil {203 if fn != nil {
196 fn()204 fn()
@@ -255,6 +263,43 @@ func New(deps *Dependencies) (*App, error) {
255 return app, nil263 return app, nil
256 }264 }
257 265
266+func (a *App) runtimeConfigSnapshot() port.RuntimeConfigSnapshot {
267+ if a == nil {
268+ return port.RuntimeConfigSnapshot{}
269+ }
270+ if a.runtimeConfigLoaded {
271+ return a.runtimeConfig
272+ }
273+ if a.deps != nil && a.deps.RuntimeConfig != nil {
274+ return a.deps.RuntimeConfig.Current()
275+ }
276+ return port.RuntimeConfigSnapshot{}
277+}
278+
279+func (a *App) updateRuntimeConfig(snapshot port.RuntimeConfigSnapshot) {
280+ if a != nil {
281+ a.runtimeConfig = snapshot
282+ a.runtimeConfigLoaded = true
283+ a.updatePopupRuntimeConfig(snapshot.UI.Workspace.BrowsingContexts)
284+ }
285+}
286+
287+func (a *App) updatePopupRuntimeConfig(cfg entity.BrowsingContextConfig) {
288+ if a != nil {
289+ a.popupBrowsingContexts = cfg
290+ }
291+}
292+
293+func (a *App) popupBrowsingContextConfig() *entity.BrowsingContextConfig {
294+ if a == nil {
295+ return nil
296+ }
297+ if !a.runtimeConfigLoaded {
298+ a.updatePopupRuntimeConfig(a.runtimeConfigSnapshot().UI.Workspace.BrowsingContexts)
299+ }
300+ return &a.popupBrowsingContexts
301+}
302+
258 // Run starts the GTK application and blocks until it exits.303 // Run starts the GTK application and blocks until it exits.
259 // Returns the exit code.304 // Returns the exit code.
260 func (a *App) Run(ctx context.Context, args []string) int {305 func (a *App) Run(ctx context.Context, args []string) int {
M internal/ui/app_clipboard_test.go +18 -6
@@ -23,11 +23,17 @@ func TestNew_DoesNotWireClipboardOrchestratorWhenClipboardMissing(t *testing.T)
2323 }).
2424 Return(nil).
2525 Once()
26+ runtimeConfig := portmocks.NewMockRuntimeConfigProvider(t)
27+ runtimeConfig.EXPECT().
28+ Current().
29+ Return(port.RuntimeConfigSnapshot{}).
30+ Once()
2631
2732 deps := &Dependencies{
28- Ctx: ctx,
29- Config: &config.Config{},
30- Engine: engine,
33+ Ctx: ctx,
34+ Config: &config.Config{},
35+ RuntimeConfig: runtimeConfig,
36+ Engine: engine,
3137 HandlerDeps: port.HandlerDeps{
3238 SaveConfig: func(context.Context, dto.WebUIConfig) error { return nil },
3339 SaveOmniboxInitialBehavior: func(context.Context, entity.OmniboxInitialBehavior) error { return nil },
@@ -52,11 +58,17 @@ func TestNew_WiresClipboardOrchestratorWhenClipboardPresent(t *testing.T) {
5258 }).
5359 Return(nil).
5460 Once()
61+ runtimeConfig := portmocks.NewMockRuntimeConfigProvider(t)
62+ runtimeConfig.EXPECT().
63+ Current().
64+ Return(port.RuntimeConfigSnapshot{}).
65+ Once()
5566
5667 deps := &Dependencies{
57- Ctx: ctx,
58- Config: &config.Config{},
59- Engine: engine,
68+ Ctx: ctx,
69+ Config: &config.Config{},
70+ RuntimeConfig: runtimeConfig,
71+ Engine: engine,
6072 HandlerDeps: port.HandlerDeps{
6173 SaveConfig: func(context.Context, dto.WebUIConfig) error { return nil },
6274 SaveOmniboxInitialBehavior: func(context.Context, entity.OmniboxInitialBehavior) error { return nil },
@@ -23,11 +23,17 @@ func TestNew_DoesNotWireClipboardOrchestratorWhenClipboardMissing(t *testing.T)
23 }).23 }).
24 Return(nil).24 Return(nil).
25 Once()25 Once()
26+ runtimeConfig := portmocks.NewMockRuntimeConfigProvider(t)
27+ runtimeConfig.EXPECT().
28+ Current().
29+ Return(port.RuntimeConfigSnapshot{}).
30+ Once()
26 31
27 deps := &Dependencies{32 deps := &Dependencies{
28- Ctx: ctx,33+ Ctx: ctx,
29- Config: &config.Config{},34+ Config: &config.Config{},
30- Engine: engine,35+ RuntimeConfig: runtimeConfig,
36+ Engine: engine,
31 HandlerDeps: port.HandlerDeps{37 HandlerDeps: port.HandlerDeps{
32 SaveConfig: func(context.Context, dto.WebUIConfig) error { return nil },38 SaveConfig: func(context.Context, dto.WebUIConfig) error { return nil },
33 SaveOmniboxInitialBehavior: func(context.Context, entity.OmniboxInitialBehavior) error { return nil },39 SaveOmniboxInitialBehavior: func(context.Context, entity.OmniboxInitialBehavior) error { return nil },
@@ -52,11 +58,17 @@ func TestNew_WiresClipboardOrchestratorWhenClipboardPresent(t *testing.T) {
52 }).58 }).
53 Return(nil).59 Return(nil).
54 Once()60 Once()
61+ runtimeConfig := portmocks.NewMockRuntimeConfigProvider(t)
62+ runtimeConfig.EXPECT().
63+ Current().
64+ Return(port.RuntimeConfigSnapshot{}).
65+ Once()
55 66
56 deps := &Dependencies{67 deps := &Dependencies{
57- Ctx: ctx,68+ Ctx: ctx,
58- Config: &config.Config{},69+ Config: &config.Config{},
59- Engine: engine,70+ RuntimeConfig: runtimeConfig,
71+ Engine: engine,
60 HandlerDeps: port.HandlerDeps{72 HandlerDeps: port.HandlerDeps{
61 SaveConfig: func(context.Context, dto.WebUIConfig) error { return nil },73 SaveConfig: func(context.Context, dto.WebUIConfig) error { return nil },
62 SaveOmniboxInitialBehavior: func(context.Context, entity.OmniboxInitialBehavior) error { return nil },74 SaveOmniboxInitialBehavior: func(context.Context, entity.OmniboxInitialBehavior) error { return nil },
M internal/ui/deps.go +4 -0
@@ -20,6 +20,7 @@ type Dependencies struct {
2020 // Core context and configuration
2121 Ctx context.Context
2222 Config *config.Config // TODO: replace with port interface in a later milestone
23+ RuntimeConfig port.RuntimeConfigProvider
2324 EngineSettingsPayload func() port.EngineSettingsPayload
2425 InitialURL string // URL to open on startup (optional)
2526 RestoreSessionID string // Session ID to restore on startup (optional)
@@ -123,6 +124,9 @@ func (d *Dependencies) Validate() error {
123124 if d.Config == nil {
124125 return ErrMissingDependency("Config")
125126 }
127+ if d.RuntimeConfig == nil {
128+ return ErrMissingDependency("RuntimeConfig")
129+ }
126130 if d.Engine == nil {
127131 return ErrMissingDependency("Engine")
128132 }
@@ -20,6 +20,7 @@ type Dependencies struct {
20 // Core context and configuration20 // Core context and configuration
21 Ctx context.Context21 Ctx context.Context
22 Config *config.Config // TODO: replace with port interface in a later milestone22 Config *config.Config // TODO: replace with port interface in a later milestone
23+ RuntimeConfig port.RuntimeConfigProvider
23 EngineSettingsPayload func() port.EngineSettingsPayload24 EngineSettingsPayload func() port.EngineSettingsPayload
24 InitialURL string // URL to open on startup (optional)25 InitialURL string // URL to open on startup (optional)
25 RestoreSessionID string // Session ID to restore on startup (optional)26 RestoreSessionID string // Session ID to restore on startup (optional)
@@ -123,6 +124,9 @@ func (d *Dependencies) Validate() error {
123 if d.Config == nil {124 if d.Config == nil {
124 return ErrMissingDependency("Config")125 return ErrMissingDependency("Config")
125 }126 }
127+ if d.RuntimeConfig == nil {
128+ return ErrMissingDependency("RuntimeConfig")
129+ }
126 if d.Engine == nil {130 if d.Engine == nil {
127 return ErrMissingDependency("Engine")131 return ErrMissingDependency("Engine")
128 }132 }
A internal/ui/runtime_config_test.go +68 -0
@@ -0,0 +1,68 @@
1+package ui
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/dumber/internal/application/port"
7+ portmocks "github.com/bnema/dumber/internal/application/port/mocks"
8+ "github.com/bnema/dumber/internal/domain/entity"
9+)
10+
11+func TestAppRuntimeConfigReturnsProviderSnapshot(t *testing.T) {
12+ provider := portmocks.NewMockRuntimeConfigProvider(t)
13+ provider.EXPECT().
14+ Current().
15+ Return(port.RuntimeConfigSnapshot{
16+ UI: port.RuntimeUIConfig{DefaultUIScale: 1.6},
17+ }).
18+ Once()
19+ app := &App{deps: &Dependencies{RuntimeConfig: provider}}
20+
21+ got := app.runtimeConfigSnapshot()
22+
23+ if got.UI.DefaultUIScale != 1.6 {
24+ t.Fatalf("DefaultUIScale=%v, want 1.6", got.UI.DefaultUIScale)
25+ }
26+}
27+
28+func TestAppUpdateRuntimeConfigStoresLatestSnapshot(t *testing.T) {
29+ app := &App{}
30+ app.updateRuntimeConfig(port.RuntimeConfigSnapshot{
31+ UI: port.RuntimeUIConfig{SidebarWidth: 340},
32+ })
33+
34+ got := app.runtimeConfigSnapshot()
35+ if got.UI.SidebarWidth != 340 {
36+ t.Fatalf("SidebarWidth=%d, want 340", got.UI.SidebarWidth)
37+ }
38+}
39+
40+func TestAppUpdateRuntimeConfigRefreshesStablePopupConfigPointer(t *testing.T) {
41+ app := &App{}
42+ app.updateRuntimeConfig(port.RuntimeConfigSnapshot{
43+ UI: port.RuntimeUIConfig{
44+ Workspace: entity.WorkspaceConfig{
45+ BrowsingContexts: entity.BrowsingContextConfig{OpenInNewPane: true},
46+ },
47+ },
48+ })
49+ ptr := app.popupBrowsingContextConfig()
50+
51+ app.updateRuntimeConfig(port.RuntimeConfigSnapshot{
52+ UI: port.RuntimeUIConfig{
53+ Workspace: entity.WorkspaceConfig{
54+ BrowsingContexts: entity.BrowsingContextConfig{OpenInNewPane: false, OAuthAutoClose: true},
55+ },
56+ },
57+ })
58+
59+ if ptr != app.popupBrowsingContextConfig() {
60+ t.Fatal("popup config pointer must remain stable for content coordinator")
61+ }
62+ if ptr.OpenInNewPane {
63+ t.Fatal("popup config pointer did not observe updated OpenInNewPane")
64+ }
65+ if !ptr.OAuthAutoClose {
66+ t.Fatal("popup config pointer did not observe updated OAuthAutoClose")
67+ }
68+}
@@ -0,0 +1,68 @@
1+package ui
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/dumber/internal/application/port"
7+ portmocks "github.com/bnema/dumber/internal/application/port/mocks"
8+ "github.com/bnema/dumber/internal/domain/entity"
9+)
10+
11+func TestAppRuntimeConfigReturnsProviderSnapshot(t *testing.T) {
12+ provider := portmocks.NewMockRuntimeConfigProvider(t)
13+ provider.EXPECT().
14+ Current().
15+ Return(port.RuntimeConfigSnapshot{
16+ UI: port.RuntimeUIConfig{DefaultUIScale: 1.6},
17+ }).
18+ Once()
19+ app := &App{deps: &Dependencies{RuntimeConfig: provider}}
20+
21+ got := app.runtimeConfigSnapshot()
22+
23+ if got.UI.DefaultUIScale != 1.6 {
24+ t.Fatalf("DefaultUIScale=%v, want 1.6", got.UI.DefaultUIScale)
25+ }
26+}
27+
28+func TestAppUpdateRuntimeConfigStoresLatestSnapshot(t *testing.T) {
29+ app := &App{}
30+ app.updateRuntimeConfig(port.RuntimeConfigSnapshot{
31+ UI: port.RuntimeUIConfig{SidebarWidth: 340},
32+ })
33+
34+ got := app.runtimeConfigSnapshot()
35+ if got.UI.SidebarWidth != 340 {
36+ t.Fatalf("SidebarWidth=%d, want 340", got.UI.SidebarWidth)
37+ }
38+}
39+
40+func TestAppUpdateRuntimeConfigRefreshesStablePopupConfigPointer(t *testing.T) {
41+ app := &App{}
42+ app.updateRuntimeConfig(port.RuntimeConfigSnapshot{
43+ UI: port.RuntimeUIConfig{
44+ Workspace: entity.WorkspaceConfig{
45+ BrowsingContexts: entity.BrowsingContextConfig{OpenInNewPane: true},
46+ },
47+ },
48+ })
49+ ptr := app.popupBrowsingContextConfig()
50+
51+ app.updateRuntimeConfig(port.RuntimeConfigSnapshot{
52+ UI: port.RuntimeUIConfig{
53+ Workspace: entity.WorkspaceConfig{
54+ BrowsingContexts: entity.BrowsingContextConfig{OpenInNewPane: false, OAuthAutoClose: true},
55+ },
56+ },
57+ })
58+
59+ if ptr != app.popupBrowsingContextConfig() {
60+ t.Fatal("popup config pointer must remain stable for content coordinator")
61+ }
62+ if ptr.OpenInNewPane {
63+ t.Fatal("popup config pointer did not observe updated OpenInNewPane")
64+ }
65+ if !ptr.OAuthAutoClose {
66+ t.Fatal("popup config pointer did not observe updated OAuthAutoClose")
67+ }
68+}

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