refactor(ui): integrate webkit pool, injector, router into app lifecycle
Major refactor of App struct to integrate the new webkit infrastructure: - Add fields for WebViewPool, ContentInjector, MessageRouter, OverlayController - Manage WebView instances by PaneID - Use context.WithCancelCause for improved shutdown handling - Prewarm WebView pool during app activation - Update Dependencies to include new injector and router instances Updates component layer (WorkspaceView, TabBar, PaneView) to support the new WebView management architecture.
- commit
- a970f83306731d8e8cfd25007cced879226bfd83
- author
- brice · · 2025-12-11 17:59 UTC
- committer
- brice · · 2025-12-11 17:12 UTC
- parent
- 738cf56
- signature
- unsigned
8 files · +385 -153
M internal/ui/app.go +337 -125
| @@ -2,19 +2,23 @@ package ui | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "errors" | |
| 6 | + | "fmt" | |
| 5 | 7 | "os" | |
| 6 | 8 | "sync" | |
| 7 | 9 | ||
| 10 | + | "github.com/jwijenbergh/puregotk/v4/gio" | |
| 11 | + | "github.com/jwijenbergh/puregotk/v4/gtk" | |
| 12 | + | ||
| 8 | 13 | "github.com/bnema/dumber/internal/application/usecase" | |
| 9 | 14 | "github.com/bnema/dumber/internal/domain/entity" | |
| 15 | + | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 10 | 16 | "github.com/bnema/dumber/internal/logging" | |
| 11 | 17 | "github.com/bnema/dumber/internal/ui/component" | |
| 18 | + | uihandler "github.com/bnema/dumber/internal/ui/handler" | |
| 12 | 19 | "github.com/bnema/dumber/internal/ui/input" | |
| 13 | 20 | "github.com/bnema/dumber/internal/ui/layout" | |
| 14 | 21 | "github.com/bnema/dumber/internal/ui/window" | |
| 15 | - | "github.com/jwijenbergh/puregotk/v4/gio" | |
| 16 | - | "github.com/jwijenbergh/puregotk/v4/gtk" | |
| 17 | - | "github.com/rs/zerolog" | |
| 18 | 22 | ) | |
| 19 | 23 | ||
| 20 | 24 | const ( | |
| @@ -40,14 +44,20 @@ type App struct { | |||
| 40 | 44 | // Input handling | |
| 41 | 45 | keyboardHandler *input.KeyboardHandler | |
| 42 | 46 | ||
| 47 | + | // Web content | |
| 48 | + | pool *webkit.WebViewPool | |
| 49 | + | injector *webkit.ContentInjector | |
| 50 | + | router *webkit.MessageRouter | |
| 51 | + | overlay *component.OverlayController | |
| 52 | + | settings *webkit.SettingsManager | |
| 53 | + | webViews map[entity.PaneID]*webkit.WebView | |
| 54 | + | ||
| 43 | 55 | // ID generator for tabs/panes | |
| 44 | 56 | idCounter uint64 | |
| 45 | 57 | idMu sync.Mutex | |
| 46 | 58 | ||
| 47 | - | // Lifecycle | |
| 48 | - | ctx context.Context | |
| 49 | - | cancel context.CancelFunc | |
| 50 | - | logger *zerolog.Logger | |
| 59 | + | // lifecycle | |
| 60 | + | cancel context.CancelCauseFunc | |
| 51 | 61 | } | |
| 52 | 62 | ||
| 53 | 63 | // New creates a new App with the given dependencies. | |
| @@ -56,8 +66,7 @@ func New(deps *Dependencies) (*App, error) { | |||
| 56 | 66 | return nil, err | |
| 57 | 67 | } | |
| 58 | 68 | ||
| 59 | - | ctx, cancel := context.WithCancel(deps.Ctx) | |
| 60 | - | logger := logging.FromContext(ctx) | |
| 69 | + | ctx, cancel := context.WithCancelCause(deps.Ctx) | |
| 61 | 70 | ||
| 62 | 71 | app := &App{ | |
| 63 | 72 | deps: deps, | |
| @@ -65,9 +74,19 @@ func New(deps *Dependencies) (*App, error) { | |||
| 65 | 74 | tabsUC: deps.TabsUC, | |
| 66 | 75 | panesUC: deps.PanesUC, | |
| 67 | 76 | workspaceViews: make(map[entity.TabID]*component.WorkspaceView), | |
| 68 | - | ctx: ctx, | |
| 77 | + | pool: deps.Pool, | |
| 78 | + | injector: deps.Injector, | |
| 79 | + | router: deps.MessageRouter, | |
| 80 | + | overlay: deps.Overlay, | |
| 81 | + | settings: deps.Settings, | |
| 82 | + | webViews: make(map[entity.PaneID]*webkit.WebView), | |
| 69 | 83 | cancel: cancel, | |
| 70 | - | logger: logger, | |
| 84 | + | } | |
| 85 | + | if app.overlay == nil { | |
| 86 | + | app.overlay = component.NewOverlayController(ctx) | |
| 87 | + | } | |
| 88 | + | if app.router == nil { | |
| 89 | + | app.router = webkit.NewMessageRouter(ctx) | |
| 71 | 90 | } | |
| 72 | 91 | ||
| 73 | 92 | return app, nil | |
| @@ -75,43 +94,50 @@ func New(deps *Dependencies) (*App, error) { | |||
| 75 | 94 | ||
| 76 | 95 | // Run starts the GTK application and blocks until it exits. | |
| 77 | 96 | // Returns the exit code. | |
| 78 | - | func (a *App) Run(args []string) int { | |
| 79 | - | a.logger.Debug().Msg("creating GTK application") | |
| 97 | + | func (a *App) Run(ctx context.Context, args []string) int { | |
| 98 | + | log := logging.FromContext(ctx) | |
| 99 | + | log.Debug().Msg("creating GTK application") | |
| 80 | 100 | ||
| 81 | 101 | a.gtkApp = gtk.NewApplication(AppID, gio.GApplicationFlagsNoneValue) | |
| 82 | 102 | if a.gtkApp == nil { | |
| 83 | - | a.logger.Error().Msg("failed to create GTK application") | |
| 103 | + | log.Error().Msg("failed to create GTK application") | |
| 84 | 104 | return 1 | |
| 85 | 105 | } | |
| 86 | 106 | defer a.gtkApp.Unref() | |
| 87 | 107 | ||
| 88 | 108 | // Connect activate signal | |
| 89 | 109 | activateCb := func(_ gio.Application) { | |
| 90 | - | a.onActivate() | |
| 110 | + | a.onActivate(ctx) | |
| 91 | 111 | } | |
| 92 | 112 | a.gtkApp.ConnectActivate(&activateCb) | |
| 93 | 113 | ||
| 94 | 114 | // Connect shutdown signal | |
| 95 | 115 | shutdownCb := func(_ gio.Application) { | |
| 96 | - | a.onShutdown() | |
| 116 | + | a.onShutdown(ctx) | |
| 97 | 117 | } | |
| 98 | 118 | a.gtkApp.ConnectShutdown(&shutdownCb) | |
| 99 | 119 | ||
| 100 | - | a.logger.Info().Msg("starting GTK main loop") | |
| 120 | + | log.Info().Msg("starting GTK main loop") | |
| 101 | 121 | code := a.gtkApp.Run(len(args), args) | |
| 102 | 122 | ||
| 103 | 123 | return code | |
| 104 | 124 | } | |
| 105 | 125 | ||
| 106 | 126 | // onActivate is called when the GTK application is activated. | |
| 107 | - | func (a *App) onActivate() { | |
| 108 | - | a.logger.Debug().Msg("GTK application activated") | |
| 127 | + | func (a *App) onActivate(ctx context.Context) { | |
| 128 | + | log := logging.FromContext(ctx) | |
| 129 | + | log.Debug().Msg("GTK application activated") | |
| 130 | + | ||
| 131 | + | // Prewarm WebView pool now that GTK is initialized | |
| 132 | + | if a.pool != nil { | |
| 133 | + | a.pool.Prewarm(0) | |
| 134 | + | } | |
| 109 | 135 | ||
| 110 | 136 | // Create the main window | |
| 111 | 137 | var err error | |
| 112 | - | a.mainWindow, err = window.New(a.gtkApp, a.deps.Config, a.logger) | |
| 138 | + | a.mainWindow, err = window.New(a.gtkApp, a.deps.Config, a.deps.Logger) | |
| 113 | 139 | if err != nil { | |
| 114 | - | a.logger.Error().Err(err).Msg("failed to create main window") | |
| 140 | + | log.Error().Err(err).Msg("failed to create main window") | |
| 115 | 141 | return | |
| 116 | 142 | } | |
| 117 | 143 | ||
| @@ -120,52 +146,62 @@ func (a *App) onActivate() { | |||
| 120 | 146 | ||
| 121 | 147 | // Create keyboard handler | |
| 122 | 148 | a.keyboardHandler = input.NewKeyboardHandler( | |
| 123 | - | a.ctx, | |
| 149 | + | ctx, | |
| 124 | 150 | &a.deps.Config.Workspace, | |
| 125 | - | a.logger, | |
| 151 | + | a.deps.Logger, | |
| 126 | 152 | ) | |
| 127 | 153 | a.keyboardHandler.SetOnAction(a.handleKeyboardAction) | |
| 128 | - | a.keyboardHandler.SetOnModeChange(a.handleModeChange) | |
| 154 | + | // Wrap handleModeChange to match the callback signature | |
| 155 | + | a.keyboardHandler.SetOnModeChange(func(from, to input.Mode) { | |
| 156 | + | a.handleModeChange(ctx, from, to) | |
| 157 | + | }) | |
| 129 | 158 | a.keyboardHandler.AttachTo(a.mainWindow.Window()) | |
| 130 | 159 | ||
| 160 | + | if err := a.registerMessageHandlers(ctx); err != nil { | |
| 161 | + | log.Warn().Err(err).Msg("failed to register message handlers") | |
| 162 | + | } | |
| 163 | + | ||
| 131 | 164 | // Create an initial tab | |
| 132 | - | a.createInitialTab() | |
| 165 | + | a.createInitialTab(ctx) | |
| 133 | 166 | ||
| 134 | 167 | // Show the window | |
| 135 | 168 | a.mainWindow.Show() | |
| 136 | 169 | ||
| 137 | - | a.logger.Info().Msg("main window displayed") | |
| 170 | + | log.Info().Msg("main window displayed") | |
| 138 | 171 | } | |
| 139 | 172 | ||
| 140 | 173 | // onShutdown is called when the GTK application is shutting down. | |
| 141 | - | func (a *App) onShutdown() { | |
| 142 | - | a.logger.Debug().Msg("GTK application shutting down") | |
| 174 | + | func (a *App) onShutdown(ctx context.Context) { | |
| 175 | + | log := logging.FromContext(ctx) | |
| 176 | + | log.Debug().Msg("GTK application shutting down") | |
| 143 | 177 | ||
| 144 | 178 | // Cancel context to signal all goroutines | |
| 145 | - | a.cancel() | |
| 179 | + | a.cancel(errors.New("application shutdown")) | |
| 146 | 180 | ||
| 147 | 181 | // Cleanup resources | |
| 148 | 182 | if a.deps.Pool != nil { | |
| 149 | 183 | a.deps.Pool.Close() | |
| 150 | 184 | } | |
| 151 | 185 | ||
| 152 | - | a.logger.Info().Msg("application shutdown complete") | |
| 186 | + | log.Info().Msg("application shutdown complete") | |
| 153 | 187 | } | |
| 154 | 188 | ||
| 155 | 189 | // createInitialTab creates the first tab when the application starts. | |
| 156 | - | func (a *App) createInitialTab() { | |
| 190 | + | func (a *App) createInitialTab(ctx context.Context) { | |
| 191 | + | log := logging.FromContext(ctx) | |
| 192 | + | ||
| 157 | 193 | if a.tabsUC == nil { | |
| 158 | 194 | // Create use case if not injected | |
| 159 | 195 | a.tabsUC = usecase.NewManageTabsUseCase(a.generateID) | |
| 160 | 196 | } | |
| 161 | 197 | ||
| 162 | - | output, err := a.tabsUC.Create(a.ctx, usecase.CreateTabInput{ | |
| 198 | + | output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{ | |
| 163 | 199 | TabList: a.tabs, | |
| 164 | 200 | Name: "", | |
| 165 | - | InitialURL: "about:blank", | |
| 201 | + | InitialURL: "https://duckduckgo.com", | |
| 166 | 202 | }) | |
| 167 | 203 | if err != nil { | |
| 168 | - | a.logger.Error().Err(err).Msg("failed to create initial tab") | |
| 204 | + | log.Error().Err(err).Msg("failed to create initial tab") | |
| 169 | 205 | return | |
| 170 | 206 | } | |
| 171 | 207 | ||
| @@ -176,11 +212,9 @@ func (a *App) createInitialTab() { | |||
| 176 | 212 | } | |
| 177 | 213 | ||
| 178 | 214 | // Create workspace view for this tab | |
| 179 | - | a.createWorkspaceView(output.Tab) | |
| 215 | + | a.createWorkspaceView(ctx, output.Tab) | |
| 180 | 216 | ||
| 181 | - | a.logger.Debug(). | |
| 182 | - | Str("tab_id", string(output.Tab.ID)). | |
| 183 | - | Msg("initial tab created") | |
| 217 | + | log.Debug().Str("tab_id", string(output.Tab.ID)).Msg("initial tab created") | |
| 184 | 218 | } | |
| 185 | 219 | ||
| 186 | 220 | // generateID generates a unique ID for tabs and panes. | |
| @@ -209,7 +243,7 @@ func (a *App) Quit() { | |||
| 209 | 243 | } | |
| 210 | 244 | ||
| 211 | 245 | // RunWithArgs is a convenience function that creates and runs an App. | |
| 212 | - | func RunWithArgs(deps *Dependencies) int { | |
| 246 | + | func RunWithArgs(ctx context.Context, deps *Dependencies) int { | |
| 213 | 247 | app, err := New(deps) | |
| 214 | 248 | if err != nil { | |
| 215 | 249 | if deps.Logger != nil { | |
| @@ -217,96 +251,93 @@ func RunWithArgs(deps *Dependencies) int { | |||
| 217 | 251 | } | |
| 218 | 252 | return 1 | |
| 219 | 253 | } | |
| 220 | - | return app.Run(os.Args) | |
| 254 | + | return app.Run(ctx, os.Args) | |
| 221 | 255 | } | |
| 222 | 256 | ||
| 223 | 257 | // handleKeyboardAction dispatches keyboard actions to appropriate handlers. | |
| 224 | 258 | func (a *App) handleKeyboardAction(ctx context.Context, action input.Action) error { | |
| 225 | - | a.logger.Debug(). | |
| 226 | - | Str("action", string(action)). | |
| 227 | - | Msg("handling keyboard action") | |
| 259 | + | log := logging.FromContext(ctx) | |
| 260 | + | log.Debug().Str("action", string(action)).Msg("handling keyboard action") | |
| 228 | 261 | ||
| 229 | 262 | switch action { | |
| 230 | 263 | // Tab actions | |
| 231 | 264 | case input.ActionNewTab: | |
| 232 | - | return a.handleNewTab() | |
| 265 | + | return a.handleNewTab(ctx) | |
| 233 | 266 | case input.ActionCloseTab: | |
| 234 | - | return a.handleCloseTab() | |
| 267 | + | return a.handleCloseTab(ctx) | |
| 235 | 268 | case input.ActionNextTab: | |
| 236 | - | return a.handleNextTab() | |
| 269 | + | return a.handleNextTab(ctx) | |
| 237 | 270 | case input.ActionPreviousTab: | |
| 238 | - | return a.handlePreviousTab() | |
| 271 | + | return a.handlePreviousTab(ctx) | |
| 239 | 272 | ||
| 240 | 273 | // Pane actions | |
| 241 | 274 | case input.ActionSplitRight: | |
| 242 | - | return a.handlePaneSplit(usecase.SplitRight) | |
| 275 | + | return a.handlePaneSplit(ctx, usecase.SplitRight) | |
| 243 | 276 | case input.ActionSplitLeft: | |
| 244 | - | return a.handlePaneSplit(usecase.SplitLeft) | |
| 277 | + | return a.handlePaneSplit(ctx, usecase.SplitLeft) | |
| 245 | 278 | case input.ActionSplitUp: | |
| 246 | - | return a.handlePaneSplit(usecase.SplitUp) | |
| 279 | + | return a.handlePaneSplit(ctx, usecase.SplitUp) | |
| 247 | 280 | case input.ActionSplitDown: | |
| 248 | - | return a.handlePaneSplit(usecase.SplitDown) | |
| 281 | + | return a.handlePaneSplit(ctx, usecase.SplitDown) | |
| 249 | 282 | case input.ActionClosePane: | |
| 250 | - | return a.handleClosePane() | |
| 283 | + | return a.handleClosePane(ctx) | |
| 251 | 284 | case input.ActionFocusRight: | |
| 252 | - | return a.handlePaneFocus(usecase.NavRight) | |
| 285 | + | return a.handlePaneFocus(ctx, usecase.NavRight) | |
| 253 | 286 | case input.ActionFocusLeft: | |
| 254 | - | return a.handlePaneFocus(usecase.NavLeft) | |
| 287 | + | return a.handlePaneFocus(ctx, usecase.NavLeft) | |
| 255 | 288 | case input.ActionFocusUp: | |
| 256 | - | return a.handlePaneFocus(usecase.NavUp) | |
| 289 | + | return a.handlePaneFocus(ctx, usecase.NavUp) | |
| 257 | 290 | case input.ActionFocusDown: | |
| 258 | - | return a.handlePaneFocus(usecase.NavDown) | |
| 291 | + | return a.handlePaneFocus(ctx, usecase.NavDown) | |
| 259 | 292 | ||
| 260 | 293 | // Navigation (stub implementations for now) | |
| 261 | 294 | case input.ActionGoBack: | |
| 262 | - | a.logger.Debug().Msg("go back action (not yet implemented)") | |
| 295 | + | log.Debug().Msg("go back action (not yet implemented)") | |
| 263 | 296 | case input.ActionGoForward: | |
| 264 | - | a.logger.Debug().Msg("go forward action (not yet implemented)") | |
| 297 | + | log.Debug().Msg("go forward action (not yet implemented)") | |
| 265 | 298 | case input.ActionReload: | |
| 266 | - | a.logger.Debug().Msg("reload action (not yet implemented)") | |
| 299 | + | log.Debug().Msg("reload action (not yet implemented)") | |
| 267 | 300 | case input.ActionHardReload: | |
| 268 | - | a.logger.Debug().Msg("hard reload action (not yet implemented)") | |
| 301 | + | log.Debug().Msg("hard reload action (not yet implemented)") | |
| 269 | 302 | ||
| 270 | 303 | // Zoom (stub implementations for now) | |
| 271 | 304 | case input.ActionZoomIn: | |
| 272 | - | a.logger.Debug().Msg("zoom in action (not yet implemented)") | |
| 305 | + | log.Debug().Msg("zoom in action (not yet implemented)") | |
| 273 | 306 | case input.ActionZoomOut: | |
| 274 | - | a.logger.Debug().Msg("zoom out action (not yet implemented)") | |
| 307 | + | log.Debug().Msg("zoom out action (not yet implemented)") | |
| 275 | 308 | case input.ActionZoomReset: | |
| 276 | - | a.logger.Debug().Msg("zoom reset action (not yet implemented)") | |
| 309 | + | log.Debug().Msg("zoom reset action (not yet implemented)") | |
| 277 | 310 | ||
| 278 | 311 | // UI | |
| 279 | 312 | case input.ActionOpenOmnibox: | |
| 280 | - | a.logger.Debug().Msg("open omnibox action (not yet implemented)") | |
| 313 | + | return a.openOmnibox(ctx) | |
| 281 | 314 | case input.ActionOpenDevTools: | |
| 282 | - | a.logger.Debug().Msg("open devtools action (not yet implemented)") | |
| 315 | + | return a.openDevTools(ctx) | |
| 283 | 316 | case input.ActionToggleFullscreen: | |
| 284 | - | a.logger.Debug().Msg("toggle fullscreen action (not yet implemented)") | |
| 317 | + | log.Debug().Msg("toggle fullscreen action (not yet implemented)") | |
| 285 | 318 | ||
| 286 | 319 | // Application | |
| 287 | 320 | case input.ActionQuit: | |
| 288 | 321 | a.Quit() | |
| 289 | 322 | ||
| 290 | 323 | default: | |
| 291 | - | a.logger.Warn().Str("action", string(action)).Msg("unhandled keyboard action") | |
| 324 | + | log.Warn().Str("action", string(action)).Msg("unhandled keyboard action") | |
| 292 | 325 | } | |
| 293 | 326 | ||
| 294 | 327 | return nil | |
| 295 | 328 | } | |
| 296 | 329 | ||
| 297 | 330 | // handleModeChange is called when the input mode changes. | |
| 298 | - | func (a *App) handleModeChange(from, to input.Mode) { | |
| 299 | - | a.logger.Debug(). | |
| 300 | - | Str("from", from.String()). | |
| 301 | - | Str("to", to.String()). | |
| 302 | - | Msg("input mode changed") | |
| 303 | - | ||
| 304 | - | // TODO: Update UI to indicate mode (e.g., change border color) | |
| 331 | + | func (a *App) handleModeChange(ctx context.Context, from, to input.Mode) { | |
| 332 | + | log := logging.FromContext(ctx) | |
| 333 | + | log.Debug().Str("from", from.String()).Str("to", to.String()).Msg("input mode changed") | |
| 305 | 334 | } | |
| 306 | 335 | ||
| 307 | 336 | // handleNewTab creates a new tab. | |
| 308 | - | func (a *App) handleNewTab() error { | |
| 309 | - | output, err := a.tabsUC.Create(a.ctx, usecase.CreateTabInput{ | |
| 337 | + | func (a *App) handleNewTab(ctx context.Context) error { | |
| 338 | + | log := logging.FromContext(ctx) | |
| 339 | + | ||
| 340 | + | output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{ | |
| 310 | 341 | TabList: a.tabs, | |
| 311 | 342 | Name: "", | |
| 312 | 343 | InitialURL: "about:blank", | |
| @@ -320,21 +351,19 @@ func (a *App) handleNewTab() error { | |||
| 320 | 351 | a.mainWindow.TabBar().SetActive(output.Tab.ID) | |
| 321 | 352 | } | |
| 322 | 353 | ||
| 323 | - | a.logger.Debug(). | |
| 324 | - | Str("tab_id", string(output.Tab.ID)). | |
| 325 | - | Msg("new tab created") | |
| 354 | + | log.Debug().Str("tab_id", string(output.Tab.ID)).Msg("new tab created") | |
| 326 | 355 | ||
| 327 | 356 | return nil | |
| 328 | 357 | } | |
| 329 | 358 | ||
| 330 | 359 | // handleCloseTab closes the active tab. | |
| 331 | - | func (a *App) handleCloseTab() error { | |
| 360 | + | func (a *App) handleCloseTab(ctx context.Context) error { | |
| 332 | 361 | activeID := a.tabs.ActiveTabID | |
| 333 | 362 | if activeID == "" { | |
| 334 | 363 | return nil | |
| 335 | 364 | } | |
| 336 | 365 | ||
| 337 | - | wasLast, err := a.tabsUC.Close(a.ctx, a.tabs, activeID) | |
| 366 | + | wasLast, err := a.tabsUC.Close(ctx, a.tabs, activeID) | |
| 338 | 367 | if err != nil { | |
| 339 | 368 | return err | |
| 340 | 369 | } | |
| @@ -357,8 +386,8 @@ func (a *App) handleCloseTab() error { | |||
| 357 | 386 | } | |
| 358 | 387 | ||
| 359 | 388 | // handleNextTab switches to the next tab. | |
| 360 | - | func (a *App) handleNextTab() error { | |
| 361 | - | if err := a.tabsUC.SwitchNext(a.ctx, a.tabs); err != nil { | |
| 389 | + | func (a *App) handleNextTab(ctx context.Context) error { | |
| 390 | + | if err := a.tabsUC.SwitchNext(ctx, a.tabs); err != nil { | |
| 362 | 391 | return err | |
| 363 | 392 | } | |
| 364 | 393 | ||
| @@ -370,8 +399,8 @@ func (a *App) handleNextTab() error { | |||
| 370 | 399 | } | |
| 371 | 400 | ||
| 372 | 401 | // handlePreviousTab switches to the previous tab. | |
| 373 | - | func (a *App) handlePreviousTab() error { | |
| 374 | - | if err := a.tabsUC.SwitchPrevious(a.ctx, a.tabs); err != nil { | |
| 402 | + | func (a *App) handlePreviousTab(ctx context.Context) error { | |
| 403 | + | if err := a.tabsUC.SwitchPrevious(ctx, a.tabs); err != nil { | |
| 375 | 404 | return err | |
| 376 | 405 | } | |
| 377 | 406 | ||
| @@ -383,25 +412,30 @@ func (a *App) handlePreviousTab() error { | |||
| 383 | 412 | } | |
| 384 | 413 | ||
| 385 | 414 | // createWorkspaceView creates a WorkspaceView for a tab and attaches it to the content area. | |
| 386 | - | func (a *App) createWorkspaceView(tab *entity.Tab) { | |
| 415 | + | func (a *App) createWorkspaceView(ctx context.Context, tab *entity.Tab) { | |
| 416 | + | log := logging.FromContext(ctx) | |
| 417 | + | ||
| 387 | 418 | if a.widgetFactory == nil { | |
| 388 | - | a.logger.Error().Msg("widget factory not initialized") | |
| 419 | + | log.Error().Msg("widget factory not initialized") | |
| 389 | 420 | return | |
| 390 | 421 | } | |
| 391 | 422 | ||
| 392 | 423 | // Create workspace view | |
| 393 | - | wsView := component.NewWorkspaceView(a.widgetFactory) | |
| 424 | + | wsView := component.NewWorkspaceView(a.widgetFactory, *a.deps.Logger) | |
| 394 | 425 | if wsView == nil { | |
| 395 | - | a.logger.Error().Msg("failed to create workspace view") | |
| 426 | + | log.Error().Msg("failed to create workspace view") | |
| 396 | 427 | return | |
| 397 | 428 | } | |
| 398 | 429 | ||
| 399 | 430 | // Set the workspace | |
| 400 | 431 | if err := wsView.SetWorkspace(tab.Workspace); err != nil { | |
| 401 | - | a.logger.Error().Err(err).Msg("failed to set workspace in view") | |
| 432 | + | log.Error().Err(err).Msg("failed to set workspace in view") | |
| 402 | 433 | return | |
| 403 | 434 | } | |
| 404 | 435 | ||
| 436 | + | // Ensure WebViews are attached to panes | |
| 437 | + | a.attachWorkspaceWebViews(ctx, tab.Workspace, wsView) | |
| 438 | + | ||
| 405 | 439 | // Store in map | |
| 406 | 440 | a.workspaceViews[tab.ID] = wsView | |
| 407 | 441 | ||
| @@ -409,15 +443,14 @@ func (a *App) createWorkspaceView(tab *entity.Tab) { | |||
| 409 | 443 | if a.mainWindow != nil { | |
| 410 | 444 | widget := wsView.Widget() | |
| 411 | 445 | if widget != nil { | |
| 412 | - | if gtkWidget := widget.GtkWidget(); gtkWidget != nil { | |
| 446 | + | gtkWidget := widget.GtkWidget() | |
| 447 | + | if gtkWidget != nil { | |
| 413 | 448 | a.mainWindow.SetContent(gtkWidget) | |
| 414 | 449 | } | |
| 415 | 450 | } | |
| 416 | 451 | } | |
| 417 | 452 | ||
| 418 | - | a.logger.Debug(). | |
| 419 | - | Str("tab_id", string(tab.ID)). | |
| 420 | - | Msg("workspace view created and attached") | |
| 453 | + | log.Debug().Str("tab_id", string(tab.ID)).Msg("workspace view created and attached") | |
| 421 | 454 | } | |
| 422 | 455 | ||
| 423 | 456 | // activeWorkspace returns the workspace of the active tab. | |
| @@ -439,31 +472,33 @@ func (a *App) activeWorkspaceView() *component.WorkspaceView { | |||
| 439 | 472 | } | |
| 440 | 473 | ||
| 441 | 474 | // handlePaneSplit splits the active pane in the given direction. | |
| 442 | - | func (a *App) handlePaneSplit(direction usecase.SplitDirection) error { | |
| 475 | + | func (a *App) handlePaneSplit(ctx context.Context, direction usecase.SplitDirection) error { | |
| 476 | + | log := logging.FromContext(ctx) | |
| 477 | + | ||
| 443 | 478 | if a.panesUC == nil { | |
| 444 | - | a.logger.Warn().Msg("panes use case not available") | |
| 479 | + | log.Warn().Msg("panes use case not available") | |
| 445 | 480 | return nil | |
| 446 | 481 | } | |
| 447 | 482 | ||
| 448 | 483 | ws := a.activeWorkspace() | |
| 449 | 484 | if ws == nil { | |
| 450 | - | a.logger.Warn().Msg("no active workspace") | |
| 485 | + | log.Warn().Msg("no active workspace") | |
| 451 | 486 | return nil | |
| 452 | 487 | } | |
| 453 | 488 | ||
| 454 | 489 | activePane := ws.ActivePane() | |
| 455 | 490 | if activePane == nil { | |
| 456 | - | a.logger.Warn().Msg("no active pane to split") | |
| 491 | + | log.Warn().Msg("no active pane to split") | |
| 457 | 492 | return nil | |
| 458 | 493 | } | |
| 459 | 494 | ||
| 460 | - | output, err := a.panesUC.Split(a.ctx, usecase.SplitPaneInput{ | |
| 495 | + | output, err := a.panesUC.Split(ctx, usecase.SplitPaneInput{ | |
| 461 | 496 | Workspace: ws, | |
| 462 | 497 | TargetPane: activePane, | |
| 463 | 498 | Direction: direction, | |
| 464 | 499 | }) | |
| 465 | 500 | if err != nil { | |
| 466 | - | a.logger.Error().Err(err).Str("direction", string(direction)).Msg("failed to split pane") | |
| 501 | + | log.Error().Err(err).Str("direction", string(direction)).Msg("failed to split pane") | |
| 467 | 502 | return err | |
| 468 | 503 | } | |
| 469 | 504 | ||
| @@ -474,45 +509,47 @@ func (a *App) handlePaneSplit(direction usecase.SplitDirection) error { | |||
| 474 | 509 | wsView := a.activeWorkspaceView() | |
| 475 | 510 | if wsView != nil { | |
| 476 | 511 | if err := wsView.Rebuild(); err != nil { | |
| 477 | - | a.logger.Error().Err(err).Msg("failed to rebuild workspace view") | |
| 512 | + | log.Error().Err(err).Msg("failed to rebuild workspace view") | |
| 478 | 513 | } | |
| 514 | + | a.attachWorkspaceWebViews(ctx, ws, wsView) | |
| 515 | + | wsView.FocusPane(ws.ActivePaneID) | |
| 479 | 516 | } | |
| 480 | 517 | ||
| 481 | - | a.logger.Info(). | |
| 482 | - | Str("direction", string(direction)). | |
| 483 | - | Str("new_pane_id", string(output.NewPaneNode.Pane.ID)). | |
| 484 | - | Msg("pane split completed") | |
| 518 | + | log.Info().Str("direction", string(direction)).Str("new_pane_id", string(output.NewPaneNode.Pane.ID)).Msg("pane split completed") | |
| 485 | 519 | ||
| 486 | 520 | return nil | |
| 487 | 521 | } | |
| 488 | 522 | ||
| 489 | 523 | // handleClosePane closes the active pane. | |
| 490 | - | func (a *App) handleClosePane() error { | |
| 524 | + | func (a *App) handleClosePane(ctx context.Context) error { | |
| 525 | + | log := logging.FromContext(ctx) | |
| 526 | + | ||
| 491 | 527 | if a.panesUC == nil { | |
| 492 | - | a.logger.Warn().Msg("panes use case not available") | |
| 528 | + | log.Warn().Msg("panes use case not available") | |
| 493 | 529 | return nil | |
| 494 | 530 | } | |
| 495 | 531 | ||
| 496 | 532 | ws := a.activeWorkspace() | |
| 497 | 533 | if ws == nil { | |
| 498 | - | a.logger.Warn().Msg("no active workspace") | |
| 534 | + | log.Warn().Msg("no active workspace") | |
| 499 | 535 | return nil | |
| 500 | 536 | } | |
| 501 | 537 | ||
| 502 | 538 | activePane := ws.ActivePane() | |
| 503 | 539 | if activePane == nil { | |
| 504 | - | a.logger.Warn().Msg("no active pane to close") | |
| 540 | + | log.Warn().Msg("no active pane to close") | |
| 505 | 541 | return nil | |
| 506 | 542 | } | |
| 543 | + | closingPaneID := activePane.Pane.ID | |
| 507 | 544 | ||
| 508 | 545 | // Don't close the last pane - close the tab instead | |
| 509 | 546 | if ws.PaneCount() <= 1 { | |
| 510 | - | return a.handleCloseTab() | |
| 547 | + | return a.handleCloseTab(ctx) | |
| 511 | 548 | } | |
| 512 | 549 | ||
| 513 | - | _, err := a.panesUC.Close(a.ctx, ws, activePane) | |
| 550 | + | _, err := a.panesUC.Close(ctx, ws, activePane) | |
| 514 | 551 | if err != nil { | |
| 515 | - | a.logger.Error().Err(err).Msg("failed to close pane") | |
| 552 | + | log.Error().Err(err).Msg("failed to close pane") | |
| 516 | 553 | return err | |
| 517 | 554 | } | |
| 518 | 555 | ||
| @@ -520,35 +557,40 @@ func (a *App) handleClosePane() error { | |||
| 520 | 557 | wsView := a.activeWorkspaceView() | |
| 521 | 558 | if wsView != nil { | |
| 522 | 559 | if err := wsView.Rebuild(); err != nil { | |
| 523 | - | a.logger.Error().Err(err).Msg("failed to rebuild workspace view") | |
| 560 | + | log.Error().Err(err).Msg("failed to rebuild workspace view") | |
| 524 | 561 | } | |
| 562 | + | a.releasePaneWebView(ctx, closingPaneID) | |
| 563 | + | a.attachWorkspaceWebViews(ctx, ws, wsView) | |
| 564 | + | wsView.FocusPane(ws.ActivePaneID) | |
| 525 | 565 | } | |
| 526 | 566 | ||
| 527 | - | a.logger.Info().Msg("pane closed") | |
| 567 | + | log.Info().Msg("pane closed") | |
| 528 | 568 | return nil | |
| 529 | 569 | } | |
| 530 | 570 | ||
| 531 | 571 | // handlePaneFocus navigates focus to an adjacent pane. | |
| 532 | - | func (a *App) handlePaneFocus(direction usecase.NavigateDirection) error { | |
| 572 | + | func (a *App) handlePaneFocus(ctx context.Context, direction usecase.NavigateDirection) error { | |
| 573 | + | log := logging.FromContext(ctx) | |
| 574 | + | ||
| 533 | 575 | if a.panesUC == nil { | |
| 534 | - | a.logger.Warn().Msg("panes use case not available") | |
| 576 | + | log.Warn().Msg("panes use case not available") | |
| 535 | 577 | return nil | |
| 536 | 578 | } | |
| 537 | 579 | ||
| 538 | 580 | ws := a.activeWorkspace() | |
| 539 | 581 | if ws == nil { | |
| 540 | - | a.logger.Warn().Msg("no active workspace") | |
| 582 | + | log.Warn().Msg("no active workspace") | |
| 541 | 583 | return nil | |
| 542 | 584 | } | |
| 543 | 585 | ||
| 544 | - | newPane, err := a.panesUC.NavigateFocus(a.ctx, ws, direction) | |
| 586 | + | newPane, err := a.panesUC.NavigateFocus(ctx, ws, direction) | |
| 545 | 587 | if err != nil { | |
| 546 | - | a.logger.Error().Err(err).Str("direction", string(direction)).Msg("failed to navigate focus") | |
| 588 | + | log.Error().Err(err).Str("direction", string(direction)).Msg("failed to navigate focus") | |
| 547 | 589 | return err | |
| 548 | 590 | } | |
| 549 | 591 | ||
| 550 | 592 | if newPane == nil { | |
| 551 | - | a.logger.Debug().Str("direction", string(direction)).Msg("no pane in that direction") | |
| 593 | + | log.Debug().Str("direction", string(direction)).Msg("no pane in that direction") | |
| 552 | 594 | return nil | |
| 553 | 595 | } | |
| 554 | 596 | ||
| @@ -556,14 +598,184 @@ func (a *App) handlePaneFocus(direction usecase.NavigateDirection) error { | |||
| 556 | 598 | wsView := a.activeWorkspaceView() | |
| 557 | 599 | if wsView != nil { | |
| 558 | 600 | if err := wsView.SetActivePaneID(newPane.Pane.ID); err != nil { | |
| 559 | - | a.logger.Warn().Err(err).Msg("failed to update active pane in view") | |
| 601 | + | log.Warn().Err(err).Msg("failed to update active pane in view") | |
| 602 | + | } else { | |
| 603 | + | wsView.FocusPane(newPane.Pane.ID) | |
| 560 | 604 | } | |
| 561 | 605 | } | |
| 562 | 606 | ||
| 563 | - | a.logger.Debug(). | |
| 564 | - | Str("direction", string(direction)). | |
| 565 | - | Str("new_pane_id", newPane.ID). | |
| 566 | - | Msg("focus navigated") | |
| 607 | + | log.Debug().Str("direction", string(direction)).Str("new_pane_id", newPane.ID).Msg("focus navigated") | |
| 567 | 608 | ||
| 568 | 609 | return nil | |
| 569 | 610 | } | |
| 611 | + | ||
| 612 | + | // registerMessageHandlers wires frontend message types to Go handlers. | |
| 613 | + | func (a *App) registerMessageHandlers(ctx context.Context) error { | |
| 614 | + | if a.router == nil { | |
| 615 | + | return fmt.Errorf("message router is nil") | |
| 616 | + | } | |
| 617 | + | ||
| 618 | + | // Omnibox search | |
| 619 | + | if err := a.router.RegisterHandlerWithCallbacks("query", "__dumber_omnibox_suggestions", "", "", uihandler.NewQueryHandler(a.deps.HistoryUC, a.deps.Config)); err != nil { | |
| 620 | + | return err | |
| 621 | + | } | |
| 622 | + | if err := a.router.RegisterHandlerWithCallbacks("omnibox_initial_history", "__dumber_omnibox_suggestions", "", "", uihandler.NewInitialHistoryHandler(a.deps.HistoryUC, a.deps.Config)); err != nil { | |
| 623 | + | return err | |
| 624 | + | } | |
| 625 | + | if err := a.router.RegisterHandlerWithCallbacks("prefix_query", "__dumber_omnibox_inline_suggestion", "", "", uihandler.NewPrefixQueryHandler(a.deps.HistoryUC)); err != nil { | |
| 626 | + | return err | |
| 627 | + | } | |
| 628 | + | ||
| 629 | + | // Navigation | |
| 630 | + | if err := a.router.RegisterHandler("navigate", uihandler.NewNavigateHandler()); err != nil { | |
| 631 | + | return err | |
| 632 | + | } | |
| 633 | + | ||
| 634 | + | // Favorites | |
| 635 | + | if err := a.router.RegisterHandlerWithCallbacks("get_favorites", "__dumber_favorites", "", "", uihandler.NewFavoritesHandler(a.deps.FavoritesUC)); err != nil { | |
| 636 | + | return err | |
| 637 | + | } | |
| 638 | + | if err := a.router.RegisterHandlerWithCallbacks("toggle_favorite", "__dumber_favorites", "", "", uihandler.NewToggleFavoriteHandler(a.deps.FavoritesUC)); err != nil { | |
| 639 | + | return err | |
| 640 | + | } | |
| 641 | + | ||
| 642 | + | // Shortcuts + palettes | |
| 643 | + | if err := a.router.RegisterHandlerWithCallbacks("get_search_shortcuts", "__dumber_search_shortcuts", "__dumber_search_shortcuts_error", "", uihandler.NewShortcutsHandler(a.deps.Config)); err != nil { | |
| 644 | + | return err | |
| 645 | + | } | |
| 646 | + | if err := a.router.RegisterHandlerWithCallbacks("get_color_palettes", "__dumber_color_palettes", "__dumber_color_palettes_error", "", uihandler.NewPaletteHandler(a.deps.Config)); err != nil { | |
| 647 | + | return err | |
| 648 | + | } | |
| 649 | + | ||
| 650 | + | // Keyboard blocking | |
| 651 | + | if err := a.router.RegisterHandler("keyboard_blocking", uihandler.NewKeyboardBlockingHandler()); err != nil { | |
| 652 | + | return err | |
| 653 | + | } | |
| 654 | + | ||
| 655 | + | return nil | |
| 656 | + | } | |
| 657 | + | ||
| 658 | + | // attachWorkspaceWebViews ensures each pane has a WebView widget attached. | |
| 659 | + | func (a *App) attachWorkspaceWebViews(ctx context.Context, ws *entity.Workspace, wsView *component.WorkspaceView) { | |
| 660 | + | log := logging.FromContext(ctx) | |
| 661 | + | ||
| 662 | + | if ws == nil || wsView == nil || a.widgetFactory == nil { | |
| 663 | + | return | |
| 664 | + | } | |
| 665 | + | ||
| 666 | + | for _, pane := range ws.AllPanes() { | |
| 667 | + | if pane == nil { | |
| 668 | + | continue | |
| 669 | + | } | |
| 670 | + | ||
| 671 | + | wv, err := a.ensureWebViewForPane(ctx, pane.ID) | |
| 672 | + | if err != nil { | |
| 673 | + | log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("failed to ensure webview for pane") | |
| 674 | + | continue | |
| 675 | + | } | |
| 676 | + | ||
| 677 | + | // Load the pane's URI if set and different from current | |
| 678 | + | if pane.URI != "" && pane.URI != wv.URI() { | |
| 679 | + | if err := wv.LoadURI(ctx, pane.URI); err != nil { | |
| 680 | + | log.Warn().Err(err).Str("pane_id", string(pane.ID)).Str("uri", pane.URI).Msg("failed to load pane URI") | |
| 681 | + | } | |
| 682 | + | } | |
| 683 | + | ||
| 684 | + | widget := a.wrapWebViewWidget(ctx, wv) | |
| 685 | + | if widget == nil { | |
| 686 | + | continue | |
| 687 | + | } | |
| 688 | + | ||
| 689 | + | if err := wsView.SetWebViewWidget(pane.ID, widget); err != nil { | |
| 690 | + | log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("failed to attach webview widget") | |
| 691 | + | } | |
| 692 | + | } | |
| 693 | + | } | |
| 694 | + | ||
| 695 | + | // ensureWebViewForPane acquires or reuses a WebView for the given pane. | |
| 696 | + | func (a *App) ensureWebViewForPane(ctx context.Context, paneID entity.PaneID) (*webkit.WebView, error) { | |
| 697 | + | if wv, ok := a.webViews[paneID]; ok && wv != nil && !wv.IsDestroyed() { | |
| 698 | + | return wv, nil | |
| 699 | + | } | |
| 700 | + | if a.pool == nil { | |
| 701 | + | return nil, fmt.Errorf("webview pool not configured") | |
| 702 | + | } | |
| 703 | + | ||
| 704 | + | wv, err := a.pool.Acquire(ctx) | |
| 705 | + | if err != nil { | |
| 706 | + | return nil, err | |
| 707 | + | } | |
| 708 | + | ||
| 709 | + | a.webViews[paneID] = wv | |
| 710 | + | return wv, nil | |
| 711 | + | } | |
| 712 | + | ||
| 713 | + | // releasePaneWebView returns the WebView for a pane to the pool. | |
| 714 | + | func (a *App) releasePaneWebView(ctx context.Context, paneID entity.PaneID) { | |
| 715 | + | wv, ok := a.webViews[paneID] | |
| 716 | + | if !ok || wv == nil { | |
| 717 | + | return | |
| 718 | + | } | |
| 719 | + | delete(a.webViews, paneID) | |
| 720 | + | ||
| 721 | + | if a.pool != nil { | |
| 722 | + | a.pool.Release(wv) | |
| 723 | + | } else { | |
| 724 | + | wv.Destroy() | |
| 725 | + | } | |
| 726 | + | } | |
| 727 | + | ||
| 728 | + | // wrapWebViewWidget converts a WebView to a layout.Widget for embedding. | |
| 729 | + | func (a *App) wrapWebViewWidget(ctx context.Context, wv *webkit.WebView) layout.Widget { | |
| 730 | + | if wv == nil || a.widgetFactory == nil { | |
| 731 | + | return nil | |
| 732 | + | } | |
| 733 | + | gtkView := wv.Widget() | |
| 734 | + | if gtkView == nil { | |
| 735 | + | return nil | |
| 736 | + | } | |
| 737 | + | return a.widgetFactory.WrapWidget(>kView.Widget) | |
| 738 | + | } | |
| 739 | + | ||
| 740 | + | // activeWebView returns the WebView for the active pane. | |
| 741 | + | func (a *App) activeWebView(ctx context.Context) *webkit.WebView { | |
| 742 | + | ws := a.activeWorkspace() | |
| 743 | + | if ws == nil { | |
| 744 | + | return nil | |
| 745 | + | } | |
| 746 | + | pane := ws.ActivePane() | |
| 747 | + | if pane == nil || pane.Pane == nil { | |
| 748 | + | return nil | |
| 749 | + | } | |
| 750 | + | return a.webViews[pane.Pane.ID] | |
| 751 | + | } | |
| 752 | + | ||
| 753 | + | // openOmnibox toggles the omnibox overlay for the active WebView. | |
| 754 | + | func (a *App) openOmnibox(ctx context.Context) error { | |
| 755 | + | log := logging.FromContext(ctx) | |
| 756 | + | ||
| 757 | + | if a.overlay == nil { | |
| 758 | + | log.Error().Msg("overlay controller not configured") | |
| 759 | + | return fmt.Errorf("overlay controller not configured") | |
| 760 | + | } | |
| 761 | + | wv := a.activeWebView(ctx) | |
| 762 | + | if wv == nil { | |
| 763 | + | log.Error().Msg("no active webview for omnibox") | |
| 764 | + | return fmt.Errorf("no active webview for omnibox") | |
| 765 | + | } | |
| 766 | + | log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("opening omnibox") | |
| 767 | + | return a.overlay.Show(ctx, wv.ID(), "omnibox", "") | |
| 768 | + | } | |
| 769 | + | ||
| 770 | + | // openDevTools opens the WebKit inspector for the active WebView. | |
| 771 | + | func (a *App) openDevTools(ctx context.Context) error { | |
| 772 | + | log := logging.FromContext(ctx) | |
| 773 | + | ||
| 774 | + | wv := a.activeWebView(ctx) | |
| 775 | + | if wv == nil { | |
| 776 | + | log.Warn().Msg("no active webview for devtools") | |
| 777 | + | return fmt.Errorf("no active webview for devtools") | |
| 778 | + | } | |
| 779 | + | log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("opening devtools") | |
| 780 | + | return wv.ShowDevTools() | |
| 781 | + | } | |
| @@ -2,19 +2,23 @@ package ui | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | + | "errors" | |||
| 6 | + | "fmt" | |||
| 5 | "os" | 7 | "os" | ||
| 6 | "sync" | 8 | "sync" | ||
| 7 | 9 | ||||
| 10 | + | "github.com/jwijenbergh/puregotk/v4/gio" | |||
| 11 | + | "github.com/jwijenbergh/puregotk/v4/gtk" | |||
| 12 | + | ||||
| 8 | "github.com/bnema/dumber/internal/application/usecase" | 13 | "github.com/bnema/dumber/internal/application/usecase" | ||
| 9 | "github.com/bnema/dumber/internal/domain/entity" | 14 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 15 | + | "github.com/bnema/dumber/internal/infrastructure/webkit" | |||
| 10 | "github.com/bnema/dumber/internal/logging" | 16 | "github.com/bnema/dumber/internal/logging" | ||
| 11 | "github.com/bnema/dumber/internal/ui/component" | 17 | "github.com/bnema/dumber/internal/ui/component" | ||
| 18 | + | uihandler "github.com/bnema/dumber/internal/ui/handler" | |||
| 12 | "github.com/bnema/dumber/internal/ui/input" | 19 | "github.com/bnema/dumber/internal/ui/input" | ||
| 13 | "github.com/bnema/dumber/internal/ui/layout" | 20 | "github.com/bnema/dumber/internal/ui/layout" | ||
| 14 | "github.com/bnema/dumber/internal/ui/window" | 21 | "github.com/bnema/dumber/internal/ui/window" | ||
| 15 | - | "github.com/jwijenbergh/puregotk/v4/gio" | |||
| 16 | - | "github.com/jwijenbergh/puregotk/v4/gtk" | |||
| 17 | - | "github.com/rs/zerolog" | |||
| 18 | ) | 22 | ) | ||
| 19 | 23 | ||||
| 20 | const ( | 24 | const ( | ||
| @@ -40,14 +44,20 @@ type App struct { | |||||
| 40 | // Input handling | 44 | // Input handling | ||
| 41 | keyboardHandler *input.KeyboardHandler | 45 | keyboardHandler *input.KeyboardHandler | ||
| 42 | 46 | ||||
| 47 | + | // Web content | |||
| 48 | + | pool *webkit.WebViewPool | |||
| 49 | + | injector *webkit.ContentInjector | |||
| 50 | + | router *webkit.MessageRouter | |||
| 51 | + | overlay *component.OverlayController | |||
| 52 | + | settings *webkit.SettingsManager | |||
| 53 | + | webViews map[entity.PaneID]*webkit.WebView | |||
| 54 | + | ||||
| 43 | // ID generator for tabs/panes | 55 | // ID generator for tabs/panes | ||
| 44 | idCounter uint64 | 56 | idCounter uint64 | ||
| 45 | idMu sync.Mutex | 57 | idMu sync.Mutex | ||
| 46 | 58 | ||||
| 47 | - | // Lifecycle | 59 | + | // lifecycle |
| 48 | - | ctx context.Context | 60 | + | cancel context.CancelCauseFunc |
| 49 | - | cancel context.CancelFunc | |||
| 50 | - | logger *zerolog.Logger | |||
| 51 | } | 61 | } | ||
| 52 | 62 | ||||
| 53 | // New creates a new App with the given dependencies. | 63 | // New creates a new App with the given dependencies. | ||
| @@ -56,8 +66,7 @@ func New(deps *Dependencies) (*App, error) { | |||||
| 56 | return nil, err | 66 | return nil, err | ||
| 57 | } | 67 | } | ||
| 58 | 68 | ||||
| 59 | - | ctx, cancel := context.WithCancel(deps.Ctx) | 69 | + | ctx, cancel := context.WithCancelCause(deps.Ctx) |
| 60 | - | logger := logging.FromContext(ctx) | |||
| 61 | 70 | ||||
| 62 | app := &App{ | 71 | app := &App{ | ||
| 63 | deps: deps, | 72 | deps: deps, | ||
| @@ -65,9 +74,19 @@ func New(deps *Dependencies) (*App, error) { | |||||
| 65 | tabsUC: deps.TabsUC, | 74 | tabsUC: deps.TabsUC, | ||
| 66 | panesUC: deps.PanesUC, | 75 | panesUC: deps.PanesUC, | ||
| 67 | workspaceViews: make(map[entity.TabID]*component.WorkspaceView), | 76 | workspaceViews: make(map[entity.TabID]*component.WorkspaceView), | ||
| 68 | - | ctx: ctx, | 77 | + | pool: deps.Pool, |
| 78 | + | injector: deps.Injector, | |||
| 79 | + | router: deps.MessageRouter, | |||
| 80 | + | overlay: deps.Overlay, | |||
| 81 | + | settings: deps.Settings, | |||
| 82 | + | webViews: make(map[entity.PaneID]*webkit.WebView), | |||
| 69 | cancel: cancel, | 83 | cancel: cancel, | ||
| 70 | - | logger: logger, | 84 | + | } |
| 85 | + | if app.overlay == nil { | |||
| 86 | + | app.overlay = component.NewOverlayController(ctx) | |||
| 87 | + | } | |||
| 88 | + | if app.router == nil { | |||
| 89 | + | app.router = webkit.NewMessageRouter(ctx) | |||
| 71 | } | 90 | } | ||
| 72 | 91 | ||||
| 73 | return app, nil | 92 | return app, nil | ||
| @@ -75,43 +94,50 @@ func New(deps *Dependencies) (*App, error) { | |||||
| 75 | 94 | ||||
| 76 | // Run starts the GTK application and blocks until it exits. | 95 | // Run starts the GTK application and blocks until it exits. | ||
| 77 | // Returns the exit code. | 96 | // Returns the exit code. | ||
| 78 | - | func (a *App) Run(args []string) int { | 97 | + | func (a *App) Run(ctx context.Context, args []string) int { |
| 79 | - | a.logger.Debug().Msg("creating GTK application") | 98 | + | log := logging.FromContext(ctx) |
| 99 | + | log.Debug().Msg("creating GTK application") | |||
| 80 | 100 | ||||
| 81 | a.gtkApp = gtk.NewApplication(AppID, gio.GApplicationFlagsNoneValue) | 101 | a.gtkApp = gtk.NewApplication(AppID, gio.GApplicationFlagsNoneValue) | ||
| 82 | if a.gtkApp == nil { | 102 | if a.gtkApp == nil { | ||
| 83 | - | a.logger.Error().Msg("failed to create GTK application") | 103 | + | log.Error().Msg("failed to create GTK application") |
| 84 | return 1 | 104 | return 1 | ||
| 85 | } | 105 | } | ||
| 86 | defer a.gtkApp.Unref() | 106 | defer a.gtkApp.Unref() | ||
| 87 | 107 | ||||
| 88 | // Connect activate signal | 108 | // Connect activate signal | ||
| 89 | activateCb := func(_ gio.Application) { | 109 | activateCb := func(_ gio.Application) { | ||
| 90 | - | a.onActivate() | 110 | + | a.onActivate(ctx) |
| 91 | } | 111 | } | ||
| 92 | a.gtkApp.ConnectActivate(&activateCb) | 112 | a.gtkApp.ConnectActivate(&activateCb) | ||
| 93 | 113 | ||||
| 94 | // Connect shutdown signal | 114 | // Connect shutdown signal | ||
| 95 | shutdownCb := func(_ gio.Application) { | 115 | shutdownCb := func(_ gio.Application) { | ||
| 96 | - | a.onShutdown() | 116 | + | a.onShutdown(ctx) |
| 97 | } | 117 | } | ||
| 98 | a.gtkApp.ConnectShutdown(&shutdownCb) | 118 | a.gtkApp.ConnectShutdown(&shutdownCb) | ||
| 99 | 119 | ||||
| 100 | - | a.logger.Info().Msg("starting GTK main loop") | 120 | + | log.Info().Msg("starting GTK main loop") |
| 101 | code := a.gtkApp.Run(len(args), args) | 121 | code := a.gtkApp.Run(len(args), args) | ||
| 102 | 122 | ||||
| 103 | return code | 123 | return code | ||
| 104 | } | 124 | } | ||
| 105 | 125 | ||||
| 106 | // onActivate is called when the GTK application is activated. | 126 | // onActivate is called when the GTK application is activated. | ||
| 107 | - | func (a *App) onActivate() { | 127 | + | func (a *App) onActivate(ctx context.Context) { |
| 108 | - | a.logger.Debug().Msg("GTK application activated") | 128 | + | log := logging.FromContext(ctx) |
| 129 | + | log.Debug().Msg("GTK application activated") | |||
| 130 | + | ||||
| 131 | + | // Prewarm WebView pool now that GTK is initialized | |||
| 132 | + | if a.pool != nil { | |||
| 133 | + | a.pool.Prewarm(0) | |||
| 134 | + | } | |||
| 109 | 135 | ||||
| 110 | // Create the main window | 136 | // Create the main window | ||
| 111 | var err error | 137 | var err error | ||
| 112 | - | a.mainWindow, err = window.New(a.gtkApp, a.deps.Config, a.logger) | 138 | + | a.mainWindow, err = window.New(a.gtkApp, a.deps.Config, a.deps.Logger) |
| 113 | if err != nil { | 139 | if err != nil { | ||
| 114 | - | a.logger.Error().Err(err).Msg("failed to create main window") | 140 | + | log.Error().Err(err).Msg("failed to create main window") |
| 115 | return | 141 | return | ||
| 116 | } | 142 | } | ||
| 117 | 143 | ||||
| @@ -120,52 +146,62 @@ func (a *App) onActivate() { | |||||
| 120 | 146 | ||||
| 121 | // Create keyboard handler | 147 | // Create keyboard handler | ||
| 122 | a.keyboardHandler = input.NewKeyboardHandler( | 148 | a.keyboardHandler = input.NewKeyboardHandler( | ||
| 123 | - | a.ctx, | 149 | + | ctx, |
| 124 | &a.deps.Config.Workspace, | 150 | &a.deps.Config.Workspace, | ||
| 125 | - | a.logger, | 151 | + | a.deps.Logger, |
| 126 | ) | 152 | ) | ||
| 127 | a.keyboardHandler.SetOnAction(a.handleKeyboardAction) | 153 | a.keyboardHandler.SetOnAction(a.handleKeyboardAction) | ||
| 128 | - | a.keyboardHandler.SetOnModeChange(a.handleModeChange) | 154 | + | // Wrap handleModeChange to match the callback signature |
| 155 | + | a.keyboardHandler.SetOnModeChange(func(from, to input.Mode) { | |||
| 156 | + | a.handleModeChange(ctx, from, to) | |||
| 157 | + | }) | |||
| 129 | a.keyboardHandler.AttachTo(a.mainWindow.Window()) | 158 | a.keyboardHandler.AttachTo(a.mainWindow.Window()) | ||
| 130 | 159 | ||||
| 160 | + | if err := a.registerMessageHandlers(ctx); err != nil { | |||
| 161 | + | log.Warn().Err(err).Msg("failed to register message handlers") | |||
| 162 | + | } | |||
| 163 | + | ||||
| 131 | // Create an initial tab | 164 | // Create an initial tab | ||
| 132 | - | a.createInitialTab() | 165 | + | a.createInitialTab(ctx) |
| 133 | 166 | ||||
| 134 | // Show the window | 167 | // Show the window | ||
| 135 | a.mainWindow.Show() | 168 | a.mainWindow.Show() | ||
| 136 | 169 | ||||
| 137 | - | a.logger.Info().Msg("main window displayed") | 170 | + | log.Info().Msg("main window displayed") |
| 138 | } | 171 | } | ||
| 139 | 172 | ||||
| 140 | // onShutdown is called when the GTK application is shutting down. | 173 | // onShutdown is called when the GTK application is shutting down. | ||
| 141 | - | func (a *App) onShutdown() { | 174 | + | func (a *App) onShutdown(ctx context.Context) { |
| 142 | - | a.logger.Debug().Msg("GTK application shutting down") | 175 | + | log := logging.FromContext(ctx) |
| 176 | + | log.Debug().Msg("GTK application shutting down") | |||
| 143 | 177 | ||||
| 144 | // Cancel context to signal all goroutines | 178 | // Cancel context to signal all goroutines | ||
| 145 | - | a.cancel() | 179 | + | a.cancel(errors.New("application shutdown")) |
| 146 | 180 | ||||
| 147 | // Cleanup resources | 181 | // Cleanup resources | ||
| 148 | if a.deps.Pool != nil { | 182 | if a.deps.Pool != nil { | ||
| 149 | a.deps.Pool.Close() | 183 | a.deps.Pool.Close() | ||
| 150 | } | 184 | } | ||
| 151 | 185 | ||||
| 152 | - | a.logger.Info().Msg("application shutdown complete") | 186 | + | log.Info().Msg("application shutdown complete") |
| 153 | } | 187 | } | ||
| 154 | 188 | ||||
| 155 | // createInitialTab creates the first tab when the application starts. | 189 | // createInitialTab creates the first tab when the application starts. | ||
| 156 | - | func (a *App) createInitialTab() { | 190 | + | func (a *App) createInitialTab(ctx context.Context) { |
| 191 | + | log := logging.FromContext(ctx) | |||
| 192 | + | ||||
| 157 | if a.tabsUC == nil { | 193 | if a.tabsUC == nil { | ||
| 158 | // Create use case if not injected | 194 | // Create use case if not injected | ||
| 159 | a.tabsUC = usecase.NewManageTabsUseCase(a.generateID) | 195 | a.tabsUC = usecase.NewManageTabsUseCase(a.generateID) | ||
| 160 | } | 196 | } | ||
| 161 | 197 | ||||
| 162 | - | output, err := a.tabsUC.Create(a.ctx, usecase.CreateTabInput{ | 198 | + | output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{ |
| 163 | TabList: a.tabs, | 199 | TabList: a.tabs, | ||
| 164 | Name: "", | 200 | Name: "", | ||
| 165 | - | InitialURL: "about:blank", | 201 | + | InitialURL: "https://duckduckgo.com", |
| 166 | }) | 202 | }) | ||
| 167 | if err != nil { | 203 | if err != nil { | ||
| 168 | - | a.logger.Error().Err(err).Msg("failed to create initial tab") | 204 | + | log.Error().Err(err).Msg("failed to create initial tab") |
| 169 | return | 205 | return | ||
| 170 | } | 206 | } | ||
| 171 | 207 | ||||
| @@ -176,11 +212,9 @@ func (a *App) createInitialTab() { | |||||
| 176 | } | 212 | } | ||
| 177 | 213 | ||||
| 178 | // Create workspace view for this tab | 214 | // Create workspace view for this tab | ||
| 179 | - | a.createWorkspaceView(output.Tab) | 215 | + | a.createWorkspaceView(ctx, output.Tab) |
| 180 | 216 | ||||
| 181 | - | a.logger.Debug(). | 217 | + | log.Debug().Str("tab_id", string(output.Tab.ID)).Msg("initial tab created") |
| 182 | - | Str("tab_id", string(output.Tab.ID)). | |||
| 183 | - | Msg("initial tab created") | |||
| 184 | } | 218 | } | ||
| 185 | 219 | ||||
| 186 | // generateID generates a unique ID for tabs and panes. | 220 | // generateID generates a unique ID for tabs and panes. | ||
| @@ -209,7 +243,7 @@ func (a *App) Quit() { | |||||
| 209 | } | 243 | } | ||
| 210 | 244 | ||||
| 211 | // RunWithArgs is a convenience function that creates and runs an App. | 245 | // RunWithArgs is a convenience function that creates and runs an App. | ||
| 212 | - | func RunWithArgs(deps *Dependencies) int { | 246 | + | func RunWithArgs(ctx context.Context, deps *Dependencies) int { |
| 213 | app, err := New(deps) | 247 | app, err := New(deps) | ||
| 214 | if err != nil { | 248 | if err != nil { | ||
| 215 | if deps.Logger != nil { | 249 | if deps.Logger != nil { | ||
| @@ -217,96 +251,93 @@ func RunWithArgs(deps *Dependencies) int { | |||||
| 217 | } | 251 | } | ||
| 218 | return 1 | 252 | return 1 | ||
| 219 | } | 253 | } | ||
| 220 | - | return app.Run(os.Args) | 254 | + | return app.Run(ctx, os.Args) |
| 221 | } | 255 | } | ||
| 222 | 256 | ||||
| 223 | // handleKeyboardAction dispatches keyboard actions to appropriate handlers. | 257 | // handleKeyboardAction dispatches keyboard actions to appropriate handlers. | ||
| 224 | func (a *App) handleKeyboardAction(ctx context.Context, action input.Action) error { | 258 | func (a *App) handleKeyboardAction(ctx context.Context, action input.Action) error { | ||
| 225 | - | a.logger.Debug(). | 259 | + | log := logging.FromContext(ctx) |
| 226 | - | Str("action", string(action)). | 260 | + | log.Debug().Str("action", string(action)).Msg("handling keyboard action") |
| 227 | - | Msg("handling keyboard action") | |||
| 228 | 261 | ||||
| 229 | switch action { | 262 | switch action { | ||
| 230 | // Tab actions | 263 | // Tab actions | ||
| 231 | case input.ActionNewTab: | 264 | case input.ActionNewTab: | ||
| 232 | - | return a.handleNewTab() | 265 | + | return a.handleNewTab(ctx) |
| 233 | case input.ActionCloseTab: | 266 | case input.ActionCloseTab: | ||
| 234 | - | return a.handleCloseTab() | 267 | + | return a.handleCloseTab(ctx) |
| 235 | case input.ActionNextTab: | 268 | case input.ActionNextTab: | ||
| 236 | - | return a.handleNextTab() | 269 | + | return a.handleNextTab(ctx) |
| 237 | case input.ActionPreviousTab: | 270 | case input.ActionPreviousTab: | ||
| 238 | - | return a.handlePreviousTab() | 271 | + | return a.handlePreviousTab(ctx) |
| 239 | 272 | ||||
| 240 | // Pane actions | 273 | // Pane actions | ||
| 241 | case input.ActionSplitRight: | 274 | case input.ActionSplitRight: | ||
| 242 | - | return a.handlePaneSplit(usecase.SplitRight) | 275 | + | return a.handlePaneSplit(ctx, usecase.SplitRight) |
| 243 | case input.ActionSplitLeft: | 276 | case input.ActionSplitLeft: | ||
| 244 | - | return a.handlePaneSplit(usecase.SplitLeft) | 277 | + | return a.handlePaneSplit(ctx, usecase.SplitLeft) |
| 245 | case input.ActionSplitUp: | 278 | case input.ActionSplitUp: | ||
| 246 | - | return a.handlePaneSplit(usecase.SplitUp) | 279 | + | return a.handlePaneSplit(ctx, usecase.SplitUp) |
| 247 | case input.ActionSplitDown: | 280 | case input.ActionSplitDown: | ||
| 248 | - | return a.handlePaneSplit(usecase.SplitDown) | 281 | + | return a.handlePaneSplit(ctx, usecase.SplitDown) |
| 249 | case input.ActionClosePane: | 282 | case input.ActionClosePane: | ||
| 250 | - | return a.handleClosePane() | 283 | + | return a.handleClosePane(ctx) |
| 251 | case input.ActionFocusRight: | 284 | case input.ActionFocusRight: | ||
| 252 | - | return a.handlePaneFocus(usecase.NavRight) | 285 | + | return a.handlePaneFocus(ctx, usecase.NavRight) |
| 253 | case input.ActionFocusLeft: | 286 | case input.ActionFocusLeft: | ||
| 254 | - | return a.handlePaneFocus(usecase.NavLeft) | 287 | + | return a.handlePaneFocus(ctx, usecase.NavLeft) |
| 255 | case input.ActionFocusUp: | 288 | case input.ActionFocusUp: | ||
| 256 | - | return a.handlePaneFocus(usecase.NavUp) | 289 | + | return a.handlePaneFocus(ctx, usecase.NavUp) |
| 257 | case input.ActionFocusDown: | 290 | case input.ActionFocusDown: | ||
| 258 | - | return a.handlePaneFocus(usecase.NavDown) | 291 | + | return a.handlePaneFocus(ctx, usecase.NavDown) |
| 259 | 292 | ||||
| 260 | // Navigation (stub implementations for now) | 293 | // Navigation (stub implementations for now) | ||
| 261 | case input.ActionGoBack: | 294 | case input.ActionGoBack: | ||
| 262 | - | a.logger.Debug().Msg("go back action (not yet implemented)") | 295 | + | log.Debug().Msg("go back action (not yet implemented)") |
| 263 | case input.ActionGoForward: | 296 | case input.ActionGoForward: | ||
| 264 | - | a.logger.Debug().Msg("go forward action (not yet implemented)") | 297 | + | log.Debug().Msg("go forward action (not yet implemented)") |
| 265 | case input.ActionReload: | 298 | case input.ActionReload: | ||
| 266 | - | a.logger.Debug().Msg("reload action (not yet implemented)") | 299 | + | log.Debug().Msg("reload action (not yet implemented)") |
| 267 | case input.ActionHardReload: | 300 | case input.ActionHardReload: | ||
| 268 | - | a.logger.Debug().Msg("hard reload action (not yet implemented)") | 301 | + | log.Debug().Msg("hard reload action (not yet implemented)") |
| 269 | 302 | ||||
| 270 | // Zoom (stub implementations for now) | 303 | // Zoom (stub implementations for now) | ||
| 271 | case input.ActionZoomIn: | 304 | case input.ActionZoomIn: | ||
| 272 | - | a.logger.Debug().Msg("zoom in action (not yet implemented)") | 305 | + | log.Debug().Msg("zoom in action (not yet implemented)") |
| 273 | case input.ActionZoomOut: | 306 | case input.ActionZoomOut: | ||
| 274 | - | a.logger.Debug().Msg("zoom out action (not yet implemented)") | 307 | + | log.Debug().Msg("zoom out action (not yet implemented)") |
| 275 | case input.ActionZoomReset: | 308 | case input.ActionZoomReset: | ||
| 276 | - | a.logger.Debug().Msg("zoom reset action (not yet implemented)") | 309 | + | log.Debug().Msg("zoom reset action (not yet implemented)") |
| 277 | 310 | ||||
| 278 | // UI | 311 | // UI | ||
| 279 | case input.ActionOpenOmnibox: | 312 | case input.ActionOpenOmnibox: | ||
| 280 | - | a.logger.Debug().Msg("open omnibox action (not yet implemented)") | 313 | + | return a.openOmnibox(ctx) |
| 281 | case input.ActionOpenDevTools: | 314 | case input.ActionOpenDevTools: | ||
| 282 | - | a.logger.Debug().Msg("open devtools action (not yet implemented)") | 315 | + | return a.openDevTools(ctx) |
| 283 | case input.ActionToggleFullscreen: | 316 | case input.ActionToggleFullscreen: | ||
| 284 | - | a.logger.Debug().Msg("toggle fullscreen action (not yet implemented)") | 317 | + | log.Debug().Msg("toggle fullscreen action (not yet implemented)") |
| 285 | 318 | ||||
| 286 | // Application | 319 | // Application | ||
| 287 | case input.ActionQuit: | 320 | case input.ActionQuit: | ||
| 288 | a.Quit() | 321 | a.Quit() | ||
| 289 | 322 | ||||
| 290 | default: | 323 | default: | ||
| 291 | - | a.logger.Warn().Str("action", string(action)).Msg("unhandled keyboard action") | 324 | + | log.Warn().Str("action", string(action)).Msg("unhandled keyboard action") |
| 292 | } | 325 | } | ||
| 293 | 326 | ||||
| 294 | return nil | 327 | return nil | ||
| 295 | } | 328 | } | ||
| 296 | 329 | ||||
| 297 | // handleModeChange is called when the input mode changes. | 330 | // handleModeChange is called when the input mode changes. | ||
| 298 | - | func (a *App) handleModeChange(from, to input.Mode) { | 331 | + | func (a *App) handleModeChange(ctx context.Context, from, to input.Mode) { |
| 299 | - | a.logger.Debug(). | 332 | + | log := logging.FromContext(ctx) |
| 300 | - | Str("from", from.String()). | 333 | + | log.Debug().Str("from", from.String()).Str("to", to.String()).Msg("input mode changed") |
| 301 | - | Str("to", to.String()). | |||
| 302 | - | Msg("input mode changed") | |||
| 303 | - | ||||
| 304 | - | // TODO: Update UI to indicate mode (e.g., change border color) | |||
| 305 | } | 334 | } | ||
| 306 | 335 | ||||
| 307 | // handleNewTab creates a new tab. | 336 | // handleNewTab creates a new tab. | ||
| 308 | - | func (a *App) handleNewTab() error { | 337 | + | func (a *App) handleNewTab(ctx context.Context) error { |
| 309 | - | output, err := a.tabsUC.Create(a.ctx, usecase.CreateTabInput{ | 338 | + | log := logging.FromContext(ctx) |
| 339 | + | ||||
| 340 | + | output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{ | |||
| 310 | TabList: a.tabs, | 341 | TabList: a.tabs, | ||
| 311 | Name: "", | 342 | Name: "", | ||
| 312 | InitialURL: "about:blank", | 343 | InitialURL: "about:blank", | ||
| @@ -320,21 +351,19 @@ func (a *App) handleNewTab() error { | |||||
| 320 | a.mainWindow.TabBar().SetActive(output.Tab.ID) | 351 | a.mainWindow.TabBar().SetActive(output.Tab.ID) | ||
| 321 | } | 352 | } | ||
| 322 | 353 | ||||
| 323 | - | a.logger.Debug(). | 354 | + | log.Debug().Str("tab_id", string(output.Tab.ID)).Msg("new tab created") |
| 324 | - | Str("tab_id", string(output.Tab.ID)). | |||
| 325 | - | Msg("new tab created") | |||
| 326 | 355 | ||||
| 327 | return nil | 356 | return nil | ||
| 328 | } | 357 | } | ||
| 329 | 358 | ||||
| 330 | // handleCloseTab closes the active tab. | 359 | // handleCloseTab closes the active tab. | ||
| 331 | - | func (a *App) handleCloseTab() error { | 360 | + | func (a *App) handleCloseTab(ctx context.Context) error { |
| 332 | activeID := a.tabs.ActiveTabID | 361 | activeID := a.tabs.ActiveTabID | ||
| 333 | if activeID == "" { | 362 | if activeID == "" { | ||
| 334 | return nil | 363 | return nil | ||
| 335 | } | 364 | } | ||
| 336 | 365 | ||||
| 337 | - | wasLast, err := a.tabsUC.Close(a.ctx, a.tabs, activeID) | 366 | + | wasLast, err := a.tabsUC.Close(ctx, a.tabs, activeID) |
| 338 | if err != nil { | 367 | if err != nil { | ||
| 339 | return err | 368 | return err | ||
| 340 | } | 369 | } | ||
| @@ -357,8 +386,8 @@ func (a *App) handleCloseTab() error { | |||||
| 357 | } | 386 | } | ||
| 358 | 387 | ||||
| 359 | // handleNextTab switches to the next tab. | 388 | // handleNextTab switches to the next tab. | ||
| 360 | - | func (a *App) handleNextTab() error { | 389 | + | func (a *App) handleNextTab(ctx context.Context) error { |
| 361 | - | if err := a.tabsUC.SwitchNext(a.ctx, a.tabs); err != nil { | 390 | + | if err := a.tabsUC.SwitchNext(ctx, a.tabs); err != nil { |
| 362 | return err | 391 | return err | ||
| 363 | } | 392 | } | ||
| 364 | 393 | ||||
| @@ -370,8 +399,8 @@ func (a *App) handleNextTab() error { | |||||
| 370 | } | 399 | } | ||
| 371 | 400 | ||||
| 372 | // handlePreviousTab switches to the previous tab. | 401 | // handlePreviousTab switches to the previous tab. | ||
| 373 | - | func (a *App) handlePreviousTab() error { | 402 | + | func (a *App) handlePreviousTab(ctx context.Context) error { |
| 374 | - | if err := a.tabsUC.SwitchPrevious(a.ctx, a.tabs); err != nil { | 403 | + | if err := a.tabsUC.SwitchPrevious(ctx, a.tabs); err != nil { |
| 375 | return err | 404 | return err | ||
| 376 | } | 405 | } | ||
| 377 | 406 | ||||
| @@ -383,25 +412,30 @@ func (a *App) handlePreviousTab() error { | |||||
| 383 | } | 412 | } | ||
| 384 | 413 | ||||
| 385 | // createWorkspaceView creates a WorkspaceView for a tab and attaches it to the content area. | 414 | // createWorkspaceView creates a WorkspaceView for a tab and attaches it to the content area. | ||
| 386 | - | func (a *App) createWorkspaceView(tab *entity.Tab) { | 415 | + | func (a *App) createWorkspaceView(ctx context.Context, tab *entity.Tab) { |
| 416 | + | log := logging.FromContext(ctx) | |||
| 417 | + | ||||
| 387 | if a.widgetFactory == nil { | 418 | if a.widgetFactory == nil { | ||
| 388 | - | a.logger.Error().Msg("widget factory not initialized") | 419 | + | log.Error().Msg("widget factory not initialized") |
| 389 | return | 420 | return | ||
| 390 | } | 421 | } | ||
| 391 | 422 | ||||
| 392 | // Create workspace view | 423 | // Create workspace view | ||
| 393 | - | wsView := component.NewWorkspaceView(a.widgetFactory) | 424 | + | wsView := component.NewWorkspaceView(a.widgetFactory, *a.deps.Logger) |
| 394 | if wsView == nil { | 425 | if wsView == nil { | ||
| 395 | - | a.logger.Error().Msg("failed to create workspace view") | 426 | + | log.Error().Msg("failed to create workspace view") |
| 396 | return | 427 | return | ||
| 397 | } | 428 | } | ||
| 398 | 429 | ||||
| 399 | // Set the workspace | 430 | // Set the workspace | ||
| 400 | if err := wsView.SetWorkspace(tab.Workspace); err != nil { | 431 | if err := wsView.SetWorkspace(tab.Workspace); err != nil { | ||
| 401 | - | a.logger.Error().Err(err).Msg("failed to set workspace in view") | 432 | + | log.Error().Err(err).Msg("failed to set workspace in view") |
| 402 | return | 433 | return | ||
| 403 | } | 434 | } | ||
| 404 | 435 | ||||
| 436 | + | // Ensure WebViews are attached to panes | |||
| 437 | + | a.attachWorkspaceWebViews(ctx, tab.Workspace, wsView) | |||
| 438 | + | ||||
| 405 | // Store in map | 439 | // Store in map | ||
| 406 | a.workspaceViews[tab.ID] = wsView | 440 | a.workspaceViews[tab.ID] = wsView | ||
| 407 | 441 | ||||
| @@ -409,15 +443,14 @@ func (a *App) createWorkspaceView(tab *entity.Tab) { | |||||
| 409 | if a.mainWindow != nil { | 443 | if a.mainWindow != nil { | ||
| 410 | widget := wsView.Widget() | 444 | widget := wsView.Widget() | ||
| 411 | if widget != nil { | 445 | if widget != nil { | ||
| 412 | - | if gtkWidget := widget.GtkWidget(); gtkWidget != nil { | 446 | + | gtkWidget := widget.GtkWidget() |
| 447 | + | if gtkWidget != nil { | |||
| 413 | a.mainWindow.SetContent(gtkWidget) | 448 | a.mainWindow.SetContent(gtkWidget) | ||
| 414 | } | 449 | } | ||
| 415 | } | 450 | } | ||
| 416 | } | 451 | } | ||
| 417 | 452 | ||||
| 418 | - | a.logger.Debug(). | 453 | + | log.Debug().Str("tab_id", string(tab.ID)).Msg("workspace view created and attached") |
| 419 | - | Str("tab_id", string(tab.ID)). | |||
| 420 | - | Msg("workspace view created and attached") | |||
| 421 | } | 454 | } | ||
| 422 | 455 | ||||
| 423 | // activeWorkspace returns the workspace of the active tab. | 456 | // activeWorkspace returns the workspace of the active tab. | ||
| @@ -439,31 +472,33 @@ func (a *App) activeWorkspaceView() *component.WorkspaceView { | |||||
| 439 | } | 472 | } | ||
| 440 | 473 | ||||
| 441 | // handlePaneSplit splits the active pane in the given direction. | 474 | // handlePaneSplit splits the active pane in the given direction. | ||
| 442 | - | func (a *App) handlePaneSplit(direction usecase.SplitDirection) error { | 475 | + | func (a *App) handlePaneSplit(ctx context.Context, direction usecase.SplitDirection) error { |
| 476 | + | log := logging.FromContext(ctx) | |||
| 477 | + | ||||
| 443 | if a.panesUC == nil { | 478 | if a.panesUC == nil { | ||
| 444 | - | a.logger.Warn().Msg("panes use case not available") | 479 | + | log.Warn().Msg("panes use case not available") |
| 445 | return nil | 480 | return nil | ||
| 446 | } | 481 | } | ||
| 447 | 482 | ||||
| 448 | ws := a.activeWorkspace() | 483 | ws := a.activeWorkspace() | ||
| 449 | if ws == nil { | 484 | if ws == nil { | ||
| 450 | - | a.logger.Warn().Msg("no active workspace") | 485 | + | log.Warn().Msg("no active workspace") |
| 451 | return nil | 486 | return nil | ||
| 452 | } | 487 | } | ||
| 453 | 488 | ||||
| 454 | activePane := ws.ActivePane() | 489 | activePane := ws.ActivePane() | ||
| 455 | if activePane == nil { | 490 | if activePane == nil { | ||
| 456 | - | a.logger.Warn().Msg("no active pane to split") | 491 | + | log.Warn().Msg("no active pane to split") |
| 457 | return nil | 492 | return nil | ||
| 458 | } | 493 | } | ||
| 459 | 494 | ||||
| 460 | - | output, err := a.panesUC.Split(a.ctx, usecase.SplitPaneInput{ | 495 | + | output, err := a.panesUC.Split(ctx, usecase.SplitPaneInput{ |
| 461 | Workspace: ws, | 496 | Workspace: ws, | ||
| 462 | TargetPane: activePane, | 497 | TargetPane: activePane, | ||
| 463 | Direction: direction, | 498 | Direction: direction, | ||
| 464 | }) | 499 | }) | ||
| 465 | if err != nil { | 500 | if err != nil { | ||
| 466 | - | a.logger.Error().Err(err).Str("direction", string(direction)).Msg("failed to split pane") | 501 | + | log.Error().Err(err).Str("direction", string(direction)).Msg("failed to split pane") |
| 467 | return err | 502 | return err | ||
| 468 | } | 503 | } | ||
| 469 | 504 | ||||
| @@ -474,45 +509,47 @@ func (a *App) handlePaneSplit(direction usecase.SplitDirection) error { | |||||
| 474 | wsView := a.activeWorkspaceView() | 509 | wsView := a.activeWorkspaceView() | ||
| 475 | if wsView != nil { | 510 | if wsView != nil { | ||
| 476 | if err := wsView.Rebuild(); err != nil { | 511 | if err := wsView.Rebuild(); err != nil { | ||
| 477 | - | a.logger.Error().Err(err).Msg("failed to rebuild workspace view") | 512 | + | log.Error().Err(err).Msg("failed to rebuild workspace view") |
| 478 | } | 513 | } | ||
| 514 | + | a.attachWorkspaceWebViews(ctx, ws, wsView) | |||
| 515 | + | wsView.FocusPane(ws.ActivePaneID) | |||
| 479 | } | 516 | } | ||
| 480 | 517 | ||||
| 481 | - | a.logger.Info(). | 518 | + | log.Info().Str("direction", string(direction)).Str("new_pane_id", string(output.NewPaneNode.Pane.ID)).Msg("pane split completed") |
| 482 | - | Str("direction", string(direction)). | |||
| 483 | - | Str("new_pane_id", string(output.NewPaneNode.Pane.ID)). | |||
| 484 | - | Msg("pane split completed") | |||
| 485 | 519 | ||||
| 486 | return nil | 520 | return nil | ||
| 487 | } | 521 | } | ||
| 488 | 522 | ||||
| 489 | // handleClosePane closes the active pane. | 523 | // handleClosePane closes the active pane. | ||
| 490 | - | func (a *App) handleClosePane() error { | 524 | + | func (a *App) handleClosePane(ctx context.Context) error { |
| 525 | + | log := logging.FromContext(ctx) | |||
| 526 | + | ||||
| 491 | if a.panesUC == nil { | 527 | if a.panesUC == nil { | ||
| 492 | - | a.logger.Warn().Msg("panes use case not available") | 528 | + | log.Warn().Msg("panes use case not available") |
| 493 | return nil | 529 | return nil | ||
| 494 | } | 530 | } | ||
| 495 | 531 | ||||
| 496 | ws := a.activeWorkspace() | 532 | ws := a.activeWorkspace() | ||
| 497 | if ws == nil { | 533 | if ws == nil { | ||
| 498 | - | a.logger.Warn().Msg("no active workspace") | 534 | + | log.Warn().Msg("no active workspace") |
| 499 | return nil | 535 | return nil | ||
| 500 | } | 536 | } | ||
| 501 | 537 | ||||
| 502 | activePane := ws.ActivePane() | 538 | activePane := ws.ActivePane() | ||
| 503 | if activePane == nil { | 539 | if activePane == nil { | ||
| 504 | - | a.logger.Warn().Msg("no active pane to close") | 540 | + | log.Warn().Msg("no active pane to close") |
| 505 | return nil | 541 | return nil | ||
| 506 | } | 542 | } | ||
| 543 | + | closingPaneID := activePane.Pane.ID | |||
| 507 | 544 | ||||
| 508 | // Don't close the last pane - close the tab instead | 545 | // Don't close the last pane - close the tab instead | ||
| 509 | if ws.PaneCount() <= 1 { | 546 | if ws.PaneCount() <= 1 { | ||
| 510 | - | return a.handleCloseTab() | 547 | + | return a.handleCloseTab(ctx) |
| 511 | } | 548 | } | ||
| 512 | 549 | ||||
| 513 | - | _, err := a.panesUC.Close(a.ctx, ws, activePane) | 550 | + | _, err := a.panesUC.Close(ctx, ws, activePane) |
| 514 | if err != nil { | 551 | if err != nil { | ||
| 515 | - | a.logger.Error().Err(err).Msg("failed to close pane") | 552 | + | log.Error().Err(err).Msg("failed to close pane") |
| 516 | return err | 553 | return err | ||
| 517 | } | 554 | } | ||
| 518 | 555 | ||||
| @@ -520,35 +557,40 @@ func (a *App) handleClosePane() error { | |||||
| 520 | wsView := a.activeWorkspaceView() | 557 | wsView := a.activeWorkspaceView() | ||
| 521 | if wsView != nil { | 558 | if wsView != nil { | ||
| 522 | if err := wsView.Rebuild(); err != nil { | 559 | if err := wsView.Rebuild(); err != nil { | ||
| 523 | - | a.logger.Error().Err(err).Msg("failed to rebuild workspace view") | 560 | + | log.Error().Err(err).Msg("failed to rebuild workspace view") |
| 524 | } | 561 | } | ||
| 562 | + | a.releasePaneWebView(ctx, closingPaneID) | |||
| 563 | + | a.attachWorkspaceWebViews(ctx, ws, wsView) | |||
| 564 | + | wsView.FocusPane(ws.ActivePaneID) | |||
| 525 | } | 565 | } | ||
| 526 | 566 | ||||
| 527 | - | a.logger.Info().Msg("pane closed") | 567 | + | log.Info().Msg("pane closed") |
| 528 | return nil | 568 | return nil | ||
| 529 | } | 569 | } | ||
| 530 | 570 | ||||
| 531 | // handlePaneFocus navigates focus to an adjacent pane. | 571 | // handlePaneFocus navigates focus to an adjacent pane. | ||
| 532 | - | func (a *App) handlePaneFocus(direction usecase.NavigateDirection) error { | 572 | + | func (a *App) handlePaneFocus(ctx context.Context, direction usecase.NavigateDirection) error { |
| 573 | + | log := logging.FromContext(ctx) | |||
| 574 | + | ||||
| 533 | if a.panesUC == nil { | 575 | if a.panesUC == nil { | ||
| 534 | - | a.logger.Warn().Msg("panes use case not available") | 576 | + | log.Warn().Msg("panes use case not available") |
| 535 | return nil | 577 | return nil | ||
| 536 | } | 578 | } | ||
| 537 | 579 | ||||
| 538 | ws := a.activeWorkspace() | 580 | ws := a.activeWorkspace() | ||
| 539 | if ws == nil { | 581 | if ws == nil { | ||
| 540 | - | a.logger.Warn().Msg("no active workspace") | 582 | + | log.Warn().Msg("no active workspace") |
| 541 | return nil | 583 | return nil | ||
| 542 | } | 584 | } | ||
| 543 | 585 | ||||
| 544 | - | newPane, err := a.panesUC.NavigateFocus(a.ctx, ws, direction) | 586 | + | newPane, err := a.panesUC.NavigateFocus(ctx, ws, direction) |
| 545 | if err != nil { | 587 | if err != nil { | ||
| 546 | - | a.logger.Error().Err(err).Str("direction", string(direction)).Msg("failed to navigate focus") | 588 | + | log.Error().Err(err).Str("direction", string(direction)).Msg("failed to navigate focus") |
| 547 | return err | 589 | return err | ||
| 548 | } | 590 | } | ||
| 549 | 591 | ||||
| 550 | if newPane == nil { | 592 | if newPane == nil { | ||
| 551 | - | a.logger.Debug().Str("direction", string(direction)).Msg("no pane in that direction") | 593 | + | log.Debug().Str("direction", string(direction)).Msg("no pane in that direction") |
| 552 | return nil | 594 | return nil | ||
| 553 | } | 595 | } | ||
| 554 | 596 | ||||
| @@ -556,14 +598,184 @@ func (a *App) handlePaneFocus(direction usecase.NavigateDirection) error { | |||||
| 556 | wsView := a.activeWorkspaceView() | 598 | wsView := a.activeWorkspaceView() | ||
| 557 | if wsView != nil { | 599 | if wsView != nil { | ||
| 558 | if err := wsView.SetActivePaneID(newPane.Pane.ID); err != nil { | 600 | if err := wsView.SetActivePaneID(newPane.Pane.ID); err != nil { | ||
| 559 | - | a.logger.Warn().Err(err).Msg("failed to update active pane in view") | 601 | + | log.Warn().Err(err).Msg("failed to update active pane in view") |
| 602 | + | } else { | |||
| 603 | + | wsView.FocusPane(newPane.Pane.ID) | |||
| 560 | } | 604 | } | ||
| 561 | } | 605 | } | ||
| 562 | 606 | ||||
| 563 | - | a.logger.Debug(). | 607 | + | log.Debug().Str("direction", string(direction)).Str("new_pane_id", newPane.ID).Msg("focus navigated") |
| 564 | - | Str("direction", string(direction)). | |||
| 565 | - | Str("new_pane_id", newPane.ID). | |||
| 566 | - | Msg("focus navigated") | |||
| 567 | 608 | ||||
| 568 | return nil | 609 | return nil | ||
| 569 | } | 610 | } | ||
| 611 | + | ||||
| 612 | + | // registerMessageHandlers wires frontend message types to Go handlers. | |||
| 613 | + | func (a *App) registerMessageHandlers(ctx context.Context) error { | |||
| 614 | + | if a.router == nil { | |||
| 615 | + | return fmt.Errorf("message router is nil") | |||
| 616 | + | } | |||
| 617 | + | ||||
| 618 | + | // Omnibox search | |||
| 619 | + | if err := a.router.RegisterHandlerWithCallbacks("query", "__dumber_omnibox_suggestions", "", "", uihandler.NewQueryHandler(a.deps.HistoryUC, a.deps.Config)); err != nil { | |||
| 620 | + | return err | |||
| 621 | + | } | |||
| 622 | + | if err := a.router.RegisterHandlerWithCallbacks("omnibox_initial_history", "__dumber_omnibox_suggestions", "", "", uihandler.NewInitialHistoryHandler(a.deps.HistoryUC, a.deps.Config)); err != nil { | |||
| 623 | + | return err | |||
| 624 | + | } | |||
| 625 | + | if err := a.router.RegisterHandlerWithCallbacks("prefix_query", "__dumber_omnibox_inline_suggestion", "", "", uihandler.NewPrefixQueryHandler(a.deps.HistoryUC)); err != nil { | |||
| 626 | + | return err | |||
| 627 | + | } | |||
| 628 | + | ||||
| 629 | + | // Navigation | |||
| 630 | + | if err := a.router.RegisterHandler("navigate", uihandler.NewNavigateHandler()); err != nil { | |||
| 631 | + | return err | |||
| 632 | + | } | |||
| 633 | + | ||||
| 634 | + | // Favorites | |||
| 635 | + | if err := a.router.RegisterHandlerWithCallbacks("get_favorites", "__dumber_favorites", "", "", uihandler.NewFavoritesHandler(a.deps.FavoritesUC)); err != nil { | |||
| 636 | + | return err | |||
| 637 | + | } | |||
| 638 | + | if err := a.router.RegisterHandlerWithCallbacks("toggle_favorite", "__dumber_favorites", "", "", uihandler.NewToggleFavoriteHandler(a.deps.FavoritesUC)); err != nil { | |||
| 639 | + | return err | |||
| 640 | + | } | |||
| 641 | + | ||||
| 642 | + | // Shortcuts + palettes | |||
| 643 | + | if err := a.router.RegisterHandlerWithCallbacks("get_search_shortcuts", "__dumber_search_shortcuts", "__dumber_search_shortcuts_error", "", uihandler.NewShortcutsHandler(a.deps.Config)); err != nil { | |||
| 644 | + | return err | |||
| 645 | + | } | |||
| 646 | + | if err := a.router.RegisterHandlerWithCallbacks("get_color_palettes", "__dumber_color_palettes", "__dumber_color_palettes_error", "", uihandler.NewPaletteHandler(a.deps.Config)); err != nil { | |||
| 647 | + | return err | |||
| 648 | + | } | |||
| 649 | + | ||||
| 650 | + | // Keyboard blocking | |||
| 651 | + | if err := a.router.RegisterHandler("keyboard_blocking", uihandler.NewKeyboardBlockingHandler()); err != nil { | |||
| 652 | + | return err | |||
| 653 | + | } | |||
| 654 | + | ||||
| 655 | + | return nil | |||
| 656 | + | } | |||
| 657 | + | ||||
| 658 | + | // attachWorkspaceWebViews ensures each pane has a WebView widget attached. | |||
| 659 | + | func (a *App) attachWorkspaceWebViews(ctx context.Context, ws *entity.Workspace, wsView *component.WorkspaceView) { | |||
| 660 | + | log := logging.FromContext(ctx) | |||
| 661 | + | ||||
| 662 | + | if ws == nil || wsView == nil || a.widgetFactory == nil { | |||
| 663 | + | return | |||
| 664 | + | } | |||
| 665 | + | ||||
| 666 | + | for _, pane := range ws.AllPanes() { | |||
| 667 | + | if pane == nil { | |||
| 668 | + | continue | |||
| 669 | + | } | |||
| 670 | + | ||||
| 671 | + | wv, err := a.ensureWebViewForPane(ctx, pane.ID) | |||
| 672 | + | if err != nil { | |||
| 673 | + | log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("failed to ensure webview for pane") | |||
| 674 | + | continue | |||
| 675 | + | } | |||
| 676 | + | ||||
| 677 | + | // Load the pane's URI if set and different from current | |||
| 678 | + | if pane.URI != "" && pane.URI != wv.URI() { | |||
| 679 | + | if err := wv.LoadURI(ctx, pane.URI); err != nil { | |||
| 680 | + | log.Warn().Err(err).Str("pane_id", string(pane.ID)).Str("uri", pane.URI).Msg("failed to load pane URI") | |||
| 681 | + | } | |||
| 682 | + | } | |||
| 683 | + | ||||
| 684 | + | widget := a.wrapWebViewWidget(ctx, wv) | |||
| 685 | + | if widget == nil { | |||
| 686 | + | continue | |||
| 687 | + | } | |||
| 688 | + | ||||
| 689 | + | if err := wsView.SetWebViewWidget(pane.ID, widget); err != nil { | |||
| 690 | + | log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("failed to attach webview widget") | |||
| 691 | + | } | |||
| 692 | + | } | |||
| 693 | + | } | |||
| 694 | + | ||||
| 695 | + | // ensureWebViewForPane acquires or reuses a WebView for the given pane. | |||
| 696 | + | func (a *App) ensureWebViewForPane(ctx context.Context, paneID entity.PaneID) (*webkit.WebView, error) { | |||
| 697 | + | if wv, ok := a.webViews[paneID]; ok && wv != nil && !wv.IsDestroyed() { | |||
| 698 | + | return wv, nil | |||
| 699 | + | } | |||
| 700 | + | if a.pool == nil { | |||
| 701 | + | return nil, fmt.Errorf("webview pool not configured") | |||
| 702 | + | } | |||
| 703 | + | ||||
| 704 | + | wv, err := a.pool.Acquire(ctx) | |||
| 705 | + | if err != nil { | |||
| 706 | + | return nil, err | |||
| 707 | + | } | |||
| 708 | + | ||||
| 709 | + | a.webViews[paneID] = wv | |||
| 710 | + | return wv, nil | |||
| 711 | + | } | |||
| 712 | + | ||||
| 713 | + | // releasePaneWebView returns the WebView for a pane to the pool. | |||
| 714 | + | func (a *App) releasePaneWebView(ctx context.Context, paneID entity.PaneID) { | |||
| 715 | + | wv, ok := a.webViews[paneID] | |||
| 716 | + | if !ok || wv == nil { | |||
| 717 | + | return | |||
| 718 | + | } | |||
| 719 | + | delete(a.webViews, paneID) | |||
| 720 | + | ||||
| 721 | + | if a.pool != nil { | |||
| 722 | + | a.pool.Release(wv) | |||
| 723 | + | } else { | |||
| 724 | + | wv.Destroy() | |||
| 725 | + | } | |||
| 726 | + | } | |||
| 727 | + | ||||
| 728 | + | // wrapWebViewWidget converts a WebView to a layout.Widget for embedding. | |||
| 729 | + | func (a *App) wrapWebViewWidget(ctx context.Context, wv *webkit.WebView) layout.Widget { | |||
| 730 | + | if wv == nil || a.widgetFactory == nil { | |||
| 731 | + | return nil | |||
| 732 | + | } | |||
| 733 | + | gtkView := wv.Widget() | |||
| 734 | + | if gtkView == nil { | |||
| 735 | + | return nil | |||
| 736 | + | } | |||
| 737 | + | return a.widgetFactory.WrapWidget(>kView.Widget) | |||
| 738 | + | } | |||
| 739 | + | ||||
| 740 | + | // activeWebView returns the WebView for the active pane. | |||
| 741 | + | func (a *App) activeWebView(ctx context.Context) *webkit.WebView { | |||
| 742 | + | ws := a.activeWorkspace() | |||
| 743 | + | if ws == nil { | |||
| 744 | + | return nil | |||
| 745 | + | } | |||
| 746 | + | pane := ws.ActivePane() | |||
| 747 | + | if pane == nil || pane.Pane == nil { | |||
| 748 | + | return nil | |||
| 749 | + | } | |||
| 750 | + | return a.webViews[pane.Pane.ID] | |||
| 751 | + | } | |||
| 752 | + | ||||
| 753 | + | // openOmnibox toggles the omnibox overlay for the active WebView. | |||
| 754 | + | func (a *App) openOmnibox(ctx context.Context) error { | |||
| 755 | + | log := logging.FromContext(ctx) | |||
| 756 | + | ||||
| 757 | + | if a.overlay == nil { | |||
| 758 | + | log.Error().Msg("overlay controller not configured") | |||
| 759 | + | return fmt.Errorf("overlay controller not configured") | |||
| 760 | + | } | |||
| 761 | + | wv := a.activeWebView(ctx) | |||
| 762 | + | if wv == nil { | |||
| 763 | + | log.Error().Msg("no active webview for omnibox") | |||
| 764 | + | return fmt.Errorf("no active webview for omnibox") | |||
| 765 | + | } | |||
| 766 | + | log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("opening omnibox") | |||
| 767 | + | return a.overlay.Show(ctx, wv.ID(), "omnibox", "") | |||
| 768 | + | } | |||
| 769 | + | ||||
| 770 | + | // openDevTools opens the WebKit inspector for the active WebView. | |||
| 771 | + | func (a *App) openDevTools(ctx context.Context) error { | |||
| 772 | + | log := logging.FromContext(ctx) | |||
| 773 | + | ||||
| 774 | + | wv := a.activeWebView(ctx) | |||
| 775 | + | if wv == nil { | |||
| 776 | + | log.Warn().Msg("no active webview for devtools") | |||
| 777 | + | return fmt.Errorf("no active webview for devtools") | |||
| 778 | + | } | |||
| 779 | + | log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("opening devtools") | |||
| 780 | + | return wv.ShowDevTools() | |||
| 781 | + | } | |||
M internal/ui/component/pane_view.go +5 -1
| @@ -34,9 +34,11 @@ func NewPaneView(factory layout.WidgetFactory, paneID entity.PaneID, webViewWidg | |||
| 34 | 34 | overlay := factory.NewOverlay() | |
| 35 | 35 | overlay.SetHexpand(true) | |
| 36 | 36 | overlay.SetVexpand(true) | |
| 37 | + | overlay.SetVisible(true) | |
| 37 | 38 | ||
| 38 | 39 | // Set the WebView as the main child | |
| 39 | 40 | if webViewWidget != nil { | |
| 41 | + | webViewWidget.SetVisible(true) | |
| 40 | 42 | overlay.SetChild(webViewWidget) | |
| 41 | 43 | } | |
| 42 | 44 | ||
| @@ -44,13 +46,14 @@ func NewPaneView(factory layout.WidgetFactory, paneID entity.PaneID, webViewWidg | |||
| 44 | 46 | // This is an empty box that gets styled via CSS to show a border | |
| 45 | 47 | borderBox := factory.NewBox(layout.OrientationVertical, 0) | |
| 46 | 48 | borderBox.SetCanFocus(false) | |
| 49 | + | borderBox.SetCanTarget(false) // Don't intercept pointer events - let them pass through to WebView | |
| 47 | 50 | borderBox.AddCssClass("pane-border") | |
| 48 | 51 | // Position border to cover entire pane | |
| 49 | 52 | borderBox.SetHexpand(true) | |
| 50 | 53 | borderBox.SetVexpand(true) | |
| 51 | 54 | overlay.AddOverlay(borderBox) | |
| 52 | 55 | ||
| 53 | - | // Don't let border intercept pointer events | |
| 56 | + | // Don't let border affect layout | |
| 54 | 57 | overlay.SetClipOverlay(borderBox, false) | |
| 55 | 58 | overlay.SetMeasureOverlay(borderBox, false) | |
| 56 | 59 | ||
| @@ -117,6 +120,7 @@ func (pv *PaneView) SetWebViewWidget(widget layout.Widget) { | |||
| 117 | 120 | pv.webViewWidget = widget | |
| 118 | 121 | ||
| 119 | 122 | if widget != nil { | |
| 123 | + | widget.SetVisible(true) | |
| 120 | 124 | pv.overlay.SetChild(widget) | |
| 121 | 125 | } | |
| 122 | 126 | } | |
| @@ -34,9 +34,11 @@ func NewPaneView(factory layout.WidgetFactory, paneID entity.PaneID, webViewWidg | |||||
| 34 | overlay := factory.NewOverlay() | 34 | overlay := factory.NewOverlay() | ||
| 35 | overlay.SetHexpand(true) | 35 | overlay.SetHexpand(true) | ||
| 36 | overlay.SetVexpand(true) | 36 | overlay.SetVexpand(true) | ||
| 37 | + | overlay.SetVisible(true) | |||
| 37 | 38 | ||||
| 38 | // Set the WebView as the main child | 39 | // Set the WebView as the main child | ||
| 39 | if webViewWidget != nil { | 40 | if webViewWidget != nil { | ||
| 41 | + | webViewWidget.SetVisible(true) | |||
| 40 | overlay.SetChild(webViewWidget) | 42 | overlay.SetChild(webViewWidget) | ||
| 41 | } | 43 | } | ||
| 42 | 44 | ||||
| @@ -44,13 +46,14 @@ func NewPaneView(factory layout.WidgetFactory, paneID entity.PaneID, webViewWidg | |||||
| 44 | // This is an empty box that gets styled via CSS to show a border | 46 | // This is an empty box that gets styled via CSS to show a border | ||
| 45 | borderBox := factory.NewBox(layout.OrientationVertical, 0) | 47 | borderBox := factory.NewBox(layout.OrientationVertical, 0) | ||
| 46 | borderBox.SetCanFocus(false) | 48 | borderBox.SetCanFocus(false) | ||
| 49 | + | borderBox.SetCanTarget(false) // Don't intercept pointer events - let them pass through to WebView | |||
| 47 | borderBox.AddCssClass("pane-border") | 50 | borderBox.AddCssClass("pane-border") | ||
| 48 | // Position border to cover entire pane | 51 | // Position border to cover entire pane | ||
| 49 | borderBox.SetHexpand(true) | 52 | borderBox.SetHexpand(true) | ||
| 50 | borderBox.SetVexpand(true) | 53 | borderBox.SetVexpand(true) | ||
| 51 | overlay.AddOverlay(borderBox) | 54 | overlay.AddOverlay(borderBox) | ||
| 52 | 55 | ||||
| 53 | - | // Don't let border intercept pointer events | 56 | + | // Don't let border affect layout |
| 54 | overlay.SetClipOverlay(borderBox, false) | 57 | overlay.SetClipOverlay(borderBox, false) | ||
| 55 | overlay.SetMeasureOverlay(borderBox, false) | 58 | overlay.SetMeasureOverlay(borderBox, false) | ||
| 56 | 59 | ||||
| @@ -117,6 +120,7 @@ func (pv *PaneView) SetWebViewWidget(widget layout.Widget) { | |||||
| 117 | pv.webViewWidget = widget | 120 | pv.webViewWidget = widget | ||
| 118 | 121 | ||||
| 119 | if widget != nil { | 122 | if widget != nil { | ||
| 123 | + | widget.SetVisible(true) | |||
| 120 | pv.overlay.SetChild(widget) | 124 | pv.overlay.SetChild(widget) | ||
| 121 | } | 125 | } | ||
| 122 | } | 126 | } | ||
M internal/ui/component/tab_bar.go +1 -0
| @@ -38,6 +38,7 @@ func NewTabBar() *TabBar { | |||
| 38 | 38 | // Configure the box | |
| 39 | 39 | tb.box.SetHexpand(true) | |
| 40 | 40 | tb.box.SetVexpand(false) | |
| 41 | + | tb.box.SetVisible(true) | |
| 41 | 42 | tb.box.AddCssClass("tab-bar") | |
| 42 | 43 | ||
| 43 | 44 | return tb | |
| @@ -38,6 +38,7 @@ func NewTabBar() *TabBar { | |||||
| 38 | // Configure the box | 38 | // Configure the box | ||
| 39 | tb.box.SetHexpand(true) | 39 | tb.box.SetHexpand(true) | ||
| 40 | tb.box.SetVexpand(false) | 40 | tb.box.SetVexpand(false) | ||
| 41 | + | tb.box.SetVisible(true) | |||
| 41 | tb.box.AddCssClass("tab-bar") | 42 | tb.box.AddCssClass("tab-bar") | ||
| 42 | 43 | ||||
| 43 | return tb | 44 | return tb | ||
M internal/ui/component/workspace_view.go +7 -2
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | ||
| 8 | 8 | "github.com/bnema/dumber/internal/domain/entity" | |
| 9 | 9 | "github.com/bnema/dumber/internal/ui/layout" | |
| 10 | + | "github.com/rs/zerolog" | |
| 10 | 11 | ) | |
| 11 | 12 | ||
| 12 | 13 | // ErrNilWorkspace is returned when attempting to set a nil workspace. | |
| @@ -21,6 +22,7 @@ type WorkspaceView struct { | |||
| 21 | 22 | factory layout.WidgetFactory | |
| 22 | 23 | treeRenderer *layout.TreeRenderer | |
| 23 | 24 | container layout.BoxWidget | |
| 25 | + | logger zerolog.Logger | |
| 24 | 26 | ||
| 25 | 27 | workspace *entity.Workspace | |
| 26 | 28 | paneViews map[entity.PaneID]*PaneView | |
| @@ -65,19 +67,21 @@ func (a *paneViewFactoryAdapter) CreatePaneView(node *entity.PaneNode) layout.Wi | |||
| 65 | 67 | } | |
| 66 | 68 | ||
| 67 | 69 | // NewWorkspaceView creates a new workspace view. | |
| 68 | - | func NewWorkspaceView(factory layout.WidgetFactory) *WorkspaceView { | |
| 70 | + | func NewWorkspaceView(factory layout.WidgetFactory, logger zerolog.Logger) *WorkspaceView { | |
| 69 | 71 | container := factory.NewBox(layout.OrientationVertical, 0) | |
| 70 | 72 | container.SetHexpand(true) | |
| 71 | 73 | container.SetVexpand(true) | |
| 74 | + | container.SetVisible(true) | |
| 72 | 75 | ||
| 73 | 76 | wv := &WorkspaceView{ | |
| 74 | 77 | factory: factory, | |
| 75 | 78 | container: container, | |
| 79 | + | logger: logger.With().Str("component", "workspace-view").Logger(), | |
| 76 | 80 | paneViews: make(map[entity.PaneID]*PaneView), | |
| 77 | 81 | } | |
| 78 | 82 | ||
| 79 | 83 | // Create tree renderer with our adapter as the pane view factory | |
| 80 | - | wv.treeRenderer = layout.NewTreeRenderer(factory, &paneViewFactoryAdapter{wv: wv}) | |
| 84 | + | wv.treeRenderer = layout.NewTreeRenderer(factory, &paneViewFactoryAdapter{wv: wv}, wv.logger) | |
| 81 | 85 | ||
| 82 | 86 | return wv | |
| 83 | 87 | } | |
| @@ -106,6 +110,7 @@ func (wv *WorkspaceView) SetWorkspace(ws *entity.Workspace) error { | |||
| 106 | 110 | } | |
| 107 | 111 | ||
| 108 | 112 | if widget != nil { | |
| 113 | + | widget.SetVisible(true) | |
| 109 | 114 | wv.container.Append(widget) | |
| 110 | 115 | } | |
| 111 | 116 | } | |
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | 7 | ||||
| 8 | "github.com/bnema/dumber/internal/domain/entity" | 8 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 9 | "github.com/bnema/dumber/internal/ui/layout" | 9 | "github.com/bnema/dumber/internal/ui/layout" | ||
| 10 | + | "github.com/rs/zerolog" | |||
| 10 | ) | 11 | ) | ||
| 11 | 12 | ||||
| 12 | // ErrNilWorkspace is returned when attempting to set a nil workspace. | 13 | // ErrNilWorkspace is returned when attempting to set a nil workspace. | ||
| @@ -21,6 +22,7 @@ type WorkspaceView struct { | |||||
| 21 | factory layout.WidgetFactory | 22 | factory layout.WidgetFactory | ||
| 22 | treeRenderer *layout.TreeRenderer | 23 | treeRenderer *layout.TreeRenderer | ||
| 23 | container layout.BoxWidget | 24 | container layout.BoxWidget | ||
| 25 | + | logger zerolog.Logger | |||
| 24 | 26 | ||||
| 25 | workspace *entity.Workspace | 27 | workspace *entity.Workspace | ||
| 26 | paneViews map[entity.PaneID]*PaneView | 28 | paneViews map[entity.PaneID]*PaneView | ||
| @@ -65,19 +67,21 @@ func (a *paneViewFactoryAdapter) CreatePaneView(node *entity.PaneNode) layout.Wi | |||||
| 65 | } | 67 | } | ||
| 66 | 68 | ||||
| 67 | // NewWorkspaceView creates a new workspace view. | 69 | // NewWorkspaceView creates a new workspace view. | ||
| 68 | - | func NewWorkspaceView(factory layout.WidgetFactory) *WorkspaceView { | 70 | + | func NewWorkspaceView(factory layout.WidgetFactory, logger zerolog.Logger) *WorkspaceView { |
| 69 | container := factory.NewBox(layout.OrientationVertical, 0) | 71 | container := factory.NewBox(layout.OrientationVertical, 0) | ||
| 70 | container.SetHexpand(true) | 72 | container.SetHexpand(true) | ||
| 71 | container.SetVexpand(true) | 73 | container.SetVexpand(true) | ||
| 74 | + | container.SetVisible(true) | |||
| 72 | 75 | ||||
| 73 | wv := &WorkspaceView{ | 76 | wv := &WorkspaceView{ | ||
| 74 | factory: factory, | 77 | factory: factory, | ||
| 75 | container: container, | 78 | container: container, | ||
| 79 | + | logger: logger.With().Str("component", "workspace-view").Logger(), | |||
| 76 | paneViews: make(map[entity.PaneID]*PaneView), | 80 | paneViews: make(map[entity.PaneID]*PaneView), | ||
| 77 | } | 81 | } | ||
| 78 | 82 | ||||
| 79 | // Create tree renderer with our adapter as the pane view factory | 83 | // Create tree renderer with our adapter as the pane view factory | ||
| 80 | - | wv.treeRenderer = layout.NewTreeRenderer(factory, &paneViewFactoryAdapter{wv: wv}) | 84 | + | wv.treeRenderer = layout.NewTreeRenderer(factory, &paneViewFactoryAdapter{wv: wv}, wv.logger) |
| 81 | 85 | ||||
| 82 | return wv | 86 | return wv | ||
| 83 | } | 87 | } | ||
| @@ -106,6 +110,7 @@ func (wv *WorkspaceView) SetWorkspace(ws *entity.Workspace) error { | |||||
| 106 | } | 110 | } | ||
| 107 | 111 | ||||
| 108 | if widget != nil { | 112 | if widget != nil { | ||
| 113 | + | widget.SetVisible(true) | |||
| 109 | wv.container.Append(widget) | 114 | wv.container.Append(widget) | ||
| 110 | } | 115 | } | ||
| 111 | } | 116 | } | ||
M internal/ui/component/workspace_view_test.go +23 -18
| @@ -3,6 +3,7 @@ package component_test | |||
| 3 | 3 | import ( | |
| 4 | 4 | "testing" | |
| 5 | 5 | ||
| 6 | + | "github.com/rs/zerolog" | |
| 6 | 7 | "github.com/stretchr/testify/assert" | |
| 7 | 8 | "github.com/stretchr/testify/mock" | |
| 8 | 9 | "github.com/stretchr/testify/require" | |
| @@ -23,7 +24,7 @@ func TestNewWorkspaceView_CreatesEmptyContainer(t *testing.T) { | |||
| 23 | 24 | mockBox.EXPECT().SetVexpand(true).Once() | |
| 24 | 25 | ||
| 25 | 26 | // Act | |
| 26 | - | wv := component.NewWorkspaceView(mockFactory) | |
| 27 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) | |
| 27 | 28 | ||
| 28 | 29 | // Assert | |
| 29 | 30 | require.NotNil(t, wv) | |
| @@ -40,7 +41,7 @@ func TestSetWorkspace_NilWorkspace_ReturnsError(t *testing.T) { | |||
| 40 | 41 | mockBox.EXPECT().SetHexpand(true).Once() | |
| 41 | 42 | mockBox.EXPECT().SetVexpand(true).Once() | |
| 42 | 43 | ||
| 43 | - | wv := component.NewWorkspaceView(mockFactory) | |
| 44 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) | |
| 44 | 45 | ||
| 45 | 46 | // Act | |
| 46 | 47 | err := wv.SetWorkspace(nil) | |
| @@ -59,7 +60,7 @@ func TestSetWorkspace_SinglePane_CreatesPaneView(t *testing.T) { | |||
| 59 | 60 | mockBox.EXPECT().SetHexpand(true).Once() | |
| 60 | 61 | mockBox.EXPECT().SetVexpand(true).Once() | |
| 61 | 62 | ||
| 62 | - | wv := component.NewWorkspaceView(mockFactory) | |
| 63 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) | |
| 63 | 64 | ||
| 64 | 65 | // PaneView creation (via paneViewFactoryAdapter) | |
| 65 | 66 | mockOverlay := mocks.NewMockOverlayWidget(t) | |
| @@ -113,7 +114,7 @@ func TestSetActivePaneID_UpdatesStyling(t *testing.T) { | |||
| 113 | 114 | mockBox.EXPECT().SetHexpand(true).Once() | |
| 114 | 115 | mockBox.EXPECT().SetVexpand(true).Once() | |
| 115 | 116 | ||
| 116 | - | wv := component.NewWorkspaceView(mockFactory) | |
| 117 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) | |
| 117 | 118 | ||
| 118 | 119 | // Setup two panes | |
| 119 | 120 | pane1 := entity.NewPane(entity.PaneID("pane-1")) | |
| @@ -162,7 +163,9 @@ func TestSetActivePaneID_UpdatesStyling(t *testing.T) { | |||
| 162 | 163 | mockPaned.EXPECT().SetShrinkEndChild(false).Once() | |
| 163 | 164 | mockPaned.EXPECT().SetStartChild(mockOverlay1).Once() | |
| 164 | 165 | mockPaned.EXPECT().SetEndChild(mockOverlay2).Once() | |
| 166 | + | mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once() | |
| 165 | 167 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | |
| 168 | + | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | |
| 166 | 169 | ||
| 167 | 170 | mockBox.EXPECT().Append(mockPaned).Once() | |
| 168 | 171 | ||
| @@ -206,7 +209,7 @@ func TestSetActivePaneID_InvalidID_ReturnsError(t *testing.T) { | |||
| 206 | 209 | mockBox.EXPECT().SetHexpand(true).Once() | |
| 207 | 210 | mockBox.EXPECT().SetVexpand(true).Once() | |
| 208 | 211 | ||
| 209 | - | wv := component.NewWorkspaceView(mockFactory) | |
| 212 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) | |
| 210 | 213 | ||
| 211 | 214 | // Act | |
| 212 | 215 | err := wv.SetActivePaneID(entity.PaneID("non-existent")) | |
| @@ -226,7 +229,7 @@ func TestGetPaneView_Existing(t *testing.T) { | |||
| 226 | 229 | mockBox.EXPECT().SetHexpand(true).Once() | |
| 227 | 230 | mockBox.EXPECT().SetVexpand(true).Once() | |
| 228 | 231 | ||
| 229 | - | wv := component.NewWorkspaceView(mockFactory) | |
| 232 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) | |
| 230 | 233 | ||
| 231 | 234 | // PaneView creation | |
| 232 | 235 | mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once() | |
| @@ -269,7 +272,7 @@ func TestGetPaneView_NonExistent(t *testing.T) { | |||
| 269 | 272 | mockBox.EXPECT().SetHexpand(true).Once() | |
| 270 | 273 | mockBox.EXPECT().SetVexpand(true).Once() | |
| 271 | 274 | ||
| 272 | - | wv := component.NewWorkspaceView(mockFactory) | |
| 275 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) | |
| 273 | 276 | ||
| 274 | 277 | // Act | |
| 275 | 278 | result := wv.GetPaneView(entity.PaneID("non-existent")) | |
| @@ -292,7 +295,7 @@ func TestGetPaneIDs_ReturnsAllIDs(t *testing.T) { | |||
| 292 | 295 | mockBox.EXPECT().SetHexpand(true).Once() | |
| 293 | 296 | mockBox.EXPECT().SetVexpand(true).Once() | |
| 294 | 297 | ||
| 295 | - | wv := component.NewWorkspaceView(mockFactory) | |
| 298 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) | |
| 296 | 299 | ||
| 297 | 300 | // First pane view | |
| 298 | 301 | mockFactory.EXPECT().NewOverlay().Return(mockOverlay1).Once() | |
| @@ -328,7 +331,9 @@ func TestGetPaneIDs_ReturnsAllIDs(t *testing.T) { | |||
| 328 | 331 | mockPaned.EXPECT().SetShrinkEndChild(false).Once() | |
| 329 | 332 | mockPaned.EXPECT().SetStartChild(mockOverlay1).Once() | |
| 330 | 333 | mockPaned.EXPECT().SetEndChild(mockOverlay2).Once() | |
| 334 | + | mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once() | |
| 331 | 335 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | |
| 336 | + | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | |
| 332 | 337 | ||
| 333 | 338 | mockBox.EXPECT().Append(mockPaned).Once() | |
| 334 | 339 | mockBorderBox1.EXPECT().AddCssClass("active-pane").Once() | |
| @@ -371,7 +376,7 @@ func TestWidget_ReturnsContainer(t *testing.T) { | |||
| 371 | 376 | mockBox.EXPECT().SetHexpand(true).Once() | |
| 372 | 377 | mockBox.EXPECT().SetVexpand(true).Once() | |
| 373 | 378 | ||
| 374 | - | wv := component.NewWorkspaceView(mockFactory) | |
| 379 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) | |
| 375 | 380 | ||
| 376 | 381 | // Act | |
| 377 | 382 | widget := wv.Widget() | |
| @@ -389,7 +394,7 @@ func TestSetOnPaneFocused_SetsCallback(t *testing.T) { | |||
| 389 | 394 | mockBox.EXPECT().SetHexpand(true).Once() | |
| 390 | 395 | mockBox.EXPECT().SetVexpand(true).Once() | |
| 391 | 396 | ||
| 392 | - | wv := component.NewWorkspaceView(mockFactory) | |
| 397 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) | |
| 393 | 398 | ||
| 394 | 399 | callback := func(paneID entity.PaneID) {} | |
| 395 | 400 | ||
| @@ -412,7 +417,7 @@ func TestSetWebViewWidget_AttachesToPane(t *testing.T) { | |||
| 412 | 417 | mockBox.EXPECT().SetHexpand(true).Once() | |
| 413 | 418 | mockBox.EXPECT().SetVexpand(true).Once() | |
| 414 | 419 | ||
| 415 | - | wv := component.NewWorkspaceView(mockFactory) | |
| 420 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) | |
| 416 | 421 | ||
| 417 | 422 | // PaneView creation | |
| 418 | 423 | mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once() | |
| @@ -459,7 +464,7 @@ func TestSetWebViewWidget_NonExistentPane_ReturnsError(t *testing.T) { | |||
| 459 | 464 | mockBox.EXPECT().SetHexpand(true).Once() | |
| 460 | 465 | mockBox.EXPECT().SetVexpand(true).Once() | |
| 461 | 466 | ||
| 462 | - | wv := component.NewWorkspaceView(mockFactory) | |
| 467 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) | |
| 463 | 468 | ||
| 464 | 469 | // Act | |
| 465 | 470 | err := wv.SetWebViewWidget(entity.PaneID("non-existent"), mockWebView) | |
| @@ -479,7 +484,7 @@ func TestFocusPane_DelegatesToPaneView(t *testing.T) { | |||
| 479 | 484 | mockBox.EXPECT().SetHexpand(true).Once() | |
| 480 | 485 | mockBox.EXPECT().SetVexpand(true).Once() | |
| 481 | 486 | ||
| 482 | - | wv := component.NewWorkspaceView(mockFactory) | |
| 487 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) | |
| 483 | 488 | ||
| 484 | 489 | // PaneView creation | |
| 485 | 490 | mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once() | |
| @@ -522,7 +527,7 @@ func TestFocusPane_NonExistentPane_ReturnsFalse(t *testing.T) { | |||
| 522 | 527 | mockBox.EXPECT().SetHexpand(true).Once() | |
| 523 | 528 | mockBox.EXPECT().SetVexpand(true).Once() | |
| 524 | 529 | ||
| 525 | - | wv := component.NewWorkspaceView(mockFactory) | |
| 530 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) | |
| 526 | 531 | ||
| 527 | 532 | // Act | |
| 528 | 533 | result := wv.FocusPane(entity.PaneID("non-existent")) | |
| @@ -540,7 +545,7 @@ func TestRebuild_NilWorkspace_ReturnsError(t *testing.T) { | |||
| 540 | 545 | mockBox.EXPECT().SetHexpand(true).Once() | |
| 541 | 546 | mockBox.EXPECT().SetVexpand(true).Once() | |
| 542 | 547 | ||
| 543 | - | wv := component.NewWorkspaceView(mockFactory) | |
| 548 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) | |
| 544 | 549 | ||
| 545 | 550 | // Act | |
| 546 | 551 | err := wv.Rebuild() | |
| @@ -558,7 +563,7 @@ func TestTreeRenderer_ReturnsRenderer(t *testing.T) { | |||
| 558 | 563 | mockBox.EXPECT().SetHexpand(true).Once() | |
| 559 | 564 | mockBox.EXPECT().SetVexpand(true).Once() | |
| 560 | 565 | ||
| 561 | - | wv := component.NewWorkspaceView(mockFactory) | |
| 566 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) | |
| 562 | 567 | ||
| 563 | 568 | // Act | |
| 564 | 569 | renderer := wv.TreeRenderer() | |
| @@ -578,7 +583,7 @@ func TestWorkspace_ReturnsCurrentWorkspace(t *testing.T) { | |||
| 578 | 583 | mockBox.EXPECT().SetHexpand(true).Once() | |
| 579 | 584 | mockBox.EXPECT().SetVexpand(true).Once() | |
| 580 | 585 | ||
| 581 | - | wv := component.NewWorkspaceView(mockFactory) | |
| 586 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) | |
| 582 | 587 | ||
| 583 | 588 | // PaneView creation | |
| 584 | 589 | mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once() | |
| @@ -621,7 +626,7 @@ func TestWorkspace_NilBeforeSet(t *testing.T) { | |||
| 621 | 626 | mockBox.EXPECT().SetHexpand(true).Once() | |
| 622 | 627 | mockBox.EXPECT().SetVexpand(true).Once() | |
| 623 | 628 | ||
| 624 | - | wv := component.NewWorkspaceView(mockFactory) | |
| 629 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) | |
| 625 | 630 | ||
| 626 | 631 | // Act | |
| 627 | 632 | result := wv.Workspace() | |
| @@ -3,6 +3,7 @@ package component_test | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "testing" | 4 | "testing" | ||
| 5 | 5 | ||||
| 6 | + | "github.com/rs/zerolog" | |||
| 6 | "github.com/stretchr/testify/assert" | 7 | "github.com/stretchr/testify/assert" | ||
| 7 | "github.com/stretchr/testify/mock" | 8 | "github.com/stretchr/testify/mock" | ||
| 8 | "github.com/stretchr/testify/require" | 9 | "github.com/stretchr/testify/require" | ||
| @@ -23,7 +24,7 @@ func TestNewWorkspaceView_CreatesEmptyContainer(t *testing.T) { | |||||
| 23 | mockBox.EXPECT().SetVexpand(true).Once() | 24 | mockBox.EXPECT().SetVexpand(true).Once() | ||
| 24 | 25 | ||||
| 25 | // Act | 26 | // Act | ||
| 26 | - | wv := component.NewWorkspaceView(mockFactory) | 27 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) |
| 27 | 28 | ||||
| 28 | // Assert | 29 | // Assert | ||
| 29 | require.NotNil(t, wv) | 30 | require.NotNil(t, wv) | ||
| @@ -40,7 +41,7 @@ func TestSetWorkspace_NilWorkspace_ReturnsError(t *testing.T) { | |||||
| 40 | mockBox.EXPECT().SetHexpand(true).Once() | 41 | mockBox.EXPECT().SetHexpand(true).Once() | ||
| 41 | mockBox.EXPECT().SetVexpand(true).Once() | 42 | mockBox.EXPECT().SetVexpand(true).Once() | ||
| 42 | 43 | ||||
| 43 | - | wv := component.NewWorkspaceView(mockFactory) | 44 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) |
| 44 | 45 | ||||
| 45 | // Act | 46 | // Act | ||
| 46 | err := wv.SetWorkspace(nil) | 47 | err := wv.SetWorkspace(nil) | ||
| @@ -59,7 +60,7 @@ func TestSetWorkspace_SinglePane_CreatesPaneView(t *testing.T) { | |||||
| 59 | mockBox.EXPECT().SetHexpand(true).Once() | 60 | mockBox.EXPECT().SetHexpand(true).Once() | ||
| 60 | mockBox.EXPECT().SetVexpand(true).Once() | 61 | mockBox.EXPECT().SetVexpand(true).Once() | ||
| 61 | 62 | ||||
| 62 | - | wv := component.NewWorkspaceView(mockFactory) | 63 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) |
| 63 | 64 | ||||
| 64 | // PaneView creation (via paneViewFactoryAdapter) | 65 | // PaneView creation (via paneViewFactoryAdapter) | ||
| 65 | mockOverlay := mocks.NewMockOverlayWidget(t) | 66 | mockOverlay := mocks.NewMockOverlayWidget(t) | ||
| @@ -113,7 +114,7 @@ func TestSetActivePaneID_UpdatesStyling(t *testing.T) { | |||||
| 113 | mockBox.EXPECT().SetHexpand(true).Once() | 114 | mockBox.EXPECT().SetHexpand(true).Once() | ||
| 114 | mockBox.EXPECT().SetVexpand(true).Once() | 115 | mockBox.EXPECT().SetVexpand(true).Once() | ||
| 115 | 116 | ||||
| 116 | - | wv := component.NewWorkspaceView(mockFactory) | 117 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) |
| 117 | 118 | ||||
| 118 | // Setup two panes | 119 | // Setup two panes | ||
| 119 | pane1 := entity.NewPane(entity.PaneID("pane-1")) | 120 | pane1 := entity.NewPane(entity.PaneID("pane-1")) | ||
| @@ -162,7 +163,9 @@ func TestSetActivePaneID_UpdatesStyling(t *testing.T) { | |||||
| 162 | mockPaned.EXPECT().SetShrinkEndChild(false).Once() | 163 | mockPaned.EXPECT().SetShrinkEndChild(false).Once() | ||
| 163 | mockPaned.EXPECT().SetStartChild(mockOverlay1).Once() | 164 | mockPaned.EXPECT().SetStartChild(mockOverlay1).Once() | ||
| 164 | mockPaned.EXPECT().SetEndChild(mockOverlay2).Once() | 165 | mockPaned.EXPECT().SetEndChild(mockOverlay2).Once() | ||
| 166 | + | mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once() | |||
| 165 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | 167 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | ||
| 168 | + | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | |||
| 166 | 169 | ||||
| 167 | mockBox.EXPECT().Append(mockPaned).Once() | 170 | mockBox.EXPECT().Append(mockPaned).Once() | ||
| 168 | 171 | ||||
| @@ -206,7 +209,7 @@ func TestSetActivePaneID_InvalidID_ReturnsError(t *testing.T) { | |||||
| 206 | mockBox.EXPECT().SetHexpand(true).Once() | 209 | mockBox.EXPECT().SetHexpand(true).Once() | ||
| 207 | mockBox.EXPECT().SetVexpand(true).Once() | 210 | mockBox.EXPECT().SetVexpand(true).Once() | ||
| 208 | 211 | ||||
| 209 | - | wv := component.NewWorkspaceView(mockFactory) | 212 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) |
| 210 | 213 | ||||
| 211 | // Act | 214 | // Act | ||
| 212 | err := wv.SetActivePaneID(entity.PaneID("non-existent")) | 215 | err := wv.SetActivePaneID(entity.PaneID("non-existent")) | ||
| @@ -226,7 +229,7 @@ func TestGetPaneView_Existing(t *testing.T) { | |||||
| 226 | mockBox.EXPECT().SetHexpand(true).Once() | 229 | mockBox.EXPECT().SetHexpand(true).Once() | ||
| 227 | mockBox.EXPECT().SetVexpand(true).Once() | 230 | mockBox.EXPECT().SetVexpand(true).Once() | ||
| 228 | 231 | ||||
| 229 | - | wv := component.NewWorkspaceView(mockFactory) | 232 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) |
| 230 | 233 | ||||
| 231 | // PaneView creation | 234 | // PaneView creation | ||
| 232 | mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once() | 235 | mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once() | ||
| @@ -269,7 +272,7 @@ func TestGetPaneView_NonExistent(t *testing.T) { | |||||
| 269 | mockBox.EXPECT().SetHexpand(true).Once() | 272 | mockBox.EXPECT().SetHexpand(true).Once() | ||
| 270 | mockBox.EXPECT().SetVexpand(true).Once() | 273 | mockBox.EXPECT().SetVexpand(true).Once() | ||
| 271 | 274 | ||||
| 272 | - | wv := component.NewWorkspaceView(mockFactory) | 275 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) |
| 273 | 276 | ||||
| 274 | // Act | 277 | // Act | ||
| 275 | result := wv.GetPaneView(entity.PaneID("non-existent")) | 278 | result := wv.GetPaneView(entity.PaneID("non-existent")) | ||
| @@ -292,7 +295,7 @@ func TestGetPaneIDs_ReturnsAllIDs(t *testing.T) { | |||||
| 292 | mockBox.EXPECT().SetHexpand(true).Once() | 295 | mockBox.EXPECT().SetHexpand(true).Once() | ||
| 293 | mockBox.EXPECT().SetVexpand(true).Once() | 296 | mockBox.EXPECT().SetVexpand(true).Once() | ||
| 294 | 297 | ||||
| 295 | - | wv := component.NewWorkspaceView(mockFactory) | 298 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) |
| 296 | 299 | ||||
| 297 | // First pane view | 300 | // First pane view | ||
| 298 | mockFactory.EXPECT().NewOverlay().Return(mockOverlay1).Once() | 301 | mockFactory.EXPECT().NewOverlay().Return(mockOverlay1).Once() | ||
| @@ -328,7 +331,9 @@ func TestGetPaneIDs_ReturnsAllIDs(t *testing.T) { | |||||
| 328 | mockPaned.EXPECT().SetShrinkEndChild(false).Once() | 331 | mockPaned.EXPECT().SetShrinkEndChild(false).Once() | ||
| 329 | mockPaned.EXPECT().SetStartChild(mockOverlay1).Once() | 332 | mockPaned.EXPECT().SetStartChild(mockOverlay1).Once() | ||
| 330 | mockPaned.EXPECT().SetEndChild(mockOverlay2).Once() | 333 | mockPaned.EXPECT().SetEndChild(mockOverlay2).Once() | ||
| 334 | + | mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once() | |||
| 331 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | 335 | mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once() | ||
| 336 | + | mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once() | |||
| 332 | 337 | ||||
| 333 | mockBox.EXPECT().Append(mockPaned).Once() | 338 | mockBox.EXPECT().Append(mockPaned).Once() | ||
| 334 | mockBorderBox1.EXPECT().AddCssClass("active-pane").Once() | 339 | mockBorderBox1.EXPECT().AddCssClass("active-pane").Once() | ||
| @@ -371,7 +376,7 @@ func TestWidget_ReturnsContainer(t *testing.T) { | |||||
| 371 | mockBox.EXPECT().SetHexpand(true).Once() | 376 | mockBox.EXPECT().SetHexpand(true).Once() | ||
| 372 | mockBox.EXPECT().SetVexpand(true).Once() | 377 | mockBox.EXPECT().SetVexpand(true).Once() | ||
| 373 | 378 | ||||
| 374 | - | wv := component.NewWorkspaceView(mockFactory) | 379 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) |
| 375 | 380 | ||||
| 376 | // Act | 381 | // Act | ||
| 377 | widget := wv.Widget() | 382 | widget := wv.Widget() | ||
| @@ -389,7 +394,7 @@ func TestSetOnPaneFocused_SetsCallback(t *testing.T) { | |||||
| 389 | mockBox.EXPECT().SetHexpand(true).Once() | 394 | mockBox.EXPECT().SetHexpand(true).Once() | ||
| 390 | mockBox.EXPECT().SetVexpand(true).Once() | 395 | mockBox.EXPECT().SetVexpand(true).Once() | ||
| 391 | 396 | ||||
| 392 | - | wv := component.NewWorkspaceView(mockFactory) | 397 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) |
| 393 | 398 | ||||
| 394 | callback := func(paneID entity.PaneID) {} | 399 | callback := func(paneID entity.PaneID) {} | ||
| 395 | 400 | ||||
| @@ -412,7 +417,7 @@ func TestSetWebViewWidget_AttachesToPane(t *testing.T) { | |||||
| 412 | mockBox.EXPECT().SetHexpand(true).Once() | 417 | mockBox.EXPECT().SetHexpand(true).Once() | ||
| 413 | mockBox.EXPECT().SetVexpand(true).Once() | 418 | mockBox.EXPECT().SetVexpand(true).Once() | ||
| 414 | 419 | ||||
| 415 | - | wv := component.NewWorkspaceView(mockFactory) | 420 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) |
| 416 | 421 | ||||
| 417 | // PaneView creation | 422 | // PaneView creation | ||
| 418 | mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once() | 423 | mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once() | ||
| @@ -459,7 +464,7 @@ func TestSetWebViewWidget_NonExistentPane_ReturnsError(t *testing.T) { | |||||
| 459 | mockBox.EXPECT().SetHexpand(true).Once() | 464 | mockBox.EXPECT().SetHexpand(true).Once() | ||
| 460 | mockBox.EXPECT().SetVexpand(true).Once() | 465 | mockBox.EXPECT().SetVexpand(true).Once() | ||
| 461 | 466 | ||||
| 462 | - | wv := component.NewWorkspaceView(mockFactory) | 467 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) |
| 463 | 468 | ||||
| 464 | // Act | 469 | // Act | ||
| 465 | err := wv.SetWebViewWidget(entity.PaneID("non-existent"), mockWebView) | 470 | err := wv.SetWebViewWidget(entity.PaneID("non-existent"), mockWebView) | ||
| @@ -479,7 +484,7 @@ func TestFocusPane_DelegatesToPaneView(t *testing.T) { | |||||
| 479 | mockBox.EXPECT().SetHexpand(true).Once() | 484 | mockBox.EXPECT().SetHexpand(true).Once() | ||
| 480 | mockBox.EXPECT().SetVexpand(true).Once() | 485 | mockBox.EXPECT().SetVexpand(true).Once() | ||
| 481 | 486 | ||||
| 482 | - | wv := component.NewWorkspaceView(mockFactory) | 487 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) |
| 483 | 488 | ||||
| 484 | // PaneView creation | 489 | // PaneView creation | ||
| 485 | mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once() | 490 | mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once() | ||
| @@ -522,7 +527,7 @@ func TestFocusPane_NonExistentPane_ReturnsFalse(t *testing.T) { | |||||
| 522 | mockBox.EXPECT().SetHexpand(true).Once() | 527 | mockBox.EXPECT().SetHexpand(true).Once() | ||
| 523 | mockBox.EXPECT().SetVexpand(true).Once() | 528 | mockBox.EXPECT().SetVexpand(true).Once() | ||
| 524 | 529 | ||||
| 525 | - | wv := component.NewWorkspaceView(mockFactory) | 530 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) |
| 526 | 531 | ||||
| 527 | // Act | 532 | // Act | ||
| 528 | result := wv.FocusPane(entity.PaneID("non-existent")) | 533 | result := wv.FocusPane(entity.PaneID("non-existent")) | ||
| @@ -540,7 +545,7 @@ func TestRebuild_NilWorkspace_ReturnsError(t *testing.T) { | |||||
| 540 | mockBox.EXPECT().SetHexpand(true).Once() | 545 | mockBox.EXPECT().SetHexpand(true).Once() | ||
| 541 | mockBox.EXPECT().SetVexpand(true).Once() | 546 | mockBox.EXPECT().SetVexpand(true).Once() | ||
| 542 | 547 | ||||
| 543 | - | wv := component.NewWorkspaceView(mockFactory) | 548 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) |
| 544 | 549 | ||||
| 545 | // Act | 550 | // Act | ||
| 546 | err := wv.Rebuild() | 551 | err := wv.Rebuild() | ||
| @@ -558,7 +563,7 @@ func TestTreeRenderer_ReturnsRenderer(t *testing.T) { | |||||
| 558 | mockBox.EXPECT().SetHexpand(true).Once() | 563 | mockBox.EXPECT().SetHexpand(true).Once() | ||
| 559 | mockBox.EXPECT().SetVexpand(true).Once() | 564 | mockBox.EXPECT().SetVexpand(true).Once() | ||
| 560 | 565 | ||||
| 561 | - | wv := component.NewWorkspaceView(mockFactory) | 566 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) |
| 562 | 567 | ||||
| 563 | // Act | 568 | // Act | ||
| 564 | renderer := wv.TreeRenderer() | 569 | renderer := wv.TreeRenderer() | ||
| @@ -578,7 +583,7 @@ func TestWorkspace_ReturnsCurrentWorkspace(t *testing.T) { | |||||
| 578 | mockBox.EXPECT().SetHexpand(true).Once() | 583 | mockBox.EXPECT().SetHexpand(true).Once() | ||
| 579 | mockBox.EXPECT().SetVexpand(true).Once() | 584 | mockBox.EXPECT().SetVexpand(true).Once() | ||
| 580 | 585 | ||||
| 581 | - | wv := component.NewWorkspaceView(mockFactory) | 586 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) |
| 582 | 587 | ||||
| 583 | // PaneView creation | 588 | // PaneView creation | ||
| 584 | mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once() | 589 | mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once() | ||
| @@ -621,7 +626,7 @@ func TestWorkspace_NilBeforeSet(t *testing.T) { | |||||
| 621 | mockBox.EXPECT().SetHexpand(true).Once() | 626 | mockBox.EXPECT().SetHexpand(true).Once() | ||
| 622 | mockBox.EXPECT().SetVexpand(true).Once() | 627 | mockBox.EXPECT().SetVexpand(true).Once() | ||
| 623 | 628 | ||||
| 624 | - | wv := component.NewWorkspaceView(mockFactory) | 629 | + | wv := component.NewWorkspaceView(mockFactory, zerolog.Nop()) |
| 625 | 630 | ||||
| 626 | // Act | 631 | // Act | ||
| 627 | result := wv.Workspace() | 632 | result := wv.Workspace() | ||
M internal/ui/deps.go +8 -4
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | 9 | "github.com/bnema/dumber/internal/domain/repository" | |
| 10 | 10 | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 11 | 11 | "github.com/bnema/dumber/internal/infrastructure/webkit" | |
| 12 | + | "github.com/bnema/dumber/internal/ui/component" | |
| 12 | 13 | "github.com/rs/zerolog" | |
| 13 | 14 | ) | |
| 14 | 15 | ||
| @@ -21,10 +22,13 @@ type Dependencies struct { | |||
| 21 | 22 | Logger *zerolog.Logger | |
| 22 | 23 | ||
| 23 | 24 | // WebKit infrastructure | |
| 24 | - | WebContext *webkit.WebKitContext | |
| 25 | - | Pool port.WebViewPool | |
| 26 | - | Factory port.WebViewFactory | |
| 27 | - | Settings *webkit.SettingsManager | |
| 25 | + | WebContext *webkit.WebKitContext | |
| 26 | + | Pool *webkit.WebViewPool | |
| 27 | + | Factory port.WebViewFactory | |
| 28 | + | Settings *webkit.SettingsManager | |
| 29 | + | Injector *webkit.ContentInjector | |
| 30 | + | MessageRouter *webkit.MessageRouter | |
| 31 | + | Overlay *component.OverlayController | |
| 28 | 32 | ||
| 29 | 33 | // Repositories | |
| 30 | 34 | HistoryRepo repository.HistoryRepository | |
| @@ -9,6 +9,7 @@ import ( | |||||
| 9 | "github.com/bnema/dumber/internal/domain/repository" | 9 | "github.com/bnema/dumber/internal/domain/repository" | ||
| 10 | "github.com/bnema/dumber/internal/infrastructure/config" | 10 | "github.com/bnema/dumber/internal/infrastructure/config" | ||
| 11 | "github.com/bnema/dumber/internal/infrastructure/webkit" | 11 | "github.com/bnema/dumber/internal/infrastructure/webkit" | ||
| 12 | + | "github.com/bnema/dumber/internal/ui/component" | |||
| 12 | "github.com/rs/zerolog" | 13 | "github.com/rs/zerolog" | ||
| 13 | ) | 14 | ) | ||
| 14 | 15 | ||||
| @@ -21,10 +22,13 @@ type Dependencies struct { | |||||
| 21 | Logger *zerolog.Logger | 22 | Logger *zerolog.Logger | ||
| 22 | 23 | ||||
| 23 | // WebKit infrastructure | 24 | // WebKit infrastructure | ||
| 24 | - | WebContext *webkit.WebKitContext | 25 | + | WebContext *webkit.WebKitContext |
| 25 | - | Pool port.WebViewPool | 26 | + | Pool *webkit.WebViewPool |
| 26 | - | Factory port.WebViewFactory | 27 | + | Factory port.WebViewFactory |
| 27 | - | Settings *webkit.SettingsManager | 28 | + | Settings *webkit.SettingsManager |
| 29 | + | Injector *webkit.ContentInjector | |||
| 30 | + | MessageRouter *webkit.MessageRouter | |||
| 31 | + | Overlay *component.OverlayController | |||
| 28 | 32 | ||||
| 29 | // Repositories | 33 | // Repositories | ||
| 30 | HistoryRepo repository.HistoryRepository | 34 | HistoryRepo repository.HistoryRepository | ||
M internal/ui/input/shortcuts.go +1 -3
| @@ -204,10 +204,8 @@ func (s *ShortcutSet) buildPaneModeShortcuts(cfg *config.WorkspaceConfig) { | |||
| 204 | 204 | func mapConfigAction(configAction string) Action { | |
| 205 | 205 | switch configAction { | |
| 206 | 206 | // Mode management | |
| 207 | - | case "cancel": | |
| 207 | + | case "cancel", "confirm": | |
| 208 | 208 | return ActionExitMode | |
| 209 | - | case "confirm": | |
| 210 | - | return "" // No action for confirm yet | |
| 211 | 209 | ||
| 212 | 210 | // Tab actions | |
| 213 | 211 | case "new-tab": | |
| @@ -204,10 +204,8 @@ func (s *ShortcutSet) buildPaneModeShortcuts(cfg *config.WorkspaceConfig) { | |||||
| 204 | func mapConfigAction(configAction string) Action { | 204 | func mapConfigAction(configAction string) Action { | ||
| 205 | switch configAction { | 205 | switch configAction { | ||
| 206 | // Mode management | 206 | // Mode management | ||
| 207 | - | case "cancel": | 207 | + | case "cancel", "confirm": |
| 208 | return ActionExitMode | 208 | return ActionExitMode | ||
| 209 | - | case "confirm": | |||
| 210 | - | return "" // No action for confirm yet | |||
| 211 | 209 | ||||
| 212 | // Tab actions | 210 | // Tab actions | ||
| 213 | case "new-tab": | 211 | case "new-tab": | ||
M internal/ui/window/main_window.go +3 -0
| @@ -50,6 +50,7 @@ func New(app *gtk.Application, cfg *config.Config, logger *zerolog.Logger) (*Mai | |||
| 50 | 50 | } | |
| 51 | 51 | mw.rootBox.SetHexpand(true) | |
| 52 | 52 | mw.rootBox.SetVexpand(true) | |
| 53 | + | mw.rootBox.SetVisible(true) | |
| 53 | 54 | ||
| 54 | 55 | // Create tab bar | |
| 55 | 56 | mw.tabBar = component.NewTabBar() | |
| @@ -69,6 +70,7 @@ func New(app *gtk.Application, cfg *config.Config, logger *zerolog.Logger) (*Mai | |||
| 69 | 70 | } | |
| 70 | 71 | mw.contentArea.SetHexpand(true) | |
| 71 | 72 | mw.contentArea.SetVexpand(true) | |
| 73 | + | mw.contentArea.SetVisible(true) | |
| 72 | 74 | mw.contentArea.AddCssClass("content-area") | |
| 73 | 75 | ||
| 74 | 76 | // Assemble layout based on tab bar position | |
| @@ -130,6 +132,7 @@ func (mw *MainWindow) SetContent(widget *gtk.Widget) { | |||
| 130 | 132 | // so we'd need to track children. For now, just append. | |
| 131 | 133 | // TODO: Track children for proper content swapping | |
| 132 | 134 | if widget != nil { | |
| 135 | + | widget.SetVisible(true) | |
| 133 | 136 | mw.contentArea.Append(widget) | |
| 134 | 137 | } | |
| 135 | 138 | } | |
| @@ -50,6 +50,7 @@ func New(app *gtk.Application, cfg *config.Config, logger *zerolog.Logger) (*Mai | |||||
| 50 | } | 50 | } | ||
| 51 | mw.rootBox.SetHexpand(true) | 51 | mw.rootBox.SetHexpand(true) | ||
| 52 | mw.rootBox.SetVexpand(true) | 52 | mw.rootBox.SetVexpand(true) | ||
| 53 | + | mw.rootBox.SetVisible(true) | |||
| 53 | 54 | ||||
| 54 | // Create tab bar | 55 | // Create tab bar | ||
| 55 | mw.tabBar = component.NewTabBar() | 56 | mw.tabBar = component.NewTabBar() | ||
| @@ -69,6 +70,7 @@ func New(app *gtk.Application, cfg *config.Config, logger *zerolog.Logger) (*Mai | |||||
| 69 | } | 70 | } | ||
| 70 | mw.contentArea.SetHexpand(true) | 71 | mw.contentArea.SetHexpand(true) | ||
| 71 | mw.contentArea.SetVexpand(true) | 72 | mw.contentArea.SetVexpand(true) | ||
| 73 | + | mw.contentArea.SetVisible(true) | |||
| 72 | mw.contentArea.AddCssClass("content-area") | 74 | mw.contentArea.AddCssClass("content-area") | ||
| 73 | 75 | ||||
| 74 | // Assemble layout based on tab bar position | 76 | // Assemble layout based on tab bar position | ||
| @@ -130,6 +132,7 @@ func (mw *MainWindow) SetContent(widget *gtk.Widget) { | |||||
| 130 | // so we'd need to track children. For now, just append. | 132 | // so we'd need to track children. For now, just append. | ||
| 131 | // TODO: Track children for proper content swapping | 133 | // TODO: Track children for proper content swapping | ||
| 132 | if widget != nil { | 134 | if widget != nil { | ||
| 135 | + | widget.SetVisible(true) | |||
| 133 | mw.contentArea.Append(widget) | 136 | mw.contentArea.Append(widget) | ||
| 134 | } | 137 | } | ||
| 135 | } | 138 | } | ||