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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +159 -63
M cmd/dumber/main.go +30 -1
| @@ -20,15 +20,20 @@ import ( | |||
| 20 | 20 | "github.com/bnema/dumber/internal/infrastructure/clipboard" | |
| 21 | 21 | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 22 | 22 | "github.com/bnema/dumber/internal/infrastructure/deps" | |
| 23 | + | "github.com/bnema/dumber/internal/infrastructure/desktop" | |
| 23 | 24 | "github.com/bnema/dumber/internal/infrastructure/favicon" | |
| 25 | + | "github.com/bnema/dumber/internal/infrastructure/filesystem" | |
| 24 | 26 | "github.com/bnema/dumber/internal/infrastructure/idle" | |
| 25 | 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 | 30 | "github.com/bnema/dumber/internal/infrastructure/updater" | |
| 27 | 31 | "github.com/bnema/dumber/internal/infrastructure/xdg" | |
| 28 | 32 | "github.com/bnema/dumber/internal/logging" | |
| 29 | 33 | "github.com/bnema/dumber/internal/ui" | |
| 30 | 34 | "github.com/bnema/dumber/internal/ui/component" | |
| 31 | 35 | "github.com/bnema/dumber/internal/ui/theme" | |
| 36 | + | "github.com/jwijenbergh/puregotk/v4/gtk" | |
| 32 | 37 | "github.com/rs/zerolog" | |
| 33 | 38 | ) | |
| 34 | 39 | ||
| @@ -510,7 +515,9 @@ func buildUIDependencies( | |||
| 510 | 515 | ) *ui.Dependencies { | |
| 511 | 516 | filterManager := engine.InternalFilterManager() | |
| 512 | 517 | ||
| 513 | - | return &ui.Dependencies{ | |
| 518 | + | focusProvider := textinput.NewFocusProvider() | |
| 519 | + | ||
| 520 | + | uiDeps := &ui.Dependencies{ | |
| 514 | 521 | Ctx: ctx, | |
| 515 | 522 | Config: cfg, | |
| 516 | 523 | InitialURL: initialURL, | |
| @@ -541,7 +548,29 @@ func buildUIDependencies( | |||
| 541 | 548 | SessionStateRepo: repos.sessionState, | |
| 542 | 549 | CurrentSessionID: currentSessionID, | |
| 543 | 550 | SnapshotUC: uc.snapshot, | |
| 551 | + | SnapshotServiceFactory: func(provider port.TabListProvider, intervalMs int) port.SnapshotService { | |
| 552 | + | return snapshot.NewService(uc.snapshot, provider, intervalMs) | |
| 553 | + | }, | |
| 544 | 554 | CheckUpdateUC: uc.checkUpdate, | |
| 545 | 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 | } | |
| @@ -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 @@ | |||
| 1 | 1 | 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 | 9 | // TabListProvider provides access to the current tab list state. | |
| 6 | 10 | // Implemented by the UI layer to allow the snapshot service to read state. | |
| @@ -16,3 +20,18 @@ type SessionSpawner interface { | |||
| 16 | 20 | // SpawnWithSession starts a new dumber instance to restore a session. | |
| 17 | 21 | 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 | + | } | |
| @@ -1,6 +1,10 @@ | |||||
| 1 | package port | 1 | 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) error | 21 | 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 { | |||
| 18 | 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 | 28 | // FocusedInputProvider tracks which text input target currently has focus. | |
| 22 | 29 | // This allows the accent picker to insert text into the correct input field. | |
| 23 | 30 | 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 ( | |||
| 19 | 19 | fkRetryDelay = 100 * time.Millisecond | |
| 20 | 20 | ) | |
| 21 | 21 | ||
| 22 | + | // Compile-time interface check. | |
| 23 | + | var _ port.SnapshotService = (*Service)(nil) | |
| 24 | + | ||
| 22 | 25 | // Service handles debounced session state snapshots. | |
| 23 | 26 | type Service struct { | |
| 24 | 27 | snapshotUC *usecase.SnapshotSessionUseCase | |
| @@ -19,6 +19,9 @@ const ( | |||||
| 19 | fkRetryDelay = 100 * time.Millisecond | 19 | 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.SnapshotSessionUseCase | 27 | snapshotUC *usecase.SnapshotSessionUseCase | ||
M internal/infrastructure/textinput/gtk_entry_target.go +5 -1
| @@ -14,8 +14,12 @@ type GTKEntryTarget struct { | |||
| 14 | 14 | entry *gtk.SearchEntry | |
| 15 | 15 | } | |
| 16 | 16 | ||
| 17 | - | // Compile-time interface check. | |
| 17 | + | // Compile-time interface checks. | |
| 18 | 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 | 24 | // NewGTKEntryTarget creates a new GTK entry target for a SearchEntry. | |
| 21 | 25 | func NewGTKEntryTarget(entry *gtk.SearchEntry) *GTKEntryTarget { | |
| @@ -14,8 +14,12 @@ type GTKEntryTarget struct { | |||||
| 14 | entry *gtk.SearchEntry | 14 | 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 ( | |||
| 14 | 14 | "github.com/bnema/dumber/internal/application/usecase" | |
| 15 | 15 | "github.com/bnema/dumber/internal/domain/entity" | |
| 16 | 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 | 18 | "github.com/bnema/dumber/internal/logging" | |
| 24 | 19 | "github.com/bnema/dumber/internal/ui/adapter" | |
| @@ -66,12 +61,11 @@ type App struct { | |||
| 66 | 61 | tabsUC *usecase.ManageTabsUseCase | |
| 67 | 62 | ||
| 68 | 63 | // 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 | |
| 75 | 69 | ||
| 76 | 70 | // Pane management (used by coordinators) | |
| 77 | 71 | panesUC *usecase.ManagePanesUseCase | |
| @@ -115,7 +109,7 @@ type App struct { | |||
| 115 | 109 | tabPicker *component.TabPicker | |
| 116 | 110 | tabPickerWidget layout.Widget | |
| 117 | 111 | tabPickerPaneID entity.PaneID | |
| 118 | - | snapshotService *snapshot.Service | |
| 112 | + | snapshotService port.SnapshotService | |
| 119 | 113 | ||
| 120 | 114 | // Update management | |
| 121 | 115 | updateCoord *coordinator.UpdateCoordinator | |
| @@ -130,7 +124,7 @@ type App struct { | |||
| 130 | 124 | // Accent picker for dead keys support | |
| 131 | 125 | accentPicker *component.AccentPicker | |
| 132 | 126 | insertAccentUC *usecase.InsertAccentUseCase | |
| 133 | - | accentFocusProvider *textinput.FocusProvider | |
| 127 | + | accentFocusProvider port.FocusedInputProvider | |
| 134 | 128 | ||
| 135 | 129 | // Deferred initialization - runs after first load_started to avoid blocking initial navigation | |
| 136 | 130 | deferredInitOnce sync.Once | |
| @@ -176,15 +170,19 @@ func New(deps *Dependencies) (*App, error) { | |||
| 176 | 170 | workspaceViews: make(map[entity.TabID]*component.WorkspaceView), | |
| 177 | 171 | floatingSessions: make(map[floatingSessionKey]*floatingWorkspaceSession), | |
| 178 | 172 | engine: deps.Engine, | |
| 179 | - | configManager: config.GetManager(), | |
| 180 | 173 | cancel: cancel, | |
| 181 | 174 | } | |
| 182 | 175 | // Register message handlers through the engine. | |
| 183 | 176 | 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 | + | }}, | |
| 188 | 186 | OnClipboardCopied: func(textLen int) { | |
| 189 | 187 | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 190 | 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) | |
| 513 | - | a.accentFocusProvider = textinput.NewFocusProvider() | |
| 510 | + | // Use focus provider from deps (tracks which input has focus) | |
| 511 | + | a.accentFocusProvider = a.deps.AccentFocusProvider | |
| 514 | 512 | ||
| 515 | 513 | // Create the use case with glib.IdleAdd for thread-safe GTK calls | |
| 516 | 514 | a.insertAccentUC = usecase.NewInsertAccentUseCase( | |
| @@ -584,7 +582,7 @@ func (a *App) initDownloadHandler(ctx context.Context) { | |||
| 584 | 582 | eventAdapter := &downloadEventAdapter{app: a} | |
| 585 | 583 | ||
| 586 | 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 | 587 | if err := a.engine.ConfigureDownloads(ctx, downloadPath, eventAdapter, prepareDownloadUC); err != nil { | |
| 590 | 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 | 617 | glib.IdleAdd(&cb, 0) | |
| 620 | 618 | } | |
| 621 | 619 | ||
| 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 | + | } | |
| 624 | 624 | ||
| 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 { | |
| 628 | 627 | return false | |
| 629 | 628 | } | |
| 630 | - | return cfg.Clipboard.AutoCopyOnSelection | |
| 629 | + | return a.fn() | |
| 631 | 630 | } | |
| 632 | 631 | ||
| 633 | 632 | func (a *App) initKeyboardHandler(ctx context.Context) { | |
| @@ -678,7 +677,7 @@ func (a *App) initKeyboardHandler(ctx context.Context) { | |||
| 678 | 677 | // visible after focus returns to the WebView, so visibility alone | |
| 679 | 678 | // would misroute keys. | |
| 680 | 679 | if a.accentFocusProvider != nil { | |
| 681 | - | if _, ok := a.accentFocusProvider.GetFocusedInput().(*textinput.GTKEntryTarget); ok { | |
| 680 | + | if _, ok := a.accentFocusProvider.GetFocusedInput().(port.GTKEntryInputTarget); ok { | |
| 682 | 681 | // GTK Entry focused (omnibox or find bar): Alt-modified keys go to | |
| 683 | 682 | // shortcuts (pane navigation), other keys pass through to the widget's | |
| 684 | 683 | // own capture-phase key controller (which handles accent detection) | |
| @@ -786,7 +785,9 @@ func (a *App) initOmniboxConfig(ctx context.Context) { | |||
| 786 | 785 | OnFocusIn: func(entry *gtk.SearchEntry) { | |
| 787 | 786 | // Set omnibox entry as the focused input for accent picker | |
| 788 | 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 | 793 | OnFocusOut: func() { | |
| @@ -822,7 +823,9 @@ func (a *App) initFindBarConfig(ctx context.Context) { | |||
| 822 | 823 | OnFocusIn: func(entry *gtk.SearchEntry) { | |
| 823 | 824 | // Set find bar entry as the focused input for accent picker | |
| 824 | 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 | 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 restoration | |
| 868 | - | spawner := desktop.NewSessionSpawner(ctx) | |
| 870 | + | // Use injected session spawner for restoration | |
| 871 | + | spawner := a.deps.SessionSpawner | |
| 869 | 872 | ||
| 870 | 873 | // Create session manager component | |
| 871 | 874 | a.sessionManager = component.NewSessionManager(ctx, component.SessionManagerConfig{ | |
| @@ -878,6 +881,10 @@ func (a *App) initSessionManager(ctx context.Context) { | |||
| 878 | 881 | }, | |
| 879 | 882 | OnOpen: func(sessionID entity.SessionID) { | |
| 880 | 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 | 888 | if err := spawner.SpawnWithSession(sessionID); err != nil { | |
| 882 | 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 | 1174 | return | |
| 1168 | 1175 | } | |
| 1169 | 1176 | ||
| 1170 | - | switchToTarget := config.Get().Workspace.SwitchToTabOnMove | |
| 1177 | + | switchToTarget := a.deps.Config.Workspace.SwitchToTabOnMove | |
| 1171 | 1178 | if out.SourceTabClosed { | |
| 1172 | 1179 | switchToTarget = true | |
| 1173 | 1180 | } | |
| @@ -1227,8 +1234,8 @@ func (a *App) initTabPicker(ctx context.Context) { | |||
| 1227 | 1234 | func (a *App) initSnapshotService(ctx context.Context) { | |
| 1228 | 1235 | log := logging.FromContext(ctx) | |
| 1229 | 1236 | ||
| 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") | |
| 1232 | 1239 | return | |
| 1233 | 1240 | } | |
| 1234 | 1241 | ||
| @@ -1237,7 +1244,11 @@ func (a *App) initSnapshotService(ctx context.Context) { | |||
| 1237 | 1244 | 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 | 1252 | a.snapshotService.Start(ctx) | |
| 1242 | 1253 | ||
| 1243 | 1254 | // 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 | 2058 | // Set find bar config for this workspace view | |
| 2048 | 2059 | wsView.SetFindBarConfig(a.findBarCfg) | |
| 2049 | 2060 | // 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 | 2065 | wsView.SetOnPaneFocused(func(paneID entity.PaneID) { | |
| 2053 | 2066 | if a.keyboardHandler != nil && a.keyboardHandler.Mode() == input.ModeResize { | |
| @@ -2380,11 +2393,20 @@ func (a *App) installFloatingOverlayPositioning(tabID entity.TabID, workspaceOve | |||
| 2380 | 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 | 2406 | if a.deps != nil && a.deps.Config != nil { | |
| 2385 | - | return a.deps.Config.Workspace.FloatingPane | |
| 2407 | + | return a.deps.Config.Workspace.FloatingPane.HeightPct | |
| 2386 | 2408 | } | |
| 2387 | - | return config.Get().Workspace.FloatingPane | |
| 2409 | + | return 0 | |
| 2388 | 2410 | } | |
| 2389 | 2411 | ||
| 2390 | 2412 | func (a *App) ensureFloatingSession( | |
| @@ -2457,10 +2479,9 @@ func (a *App) ensureFloatingSession( | |||
| 2457 | 2479 | wsView.AddWorkspaceOverlayWidget(pvOverlay) | |
| 2458 | 2480 | configureFloatingOverlayMeasurement(wsView.WorkspaceOverlayWidget(), pvOverlay) | |
| 2459 | 2481 | ||
| 2460 | - | floatingCfg := a.currentFloatingConfig() | |
| 2461 | 2482 | floatingPane := component.NewFloatingPane(wsView.WorkspaceOverlayWidget(), component.FloatingPaneOptions{ | |
| 2462 | - | WidthPct: floatingCfg.WidthPct, | |
| 2463 | - | HeightPct: floatingCfg.HeightPct, | |
| 2483 | + | WidthPct: a.currentFloatingWidthPct(), | |
| 2484 | + | HeightPct: a.currentFloatingHeightPct(), | |
| 2464 | 2485 | FallbackWidth: floatingPaneFallbackWidth, | |
| 2465 | 2486 | FallbackHeight: floatingPaneFallbackHeight, | |
| 2466 | 2487 | OnNavigate: func(navCtx context.Context, url string) error { | |
| @@ -3028,29 +3049,29 @@ func (a *App) SetOmniboxOnNavigate(fn func(url string)) { | |||
| 3028 | 3049 | func (a *App) initConfigWatcher(ctx context.Context) { | |
| 3029 | 3050 | log := logging.FromContext(ctx) | |
| 3030 | 3051 | ||
| 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") | |
| 3033 | 3054 | return | |
| 3034 | 3055 | } | |
| 3035 | 3056 | ||
| 3036 | 3057 | // Start viper watcher | |
| 3037 | - | if err := a.configManager.Watch(); err != nil { | |
| 3058 | + | if err := a.deps.WatchConfig(); err != nil { | |
| 3038 | 3059 | log.Warn().Err(err).Msg("failed to start config watcher") | |
| 3039 | 3060 | return | |
| 3040 | 3061 | } | |
| 3041 | 3062 | ||
| 3042 | 3063 | // 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() { | |
| 3045 | 3066 | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 3046 | - | a.applyAppearanceConfig(ctx, cfgCopy) | |
| 3067 | + | a.applyAppearanceConfig(ctx) | |
| 3047 | 3068 | // Reload keyboard shortcuts | |
| 3048 | 3069 | if a.keyboardHandler != nil { | |
| 3049 | - | a.keyboardHandler.ReloadShortcuts(ctx, cfgCopy) | |
| 3070 | + | a.keyboardHandler.ReloadShortcuts(ctx, a.deps.Config) | |
| 3050 | 3071 | } | |
| 3051 | 3072 | // Reload global shortcuts (removes stale, re-registers from config) | |
| 3052 | 3073 | if a.globalShortcutHandler != nil { | |
| 3053 | - | a.globalShortcutHandler.ReloadShortcuts(ctx, cfgCopy) | |
| 3074 | + | a.globalShortcutHandler.ReloadShortcuts(ctx, a.deps.Config) | |
| 3054 | 3075 | } | |
| 3055 | 3076 | return false | |
| 3056 | 3077 | }) | |
| @@ -3060,16 +3081,12 @@ func (a *App) initConfigWatcher(ctx context.Context) { | |||
| 3060 | 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 | 3085 | log := logging.FromContext(ctx) | |
| 3065 | - | if cfg == nil { | |
| 3086 | + | if a.deps == nil || a.deps.Config == nil { | |
| 3066 | 3087 | return | |
| 3067 | 3088 | } | |
| 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 | |
| 3073 | 3090 | ||
| 3074 | 3091 | if a.engine != nil { | |
| 3075 | 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 | 3100 | // Update GTK theme and injected WebUI theme vars | |
| 3084 | - | if a.deps != nil && a.deps.Theme != nil { | |
| 3101 | + | if a.deps.Theme != nil { | |
| 3085 | 3102 | var display *gdk.Display | |
| 3086 | 3103 | if a.mainWindow != nil && a.mainWindow.Window() != nil { | |
| 3087 | 3104 | 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.ManageTabsUseCase | 61 | tabsUC *usecase.ManageTabsUseCase | ||
| 67 | 62 | ||||
| 68 | // Coordinators (new architecture) | 63 | // Coordinators (new architecture) | ||
| 69 | - | contentCoord *content.Coordinator | 64 | + | contentCoord *content.Coordinator |
| 70 | - | tabCoord *coordinator.TabCoordinator | 65 | + | tabCoord *coordinator.TabCoordinator |
| 71 | - | wsCoord *coordinator.WorkspaceCoordinator | 66 | + | wsCoord *coordinator.WorkspaceCoordinator |
| 72 | - | navCoord *coordinator.NavigationCoordinator | 67 | + | navCoord *coordinator.NavigationCoordinator |
| 73 | - | kbDispatcher *dispatcher.KeyboardDispatcher | 68 | + | kbDispatcher *dispatcher.KeyboardDispatcher |
| 74 | - | configManager *config.Manager | |||
| 75 | 69 | ||||
| 76 | // Pane management (used by coordinators) | 70 | // Pane management (used by coordinators) | ||
| 77 | panesUC *usecase.ManagePanesUseCase | 71 | panesUC *usecase.ManagePanesUseCase | ||
| @@ -115,7 +109,7 @@ type App struct { | |||||
| 115 | tabPicker *component.TabPicker | 109 | tabPicker *component.TabPicker | ||
| 116 | tabPickerWidget layout.Widget | 110 | tabPickerWidget layout.Widget | ||
| 117 | tabPickerPaneID entity.PaneID | 111 | tabPickerPaneID entity.PaneID | ||
| 118 | - | snapshotService *snapshot.Service | 112 | + | snapshotService port.SnapshotService |
| 119 | 113 | ||||
| 120 | // Update management | 114 | // Update management | ||
| 121 | updateCoord *coordinator.UpdateCoordinator | 115 | updateCoord *coordinator.UpdateCoordinator | ||
| @@ -130,7 +124,7 @@ type App struct { | |||||
| 130 | // Accent picker for dead keys support | 124 | // Accent picker for dead keys support | ||
| 131 | accentPicker *component.AccentPicker | 125 | accentPicker *component.AccentPicker | ||
| 132 | insertAccentUC *usecase.InsertAccentUseCase | 126 | insertAccentUC *usecase.InsertAccentUseCase | ||
| 133 | - | accentFocusProvider *textinput.FocusProvider | 127 | + | accentFocusProvider port.FocusedInputProvider |
| 134 | 128 | ||||
| 135 | // Deferred initialization - runs after first load_started to avoid blocking initial navigation | 129 | // Deferred initialization - runs after first load_started to avoid blocking initial navigation | ||
| 136 | deferredInitOnce sync.Once | 130 | 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 calls | 513 | // 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 false | 627 | return false | ||
| 629 | } | 628 | } | ||
| 630 | - | return cfg.Clipboard.AutoCopyOnSelection | 629 | + | 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 alone | 677 | // 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 to | 681 | // GTK Entry focused (omnibox or find bar): Alt-modified keys go to | ||
| 683 | // shortcuts (pane navigation), other keys pass through to the widget's | 682 | // 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 picker | 786 | // 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 picker | 824 | // 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 restoration | 870 | + | // Use injected session spawner for restoration |
| 868 | - | spawner := desktop.NewSessionSpawner(ctx) | 871 | + | spawner := a.deps.SessionSpawner |
| 869 | 872 | ||||
| 870 | // Create session manager component | 873 | // 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 | return | 1174 | return | ||
| 1168 | } | 1175 | } | ||
| 1169 | 1176 | ||||
| 1170 | - | switchToTarget := config.Get().Workspace.SwitchToTabOnMove | 1177 | + | switchToTarget := a.deps.Config.Workspace.SwitchToTabOnMove |
| 1171 | if out.SourceTabClosed { | 1178 | if out.SourceTabClosed { | ||
| 1172 | switchToTarget = true | 1179 | 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 | return | 1239 | return | ||
| 1233 | } | 1240 | } | ||
| 1234 | 1241 | ||||
| @@ -1237,7 +1244,11 @@ func (a *App) initSnapshotService(ctx context.Context) { | |||||
| 1237 | intervalMs = a.deps.Config.Session.SnapshotIntervalMs | 1244 | 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 persisted | 1254 | // 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 view | 2058 | // 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 pane | 2060 | // 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.FloatingPane | 2407 | + | return a.deps.Config.Workspace.FloatingPane.HeightPct |
| 2386 | } | 2408 | } | ||
| 2387 | - | return config.Get().Workspace.FloatingPane | 2409 | + | 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 | return | 3054 | return | ||
| 3034 | } | 3055 | } | ||
| 3035 | 3056 | ||||
| 3036 | // Start viper watcher | 3057 | // 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 | return | 3060 | 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 := newCfg | 3065 | + | 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 shortcuts | 3068 | // 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 false | 3076 | 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 | return | 3087 | 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 vars | 3100 | // 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.Display | 3102 | 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 ( | |||
| 10 | 10 | "github.com/bnema/dumber/internal/domain/repository" | |
| 11 | 11 | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 12 | 12 | "github.com/bnema/dumber/internal/ui/theme" | |
| 13 | + | "github.com/jwijenbergh/puregotk/v4/gtk" | |
| 13 | 14 | ) | |
| 14 | 15 | ||
| 15 | 16 | // Dependencies holds all injected dependencies for the UI layer. | |
| @@ -60,13 +61,29 @@ type Dependencies struct { | |||
| 60 | 61 | IdleInhibitor port.IdleInhibitor | |
| 61 | 62 | ||
| 62 | 63 | // 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 | |
| 64 | 69 | ||
| 65 | 70 | // Session management | |
| 66 | 71 | SessionStateRepo repository.SessionStateRepository | |
| 67 | 72 | SessionRepo repository.SessionRepository | |
| 68 | 73 | CurrentSessionID entity.SessionID | |
| 69 | 74 | 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 | 88 | // Update management | |
| 72 | 89 | 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.IdleInhibitor | 61 | IdleInhibitor port.IdleInhibitor | ||
| 61 | 62 | ||||
| 62 | // Accent picker for dead keys support | 63 | // 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 | |||
| 64 | 69 | ||||
| 65 | // Session management | 70 | // Session management | ||
| 66 | SessionStateRepo repository.SessionStateRepository | 71 | SessionStateRepo repository.SessionStateRepository | ||
| 67 | SessionRepo repository.SessionRepository | 72 | SessionRepo repository.SessionRepository | ||
| 68 | CurrentSessionID entity.SessionID | 73 | CurrentSessionID entity.SessionID | ||
| 69 | SnapshotUC *usecase.SnapshotSessionUseCase | 74 | 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 management | 88 | // Update management | ||
| 72 | CheckUpdateUC *usecase.CheckUpdateUseCase | 89 | CheckUpdateUC *usecase.CheckUpdateUseCase | ||