detached 10854a9

refactor(M6): remove infrastructure imports from ui/app.go

Remove all 5 infrastructure imports (config, desktop, filesystem,
snapshot, textinput) from internal/ui/app.go by injecting them
through the Dependencies struct.

- config: replace config.Get() with a.deps.Config; wire
  OnConfigChange/WatchConfig as func() callbacks in deps so app.go
  never names *config.Config; change applyAppearanceConfig to read
  from deps.Config (updated in-place before callback fires)
- desktop: add SessionSpawner port.SessionSpawner to deps; wire
  desktop.NewSessionSpawner in main.go
- filesystem: add FileSystem port.FileSystem to deps; wire
  filesystem.New() in main.go
- snapshot: add SnapshotServiceFactory to deps; factory closes over
  the use case so the App can pass itself as provider at init time;
  add port.SnapshotService interface in port/session_state.go
- textinput: add AccentFocusProvider and NewGTKEntryTarget func to
  deps; replace *textinput.GTKEntryTarget type-check with
  port.GTKEntryInputTarget marker interface; add IsGTKEntryTarget()
  to GTKEntryTarget for the compile-time check

Gate: grep "infrastructure/" internal/ui/app.go returns no matches.
commit
10854a9320092d283fc6a5237c11c6ed582b3bfc
author
brice · · 2026-03-17 10:28 UTC
parent
7ba658b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +159 -63
M cmd/dumber/main.go +30 -1
@@ -20,15 +20,20 @@ import (
2020 "github.com/bnema/dumber/internal/infrastructure/clipboard"
2121 "github.com/bnema/dumber/internal/infrastructure/config"
2222 "github.com/bnema/dumber/internal/infrastructure/deps"
23+ "github.com/bnema/dumber/internal/infrastructure/desktop"
2324 "github.com/bnema/dumber/internal/infrastructure/favicon"
25+ "github.com/bnema/dumber/internal/infrastructure/filesystem"
2426 "github.com/bnema/dumber/internal/infrastructure/idle"
2527 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
28+ "github.com/bnema/dumber/internal/infrastructure/snapshot"
29+ "github.com/bnema/dumber/internal/infrastructure/textinput"
2630 "github.com/bnema/dumber/internal/infrastructure/updater"
2731 "github.com/bnema/dumber/internal/infrastructure/xdg"
2832 "github.com/bnema/dumber/internal/logging"
2933 "github.com/bnema/dumber/internal/ui"
3034 "github.com/bnema/dumber/internal/ui/component"
3135 "github.com/bnema/dumber/internal/ui/theme"
36+ "github.com/jwijenbergh/puregotk/v4/gtk"
3237 "github.com/rs/zerolog"
3338 )
3439
@@ -510,7 +515,9 @@ func buildUIDependencies(
510515 ) *ui.Dependencies {
511516 filterManager := engine.InternalFilterManager()
512517
513- return &ui.Dependencies{
518+ focusProvider := textinput.NewFocusProvider()
519+
520+ uiDeps := &ui.Dependencies{
514521 Ctx: ctx,
515522 Config: cfg,
516523 InitialURL: initialURL,
@@ -541,7 +548,29 @@ func buildUIDependencies(
541548 SessionStateRepo: repos.sessionState,
542549 CurrentSessionID: currentSessionID,
543550 SnapshotUC: uc.snapshot,
551+ SnapshotServiceFactory: func(provider port.TabListProvider, intervalMs int) port.SnapshotService {
552+ return snapshot.NewService(uc.snapshot, provider, intervalMs)
553+ },
544554 CheckUpdateUC: uc.checkUpdate,
545555 ApplyUpdateUC: uc.applyUpdate,
556+ SessionSpawner: desktop.NewSessionSpawner(ctx),
557+ FileSystem: filesystem.New(),
558+ AccentFocusProvider: focusProvider,
559+ NewGTKEntryTarget: func(entry *gtk.SearchEntry) port.TextInputTarget {
560+ return textinput.NewGTKEntryTarget(entry)
561+ },
562+ OnConfigChange: func(callback func()) {
563+ config.GetManager().OnConfigChange(func(newCfg *config.Config) {
564+ if cfg != nil && newCfg != nil {
565+ *cfg = *newCfg
566+ }
567+ callback()
568+ })
569+ },
570+ WatchConfig: func() error {
571+ return config.GetManager().Watch()
572+ },
546573 }
574+
575+ return uiDeps
547576 }
@@ -20,15 +20,20 @@ import (
20 "github.com/bnema/dumber/internal/infrastructure/clipboard"20 "github.com/bnema/dumber/internal/infrastructure/clipboard"
21 "github.com/bnema/dumber/internal/infrastructure/config"21 "github.com/bnema/dumber/internal/infrastructure/config"
22 "github.com/bnema/dumber/internal/infrastructure/deps"22 "github.com/bnema/dumber/internal/infrastructure/deps"
23+ "github.com/bnema/dumber/internal/infrastructure/desktop"
23 "github.com/bnema/dumber/internal/infrastructure/favicon"24 "github.com/bnema/dumber/internal/infrastructure/favicon"
25+ "github.com/bnema/dumber/internal/infrastructure/filesystem"
24 "github.com/bnema/dumber/internal/infrastructure/idle"26 "github.com/bnema/dumber/internal/infrastructure/idle"
25 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"27 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
28+ "github.com/bnema/dumber/internal/infrastructure/snapshot"
29+ "github.com/bnema/dumber/internal/infrastructure/textinput"
26 "github.com/bnema/dumber/internal/infrastructure/updater"30 "github.com/bnema/dumber/internal/infrastructure/updater"
27 "github.com/bnema/dumber/internal/infrastructure/xdg"31 "github.com/bnema/dumber/internal/infrastructure/xdg"
28 "github.com/bnema/dumber/internal/logging"32 "github.com/bnema/dumber/internal/logging"
29 "github.com/bnema/dumber/internal/ui"33 "github.com/bnema/dumber/internal/ui"
30 "github.com/bnema/dumber/internal/ui/component"34 "github.com/bnema/dumber/internal/ui/component"
31 "github.com/bnema/dumber/internal/ui/theme"35 "github.com/bnema/dumber/internal/ui/theme"
36+ "github.com/jwijenbergh/puregotk/v4/gtk"
32 "github.com/rs/zerolog"37 "github.com/rs/zerolog"
33 )38 )
34 39
@@ -510,7 +515,9 @@ func buildUIDependencies(
510 ) *ui.Dependencies {515 ) *ui.Dependencies {
511 filterManager := engine.InternalFilterManager()516 filterManager := engine.InternalFilterManager()
512 517
513- return &ui.Dependencies{518+ focusProvider := textinput.NewFocusProvider()
519+
520+ uiDeps := &ui.Dependencies{
514 Ctx: ctx,521 Ctx: ctx,
515 Config: cfg,522 Config: cfg,
516 InitialURL: initialURL,523 InitialURL: initialURL,
@@ -541,7 +548,29 @@ func buildUIDependencies(
541 SessionStateRepo: repos.sessionState,548 SessionStateRepo: repos.sessionState,
542 CurrentSessionID: currentSessionID,549 CurrentSessionID: currentSessionID,
543 SnapshotUC: uc.snapshot,550 SnapshotUC: uc.snapshot,
551+ SnapshotServiceFactory: func(provider port.TabListProvider, intervalMs int) port.SnapshotService {
552+ return snapshot.NewService(uc.snapshot, provider, intervalMs)
553+ },
544 CheckUpdateUC: uc.checkUpdate,554 CheckUpdateUC: uc.checkUpdate,
545 ApplyUpdateUC: uc.applyUpdate,555 ApplyUpdateUC: uc.applyUpdate,
556+ SessionSpawner: desktop.NewSessionSpawner(ctx),
557+ FileSystem: filesystem.New(),
558+ AccentFocusProvider: focusProvider,
559+ NewGTKEntryTarget: func(entry *gtk.SearchEntry) port.TextInputTarget {
560+ return textinput.NewGTKEntryTarget(entry)
561+ },
562+ OnConfigChange: func(callback func()) {
563+ config.GetManager().OnConfigChange(func(newCfg *config.Config) {
564+ if cfg != nil && newCfg != nil {
565+ *cfg = *newCfg
566+ }
567+ callback()
568+ })
569+ },
570+ WatchConfig: func() error {
571+ return config.GetManager().Watch()
572+ },
546 }573 }
574+
575+ return uiDeps
547 }576 }
M internal/application/port/session_state.go +20 -1
@@ -1,6 +1,10 @@
11 package port
22
3-import "github.com/bnema/dumber/internal/domain/entity"
3+import (
4+ "context"
5+
6+ "github.com/bnema/dumber/internal/domain/entity"
7+)
48
59 // TabListProvider provides access to the current tab list state.
610 // Implemented by the UI layer to allow the snapshot service to read state.
@@ -16,3 +20,18 @@ type SessionSpawner interface {
1620 // SpawnWithSession starts a new dumber instance to restore a session.
1721 SpawnWithSession(sessionID entity.SessionID) error
1822 }
23+
24+// SnapshotService manages debounced session-state snapshots.
25+type SnapshotService interface {
26+ // Start begins the snapshot service with the given context.
27+ Start(ctx context.Context)
28+ // Stop stops the service and saves the final state.
29+ Stop(ctx context.Context) error
30+ // SetReady marks the service as ready to save snapshots.
31+ // Call this after the session has been persisted to the database.
32+ SetReady()
33+ // MarkDirty signals that session state has changed and should be saved.
34+ MarkDirty()
35+ // SaveNow forces an immediate save (e.g. for shutdown).
36+ SaveNow(ctx context.Context) error
37+}
@@ -1,6 +1,10 @@
1 package port1 package port
2 2
3-import "github.com/bnema/dumber/internal/domain/entity"3+import (
4+ "context"
5+
6+ "github.com/bnema/dumber/internal/domain/entity"
7+)
4 8
5 // TabListProvider provides access to the current tab list state.9 // TabListProvider provides access to the current tab list state.
6 // Implemented by the UI layer to allow the snapshot service to read state.10 // Implemented by the UI layer to allow the snapshot service to read state.
@@ -16,3 +20,18 @@ type SessionSpawner interface {
16 // SpawnWithSession starts a new dumber instance to restore a session.20 // SpawnWithSession starts a new dumber instance to restore a session.
17 SpawnWithSession(sessionID entity.SessionID) error21 SpawnWithSession(sessionID entity.SessionID) error
18 }22 }
23+
24+// SnapshotService manages debounced session-state snapshots.
25+type SnapshotService interface {
26+ // Start begins the snapshot service with the given context.
27+ Start(ctx context.Context)
28+ // Stop stops the service and saves the final state.
29+ Stop(ctx context.Context) error
30+ // SetReady marks the service as ready to save snapshots.
31+ // Call this after the session has been persisted to the database.
32+ SetReady()
33+ // MarkDirty signals that session state has changed and should be saved.
34+ MarkDirty()
35+ // SaveNow forces an immediate save (e.g. for shutdown).
36+ SaveNow(ctx context.Context) error
37+}
M internal/application/port/text_input.go +7 -0
@@ -18,6 +18,13 @@ type Focusable interface {
1818 Focus(ctx context.Context)
1919 }
2020
21+// GTKEntryInputTarget is a marker interface implemented by text input targets
22+// that wrap a GTK Entry widget. Used for keyboard routing decisions.
23+type GTKEntryInputTarget interface {
24+ TextInputTarget
25+ IsGTKEntryTarget() bool
26+}
27+
2128 // FocusedInputProvider tracks which text input target currently has focus.
2229 // This allows the accent picker to insert text into the correct input field.
2330 type FocusedInputProvider interface {
@@ -18,6 +18,13 @@ type Focusable interface {
18 Focus(ctx context.Context)18 Focus(ctx context.Context)
19 }19 }
20 20
21+// GTKEntryInputTarget is a marker interface implemented by text input targets
22+// that wrap a GTK Entry widget. Used for keyboard routing decisions.
23+type GTKEntryInputTarget interface {
24+ TextInputTarget
25+ IsGTKEntryTarget() bool
26+}
27+
21 // FocusedInputProvider tracks which text input target currently has focus.28 // FocusedInputProvider tracks which text input target currently has focus.
22 // This allows the accent picker to insert text into the correct input field.29 // This allows the accent picker to insert text into the correct input field.
23 type FocusedInputProvider interface {30 type FocusedInputProvider interface {
M internal/infrastructure/snapshot/service.go +3 -0
@@ -19,6 +19,9 @@ const (
1919 fkRetryDelay = 100 * time.Millisecond
2020 )
2121
22+// Compile-time interface check.
23+var _ port.SnapshotService = (*Service)(nil)
24+
2225 // Service handles debounced session state snapshots.
2326 type Service struct {
2427 snapshotUC *usecase.SnapshotSessionUseCase
@@ -19,6 +19,9 @@ const (
19 fkRetryDelay = 100 * time.Millisecond19 fkRetryDelay = 100 * time.Millisecond
20 )20 )
21 21
22+// Compile-time interface check.
23+var _ port.SnapshotService = (*Service)(nil)
24+
22 // Service handles debounced session state snapshots.25 // Service handles debounced session state snapshots.
23 type Service struct {26 type Service struct {
24 snapshotUC *usecase.SnapshotSessionUseCase27 snapshotUC *usecase.SnapshotSessionUseCase
M internal/infrastructure/textinput/gtk_entry_target.go +5 -1
@@ -14,8 +14,12 @@ type GTKEntryTarget struct {
1414 entry *gtk.SearchEntry
1515 }
1616
17-// Compile-time interface check.
17+// Compile-time interface checks.
1818 var _ port.TextInputTarget = (*GTKEntryTarget)(nil)
19+var _ port.GTKEntryInputTarget = (*GTKEntryTarget)(nil)
20+
21+// IsGTKEntryTarget implements port.GTKEntryInputTarget.
22+func (t *GTKEntryTarget) IsGTKEntryTarget() bool { return true }
1923
2024 // NewGTKEntryTarget creates a new GTK entry target for a SearchEntry.
2125 func NewGTKEntryTarget(entry *gtk.SearchEntry) *GTKEntryTarget {
@@ -14,8 +14,12 @@ type GTKEntryTarget struct {
14 entry *gtk.SearchEntry14 entry *gtk.SearchEntry
15 }15 }
16 16
17-// Compile-time interface check.17+// Compile-time interface checks.
18 var _ port.TextInputTarget = (*GTKEntryTarget)(nil)18 var _ port.TextInputTarget = (*GTKEntryTarget)(nil)
19+var _ port.GTKEntryInputTarget = (*GTKEntryTarget)(nil)
20+
21+// IsGTKEntryTarget implements port.GTKEntryInputTarget.
22+func (t *GTKEntryTarget) IsGTKEntryTarget() bool { return true }
19 23
20 // NewGTKEntryTarget creates a new GTK entry target for a SearchEntry.24 // NewGTKEntryTarget creates a new GTK entry target for a SearchEntry.
21 func NewGTKEntryTarget(entry *gtk.SearchEntry) *GTKEntryTarget {25 func NewGTKEntryTarget(entry *gtk.SearchEntry) *GTKEntryTarget {
M internal/ui/app.go +76 -59
@@ -14,11 +14,6 @@ import (
1414 "github.com/bnema/dumber/internal/application/usecase"
1515 "github.com/bnema/dumber/internal/domain/entity"
1616 urlutil "github.com/bnema/dumber/internal/domain/url"
17- "github.com/bnema/dumber/internal/infrastructure/config"
18- "github.com/bnema/dumber/internal/infrastructure/desktop"
19- "github.com/bnema/dumber/internal/infrastructure/filesystem"
20- "github.com/bnema/dumber/internal/infrastructure/snapshot"
21- "github.com/bnema/dumber/internal/infrastructure/textinput"
2217
2318 "github.com/bnema/dumber/internal/logging"
2419 "github.com/bnema/dumber/internal/ui/adapter"
@@ -66,12 +61,11 @@ type App struct {
6661 tabsUC *usecase.ManageTabsUseCase
6762
6863 // Coordinators (new architecture)
69- contentCoord *content.Coordinator
70- tabCoord *coordinator.TabCoordinator
71- wsCoord *coordinator.WorkspaceCoordinator
72- navCoord *coordinator.NavigationCoordinator
73- kbDispatcher *dispatcher.KeyboardDispatcher
74- configManager *config.Manager
64+ contentCoord *content.Coordinator
65+ tabCoord *coordinator.TabCoordinator
66+ wsCoord *coordinator.WorkspaceCoordinator
67+ navCoord *coordinator.NavigationCoordinator
68+ kbDispatcher *dispatcher.KeyboardDispatcher
7569
7670 // Pane management (used by coordinators)
7771 panesUC *usecase.ManagePanesUseCase
@@ -115,7 +109,7 @@ type App struct {
115109 tabPicker *component.TabPicker
116110 tabPickerWidget layout.Widget
117111 tabPickerPaneID entity.PaneID
118- snapshotService *snapshot.Service
112+ snapshotService port.SnapshotService
119113
120114 // Update management
121115 updateCoord *coordinator.UpdateCoordinator
@@ -130,7 +124,7 @@ type App struct {
130124 // Accent picker for dead keys support
131125 accentPicker *component.AccentPicker
132126 insertAccentUC *usecase.InsertAccentUseCase
133- accentFocusProvider *textinput.FocusProvider
127+ accentFocusProvider port.FocusedInputProvider
134128
135129 // Deferred initialization - runs after first load_started to avoid blocking initial navigation
136130 deferredInitOnce sync.Once
@@ -176,15 +170,19 @@ func New(deps *Dependencies) (*App, error) {
176170 workspaceViews: make(map[entity.TabID]*component.WorkspaceView),
177171 floatingSessions: make(map[floatingSessionKey]*floatingWorkspaceSession),
178172 engine: deps.Engine,
179- configManager: config.GetManager(),
180173 cancel: cancel,
181174 }
182175 // Register message handlers through the engine.
183176 if err := deps.Engine.RegisterHandlers(ctx, port.HandlerDependencies{
184- HistoryUC: deps.HistoryUC,
185- FavoritesUC: deps.FavoritesUC,
186- Clipboard: deps.Clipboard,
187- AutoCopyConfig: &configAutoCopyAdapter{},
177+ HistoryUC: deps.HistoryUC,
178+ FavoritesUC: deps.FavoritesUC,
179+ Clipboard: deps.Clipboard,
180+ AutoCopyConfig: &autoCopyConfigFn{fn: func() bool {
181+ if deps.Config == nil {
182+ return false
183+ }
184+ return deps.Config.Clipboard.AutoCopyOnSelection
185+ }},
188186 OnClipboardCopied: func(textLen int) {
189187 cb := glib.SourceFunc(func(_ uintptr) bool {
190188 if app.appToaster != nil {
@@ -509,8 +507,8 @@ func (a *App) initAccentPicker(ctx context.Context) {
509507 }
510508 }
511509
512- // Create focus provider (tracks which input has focus)
513- a.accentFocusProvider = textinput.NewFocusProvider()
510+ // Use focus provider from deps (tracks which input has focus)
511+ a.accentFocusProvider = a.deps.AccentFocusProvider
514512
515513 // Create the use case with glib.IdleAdd for thread-safe GTK calls
516514 a.insertAccentUC = usecase.NewInsertAccentUseCase(
@@ -584,7 +582,7 @@ func (a *App) initDownloadHandler(ctx context.Context) {
584582 eventAdapter := &downloadEventAdapter{app: a}
585583
586584 // Create use case for preparing download destinations with file deduplication.
587- prepareDownloadUC := usecase.NewPrepareDownloadUseCase(filesystem.New())
585+ prepareDownloadUC := usecase.NewPrepareDownloadUseCase(a.deps.FileSystem)
588586
589587 if err := a.engine.ConfigureDownloads(ctx, downloadPath, eventAdapter, prepareDownloadUC); err != nil {
590588 log.Error().Err(err).Msg("failed to configure downloads")
@@ -619,15 +617,16 @@ func (d *downloadEventAdapter) OnDownloadEvent(ctx context.Context, event port.D
619617 glib.IdleAdd(&cb, 0)
620618 }
621619
622-// configAutoCopyAdapter implements port.AutoCopyConfig by reading the live config.
623-type configAutoCopyAdapter struct{}
620+// autoCopyConfigFn implements port.AutoCopyConfig using a function closure.
621+type autoCopyConfigFn struct {
622+ fn func() bool
623+}
624624
625-func (a *configAutoCopyAdapter) IsAutoCopyEnabled() bool {
626- cfg := config.Get()
627- if cfg == nil {
625+func (a *autoCopyConfigFn) IsAutoCopyEnabled() bool {
626+ if a.fn == nil {
628627 return false
629628 }
630- return cfg.Clipboard.AutoCopyOnSelection
629+ return a.fn()
631630 }
632631
633632 func (a *App) initKeyboardHandler(ctx context.Context) {
@@ -678,7 +677,7 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
678677 // visible after focus returns to the WebView, so visibility alone
679678 // would misroute keys.
680679 if a.accentFocusProvider != nil {
681- if _, ok := a.accentFocusProvider.GetFocusedInput().(*textinput.GTKEntryTarget); ok {
680+ if _, ok := a.accentFocusProvider.GetFocusedInput().(port.GTKEntryInputTarget); ok {
682681 // GTK Entry focused (omnibox or find bar): Alt-modified keys go to
683682 // shortcuts (pane navigation), other keys pass through to the widget's
684683 // own capture-phase key controller (which handles accent detection)
@@ -786,7 +785,9 @@ func (a *App) initOmniboxConfig(ctx context.Context) {
786785 OnFocusIn: func(entry *gtk.SearchEntry) {
787786 // Set omnibox entry as the focused input for accent picker
788787 if a.accentFocusProvider != nil && entry != nil {
789- a.accentFocusProvider.SetFocusedInput(textinput.NewGTKEntryTarget(entry))
788+ if a.deps.NewGTKEntryTarget != nil {
789+ a.accentFocusProvider.SetFocusedInput(a.deps.NewGTKEntryTarget(entry))
790+ }
790791 }
791792 },
792793 OnFocusOut: func() {
@@ -822,7 +823,9 @@ func (a *App) initFindBarConfig(ctx context.Context) {
822823 OnFocusIn: func(entry *gtk.SearchEntry) {
823824 // Set find bar entry as the focused input for accent picker
824825 if a.accentFocusProvider != nil && entry != nil {
825- a.accentFocusProvider.SetFocusedInput(textinput.NewGTKEntryTarget(entry))
826+ if a.deps.NewGTKEntryTarget != nil {
827+ a.accentFocusProvider.SetFocusedInput(a.deps.NewGTKEntryTarget(entry))
828+ }
826829 }
827830 },
828831 OnFocusOut: func() {
@@ -864,8 +867,8 @@ func (a *App) initSessionManager(ctx context.Context) {
864867 )
865868 }
866869
867- // Create session spawner for restoration
868- spawner := desktop.NewSessionSpawner(ctx)
870+ // Use injected session spawner for restoration
871+ spawner := a.deps.SessionSpawner
869872
870873 // Create session manager component
871874 a.sessionManager = component.NewSessionManager(ctx, component.SessionManagerConfig{
@@ -878,6 +881,10 @@ func (a *App) initSessionManager(ctx context.Context) {
878881 },
879882 OnOpen: func(sessionID entity.SessionID) {
880883 log.Info().Str("session_id", string(sessionID)).Msg("session restoration requested")
884+ if spawner == nil {
885+ log.Warn().Str("session_id", string(sessionID)).Msg("session spawner not available")
886+ return
887+ }
881888 if err := spawner.SpawnWithSession(sessionID); err != nil {
882889 log.Error().Err(err).Str("session_id", string(sessionID)).Msg("failed to spawn session")
883890 }
@@ -1167,7 +1174,7 @@ func (a *App) switchToTargetTabIfConfigured(ctx context.Context, out *usecase.Mo
11671174 return
11681175 }
11691176
1170- switchToTarget := config.Get().Workspace.SwitchToTabOnMove
1177+ switchToTarget := a.deps.Config.Workspace.SwitchToTabOnMove
11711178 if out.SourceTabClosed {
11721179 switchToTarget = true
11731180 }
@@ -1227,8 +1234,8 @@ func (a *App) initTabPicker(ctx context.Context) {
12271234 func (a *App) initSnapshotService(ctx context.Context) {
12281235 log := logging.FromContext(ctx)
12291236
1230- if a.deps == nil || a.deps.SnapshotUC == nil {
1231- log.Debug().Msg("snapshot use case not available, skipping snapshot service")
1237+ if a.deps == nil || a.deps.SnapshotServiceFactory == nil {
1238+ log.Debug().Msg("snapshot service factory not available, skipping")
12321239 return
12331240 }
12341241
@@ -1237,7 +1244,11 @@ func (a *App) initSnapshotService(ctx context.Context) {
12371244 intervalMs = a.deps.Config.Session.SnapshotIntervalMs
12381245 }
12391246
1240- a.snapshotService = snapshot.NewService(a.deps.SnapshotUC, a, intervalMs)
1247+ a.snapshotService = a.deps.SnapshotServiceFactory(a, intervalMs)
1248+ if a.snapshotService == nil {
1249+ log.Warn().Msg("snapshot service factory returned nil")
1250+ return
1251+ }
12411252 a.snapshotService.Start(ctx)
12421253
12431254 // Set up callback for main.go to notify when session is persisted
@@ -2047,7 +2058,9 @@ func (a *App) createWorkspaceViewWithoutAttach(ctx context.Context, tab *entity.
20472058 // Set find bar config for this workspace view
20482059 wsView.SetFindBarConfig(a.findBarCfg)
20492060 // Set auto-open omnibox on new pane
2050- wsView.SetAutoOpenOnNewPane(config.Get().Omnibox.AutoOpenOnNewPane)
2061+ if a.deps.Config != nil {
2062+ wsView.SetAutoOpenOnNewPane(a.deps.Config.Omnibox.AutoOpenOnNewPane)
2063+ }
20512064
20522065 wsView.SetOnPaneFocused(func(paneID entity.PaneID) {
20532066 if a.keyboardHandler != nil && a.keyboardHandler.Mode() == input.ModeResize {
@@ -2380,11 +2393,20 @@ func (a *App) installFloatingOverlayPositioning(tabID entity.TabID, workspaceOve
23802393 gtkOverlay.ConnectGetChildPosition(&cb)
23812394 }
23822395
2383-func (a *App) currentFloatingConfig() config.FloatingPaneConfig {
2396+// currentFloatingWidthPct returns the configured floating pane width percentage.
2397+func (a *App) currentFloatingWidthPct() float64 {
2398+ if a.deps != nil && a.deps.Config != nil {
2399+ return a.deps.Config.Workspace.FloatingPane.WidthPct
2400+ }
2401+ return 0
2402+}
2403+
2404+// currentFloatingHeightPct returns the configured floating pane height percentage.
2405+func (a *App) currentFloatingHeightPct() float64 {
23842406 if a.deps != nil && a.deps.Config != nil {
2385- return a.deps.Config.Workspace.FloatingPane
2407+ return a.deps.Config.Workspace.FloatingPane.HeightPct
23862408 }
2387- return config.Get().Workspace.FloatingPane
2409+ return 0
23882410 }
23892411
23902412 func (a *App) ensureFloatingSession(
@@ -2457,10 +2479,9 @@ func (a *App) ensureFloatingSession(
24572479 wsView.AddWorkspaceOverlayWidget(pvOverlay)
24582480 configureFloatingOverlayMeasurement(wsView.WorkspaceOverlayWidget(), pvOverlay)
24592481
2460- floatingCfg := a.currentFloatingConfig()
24612482 floatingPane := component.NewFloatingPane(wsView.WorkspaceOverlayWidget(), component.FloatingPaneOptions{
2462- WidthPct: floatingCfg.WidthPct,
2463- HeightPct: floatingCfg.HeightPct,
2483+ WidthPct: a.currentFloatingWidthPct(),
2484+ HeightPct: a.currentFloatingHeightPct(),
24642485 FallbackWidth: floatingPaneFallbackWidth,
24652486 FallbackHeight: floatingPaneFallbackHeight,
24662487 OnNavigate: func(navCtx context.Context, url string) error {
@@ -3028,29 +3049,29 @@ func (a *App) SetOmniboxOnNavigate(fn func(url string)) {
30283049 func (a *App) initConfigWatcher(ctx context.Context) {
30293050 log := logging.FromContext(ctx)
30303051
3031- if a.configManager == nil {
3032- log.Debug().Msg("no config manager available, skipping watcher")
3052+ if a.deps.WatchConfig == nil || a.deps.OnConfigChange == nil {
3053+ log.Debug().Msg("no config watcher available, skipping")
30333054 return
30343055 }
30353056
30363057 // Start viper watcher
3037- if err := a.configManager.Watch(); err != nil {
3058+ if err := a.deps.WatchConfig(); err != nil {
30383059 log.Warn().Err(err).Msg("failed to start config watcher")
30393060 return
30403061 }
30413062
30423063 // Hot-reload appearance and keybindings on config change.
3043- a.configManager.OnConfigChange(func(newCfg *config.Config) {
3044- cfgCopy := newCfg
3064+ // deps.Config is updated in-place before this callback fires.
3065+ a.deps.OnConfigChange(func() {
30453066 cb := glib.SourceFunc(func(_ uintptr) bool {
3046- a.applyAppearanceConfig(ctx, cfgCopy)
3067+ a.applyAppearanceConfig(ctx)
30473068 // Reload keyboard shortcuts
30483069 if a.keyboardHandler != nil {
3049- a.keyboardHandler.ReloadShortcuts(ctx, cfgCopy)
3070+ a.keyboardHandler.ReloadShortcuts(ctx, a.deps.Config)
30503071 }
30513072 // Reload global shortcuts (removes stale, re-registers from config)
30523073 if a.globalShortcutHandler != nil {
3053- a.globalShortcutHandler.ReloadShortcuts(ctx, cfgCopy)
3074+ a.globalShortcutHandler.ReloadShortcuts(ctx, a.deps.Config)
30543075 }
30553076 return false
30563077 })
@@ -3060,16 +3081,12 @@ func (a *App) initConfigWatcher(ctx context.Context) {
30603081 log.Debug().Msg("config watcher initialized")
30613082 }
30623083
3063-func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) {
3084+func (a *App) applyAppearanceConfig(ctx context.Context) {
30643085 log := logging.FromContext(ctx)
3065- if cfg == nil {
3086+ if a.deps == nil || a.deps.Config == nil {
30663087 return
30673088 }
3068-
3069- // Update the shared config pointer in-place so existing references see changes.
3070- if a.deps != nil && a.deps.Config != nil {
3071- *a.deps.Config = *cfg
3072- }
3089+ cfg := a.deps.Config
30733090
30743091 if a.engine != nil {
30753092 _ = a.engine.UpdateSettings(ctx, port.EngineSettingsUpdate{Raw: cfg})
@@ -3081,7 +3098,7 @@ func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) {
30813098 }
30823099
30833100 // Update GTK theme and injected WebUI theme vars
3084- if a.deps != nil && a.deps.Theme != nil {
3101+ if a.deps.Theme != nil {
30853102 var display *gdk.Display
30863103 if a.mainWindow != nil && a.mainWindow.Window() != nil {
30873104 display = a.mainWindow.Window().GetDisplay()
@@ -14,11 +14,6 @@ import (
14 "github.com/bnema/dumber/internal/application/usecase"14 "github.com/bnema/dumber/internal/application/usecase"
15 "github.com/bnema/dumber/internal/domain/entity"15 "github.com/bnema/dumber/internal/domain/entity"
16 urlutil "github.com/bnema/dumber/internal/domain/url"16 urlutil "github.com/bnema/dumber/internal/domain/url"
17- "github.com/bnema/dumber/internal/infrastructure/config"
18- "github.com/bnema/dumber/internal/infrastructure/desktop"
19- "github.com/bnema/dumber/internal/infrastructure/filesystem"
20- "github.com/bnema/dumber/internal/infrastructure/snapshot"
21- "github.com/bnema/dumber/internal/infrastructure/textinput"
22 17
23 "github.com/bnema/dumber/internal/logging"18 "github.com/bnema/dumber/internal/logging"
24 "github.com/bnema/dumber/internal/ui/adapter"19 "github.com/bnema/dumber/internal/ui/adapter"
@@ -66,12 +61,11 @@ type App struct {
66 tabsUC *usecase.ManageTabsUseCase61 tabsUC *usecase.ManageTabsUseCase
67 62
68 // Coordinators (new architecture)63 // Coordinators (new architecture)
69- contentCoord *content.Coordinator64+ contentCoord *content.Coordinator
70- tabCoord *coordinator.TabCoordinator65+ tabCoord *coordinator.TabCoordinator
71- wsCoord *coordinator.WorkspaceCoordinator66+ wsCoord *coordinator.WorkspaceCoordinator
72- navCoord *coordinator.NavigationCoordinator67+ navCoord *coordinator.NavigationCoordinator
73- kbDispatcher *dispatcher.KeyboardDispatcher68+ kbDispatcher *dispatcher.KeyboardDispatcher
74- configManager *config.Manager
75 69
76 // Pane management (used by coordinators)70 // Pane management (used by coordinators)
77 panesUC *usecase.ManagePanesUseCase71 panesUC *usecase.ManagePanesUseCase
@@ -115,7 +109,7 @@ type App struct {
115 tabPicker *component.TabPicker109 tabPicker *component.TabPicker
116 tabPickerWidget layout.Widget110 tabPickerWidget layout.Widget
117 tabPickerPaneID entity.PaneID111 tabPickerPaneID entity.PaneID
118- snapshotService *snapshot.Service112+ snapshotService port.SnapshotService
119 113
120 // Update management114 // Update management
121 updateCoord *coordinator.UpdateCoordinator115 updateCoord *coordinator.UpdateCoordinator
@@ -130,7 +124,7 @@ type App struct {
130 // Accent picker for dead keys support124 // Accent picker for dead keys support
131 accentPicker *component.AccentPicker125 accentPicker *component.AccentPicker
132 insertAccentUC *usecase.InsertAccentUseCase126 insertAccentUC *usecase.InsertAccentUseCase
133- accentFocusProvider *textinput.FocusProvider127+ accentFocusProvider port.FocusedInputProvider
134 128
135 // Deferred initialization - runs after first load_started to avoid blocking initial navigation129 // Deferred initialization - runs after first load_started to avoid blocking initial navigation
136 deferredInitOnce sync.Once130 deferredInitOnce sync.Once
@@ -176,15 +170,19 @@ func New(deps *Dependencies) (*App, error) {
176 workspaceViews: make(map[entity.TabID]*component.WorkspaceView),170 workspaceViews: make(map[entity.TabID]*component.WorkspaceView),
177 floatingSessions: make(map[floatingSessionKey]*floatingWorkspaceSession),171 floatingSessions: make(map[floatingSessionKey]*floatingWorkspaceSession),
178 engine: deps.Engine,172 engine: deps.Engine,
179- configManager: config.GetManager(),
180 cancel: cancel,173 cancel: cancel,
181 }174 }
182 // Register message handlers through the engine.175 // Register message handlers through the engine.
183 if err := deps.Engine.RegisterHandlers(ctx, port.HandlerDependencies{176 if err := deps.Engine.RegisterHandlers(ctx, port.HandlerDependencies{
184- HistoryUC: deps.HistoryUC,177+ HistoryUC: deps.HistoryUC,
185- FavoritesUC: deps.FavoritesUC,178+ FavoritesUC: deps.FavoritesUC,
186- Clipboard: deps.Clipboard,179+ Clipboard: deps.Clipboard,
187- AutoCopyConfig: &configAutoCopyAdapter{},180+ AutoCopyConfig: &autoCopyConfigFn{fn: func() bool {
181+ if deps.Config == nil {
182+ return false
183+ }
184+ return deps.Config.Clipboard.AutoCopyOnSelection
185+ }},
188 OnClipboardCopied: func(textLen int) {186 OnClipboardCopied: func(textLen int) {
189 cb := glib.SourceFunc(func(_ uintptr) bool {187 cb := glib.SourceFunc(func(_ uintptr) bool {
190 if app.appToaster != nil {188 if app.appToaster != nil {
@@ -509,8 +507,8 @@ func (a *App) initAccentPicker(ctx context.Context) {
509 }507 }
510 }508 }
511 509
512- // Create focus provider (tracks which input has focus)510+ // Use focus provider from deps (tracks which input has focus)
513- a.accentFocusProvider = textinput.NewFocusProvider()511+ a.accentFocusProvider = a.deps.AccentFocusProvider
514 512
515 // Create the use case with glib.IdleAdd for thread-safe GTK calls513 // Create the use case with glib.IdleAdd for thread-safe GTK calls
516 a.insertAccentUC = usecase.NewInsertAccentUseCase(514 a.insertAccentUC = usecase.NewInsertAccentUseCase(
@@ -584,7 +582,7 @@ func (a *App) initDownloadHandler(ctx context.Context) {
584 eventAdapter := &downloadEventAdapter{app: a}582 eventAdapter := &downloadEventAdapter{app: a}
585 583
586 // Create use case for preparing download destinations with file deduplication.584 // Create use case for preparing download destinations with file deduplication.
587- prepareDownloadUC := usecase.NewPrepareDownloadUseCase(filesystem.New())585+ prepareDownloadUC := usecase.NewPrepareDownloadUseCase(a.deps.FileSystem)
588 586
589 if err := a.engine.ConfigureDownloads(ctx, downloadPath, eventAdapter, prepareDownloadUC); err != nil {587 if err := a.engine.ConfigureDownloads(ctx, downloadPath, eventAdapter, prepareDownloadUC); err != nil {
590 log.Error().Err(err).Msg("failed to configure downloads")588 log.Error().Err(err).Msg("failed to configure downloads")
@@ -619,15 +617,16 @@ func (d *downloadEventAdapter) OnDownloadEvent(ctx context.Context, event port.D
619 glib.IdleAdd(&cb, 0)617 glib.IdleAdd(&cb, 0)
620 }618 }
621 619
622-// configAutoCopyAdapter implements port.AutoCopyConfig by reading the live config.620+// autoCopyConfigFn implements port.AutoCopyConfig using a function closure.
623-type configAutoCopyAdapter struct{}621+type autoCopyConfigFn struct {
622+ fn func() bool
623+}
624 624
625-func (a *configAutoCopyAdapter) IsAutoCopyEnabled() bool {625+func (a *autoCopyConfigFn) IsAutoCopyEnabled() bool {
626- cfg := config.Get()626+ if a.fn == nil {
627- if cfg == nil {
628 return false627 return false
629 }628 }
630- return cfg.Clipboard.AutoCopyOnSelection629+ return a.fn()
631 }630 }
632 631
633 func (a *App) initKeyboardHandler(ctx context.Context) {632 func (a *App) initKeyboardHandler(ctx context.Context) {
@@ -678,7 +677,7 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
678 // visible after focus returns to the WebView, so visibility alone677 // visible after focus returns to the WebView, so visibility alone
679 // would misroute keys.678 // would misroute keys.
680 if a.accentFocusProvider != nil {679 if a.accentFocusProvider != nil {
681- if _, ok := a.accentFocusProvider.GetFocusedInput().(*textinput.GTKEntryTarget); ok {680+ if _, ok := a.accentFocusProvider.GetFocusedInput().(port.GTKEntryInputTarget); ok {
682 // GTK Entry focused (omnibox or find bar): Alt-modified keys go to681 // GTK Entry focused (omnibox or find bar): Alt-modified keys go to
683 // shortcuts (pane navigation), other keys pass through to the widget's682 // shortcuts (pane navigation), other keys pass through to the widget's
684 // own capture-phase key controller (which handles accent detection)683 // own capture-phase key controller (which handles accent detection)
@@ -786,7 +785,9 @@ func (a *App) initOmniboxConfig(ctx context.Context) {
786 OnFocusIn: func(entry *gtk.SearchEntry) {785 OnFocusIn: func(entry *gtk.SearchEntry) {
787 // Set omnibox entry as the focused input for accent picker786 // Set omnibox entry as the focused input for accent picker
788 if a.accentFocusProvider != nil && entry != nil {787 if a.accentFocusProvider != nil && entry != nil {
789- a.accentFocusProvider.SetFocusedInput(textinput.NewGTKEntryTarget(entry))788+ if a.deps.NewGTKEntryTarget != nil {
789+ a.accentFocusProvider.SetFocusedInput(a.deps.NewGTKEntryTarget(entry))
790+ }
790 }791 }
791 },792 },
792 OnFocusOut: func() {793 OnFocusOut: func() {
@@ -822,7 +823,9 @@ func (a *App) initFindBarConfig(ctx context.Context) {
822 OnFocusIn: func(entry *gtk.SearchEntry) {823 OnFocusIn: func(entry *gtk.SearchEntry) {
823 // Set find bar entry as the focused input for accent picker824 // Set find bar entry as the focused input for accent picker
824 if a.accentFocusProvider != nil && entry != nil {825 if a.accentFocusProvider != nil && entry != nil {
825- a.accentFocusProvider.SetFocusedInput(textinput.NewGTKEntryTarget(entry))826+ if a.deps.NewGTKEntryTarget != nil {
827+ a.accentFocusProvider.SetFocusedInput(a.deps.NewGTKEntryTarget(entry))
828+ }
826 }829 }
827 },830 },
828 OnFocusOut: func() {831 OnFocusOut: func() {
@@ -864,8 +867,8 @@ func (a *App) initSessionManager(ctx context.Context) {
864 )867 )
865 }868 }
866 869
867- // Create session spawner for restoration870+ // Use injected session spawner for restoration
868- spawner := desktop.NewSessionSpawner(ctx)871+ spawner := a.deps.SessionSpawner
869 872
870 // Create session manager component873 // Create session manager component
871 a.sessionManager = component.NewSessionManager(ctx, component.SessionManagerConfig{874 a.sessionManager = component.NewSessionManager(ctx, component.SessionManagerConfig{
@@ -878,6 +881,10 @@ func (a *App) initSessionManager(ctx context.Context) {
878 },881 },
879 OnOpen: func(sessionID entity.SessionID) {882 OnOpen: func(sessionID entity.SessionID) {
880 log.Info().Str("session_id", string(sessionID)).Msg("session restoration requested")883 log.Info().Str("session_id", string(sessionID)).Msg("session restoration requested")
884+ if spawner == nil {
885+ log.Warn().Str("session_id", string(sessionID)).Msg("session spawner not available")
886+ return
887+ }
881 if err := spawner.SpawnWithSession(sessionID); err != nil {888 if err := spawner.SpawnWithSession(sessionID); err != nil {
882 log.Error().Err(err).Str("session_id", string(sessionID)).Msg("failed to spawn session")889 log.Error().Err(err).Str("session_id", string(sessionID)).Msg("failed to spawn session")
883 }890 }
@@ -1167,7 +1174,7 @@ func (a *App) switchToTargetTabIfConfigured(ctx context.Context, out *usecase.Mo
1167 return1174 return
1168 }1175 }
1169 1176
1170- switchToTarget := config.Get().Workspace.SwitchToTabOnMove1177+ switchToTarget := a.deps.Config.Workspace.SwitchToTabOnMove
1171 if out.SourceTabClosed {1178 if out.SourceTabClosed {
1172 switchToTarget = true1179 switchToTarget = true
1173 }1180 }
@@ -1227,8 +1234,8 @@ func (a *App) initTabPicker(ctx context.Context) {
1227 func (a *App) initSnapshotService(ctx context.Context) {1234 func (a *App) initSnapshotService(ctx context.Context) {
1228 log := logging.FromContext(ctx)1235 log := logging.FromContext(ctx)
1229 1236
1230- if a.deps == nil || a.deps.SnapshotUC == nil {1237+ if a.deps == nil || a.deps.SnapshotServiceFactory == nil {
1231- log.Debug().Msg("snapshot use case not available, skipping snapshot service")1238+ log.Debug().Msg("snapshot service factory not available, skipping")
1232 return1239 return
1233 }1240 }
1234 1241
@@ -1237,7 +1244,11 @@ func (a *App) initSnapshotService(ctx context.Context) {
1237 intervalMs = a.deps.Config.Session.SnapshotIntervalMs1244 intervalMs = a.deps.Config.Session.SnapshotIntervalMs
1238 }1245 }
1239 1246
1240- a.snapshotService = snapshot.NewService(a.deps.SnapshotUC, a, intervalMs)1247+ a.snapshotService = a.deps.SnapshotServiceFactory(a, intervalMs)
1248+ if a.snapshotService == nil {
1249+ log.Warn().Msg("snapshot service factory returned nil")
1250+ return
1251+ }
1241 a.snapshotService.Start(ctx)1252 a.snapshotService.Start(ctx)
1242 1253
1243 // Set up callback for main.go to notify when session is persisted1254 // Set up callback for main.go to notify when session is persisted
@@ -2047,7 +2058,9 @@ func (a *App) createWorkspaceViewWithoutAttach(ctx context.Context, tab *entity.
2047 // Set find bar config for this workspace view2058 // Set find bar config for this workspace view
2048 wsView.SetFindBarConfig(a.findBarCfg)2059 wsView.SetFindBarConfig(a.findBarCfg)
2049 // Set auto-open omnibox on new pane2060 // Set auto-open omnibox on new pane
2050- wsView.SetAutoOpenOnNewPane(config.Get().Omnibox.AutoOpenOnNewPane)2061+ if a.deps.Config != nil {
2062+ wsView.SetAutoOpenOnNewPane(a.deps.Config.Omnibox.AutoOpenOnNewPane)
2063+ }
2051 2064
2052 wsView.SetOnPaneFocused(func(paneID entity.PaneID) {2065 wsView.SetOnPaneFocused(func(paneID entity.PaneID) {
2053 if a.keyboardHandler != nil && a.keyboardHandler.Mode() == input.ModeResize {2066 if a.keyboardHandler != nil && a.keyboardHandler.Mode() == input.ModeResize {
@@ -2380,11 +2393,20 @@ func (a *App) installFloatingOverlayPositioning(tabID entity.TabID, workspaceOve
2380 gtkOverlay.ConnectGetChildPosition(&cb)2393 gtkOverlay.ConnectGetChildPosition(&cb)
2381 }2394 }
2382 2395
2383-func (a *App) currentFloatingConfig() config.FloatingPaneConfig {2396+// currentFloatingWidthPct returns the configured floating pane width percentage.
2397+func (a *App) currentFloatingWidthPct() float64 {
2398+ if a.deps != nil && a.deps.Config != nil {
2399+ return a.deps.Config.Workspace.FloatingPane.WidthPct
2400+ }
2401+ return 0
2402+}
2403+
2404+// currentFloatingHeightPct returns the configured floating pane height percentage.
2405+func (a *App) currentFloatingHeightPct() float64 {
2384 if a.deps != nil && a.deps.Config != nil {2406 if a.deps != nil && a.deps.Config != nil {
2385- return a.deps.Config.Workspace.FloatingPane2407+ return a.deps.Config.Workspace.FloatingPane.HeightPct
2386 }2408 }
2387- return config.Get().Workspace.FloatingPane2409+ return 0
2388 }2410 }
2389 2411
2390 func (a *App) ensureFloatingSession(2412 func (a *App) ensureFloatingSession(
@@ -2457,10 +2479,9 @@ func (a *App) ensureFloatingSession(
2457 wsView.AddWorkspaceOverlayWidget(pvOverlay)2479 wsView.AddWorkspaceOverlayWidget(pvOverlay)
2458 configureFloatingOverlayMeasurement(wsView.WorkspaceOverlayWidget(), pvOverlay)2480 configureFloatingOverlayMeasurement(wsView.WorkspaceOverlayWidget(), pvOverlay)
2459 2481
2460- floatingCfg := a.currentFloatingConfig()
2461 floatingPane := component.NewFloatingPane(wsView.WorkspaceOverlayWidget(), component.FloatingPaneOptions{2482 floatingPane := component.NewFloatingPane(wsView.WorkspaceOverlayWidget(), component.FloatingPaneOptions{
2462- WidthPct: floatingCfg.WidthPct,2483+ WidthPct: a.currentFloatingWidthPct(),
2463- HeightPct: floatingCfg.HeightPct,2484+ HeightPct: a.currentFloatingHeightPct(),
2464 FallbackWidth: floatingPaneFallbackWidth,2485 FallbackWidth: floatingPaneFallbackWidth,
2465 FallbackHeight: floatingPaneFallbackHeight,2486 FallbackHeight: floatingPaneFallbackHeight,
2466 OnNavigate: func(navCtx context.Context, url string) error {2487 OnNavigate: func(navCtx context.Context, url string) error {
@@ -3028,29 +3049,29 @@ func (a *App) SetOmniboxOnNavigate(fn func(url string)) {
3028 func (a *App) initConfigWatcher(ctx context.Context) {3049 func (a *App) initConfigWatcher(ctx context.Context) {
3029 log := logging.FromContext(ctx)3050 log := logging.FromContext(ctx)
3030 3051
3031- if a.configManager == nil {3052+ if a.deps.WatchConfig == nil || a.deps.OnConfigChange == nil {
3032- log.Debug().Msg("no config manager available, skipping watcher")3053+ log.Debug().Msg("no config watcher available, skipping")
3033 return3054 return
3034 }3055 }
3035 3056
3036 // Start viper watcher3057 // Start viper watcher
3037- if err := a.configManager.Watch(); err != nil {3058+ if err := a.deps.WatchConfig(); err != nil {
3038 log.Warn().Err(err).Msg("failed to start config watcher")3059 log.Warn().Err(err).Msg("failed to start config watcher")
3039 return3060 return
3040 }3061 }
3041 3062
3042 // Hot-reload appearance and keybindings on config change.3063 // Hot-reload appearance and keybindings on config change.
3043- a.configManager.OnConfigChange(func(newCfg *config.Config) {3064+ // deps.Config is updated in-place before this callback fires.
3044- cfgCopy := newCfg3065+ a.deps.OnConfigChange(func() {
3045 cb := glib.SourceFunc(func(_ uintptr) bool {3066 cb := glib.SourceFunc(func(_ uintptr) bool {
3046- a.applyAppearanceConfig(ctx, cfgCopy)3067+ a.applyAppearanceConfig(ctx)
3047 // Reload keyboard shortcuts3068 // Reload keyboard shortcuts
3048 if a.keyboardHandler != nil {3069 if a.keyboardHandler != nil {
3049- a.keyboardHandler.ReloadShortcuts(ctx, cfgCopy)3070+ a.keyboardHandler.ReloadShortcuts(ctx, a.deps.Config)
3050 }3071 }
3051 // Reload global shortcuts (removes stale, re-registers from config)3072 // Reload global shortcuts (removes stale, re-registers from config)
3052 if a.globalShortcutHandler != nil {3073 if a.globalShortcutHandler != nil {
3053- a.globalShortcutHandler.ReloadShortcuts(ctx, cfgCopy)3074+ a.globalShortcutHandler.ReloadShortcuts(ctx, a.deps.Config)
3054 }3075 }
3055 return false3076 return false
3056 })3077 })
@@ -3060,16 +3081,12 @@ func (a *App) initConfigWatcher(ctx context.Context) {
3060 log.Debug().Msg("config watcher initialized")3081 log.Debug().Msg("config watcher initialized")
3061 }3082 }
3062 3083
3063-func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) {3084+func (a *App) applyAppearanceConfig(ctx context.Context) {
3064 log := logging.FromContext(ctx)3085 log := logging.FromContext(ctx)
3065- if cfg == nil {3086+ if a.deps == nil || a.deps.Config == nil {
3066 return3087 return
3067 }3088 }
3068-3089+ cfg := a.deps.Config
3069- // Update the shared config pointer in-place so existing references see changes.
3070- if a.deps != nil && a.deps.Config != nil {
3071- *a.deps.Config = *cfg
3072- }
3073 3090
3074 if a.engine != nil {3091 if a.engine != nil {
3075 _ = a.engine.UpdateSettings(ctx, port.EngineSettingsUpdate{Raw: cfg})3092 _ = a.engine.UpdateSettings(ctx, port.EngineSettingsUpdate{Raw: cfg})
@@ -3081,7 +3098,7 @@ func (a *App) applyAppearanceConfig(ctx context.Context, cfg *config.Config) {
3081 }3098 }
3082 3099
3083 // Update GTK theme and injected WebUI theme vars3100 // Update GTK theme and injected WebUI theme vars
3084- if a.deps != nil && a.deps.Theme != nil {3101+ if a.deps.Theme != nil {
3085 var display *gdk.Display3102 var display *gdk.Display
3086 if a.mainWindow != nil && a.mainWindow.Window() != nil {3103 if a.mainWindow != nil && a.mainWindow.Window() != nil {
3087 display = a.mainWindow.Window().GetDisplay()3104 display = a.mainWindow.Window().GetDisplay()
M internal/ui/deps.go +18 -1
@@ -10,6 +10,7 @@ import (
1010 "github.com/bnema/dumber/internal/domain/repository"
1111 "github.com/bnema/dumber/internal/infrastructure/config"
1212 "github.com/bnema/dumber/internal/ui/theme"
13+ "github.com/jwijenbergh/puregotk/v4/gtk"
1314 )
1415
1516 // Dependencies holds all injected dependencies for the UI layer.
@@ -60,13 +61,29 @@ type Dependencies struct {
6061 IdleInhibitor port.IdleInhibitor
6162
6263 // Accent picker for dead keys support
63- InsertAccentUC *usecase.InsertAccentUseCase
64+ InsertAccentUC *usecase.InsertAccentUseCase
65+ AccentFocusProvider port.FocusedInputProvider
66+ // NewGTKEntryTarget creates a port.TextInputTarget from a GTK SearchEntry.
67+ // Injected to avoid importing infrastructure/textinput from the UI layer.
68+ NewGTKEntryTarget func(entry *gtk.SearchEntry) port.TextInputTarget
6469
6570 // Session management
6671 SessionStateRepo repository.SessionStateRepository
6772 SessionRepo repository.SessionRepository
6873 CurrentSessionID entity.SessionID
6974 SnapshotUC *usecase.SnapshotSessionUseCase
75+ // SnapshotServiceFactory creates a snapshot service bound to the given tab-list provider.
76+ // Called after the App is initialized so the App can serve as the provider.
77+ SnapshotServiceFactory func(provider port.TabListProvider, intervalMs int) port.SnapshotService
78+ // SessionSpawner spawns a new dumber instance for session restoration.
79+ SessionSpawner port.SessionSpawner
80+ // FileSystem provides file operations (e.g., for download deduplication).
81+ FileSystem port.FileSystem
82+ // OnConfigChange registers a callback for config hot-reload.
83+ // The config pointer in deps.Config is updated in-place before the callback fires.
84+ OnConfigChange func(callback func())
85+ // WatchConfig starts watching the config file for changes.
86+ WatchConfig func() error
7087
7188 // Update management
7289 CheckUpdateUC *usecase.CheckUpdateUseCase
@@ -10,6 +10,7 @@ import (
10 "github.com/bnema/dumber/internal/domain/repository"10 "github.com/bnema/dumber/internal/domain/repository"
11 "github.com/bnema/dumber/internal/infrastructure/config"11 "github.com/bnema/dumber/internal/infrastructure/config"
12 "github.com/bnema/dumber/internal/ui/theme"12 "github.com/bnema/dumber/internal/ui/theme"
13+ "github.com/jwijenbergh/puregotk/v4/gtk"
13 )14 )
14 15
15 // Dependencies holds all injected dependencies for the UI layer.16 // Dependencies holds all injected dependencies for the UI layer.
@@ -60,13 +61,29 @@ type Dependencies struct {
60 IdleInhibitor port.IdleInhibitor61 IdleInhibitor port.IdleInhibitor
61 62
62 // Accent picker for dead keys support63 // Accent picker for dead keys support
63- InsertAccentUC *usecase.InsertAccentUseCase64+ InsertAccentUC *usecase.InsertAccentUseCase
65+ AccentFocusProvider port.FocusedInputProvider
66+ // NewGTKEntryTarget creates a port.TextInputTarget from a GTK SearchEntry.
67+ // Injected to avoid importing infrastructure/textinput from the UI layer.
68+ NewGTKEntryTarget func(entry *gtk.SearchEntry) port.TextInputTarget
64 69
65 // Session management70 // Session management
66 SessionStateRepo repository.SessionStateRepository71 SessionStateRepo repository.SessionStateRepository
67 SessionRepo repository.SessionRepository72 SessionRepo repository.SessionRepository
68 CurrentSessionID entity.SessionID73 CurrentSessionID entity.SessionID
69 SnapshotUC *usecase.SnapshotSessionUseCase74 SnapshotUC *usecase.SnapshotSessionUseCase
75+ // SnapshotServiceFactory creates a snapshot service bound to the given tab-list provider.
76+ // Called after the App is initialized so the App can serve as the provider.
77+ SnapshotServiceFactory func(provider port.TabListProvider, intervalMs int) port.SnapshotService
78+ // SessionSpawner spawns a new dumber instance for session restoration.
79+ SessionSpawner port.SessionSpawner
80+ // FileSystem provides file operations (e.g., for download deduplication).
81+ FileSystem port.FileSystem
82+ // OnConfigChange registers a callback for config hot-reload.
83+ // The config pointer in deps.Config is updated in-place before the callback fires.
84+ OnConfigChange func(callback func())
85+ // WatchConfig starts watching the config file for changes.
86+ WatchConfig func() error
70 87
71 // Update management88 // Update management
72 CheckUpdateUC *usecase.CheckUpdateUseCase89 CheckUpdateUC *usecase.CheckUpdateUseCase

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