detached 3ada45b

refactor(ui): complete runtime config provider migration

commit
3ada45bd412a3ddecd1c4229dc20a81caff50801
author
brice · · 2026-06-23 15:47 UTC
committer
brice · · 2026-06-23 20:01 UTC
parent
ccbb812
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
14 files · +336 -252
M cmd/dumber/main.go +3 -21
@@ -868,12 +868,8 @@ func buildUIDependencies(
868868 runtimeConfig := bootstrap.NewRuntimeConfigProvider(cfg, config.GetManager())
869869
870870 uiDeps := &ui.Dependencies{
871- Ctx: ctx,
872- Config: cfg,
873- RuntimeConfig: runtimeConfig,
874- EngineSettingsPayload: func() port.EngineSettingsPayload {
875- return bootstrap.EngineSettingsPayloadFromConfig(cfg)
876- },
871+ Ctx: ctx,
872+ RuntimeConfig: runtimeConfig,
877873 InitialURL: initialURL,
878874 RestoreSessionID: restoreSessionID,
879875 StartupCrashReports: startupCrashReports,
@@ -927,26 +923,12 @@ func buildUIDependencies(
927923 NewGTKEntryTarget: func(entry *gtk.SearchEntry) port.TextInputTarget {
928924 return textinput.NewGTKEntryTarget(entry)
929925 },
930- OnConfigChange: func(callback func()) {
931- config.GetManager().OnConfigChange(func(newCfg *config.Config) {
932- // No synchronization needed: viper fires this callback on the
933- // GTK main thread (via glib.IdleAdd), and all callers of cfg run
934- // on the same GTK main thread, so there is no concurrent access.
935- if cfg != nil && newCfg != nil {
936- *cfg = *newCfg
937- }
938- callback()
939- })
940- },
941- WatchConfig: func() error {
942- return config.GetManager().Watch()
943- },
944926 LaunchExternalURL: desktop.LaunchExternalURL,
945927 LaunchBrowserURL: func(navCtx context.Context, uri string) error {
946928 return launchStandaloneBrowserURL(navCtx, browserLauncher.LaunchURL, uri)
947929 },
948930 BrowserLaunchRelay: browserLaunchRelay,
949- ConfigMigrator: config.NewMigrator(),
931+ MigrationChecker: config.NewMigrator(),
950932 HandlerDeps: *handlerDeps,
951933 }
952934
@@ -868,12 +868,8 @@ func buildUIDependencies(
868 runtimeConfig := bootstrap.NewRuntimeConfigProvider(cfg, config.GetManager())868 runtimeConfig := bootstrap.NewRuntimeConfigProvider(cfg, config.GetManager())
869 869
870 uiDeps := &ui.Dependencies{870 uiDeps := &ui.Dependencies{
871- Ctx: ctx,871+ Ctx: ctx,
872- Config: cfg,872+ RuntimeConfig: runtimeConfig,
873- RuntimeConfig: runtimeConfig,
874- EngineSettingsPayload: func() port.EngineSettingsPayload {
875- return bootstrap.EngineSettingsPayloadFromConfig(cfg)
876- },
877 InitialURL: initialURL,873 InitialURL: initialURL,
878 RestoreSessionID: restoreSessionID,874 RestoreSessionID: restoreSessionID,
879 StartupCrashReports: startupCrashReports,875 StartupCrashReports: startupCrashReports,
@@ -927,26 +923,12 @@ func buildUIDependencies(
927 NewGTKEntryTarget: func(entry *gtk.SearchEntry) port.TextInputTarget {923 NewGTKEntryTarget: func(entry *gtk.SearchEntry) port.TextInputTarget {
928 return textinput.NewGTKEntryTarget(entry)924 return textinput.NewGTKEntryTarget(entry)
929 },925 },
930- OnConfigChange: func(callback func()) {
931- config.GetManager().OnConfigChange(func(newCfg *config.Config) {
932- // No synchronization needed: viper fires this callback on the
933- // GTK main thread (via glib.IdleAdd), and all callers of cfg run
934- // on the same GTK main thread, so there is no concurrent access.
935- if cfg != nil && newCfg != nil {
936- *cfg = *newCfg
937- }
938- callback()
939- })
940- },
941- WatchConfig: func() error {
942- return config.GetManager().Watch()
943- },
944 LaunchExternalURL: desktop.LaunchExternalURL,926 LaunchExternalURL: desktop.LaunchExternalURL,
945 LaunchBrowserURL: func(navCtx context.Context, uri string) error {927 LaunchBrowserURL: func(navCtx context.Context, uri string) error {
946 return launchStandaloneBrowserURL(navCtx, browserLauncher.LaunchURL, uri)928 return launchStandaloneBrowserURL(navCtx, browserLauncher.LaunchURL, uri)
947 },929 },
948 BrowserLaunchRelay: browserLaunchRelay,930 BrowserLaunchRelay: browserLaunchRelay,
949- ConfigMigrator: config.NewMigrator(),931+ MigrationChecker: config.NewMigrator(),
950 HandlerDeps: *handlerDeps,932 HandlerDeps: *handlerDeps,
951 }933 }
952 934
M internal/ui/app.go +77 -79
@@ -212,10 +212,7 @@ func New(deps *Dependencies) (*App, error) {
212212 var clipboardOrchestrator port.ClipboardTextOrchestrator
213213 if deps.Clipboard != nil {
214214 autoCopyConfig = &autoCopyConfigFn{fn: func() bool {
215- if deps.Config == nil {
216- return false
217- }
218- return deps.Config.Clipboard.AutoCopyOnSelection
215+ return app.runtimeConfigSnapshot().UI.Clipboard.AutoCopyOnSelection
219216 }}
220217 clipboardOrchestrator = usecase.NewClipboardTextOrchestrator(deps.Clipboard, autoCopyConfig, func(textLen int) {
221218 cb := glib.SourceFunc(func(_ uintptr) bool {
@@ -392,8 +389,8 @@ func (a *App) applyGTKColorSchemePreference(ctx context.Context) {
392389
393390 // Get the config color scheme preference (default follows system theme)
394391 scheme := "default"
395- if a.deps != nil && a.deps.Config != nil && a.deps.Config.Appearance.ColorScheme != "" {
396- scheme = a.deps.Config.Appearance.ColorScheme
392+ if runtimeScheme := a.runtimeConfigSnapshot().UI.Appearance.ColorScheme; runtimeScheme != "" {
393+ scheme = runtimeScheme
397394 }
398395
399396 // Apply color scheme via libadwaita's StyleManager.
@@ -488,7 +485,8 @@ func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*brow
488485 return created, nil
489486 }
490487
491- mainWindow, err := window.New(ctx, a.gtkApp, a.deps.Config.Workspace.TabBarPosition)
488+ runtimeCfg := a.runtimeConfigSnapshot().UI
489+ mainWindow, err := window.New(ctx, a.gtkApp, runtimeCfg.Workspace.TabBarPosition)
492490 if err != nil {
493491 log.Warn().Err(err).
494492 Str("url_host", logging.SafeURLHost(initialURL)).
@@ -515,9 +513,9 @@ func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*brow
515513
516514 // Create permission popup and dialog presenter
517515 if a.deps != nil && a.deps.PermissionUC != nil {
518- uiScale := 1.0
519- if a.deps.Config != nil {
520- uiScale = a.deps.Config.DefaultUIScale
516+ uiScale := runtimeCfg.DefaultUIScale
517+ if uiScale == 0 {
518+ uiScale = 1.0
521519 }
522520 permPopup := component.NewPermissionPopup(nil, uiScale)
523521 if permPopup != nil {
@@ -740,7 +738,7 @@ func (a *App) updateBrowserWindowTabBarVisibility(bw *browserWindow) {
740738 if bw == nil || bw.mainWindow == nil || bw.mainWindow.TabBar() == nil {
741739 return
742740 }
743- if a.deps == nil || a.deps.Config == nil || !a.deps.Config.Workspace.HideTabBarWhenSingleTab {
741+ if !a.runtimeConfigSnapshot().UI.Workspace.HideTabBarWhenSingleTab {
744742 bw.mainWindow.TabBar().SetAutoHidden(false)
745743 bw.mainWindow.SetTabBarContentInsetVisible(true)
746744 return
@@ -1108,10 +1106,7 @@ func (a *App) initDownloadHandler(ctx context.Context) {
11081106 }
11091107
11101108 // Determine download path from config, fallback to XDG.
1111- downloadPath := ""
1112- if a.deps.Config != nil {
1113- downloadPath = a.deps.Config.Downloads.Path
1114- }
1109+ downloadPath := a.runtimeConfigSnapshot().UI.Downloads.Path
11151110 if downloadPath == "" && a.deps.XDG != nil {
11161111 var err error
11171112 downloadPath, err = a.deps.XDG.DownloadDir()
@@ -1304,14 +1299,17 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
13041299 func (a *App) initBrowserWindowInput(ctx context.Context, bw *browserWindow) {
13051300 log := logging.FromContext(ctx)
13061301
1307- if bw == nil || bw.mainWindow == nil || a.deps == nil || a.deps.Config == nil || a.kbDispatcher == nil {
1302+ if bw == nil || bw.mainWindow == nil || a.deps == nil || a.kbDispatcher == nil {
13081303 return
13091304 }
13101305 if bw.keyboardHandler != nil || bw.globalShortcutHandler != nil {
13111306 return
13121307 }
13131308
1314- bw.keyboardHandler = input.NewKeyboardHandler(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session)
1309+ runtimeCfg := a.runtimeConfigSnapshot().UI
1310+ workspaceCfg := runtimeCfg.Workspace
1311+ sessionCfg := runtimeCfg.Session
1312+ bw.keyboardHandler = input.NewKeyboardHandler(ctx, &workspaceCfg, &sessionCfg)
13151313 bw.keyboardHandler.SetOnAction(func(actionCtx context.Context, action input.Action) error {
13161314 a.activateBrowserWindow(bw)
13171315 if action == input.ActionClosePane {
@@ -1361,8 +1359,8 @@ func (a *App) initBrowserWindowInput(ctx context.Context, bw *browserWindow) {
13611359 bw.globalShortcutHandler = input.NewGlobalShortcutHandler(
13621360 ctx,
13631361 bw.mainWindow.Window(),
1364- &a.deps.Config.Workspace,
1365- &a.deps.Config.Session,
1362+ &workspaceCfg,
1363+ &sessionCfg,
13661364 bw.keyboardHandler,
13671365 func(actionCtx context.Context, action input.Action) error {
13681366 a.activateBrowserWindow(bw)
@@ -1462,15 +1460,16 @@ type omniboxCallbacks struct {
14621460
14631461 func buildOmniboxConfig(
14641462 deps *Dependencies,
1463+ runtimeCfg port.RuntimeUIConfig,
14651464 faviconAdapter *adapter.FaviconAdapter,
14661465 callbacks omniboxCallbacks,
14671466 ) component.OmniboxConfig {
1468- if deps == nil || deps.Config == nil {
1467+ if deps == nil {
14691468 return component.OmniboxConfig{}
14701469 }
14711470
1472- shortcuts := make(map[string]usecase.SearchShortcut, len(deps.Config.SearchShortcuts))
1473- for key, shortcut := range deps.Config.SearchShortcuts {
1471+ shortcuts := make(map[string]usecase.SearchShortcut, len(runtimeCfg.SearchShortcuts))
1472+ for key, shortcut := range runtimeCfg.SearchShortcuts {
14741473 shortcuts[key] = usecase.SearchShortcut{
14751474 URL: shortcut.URL,
14761475 Description: shortcut.Description,
@@ -1483,11 +1482,11 @@ func buildOmniboxConfig(
14831482 FaviconAdapter: faviconAdapter,
14841483 CopyURLUC: deps.CopyURLUC,
14851484 ShortcutsUC: usecase.NewSearchShortcutsUseCase(shortcuts),
1486- DefaultSearch: deps.Config.DefaultSearchEngine,
1487- InitialBehavior: deps.Config.Omnibox.InitialBehavior,
1488- MostVisitedDays: deps.Config.Omnibox.MostVisitedDays,
1485+ DefaultSearch: runtimeCfg.DefaultSearchEngine,
1486+ InitialBehavior: runtimeCfg.Omnibox.InitialBehavior,
1487+ MostVisitedDays: runtimeCfg.Omnibox.MostVisitedDays,
14891488 SaveInitialBehavior: deps.HandlerDeps.SaveOmniboxInitialBehavior,
1490- UIScale: deps.Config.DefaultUIScale,
1489+ UIScale: runtimeCfg.DefaultUIScale,
14911490 OnNavigate: callbacks.OnNavigate,
14921491 OnToast: callbacks.OnToast,
14931492 OnFocusIn: callbacks.OnFocusIn,
@@ -1516,7 +1515,11 @@ func NewStandaloneOmniboxRuntime(
15161515 registerFaviconInvalidator(deps.FaviconResolver, faviconAdapter)
15171516 }
15181517
1519- omniboxCfg := buildOmniboxConfig(deps, faviconAdapter, omniboxCallbacks{
1518+ var runtimeCfg port.RuntimeUIConfig
1519+ if deps != nil && deps.RuntimeConfig != nil {
1520+ runtimeCfg = deps.RuntimeConfig.Current().UI
1521+ }
1522+ omniboxCfg := buildOmniboxConfig(deps, runtimeCfg, faviconAdapter, omniboxCallbacks{
15201523 OnNavigate: func(navCtx context.Context, url string) error {
15211524 return handleStandaloneOmniboxNavigation(deps, navCtx, url)
15221525 },
@@ -1554,11 +1557,11 @@ func handleStandaloneOmniboxNavigation(deps *Dependencies, ctx context.Context,
15541557 }
15551558
15561559 func (a *App) initOmniboxConfig(ctx context.Context) {
1557- if a.deps == nil || a.deps.Config == nil {
1560+ if a.deps == nil {
15581561 return
15591562 }
15601563
1561- a.omniboxCfg = buildOmniboxConfig(a.deps, a.faviconAdapter, omniboxCallbacks{
1564+ a.omniboxCfg = buildOmniboxConfig(a.deps, a.runtimeConfigSnapshot().UI, a.faviconAdapter, omniboxCallbacks{
15621565 OnNavigate: func(navCtx context.Context, url string) error {
15631566 return a.navigateFromOmnibox(navCtx, url)
15641567 },
@@ -1996,9 +1999,7 @@ func (a *App) switchToTargetTabIfConfigured(
19961999 }
19972000
19982001 switchToTarget := false
1999- if a.deps != nil && a.deps.Config != nil {
2000- switchToTarget = a.deps.Config.Workspace.SwitchToTabOnMove
2001- }
2002+ switchToTarget = a.runtimeConfigSnapshot().UI.Workspace.SwitchToTabOnMove
20022003 if out.SourceTabClosed {
20032004 switchToTarget = true
20042005 }
@@ -2035,8 +2036,8 @@ func (a *App) initSnapshotService(ctx context.Context) {
20352036 }
20362037
20372038 intervalMs := 5000 // default
2038- if a.deps.Config != nil && a.deps.Config.Session.SnapshotIntervalMs > 0 {
2039- intervalMs = a.deps.Config.Session.SnapshotIntervalMs
2039+ if runtimeIntervalMs := a.runtimeConfigSnapshot().UI.Session.SnapshotIntervalMs; runtimeIntervalMs > 0 {
2040+ intervalMs = runtimeIntervalMs
20402041 }
20412042
20422043 a.snapshotService = a.deps.SnapshotServiceFactory(a, intervalMs)
@@ -2062,14 +2063,15 @@ func (a *App) initUpdateCoordinator(ctx context.Context) {
20622063 return
20632064 }
20642065
2066+ runtimeCfg := a.runtimeConfigSnapshot().UI
20652067 a.updateCoord = coordinator.NewUpdateCoordinator(
20662068 a.deps.CheckUpdateUC,
20672069 a.deps.ApplyUpdateUC,
20682070 func(ctx context.Context, msg string, level component.ToastLevel) {
20692071 a.showToastOnLastFocusedBrowserWindow(ctx, msg, level)
20702072 },
2071- a.deps.Config.Update.EnableOnStartup,
2072- a.deps.Config.Update.AutoDownload,
2073+ runtimeCfg.Update.EnableOnStartup,
2074+ runtimeCfg.Update.AutoDownload,
20732075 )
20742076
20752077 // Start async update check
@@ -2603,13 +2605,14 @@ func (a *App) switchBrowserWindowTabIndex(ctx context.Context, bw *browserWindow
26032605 a.activateBrowserWindow(bw)
26042606 return a.tabCoord.SwitchByIndex(ctx, target, index)
26052607 }
2606- if a.deps == nil || a.deps.Config == nil || a.deps.Config.Workspace.NewPaneURL == "" {
2608+ newPaneURL := a.runtimeConfigSnapshot().UI.Workspace.NewPaneURL
2609+ if newPaneURL == "" {
26072610 logging.FromContext(ctx).Warn().Msg("switch tab index ignored: new pane URL is not configured")
26082611 return fmt.Errorf("newPaneURL is not configured")
26092612 }
26102613 a.activateBrowserWindow(bw)
26112614 ensureTarget := a.ensureTabTargetForBrowserWindow(bw)
2612- _, err := a.tabCoord.Create(ctx, ensureTarget, urlutil.Normalize(a.deps.Config.Workspace.NewPaneURL))
2615+ _, err := a.tabCoord.Create(ctx, ensureTarget, urlutil.Normalize(newPaneURL))
26132616 return err
26142617 }
26152618
@@ -3251,10 +3254,11 @@ func (a *App) initContentCoordinator(
32513254
32523255 // initTabCoordinator creates the TabCoordinator and wires all its callbacks.
32533256 func (a *App) initTabCoordinator(ctx context.Context) {
3257+ runtimeCfg := a.runtimeConfigSnapshot().UI
32543258 a.tabCoord = coordinator.NewTabCoordinator(ctx, coordinator.TabCoordinatorConfig{
32553259 TabsUC: a.tabsUC,
32563260 MainWindow: a.mainWindow,
3257- HideTabBarWhenSingleTab: a.deps.Config.Workspace.HideTabBarWhenSingleTab,
3261+ HideTabBarWhenSingleTab: runtimeCfg.Workspace.HideTabBarWhenSingleTab,
32583262 })
32593263 a.tabCoord.SetOnTabCreated(func(ctx context.Context, target coordinator.TabTarget, tab *entity.Tab) {
32603264 // Assign ownership from the callback target, not focus/global state.
@@ -3359,6 +3363,7 @@ func (a *App) initCoordinators(ctx context.Context) {
33593363 })
33603364
33613365 // 3. Workspace Coordinator
3366+ runtimeCfg := a.runtimeConfigSnapshot().UI
33623367 a.wsCoord = coordinator.NewWorkspaceCoordinator(ctx, coordinator.WorkspaceCoordinatorConfig{
33633368 PanesUC: a.panesUC,
33643369 FocusMgr: a.focusMgr,
@@ -3367,9 +3372,9 @@ func (a *App) initCoordinators(ctx context.Context) {
33673372 ContentCoord: a.contentCoord,
33683373 GetActiveWS: getActiveWS,
33693374 GenerateID: a.generateID,
3370- NewPaneURL: a.deps.Config.Workspace.NewPaneURL,
3371- ResizeStepPercent: a.deps.Config.Workspace.ResizeMode.StepPercent,
3372- ResizeMinPanePercent: a.deps.Config.Workspace.ResizeMode.MinPanePercent,
3375+ NewPaneURL: runtimeCfg.Workspace.NewPaneURL,
3376+ ResizeStepPercent: runtimeCfg.Workspace.ResizeMode.StepPercent,
3377+ ResizeMinPanePercent: runtimeCfg.Workspace.ResizeMode.MinPanePercent,
33733378 })
33743379 a.wsCoord.SetOnCloseLastPane(func(ctx context.Context) error {
33753380 bw := a.lastFocusedBrowserWindow()
@@ -3385,7 +3390,7 @@ func (a *App) initCoordinators(ctx context.Context) {
33853390 }
33863391 a.contentCoord.SetPopupConfig(
33873392 a.engine.Factory(),
3388- &a.deps.Config.Workspace.BrowsingContexts,
3393+ a.popupBrowsingContextConfig(),
33893394 a.generateID,
33903395 )
33913396 a.contentCoord.SetPopupWindowIDResolver(func(paneID entity.PaneID) (string, bool) {
@@ -3500,12 +3505,13 @@ func (a *App) initCoordinators(ctx context.Context) {
35003505 func (a *App) keyboardActions() dispatcher.KeyboardActions {
35013506 return dispatcher.KeyboardActions{
35023507 NewTab: func(ctx context.Context) error {
3503- if a.deps == nil || a.deps.Config == nil || a.deps.Config.Workspace.NewPaneURL == "" {
3508+ newPaneURL := a.runtimeConfigSnapshot().UI.Workspace.NewPaneURL
3509+ if newPaneURL == "" {
35043510 logging.FromContext(ctx).Warn().Msg("new tab ignored: new pane URL is not configured")
35053511 return fmt.Errorf("newPaneURL is not configured")
35063512 }
35073513 return a.withFocusedTabTarget(ctx, "new tab", true, func(target coordinator.TabTarget) error {
3508- _, err := a.tabCoord.Create(ctx, target, urlutil.Normalize(a.deps.Config.Workspace.NewPaneURL))
3514+ _, err := a.tabCoord.Create(ctx, target, urlutil.Normalize(newPaneURL))
35093515 return err
35103516 })
35113517 },
@@ -3918,7 +3924,7 @@ func (a *App) updateModeIndicatorToaster(ctx context.Context, mode input.Mode) {
39183924 }
39193925
39203926 // Check if mode indicator toaster is enabled in config.
3921- if a.deps == nil || a.deps.Config == nil || !a.deps.Config.Workspace.Styling.ModeIndicatorToasterEnabled {
3927+ if !a.runtimeConfigSnapshot().UI.Workspace.Styling.ModeIndicatorToasterEnabled {
39223928 bw.modeToaster.Hide()
39233929 return
39243930 }
@@ -4083,9 +4089,7 @@ func (a *App) createWorkspaceViewWithoutAttach(ctx context.Context, tab *entity.
40834089 // Set find bar config for this workspace view
40844090 wsView.SetFindBarConfig(a.findBarCfg)
40854091 // Set auto-open omnibox on new pane
4086- if a.deps.Config != nil {
4087- wsView.SetAutoOpenOnNewPane(a.deps.Config.Omnibox.AutoOpenOnNewPane)
4088- }
4092+ wsView.SetAutoOpenOnNewPane(a.runtimeConfigSnapshot().UI.Omnibox.AutoOpenOnNewPane)
40894093
40904094 wsView.SetOnPaneFocused(func(paneID entity.PaneID) {
40914095 if a.keyboardHandler != nil && a.keyboardHandler.Mode() == input.ModeResize {
@@ -4442,18 +4446,12 @@ func (a *App) installFloatingOverlayPositioning(tabID entity.TabID, workspaceOve
44424446
44434447 // currentFloatingWidthPct returns the configured floating pane width percentage.
44444448 func (a *App) currentFloatingWidthPct() float64 {
4445- if a.deps != nil && a.deps.Config != nil {
4446- return a.deps.Config.Workspace.FloatingPane.WidthPct
4447- }
4448- return 0
4449+ return a.runtimeConfigSnapshot().UI.Workspace.FloatingPane.WidthPct
44494450 }
44504451
44514452 // currentFloatingHeightPct returns the configured floating pane height percentage.
44524453 func (a *App) currentFloatingHeightPct() float64 {
4453- if a.deps != nil && a.deps.Config != nil {
4454- return a.deps.Config.Workspace.FloatingPane.HeightPct
4455- }
4456- return 0
4454+ return a.runtimeConfigSnapshot().UI.Workspace.FloatingPane.HeightPct
44574455 }
44584456
44594457 func (a *App) ensureFloatingSession(
@@ -5101,21 +5099,23 @@ func (a *App) initConfigWatcher(ctx context.Context) {
51015099
51025100 a.syncExternalThemeWatcher(ctx)
51035101
5104- if a.deps.WatchConfig == nil || a.deps.OnConfigChange == nil {
5102+ if a.deps == nil || a.deps.RuntimeConfig == nil {
51055103 log.Debug().Msg("no config watcher available, skipping config file watcher")
51065104 return
51075105 }
51085106
5109- // Start viper watcher.
5110- if err := a.deps.WatchConfig(); err != nil {
5107+ if err := a.deps.RuntimeConfig.Watch(); err != nil {
51115108 log.Warn().Err(err).Msg("failed to start config watcher")
51125109 return
51135110 }
51145111
51155112 // Hot-reload appearance and keybindings on config change.
5116- // deps.Config is updated in-place before this callback fires.
5117- a.deps.OnConfigChange(func() {
5113+ a.deps.RuntimeConfig.OnChange(func(snapshot port.RuntimeConfigSnapshot) {
51185114 cb := glib.SourceFunc(func(_ uintptr) bool {
5115+ a.updateRuntimeConfig(snapshot)
5116+ runtimeCfg := snapshot.UI
5117+ workspaceCfg := runtimeCfg.Workspace
5118+ sessionCfg := runtimeCfg.Session
51195119 a.syncExternalThemeWatcher(ctx)
51205120 a.applyAppearanceConfig(ctx)
51215121 for _, bw := range a.browserWindows {
@@ -5126,10 +5126,10 @@ func (a *App) initConfigWatcher(ctx context.Context) {
51265126 // changes in the config file).
51275127 bw.applySidebarWidthConfig(a)
51285128 if bw.keyboardHandler != nil {
5129- bw.keyboardHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session)
5129+ bw.keyboardHandler.ReloadShortcuts(ctx, &workspaceCfg, &sessionCfg)
51305130 }
51315131 if bw.globalShortcutHandler != nil {
5132- bw.globalShortcutHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session)
5132+ bw.globalShortcutHandler.ReloadShortcuts(ctx, &workspaceCfg, &sessionCfg)
51335133 }
51345134 }
51355135 return false
@@ -5141,11 +5141,11 @@ func (a *App) initConfigWatcher(ctx context.Context) {
51415141 }
51425142
51435143 func (a *App) syncExternalThemeWatcher(ctx context.Context) {
5144- if a == nil || a.deps == nil || a.deps.Config == nil || a.deps.ExternalThemeWatcher == nil {
5144+ if a == nil || a.deps == nil || a.deps.ExternalThemeWatcher == nil {
51455145 return
51465146 }
51475147 log := logging.FromContext(ctx)
5148- cfg := a.deps.Config.Appearance.ExternalTheme
5148+ cfg := a.runtimeConfigSnapshot().UI.Appearance.ExternalTheme
51495149 if err := a.deps.ExternalThemeWatcher.Start(ctx, cfg, func() {
51505150 a.dispatchOnMainThread("ui.external_theme_reload", func() {
51515151 a.applyAppearanceConfig(ctx)
@@ -5157,18 +5157,14 @@ func (a *App) syncExternalThemeWatcher(ctx context.Context) {
51575157
51585158 func (a *App) applyAppearanceConfig(ctx context.Context) {
51595159 log := logging.FromContext(ctx)
5160- if a.deps == nil || a.deps.Config == nil {
5160+ if a == nil {
51615161 return
51625162 }
5163- cfg := a.deps.Config
5163+ snapshot := a.runtimeConfigSnapshot()
51645164
51655165 if a.engine != nil {
5166- settings := port.EngineSettingsPayload{DefaultUIScale: cfg.DefaultUIScale}
5167- if a.deps.EngineSettingsPayload != nil {
5168- settings = a.deps.EngineSettingsPayload()
5169- }
51705166 if err := a.engine.UpdateSettings(ctx, port.EngineSettingsUpdate{
5171- Settings: settings,
5167+ Settings: snapshot.EngineSettings,
51725168 }); err != nil {
51735169 log.Warn().Err(err).Msg("failed to apply engine settings update")
51745170 }
@@ -5186,10 +5182,12 @@ func (a *App) applyAppearanceConfig(ctx context.Context) {
51865182
51875183 func (a *App) applyThemeAppearance(ctx context.Context) {
51885184 log := logging.FromContext(ctx)
5189- if a.deps == nil || a.deps.Config == nil || a.deps.Theme == nil {
5185+ if a.deps == nil || a.deps.Theme == nil {
51905186 return
51915187 }
5192- cfg := a.deps.Config
5188+ runtimeCfg := a.runtimeConfigSnapshot().UI
5189+ appearanceCfg := runtimeCfg.Appearance
5190+ workspaceStyling := runtimeCfg.Workspace.Styling
51935191
51945192 var display *gdk.Display
51955193 if a.mainWindow != nil && a.mainWindow.Window() != nil {
@@ -5201,16 +5199,16 @@ func (a *App) applyThemeAppearance(ctx context.Context) {
52015199 preference = a.deps.ColorResolver.Refresh()
52025200 }
52035201 if a.deps.ExternalThemeSource != nil {
5204- a.deps.ExternalThemeSource.Configure(cfg.Appearance.ExternalTheme)
5202+ a.deps.ExternalThemeSource.Configure(appearanceCfg.ExternalTheme)
52055203 }
52065204 resolveThemeUC := a.deps.ResolveThemeUC
52075205 if resolveThemeUC == nil {
52085206 resolveThemeUC = usecase.NewResolveThemeUseCase(a.deps.ExternalThemeSource)
52095207 }
52105208 resolved, err := resolveThemeUC.Refresh(ctx, usecase.ResolveThemeInputFromConfig(
5211- &cfg.Appearance,
5212- cfg.DefaultUIScale,
5213- &cfg.Workspace.Styling,
5209+ &appearanceCfg,
5210+ runtimeCfg.DefaultUIScale,
5211+ &workspaceStyling,
52145212 preference,
52155213 ))
52165214 if err != nil {
@@ -212,10 +212,7 @@ func New(deps *Dependencies) (*App, error) {
212 var clipboardOrchestrator port.ClipboardTextOrchestrator212 var clipboardOrchestrator port.ClipboardTextOrchestrator
213 if deps.Clipboard != nil {213 if deps.Clipboard != nil {
214 autoCopyConfig = &autoCopyConfigFn{fn: func() bool {214 autoCopyConfig = &autoCopyConfigFn{fn: func() bool {
215- if deps.Config == nil {215+ return app.runtimeConfigSnapshot().UI.Clipboard.AutoCopyOnSelection
216- return false
217- }
218- return deps.Config.Clipboard.AutoCopyOnSelection
219 }}216 }}
220 clipboardOrchestrator = usecase.NewClipboardTextOrchestrator(deps.Clipboard, autoCopyConfig, func(textLen int) {217 clipboardOrchestrator = usecase.NewClipboardTextOrchestrator(deps.Clipboard, autoCopyConfig, func(textLen int) {
221 cb := glib.SourceFunc(func(_ uintptr) bool {218 cb := glib.SourceFunc(func(_ uintptr) bool {
@@ -392,8 +389,8 @@ func (a *App) applyGTKColorSchemePreference(ctx context.Context) {
392 389
393 // Get the config color scheme preference (default follows system theme)390 // Get the config color scheme preference (default follows system theme)
394 scheme := "default"391 scheme := "default"
395- if a.deps != nil && a.deps.Config != nil && a.deps.Config.Appearance.ColorScheme != "" {392+ if runtimeScheme := a.runtimeConfigSnapshot().UI.Appearance.ColorScheme; runtimeScheme != "" {
396- scheme = a.deps.Config.Appearance.ColorScheme393+ scheme = runtimeScheme
397 }394 }
398 395
399 // Apply color scheme via libadwaita's StyleManager.396 // Apply color scheme via libadwaita's StyleManager.
@@ -488,7 +485,8 @@ func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*brow
488 return created, nil485 return created, nil
489 }486 }
490 487
491- mainWindow, err := window.New(ctx, a.gtkApp, a.deps.Config.Workspace.TabBarPosition)488+ runtimeCfg := a.runtimeConfigSnapshot().UI
489+ mainWindow, err := window.New(ctx, a.gtkApp, runtimeCfg.Workspace.TabBarPosition)
492 if err != nil {490 if err != nil {
493 log.Warn().Err(err).491 log.Warn().Err(err).
494 Str("url_host", logging.SafeURLHost(initialURL)).492 Str("url_host", logging.SafeURLHost(initialURL)).
@@ -515,9 +513,9 @@ func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*brow
515 513
516 // Create permission popup and dialog presenter514 // Create permission popup and dialog presenter
517 if a.deps != nil && a.deps.PermissionUC != nil {515 if a.deps != nil && a.deps.PermissionUC != nil {
518- uiScale := 1.0516+ uiScale := runtimeCfg.DefaultUIScale
519- if a.deps.Config != nil {517+ if uiScale == 0 {
520- uiScale = a.deps.Config.DefaultUIScale518+ uiScale = 1.0
521 }519 }
522 permPopup := component.NewPermissionPopup(nil, uiScale)520 permPopup := component.NewPermissionPopup(nil, uiScale)
523 if permPopup != nil {521 if permPopup != nil {
@@ -740,7 +738,7 @@ func (a *App) updateBrowserWindowTabBarVisibility(bw *browserWindow) {
740 if bw == nil || bw.mainWindow == nil || bw.mainWindow.TabBar() == nil {738 if bw == nil || bw.mainWindow == nil || bw.mainWindow.TabBar() == nil {
741 return739 return
742 }740 }
743- if a.deps == nil || a.deps.Config == nil || !a.deps.Config.Workspace.HideTabBarWhenSingleTab {741+ if !a.runtimeConfigSnapshot().UI.Workspace.HideTabBarWhenSingleTab {
744 bw.mainWindow.TabBar().SetAutoHidden(false)742 bw.mainWindow.TabBar().SetAutoHidden(false)
745 bw.mainWindow.SetTabBarContentInsetVisible(true)743 bw.mainWindow.SetTabBarContentInsetVisible(true)
746 return744 return
@@ -1108,10 +1106,7 @@ func (a *App) initDownloadHandler(ctx context.Context) {
1108 }1106 }
1109 1107
1110 // Determine download path from config, fallback to XDG.1108 // Determine download path from config, fallback to XDG.
1111- downloadPath := ""1109+ downloadPath := a.runtimeConfigSnapshot().UI.Downloads.Path
1112- if a.deps.Config != nil {
1113- downloadPath = a.deps.Config.Downloads.Path
1114- }
1115 if downloadPath == "" && a.deps.XDG != nil {1110 if downloadPath == "" && a.deps.XDG != nil {
1116 var err error1111 var err error
1117 downloadPath, err = a.deps.XDG.DownloadDir()1112 downloadPath, err = a.deps.XDG.DownloadDir()
@@ -1304,14 +1299,17 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
1304 func (a *App) initBrowserWindowInput(ctx context.Context, bw *browserWindow) {1299 func (a *App) initBrowserWindowInput(ctx context.Context, bw *browserWindow) {
1305 log := logging.FromContext(ctx)1300 log := logging.FromContext(ctx)
1306 1301
1307- if bw == nil || bw.mainWindow == nil || a.deps == nil || a.deps.Config == nil || a.kbDispatcher == nil {1302+ if bw == nil || bw.mainWindow == nil || a.deps == nil || a.kbDispatcher == nil {
1308 return1303 return
1309 }1304 }
1310 if bw.keyboardHandler != nil || bw.globalShortcutHandler != nil {1305 if bw.keyboardHandler != nil || bw.globalShortcutHandler != nil {
1311 return1306 return
1312 }1307 }
1313 1308
1314- bw.keyboardHandler = input.NewKeyboardHandler(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session)1309+ runtimeCfg := a.runtimeConfigSnapshot().UI
1310+ workspaceCfg := runtimeCfg.Workspace
1311+ sessionCfg := runtimeCfg.Session
1312+ bw.keyboardHandler = input.NewKeyboardHandler(ctx, &workspaceCfg, &sessionCfg)
1315 bw.keyboardHandler.SetOnAction(func(actionCtx context.Context, action input.Action) error {1313 bw.keyboardHandler.SetOnAction(func(actionCtx context.Context, action input.Action) error {
1316 a.activateBrowserWindow(bw)1314 a.activateBrowserWindow(bw)
1317 if action == input.ActionClosePane {1315 if action == input.ActionClosePane {
@@ -1361,8 +1359,8 @@ func (a *App) initBrowserWindowInput(ctx context.Context, bw *browserWindow) {
1361 bw.globalShortcutHandler = input.NewGlobalShortcutHandler(1359 bw.globalShortcutHandler = input.NewGlobalShortcutHandler(
1362 ctx,1360 ctx,
1363 bw.mainWindow.Window(),1361 bw.mainWindow.Window(),
1364- &a.deps.Config.Workspace,1362+ &workspaceCfg,
1365- &a.deps.Config.Session,1363+ &sessionCfg,
1366 bw.keyboardHandler,1364 bw.keyboardHandler,
1367 func(actionCtx context.Context, action input.Action) error {1365 func(actionCtx context.Context, action input.Action) error {
1368 a.activateBrowserWindow(bw)1366 a.activateBrowserWindow(bw)
@@ -1462,15 +1460,16 @@ type omniboxCallbacks struct {
1462 1460
1463 func buildOmniboxConfig(1461 func buildOmniboxConfig(
1464 deps *Dependencies,1462 deps *Dependencies,
1463+ runtimeCfg port.RuntimeUIConfig,
1465 faviconAdapter *adapter.FaviconAdapter,1464 faviconAdapter *adapter.FaviconAdapter,
1466 callbacks omniboxCallbacks,1465 callbacks omniboxCallbacks,
1467 ) component.OmniboxConfig {1466 ) component.OmniboxConfig {
1468- if deps == nil || deps.Config == nil {1467+ if deps == nil {
1469 return component.OmniboxConfig{}1468 return component.OmniboxConfig{}
1470 }1469 }
1471 1470
1472- shortcuts := make(map[string]usecase.SearchShortcut, len(deps.Config.SearchShortcuts))1471+ shortcuts := make(map[string]usecase.SearchShortcut, len(runtimeCfg.SearchShortcuts))
1473- for key, shortcut := range deps.Config.SearchShortcuts {1472+ for key, shortcut := range runtimeCfg.SearchShortcuts {
1474 shortcuts[key] = usecase.SearchShortcut{1473 shortcuts[key] = usecase.SearchShortcut{
1475 URL: shortcut.URL,1474 URL: shortcut.URL,
1476 Description: shortcut.Description,1475 Description: shortcut.Description,
@@ -1483,11 +1482,11 @@ func buildOmniboxConfig(
1483 FaviconAdapter: faviconAdapter,1482 FaviconAdapter: faviconAdapter,
1484 CopyURLUC: deps.CopyURLUC,1483 CopyURLUC: deps.CopyURLUC,
1485 ShortcutsUC: usecase.NewSearchShortcutsUseCase(shortcuts),1484 ShortcutsUC: usecase.NewSearchShortcutsUseCase(shortcuts),
1486- DefaultSearch: deps.Config.DefaultSearchEngine,1485+ DefaultSearch: runtimeCfg.DefaultSearchEngine,
1487- InitialBehavior: deps.Config.Omnibox.InitialBehavior,1486+ InitialBehavior: runtimeCfg.Omnibox.InitialBehavior,
1488- MostVisitedDays: deps.Config.Omnibox.MostVisitedDays,1487+ MostVisitedDays: runtimeCfg.Omnibox.MostVisitedDays,
1489 SaveInitialBehavior: deps.HandlerDeps.SaveOmniboxInitialBehavior,1488 SaveInitialBehavior: deps.HandlerDeps.SaveOmniboxInitialBehavior,
1490- UIScale: deps.Config.DefaultUIScale,1489+ UIScale: runtimeCfg.DefaultUIScale,
1491 OnNavigate: callbacks.OnNavigate,1490 OnNavigate: callbacks.OnNavigate,
1492 OnToast: callbacks.OnToast,1491 OnToast: callbacks.OnToast,
1493 OnFocusIn: callbacks.OnFocusIn,1492 OnFocusIn: callbacks.OnFocusIn,
@@ -1516,7 +1515,11 @@ func NewStandaloneOmniboxRuntime(
1516 registerFaviconInvalidator(deps.FaviconResolver, faviconAdapter)1515 registerFaviconInvalidator(deps.FaviconResolver, faviconAdapter)
1517 }1516 }
1518 1517
1519- omniboxCfg := buildOmniboxConfig(deps, faviconAdapter, omniboxCallbacks{1518+ var runtimeCfg port.RuntimeUIConfig
1519+ if deps != nil && deps.RuntimeConfig != nil {
1520+ runtimeCfg = deps.RuntimeConfig.Current().UI
1521+ }
1522+ omniboxCfg := buildOmniboxConfig(deps, runtimeCfg, faviconAdapter, omniboxCallbacks{
1520 OnNavigate: func(navCtx context.Context, url string) error {1523 OnNavigate: func(navCtx context.Context, url string) error {
1521 return handleStandaloneOmniboxNavigation(deps, navCtx, url)1524 return handleStandaloneOmniboxNavigation(deps, navCtx, url)
1522 },1525 },
@@ -1554,11 +1557,11 @@ func handleStandaloneOmniboxNavigation(deps *Dependencies, ctx context.Context,
1554 }1557 }
1555 1558
1556 func (a *App) initOmniboxConfig(ctx context.Context) {1559 func (a *App) initOmniboxConfig(ctx context.Context) {
1557- if a.deps == nil || a.deps.Config == nil {1560+ if a.deps == nil {
1558 return1561 return
1559 }1562 }
1560 1563
1561- a.omniboxCfg = buildOmniboxConfig(a.deps, a.faviconAdapter, omniboxCallbacks{1564+ a.omniboxCfg = buildOmniboxConfig(a.deps, a.runtimeConfigSnapshot().UI, a.faviconAdapter, omniboxCallbacks{
1562 OnNavigate: func(navCtx context.Context, url string) error {1565 OnNavigate: func(navCtx context.Context, url string) error {
1563 return a.navigateFromOmnibox(navCtx, url)1566 return a.navigateFromOmnibox(navCtx, url)
1564 },1567 },
@@ -1996,9 +1999,7 @@ func (a *App) switchToTargetTabIfConfigured(
1996 }1999 }
1997 2000
1998 switchToTarget := false2001 switchToTarget := false
1999- if a.deps != nil && a.deps.Config != nil {2002+ switchToTarget = a.runtimeConfigSnapshot().UI.Workspace.SwitchToTabOnMove
2000- switchToTarget = a.deps.Config.Workspace.SwitchToTabOnMove
2001- }
2002 if out.SourceTabClosed {2003 if out.SourceTabClosed {
2003 switchToTarget = true2004 switchToTarget = true
2004 }2005 }
@@ -2035,8 +2036,8 @@ func (a *App) initSnapshotService(ctx context.Context) {
2035 }2036 }
2036 2037
2037 intervalMs := 5000 // default2038 intervalMs := 5000 // default
2038- if a.deps.Config != nil && a.deps.Config.Session.SnapshotIntervalMs > 0 {2039+ if runtimeIntervalMs := a.runtimeConfigSnapshot().UI.Session.SnapshotIntervalMs; runtimeIntervalMs > 0 {
2039- intervalMs = a.deps.Config.Session.SnapshotIntervalMs2040+ intervalMs = runtimeIntervalMs
2040 }2041 }
2041 2042
2042 a.snapshotService = a.deps.SnapshotServiceFactory(a, intervalMs)2043 a.snapshotService = a.deps.SnapshotServiceFactory(a, intervalMs)
@@ -2062,14 +2063,15 @@ func (a *App) initUpdateCoordinator(ctx context.Context) {
2062 return2063 return
2063 }2064 }
2064 2065
2066+ runtimeCfg := a.runtimeConfigSnapshot().UI
2065 a.updateCoord = coordinator.NewUpdateCoordinator(2067 a.updateCoord = coordinator.NewUpdateCoordinator(
2066 a.deps.CheckUpdateUC,2068 a.deps.CheckUpdateUC,
2067 a.deps.ApplyUpdateUC,2069 a.deps.ApplyUpdateUC,
2068 func(ctx context.Context, msg string, level component.ToastLevel) {2070 func(ctx context.Context, msg string, level component.ToastLevel) {
2069 a.showToastOnLastFocusedBrowserWindow(ctx, msg, level)2071 a.showToastOnLastFocusedBrowserWindow(ctx, msg, level)
2070 },2072 },
2071- a.deps.Config.Update.EnableOnStartup,2073+ runtimeCfg.Update.EnableOnStartup,
2072- a.deps.Config.Update.AutoDownload,2074+ runtimeCfg.Update.AutoDownload,
2073 )2075 )
2074 2076
2075 // Start async update check2077 // Start async update check
@@ -2603,13 +2605,14 @@ func (a *App) switchBrowserWindowTabIndex(ctx context.Context, bw *browserWindow
2603 a.activateBrowserWindow(bw)2605 a.activateBrowserWindow(bw)
2604 return a.tabCoord.SwitchByIndex(ctx, target, index)2606 return a.tabCoord.SwitchByIndex(ctx, target, index)
2605 }2607 }
2606- if a.deps == nil || a.deps.Config == nil || a.deps.Config.Workspace.NewPaneURL == "" {2608+ newPaneURL := a.runtimeConfigSnapshot().UI.Workspace.NewPaneURL
2609+ if newPaneURL == "" {
2607 logging.FromContext(ctx).Warn().Msg("switch tab index ignored: new pane URL is not configured")2610 logging.FromContext(ctx).Warn().Msg("switch tab index ignored: new pane URL is not configured")
2608 return fmt.Errorf("newPaneURL is not configured")2611 return fmt.Errorf("newPaneURL is not configured")
2609 }2612 }
2610 a.activateBrowserWindow(bw)2613 a.activateBrowserWindow(bw)
2611 ensureTarget := a.ensureTabTargetForBrowserWindow(bw)2614 ensureTarget := a.ensureTabTargetForBrowserWindow(bw)
2612- _, err := a.tabCoord.Create(ctx, ensureTarget, urlutil.Normalize(a.deps.Config.Workspace.NewPaneURL))2615+ _, err := a.tabCoord.Create(ctx, ensureTarget, urlutil.Normalize(newPaneURL))
2613 return err2616 return err
2614 }2617 }
2615 2618
@@ -3251,10 +3254,11 @@ func (a *App) initContentCoordinator(
3251 3254
3252 // initTabCoordinator creates the TabCoordinator and wires all its callbacks.3255 // initTabCoordinator creates the TabCoordinator and wires all its callbacks.
3253 func (a *App) initTabCoordinator(ctx context.Context) {3256 func (a *App) initTabCoordinator(ctx context.Context) {
3257+ runtimeCfg := a.runtimeConfigSnapshot().UI
3254 a.tabCoord = coordinator.NewTabCoordinator(ctx, coordinator.TabCoordinatorConfig{3258 a.tabCoord = coordinator.NewTabCoordinator(ctx, coordinator.TabCoordinatorConfig{
3255 TabsUC: a.tabsUC,3259 TabsUC: a.tabsUC,
3256 MainWindow: a.mainWindow,3260 MainWindow: a.mainWindow,
3257- HideTabBarWhenSingleTab: a.deps.Config.Workspace.HideTabBarWhenSingleTab,3261+ HideTabBarWhenSingleTab: runtimeCfg.Workspace.HideTabBarWhenSingleTab,
3258 })3262 })
3259 a.tabCoord.SetOnTabCreated(func(ctx context.Context, target coordinator.TabTarget, tab *entity.Tab) {3263 a.tabCoord.SetOnTabCreated(func(ctx context.Context, target coordinator.TabTarget, tab *entity.Tab) {
3260 // Assign ownership from the callback target, not focus/global state.3264 // Assign ownership from the callback target, not focus/global state.
@@ -3359,6 +3363,7 @@ func (a *App) initCoordinators(ctx context.Context) {
3359 })3363 })
3360 3364
3361 // 3. Workspace Coordinator3365 // 3. Workspace Coordinator
3366+ runtimeCfg := a.runtimeConfigSnapshot().UI
3362 a.wsCoord = coordinator.NewWorkspaceCoordinator(ctx, coordinator.WorkspaceCoordinatorConfig{3367 a.wsCoord = coordinator.NewWorkspaceCoordinator(ctx, coordinator.WorkspaceCoordinatorConfig{
3363 PanesUC: a.panesUC,3368 PanesUC: a.panesUC,
3364 FocusMgr: a.focusMgr,3369 FocusMgr: a.focusMgr,
@@ -3367,9 +3372,9 @@ func (a *App) initCoordinators(ctx context.Context) {
3367 ContentCoord: a.contentCoord,3372 ContentCoord: a.contentCoord,
3368 GetActiveWS: getActiveWS,3373 GetActiveWS: getActiveWS,
3369 GenerateID: a.generateID,3374 GenerateID: a.generateID,
3370- NewPaneURL: a.deps.Config.Workspace.NewPaneURL,3375+ NewPaneURL: runtimeCfg.Workspace.NewPaneURL,
3371- ResizeStepPercent: a.deps.Config.Workspace.ResizeMode.StepPercent,3376+ ResizeStepPercent: runtimeCfg.Workspace.ResizeMode.StepPercent,
3372- ResizeMinPanePercent: a.deps.Config.Workspace.ResizeMode.MinPanePercent,3377+ ResizeMinPanePercent: runtimeCfg.Workspace.ResizeMode.MinPanePercent,
3373 })3378 })
3374 a.wsCoord.SetOnCloseLastPane(func(ctx context.Context) error {3379 a.wsCoord.SetOnCloseLastPane(func(ctx context.Context) error {
3375 bw := a.lastFocusedBrowserWindow()3380 bw := a.lastFocusedBrowserWindow()
@@ -3385,7 +3390,7 @@ func (a *App) initCoordinators(ctx context.Context) {
3385 }3390 }
3386 a.contentCoord.SetPopupConfig(3391 a.contentCoord.SetPopupConfig(
3387 a.engine.Factory(),3392 a.engine.Factory(),
3388- &a.deps.Config.Workspace.BrowsingContexts,3393+ a.popupBrowsingContextConfig(),
3389 a.generateID,3394 a.generateID,
3390 )3395 )
3391 a.contentCoord.SetPopupWindowIDResolver(func(paneID entity.PaneID) (string, bool) {3396 a.contentCoord.SetPopupWindowIDResolver(func(paneID entity.PaneID) (string, bool) {
@@ -3500,12 +3505,13 @@ func (a *App) initCoordinators(ctx context.Context) {
3500 func (a *App) keyboardActions() dispatcher.KeyboardActions {3505 func (a *App) keyboardActions() dispatcher.KeyboardActions {
3501 return dispatcher.KeyboardActions{3506 return dispatcher.KeyboardActions{
3502 NewTab: func(ctx context.Context) error {3507 NewTab: func(ctx context.Context) error {
3503- if a.deps == nil || a.deps.Config == nil || a.deps.Config.Workspace.NewPaneURL == "" {3508+ newPaneURL := a.runtimeConfigSnapshot().UI.Workspace.NewPaneURL
3509+ if newPaneURL == "" {
3504 logging.FromContext(ctx).Warn().Msg("new tab ignored: new pane URL is not configured")3510 logging.FromContext(ctx).Warn().Msg("new tab ignored: new pane URL is not configured")
3505 return fmt.Errorf("newPaneURL is not configured")3511 return fmt.Errorf("newPaneURL is not configured")
3506 }3512 }
3507 return a.withFocusedTabTarget(ctx, "new tab", true, func(target coordinator.TabTarget) error {3513 return a.withFocusedTabTarget(ctx, "new tab", true, func(target coordinator.TabTarget) error {
3508- _, err := a.tabCoord.Create(ctx, target, urlutil.Normalize(a.deps.Config.Workspace.NewPaneURL))3514+ _, err := a.tabCoord.Create(ctx, target, urlutil.Normalize(newPaneURL))
3509 return err3515 return err
3510 })3516 })
3511 },3517 },
@@ -3918,7 +3924,7 @@ func (a *App) updateModeIndicatorToaster(ctx context.Context, mode input.Mode) {
3918 }3924 }
3919 3925
3920 // Check if mode indicator toaster is enabled in config.3926 // Check if mode indicator toaster is enabled in config.
3921- if a.deps == nil || a.deps.Config == nil || !a.deps.Config.Workspace.Styling.ModeIndicatorToasterEnabled {3927+ if !a.runtimeConfigSnapshot().UI.Workspace.Styling.ModeIndicatorToasterEnabled {
3922 bw.modeToaster.Hide()3928 bw.modeToaster.Hide()
3923 return3929 return
3924 }3930 }
@@ -4083,9 +4089,7 @@ func (a *App) createWorkspaceViewWithoutAttach(ctx context.Context, tab *entity.
4083 // Set find bar config for this workspace view4089 // Set find bar config for this workspace view
4084 wsView.SetFindBarConfig(a.findBarCfg)4090 wsView.SetFindBarConfig(a.findBarCfg)
4085 // Set auto-open omnibox on new pane4091 // Set auto-open omnibox on new pane
4086- if a.deps.Config != nil {4092+ wsView.SetAutoOpenOnNewPane(a.runtimeConfigSnapshot().UI.Omnibox.AutoOpenOnNewPane)
4087- wsView.SetAutoOpenOnNewPane(a.deps.Config.Omnibox.AutoOpenOnNewPane)
4088- }
4089 4093
4090 wsView.SetOnPaneFocused(func(paneID entity.PaneID) {4094 wsView.SetOnPaneFocused(func(paneID entity.PaneID) {
4091 if a.keyboardHandler != nil && a.keyboardHandler.Mode() == input.ModeResize {4095 if a.keyboardHandler != nil && a.keyboardHandler.Mode() == input.ModeResize {
@@ -4442,18 +4446,12 @@ func (a *App) installFloatingOverlayPositioning(tabID entity.TabID, workspaceOve
4442 4446
4443 // currentFloatingWidthPct returns the configured floating pane width percentage.4447 // currentFloatingWidthPct returns the configured floating pane width percentage.
4444 func (a *App) currentFloatingWidthPct() float64 {4448 func (a *App) currentFloatingWidthPct() float64 {
4445- if a.deps != nil && a.deps.Config != nil {4449+ return a.runtimeConfigSnapshot().UI.Workspace.FloatingPane.WidthPct
4446- return a.deps.Config.Workspace.FloatingPane.WidthPct
4447- }
4448- return 0
4449 }4450 }
4450 4451
4451 // currentFloatingHeightPct returns the configured floating pane height percentage.4452 // currentFloatingHeightPct returns the configured floating pane height percentage.
4452 func (a *App) currentFloatingHeightPct() float64 {4453 func (a *App) currentFloatingHeightPct() float64 {
4453- if a.deps != nil && a.deps.Config != nil {4454+ return a.runtimeConfigSnapshot().UI.Workspace.FloatingPane.HeightPct
4454- return a.deps.Config.Workspace.FloatingPane.HeightPct
4455- }
4456- return 0
4457 }4455 }
4458 4456
4459 func (a *App) ensureFloatingSession(4457 func (a *App) ensureFloatingSession(
@@ -5101,21 +5099,23 @@ func (a *App) initConfigWatcher(ctx context.Context) {
5101 5099
5102 a.syncExternalThemeWatcher(ctx)5100 a.syncExternalThemeWatcher(ctx)
5103 5101
5104- if a.deps.WatchConfig == nil || a.deps.OnConfigChange == nil {5102+ if a.deps == nil || a.deps.RuntimeConfig == nil {
5105 log.Debug().Msg("no config watcher available, skipping config file watcher")5103 log.Debug().Msg("no config watcher available, skipping config file watcher")
5106 return5104 return
5107 }5105 }
5108 5106
5109- // Start viper watcher.5107+ if err := a.deps.RuntimeConfig.Watch(); err != nil {
5110- if err := a.deps.WatchConfig(); err != nil {
5111 log.Warn().Err(err).Msg("failed to start config watcher")5108 log.Warn().Err(err).Msg("failed to start config watcher")
5112 return5109 return
5113 }5110 }
5114 5111
5115 // Hot-reload appearance and keybindings on config change.5112 // Hot-reload appearance and keybindings on config change.
5116- // deps.Config is updated in-place before this callback fires.5113+ a.deps.RuntimeConfig.OnChange(func(snapshot port.RuntimeConfigSnapshot) {
5117- a.deps.OnConfigChange(func() {
5118 cb := glib.SourceFunc(func(_ uintptr) bool {5114 cb := glib.SourceFunc(func(_ uintptr) bool {
5115+ a.updateRuntimeConfig(snapshot)
5116+ runtimeCfg := snapshot.UI
5117+ workspaceCfg := runtimeCfg.Workspace
5118+ sessionCfg := runtimeCfg.Session
5119 a.syncExternalThemeWatcher(ctx)5119 a.syncExternalThemeWatcher(ctx)
5120 a.applyAppearanceConfig(ctx)5120 a.applyAppearanceConfig(ctx)
5121 for _, bw := range a.browserWindows {5121 for _, bw := range a.browserWindows {
@@ -5126,10 +5126,10 @@ func (a *App) initConfigWatcher(ctx context.Context) {
5126 // changes in the config file).5126 // changes in the config file).
5127 bw.applySidebarWidthConfig(a)5127 bw.applySidebarWidthConfig(a)
5128 if bw.keyboardHandler != nil {5128 if bw.keyboardHandler != nil {
5129- bw.keyboardHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session)5129+ bw.keyboardHandler.ReloadShortcuts(ctx, &workspaceCfg, &sessionCfg)
5130 }5130 }
5131 if bw.globalShortcutHandler != nil {5131 if bw.globalShortcutHandler != nil {
5132- bw.globalShortcutHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session)5132+ bw.globalShortcutHandler.ReloadShortcuts(ctx, &workspaceCfg, &sessionCfg)
5133 }5133 }
5134 }5134 }
5135 return false5135 return false
@@ -5141,11 +5141,11 @@ func (a *App) initConfigWatcher(ctx context.Context) {
5141 }5141 }
5142 5142
5143 func (a *App) syncExternalThemeWatcher(ctx context.Context) {5143 func (a *App) syncExternalThemeWatcher(ctx context.Context) {
5144- if a == nil || a.deps == nil || a.deps.Config == nil || a.deps.ExternalThemeWatcher == nil {5144+ if a == nil || a.deps == nil || a.deps.ExternalThemeWatcher == nil {
5145 return5145 return
5146 }5146 }
5147 log := logging.FromContext(ctx)5147 log := logging.FromContext(ctx)
5148- cfg := a.deps.Config.Appearance.ExternalTheme5148+ cfg := a.runtimeConfigSnapshot().UI.Appearance.ExternalTheme
5149 if err := a.deps.ExternalThemeWatcher.Start(ctx, cfg, func() {5149 if err := a.deps.ExternalThemeWatcher.Start(ctx, cfg, func() {
5150 a.dispatchOnMainThread("ui.external_theme_reload", func() {5150 a.dispatchOnMainThread("ui.external_theme_reload", func() {
5151 a.applyAppearanceConfig(ctx)5151 a.applyAppearanceConfig(ctx)
@@ -5157,18 +5157,14 @@ func (a *App) syncExternalThemeWatcher(ctx context.Context) {
5157 5157
5158 func (a *App) applyAppearanceConfig(ctx context.Context) {5158 func (a *App) applyAppearanceConfig(ctx context.Context) {
5159 log := logging.FromContext(ctx)5159 log := logging.FromContext(ctx)
5160- if a.deps == nil || a.deps.Config == nil {5160+ if a == nil {
5161 return5161 return
5162 }5162 }
5163- cfg := a.deps.Config5163+ snapshot := a.runtimeConfigSnapshot()
5164 5164
5165 if a.engine != nil {5165 if a.engine != nil {
5166- settings := port.EngineSettingsPayload{DefaultUIScale: cfg.DefaultUIScale}
5167- if a.deps.EngineSettingsPayload != nil {
5168- settings = a.deps.EngineSettingsPayload()
5169- }
5170 if err := a.engine.UpdateSettings(ctx, port.EngineSettingsUpdate{5166 if err := a.engine.UpdateSettings(ctx, port.EngineSettingsUpdate{
5171- Settings: settings,5167+ Settings: snapshot.EngineSettings,
5172 }); err != nil {5168 }); err != nil {
5173 log.Warn().Err(err).Msg("failed to apply engine settings update")5169 log.Warn().Err(err).Msg("failed to apply engine settings update")
5174 }5170 }
@@ -5186,10 +5182,12 @@ func (a *App) applyAppearanceConfig(ctx context.Context) {
5186 5182
5187 func (a *App) applyThemeAppearance(ctx context.Context) {5183 func (a *App) applyThemeAppearance(ctx context.Context) {
5188 log := logging.FromContext(ctx)5184 log := logging.FromContext(ctx)
5189- if a.deps == nil || a.deps.Config == nil || a.deps.Theme == nil {5185+ if a.deps == nil || a.deps.Theme == nil {
5190 return5186 return
5191 }5187 }
5192- cfg := a.deps.Config5188+ runtimeCfg := a.runtimeConfigSnapshot().UI
5189+ appearanceCfg := runtimeCfg.Appearance
5190+ workspaceStyling := runtimeCfg.Workspace.Styling
5193 5191
5194 var display *gdk.Display5192 var display *gdk.Display
5195 if a.mainWindow != nil && a.mainWindow.Window() != nil {5193 if a.mainWindow != nil && a.mainWindow.Window() != nil {
@@ -5201,16 +5199,16 @@ func (a *App) applyThemeAppearance(ctx context.Context) {
5201 preference = a.deps.ColorResolver.Refresh()5199 preference = a.deps.ColorResolver.Refresh()
5202 }5200 }
5203 if a.deps.ExternalThemeSource != nil {5201 if a.deps.ExternalThemeSource != nil {
5204- a.deps.ExternalThemeSource.Configure(cfg.Appearance.ExternalTheme)5202+ a.deps.ExternalThemeSource.Configure(appearanceCfg.ExternalTheme)
5205 }5203 }
5206 resolveThemeUC := a.deps.ResolveThemeUC5204 resolveThemeUC := a.deps.ResolveThemeUC
5207 if resolveThemeUC == nil {5205 if resolveThemeUC == nil {
5208 resolveThemeUC = usecase.NewResolveThemeUseCase(a.deps.ExternalThemeSource)5206 resolveThemeUC = usecase.NewResolveThemeUseCase(a.deps.ExternalThemeSource)
5209 }5207 }
5210 resolved, err := resolveThemeUC.Refresh(ctx, usecase.ResolveThemeInputFromConfig(5208 resolved, err := resolveThemeUC.Refresh(ctx, usecase.ResolveThemeInputFromConfig(
5211- &cfg.Appearance,5209+ &appearanceCfg,
5212- cfg.DefaultUIScale,5210+ runtimeCfg.DefaultUIScale,
5213- &cfg.Workspace.Styling,5211+ &workspaceStyling,
5214 preference,5212 preference,
5215 ))5213 ))
5216 if err != nil {5214 if err != nil {
M internal/ui/app_browser_launch_test.go +128 -84
@@ -1016,13 +1016,15 @@ func TestApp_CheckConfigMigrationUsesLastFocusedBrowserWindowToaster(t *testing.
10161016 migrator.EXPECT().CheckMigration().Return(&port.MigrationResult{MissingKeys: []string{"update.notify_on_new_settings"}}, nil).Once()
10171017
10181018 bw := &browserWindow{id: "window-1", appToaster: toaster}
1019+ cfg := &config.Config{
1020+ Update: config.UpdateConfig{NotifyOnNewSettings: true},
1021+ }
10191022 app := &App{
10201023 deps: &Dependencies{
1021- Config: &config.Config{
1022- Update: config.UpdateConfig{NotifyOnNewSettings: true},
1023- },
1024- ConfigMigrator: migrator,
1024+ MigrationChecker: migrator,
10251025 },
1026+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
1027+ runtimeConfigLoaded: true,
10261028 browserWindows: map[string]*browserWindow{bw.id: bw},
10271029 lastFocusedWindowID: bw.id,
10281030 }
@@ -1132,9 +1134,10 @@ func TestApp_RestoreSessionHandlesEmptyWindowSnapshotAsSuccess(t *testing.T) {
11321134
11331135 app := &App{
11341136 deps: &Dependencies{
1135- Config: &config.Config{},
11361137 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
11371138 },
1139+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1140+ runtimeConfigLoaded: true,
11381141 tabs: entity.NewTabList(),
11391142 browserWindows: map[string]*browserWindow{staleWindow.id: staleWindow},
11401143 lastFocusedWindowID: staleWindow.id,
@@ -1171,11 +1174,12 @@ func TestApp_CreateInitialTabNoFallbackAfterEmptyWindowRestore(t *testing.T) {
11711174 bw := &browserWindow{id: "window-1", tabs: windowTabs}
11721175 app := &App{
11731176 deps: &Dependencies{
1174- Config: &config.Config{},
11751177 RestoreSessionID: string(sessionID),
11761178 InitialURL: "https://fallback.example",
11771179 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
11781180 },
1181+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1182+ runtimeConfigLoaded: true,
11791183 tabs: entity.NewTabList(),
11801184 tabsUC: tabsUC,
11811185 tabCoord: coordinator.NewTabCoordinator(ctx, coordinator.TabCoordinatorConfig{TabsUC: tabsUC, Tabs: entity.NewTabList()}),
@@ -1213,15 +1217,16 @@ func TestApp_RestoreSessionClearsStaleUIStateBeforeApplyingRestoredTabs(t *testi
12131217
12141218 app := &App{
12151219 deps: &Dependencies{
1216- Config: &config.Config{},
12171220 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, restoredSessionID, entity.SnapshotFromTabList(restoredSessionID, restoredTabs)),
12181221 },
1219- mainWindow: mainWindow,
1220- widgetFactory: layout.NewGtkWidgetFactory(),
1221- tabs: entity.NewTabList(),
1222- browserWindows: map[string]*browserWindow{staleWindow.id: staleWindow},
1223- workspaceViews: map[entity.TabID]*component.WorkspaceView{staleTab.ID: &component.WorkspaceView{}},
1224- windowForTab: map[entity.TabID]*browserWindow{staleTab.ID: staleWindow},
1222+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1223+ runtimeConfigLoaded: true,
1224+ mainWindow: mainWindow,
1225+ widgetFactory: layout.NewGtkWidgetFactory(),
1226+ tabs: entity.NewTabList(),
1227+ browserWindows: map[string]*browserWindow{staleWindow.id: staleWindow},
1228+ workspaceViews: map[entity.TabID]*component.WorkspaceView{staleTab.ID: &component.WorkspaceView{}},
1229+ windowForTab: map[entity.TabID]*browserWindow{staleTab.ID: staleWindow},
12251230 floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{
12261231 staleSessionKey: {},
12271232 },
@@ -1280,17 +1285,18 @@ func TestApp_RestoreSessionWiresStackedPaneTitleBarCallbacks(t *testing.T) {
12801285
12811286 app := &App{
12821287 deps: &Dependencies{
1283- Config: &config.Config{},
12841288 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, stackedSessionID, entity.SnapshotFromTabList(stackedSessionID, stackedTabs)),
12851289 },
1286- mainWindow: mainWindow,
1287- widgetFactory: layout.NewGtkWidgetFactory(),
1288- contentCoord: &contentcoord.Coordinator{},
1289- wsCoord: coordinator.NewWorkspaceCoordinator(context.Background(), coordinator.WorkspaceCoordinatorConfig{ContentCoord: &contentcoord.Coordinator{}}),
1290- tabs: entity.NewTabList(),
1291- browserWindows: map[string]*browserWindow{},
1292- workspaceViews: map[entity.TabID]*component.WorkspaceView{},
1293- windowForTab: map[entity.TabID]*browserWindow{},
1290+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1291+ runtimeConfigLoaded: true,
1292+ mainWindow: mainWindow,
1293+ widgetFactory: layout.NewGtkWidgetFactory(),
1294+ contentCoord: &contentcoord.Coordinator{},
1295+ wsCoord: coordinator.NewWorkspaceCoordinator(context.Background(), coordinator.WorkspaceCoordinatorConfig{ContentCoord: &contentcoord.Coordinator{}}),
1296+ tabs: entity.NewTabList(),
1297+ browserWindows: map[string]*browserWindow{},
1298+ workspaceViews: map[entity.TabID]*component.WorkspaceView{},
1299+ windowForTab: map[entity.TabID]*browserWindow{},
12941300 }
12951301
12961302 if err := app.restoreSession(context.Background(), stackedSessionID); err != nil {
@@ -1767,7 +1773,9 @@ func TestApp_MoveActivePaneToTabFromBrowserWindowAnchorsOwningWindow(t *testing.
17671773 app := &App{
17681774 tabs: tabs,
17691775 movePaneToTabUC: usecase.NewMovePaneToTabUseCase(func() string { return "new-tab" }),
1770- deps: &Dependencies{Config: &config.Config{}},
1776+ deps: &Dependencies{},
1777+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1778+ runtimeConfigLoaded: true,
17711779 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
17721780 lastFocusedWindowID: second.id,
17731781 }
@@ -1804,7 +1812,9 @@ func TestApp_MoveActivePaneToTabFromBrowserWindowActivatesTargetOwnerForCrossWin
18041812 app := &App{
18051813 tabs: tabs,
18061814 movePaneToTabUC: usecase.NewMovePaneToTabUseCase(func() string { return "generated" }),
1807- deps: &Dependencies{Config: &config.Config{}},
1815+ deps: &Dependencies{},
1816+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1817+ runtimeConfigLoaded: true,
18081818 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
18091819 windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second, tab3.ID: second},
18101820 lastFocusedWindowID: first.id,
@@ -1859,7 +1869,9 @@ func TestApp_MoveActivePaneToTabFromBrowserWindowCrossWindowDerivedMirror(t *tes
18591869 app := &App{
18601870 tabs: tabs,
18611871 movePaneToTabUC: usecase.NewMovePaneToTabUseCase(func() string { return "generated" }),
1862- deps: &Dependencies{Config: &config.Config{}},
1872+ deps: &Dependencies{},
1873+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1874+ runtimeConfigLoaded: true,
18631875 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
18641876 windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab3.ID: second},
18651877 lastFocusedWindowID: first.id,
@@ -1909,10 +1921,12 @@ func TestApp_InitKeyboardHandlerDoesNotReattachExistingWindowInput(t *testing.T)
19091921 globalShortcutHandler: sentinelShortcutHandler,
19101922 }
19111923 app := &App{
1912- deps: &Dependencies{Config: &config.Config{}},
1913- mainWindow: mainWindow,
1914- browserWindows: map[string]*browserWindow{bw.id: bw},
1915- kbDispatcher: dispatcher.NewKeyboardDispatcher(context.Background(), nil, nil, nil, nil, dispatcher.KeyboardActions{}, func(context.Context) entity.PaneID { return "" }),
1924+ deps: &Dependencies{},
1925+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1926+ runtimeConfigLoaded: true,
1927+ mainWindow: mainWindow,
1928+ browserWindows: map[string]*browserWindow{bw.id: bw},
1929+ kbDispatcher: dispatcher.NewKeyboardDispatcher(context.Background(), nil, nil, nil, nil, dispatcher.KeyboardActions{}, func(context.Context) entity.PaneID { return "" }),
19161930 }
19171931
19181932 app.initKeyboardHandler(context.Background())
@@ -1961,10 +1975,13 @@ func TestApp_UpdateBrowserWindowTabBarVisibilityAutoHidesSingleTabButPreservesAl
19611975 }
19621976 setWindowTabBar(t, mainWindow, tabBar)
19631977 bw := &browserWindow{id: "window-1", mainWindow: mainWindow}
1978+ cfg := &config.Config{}
1979+ cfg.Workspace.HideTabBarWhenSingleTab = true
19641980 app := &App{
1965- deps: &Dependencies{Config: &config.Config{}},
1981+ deps: &Dependencies{},
1982+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
1983+ runtimeConfigLoaded: true,
19661984 }
1967- app.deps.Config.Workspace.HideTabBarWhenSingleTab = true
19681985
19691986 tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1")))
19701987 tab2 := entity.NewTab(entity.TabID("tab-2"), entity.WorkspaceID("workspace-2"), entity.NewPane(entity.PaneID("pane-2")))
@@ -2031,10 +2048,13 @@ func TestApp_UpdateBrowserWindowTabBarVisibilityHonorsHideWhenSingleTabDisabled(
20312048 }
20322049 setWindowTabBar(t, mainWindow, tabBar)
20332050 bw := &browserWindow{id: "window-1", mainWindow: mainWindow}
2051+ cfg := &config.Config{}
2052+ cfg.Workspace.HideTabBarWhenSingleTab = false
20342053 app := &App{
2035- deps: &Dependencies{Config: &config.Config{}},
2054+ deps: &Dependencies{},
2055+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
2056+ runtimeConfigLoaded: true,
20362057 }
2037- app.deps.Config.Workspace.HideTabBarWhenSingleTab = false
20382058
20392059 app.updateBrowserWindowTabBarVisibility(bw)
20402060
@@ -2064,8 +2084,9 @@ func TestApp_BuildRestoredWindowUIUpdatesEmptyWindowTabBarVisibility(t *testing.
20642084
20652085 mw.SetTabBarContentInsetVisible(true)
20662086 bw := &browserWindow{id: "window-1", mainWindow: mw, tabs: entity.NewTabList()}
2067- app := &App{deps: &Dependencies{Config: &config.Config{}}}
2068- app.deps.Config.Workspace.HideTabBarWhenSingleTab = true
2087+ cfg := &config.Config{}
2088+ cfg.Workspace.HideTabBarWhenSingleTab = true
2089+ app := appWithRuntimeConfigForTest(cfg)
20692090
20702091 app.buildRestoredWindowUI(context.Background(), []*browserWindow{bw})
20712092
@@ -2087,10 +2108,13 @@ func TestApp_UpdateBrowserWindowTabBarVisibility_BottomInset(t *testing.T) {
20872108 defer mainWindow.Destroy()
20882109
20892110 bw := &browserWindow{id: "window-1", mainWindow: mainWindow}
2111+ cfg := &config.Config{}
2112+ cfg.Workspace.HideTabBarWhenSingleTab = true
20902113 app := &App{
2091- deps: &Dependencies{Config: &config.Config{}},
2114+ deps: &Dependencies{},
2115+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
2116+ runtimeConfigLoaded: true,
20922117 }
2093- app.deps.Config.Workspace.HideTabBarWhenSingleTab = true
20942118
20952119 tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1")))
20962120 tab2 := entity.NewTab(entity.TabID("tab-2"), entity.WorkspaceID("workspace-2"), entity.NewPane(entity.PaneID("pane-2")))
@@ -2142,13 +2166,16 @@ func TestApp_HandlePaneFullscreenChanged_ClearsBottomInset(t *testing.T) {
21422166 tabs.SetActive(tab2.ID)
21432167
21442168 bw := &browserWindow{id: "window-1", mainWindow: mainWindow, tabs: tabs}
2169+ cfg := &config.Config{}
2170+ cfg.Workspace.HideTabBarWhenSingleTab = true
21452171 app := &App{
21462172 tabs: tabs,
2147- deps: &Dependencies{Config: &config.Config{}},
2173+ deps: &Dependencies{},
2174+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
2175+ runtimeConfigLoaded: true,
21482176 browserWindows: map[string]*browserWindow{bw.id: bw},
21492177 lastFocusedWindowID: bw.id,
21502178 }
2151- app.deps.Config.Workspace.HideTabBarWhenSingleTab = true
21522179
21532180 // Start with two tabs: inset should be present
21542181 app.updateBrowserWindowTabBarVisibility(bw)
@@ -2263,7 +2290,9 @@ func TestApp_CreatePopupTabUsesParentPaneOwnerWhenFocusIsStale(t *testing.T) {
22632290 first := &browserWindow{id: "window-1", tabs: firstTabs, mainWindow: &window.MainWindow{}}
22642291 second := &browserWindow{id: "window-2", tabs: secondTabs, mainWindow: &window.MainWindow{}}
22652292 app := &App{
2266- deps: &Dependencies{Config: &config.Config{}},
2293+ deps: &Dependencies{},
2294+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
2295+ runtimeConfigLoaded: true,
22672296 tabs: globalTabs,
22682297 tabsUC: usecase.NewManageTabsUseCase(func() string { return "popup-tab" }),
22692298 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
@@ -2323,7 +2352,9 @@ func TestApp_TabCoordinatorCallbacksUseTargetWindowWhenFocusIsStale(t *testing.T
23232352 first := &browserWindow{id: "window-1", tabs: firstTabs, mainWindow: &window.MainWindow{}}
23242353 second := &browserWindow{id: "window-2", tabs: secondTabs, mainWindow: &window.MainWindow{}}
23252354 app := &App{
2326- deps: &Dependencies{Config: &config.Config{}},
2355+ deps: &Dependencies{},
2356+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
2357+ runtimeConfigLoaded: true,
23272358 tabs: entity.NewTabList(),
23282359 tabsUC: usecase.NewManageTabsUseCase(func() string { return "created-tab" }),
23292360 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
@@ -2949,14 +2980,15 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexUsesSourceWindow(t *testin
29492980 secondTabs.SetActive(secondTab1.ID)
29502981 second := &browserWindow{id: "window-2", tabs: secondTabs}
29512982
2952- app := &App{
2953- deps: &Dependencies{
2954- Config: &config.Config{
2955- Workspace: config.WorkspaceConfig{
2956- NewPaneURL: "about:blank",
2957- },
2958- },
2983+ cfg := &config.Config{
2984+ Workspace: config.WorkspaceConfig{
2985+ NewPaneURL: "about:blank",
29592986 },
2987+ }
2988+ app := &App{
2989+ deps: &Dependencies{},
2990+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
2991+ runtimeConfigLoaded: true,
29602992 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
29612993 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
29622994 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab1.ID: second, secondTab2.ID: second},
@@ -2992,14 +3024,15 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexCreatesOnlyOneMissingTab(t
29923024 secondTabs.SetActive(secondTab.ID)
29933025 second := &browserWindow{id: "window-2", tabs: secondTabs}
29943026
2995- app := &App{
2996- deps: &Dependencies{
2997- Config: &config.Config{
2998- Workspace: config.WorkspaceConfig{
2999- NewPaneURL: "about:blank",
3000- },
3001- },
3027+ cfg := &config.Config{
3028+ Workspace: config.WorkspaceConfig{
3029+ NewPaneURL: "about:blank",
30023030 },
3031+ }
3032+ app := &App{
3033+ deps: &Dependencies{},
3034+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
3035+ runtimeConfigLoaded: true,
30033036 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
30043037 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
30053038 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab.ID: second},
@@ -3042,13 +3075,14 @@ func TestApp_SwitchBrowserWindowTabIndexNegativeIndexIsIgnored(t *testing.T) {
30423075 secondTabs.SetActive(secondTab.ID)
30433076 second := &browserWindow{id: "window-2", tabs: secondTabs}
30443077
3078+ cfg := &config.Config{Workspace: config.WorkspaceConfig{NewPaneURL: "about:blank"}}
30453079 app := &App{
3046- deps: &Dependencies{
3047- Config: &config.Config{Workspace: config.WorkspaceConfig{NewPaneURL: "about:blank"}},
3048- },
3049- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3050- browserWindows: map[string]*browserWindow{second.id: second},
3051- windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},
3080+ deps: &Dependencies{},
3081+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
3082+ runtimeConfigLoaded: true,
3083+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3084+ browserWindows: map[string]*browserWindow{second.id: second},
3085+ windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},
30523086 }
30533087 app.initTabCoordinator(ctx)
30543088
@@ -3069,10 +3103,11 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexNegativeDoesNotMutateWindo
30693103
30703104 second := &browserWindow{id: "window-2"}
30713105
3106+ cfg := &config.Config{Workspace: config.WorkspaceConfig{NewPaneURL: "about:blank"}}
30723107 app := &App{
3073- deps: &Dependencies{
3074- Config: &config.Config{Workspace: config.WorkspaceConfig{NewPaneURL: "about:blank"}},
3075- },
3108+ deps: &Dependencies{},
3109+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
3110+ runtimeConfigLoaded: true,
30763111 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
30773112 browserWindows: map[string]*browserWindow{second.id: second},
30783113 lastFocusedWindowID: "window-1",
@@ -3101,10 +3136,12 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexMissingURLDoesNotCreateTab
31013136 second := &browserWindow{id: "window-2", tabs: secondTabs}
31023137
31033138 app := &App{
3104- deps: &Dependencies{Config: &config.Config{}},
3105- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3106- browserWindows: map[string]*browserWindow{second.id: second},
3107- windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},
3139+ deps: &Dependencies{},
3140+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
3141+ runtimeConfigLoaded: true,
3142+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3143+ browserWindows: map[string]*browserWindow{second.id: second},
3144+ windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},
31083145 }
31093146 app.initTabCoordinator(ctx)
31103147
@@ -3126,7 +3163,9 @@ func TestApp_SwitchBrowserWindowTabIndexMissingURLDoesNotMutateWindowState(t *te
31263163 second := &browserWindow{id: "window-2"}
31273164
31283165 app := &App{
3129- deps: &Dependencies{Config: &config.Config{}},
3166+ deps: &Dependencies{},
3167+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
3168+ runtimeConfigLoaded: true,
31303169 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
31313170 browserWindows: map[string]*browserWindow{second.id: second},
31323171 lastFocusedWindowID: "window-1",
@@ -3168,7 +3207,9 @@ func TestApp_WorkspaceOmniboxNavigateUsesOwnerWindow(t *testing.T) {
31683207 contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), recordingWv2)
31693208
31703209 app := &App{
3171- deps: &Dependencies{Config: &config.Config{}},
3210+ deps: &Dependencies{},
3211+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
3212+ runtimeConfigLoaded: true,
31723213 widgetFactory: layout.NewGtkWidgetFactory(),
31733214 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
31743215 windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second},
@@ -3263,10 +3304,11 @@ func TestApp_RestoreSessionDoesNotLeakStaleWindowsIntoTabMerge(t *testing.T) {
32633304
32643305 app := &App{
32653306 deps: &Dependencies{
3266- Config: &config.Config{},
32673307 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
32683308 },
3269- mainWindow: mainWindow,
3309+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
3310+ runtimeConfigLoaded: true,
3311+ mainWindow: mainWindow,
32703312 browserWindows: map[string]*browserWindow{
32713313 staleBW1.id: staleBW1,
32723314 staleBW2.id: staleBW2,
@@ -3343,15 +3385,16 @@ func TestApp_RestoreSessionHonorsActiveWindowIndex(t *testing.T) {
33433385 var runtimeW1 *browserWindow
33443386 app := &App{
33453387 deps: &Dependencies{
3346- Config: &config.Config{},
33473388 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
33483389 },
3349- mainWindow: mainWindow,
3350- widgetFactory: layout.NewGtkWidgetFactory(),
3351- browserWindows: map[string]*browserWindow{firstBW.id: firstBW},
3352- tabs: entity.NewTabList(),
3353- windowForTab: map[entity.TabID]*browserWindow{},
3354- workspaceViews: map[entity.TabID]*component.WorkspaceView{},
3390+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
3391+ runtimeConfigLoaded: true,
3392+ mainWindow: mainWindow,
3393+ widgetFactory: layout.NewGtkWidgetFactory(),
3394+ browserWindows: map[string]*browserWindow{firstBW.id: firstBW},
3395+ tabs: entity.NewTabList(),
3396+ windowForTab: map[entity.TabID]*browserWindow{},
3397+ workspaceViews: map[entity.TabID]*component.WorkspaceView{},
33553398 browserWindowFactory: func(ctx context.Context, url string) (*browserWindow, error) {
33563399 runtimeW1 = &browserWindow{id: "runtime-w1", mainWindow: mainWindow}
33573400 return runtimeW1, nil
@@ -3410,13 +3453,14 @@ func TestApp_RestoreSessionFailsOnAdditionalWindowCreationError(t *testing.T) {
34103453 factoryErr := errors.New("window creation failed")
34113454 app := &App{
34123455 deps: &Dependencies{
3413- Config: &config.Config{},
34143456 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
34153457 },
3416- mainWindow: mainWindow,
3417- browserWindows: map[string]*browserWindow{firstBW.id: firstBW},
3418- tabs: entity.NewTabList(),
3419- windowForTab: map[entity.TabID]*browserWindow{},
3458+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
3459+ runtimeConfigLoaded: true,
3460+ mainWindow: mainWindow,
3461+ browserWindows: map[string]*browserWindow{firstBW.id: firstBW},
3462+ tabs: entity.NewTabList(),
3463+ windowForTab: map[entity.TabID]*browserWindow{},
34203464 browserWindowFactory: func(ctx context.Context, url string) (*browserWindow, error) {
34213465 return nil, factoryErr
34223466 },
@@ -1016,13 +1016,15 @@ func TestApp_CheckConfigMigrationUsesLastFocusedBrowserWindowToaster(t *testing.
1016 migrator.EXPECT().CheckMigration().Return(&port.MigrationResult{MissingKeys: []string{"update.notify_on_new_settings"}}, nil).Once()1016 migrator.EXPECT().CheckMigration().Return(&port.MigrationResult{MissingKeys: []string{"update.notify_on_new_settings"}}, nil).Once()
1017 1017
1018 bw := &browserWindow{id: "window-1", appToaster: toaster}1018 bw := &browserWindow{id: "window-1", appToaster: toaster}
1019+ cfg := &config.Config{
1020+ Update: config.UpdateConfig{NotifyOnNewSettings: true},
1021+ }
1019 app := &App{1022 app := &App{
1020 deps: &Dependencies{1023 deps: &Dependencies{
1021- Config: &config.Config{1024+ MigrationChecker: migrator,
1022- Update: config.UpdateConfig{NotifyOnNewSettings: true},
1023- },
1024- ConfigMigrator: migrator,
1025 },1025 },
1026+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
1027+ runtimeConfigLoaded: true,
1026 browserWindows: map[string]*browserWindow{bw.id: bw},1028 browserWindows: map[string]*browserWindow{bw.id: bw},
1027 lastFocusedWindowID: bw.id,1029 lastFocusedWindowID: bw.id,
1028 }1030 }
@@ -1132,9 +1134,10 @@ func TestApp_RestoreSessionHandlesEmptyWindowSnapshotAsSuccess(t *testing.T) {
1132 1134
1133 app := &App{1135 app := &App{
1134 deps: &Dependencies{1136 deps: &Dependencies{
1135- Config: &config.Config{},
1136 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),1137 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
1137 },1138 },
1139+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1140+ runtimeConfigLoaded: true,
1138 tabs: entity.NewTabList(),1141 tabs: entity.NewTabList(),
1139 browserWindows: map[string]*browserWindow{staleWindow.id: staleWindow},1142 browserWindows: map[string]*browserWindow{staleWindow.id: staleWindow},
1140 lastFocusedWindowID: staleWindow.id,1143 lastFocusedWindowID: staleWindow.id,
@@ -1171,11 +1174,12 @@ func TestApp_CreateInitialTabNoFallbackAfterEmptyWindowRestore(t *testing.T) {
1171 bw := &browserWindow{id: "window-1", tabs: windowTabs}1174 bw := &browserWindow{id: "window-1", tabs: windowTabs}
1172 app := &App{1175 app := &App{
1173 deps: &Dependencies{1176 deps: &Dependencies{
1174- Config: &config.Config{},
1175 RestoreSessionID: string(sessionID),1177 RestoreSessionID: string(sessionID),
1176 InitialURL: "https://fallback.example",1178 InitialURL: "https://fallback.example",
1177 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),1179 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
1178 },1180 },
1181+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1182+ runtimeConfigLoaded: true,
1179 tabs: entity.NewTabList(),1183 tabs: entity.NewTabList(),
1180 tabsUC: tabsUC,1184 tabsUC: tabsUC,
1181 tabCoord: coordinator.NewTabCoordinator(ctx, coordinator.TabCoordinatorConfig{TabsUC: tabsUC, Tabs: entity.NewTabList()}),1185 tabCoord: coordinator.NewTabCoordinator(ctx, coordinator.TabCoordinatorConfig{TabsUC: tabsUC, Tabs: entity.NewTabList()}),
@@ -1213,15 +1217,16 @@ func TestApp_RestoreSessionClearsStaleUIStateBeforeApplyingRestoredTabs(t *testi
1213 1217
1214 app := &App{1218 app := &App{
1215 deps: &Dependencies{1219 deps: &Dependencies{
1216- Config: &config.Config{},
1217 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, restoredSessionID, entity.SnapshotFromTabList(restoredSessionID, restoredTabs)),1220 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, restoredSessionID, entity.SnapshotFromTabList(restoredSessionID, restoredTabs)),
1218 },1221 },
1219- mainWindow: mainWindow,1222+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1220- widgetFactory: layout.NewGtkWidgetFactory(),1223+ runtimeConfigLoaded: true,
1221- tabs: entity.NewTabList(),1224+ mainWindow: mainWindow,
1222- browserWindows: map[string]*browserWindow{staleWindow.id: staleWindow},1225+ widgetFactory: layout.NewGtkWidgetFactory(),
1223- workspaceViews: map[entity.TabID]*component.WorkspaceView{staleTab.ID: &component.WorkspaceView{}},1226+ tabs: entity.NewTabList(),
1224- windowForTab: map[entity.TabID]*browserWindow{staleTab.ID: staleWindow},1227+ browserWindows: map[string]*browserWindow{staleWindow.id: staleWindow},
1228+ workspaceViews: map[entity.TabID]*component.WorkspaceView{staleTab.ID: &component.WorkspaceView{}},
1229+ windowForTab: map[entity.TabID]*browserWindow{staleTab.ID: staleWindow},
1225 floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{1230 floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{
1226 staleSessionKey: {},1231 staleSessionKey: {},
1227 },1232 },
@@ -1280,17 +1285,18 @@ func TestApp_RestoreSessionWiresStackedPaneTitleBarCallbacks(t *testing.T) {
1280 1285
1281 app := &App{1286 app := &App{
1282 deps: &Dependencies{1287 deps: &Dependencies{
1283- Config: &config.Config{},
1284 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, stackedSessionID, entity.SnapshotFromTabList(stackedSessionID, stackedTabs)),1288 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, stackedSessionID, entity.SnapshotFromTabList(stackedSessionID, stackedTabs)),
1285 },1289 },
1286- mainWindow: mainWindow,1290+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1287- widgetFactory: layout.NewGtkWidgetFactory(),1291+ runtimeConfigLoaded: true,
1288- contentCoord: &contentcoord.Coordinator{},1292+ mainWindow: mainWindow,
1289- wsCoord: coordinator.NewWorkspaceCoordinator(context.Background(), coordinator.WorkspaceCoordinatorConfig{ContentCoord: &contentcoord.Coordinator{}}),1293+ widgetFactory: layout.NewGtkWidgetFactory(),
1290- tabs: entity.NewTabList(),1294+ contentCoord: &contentcoord.Coordinator{},
1291- browserWindows: map[string]*browserWindow{},1295+ wsCoord: coordinator.NewWorkspaceCoordinator(context.Background(), coordinator.WorkspaceCoordinatorConfig{ContentCoord: &contentcoord.Coordinator{}}),
1292- workspaceViews: map[entity.TabID]*component.WorkspaceView{},1296+ tabs: entity.NewTabList(),
1293- windowForTab: map[entity.TabID]*browserWindow{},1297+ browserWindows: map[string]*browserWindow{},
1298+ workspaceViews: map[entity.TabID]*component.WorkspaceView{},
1299+ windowForTab: map[entity.TabID]*browserWindow{},
1294 }1300 }
1295 1301
1296 if err := app.restoreSession(context.Background(), stackedSessionID); err != nil {1302 if err := app.restoreSession(context.Background(), stackedSessionID); err != nil {
@@ -1767,7 +1773,9 @@ func TestApp_MoveActivePaneToTabFromBrowserWindowAnchorsOwningWindow(t *testing.
1767 app := &App{1773 app := &App{
1768 tabs: tabs,1774 tabs: tabs,
1769 movePaneToTabUC: usecase.NewMovePaneToTabUseCase(func() string { return "new-tab" }),1775 movePaneToTabUC: usecase.NewMovePaneToTabUseCase(func() string { return "new-tab" }),
1770- deps: &Dependencies{Config: &config.Config{}},1776+ deps: &Dependencies{},
1777+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1778+ runtimeConfigLoaded: true,
1771 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},1779 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
1772 lastFocusedWindowID: second.id,1780 lastFocusedWindowID: second.id,
1773 }1781 }
@@ -1804,7 +1812,9 @@ func TestApp_MoveActivePaneToTabFromBrowserWindowActivatesTargetOwnerForCrossWin
1804 app := &App{1812 app := &App{
1805 tabs: tabs,1813 tabs: tabs,
1806 movePaneToTabUC: usecase.NewMovePaneToTabUseCase(func() string { return "generated" }),1814 movePaneToTabUC: usecase.NewMovePaneToTabUseCase(func() string { return "generated" }),
1807- deps: &Dependencies{Config: &config.Config{}},1815+ deps: &Dependencies{},
1816+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1817+ runtimeConfigLoaded: true,
1808 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},1818 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
1809 windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second, tab3.ID: second},1819 windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second, tab3.ID: second},
1810 lastFocusedWindowID: first.id,1820 lastFocusedWindowID: first.id,
@@ -1859,7 +1869,9 @@ func TestApp_MoveActivePaneToTabFromBrowserWindowCrossWindowDerivedMirror(t *tes
1859 app := &App{1869 app := &App{
1860 tabs: tabs,1870 tabs: tabs,
1861 movePaneToTabUC: usecase.NewMovePaneToTabUseCase(func() string { return "generated" }),1871 movePaneToTabUC: usecase.NewMovePaneToTabUseCase(func() string { return "generated" }),
1862- deps: &Dependencies{Config: &config.Config{}},1872+ deps: &Dependencies{},
1873+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1874+ runtimeConfigLoaded: true,
1863 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},1875 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
1864 windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab3.ID: second},1876 windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab3.ID: second},
1865 lastFocusedWindowID: first.id,1877 lastFocusedWindowID: first.id,
@@ -1909,10 +1921,12 @@ func TestApp_InitKeyboardHandlerDoesNotReattachExistingWindowInput(t *testing.T)
1909 globalShortcutHandler: sentinelShortcutHandler,1921 globalShortcutHandler: sentinelShortcutHandler,
1910 }1922 }
1911 app := &App{1923 app := &App{
1912- deps: &Dependencies{Config: &config.Config{}},1924+ deps: &Dependencies{},
1913- mainWindow: mainWindow,1925+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
1914- browserWindows: map[string]*browserWindow{bw.id: bw},1926+ runtimeConfigLoaded: true,
1915- kbDispatcher: dispatcher.NewKeyboardDispatcher(context.Background(), nil, nil, nil, nil, dispatcher.KeyboardActions{}, func(context.Context) entity.PaneID { return "" }),1927+ mainWindow: mainWindow,
1928+ browserWindows: map[string]*browserWindow{bw.id: bw},
1929+ kbDispatcher: dispatcher.NewKeyboardDispatcher(context.Background(), nil, nil, nil, nil, dispatcher.KeyboardActions{}, func(context.Context) entity.PaneID { return "" }),
1916 }1930 }
1917 1931
1918 app.initKeyboardHandler(context.Background())1932 app.initKeyboardHandler(context.Background())
@@ -1961,10 +1975,13 @@ func TestApp_UpdateBrowserWindowTabBarVisibilityAutoHidesSingleTabButPreservesAl
1961 }1975 }
1962 setWindowTabBar(t, mainWindow, tabBar)1976 setWindowTabBar(t, mainWindow, tabBar)
1963 bw := &browserWindow{id: "window-1", mainWindow: mainWindow}1977 bw := &browserWindow{id: "window-1", mainWindow: mainWindow}
1978+ cfg := &config.Config{}
1979+ cfg.Workspace.HideTabBarWhenSingleTab = true
1964 app := &App{1980 app := &App{
1965- deps: &Dependencies{Config: &config.Config{}},1981+ deps: &Dependencies{},
1982+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
1983+ runtimeConfigLoaded: true,
1966 }1984 }
1967- app.deps.Config.Workspace.HideTabBarWhenSingleTab = true
1968 1985
1969 tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1")))1986 tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1")))
1970 tab2 := entity.NewTab(entity.TabID("tab-2"), entity.WorkspaceID("workspace-2"), entity.NewPane(entity.PaneID("pane-2")))1987 tab2 := entity.NewTab(entity.TabID("tab-2"), entity.WorkspaceID("workspace-2"), entity.NewPane(entity.PaneID("pane-2")))
@@ -2031,10 +2048,13 @@ func TestApp_UpdateBrowserWindowTabBarVisibilityHonorsHideWhenSingleTabDisabled(
2031 }2048 }
2032 setWindowTabBar(t, mainWindow, tabBar)2049 setWindowTabBar(t, mainWindow, tabBar)
2033 bw := &browserWindow{id: "window-1", mainWindow: mainWindow}2050 bw := &browserWindow{id: "window-1", mainWindow: mainWindow}
2051+ cfg := &config.Config{}
2052+ cfg.Workspace.HideTabBarWhenSingleTab = false
2034 app := &App{2053 app := &App{
2035- deps: &Dependencies{Config: &config.Config{}},2054+ deps: &Dependencies{},
2055+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
2056+ runtimeConfigLoaded: true,
2036 }2057 }
2037- app.deps.Config.Workspace.HideTabBarWhenSingleTab = false
2038 2058
2039 app.updateBrowserWindowTabBarVisibility(bw)2059 app.updateBrowserWindowTabBarVisibility(bw)
2040 2060
@@ -2064,8 +2084,9 @@ func TestApp_BuildRestoredWindowUIUpdatesEmptyWindowTabBarVisibility(t *testing.
2064 2084
2065 mw.SetTabBarContentInsetVisible(true)2085 mw.SetTabBarContentInsetVisible(true)
2066 bw := &browserWindow{id: "window-1", mainWindow: mw, tabs: entity.NewTabList()}2086 bw := &browserWindow{id: "window-1", mainWindow: mw, tabs: entity.NewTabList()}
2067- app := &App{deps: &Dependencies{Config: &config.Config{}}}2087+ cfg := &config.Config{}
2068- app.deps.Config.Workspace.HideTabBarWhenSingleTab = true2088+ cfg.Workspace.HideTabBarWhenSingleTab = true
2089+ app := appWithRuntimeConfigForTest(cfg)
2069 2090
2070 app.buildRestoredWindowUI(context.Background(), []*browserWindow{bw})2091 app.buildRestoredWindowUI(context.Background(), []*browserWindow{bw})
2071 2092
@@ -2087,10 +2108,13 @@ func TestApp_UpdateBrowserWindowTabBarVisibility_BottomInset(t *testing.T) {
2087 defer mainWindow.Destroy()2108 defer mainWindow.Destroy()
2088 2109
2089 bw := &browserWindow{id: "window-1", mainWindow: mainWindow}2110 bw := &browserWindow{id: "window-1", mainWindow: mainWindow}
2111+ cfg := &config.Config{}
2112+ cfg.Workspace.HideTabBarWhenSingleTab = true
2090 app := &App{2113 app := &App{
2091- deps: &Dependencies{Config: &config.Config{}},2114+ deps: &Dependencies{},
2115+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
2116+ runtimeConfigLoaded: true,
2092 }2117 }
2093- app.deps.Config.Workspace.HideTabBarWhenSingleTab = true
2094 2118
2095 tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1")))2119 tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1")))
2096 tab2 := entity.NewTab(entity.TabID("tab-2"), entity.WorkspaceID("workspace-2"), entity.NewPane(entity.PaneID("pane-2")))2120 tab2 := entity.NewTab(entity.TabID("tab-2"), entity.WorkspaceID("workspace-2"), entity.NewPane(entity.PaneID("pane-2")))
@@ -2142,13 +2166,16 @@ func TestApp_HandlePaneFullscreenChanged_ClearsBottomInset(t *testing.T) {
2142 tabs.SetActive(tab2.ID)2166 tabs.SetActive(tab2.ID)
2143 2167
2144 bw := &browserWindow{id: "window-1", mainWindow: mainWindow, tabs: tabs}2168 bw := &browserWindow{id: "window-1", mainWindow: mainWindow, tabs: tabs}
2169+ cfg := &config.Config{}
2170+ cfg.Workspace.HideTabBarWhenSingleTab = true
2145 app := &App{2171 app := &App{
2146 tabs: tabs,2172 tabs: tabs,
2147- deps: &Dependencies{Config: &config.Config{}},2173+ deps: &Dependencies{},
2174+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
2175+ runtimeConfigLoaded: true,
2148 browserWindows: map[string]*browserWindow{bw.id: bw},2176 browserWindows: map[string]*browserWindow{bw.id: bw},
2149 lastFocusedWindowID: bw.id,2177 lastFocusedWindowID: bw.id,
2150 }2178 }
2151- app.deps.Config.Workspace.HideTabBarWhenSingleTab = true
2152 2179
2153 // Start with two tabs: inset should be present2180 // Start with two tabs: inset should be present
2154 app.updateBrowserWindowTabBarVisibility(bw)2181 app.updateBrowserWindowTabBarVisibility(bw)
@@ -2263,7 +2290,9 @@ func TestApp_CreatePopupTabUsesParentPaneOwnerWhenFocusIsStale(t *testing.T) {
2263 first := &browserWindow{id: "window-1", tabs: firstTabs, mainWindow: &window.MainWindow{}}2290 first := &browserWindow{id: "window-1", tabs: firstTabs, mainWindow: &window.MainWindow{}}
2264 second := &browserWindow{id: "window-2", tabs: secondTabs, mainWindow: &window.MainWindow{}}2291 second := &browserWindow{id: "window-2", tabs: secondTabs, mainWindow: &window.MainWindow{}}
2265 app := &App{2292 app := &App{
2266- deps: &Dependencies{Config: &config.Config{}},2293+ deps: &Dependencies{},
2294+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
2295+ runtimeConfigLoaded: true,
2267 tabs: globalTabs,2296 tabs: globalTabs,
2268 tabsUC: usecase.NewManageTabsUseCase(func() string { return "popup-tab" }),2297 tabsUC: usecase.NewManageTabsUseCase(func() string { return "popup-tab" }),
2269 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},2298 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
@@ -2323,7 +2352,9 @@ func TestApp_TabCoordinatorCallbacksUseTargetWindowWhenFocusIsStale(t *testing.T
2323 first := &browserWindow{id: "window-1", tabs: firstTabs, mainWindow: &window.MainWindow{}}2352 first := &browserWindow{id: "window-1", tabs: firstTabs, mainWindow: &window.MainWindow{}}
2324 second := &browserWindow{id: "window-2", tabs: secondTabs, mainWindow: &window.MainWindow{}}2353 second := &browserWindow{id: "window-2", tabs: secondTabs, mainWindow: &window.MainWindow{}}
2325 app := &App{2354 app := &App{
2326- deps: &Dependencies{Config: &config.Config{}},2355+ deps: &Dependencies{},
2356+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
2357+ runtimeConfigLoaded: true,
2327 tabs: entity.NewTabList(),2358 tabs: entity.NewTabList(),
2328 tabsUC: usecase.NewManageTabsUseCase(func() string { return "created-tab" }),2359 tabsUC: usecase.NewManageTabsUseCase(func() string { return "created-tab" }),
2329 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},2360 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
@@ -2949,14 +2980,15 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexUsesSourceWindow(t *testin
2949 secondTabs.SetActive(secondTab1.ID)2980 secondTabs.SetActive(secondTab1.ID)
2950 second := &browserWindow{id: "window-2", tabs: secondTabs}2981 second := &browserWindow{id: "window-2", tabs: secondTabs}
2951 2982
2952- app := &App{2983+ cfg := &config.Config{
2953- deps: &Dependencies{2984+ Workspace: config.WorkspaceConfig{
2954- Config: &config.Config{2985+ NewPaneURL: "about:blank",
2955- Workspace: config.WorkspaceConfig{
2956- NewPaneURL: "about:blank",
2957- },
2958- },
2959 },2986 },
2987+ }
2988+ app := &App{
2989+ deps: &Dependencies{},
2990+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
2991+ runtimeConfigLoaded: true,
2960 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),2992 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
2961 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},2993 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
2962 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab1.ID: second, secondTab2.ID: second},2994 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab1.ID: second, secondTab2.ID: second},
@@ -2992,14 +3024,15 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexCreatesOnlyOneMissingTab(t
2992 secondTabs.SetActive(secondTab.ID)3024 secondTabs.SetActive(secondTab.ID)
2993 second := &browserWindow{id: "window-2", tabs: secondTabs}3025 second := &browserWindow{id: "window-2", tabs: secondTabs}
2994 3026
2995- app := &App{3027+ cfg := &config.Config{
2996- deps: &Dependencies{3028+ Workspace: config.WorkspaceConfig{
2997- Config: &config.Config{3029+ NewPaneURL: "about:blank",
2998- Workspace: config.WorkspaceConfig{
2999- NewPaneURL: "about:blank",
3000- },
3001- },
3002 },3030 },
3031+ }
3032+ app := &App{
3033+ deps: &Dependencies{},
3034+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
3035+ runtimeConfigLoaded: true,
3003 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),3036 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3004 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},3037 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
3005 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab.ID: second},3038 windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: first, secondTab.ID: second},
@@ -3042,13 +3075,14 @@ func TestApp_SwitchBrowserWindowTabIndexNegativeIndexIsIgnored(t *testing.T) {
3042 secondTabs.SetActive(secondTab.ID)3075 secondTabs.SetActive(secondTab.ID)
3043 second := &browserWindow{id: "window-2", tabs: secondTabs}3076 second := &browserWindow{id: "window-2", tabs: secondTabs}
3044 3077
3078+ cfg := &config.Config{Workspace: config.WorkspaceConfig{NewPaneURL: "about:blank"}}
3045 app := &App{3079 app := &App{
3046- deps: &Dependencies{3080+ deps: &Dependencies{},
3047- Config: &config.Config{Workspace: config.WorkspaceConfig{NewPaneURL: "about:blank"}},3081+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
3048- },3082+ runtimeConfigLoaded: true,
3049- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),3083+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3050- browserWindows: map[string]*browserWindow{second.id: second},3084+ browserWindows: map[string]*browserWindow{second.id: second},
3051- windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},3085+ windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},
3052 }3086 }
3053 app.initTabCoordinator(ctx)3087 app.initTabCoordinator(ctx)
3054 3088
@@ -3069,10 +3103,11 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexNegativeDoesNotMutateWindo
3069 3103
3070 second := &browserWindow{id: "window-2"}3104 second := &browserWindow{id: "window-2"}
3071 3105
3106+ cfg := &config.Config{Workspace: config.WorkspaceConfig{NewPaneURL: "about:blank"}}
3072 app := &App{3107 app := &App{
3073- deps: &Dependencies{3108+ deps: &Dependencies{},
3074- Config: &config.Config{Workspace: config.WorkspaceConfig{NewPaneURL: "about:blank"}},3109+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
3075- },3110+ runtimeConfigLoaded: true,
3076 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),3111 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3077 browserWindows: map[string]*browserWindow{second.id: second},3112 browserWindows: map[string]*browserWindow{second.id: second},
3078 lastFocusedWindowID: "window-1",3113 lastFocusedWindowID: "window-1",
@@ -3101,10 +3136,12 @@ func TestApp_DispatchBrowserWindowActionSwitchTabIndexMissingURLDoesNotCreateTab
3101 second := &browserWindow{id: "window-2", tabs: secondTabs}3136 second := &browserWindow{id: "window-2", tabs: secondTabs}
3102 3137
3103 app := &App{3138 app := &App{
3104- deps: &Dependencies{Config: &config.Config{}},3139+ deps: &Dependencies{},
3105- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),3140+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
3106- browserWindows: map[string]*browserWindow{second.id: second},3141+ runtimeConfigLoaded: true,
3107- windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},3142+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3143+ browserWindows: map[string]*browserWindow{second.id: second},
3144+ windowForTab: map[entity.TabID]*browserWindow{secondTab.ID: second},
3108 }3145 }
3109 app.initTabCoordinator(ctx)3146 app.initTabCoordinator(ctx)
3110 3147
@@ -3126,7 +3163,9 @@ func TestApp_SwitchBrowserWindowTabIndexMissingURLDoesNotMutateWindowState(t *te
3126 second := &browserWindow{id: "window-2"}3163 second := &browserWindow{id: "window-2"}
3127 3164
3128 app := &App{3165 app := &App{
3129- deps: &Dependencies{Config: &config.Config{}},3166+ deps: &Dependencies{},
3167+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
3168+ runtimeConfigLoaded: true,
3130 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),3169 tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
3131 browserWindows: map[string]*browserWindow{second.id: second},3170 browserWindows: map[string]*browserWindow{second.id: second},
3132 lastFocusedWindowID: "window-1",3171 lastFocusedWindowID: "window-1",
@@ -3168,7 +3207,9 @@ func TestApp_WorkspaceOmniboxNavigateUsesOwnerWindow(t *testing.T) {
3168 contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), recordingWv2)3207 contentCoord.RegisterPopupWebView(entity.PaneID("pane-2"), recordingWv2)
3169 3208
3170 app := &App{3209 app := &App{
3171- deps: &Dependencies{Config: &config.Config{}},3210+ deps: &Dependencies{},
3211+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
3212+ runtimeConfigLoaded: true,
3172 widgetFactory: layout.NewGtkWidgetFactory(),3213 widgetFactory: layout.NewGtkWidgetFactory(),
3173 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},3214 browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
3174 windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second},3215 windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second},
@@ -3263,10 +3304,11 @@ func TestApp_RestoreSessionDoesNotLeakStaleWindowsIntoTabMerge(t *testing.T) {
3263 3304
3264 app := &App{3305 app := &App{
3265 deps: &Dependencies{3306 deps: &Dependencies{
3266- Config: &config.Config{},
3267 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),3307 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
3268 },3308 },
3269- mainWindow: mainWindow,3309+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
3310+ runtimeConfigLoaded: true,
3311+ mainWindow: mainWindow,
3270 browserWindows: map[string]*browserWindow{3312 browserWindows: map[string]*browserWindow{
3271 staleBW1.id: staleBW1,3313 staleBW1.id: staleBW1,
3272 staleBW2.id: staleBW2,3314 staleBW2.id: staleBW2,
@@ -3343,15 +3385,16 @@ func TestApp_RestoreSessionHonorsActiveWindowIndex(t *testing.T) {
3343 var runtimeW1 *browserWindow3385 var runtimeW1 *browserWindow
3344 app := &App{3386 app := &App{
3345 deps: &Dependencies{3387 deps: &Dependencies{
3346- Config: &config.Config{},
3347 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),3388 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
3348 },3389 },
3349- mainWindow: mainWindow,3390+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
3350- widgetFactory: layout.NewGtkWidgetFactory(),3391+ runtimeConfigLoaded: true,
3351- browserWindows: map[string]*browserWindow{firstBW.id: firstBW},3392+ mainWindow: mainWindow,
3352- tabs: entity.NewTabList(),3393+ widgetFactory: layout.NewGtkWidgetFactory(),
3353- windowForTab: map[entity.TabID]*browserWindow{},3394+ browserWindows: map[string]*browserWindow{firstBW.id: firstBW},
3354- workspaceViews: map[entity.TabID]*component.WorkspaceView{},3395+ tabs: entity.NewTabList(),
3396+ windowForTab: map[entity.TabID]*browserWindow{},
3397+ workspaceViews: map[entity.TabID]*component.WorkspaceView{},
3355 browserWindowFactory: func(ctx context.Context, url string) (*browserWindow, error) {3398 browserWindowFactory: func(ctx context.Context, url string) (*browserWindow, error) {
3356 runtimeW1 = &browserWindow{id: "runtime-w1", mainWindow: mainWindow}3399 runtimeW1 = &browserWindow{id: "runtime-w1", mainWindow: mainWindow}
3357 return runtimeW1, nil3400 return runtimeW1, nil
@@ -3410,13 +3453,14 @@ func TestApp_RestoreSessionFailsOnAdditionalWindowCreationError(t *testing.T) {
3410 factoryErr := errors.New("window creation failed")3453 factoryErr := errors.New("window creation failed")
3411 app := &App{3454 app := &App{
3412 deps: &Dependencies{3455 deps: &Dependencies{
3413- Config: &config.Config{},
3414 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),3456 SessionStateRepo: mockSessionStateRepoWithSnapshot(t, sessionID, state),
3415 },3457 },
3416- mainWindow: mainWindow,3458+ runtimeConfig: runtimeConfigSnapshotForTest(&config.Config{}),
3417- browserWindows: map[string]*browserWindow{firstBW.id: firstBW},3459+ runtimeConfigLoaded: true,
3418- tabs: entity.NewTabList(),3460+ mainWindow: mainWindow,
3419- windowForTab: map[entity.TabID]*browserWindow{},3461+ browserWindows: map[string]*browserWindow{firstBW.id: firstBW},
3462+ tabs: entity.NewTabList(),
3463+ windowForTab: map[entity.TabID]*browserWindow{},
3420 browserWindowFactory: func(ctx context.Context, url string) (*browserWindow, error) {3464 browserWindowFactory: func(ctx context.Context, url string) (*browserWindow, error) {
3421 return nil, factoryErr3465 return nil, factoryErr
3422 },3466 },
M internal/ui/app_clipboard_test.go +0 -3
@@ -8,7 +8,6 @@ import (
88 "github.com/bnema/dumber/internal/application/port"
99 portmocks "github.com/bnema/dumber/internal/application/port/mocks"
1010 "github.com/bnema/dumber/internal/domain/entity"
11- "github.com/bnema/dumber/internal/infrastructure/config"
1211 "github.com/stretchr/testify/mock"
1312 "github.com/stretchr/testify/require"
1413 )
@@ -31,7 +30,6 @@ func TestNew_DoesNotWireClipboardOrchestratorWhenClipboardMissing(t *testing.T)
3130
3231 deps := &Dependencies{
3332 Ctx: ctx,
34- Config: &config.Config{},
3533 RuntimeConfig: runtimeConfig,
3634 Engine: engine,
3735 HandlerDeps: port.HandlerDeps{
@@ -66,7 +64,6 @@ func TestNew_WiresClipboardOrchestratorWhenClipboardPresent(t *testing.T) {
6664
6765 deps := &Dependencies{
6866 Ctx: ctx,
69- Config: &config.Config{},
7067 RuntimeConfig: runtimeConfig,
7168 Engine: engine,
7269 HandlerDeps: port.HandlerDeps{
@@ -8,7 +8,6 @@ import (
8 "github.com/bnema/dumber/internal/application/port"8 "github.com/bnema/dumber/internal/application/port"
9 portmocks "github.com/bnema/dumber/internal/application/port/mocks"9 portmocks "github.com/bnema/dumber/internal/application/port/mocks"
10 "github.com/bnema/dumber/internal/domain/entity"10 "github.com/bnema/dumber/internal/domain/entity"
11- "github.com/bnema/dumber/internal/infrastructure/config"
12 "github.com/stretchr/testify/mock"11 "github.com/stretchr/testify/mock"
13 "github.com/stretchr/testify/require"12 "github.com/stretchr/testify/require"
14 )13 )
@@ -31,7 +30,6 @@ func TestNew_DoesNotWireClipboardOrchestratorWhenClipboardMissing(t *testing.T)
31 30
32 deps := &Dependencies{31 deps := &Dependencies{
33 Ctx: ctx,32 Ctx: ctx,
34- Config: &config.Config{},
35 RuntimeConfig: runtimeConfig,33 RuntimeConfig: runtimeConfig,
36 Engine: engine,34 Engine: engine,
37 HandlerDeps: port.HandlerDeps{35 HandlerDeps: port.HandlerDeps{
@@ -66,7 +64,6 @@ func TestNew_WiresClipboardOrchestratorWhenClipboardPresent(t *testing.T) {
66 64
67 deps := &Dependencies{65 deps := &Dependencies{
68 Ctx: ctx,66 Ctx: ctx,
69- Config: &config.Config{},
70 RuntimeConfig: runtimeConfig,67 RuntimeConfig: runtimeConfig,
71 Engine: engine,68 Engine: engine,
72 HandlerDeps: port.HandlerDeps{69 HandlerDeps: port.HandlerDeps{
M internal/ui/app_omnibox_config_test.go +28 -7
@@ -3,20 +3,41 @@ package ui
33 import (
44 "testing"
55
6- configpkg "github.com/bnema/dumber/internal/infrastructure/config"
6+ "github.com/bnema/dumber/internal/application/port"
7+ "github.com/bnema/dumber/internal/domain/entity"
78 )
89
9-func TestBuildOmniboxConfig_UsesConfiguredMostVisitedDays(t *testing.T) {
10- deps := &Dependencies{
11- Config: &configpkg.Config{
12- Omnibox: configpkg.OmniboxConfig{
13- MostVisitedDays: 7,
10+func TestBuildOmniboxConfigUsesRuntimeUIConfig(t *testing.T) {
11+ deps := &Dependencies{}
12+ runtimeCfg := port.RuntimeUIConfig{
13+ DefaultUIScale: 1.35,
14+ DefaultSearchEngine: "https://search.example/?q=%s",
15+ SearchShortcuts: map[string]port.RuntimeSearchShortcut{
16+ "gh": {
17+ URL: "https://github.com/search?q=%s",
18+ Description: "GitHub",
1419 },
1520 },
21+ Omnibox: port.RuntimeOmniboxConfig{
22+ InitialBehavior: entity.OmniboxInitialBehaviorMostVisited,
23+ MostVisitedDays: 7,
24+ },
1625 }
1726
18- got := buildOmniboxConfig(deps, nil, omniboxCallbacks{})
27+ got := buildOmniboxConfig(deps, runtimeCfg, nil, omniboxCallbacks{})
1928 if got.MostVisitedDays != 7 {
2029 t.Fatalf("MostVisitedDays = %d, want 7", got.MostVisitedDays)
2130 }
31+ if got.DefaultSearch != "https://search.example/?q=%s" {
32+ t.Fatalf("DefaultSearch = %q, want runtime default search", got.DefaultSearch)
33+ }
34+ if got.InitialBehavior != entity.OmniboxInitialBehaviorMostVisited {
35+ t.Fatalf("InitialBehavior = %q, want most_visited", got.InitialBehavior)
36+ }
37+ if got.UIScale != 1.35 {
38+ t.Fatalf("UIScale = %v, want 1.35", got.UIScale)
39+ }
40+ if got.ShortcutsUC == nil {
41+ t.Fatal("ShortcutsUC is nil")
42+ }
2243 }
@@ -3,20 +3,41 @@ package ui
3 import (3 import (
4 "testing"4 "testing"
5 5
6- configpkg "github.com/bnema/dumber/internal/infrastructure/config"6+ "github.com/bnema/dumber/internal/application/port"
7+ "github.com/bnema/dumber/internal/domain/entity"
7 )8 )
8 9
9-func TestBuildOmniboxConfig_UsesConfiguredMostVisitedDays(t *testing.T) {10+func TestBuildOmniboxConfigUsesRuntimeUIConfig(t *testing.T) {
10- deps := &Dependencies{11+ deps := &Dependencies{}
11- Config: &configpkg.Config{12+ runtimeCfg := port.RuntimeUIConfig{
12- Omnibox: configpkg.OmniboxConfig{13+ DefaultUIScale: 1.35,
13- MostVisitedDays: 7,14+ DefaultSearchEngine: "https://search.example/?q=%s",
15+ SearchShortcuts: map[string]port.RuntimeSearchShortcut{
16+ "gh": {
17+ URL: "https://github.com/search?q=%s",
18+ Description: "GitHub",
14 },19 },
15 },20 },
21+ Omnibox: port.RuntimeOmniboxConfig{
22+ InitialBehavior: entity.OmniboxInitialBehaviorMostVisited,
23+ MostVisitedDays: 7,
24+ },
16 }25 }
17 26
18- got := buildOmniboxConfig(deps, nil, omniboxCallbacks{})27+ got := buildOmniboxConfig(deps, runtimeCfg, nil, omniboxCallbacks{})
19 if got.MostVisitedDays != 7 {28 if got.MostVisitedDays != 7 {
20 t.Fatalf("MostVisitedDays = %d, want 7", got.MostVisitedDays)29 t.Fatalf("MostVisitedDays = %d, want 7", got.MostVisitedDays)
21 }30 }
31+ if got.DefaultSearch != "https://search.example/?q=%s" {
32+ t.Fatalf("DefaultSearch = %q, want runtime default search", got.DefaultSearch)
33+ }
34+ if got.InitialBehavior != entity.OmniboxInitialBehaviorMostVisited {
35+ t.Fatalf("InitialBehavior = %q, want most_visited", got.InitialBehavior)
36+ }
37+ if got.UIScale != 1.35 {
38+ t.Fatalf("UIScale = %v, want 1.35", got.UIScale)
39+ }
40+ if got.ShortcutsUC == nil {
41+ t.Fatal("ShortcutsUC is nil")
42+ }
22 }43 }
M internal/ui/browser_window.go +6 -4
@@ -195,10 +195,11 @@ func (bw *browserWindow) initAccentPicker(ctx context.Context, a *App) {
195195
196196 func (bw *browserWindow) initSessionManager(ctx context.Context, a *App) {
197197 log := logging.FromContext(ctx)
198- if bw == nil || a == nil || a.deps == nil || a.deps.Config == nil || bw.mainWindow == nil {
198+ if bw == nil || a == nil || a.deps == nil || bw.mainWindow == nil {
199199 log.Debug().Msg("deps or main window not available, skipping session manager")
200200 return
201201 }
202+ runtimeCfg := a.runtimeConfigSnapshot().UI
202203
203204 var listSessionsUC *usecase.ListSessionsUseCase
204205 var deleteSessionUC *usecase.DeleteSessionUseCase
@@ -217,7 +218,7 @@ func (bw *browserWindow) initSessionManager(ctx context.Context, a *App) {
217218 ListSessionsUC: listSessionsUC,
218219 DeleteSessionUC: deleteSessionUC,
219220 CurrentSession: a.deps.CurrentSessionID,
220- UIScale: a.deps.Config.DefaultUIScale,
221+ UIScale: runtimeCfg.DefaultUIScale,
221222 OnClose: func() {
222223 log.Debug().Msg("session manager closed")
223224 },
@@ -249,13 +250,14 @@ func (bw *browserWindow) initSessionManager(ctx context.Context, a *App) {
249250
250251 func (bw *browserWindow) initTabPicker(ctx context.Context, a *App) {
251252 log := logging.FromContext(ctx)
252- if bw == nil || a == nil || a.deps == nil || a.deps.Config == nil {
253+ if bw == nil || a == nil || a.deps == nil {
253254 log.Debug().Msg("deps/config not available, skipping tab picker")
254255 return
255256 }
257+ runtimeCfg := a.runtimeConfigSnapshot().UI
256258
257259 bw.tabPicker = component.NewTabPicker(ctx, component.TabPickerConfig{
258- UIScale: a.deps.Config.DefaultUIScale,
260+ UIScale: runtimeCfg.DefaultUIScale,
259261 OnClose: func() {
260262 log.Debug().Msg("tab picker closed")
261263 },
@@ -195,10 +195,11 @@ func (bw *browserWindow) initAccentPicker(ctx context.Context, a *App) {
195 195
196 func (bw *browserWindow) initSessionManager(ctx context.Context, a *App) {196 func (bw *browserWindow) initSessionManager(ctx context.Context, a *App) {
197 log := logging.FromContext(ctx)197 log := logging.FromContext(ctx)
198- if bw == nil || a == nil || a.deps == nil || a.deps.Config == nil || bw.mainWindow == nil {198+ if bw == nil || a == nil || a.deps == nil || bw.mainWindow == nil {
199 log.Debug().Msg("deps or main window not available, skipping session manager")199 log.Debug().Msg("deps or main window not available, skipping session manager")
200 return200 return
201 }201 }
202+ runtimeCfg := a.runtimeConfigSnapshot().UI
202 203
203 var listSessionsUC *usecase.ListSessionsUseCase204 var listSessionsUC *usecase.ListSessionsUseCase
204 var deleteSessionUC *usecase.DeleteSessionUseCase205 var deleteSessionUC *usecase.DeleteSessionUseCase
@@ -217,7 +218,7 @@ func (bw *browserWindow) initSessionManager(ctx context.Context, a *App) {
217 ListSessionsUC: listSessionsUC,218 ListSessionsUC: listSessionsUC,
218 DeleteSessionUC: deleteSessionUC,219 DeleteSessionUC: deleteSessionUC,
219 CurrentSession: a.deps.CurrentSessionID,220 CurrentSession: a.deps.CurrentSessionID,
220- UIScale: a.deps.Config.DefaultUIScale,221+ UIScale: runtimeCfg.DefaultUIScale,
221 OnClose: func() {222 OnClose: func() {
222 log.Debug().Msg("session manager closed")223 log.Debug().Msg("session manager closed")
223 },224 },
@@ -249,13 +250,14 @@ func (bw *browserWindow) initSessionManager(ctx context.Context, a *App) {
249 250
250 func (bw *browserWindow) initTabPicker(ctx context.Context, a *App) {251 func (bw *browserWindow) initTabPicker(ctx context.Context, a *App) {
251 log := logging.FromContext(ctx)252 log := logging.FromContext(ctx)
252- if bw == nil || a == nil || a.deps == nil || a.deps.Config == nil {253+ if bw == nil || a == nil || a.deps == nil {
253 log.Debug().Msg("deps/config not available, skipping tab picker")254 log.Debug().Msg("deps/config not available, skipping tab picker")
254 return255 return
255 }256 }
257+ runtimeCfg := a.runtimeConfigSnapshot().UI
256 258
257 bw.tabPicker = component.NewTabPicker(ctx, component.TabPickerConfig{259 bw.tabPicker = component.NewTabPicker(ctx, component.TabPickerConfig{
258- UIScale: a.deps.Config.DefaultUIScale,260+ UIScale: runtimeCfg.DefaultUIScale,
259 OnClose: func() {261 OnClose: func() {
260 log.Debug().Msg("tab picker closed")262 log.Debug().Msg("tab picker closed")
261 },263 },
M internal/ui/browser_window_history_sidebar.go +2 -2
@@ -127,10 +127,10 @@ func historySidebarWidthConfig(widthPx int) window.SidebarWidthConfig {
127127 // applySidebarWidthConfig applies the config-backed sidebar width to the
128128 // main window's sidebar.
129129 func (bw *browserWindow) applySidebarWidthConfig(a *App) {
130- if bw == nil || bw.mainWindow == nil || a == nil || a.deps == nil || a.deps.Config == nil {
130+ if bw == nil || bw.mainWindow == nil || a == nil {
131131 return
132132 }
133- bw.mainWindow.SetSidebarWidth(historySidebarWidthConfig(a.deps.Config.SidebarWidth))
133+ bw.mainWindow.SetSidebarWidth(historySidebarWidthConfig(a.runtimeConfigSnapshot().UI.SidebarWidth))
134134 }
135135
136136 // toggleHistorySidebarAction is the keyboard-action handler for toggling the
@@ -127,10 +127,10 @@ func historySidebarWidthConfig(widthPx int) window.SidebarWidthConfig {
127 // applySidebarWidthConfig applies the config-backed sidebar width to the127 // applySidebarWidthConfig applies the config-backed sidebar width to the
128 // main window's sidebar.128 // main window's sidebar.
129 func (bw *browserWindow) applySidebarWidthConfig(a *App) {129 func (bw *browserWindow) applySidebarWidthConfig(a *App) {
130- if bw == nil || bw.mainWindow == nil || a == nil || a.deps == nil || a.deps.Config == nil {130+ if bw == nil || bw.mainWindow == nil || a == nil {
131 return131 return
132 }132 }
133- bw.mainWindow.SetSidebarWidth(historySidebarWidthConfig(a.deps.Config.SidebarWidth))133+ bw.mainWindow.SetSidebarWidth(historySidebarWidthConfig(a.runtimeConfigSnapshot().UI.SidebarWidth))
134 }134 }
135 135
136 // toggleHistorySidebarAction is the keyboard-action handler for toggling the136 // toggleHistorySidebarAction is the keyboard-action handler for toggling the
M internal/ui/browser_window_history_sidebar_test.go +1 -1
@@ -877,7 +877,7 @@ func TestHistorySidebarWidthConfig_DefaultValue(t *testing.T) {
877877 // that applySidebarWidthConfig handles nil mainWindow without panic.
878878 func TestBrowserWindow_ApplySidebarWidthConfig_NilMainWindowIsSafe(t *testing.T) {
879879 bw := &browserWindow{mainWindow: nil}
880- app := &App{deps: &Dependencies{Config: &config.Config{SidebarWidth: 300}}}
880+ app := appWithRuntimeConfigForTest(&config.Config{SidebarWidth: 300})
881881 require.NotPanics(t, func() { bw.applySidebarWidthConfig(app) })
882882 }
883883
@@ -877,7 +877,7 @@ func TestHistorySidebarWidthConfig_DefaultValue(t *testing.T) {
877 // that applySidebarWidthConfig handles nil mainWindow without panic.877 // that applySidebarWidthConfig handles nil mainWindow without panic.
878 func TestBrowserWindow_ApplySidebarWidthConfig_NilMainWindowIsSafe(t *testing.T) {878 func TestBrowserWindow_ApplySidebarWidthConfig_NilMainWindowIsSafe(t *testing.T) {
879 bw := &browserWindow{mainWindow: nil}879 bw := &browserWindow{mainWindow: nil}
880- app := &App{deps: &Dependencies{Config: &config.Config{SidebarWidth: 300}}}880+ app := appWithRuntimeConfigForTest(&config.Config{SidebarWidth: 300})
881 require.NotPanics(t, func() { bw.applySidebarWidthConfig(app) })881 require.NotPanics(t, func() { bw.applySidebarWidthConfig(app) })
882 }882 }
883 883
M internal/ui/browser_window_test.go +30 -24
@@ -137,14 +137,16 @@ func TestTabCoordinator_CloseReleasesClosedTabWorkspaceWebViews(t *testing.T) {
137137
138138 bw := &browserWindow{id: "window-1", tabs: tabs}
139139 app := &App{
140- deps: &Dependencies{Config: config.DefaultConfig()},
141- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
142- contentCoord: contentCoord,
143- browserWindows: map[string]*browserWindow{bw.id: bw},
144- tabs: entity.NewTabList(),
145- workspaceViews: map[entity.TabID]*component.WorkspaceView{closedTab.ID: {}, survivingTab.ID: {}},
146- windowForTab: map[entity.TabID]*browserWindow{closedTab.ID: bw, survivingTab.ID: bw},
147- floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
140+ deps: &Dependencies{},
141+ runtimeConfig: runtimeConfigSnapshotForTest(config.DefaultConfig()),
142+ runtimeConfigLoaded: true,
143+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
144+ contentCoord: contentCoord,
145+ browserWindows: map[string]*browserWindow{bw.id: bw},
146+ tabs: entity.NewTabList(),
147+ workspaceViews: map[entity.TabID]*component.WorkspaceView{closedTab.ID: {}, survivingTab.ID: {}},
148+ windowForTab: map[entity.TabID]*browserWindow{closedTab.ID: bw, survivingTab.ID: bw},
149+ floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
148150 }
149151 app.tabs.Add(closedTab)
150152 app.tabs.Add(survivingTab)
@@ -180,14 +182,16 @@ func TestTabCoordinator_SwitchDoesNotReleaseWorkspaceWebViews(t *testing.T) {
180182
181183 bw := &browserWindow{id: "window-1", tabs: tabs}
182184 app := &App{
183- deps: &Dependencies{Config: config.DefaultConfig()},
184- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
185- contentCoord: contentCoord,
186- browserWindows: map[string]*browserWindow{bw.id: bw},
187- tabs: entity.NewTabList(),
188- workspaceViews: map[entity.TabID]*component.WorkspaceView{firstTab.ID: {}, secondTab.ID: {}},
189- windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: bw, secondTab.ID: bw},
190- floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
185+ deps: &Dependencies{},
186+ runtimeConfig: runtimeConfigSnapshotForTest(config.DefaultConfig()),
187+ runtimeConfigLoaded: true,
188+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
189+ contentCoord: contentCoord,
190+ browserWindows: map[string]*browserWindow{bw.id: bw},
191+ tabs: entity.NewTabList(),
192+ workspaceViews: map[entity.TabID]*component.WorkspaceView{firstTab.ID: {}, secondTab.ID: {}},
193+ windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: bw, secondTab.ID: bw},
194+ floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
191195 }
192196 app.tabs.Add(firstTab)
193197 app.tabs.Add(secondTab)
@@ -214,14 +218,16 @@ func TestTabCoordinator_CloseLastTabReleasesWorkspaceBeforeWindowRemoval(t *test
214218
215219 bw := &browserWindow{id: "window-1", tabs: tabs}
216220 app := &App{
217- deps: &Dependencies{Config: config.DefaultConfig()},
218- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
219- contentCoord: contentCoord,
220- browserWindows: map[string]*browserWindow{bw.id: bw},
221- tabs: entity.NewTabList(),
222- workspaceViews: map[entity.TabID]*component.WorkspaceView{closedTab.ID: {}},
223- windowForTab: map[entity.TabID]*browserWindow{closedTab.ID: bw},
224- floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
221+ deps: &Dependencies{},
222+ runtimeConfig: runtimeConfigSnapshotForTest(config.DefaultConfig()),
223+ runtimeConfigLoaded: true,
224+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
225+ contentCoord: contentCoord,
226+ browserWindows: map[string]*browserWindow{bw.id: bw},
227+ tabs: entity.NewTabList(),
228+ workspaceViews: map[entity.TabID]*component.WorkspaceView{closedTab.ID: {}},
229+ windowForTab: map[entity.TabID]*browserWindow{closedTab.ID: bw},
230+ floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
225231 }
226232 app.tabs.Add(closedTab)
227233 app.initTabCoordinator(ctx)
@@ -137,14 +137,16 @@ func TestTabCoordinator_CloseReleasesClosedTabWorkspaceWebViews(t *testing.T) {
137 137
138 bw := &browserWindow{id: "window-1", tabs: tabs}138 bw := &browserWindow{id: "window-1", tabs: tabs}
139 app := &App{139 app := &App{
140- deps: &Dependencies{Config: config.DefaultConfig()},140+ deps: &Dependencies{},
141- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),141+ runtimeConfig: runtimeConfigSnapshotForTest(config.DefaultConfig()),
142- contentCoord: contentCoord,142+ runtimeConfigLoaded: true,
143- browserWindows: map[string]*browserWindow{bw.id: bw},143+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
144- tabs: entity.NewTabList(),144+ contentCoord: contentCoord,
145- workspaceViews: map[entity.TabID]*component.WorkspaceView{closedTab.ID: {}, survivingTab.ID: {}},145+ browserWindows: map[string]*browserWindow{bw.id: bw},
146- windowForTab: map[entity.TabID]*browserWindow{closedTab.ID: bw, survivingTab.ID: bw},146+ tabs: entity.NewTabList(),
147- floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},147+ workspaceViews: map[entity.TabID]*component.WorkspaceView{closedTab.ID: {}, survivingTab.ID: {}},
148+ windowForTab: map[entity.TabID]*browserWindow{closedTab.ID: bw, survivingTab.ID: bw},
149+ floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
148 }150 }
149 app.tabs.Add(closedTab)151 app.tabs.Add(closedTab)
150 app.tabs.Add(survivingTab)152 app.tabs.Add(survivingTab)
@@ -180,14 +182,16 @@ func TestTabCoordinator_SwitchDoesNotReleaseWorkspaceWebViews(t *testing.T) {
180 182
181 bw := &browserWindow{id: "window-1", tabs: tabs}183 bw := &browserWindow{id: "window-1", tabs: tabs}
182 app := &App{184 app := &App{
183- deps: &Dependencies{Config: config.DefaultConfig()},185+ deps: &Dependencies{},
184- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),186+ runtimeConfig: runtimeConfigSnapshotForTest(config.DefaultConfig()),
185- contentCoord: contentCoord,187+ runtimeConfigLoaded: true,
186- browserWindows: map[string]*browserWindow{bw.id: bw},188+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
187- tabs: entity.NewTabList(),189+ contentCoord: contentCoord,
188- workspaceViews: map[entity.TabID]*component.WorkspaceView{firstTab.ID: {}, secondTab.ID: {}},190+ browserWindows: map[string]*browserWindow{bw.id: bw},
189- windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: bw, secondTab.ID: bw},191+ tabs: entity.NewTabList(),
190- floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},192+ workspaceViews: map[entity.TabID]*component.WorkspaceView{firstTab.ID: {}, secondTab.ID: {}},
193+ windowForTab: map[entity.TabID]*browserWindow{firstTab.ID: bw, secondTab.ID: bw},
194+ floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
191 }195 }
192 app.tabs.Add(firstTab)196 app.tabs.Add(firstTab)
193 app.tabs.Add(secondTab)197 app.tabs.Add(secondTab)
@@ -214,14 +218,16 @@ func TestTabCoordinator_CloseLastTabReleasesWorkspaceBeforeWindowRemoval(t *test
214 218
215 bw := &browserWindow{id: "window-1", tabs: tabs}219 bw := &browserWindow{id: "window-1", tabs: tabs}
216 app := &App{220 app := &App{
217- deps: &Dependencies{Config: config.DefaultConfig()},221+ deps: &Dependencies{},
218- tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),222+ runtimeConfig: runtimeConfigSnapshotForTest(config.DefaultConfig()),
219- contentCoord: contentCoord,223+ runtimeConfigLoaded: true,
220- browserWindows: map[string]*browserWindow{bw.id: bw},224+ tabsUC: usecase.NewManageTabsUseCase(counterIDGen()),
221- tabs: entity.NewTabList(),225+ contentCoord: contentCoord,
222- workspaceViews: map[entity.TabID]*component.WorkspaceView{closedTab.ID: {}},226+ browserWindows: map[string]*browserWindow{bw.id: bw},
223- windowForTab: map[entity.TabID]*browserWindow{closedTab.ID: bw},227+ tabs: entity.NewTabList(),
224- floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},228+ workspaceViews: map[entity.TabID]*component.WorkspaceView{closedTab.ID: {}},
229+ windowForTab: map[entity.TabID]*browserWindow{closedTab.ID: bw},
230+ floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{},
225 }231 }
226 app.tabs.Add(closedTab)232 app.tabs.Add(closedTab)
227 app.initTabCoordinator(ctx)233 app.initTabCoordinator(ctx)
M internal/ui/config_migration.go +3 -3
@@ -12,11 +12,11 @@ import (
1212 func (a *App) checkConfigMigration(ctx context.Context) {
1313 log := logging.FromContext(ctx)
1414
15- if a.deps.ConfigMigrator == nil {
15+ if a.deps.MigrationChecker == nil {
1616 return
1717 }
1818
19- result, err := a.deps.ConfigMigrator.CheckMigration()
19+ result, err := a.deps.MigrationChecker.CheckMigration()
2020 if err != nil {
2121 log.Debug().Err(err).Msg("config migration check failed")
2222 return
@@ -32,7 +32,7 @@ func (a *App) checkConfigMigration(ctx context.Context) {
3232 Msg("config migration available")
3333
3434 // Show toast notification if enabled
35- if a.deps.Config != nil && a.deps.Config.Update.NotifyOnNewSettings {
35+ if a.runtimeConfigSnapshot().UI.Update.NotifyOnNewSettings {
3636 msg := fmt.Sprintf("Config has %d new settings. Run 'dumber config migrate'", len(result.MissingKeys))
3737 a.showToastOnLastFocusedBrowserWindow(ctx, msg, component.ToastInfo)
3838 }
@@ -12,11 +12,11 @@ import (
12 func (a *App) checkConfigMigration(ctx context.Context) {12 func (a *App) checkConfigMigration(ctx context.Context) {
13 log := logging.FromContext(ctx)13 log := logging.FromContext(ctx)
14 14
15- if a.deps.ConfigMigrator == nil {15+ if a.deps.MigrationChecker == nil {
16 return16 return
17 }17 }
18 18
19- result, err := a.deps.ConfigMigrator.CheckMigration()19+ result, err := a.deps.MigrationChecker.CheckMigration()
20 if err != nil {20 if err != nil {
21 log.Debug().Err(err).Msg("config migration check failed")21 log.Debug().Err(err).Msg("config migration check failed")
22 return22 return
@@ -32,7 +32,7 @@ func (a *App) checkConfigMigration(ctx context.Context) {
32 Msg("config migration available")32 Msg("config migration available")
33 33
34 // Show toast notification if enabled34 // Show toast notification if enabled
35- if a.deps.Config != nil && a.deps.Config.Update.NotifyOnNewSettings {35+ if a.runtimeConfigSnapshot().UI.Update.NotifyOnNewSettings {
36 msg := fmt.Sprintf("Config has %d new settings. Run 'dumber config migrate'", len(result.MissingKeys))36 msg := fmt.Sprintf("Config has %d new settings. Run 'dumber config migrate'", len(result.MissingKeys))
37 a.showToastOnLastFocusedBrowserWindow(ctx, msg, component.ToastInfo)37 a.showToastOnLastFocusedBrowserWindow(ctx, msg, component.ToastInfo)
38 }38 }
M internal/ui/deps.go +1 -12
@@ -8,7 +8,6 @@ import (
88 "github.com/bnema/dumber/internal/application/usecase"
99 "github.com/bnema/dumber/internal/domain/entity"
1010 "github.com/bnema/dumber/internal/domain/repository"
11- "github.com/bnema/dumber/internal/infrastructure/config"
1211 "github.com/bnema/dumber/internal/ui/adapter"
1312 "github.com/bnema/dumber/internal/ui/theme"
1413 "github.com/bnema/puregotk/v4/gtk"
@@ -19,9 +18,7 @@ import (
1918 type Dependencies struct {
2019 // Core context and configuration
2120 Ctx context.Context
22- Config *config.Config // TODO: replace with port interface in a later milestone
2321 RuntimeConfig port.RuntimeConfigProvider
24- EngineSettingsPayload func() port.EngineSettingsPayload
2522 InitialURL string // URL to open on startup (optional)
2623 RestoreSessionID string // Session ID to restore on startup (optional)
2724 StartupCrashReports []string
@@ -91,18 +88,13 @@ type Dependencies struct {
9188 SessionSpawner port.SessionSpawner
9289 // FileSystem provides file operations (e.g., for download deduplication).
9390 FileSystem port.FileSystem
94- // OnConfigChange registers a callback for config hot-reload.
95- // The config pointer in deps.Config is updated in-place before the callback fires.
96- OnConfigChange func(callback func())
97- // WatchConfig starts watching the config file for changes.
98- WatchConfig func() error
9991
10092 // Update management
10193 CheckUpdateUC *usecase.CheckUpdateUseCase
10294 ApplyUpdateUC *usecase.ApplyUpdateUseCase
10395
10496 // Config migration checker (optional; nil disables migration notifications)
105- ConfigMigrator port.ConfigMigrator
97+ MigrationChecker port.ConfigMigrator
10698
10799 // LaunchExternalURL opens a URI with the system's default handler (e.g. xdg-open).
108100 // Used for external URL schemes (vscode://, spotify://, etc.).
@@ -121,9 +113,6 @@ func (d *Dependencies) Validate() error {
121113 if d.Ctx == nil {
122114 return ErrMissingDependency("Ctx")
123115 }
124- if d.Config == nil {
125- return ErrMissingDependency("Config")
126- }
127116 if d.RuntimeConfig == nil {
128117 return ErrMissingDependency("RuntimeConfig")
129118 }
@@ -8,7 +8,6 @@ import (
8 "github.com/bnema/dumber/internal/application/usecase"8 "github.com/bnema/dumber/internal/application/usecase"
9 "github.com/bnema/dumber/internal/domain/entity"9 "github.com/bnema/dumber/internal/domain/entity"
10 "github.com/bnema/dumber/internal/domain/repository"10 "github.com/bnema/dumber/internal/domain/repository"
11- "github.com/bnema/dumber/internal/infrastructure/config"
12 "github.com/bnema/dumber/internal/ui/adapter"11 "github.com/bnema/dumber/internal/ui/adapter"
13 "github.com/bnema/dumber/internal/ui/theme"12 "github.com/bnema/dumber/internal/ui/theme"
14 "github.com/bnema/puregotk/v4/gtk"13 "github.com/bnema/puregotk/v4/gtk"
@@ -19,9 +18,7 @@ import (
19 type Dependencies struct {18 type Dependencies struct {
20 // Core context and configuration19 // Core context and configuration
21 Ctx context.Context20 Ctx context.Context
22- Config *config.Config // TODO: replace with port interface in a later milestone
23 RuntimeConfig port.RuntimeConfigProvider21 RuntimeConfig port.RuntimeConfigProvider
24- EngineSettingsPayload func() port.EngineSettingsPayload
25 InitialURL string // URL to open on startup (optional)22 InitialURL string // URL to open on startup (optional)
26 RestoreSessionID string // Session ID to restore on startup (optional)23 RestoreSessionID string // Session ID to restore on startup (optional)
27 StartupCrashReports []string24 StartupCrashReports []string
@@ -91,18 +88,13 @@ type Dependencies struct {
91 SessionSpawner port.SessionSpawner88 SessionSpawner port.SessionSpawner
92 // FileSystem provides file operations (e.g., for download deduplication).89 // FileSystem provides file operations (e.g., for download deduplication).
93 FileSystem port.FileSystem90 FileSystem port.FileSystem
94- // OnConfigChange registers a callback for config hot-reload.
95- // The config pointer in deps.Config is updated in-place before the callback fires.
96- OnConfigChange func(callback func())
97- // WatchConfig starts watching the config file for changes.
98- WatchConfig func() error
99 91
100 // Update management92 // Update management
101 CheckUpdateUC *usecase.CheckUpdateUseCase93 CheckUpdateUC *usecase.CheckUpdateUseCase
102 ApplyUpdateUC *usecase.ApplyUpdateUseCase94 ApplyUpdateUC *usecase.ApplyUpdateUseCase
103 95
104 // Config migration checker (optional; nil disables migration notifications)96 // Config migration checker (optional; nil disables migration notifications)
105- ConfigMigrator port.ConfigMigrator97+ MigrationChecker port.ConfigMigrator
106 98
107 // LaunchExternalURL opens a URI with the system's default handler (e.g. xdg-open).99 // LaunchExternalURL opens a URI with the system's default handler (e.g. xdg-open).
108 // Used for external URL schemes (vscode://, spotify://, etc.).100 // Used for external URL schemes (vscode://, spotify://, etc.).
@@ -121,9 +113,6 @@ func (d *Dependencies) Validate() error {
121 if d.Ctx == nil {113 if d.Ctx == nil {
122 return ErrMissingDependency("Ctx")114 return ErrMissingDependency("Ctx")
123 }115 }
124- if d.Config == nil {
125- return ErrMissingDependency("Config")
126- }
127 if d.RuntimeConfig == nil {116 if d.RuntimeConfig == nil {
128 return ErrMissingDependency("RuntimeConfig")117 return ErrMissingDependency("RuntimeConfig")
129 }118 }
M internal/ui/external_theme_reload_test.go +14 -12
@@ -11,6 +11,7 @@ import (
1111 "github.com/bnema/dumber/internal/application/port"
1212 portmocks "github.com/bnema/dumber/internal/application/port/mocks"
1313 "github.com/bnema/dumber/internal/application/usecase"
14+ "github.com/bnema/dumber/internal/bootstrap"
1415 "github.com/bnema/dumber/internal/domain/entity"
1516 "github.com/bnema/dumber/internal/infrastructure/config"
1617 "github.com/bnema/dumber/internal/shared/syncdispatch"
@@ -103,19 +104,15 @@ func TestApplyAppearanceConfigSendsCompleteEngineSettingsPayload(t *testing.T) {
103104 Return(nil).
104105 Once()
105106 app := &App{
106- deps: &Dependencies{
107- Config: cfg,
108- EngineSettingsPayload: func() port.EngineSettingsPayload {
109- return expected
110- },
111- },
112- engine: engine,
107+ deps: &Dependencies{},
108+ runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),
109+ runtimeConfigLoaded: true,
110+ engine: engine,
113111 }
114112
115113 app.applyAppearanceConfig(ctx)
116114
117115 require.Equal(t, expected, captured.Settings)
118- require.Nil(t, captured.Raw)
119116 }
120117
121118 func TestExternalThemeWatcherCallbackAppliesThemeThroughSharedPath(t *testing.T) {
@@ -147,12 +144,13 @@ func TestExternalThemeWatcherCallbackAppliesThemeThroughSharedPath(t *testing.T)
147144 engine.EXPECT().ContentInjector().Return(nil).Once()
148145 app := &App{
149146 deps: &Dependencies{
150- Config: cfg,
151147 Theme: manager,
152148 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),
153149 ExternalThemeSource: source,
154150 ExternalThemeWatcher: watcher,
155151 },
152+ runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),
153+ runtimeConfigLoaded: true,
156154 dispatchOnMainThread: immediateDispatchForExternalThemeTest,
157155 engine: engine,
158156 }
@@ -182,11 +180,12 @@ func TestExternalThemeReloadKeepsLastGoodAndDisablingClearsIt(t *testing.T) {
182180 manager := uitheme.NewManager(ctx, resolvedThemeForUITest(cfg.Appearance.LightPalette, cfg.Appearance.DarkPalette))
183181 app := &App{
184182 deps: &Dependencies{
185- Config: cfg,
186183 Theme: manager,
187184 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),
188185 ExternalThemeSource: source,
189186 },
187+ runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),
188+ runtimeConfigLoaded: true,
190189 dispatchOnMainThread: immediateDispatchForExternalThemeTest,
191190 }
192191
@@ -203,6 +202,7 @@ func TestExternalThemeReloadKeepsLastGoodAndDisablingClearsIt(t *testing.T) {
203202 require.Equal(t, "#000000", app.deps.Theme.GetCurrentPalette().Background)
204203
205204 cfg.Appearance.ExternalTheme.Enabled = false
205+ app.updateRuntimeConfig(bootstrap.RuntimeConfigSnapshotFromConfig(cfg))
206206 app.applyAppearanceConfig(ctx)
207207 require.Equal(t, cfg.Appearance.DarkPalette.Background, app.deps.Theme.GetCurrentPalette().Background)
208208 }
@@ -221,11 +221,12 @@ func TestExternalThemeReloadKeepsLastGoodOnReadError(t *testing.T) {
221221 manager := uitheme.NewManager(ctx, resolvedThemeForUITest(cfg.Appearance.LightPalette, cfg.Appearance.DarkPalette))
222222 app := &App{
223223 deps: &Dependencies{
224- Config: cfg,
225224 Theme: manager,
226225 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),
227226 ExternalThemeSource: source,
228227 },
228+ runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),
229+ runtimeConfigLoaded: true,
229230 dispatchOnMainThread: immediateDispatchForExternalThemeTest,
230231 }
231232
@@ -252,11 +253,12 @@ func TestExternalThemeReloadRecoversAfterMalformedUpdate(t *testing.T) {
252253 manager := uitheme.NewManager(ctx, resolvedThemeForUITest(cfg.Appearance.LightPalette, cfg.Appearance.DarkPalette))
253254 app := &App{
254255 deps: &Dependencies{
255- Config: cfg,
256256 Theme: manager,
257257 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),
258258 ExternalThemeSource: source,
259259 },
260+ runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),
261+ runtimeConfigLoaded: true,
260262 dispatchOnMainThread: immediateDispatchForExternalThemeTest,
261263 }
262264
@@ -11,6 +11,7 @@ import (
11 "github.com/bnema/dumber/internal/application/port"11 "github.com/bnema/dumber/internal/application/port"
12 portmocks "github.com/bnema/dumber/internal/application/port/mocks"12 portmocks "github.com/bnema/dumber/internal/application/port/mocks"
13 "github.com/bnema/dumber/internal/application/usecase"13 "github.com/bnema/dumber/internal/application/usecase"
14+ "github.com/bnema/dumber/internal/bootstrap"
14 "github.com/bnema/dumber/internal/domain/entity"15 "github.com/bnema/dumber/internal/domain/entity"
15 "github.com/bnema/dumber/internal/infrastructure/config"16 "github.com/bnema/dumber/internal/infrastructure/config"
16 "github.com/bnema/dumber/internal/shared/syncdispatch"17 "github.com/bnema/dumber/internal/shared/syncdispatch"
@@ -103,19 +104,15 @@ func TestApplyAppearanceConfigSendsCompleteEngineSettingsPayload(t *testing.T) {
103 Return(nil).104 Return(nil).
104 Once()105 Once()
105 app := &App{106 app := &App{
106- deps: &Dependencies{107+ deps: &Dependencies{},
107- Config: cfg,108+ runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),
108- EngineSettingsPayload: func() port.EngineSettingsPayload {109+ runtimeConfigLoaded: true,
109- return expected110+ engine: engine,
110- },
111- },
112- engine: engine,
113 }111 }
114 112
115 app.applyAppearanceConfig(ctx)113 app.applyAppearanceConfig(ctx)
116 114
117 require.Equal(t, expected, captured.Settings)115 require.Equal(t, expected, captured.Settings)
118- require.Nil(t, captured.Raw)
119 }116 }
120 117
121 func TestExternalThemeWatcherCallbackAppliesThemeThroughSharedPath(t *testing.T) {118 func TestExternalThemeWatcherCallbackAppliesThemeThroughSharedPath(t *testing.T) {
@@ -147,12 +144,13 @@ func TestExternalThemeWatcherCallbackAppliesThemeThroughSharedPath(t *testing.T)
147 engine.EXPECT().ContentInjector().Return(nil).Once()144 engine.EXPECT().ContentInjector().Return(nil).Once()
148 app := &App{145 app := &App{
149 deps: &Dependencies{146 deps: &Dependencies{
150- Config: cfg,
151 Theme: manager,147 Theme: manager,
152 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),148 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),
153 ExternalThemeSource: source,149 ExternalThemeSource: source,
154 ExternalThemeWatcher: watcher,150 ExternalThemeWatcher: watcher,
155 },151 },
152+ runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),
153+ runtimeConfigLoaded: true,
156 dispatchOnMainThread: immediateDispatchForExternalThemeTest,154 dispatchOnMainThread: immediateDispatchForExternalThemeTest,
157 engine: engine,155 engine: engine,
158 }156 }
@@ -182,11 +180,12 @@ func TestExternalThemeReloadKeepsLastGoodAndDisablingClearsIt(t *testing.T) {
182 manager := uitheme.NewManager(ctx, resolvedThemeForUITest(cfg.Appearance.LightPalette, cfg.Appearance.DarkPalette))180 manager := uitheme.NewManager(ctx, resolvedThemeForUITest(cfg.Appearance.LightPalette, cfg.Appearance.DarkPalette))
183 app := &App{181 app := &App{
184 deps: &Dependencies{182 deps: &Dependencies{
185- Config: cfg,
186 Theme: manager,183 Theme: manager,
187 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),184 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),
188 ExternalThemeSource: source,185 ExternalThemeSource: source,
189 },186 },
187+ runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),
188+ runtimeConfigLoaded: true,
190 dispatchOnMainThread: immediateDispatchForExternalThemeTest,189 dispatchOnMainThread: immediateDispatchForExternalThemeTest,
191 }190 }
192 191
@@ -203,6 +202,7 @@ func TestExternalThemeReloadKeepsLastGoodAndDisablingClearsIt(t *testing.T) {
203 require.Equal(t, "#000000", app.deps.Theme.GetCurrentPalette().Background)202 require.Equal(t, "#000000", app.deps.Theme.GetCurrentPalette().Background)
204 203
205 cfg.Appearance.ExternalTheme.Enabled = false204 cfg.Appearance.ExternalTheme.Enabled = false
205+ app.updateRuntimeConfig(bootstrap.RuntimeConfigSnapshotFromConfig(cfg))
206 app.applyAppearanceConfig(ctx)206 app.applyAppearanceConfig(ctx)
207 require.Equal(t, cfg.Appearance.DarkPalette.Background, app.deps.Theme.GetCurrentPalette().Background)207 require.Equal(t, cfg.Appearance.DarkPalette.Background, app.deps.Theme.GetCurrentPalette().Background)
208 }208 }
@@ -221,11 +221,12 @@ func TestExternalThemeReloadKeepsLastGoodOnReadError(t *testing.T) {
221 manager := uitheme.NewManager(ctx, resolvedThemeForUITest(cfg.Appearance.LightPalette, cfg.Appearance.DarkPalette))221 manager := uitheme.NewManager(ctx, resolvedThemeForUITest(cfg.Appearance.LightPalette, cfg.Appearance.DarkPalette))
222 app := &App{222 app := &App{
223 deps: &Dependencies{223 deps: &Dependencies{
224- Config: cfg,
225 Theme: manager,224 Theme: manager,
226 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),225 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),
227 ExternalThemeSource: source,226 ExternalThemeSource: source,
228 },227 },
228+ runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),
229+ runtimeConfigLoaded: true,
229 dispatchOnMainThread: immediateDispatchForExternalThemeTest,230 dispatchOnMainThread: immediateDispatchForExternalThemeTest,
230 }231 }
231 232
@@ -252,11 +253,12 @@ func TestExternalThemeReloadRecoversAfterMalformedUpdate(t *testing.T) {
252 manager := uitheme.NewManager(ctx, resolvedThemeForUITest(cfg.Appearance.LightPalette, cfg.Appearance.DarkPalette))253 manager := uitheme.NewManager(ctx, resolvedThemeForUITest(cfg.Appearance.LightPalette, cfg.Appearance.DarkPalette))
253 app := &App{254 app := &App{
254 deps: &Dependencies{255 deps: &Dependencies{
255- Config: cfg,
256 Theme: manager,256 Theme: manager,
257 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),257 ResolveThemeUC: usecase.NewResolveThemeUseCase(source),
258 ExternalThemeSource: source,258 ExternalThemeSource: source,
259 },259 },
260+ runtimeConfig: bootstrap.RuntimeConfigSnapshotFromConfig(cfg),
261+ runtimeConfigLoaded: true,
260 dispatchOnMainThread: immediateDispatchForExternalThemeTest,262 dispatchOnMainThread: immediateDispatchForExternalThemeTest,
261 }263 }
262 264
M internal/ui/runtime_config_test.go +24 -0
@@ -1,13 +1,37 @@
11 package ui
22
33 import (
4+ "context"
45 "testing"
56
7+ "github.com/bnema/dumber/internal/application/dto"
68 "github.com/bnema/dumber/internal/application/port"
79 portmocks "github.com/bnema/dumber/internal/application/port/mocks"
810 "github.com/bnema/dumber/internal/domain/entity"
911 )
1012
13+func TestDependenciesValidateAcceptsRuntimeConfigOnly(t *testing.T) {
14+ runtimeConfig := portmocks.NewMockRuntimeConfigProvider(t)
15+ engine := portmocks.NewMockEngine(t)
16+ deps := &Dependencies{
17+ Ctx: context.Background(),
18+ RuntimeConfig: runtimeConfig,
19+ Engine: engine,
20+ HandlerDeps: port.HandlerDeps{
21+ SaveConfig: func(context.Context, dto.WebUIConfig) error {
22+ return nil
23+ },
24+ SaveOmniboxInitialBehavior: func(context.Context, entity.OmniboxInitialBehavior) error {
25+ return nil
26+ },
27+ },
28+ }
29+
30+ if err := deps.Validate(); err != nil {
31+ t.Fatalf("Validate() error = %v, want nil", err)
32+ }
33+}
34+
1135 func TestAppRuntimeConfigReturnsProviderSnapshot(t *testing.T) {
1236 provider := portmocks.NewMockRuntimeConfigProvider(t)
1337 provider.EXPECT().
@@ -1,13 +1,37 @@
1 package ui1 package ui
2 2
3 import (3 import (
4+ "context"
4 "testing"5 "testing"
5 6
7+ "github.com/bnema/dumber/internal/application/dto"
6 "github.com/bnema/dumber/internal/application/port"8 "github.com/bnema/dumber/internal/application/port"
7 portmocks "github.com/bnema/dumber/internal/application/port/mocks"9 portmocks "github.com/bnema/dumber/internal/application/port/mocks"
8 "github.com/bnema/dumber/internal/domain/entity"10 "github.com/bnema/dumber/internal/domain/entity"
9 )11 )
10 12
13+func TestDependenciesValidateAcceptsRuntimeConfigOnly(t *testing.T) {
14+ runtimeConfig := portmocks.NewMockRuntimeConfigProvider(t)
15+ engine := portmocks.NewMockEngine(t)
16+ deps := &Dependencies{
17+ Ctx: context.Background(),
18+ RuntimeConfig: runtimeConfig,
19+ Engine: engine,
20+ HandlerDeps: port.HandlerDeps{
21+ SaveConfig: func(context.Context, dto.WebUIConfig) error {
22+ return nil
23+ },
24+ SaveOmniboxInitialBehavior: func(context.Context, entity.OmniboxInitialBehavior) error {
25+ return nil
26+ },
27+ },
28+ }
29+
30+ if err := deps.Validate(); err != nil {
31+ t.Fatalf("Validate() error = %v, want nil", err)
32+ }
33+}
34+
11 func TestAppRuntimeConfigReturnsProviderSnapshot(t *testing.T) {35 func TestAppRuntimeConfigReturnsProviderSnapshot(t *testing.T) {
12 provider := portmocks.NewMockRuntimeConfigProvider(t)36 provider := portmocks.NewMockRuntimeConfigProvider(t)
13 provider.EXPECT().37 provider.EXPECT().
A internal/ui/runtime_config_test_helpers_test.go +19 -0
@@ -0,0 +1,19 @@
1+package ui
2+
3+import (
4+ "github.com/bnema/dumber/internal/application/port"
5+ "github.com/bnema/dumber/internal/bootstrap"
6+ "github.com/bnema/dumber/internal/infrastructure/config"
7+)
8+
9+func runtimeConfigSnapshotForTest(cfg *config.Config) port.RuntimeConfigSnapshot {
10+ return bootstrap.RuntimeConfigSnapshotFromConfig(cfg)
11+}
12+
13+func appWithRuntimeConfigForTest(cfg *config.Config) *App {
14+ return &App{
15+ deps: &Dependencies{},
16+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
17+ runtimeConfigLoaded: true,
18+ }
19+}
@@ -0,0 +1,19 @@
1+package ui
2+
3+import (
4+ "github.com/bnema/dumber/internal/application/port"
5+ "github.com/bnema/dumber/internal/bootstrap"
6+ "github.com/bnema/dumber/internal/infrastructure/config"
7+)
8+
9+func runtimeConfigSnapshotForTest(cfg *config.Config) port.RuntimeConfigSnapshot {
10+ return bootstrap.RuntimeConfigSnapshotFromConfig(cfg)
11+}
12+
13+func appWithRuntimeConfigForTest(cfg *config.Config) *App {
14+ return &App{
15+ deps: &Dependencies{},
16+ runtimeConfig: runtimeConfigSnapshotForTest(cfg),
17+ runtimeConfigLoaded: true,
18+ }
19+}

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