detached a970f83

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
22
33 import (
44 "context"
5+ "errors"
6+ "fmt"
57 "os"
68 "sync"
79
10+ "github.com/jwijenbergh/puregotk/v4/gio"
11+ "github.com/jwijenbergh/puregotk/v4/gtk"
12+
813 "github.com/bnema/dumber/internal/application/usecase"
914 "github.com/bnema/dumber/internal/domain/entity"
15+ "github.com/bnema/dumber/internal/infrastructure/webkit"
1016 "github.com/bnema/dumber/internal/logging"
1117 "github.com/bnema/dumber/internal/ui/component"
18+ uihandler "github.com/bnema/dumber/internal/ui/handler"
1219 "github.com/bnema/dumber/internal/ui/input"
1320 "github.com/bnema/dumber/internal/ui/layout"
1421 "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"
1822 )
1923
2024 const (
@@ -40,14 +44,20 @@ type App struct {
4044 // Input handling
4145 keyboardHandler *input.KeyboardHandler
4246
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+
4355 // ID generator for tabs/panes
4456 idCounter uint64
4557 idMu sync.Mutex
4658
47- // Lifecycle
48- ctx context.Context
49- cancel context.CancelFunc
50- logger *zerolog.Logger
59+ // lifecycle
60+ cancel context.CancelCauseFunc
5161 }
5262
5363 // New creates a new App with the given dependencies.
@@ -56,8 +66,7 @@ func New(deps *Dependencies) (*App, error) {
5666 return nil, err
5767 }
5868
59- ctx, cancel := context.WithCancel(deps.Ctx)
60- logger := logging.FromContext(ctx)
69+ ctx, cancel := context.WithCancelCause(deps.Ctx)
6170
6271 app := &App{
6372 deps: deps,
@@ -65,9 +74,19 @@ func New(deps *Dependencies) (*App, error) {
6574 tabsUC: deps.TabsUC,
6675 panesUC: deps.PanesUC,
6776 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),
6983 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)
7190 }
7291
7392 return app, nil
@@ -75,43 +94,50 @@ func New(deps *Dependencies) (*App, error) {
7594
7695 // Run starts the GTK application and blocks until it exits.
7796 // 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")
80100
81101 a.gtkApp = gtk.NewApplication(AppID, gio.GApplicationFlagsNoneValue)
82102 if a.gtkApp == nil {
83- a.logger.Error().Msg("failed to create GTK application")
103+ log.Error().Msg("failed to create GTK application")
84104 return 1
85105 }
86106 defer a.gtkApp.Unref()
87107
88108 // Connect activate signal
89109 activateCb := func(_ gio.Application) {
90- a.onActivate()
110+ a.onActivate(ctx)
91111 }
92112 a.gtkApp.ConnectActivate(&activateCb)
93113
94114 // Connect shutdown signal
95115 shutdownCb := func(_ gio.Application) {
96- a.onShutdown()
116+ a.onShutdown(ctx)
97117 }
98118 a.gtkApp.ConnectShutdown(&shutdownCb)
99119
100- a.logger.Info().Msg("starting GTK main loop")
120+ log.Info().Msg("starting GTK main loop")
101121 code := a.gtkApp.Run(len(args), args)
102122
103123 return code
104124 }
105125
106126 // 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+ }
109135
110136 // Create the main window
111137 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)
113139 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")
115141 return
116142 }
117143
@@ -120,52 +146,62 @@ func (a *App) onActivate() {
120146
121147 // Create keyboard handler
122148 a.keyboardHandler = input.NewKeyboardHandler(
123- a.ctx,
149+ ctx,
124150 &a.deps.Config.Workspace,
125- a.logger,
151+ a.deps.Logger,
126152 )
127153 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+ })
129158 a.keyboardHandler.AttachTo(a.mainWindow.Window())
130159
160+ if err := a.registerMessageHandlers(ctx); err != nil {
161+ log.Warn().Err(err).Msg("failed to register message handlers")
162+ }
163+
131164 // Create an initial tab
132- a.createInitialTab()
165+ a.createInitialTab(ctx)
133166
134167 // Show the window
135168 a.mainWindow.Show()
136169
137- a.logger.Info().Msg("main window displayed")
170+ log.Info().Msg("main window displayed")
138171 }
139172
140173 // 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")
143177
144178 // Cancel context to signal all goroutines
145- a.cancel()
179+ a.cancel(errors.New("application shutdown"))
146180
147181 // Cleanup resources
148182 if a.deps.Pool != nil {
149183 a.deps.Pool.Close()
150184 }
151185
152- a.logger.Info().Msg("application shutdown complete")
186+ log.Info().Msg("application shutdown complete")
153187 }
154188
155189 // 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+
157193 if a.tabsUC == nil {
158194 // Create use case if not injected
159195 a.tabsUC = usecase.NewManageTabsUseCase(a.generateID)
160196 }
161197
162- output, err := a.tabsUC.Create(a.ctx, usecase.CreateTabInput{
198+ output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{
163199 TabList: a.tabs,
164200 Name: "",
165- InitialURL: "about:blank",
201+ InitialURL: "https://duckduckgo.com",
166202 })
167203 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")
169205 return
170206 }
171207
@@ -176,11 +212,9 @@ func (a *App) createInitialTab() {
176212 }
177213
178214 // Create workspace view for this tab
179- a.createWorkspaceView(output.Tab)
215+ a.createWorkspaceView(ctx, output.Tab)
180216
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")
184218 }
185219
186220 // generateID generates a unique ID for tabs and panes.
@@ -209,7 +243,7 @@ func (a *App) Quit() {
209243 }
210244
211245 // 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 {
213247 app, err := New(deps)
214248 if err != nil {
215249 if deps.Logger != nil {
@@ -217,96 +251,93 @@ func RunWithArgs(deps *Dependencies) int {
217251 }
218252 return 1
219253 }
220- return app.Run(os.Args)
254+ return app.Run(ctx, os.Args)
221255 }
222256
223257 // handleKeyboardAction dispatches keyboard actions to appropriate handlers.
224258 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")
228261
229262 switch action {
230263 // Tab actions
231264 case input.ActionNewTab:
232- return a.handleNewTab()
265+ return a.handleNewTab(ctx)
233266 case input.ActionCloseTab:
234- return a.handleCloseTab()
267+ return a.handleCloseTab(ctx)
235268 case input.ActionNextTab:
236- return a.handleNextTab()
269+ return a.handleNextTab(ctx)
237270 case input.ActionPreviousTab:
238- return a.handlePreviousTab()
271+ return a.handlePreviousTab(ctx)
239272
240273 // Pane actions
241274 case input.ActionSplitRight:
242- return a.handlePaneSplit(usecase.SplitRight)
275+ return a.handlePaneSplit(ctx, usecase.SplitRight)
243276 case input.ActionSplitLeft:
244- return a.handlePaneSplit(usecase.SplitLeft)
277+ return a.handlePaneSplit(ctx, usecase.SplitLeft)
245278 case input.ActionSplitUp:
246- return a.handlePaneSplit(usecase.SplitUp)
279+ return a.handlePaneSplit(ctx, usecase.SplitUp)
247280 case input.ActionSplitDown:
248- return a.handlePaneSplit(usecase.SplitDown)
281+ return a.handlePaneSplit(ctx, usecase.SplitDown)
249282 case input.ActionClosePane:
250- return a.handleClosePane()
283+ return a.handleClosePane(ctx)
251284 case input.ActionFocusRight:
252- return a.handlePaneFocus(usecase.NavRight)
285+ return a.handlePaneFocus(ctx, usecase.NavRight)
253286 case input.ActionFocusLeft:
254- return a.handlePaneFocus(usecase.NavLeft)
287+ return a.handlePaneFocus(ctx, usecase.NavLeft)
255288 case input.ActionFocusUp:
256- return a.handlePaneFocus(usecase.NavUp)
289+ return a.handlePaneFocus(ctx, usecase.NavUp)
257290 case input.ActionFocusDown:
258- return a.handlePaneFocus(usecase.NavDown)
291+ return a.handlePaneFocus(ctx, usecase.NavDown)
259292
260293 // Navigation (stub implementations for now)
261294 case input.ActionGoBack:
262- a.logger.Debug().Msg("go back action (not yet implemented)")
295+ log.Debug().Msg("go back action (not yet implemented)")
263296 case input.ActionGoForward:
264- a.logger.Debug().Msg("go forward action (not yet implemented)")
297+ log.Debug().Msg("go forward action (not yet implemented)")
265298 case input.ActionReload:
266- a.logger.Debug().Msg("reload action (not yet implemented)")
299+ log.Debug().Msg("reload action (not yet implemented)")
267300 case input.ActionHardReload:
268- a.logger.Debug().Msg("hard reload action (not yet implemented)")
301+ log.Debug().Msg("hard reload action (not yet implemented)")
269302
270303 // Zoom (stub implementations for now)
271304 case input.ActionZoomIn:
272- a.logger.Debug().Msg("zoom in action (not yet implemented)")
305+ log.Debug().Msg("zoom in action (not yet implemented)")
273306 case input.ActionZoomOut:
274- a.logger.Debug().Msg("zoom out action (not yet implemented)")
307+ log.Debug().Msg("zoom out action (not yet implemented)")
275308 case input.ActionZoomReset:
276- a.logger.Debug().Msg("zoom reset action (not yet implemented)")
309+ log.Debug().Msg("zoom reset action (not yet implemented)")
277310
278311 // UI
279312 case input.ActionOpenOmnibox:
280- a.logger.Debug().Msg("open omnibox action (not yet implemented)")
313+ return a.openOmnibox(ctx)
281314 case input.ActionOpenDevTools:
282- a.logger.Debug().Msg("open devtools action (not yet implemented)")
315+ return a.openDevTools(ctx)
283316 case input.ActionToggleFullscreen:
284- a.logger.Debug().Msg("toggle fullscreen action (not yet implemented)")
317+ log.Debug().Msg("toggle fullscreen action (not yet implemented)")
285318
286319 // Application
287320 case input.ActionQuit:
288321 a.Quit()
289322
290323 default:
291- a.logger.Warn().Str("action", string(action)).Msg("unhandled keyboard action")
324+ log.Warn().Str("action", string(action)).Msg("unhandled keyboard action")
292325 }
293326
294327 return nil
295328 }
296329
297330 // 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")
305334 }
306335
307336 // 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{
310341 TabList: a.tabs,
311342 Name: "",
312343 InitialURL: "about:blank",
@@ -320,21 +351,19 @@ func (a *App) handleNewTab() error {
320351 a.mainWindow.TabBar().SetActive(output.Tab.ID)
321352 }
322353
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")
326355
327356 return nil
328357 }
329358
330359 // handleCloseTab closes the active tab.
331-func (a *App) handleCloseTab() error {
360+func (a *App) handleCloseTab(ctx context.Context) error {
332361 activeID := a.tabs.ActiveTabID
333362 if activeID == "" {
334363 return nil
335364 }
336365
337- wasLast, err := a.tabsUC.Close(a.ctx, a.tabs, activeID)
366+ wasLast, err := a.tabsUC.Close(ctx, a.tabs, activeID)
338367 if err != nil {
339368 return err
340369 }
@@ -357,8 +386,8 @@ func (a *App) handleCloseTab() error {
357386 }
358387
359388 // 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 {
362391 return err
363392 }
364393
@@ -370,8 +399,8 @@ func (a *App) handleNextTab() error {
370399 }
371400
372401 // 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 {
375404 return err
376405 }
377406
@@ -383,25 +412,30 @@ func (a *App) handlePreviousTab() error {
383412 }
384413
385414 // 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+
387418 if a.widgetFactory == nil {
388- a.logger.Error().Msg("widget factory not initialized")
419+ log.Error().Msg("widget factory not initialized")
389420 return
390421 }
391422
392423 // Create workspace view
393- wsView := component.NewWorkspaceView(a.widgetFactory)
424+ wsView := component.NewWorkspaceView(a.widgetFactory, *a.deps.Logger)
394425 if wsView == nil {
395- a.logger.Error().Msg("failed to create workspace view")
426+ log.Error().Msg("failed to create workspace view")
396427 return
397428 }
398429
399430 // Set the workspace
400431 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")
402433 return
403434 }
404435
436+ // Ensure WebViews are attached to panes
437+ a.attachWorkspaceWebViews(ctx, tab.Workspace, wsView)
438+
405439 // Store in map
406440 a.workspaceViews[tab.ID] = wsView
407441
@@ -409,15 +443,14 @@ func (a *App) createWorkspaceView(tab *entity.Tab) {
409443 if a.mainWindow != nil {
410444 widget := wsView.Widget()
411445 if widget != nil {
412- if gtkWidget := widget.GtkWidget(); gtkWidget != nil {
446+ gtkWidget := widget.GtkWidget()
447+ if gtkWidget != nil {
413448 a.mainWindow.SetContent(gtkWidget)
414449 }
415450 }
416451 }
417452
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")
421454 }
422455
423456 // activeWorkspace returns the workspace of the active tab.
@@ -439,31 +472,33 @@ func (a *App) activeWorkspaceView() *component.WorkspaceView {
439472 }
440473
441474 // 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+
443478 if a.panesUC == nil {
444- a.logger.Warn().Msg("panes use case not available")
479+ log.Warn().Msg("panes use case not available")
445480 return nil
446481 }
447482
448483 ws := a.activeWorkspace()
449484 if ws == nil {
450- a.logger.Warn().Msg("no active workspace")
485+ log.Warn().Msg("no active workspace")
451486 return nil
452487 }
453488
454489 activePane := ws.ActivePane()
455490 if activePane == nil {
456- a.logger.Warn().Msg("no active pane to split")
491+ log.Warn().Msg("no active pane to split")
457492 return nil
458493 }
459494
460- output, err := a.panesUC.Split(a.ctx, usecase.SplitPaneInput{
495+ output, err := a.panesUC.Split(ctx, usecase.SplitPaneInput{
461496 Workspace: ws,
462497 TargetPane: activePane,
463498 Direction: direction,
464499 })
465500 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")
467502 return err
468503 }
469504
@@ -474,45 +509,47 @@ func (a *App) handlePaneSplit(direction usecase.SplitDirection) error {
474509 wsView := a.activeWorkspaceView()
475510 if wsView != nil {
476511 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")
478513 }
514+ a.attachWorkspaceWebViews(ctx, ws, wsView)
515+ wsView.FocusPane(ws.ActivePaneID)
479516 }
480517
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")
485519
486520 return nil
487521 }
488522
489523 // 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+
491527 if a.panesUC == nil {
492- a.logger.Warn().Msg("panes use case not available")
528+ log.Warn().Msg("panes use case not available")
493529 return nil
494530 }
495531
496532 ws := a.activeWorkspace()
497533 if ws == nil {
498- a.logger.Warn().Msg("no active workspace")
534+ log.Warn().Msg("no active workspace")
499535 return nil
500536 }
501537
502538 activePane := ws.ActivePane()
503539 if activePane == nil {
504- a.logger.Warn().Msg("no active pane to close")
540+ log.Warn().Msg("no active pane to close")
505541 return nil
506542 }
543+ closingPaneID := activePane.Pane.ID
507544
508545 // Don't close the last pane - close the tab instead
509546 if ws.PaneCount() <= 1 {
510- return a.handleCloseTab()
547+ return a.handleCloseTab(ctx)
511548 }
512549
513- _, err := a.panesUC.Close(a.ctx, ws, activePane)
550+ _, err := a.panesUC.Close(ctx, ws, activePane)
514551 if err != nil {
515- a.logger.Error().Err(err).Msg("failed to close pane")
552+ log.Error().Err(err).Msg("failed to close pane")
516553 return err
517554 }
518555
@@ -520,35 +557,40 @@ func (a *App) handleClosePane() error {
520557 wsView := a.activeWorkspaceView()
521558 if wsView != nil {
522559 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")
524561 }
562+ a.releasePaneWebView(ctx, closingPaneID)
563+ a.attachWorkspaceWebViews(ctx, ws, wsView)
564+ wsView.FocusPane(ws.ActivePaneID)
525565 }
526566
527- a.logger.Info().Msg("pane closed")
567+ log.Info().Msg("pane closed")
528568 return nil
529569 }
530570
531571 // 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+
533575 if a.panesUC == nil {
534- a.logger.Warn().Msg("panes use case not available")
576+ log.Warn().Msg("panes use case not available")
535577 return nil
536578 }
537579
538580 ws := a.activeWorkspace()
539581 if ws == nil {
540- a.logger.Warn().Msg("no active workspace")
582+ log.Warn().Msg("no active workspace")
541583 return nil
542584 }
543585
544- newPane, err := a.panesUC.NavigateFocus(a.ctx, ws, direction)
586+ newPane, err := a.panesUC.NavigateFocus(ctx, ws, direction)
545587 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")
547589 return err
548590 }
549591
550592 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")
552594 return nil
553595 }
554596
@@ -556,14 +598,184 @@ func (a *App) handlePaneFocus(direction usecase.NavigateDirection) error {
556598 wsView := a.activeWorkspaceView()
557599 if wsView != nil {
558600 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)
560604 }
561605 }
562606
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")
567608
568609 return nil
569610 }
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(&gtkView.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 handling44 // Input handling
41 keyboardHandler *input.KeyboardHandler45 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/panes55 // ID generator for tabs/panes
44 idCounter uint6456 idCounter uint64
45 idMu sync.Mutex57 idMu sync.Mutex
46 58
47- // Lifecycle59+ // lifecycle
48- ctx context.Context60+ 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, err66 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, nil92 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 1104 return 1
85 }105 }
86 defer a.gtkApp.Unref()106 defer a.gtkApp.Unref()
87 107
88 // Connect activate signal108 // 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 signal114 // 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 code123 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 window136 // Create the main window
111 var err error137 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 return141 return
116 }142 }
117 143
@@ -120,52 +146,62 @@ func (a *App) onActivate() {
120 146
121 // Create keyboard handler147 // 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 tab164 // Create an initial tab
132- a.createInitialTab()165+ a.createInitialTab(ctx)
133 166
134 // Show the window167 // 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 goroutines178 // Cancel context to signal all goroutines
145- a.cancel()179+ a.cancel(errors.New("application shutdown"))
146 180
147 // Cleanup resources181 // 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 injected194 // 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 return205 return
170 }206 }
171 207
@@ -176,11 +212,9 @@ func (a *App) createInitialTab() {
176 }212 }
177 213
178 // Create workspace view for this tab214 // 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 1252 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 actions263 // 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 actions273 // 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 // UI311 // 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 // Application319 // 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 nil327 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 nil356 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.ActiveTabID361 activeID := a.tabs.ActiveTabID
333 if activeID == "" {362 if activeID == "" {
334 return nil363 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 err368 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 err391 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 err404 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 return420 return
390 }421 }
391 422
392 // Create workspace view423 // 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 return427 return
397 }428 }
398 429
399 // Set the workspace430 // 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 return433 return
403 }434 }
404 435
436+ // Ensure WebViews are attached to panes
437+ a.attachWorkspaceWebViews(ctx, tab.Workspace, wsView)
438+
405 // Store in map439 // Store in map
406 a.workspaceViews[tab.ID] = wsView440 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 nil480 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 nil486 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 nil492 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 err502 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 nil520 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 nil529 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 nil535 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 nil541 return nil
506 }542 }
543+ closingPaneID := activePane.Pane.ID
507 544
508 // Don't close the last pane - close the tab instead545 // 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 err553 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 nil568 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 nil577 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 nil583 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 err589 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 nil594 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 nil609 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(&gtkView.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
3434 overlay := factory.NewOverlay()
3535 overlay.SetHexpand(true)
3636 overlay.SetVexpand(true)
37+ overlay.SetVisible(true)
3738
3839 // Set the WebView as the main child
3940 if webViewWidget != nil {
41+ webViewWidget.SetVisible(true)
4042 overlay.SetChild(webViewWidget)
4143 }
4244
@@ -44,13 +46,14 @@ func NewPaneView(factory layout.WidgetFactory, paneID entity.PaneID, webViewWidg
4446 // This is an empty box that gets styled via CSS to show a border
4547 borderBox := factory.NewBox(layout.OrientationVertical, 0)
4648 borderBox.SetCanFocus(false)
49+ borderBox.SetCanTarget(false) // Don't intercept pointer events - let them pass through to WebView
4750 borderBox.AddCssClass("pane-border")
4851 // Position border to cover entire pane
4952 borderBox.SetHexpand(true)
5053 borderBox.SetVexpand(true)
5154 overlay.AddOverlay(borderBox)
5255
53- // Don't let border intercept pointer events
56+ // Don't let border affect layout
5457 overlay.SetClipOverlay(borderBox, false)
5558 overlay.SetMeasureOverlay(borderBox, false)
5659
@@ -117,6 +120,7 @@ func (pv *PaneView) SetWebViewWidget(widget layout.Widget) {
117120 pv.webViewWidget = widget
118121
119122 if widget != nil {
123+ widget.SetVisible(true)
120124 pv.overlay.SetChild(widget)
121125 }
122126 }
@@ -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 child39 // 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 border46 // 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 pane51 // 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 events56+ // 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 = widget120 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 {
3838 // Configure the box
3939 tb.box.SetHexpand(true)
4040 tb.box.SetVexpand(false)
41+ tb.box.SetVisible(true)
4142 tb.box.AddCssClass("tab-bar")
4243
4344 return tb
@@ -38,6 +38,7 @@ func NewTabBar() *TabBar {
38 // Configure the box38 // 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 tb44 return tb
M internal/ui/component/workspace_view.go +7 -2
@@ -7,6 +7,7 @@ import (
77
88 "github.com/bnema/dumber/internal/domain/entity"
99 "github.com/bnema/dumber/internal/ui/layout"
10+ "github.com/rs/zerolog"
1011 )
1112
1213 // ErrNilWorkspace is returned when attempting to set a nil workspace.
@@ -21,6 +22,7 @@ type WorkspaceView struct {
2122 factory layout.WidgetFactory
2223 treeRenderer *layout.TreeRenderer
2324 container layout.BoxWidget
25+ logger zerolog.Logger
2426
2527 workspace *entity.Workspace
2628 paneViews map[entity.PaneID]*PaneView
@@ -65,19 +67,21 @@ func (a *paneViewFactoryAdapter) CreatePaneView(node *entity.PaneNode) layout.Wi
6567 }
6668
6769 // NewWorkspaceView creates a new workspace view.
68-func NewWorkspaceView(factory layout.WidgetFactory) *WorkspaceView {
70+func NewWorkspaceView(factory layout.WidgetFactory, logger zerolog.Logger) *WorkspaceView {
6971 container := factory.NewBox(layout.OrientationVertical, 0)
7072 container.SetHexpand(true)
7173 container.SetVexpand(true)
74+ container.SetVisible(true)
7275
7376 wv := &WorkspaceView{
7477 factory: factory,
7578 container: container,
79+ logger: logger.With().Str("component", "workspace-view").Logger(),
7680 paneViews: make(map[entity.PaneID]*PaneView),
7781 }
7882
7983 // 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)
8185
8286 return wv
8387 }
@@ -106,6 +110,7 @@ func (wv *WorkspaceView) SetWorkspace(ws *entity.Workspace) error {
106110 }
107111
108112 if widget != nil {
113+ widget.SetVisible(true)
109114 wv.container.Append(widget)
110115 }
111116 }
@@ -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.WidgetFactory22 factory layout.WidgetFactory
22 treeRenderer *layout.TreeRenderer23 treeRenderer *layout.TreeRenderer
23 container layout.BoxWidget24 container layout.BoxWidget
25+ logger zerolog.Logger
24 26
25 workspace *entity.Workspace27 workspace *entity.Workspace
26 paneViews map[entity.PaneID]*PaneView28 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 factory83 // 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 wv86 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
33 import (
44 "testing"
55
6+ "github.com/rs/zerolog"
67 "github.com/stretchr/testify/assert"
78 "github.com/stretchr/testify/mock"
89 "github.com/stretchr/testify/require"
@@ -23,7 +24,7 @@ func TestNewWorkspaceView_CreatesEmptyContainer(t *testing.T) {
2324 mockBox.EXPECT().SetVexpand(true).Once()
2425
2526 // Act
26- wv := component.NewWorkspaceView(mockFactory)
27+ wv := component.NewWorkspaceView(mockFactory, zerolog.Nop())
2728
2829 // Assert
2930 require.NotNil(t, wv)
@@ -40,7 +41,7 @@ func TestSetWorkspace_NilWorkspace_ReturnsError(t *testing.T) {
4041 mockBox.EXPECT().SetHexpand(true).Once()
4142 mockBox.EXPECT().SetVexpand(true).Once()
4243
43- wv := component.NewWorkspaceView(mockFactory)
44+ wv := component.NewWorkspaceView(mockFactory, zerolog.Nop())
4445
4546 // Act
4647 err := wv.SetWorkspace(nil)
@@ -59,7 +60,7 @@ func TestSetWorkspace_SinglePane_CreatesPaneView(t *testing.T) {
5960 mockBox.EXPECT().SetHexpand(true).Once()
6061 mockBox.EXPECT().SetVexpand(true).Once()
6162
62- wv := component.NewWorkspaceView(mockFactory)
63+ wv := component.NewWorkspaceView(mockFactory, zerolog.Nop())
6364
6465 // PaneView creation (via paneViewFactoryAdapter)
6566 mockOverlay := mocks.NewMockOverlayWidget(t)
@@ -113,7 +114,7 @@ func TestSetActivePaneID_UpdatesStyling(t *testing.T) {
113114 mockBox.EXPECT().SetHexpand(true).Once()
114115 mockBox.EXPECT().SetVexpand(true).Once()
115116
116- wv := component.NewWorkspaceView(mockFactory)
117+ wv := component.NewWorkspaceView(mockFactory, zerolog.Nop())
117118
118119 // Setup two panes
119120 pane1 := entity.NewPane(entity.PaneID("pane-1"))
@@ -162,7 +163,9 @@ func TestSetActivePaneID_UpdatesStyling(t *testing.T) {
162163 mockPaned.EXPECT().SetShrinkEndChild(false).Once()
163164 mockPaned.EXPECT().SetStartChild(mockOverlay1).Once()
164165 mockPaned.EXPECT().SetEndChild(mockOverlay2).Once()
166+ mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once()
165167 mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once()
168+ mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once()
166169
167170 mockBox.EXPECT().Append(mockPaned).Once()
168171
@@ -206,7 +209,7 @@ func TestSetActivePaneID_InvalidID_ReturnsError(t *testing.T) {
206209 mockBox.EXPECT().SetHexpand(true).Once()
207210 mockBox.EXPECT().SetVexpand(true).Once()
208211
209- wv := component.NewWorkspaceView(mockFactory)
212+ wv := component.NewWorkspaceView(mockFactory, zerolog.Nop())
210213
211214 // Act
212215 err := wv.SetActivePaneID(entity.PaneID("non-existent"))
@@ -226,7 +229,7 @@ func TestGetPaneView_Existing(t *testing.T) {
226229 mockBox.EXPECT().SetHexpand(true).Once()
227230 mockBox.EXPECT().SetVexpand(true).Once()
228231
229- wv := component.NewWorkspaceView(mockFactory)
232+ wv := component.NewWorkspaceView(mockFactory, zerolog.Nop())
230233
231234 // PaneView creation
232235 mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
@@ -269,7 +272,7 @@ func TestGetPaneView_NonExistent(t *testing.T) {
269272 mockBox.EXPECT().SetHexpand(true).Once()
270273 mockBox.EXPECT().SetVexpand(true).Once()
271274
272- wv := component.NewWorkspaceView(mockFactory)
275+ wv := component.NewWorkspaceView(mockFactory, zerolog.Nop())
273276
274277 // Act
275278 result := wv.GetPaneView(entity.PaneID("non-existent"))
@@ -292,7 +295,7 @@ func TestGetPaneIDs_ReturnsAllIDs(t *testing.T) {
292295 mockBox.EXPECT().SetHexpand(true).Once()
293296 mockBox.EXPECT().SetVexpand(true).Once()
294297
295- wv := component.NewWorkspaceView(mockFactory)
298+ wv := component.NewWorkspaceView(mockFactory, zerolog.Nop())
296299
297300 // First pane view
298301 mockFactory.EXPECT().NewOverlay().Return(mockOverlay1).Once()
@@ -328,7 +331,9 @@ func TestGetPaneIDs_ReturnsAllIDs(t *testing.T) {
328331 mockPaned.EXPECT().SetShrinkEndChild(false).Once()
329332 mockPaned.EXPECT().SetStartChild(mockOverlay1).Once()
330333 mockPaned.EXPECT().SetEndChild(mockOverlay2).Once()
334+ mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once()
331335 mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once()
336+ mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once()
332337
333338 mockBox.EXPECT().Append(mockPaned).Once()
334339 mockBorderBox1.EXPECT().AddCssClass("active-pane").Once()
@@ -371,7 +376,7 @@ func TestWidget_ReturnsContainer(t *testing.T) {
371376 mockBox.EXPECT().SetHexpand(true).Once()
372377 mockBox.EXPECT().SetVexpand(true).Once()
373378
374- wv := component.NewWorkspaceView(mockFactory)
379+ wv := component.NewWorkspaceView(mockFactory, zerolog.Nop())
375380
376381 // Act
377382 widget := wv.Widget()
@@ -389,7 +394,7 @@ func TestSetOnPaneFocused_SetsCallback(t *testing.T) {
389394 mockBox.EXPECT().SetHexpand(true).Once()
390395 mockBox.EXPECT().SetVexpand(true).Once()
391396
392- wv := component.NewWorkspaceView(mockFactory)
397+ wv := component.NewWorkspaceView(mockFactory, zerolog.Nop())
393398
394399 callback := func(paneID entity.PaneID) {}
395400
@@ -412,7 +417,7 @@ func TestSetWebViewWidget_AttachesToPane(t *testing.T) {
412417 mockBox.EXPECT().SetHexpand(true).Once()
413418 mockBox.EXPECT().SetVexpand(true).Once()
414419
415- wv := component.NewWorkspaceView(mockFactory)
420+ wv := component.NewWorkspaceView(mockFactory, zerolog.Nop())
416421
417422 // PaneView creation
418423 mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
@@ -459,7 +464,7 @@ func TestSetWebViewWidget_NonExistentPane_ReturnsError(t *testing.T) {
459464 mockBox.EXPECT().SetHexpand(true).Once()
460465 mockBox.EXPECT().SetVexpand(true).Once()
461466
462- wv := component.NewWorkspaceView(mockFactory)
467+ wv := component.NewWorkspaceView(mockFactory, zerolog.Nop())
463468
464469 // Act
465470 err := wv.SetWebViewWidget(entity.PaneID("non-existent"), mockWebView)
@@ -479,7 +484,7 @@ func TestFocusPane_DelegatesToPaneView(t *testing.T) {
479484 mockBox.EXPECT().SetHexpand(true).Once()
480485 mockBox.EXPECT().SetVexpand(true).Once()
481486
482- wv := component.NewWorkspaceView(mockFactory)
487+ wv := component.NewWorkspaceView(mockFactory, zerolog.Nop())
483488
484489 // PaneView creation
485490 mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
@@ -522,7 +527,7 @@ func TestFocusPane_NonExistentPane_ReturnsFalse(t *testing.T) {
522527 mockBox.EXPECT().SetHexpand(true).Once()
523528 mockBox.EXPECT().SetVexpand(true).Once()
524529
525- wv := component.NewWorkspaceView(mockFactory)
530+ wv := component.NewWorkspaceView(mockFactory, zerolog.Nop())
526531
527532 // Act
528533 result := wv.FocusPane(entity.PaneID("non-existent"))
@@ -540,7 +545,7 @@ func TestRebuild_NilWorkspace_ReturnsError(t *testing.T) {
540545 mockBox.EXPECT().SetHexpand(true).Once()
541546 mockBox.EXPECT().SetVexpand(true).Once()
542547
543- wv := component.NewWorkspaceView(mockFactory)
548+ wv := component.NewWorkspaceView(mockFactory, zerolog.Nop())
544549
545550 // Act
546551 err := wv.Rebuild()
@@ -558,7 +563,7 @@ func TestTreeRenderer_ReturnsRenderer(t *testing.T) {
558563 mockBox.EXPECT().SetHexpand(true).Once()
559564 mockBox.EXPECT().SetVexpand(true).Once()
560565
561- wv := component.NewWorkspaceView(mockFactory)
566+ wv := component.NewWorkspaceView(mockFactory, zerolog.Nop())
562567
563568 // Act
564569 renderer := wv.TreeRenderer()
@@ -578,7 +583,7 @@ func TestWorkspace_ReturnsCurrentWorkspace(t *testing.T) {
578583 mockBox.EXPECT().SetHexpand(true).Once()
579584 mockBox.EXPECT().SetVexpand(true).Once()
580585
581- wv := component.NewWorkspaceView(mockFactory)
586+ wv := component.NewWorkspaceView(mockFactory, zerolog.Nop())
582587
583588 // PaneView creation
584589 mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
@@ -621,7 +626,7 @@ func TestWorkspace_NilBeforeSet(t *testing.T) {
621626 mockBox.EXPECT().SetHexpand(true).Once()
622627 mockBox.EXPECT().SetVexpand(true).Once()
623628
624- wv := component.NewWorkspaceView(mockFactory)
629+ wv := component.NewWorkspaceView(mockFactory, zerolog.Nop())
625630
626631 // Act
627632 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 // Act26 // Act
26- wv := component.NewWorkspaceView(mockFactory)27+ wv := component.NewWorkspaceView(mockFactory, zerolog.Nop())
27 28
28 // Assert29 // 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 // Act46 // 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 panes119 // 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 // Act214 // 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 creation234 // 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 // Act277 // 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 view300 // 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 // Act381 // 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 creation422 // 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 // Act469 // 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 creation489 // 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 // Act532 // 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 // Act550 // 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 // Act568 // 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 creation588 // 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 // Act631 // Act
627 result := wv.Workspace()632 result := wv.Workspace()
M internal/ui/deps.go +8 -4
@@ -9,6 +9,7 @@ import (
99 "github.com/bnema/dumber/internal/domain/repository"
1010 "github.com/bnema/dumber/internal/infrastructure/config"
1111 "github.com/bnema/dumber/internal/infrastructure/webkit"
12+ "github.com/bnema/dumber/internal/ui/component"
1213 "github.com/rs/zerolog"
1314 )
1415
@@ -21,10 +22,13 @@ type Dependencies struct {
2122 Logger *zerolog.Logger
2223
2324 // 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
2832
2933 // Repositories
3034 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.Logger22 Logger *zerolog.Logger
22 23
23 // WebKit infrastructure24 // WebKit infrastructure
24- WebContext *webkit.WebKitContext25+ WebContext *webkit.WebKitContext
25- Pool port.WebViewPool26+ Pool *webkit.WebViewPool
26- Factory port.WebViewFactory27+ Factory port.WebViewFactory
27- Settings *webkit.SettingsManager28+ Settings *webkit.SettingsManager
29+ Injector *webkit.ContentInjector
30+ MessageRouter *webkit.MessageRouter
31+ Overlay *component.OverlayController
28 32
29 // Repositories33 // Repositories
30 HistoryRepo repository.HistoryRepository34 HistoryRepo repository.HistoryRepository
M internal/ui/input/shortcuts.go +1 -3
@@ -204,10 +204,8 @@ func (s *ShortcutSet) buildPaneModeShortcuts(cfg *config.WorkspaceConfig) {
204204 func mapConfigAction(configAction string) Action {
205205 switch configAction {
206206 // Mode management
207- case "cancel":
207+ case "cancel", "confirm":
208208 return ActionExitMode
209- case "confirm":
210- return "" // No action for confirm yet
211209
212210 // Tab actions
213211 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 management206 // Mode management
207- case "cancel":207+ case "cancel", "confirm":
208 return ActionExitMode208 return ActionExitMode
209- case "confirm":
210- return "" // No action for confirm yet
211 209
212 // Tab actions210 // 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
5050 }
5151 mw.rootBox.SetHexpand(true)
5252 mw.rootBox.SetVexpand(true)
53+ mw.rootBox.SetVisible(true)
5354
5455 // Create tab bar
5556 mw.tabBar = component.NewTabBar()
@@ -69,6 +70,7 @@ func New(app *gtk.Application, cfg *config.Config, logger *zerolog.Logger) (*Mai
6970 }
7071 mw.contentArea.SetHexpand(true)
7172 mw.contentArea.SetVexpand(true)
73+ mw.contentArea.SetVisible(true)
7274 mw.contentArea.AddCssClass("content-area")
7375
7476 // Assemble layout based on tab bar position
@@ -130,6 +132,7 @@ func (mw *MainWindow) SetContent(widget *gtk.Widget) {
130132 // so we'd need to track children. For now, just append.
131133 // TODO: Track children for proper content swapping
132134 if widget != nil {
135+ widget.SetVisible(true)
133136 mw.contentArea.Append(widget)
134137 }
135138 }
@@ -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 bar55 // 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 position76 // 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 swapping133 // 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 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close