feat: listen for browser launch requests in ui app
- commit
- f7d1190648600182b6a996ef2f54ab0f720fc304
- author
- brice · · 2026-04-14 15:51 UTC
- parent
- 68837f5
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +267 -29
M cmd/dumber/main.go +3 -2
| @@ -781,8 +781,9 @@ func buildUIDependencies( | |||
| 781 | 781 | LaunchBrowserURL: func(uri string) { | |
| 782 | 782 | browserLauncher.LaunchURL(ctx, uri) | |
| 783 | 783 | }, | |
| 784 | - | ConfigMigrator: config.NewMigrator(), | |
| 785 | - | HandlerDeps: *handlerDeps, | |
| 784 | + | BrowserLaunchRelay: browserLaunchRelay, | |
| 785 | + | ConfigMigrator: config.NewMigrator(), | |
| 786 | + | HandlerDeps: *handlerDeps, | |
| 786 | 787 | } | |
| 787 | 788 | ||
| 788 | 789 | return uiDeps, nil | |
| @@ -781,8 +781,9 @@ func buildUIDependencies( | |||||
| 781 | LaunchBrowserURL: func(uri string) { | 781 | LaunchBrowserURL: func(uri string) { | ||
| 782 | browserLauncher.LaunchURL(ctx, uri) | 782 | browserLauncher.LaunchURL(ctx, uri) | ||
| 783 | }, | 783 | }, | ||
| 784 | - | ConfigMigrator: config.NewMigrator(), | 784 | + | BrowserLaunchRelay: browserLaunchRelay, |
| 785 | - | HandlerDeps: *handlerDeps, | 785 | + | ConfigMigrator: config.NewMigrator(), |
| 786 | + | HandlerDeps: *handlerDeps, | |||
| 786 | } | 787 | } | ||
| 787 | 788 | ||||
| 788 | return uiDeps, nil | 789 | return uiDeps, nil | ||
M internal/ui/app.go +151 -14
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "errors" | |
| 6 | 6 | "fmt" | |
| 7 | + | "io" | |
| 7 | 8 | "os" | |
| 8 | 9 | "path/filepath" | |
| 9 | 10 | "strings" | |
| @@ -67,6 +68,7 @@ type App struct { | |||
| 67 | 68 | ||
| 68 | 69 | browserWindows map[string]*browserWindow | |
| 69 | 70 | browserWindowFactory func(context.Context, string) (*browserWindow, error) | |
| 71 | + | tabCreationWindow *browserWindow | |
| 70 | 72 | dispatchOnMainThread func(func()) | |
| 71 | 73 | ||
| 72 | 74 | // State | |
| @@ -83,6 +85,7 @@ type App struct { | |||
| 83 | 85 | // Pane management (used by coordinators) | |
| 84 | 86 | panesUC *usecase.ManagePanesUseCase | |
| 85 | 87 | workspaceViews map[entity.TabID]*component.WorkspaceView | |
| 88 | + | windowForTab map[entity.TabID]*browserWindow | |
| 86 | 89 | widgetFactory layout.WidgetFactory | |
| 87 | 90 | stackedPaneMgr *component.StackedPaneManager | |
| 88 | 91 | ||
| @@ -146,7 +149,9 @@ type App struct { | |||
| 146 | 149 | deferredInitFn func() | |
| 147 | 150 | ||
| 148 | 151 | // lifecycle | |
| 149 | - | cancel context.CancelCauseFunc | |
| 152 | + | cancel context.CancelCauseFunc | |
| 153 | + | browserLaunchRelayOnce sync.Once | |
| 154 | + | browserLaunchRelayCloser io.Closer | |
| 150 | 155 | } | |
| 151 | 156 | ||
| 152 | 157 | type floatingWorkspaceSession struct { | |
| @@ -183,6 +188,7 @@ func New(deps *Dependencies) (*App, error) { | |||
| 183 | 188 | tabsUC: deps.TabsUC, | |
| 184 | 189 | panesUC: deps.PanesUC, | |
| 185 | 190 | workspaceViews: make(map[entity.TabID]*component.WorkspaceView), | |
| 191 | + | windowForTab: make(map[entity.TabID]*browserWindow), | |
| 186 | 192 | floatingSessions: make(map[floatingSessionKey]*floatingWorkspaceSession), | |
| 187 | 193 | browserWindows: make(map[string]*browserWindow), | |
| 188 | 194 | dispatchOnMainThread: func(fn func()) { fn() }, | |
| @@ -282,7 +288,7 @@ func (a *App) onActivate(ctx context.Context) { | |||
| 282 | 288 | // synchronous prewarming that delays the first navigation on cold start. | |
| 283 | 289 | a.setupPoolBackgroundColor(ctx) | |
| 284 | 290 | ||
| 285 | - | if _, err := a.OpenFreshWindow(ctx, a.initialWindowURL()); err != nil { | |
| 291 | + | if err := a.OpenFreshWindow(ctx, a.initialWindowURL()); err != nil { | |
| 286 | 292 | log.Error().Err(err).Msg("failed to create main window") | |
| 287 | 293 | return | |
| 288 | 294 | } | |
| @@ -451,6 +457,120 @@ func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*brow | |||
| 451 | 457 | return browserWindow, nil | |
| 452 | 458 | } | |
| 453 | 459 | ||
| 460 | + | func (a *App) ensureWindowForTabMap() { | |
| 461 | + | if a.windowForTab == nil { | |
| 462 | + | a.windowForTab = make(map[entity.TabID]*browserWindow) | |
| 463 | + | } | |
| 464 | + | } | |
| 465 | + | ||
| 466 | + | func (a *App) browserWindowForMainWindow(mainWindow *window.MainWindow) *browserWindow { | |
| 467 | + | if mainWindow == nil { | |
| 468 | + | return nil | |
| 469 | + | } | |
| 470 | + | for _, bw := range a.browserWindows { | |
| 471 | + | if bw != nil && bw.mainWindow == mainWindow { | |
| 472 | + | return bw | |
| 473 | + | } | |
| 474 | + | } | |
| 475 | + | return nil | |
| 476 | + | } | |
| 477 | + | ||
| 478 | + | func (a *App) browserWindowForTab(tabID entity.TabID) *browserWindow { | |
| 479 | + | if a.windowForTab != nil { | |
| 480 | + | if bw := a.windowForTab[tabID]; bw != nil { | |
| 481 | + | return bw | |
| 482 | + | } | |
| 483 | + | } | |
| 484 | + | if a.tabCreationWindow != nil { | |
| 485 | + | return a.tabCreationWindow | |
| 486 | + | } | |
| 487 | + | return a.browserWindowForMainWindow(a.mainWindow) | |
| 488 | + | } | |
| 489 | + | ||
| 490 | + | func (a *App) setBrowserWindowForTab(tabID entity.TabID, bw *browserWindow) { | |
| 491 | + | if bw == nil { | |
| 492 | + | return | |
| 493 | + | } | |
| 494 | + | a.ensureWindowForTabMap() | |
| 495 | + | a.windowForTab[tabID] = bw | |
| 496 | + | } | |
| 497 | + | ||
| 498 | + | func (a *App) startBrowserLaunchRelayListener(ctx context.Context) { | |
| 499 | + | if a == nil || a.deps == nil || a.deps.BrowserLaunchRelay == nil { | |
| 500 | + | return | |
| 501 | + | } | |
| 502 | + | a.browserLaunchRelayOnce.Do(func() { | |
| 503 | + | closer, err := a.deps.BrowserLaunchRelay.Listen(ctx, a) | |
| 504 | + | if err != nil { | |
| 505 | + | logging.FromContext(ctx).Warn().Err(err).Msg("failed to start browser launch relay listener") | |
| 506 | + | return | |
| 507 | + | } | |
| 508 | + | a.browserLaunchRelayCloser = closer | |
| 509 | + | }) | |
| 510 | + | } | |
| 511 | + | ||
| 512 | + | func (a *App) closeBrowserLaunchRelayListener() { | |
| 513 | + | if a.browserLaunchRelayCloser == nil { | |
| 514 | + | return | |
| 515 | + | } | |
| 516 | + | _ = a.browserLaunchRelayCloser.Close() | |
| 517 | + | a.browserLaunchRelayCloser = nil | |
| 518 | + | } | |
| 519 | + | ||
| 520 | + | func (a *App) openFreshWindow(ctx context.Context, url string) error { | |
| 521 | + | created, err := a.createBrowserWindow(ctx, url) | |
| 522 | + | if err != nil { | |
| 523 | + | return err | |
| 524 | + | } | |
| 525 | + | a.registerBrowserWindow(created) | |
| 526 | + | ||
| 527 | + | if a.tabs == nil || a.tabs.Count() == 0 { | |
| 528 | + | return nil | |
| 529 | + | } | |
| 530 | + | ||
| 531 | + | if a.tabCoord != nil && a.widgetFactory != nil { | |
| 532 | + | a.tabCreationWindow = created | |
| 533 | + | defer func() { | |
| 534 | + | a.tabCreationWindow = nil | |
| 535 | + | }() | |
| 536 | + | ||
| 537 | + | tab, err := a.tabCoord.Create(ctx, url) | |
| 538 | + | if err != nil { | |
| 539 | + | return err | |
| 540 | + | } | |
| 541 | + | a.setBrowserWindowForTab(tab.ID, created) | |
| 542 | + | if created.mainWindow != nil { | |
| 543 | + | created.mainWindow.Show() | |
| 544 | + | } | |
| 545 | + | return nil | |
| 546 | + | } | |
| 547 | + | ||
| 548 | + | if a.tabsUC == nil { | |
| 549 | + | return nil | |
| 550 | + | } | |
| 551 | + | ||
| 552 | + | output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{ | |
| 553 | + | TabList: a.tabs, | |
| 554 | + | InitialURL: url, | |
| 555 | + | }) | |
| 556 | + | if err != nil { | |
| 557 | + | return err | |
| 558 | + | } | |
| 559 | + | a.setBrowserWindowForTab(output.Tab.ID, created) | |
| 560 | + | if a.widgetFactory != nil { | |
| 561 | + | a.tabCreationWindow = created | |
| 562 | + | defer func() { | |
| 563 | + | a.tabCreationWindow = nil | |
| 564 | + | }() | |
| 565 | + | a.createWorkspaceView(ctx, output.Tab) | |
| 566 | + | a.switchWorkspaceView(ctx, output.Tab.ID) | |
| 567 | + | } | |
| 568 | + | if created.mainWindow != nil { | |
| 569 | + | created.mainWindow.Show() | |
| 570 | + | } | |
| 571 | + | return nil | |
| 572 | + | } | |
| 573 | + | ||
| 454 | 574 | func (a *App) initialWindowURL() string { | |
| 455 | 575 | if a.deps != nil && a.deps.InitialURL != "" { | |
| 456 | 576 | return a.deps.InitialURL | |
| @@ -1504,6 +1624,7 @@ func (a *App) finalizeActivation(ctx context.Context) { | |||
| 1504 | 1624 | if a.mainWindow != nil { | |
| 1505 | 1625 | a.mainWindow.Show() | |
| 1506 | 1626 | } | |
| 1627 | + | a.startBrowserLaunchRelayListener(ctx) | |
| 1507 | 1628 | log.Info().Msg("main window displayed") | |
| 1508 | 1629 | ||
| 1509 | 1630 | if a.deps != nil && len(a.deps.StartupCrashReports) > 0 { | |
| @@ -1616,6 +1737,7 @@ func (a *App) onShutdown(ctx context.Context) { | |||
| 1616 | 1737 | log.Warn().Err(err).Msg("failed to close idle inhibitor") | |
| 1617 | 1738 | } | |
| 1618 | 1739 | } | |
| 1740 | + | a.closeBrowserLaunchRelayListener() | |
| 1619 | 1741 | ||
| 1620 | 1742 | log.Info().Msg("application shutdown complete") | |
| 1621 | 1743 | } | |
| @@ -1793,7 +1915,7 @@ func (a *App) initCoordinators(ctx context.Context) { | |||
| 1793 | 1915 | ||
| 1794 | 1916 | // Wire window title updates when active pane's title changes | |
| 1795 | 1917 | a.contentCoord.SetOnWindowTitleChanged(func(title string) { | |
| 1796 | - | a.updateWindowTitle(title) | |
| 1918 | + | a.updateWindowTitle(title, a.browserWindowForTab(a.tabs.ActiveTabID)) | |
| 1797 | 1919 | }) | |
| 1798 | 1920 | ||
| 1799 | 1921 | // Wire pane URI updates for session snapshots (searches all tabs) | |
| @@ -2043,8 +2165,11 @@ func RunWithArgs(ctx context.Context, deps *Dependencies) int { | |||
| 2043 | 2165 | ||
| 2044 | 2166 | // updateWindowTitle updates the window title with the given page title. | |
| 2045 | 2167 | // Format: "<Page Title> - Dumber" or just "Dumber" if title is empty. | |
| 2046 | - | func (a *App) updateWindowTitle(pageTitle string) { | |
| 2047 | - | if a.mainWindow == nil { | |
| 2168 | + | func (a *App) updateWindowTitle(pageTitle string, target *browserWindow) { | |
| 2169 | + | if target == nil { | |
| 2170 | + | target = a.browserWindowForMainWindow(a.mainWindow) | |
| 2171 | + | } | |
| 2172 | + | if target == nil || target.mainWindow == nil { | |
| 2048 | 2173 | return | |
| 2049 | 2174 | } | |
| 2050 | 2175 | ||
| @@ -2052,18 +2177,23 @@ func (a *App) updateWindowTitle(pageTitle string) { | |||
| 2052 | 2177 | if pageTitle != "" { | |
| 2053 | 2178 | title = pageTitle + " - Dumber" | |
| 2054 | 2179 | } | |
| 2055 | - | a.mainWindow.SetTitle(title) | |
| 2180 | + | target.mainWindow.SetTitle(title) | |
| 2056 | 2181 | } | |
| 2057 | 2182 | ||
| 2058 | 2183 | // updateWindowTitleFromActivePane updates the window title based on the current active pane. | |
| 2059 | - | func (a *App) updateWindowTitleFromActivePane() { | |
| 2060 | - | ws := a.activeWorkspace() | |
| 2184 | + | func (a *App) updateWindowTitleFromActivePane(tabID entity.TabID) { | |
| 2185 | + | var ws *entity.Workspace | |
| 2186 | + | if tab := a.tabs.Find(tabID); tab != nil { | |
| 2187 | + | ws = tab.Workspace | |
| 2188 | + | } else { | |
| 2189 | + | ws = a.activeWorkspace() | |
| 2190 | + | } | |
| 2061 | 2191 | if ws == nil || a.contentCoord == nil { | |
| 2062 | - | a.updateWindowTitle("") | |
| 2192 | + | a.updateWindowTitle("", a.browserWindowForTab(tabID)) | |
| 2063 | 2193 | return | |
| 2064 | 2194 | } | |
| 2065 | 2195 | title := a.contentCoord.GetTitle(ws.ActivePaneID) | |
| 2066 | - | a.updateWindowTitle(title) | |
| 2196 | + | a.updateWindowTitle(title, a.browserWindowForTab(tabID)) | |
| 2067 | 2197 | } | |
| 2068 | 2198 | ||
| 2069 | 2199 | // handleModeChange is called when the input mode changes. | |
| @@ -2176,14 +2306,19 @@ func (a *App) applyResizeModeBorder(ctx context.Context, ws *entity.Workspace) { | |||
| 2176 | 2306 | func (a *App) createWorkspaceView(ctx context.Context, tab *entity.Tab) { | |
| 2177 | 2307 | a.createWorkspaceViewWithoutAttach(ctx, tab) | |
| 2178 | 2308 | ||
| 2309 | + | target := a.browserWindowForTab(tab.ID) | |
| 2310 | + | if target != nil { | |
| 2311 | + | a.setBrowserWindowForTab(tab.ID, target) | |
| 2312 | + | } | |
| 2313 | + | ||
| 2179 | 2314 | // Attach to content area | |
| 2180 | 2315 | wsView := a.workspaceViews[tab.ID] | |
| 2181 | - | if wsView != nil && a.mainWindow != nil { | |
| 2316 | + | if wsView != nil && target != nil && target.mainWindow != nil { | |
| 2182 | 2317 | widget := wsView.Widget() | |
| 2183 | 2318 | if widget != nil { | |
| 2184 | 2319 | gtkWidget := widget.GtkWidget() | |
| 2185 | 2320 | if gtkWidget != nil { | |
| 2186 | - | a.mainWindow.SetContent(gtkWidget) | |
| 2321 | + | target.mainWindow.SetContent(gtkWidget) | |
| 2187 | 2322 | } | |
| 2188 | 2323 | } | |
| 2189 | 2324 | } | |
| @@ -2362,12 +2497,14 @@ func (a *App) switchWorkspaceView(ctx context.Context, tabID entity.TabID) { | |||
| 2362 | 2497 | } | |
| 2363 | 2498 | ||
| 2364 | 2499 | // Swap content (MainWindow.SetContent now properly removes old content) | |
| 2365 | - | if a.mainWindow != nil { | |
| 2500 | + | if target := a.browserWindowForTab(tabID); target != nil && target.mainWindow != nil { | |
| 2501 | + | target.mainWindow.SetContent(gtkWidget) | |
| 2502 | + | } else if a.mainWindow != nil { | |
| 2366 | 2503 | a.mainWindow.SetContent(gtkWidget) | |
| 2367 | 2504 | } | |
| 2368 | 2505 | ||
| 2369 | 2506 | // Update window title with the new active pane's title | |
| 2370 | - | a.updateWindowTitleFromActivePane() | |
| 2507 | + | a.updateWindowTitleFromActivePane(tabID) | |
| 2371 | 2508 | a.syncFloatingFocus() | |
| 2372 | 2509 | ||
| 2373 | 2510 | log.Debug().Str("tab_id", string(tabID)).Msg("workspace view switched") | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "errors" | 5 | "errors" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | + | "io" | |||
| 7 | "os" | 8 | "os" | ||
| 8 | "path/filepath" | 9 | "path/filepath" | ||
| 9 | "strings" | 10 | "strings" | ||
| @@ -67,6 +68,7 @@ type App struct { | |||||
| 67 | 68 | ||||
| 68 | browserWindows map[string]*browserWindow | 69 | browserWindows map[string]*browserWindow | ||
| 69 | browserWindowFactory func(context.Context, string) (*browserWindow, error) | 70 | browserWindowFactory func(context.Context, string) (*browserWindow, error) | ||
| 71 | + | tabCreationWindow *browserWindow | |||
| 70 | dispatchOnMainThread func(func()) | 72 | dispatchOnMainThread func(func()) | ||
| 71 | 73 | ||||
| 72 | // State | 74 | // State | ||
| @@ -83,6 +85,7 @@ type App struct { | |||||
| 83 | // Pane management (used by coordinators) | 85 | // Pane management (used by coordinators) | ||
| 84 | panesUC *usecase.ManagePanesUseCase | 86 | panesUC *usecase.ManagePanesUseCase | ||
| 85 | workspaceViews map[entity.TabID]*component.WorkspaceView | 87 | workspaceViews map[entity.TabID]*component.WorkspaceView | ||
| 88 | + | windowForTab map[entity.TabID]*browserWindow | |||
| 86 | widgetFactory layout.WidgetFactory | 89 | widgetFactory layout.WidgetFactory | ||
| 87 | stackedPaneMgr *component.StackedPaneManager | 90 | stackedPaneMgr *component.StackedPaneManager | ||
| 88 | 91 | ||||
| @@ -146,7 +149,9 @@ type App struct { | |||||
| 146 | deferredInitFn func() | 149 | deferredInitFn func() | ||
| 147 | 150 | ||||
| 148 | // lifecycle | 151 | // lifecycle | ||
| 149 | - | cancel context.CancelCauseFunc | 152 | + | cancel context.CancelCauseFunc |
| 153 | + | browserLaunchRelayOnce sync.Once | |||
| 154 | + | browserLaunchRelayCloser io.Closer | |||
| 150 | } | 155 | } | ||
| 151 | 156 | ||||
| 152 | type floatingWorkspaceSession struct { | 157 | type floatingWorkspaceSession struct { | ||
| @@ -183,6 +188,7 @@ func New(deps *Dependencies) (*App, error) { | |||||
| 183 | tabsUC: deps.TabsUC, | 188 | tabsUC: deps.TabsUC, | ||
| 184 | panesUC: deps.PanesUC, | 189 | panesUC: deps.PanesUC, | ||
| 185 | workspaceViews: make(map[entity.TabID]*component.WorkspaceView), | 190 | workspaceViews: make(map[entity.TabID]*component.WorkspaceView), | ||
| 191 | + | windowForTab: make(map[entity.TabID]*browserWindow), | |||
| 186 | floatingSessions: make(map[floatingSessionKey]*floatingWorkspaceSession), | 192 | floatingSessions: make(map[floatingSessionKey]*floatingWorkspaceSession), | ||
| 187 | browserWindows: make(map[string]*browserWindow), | 193 | browserWindows: make(map[string]*browserWindow), | ||
| 188 | dispatchOnMainThread: func(fn func()) { fn() }, | 194 | dispatchOnMainThread: func(fn func()) { fn() }, | ||
| @@ -282,7 +288,7 @@ func (a *App) onActivate(ctx context.Context) { | |||||
| 282 | // synchronous prewarming that delays the first navigation on cold start. | 288 | // synchronous prewarming that delays the first navigation on cold start. | ||
| 283 | a.setupPoolBackgroundColor(ctx) | 289 | a.setupPoolBackgroundColor(ctx) | ||
| 284 | 290 | ||||
| 285 | - | if _, err := a.OpenFreshWindow(ctx, a.initialWindowURL()); err != nil { | 291 | + | if err := a.OpenFreshWindow(ctx, a.initialWindowURL()); err != nil { |
| 286 | log.Error().Err(err).Msg("failed to create main window") | 292 | log.Error().Err(err).Msg("failed to create main window") | ||
| 287 | return | 293 | return | ||
| 288 | } | 294 | } | ||
| @@ -451,6 +457,120 @@ func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*brow | |||||
| 451 | return browserWindow, nil | 457 | return browserWindow, nil | ||
| 452 | } | 458 | } | ||
| 453 | 459 | ||||
| 460 | + | func (a *App) ensureWindowForTabMap() { | |||
| 461 | + | if a.windowForTab == nil { | |||
| 462 | + | a.windowForTab = make(map[entity.TabID]*browserWindow) | |||
| 463 | + | } | |||
| 464 | + | } | |||
| 465 | + | ||||
| 466 | + | func (a *App) browserWindowForMainWindow(mainWindow *window.MainWindow) *browserWindow { | |||
| 467 | + | if mainWindow == nil { | |||
| 468 | + | return nil | |||
| 469 | + | } | |||
| 470 | + | for _, bw := range a.browserWindows { | |||
| 471 | + | if bw != nil && bw.mainWindow == mainWindow { | |||
| 472 | + | return bw | |||
| 473 | + | } | |||
| 474 | + | } | |||
| 475 | + | return nil | |||
| 476 | + | } | |||
| 477 | + | ||||
| 478 | + | func (a *App) browserWindowForTab(tabID entity.TabID) *browserWindow { | |||
| 479 | + | if a.windowForTab != nil { | |||
| 480 | + | if bw := a.windowForTab[tabID]; bw != nil { | |||
| 481 | + | return bw | |||
| 482 | + | } | |||
| 483 | + | } | |||
| 484 | + | if a.tabCreationWindow != nil { | |||
| 485 | + | return a.tabCreationWindow | |||
| 486 | + | } | |||
| 487 | + | return a.browserWindowForMainWindow(a.mainWindow) | |||
| 488 | + | } | |||
| 489 | + | ||||
| 490 | + | func (a *App) setBrowserWindowForTab(tabID entity.TabID, bw *browserWindow) { | |||
| 491 | + | if bw == nil { | |||
| 492 | + | return | |||
| 493 | + | } | |||
| 494 | + | a.ensureWindowForTabMap() | |||
| 495 | + | a.windowForTab[tabID] = bw | |||
| 496 | + | } | |||
| 497 | + | ||||
| 498 | + | func (a *App) startBrowserLaunchRelayListener(ctx context.Context) { | |||
| 499 | + | if a == nil || a.deps == nil || a.deps.BrowserLaunchRelay == nil { | |||
| 500 | + | return | |||
| 501 | + | } | |||
| 502 | + | a.browserLaunchRelayOnce.Do(func() { | |||
| 503 | + | closer, err := a.deps.BrowserLaunchRelay.Listen(ctx, a) | |||
| 504 | + | if err != nil { | |||
| 505 | + | logging.FromContext(ctx).Warn().Err(err).Msg("failed to start browser launch relay listener") | |||
| 506 | + | return | |||
| 507 | + | } | |||
| 508 | + | a.browserLaunchRelayCloser = closer | |||
| 509 | + | }) | |||
| 510 | + | } | |||
| 511 | + | ||||
| 512 | + | func (a *App) closeBrowserLaunchRelayListener() { | |||
| 513 | + | if a.browserLaunchRelayCloser == nil { | |||
| 514 | + | return | |||
| 515 | + | } | |||
| 516 | + | _ = a.browserLaunchRelayCloser.Close() | |||
| 517 | + | a.browserLaunchRelayCloser = nil | |||
| 518 | + | } | |||
| 519 | + | ||||
| 520 | + | func (a *App) openFreshWindow(ctx context.Context, url string) error { | |||
| 521 | + | created, err := a.createBrowserWindow(ctx, url) | |||
| 522 | + | if err != nil { | |||
| 523 | + | return err | |||
| 524 | + | } | |||
| 525 | + | a.registerBrowserWindow(created) | |||
| 526 | + | ||||
| 527 | + | if a.tabs == nil || a.tabs.Count() == 0 { | |||
| 528 | + | return nil | |||
| 529 | + | } | |||
| 530 | + | ||||
| 531 | + | if a.tabCoord != nil && a.widgetFactory != nil { | |||
| 532 | + | a.tabCreationWindow = created | |||
| 533 | + | defer func() { | |||
| 534 | + | a.tabCreationWindow = nil | |||
| 535 | + | }() | |||
| 536 | + | ||||
| 537 | + | tab, err := a.tabCoord.Create(ctx, url) | |||
| 538 | + | if err != nil { | |||
| 539 | + | return err | |||
| 540 | + | } | |||
| 541 | + | a.setBrowserWindowForTab(tab.ID, created) | |||
| 542 | + | if created.mainWindow != nil { | |||
| 543 | + | created.mainWindow.Show() | |||
| 544 | + | } | |||
| 545 | + | return nil | |||
| 546 | + | } | |||
| 547 | + | ||||
| 548 | + | if a.tabsUC == nil { | |||
| 549 | + | return nil | |||
| 550 | + | } | |||
| 551 | + | ||||
| 552 | + | output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{ | |||
| 553 | + | TabList: a.tabs, | |||
| 554 | + | InitialURL: url, | |||
| 555 | + | }) | |||
| 556 | + | if err != nil { | |||
| 557 | + | return err | |||
| 558 | + | } | |||
| 559 | + | a.setBrowserWindowForTab(output.Tab.ID, created) | |||
| 560 | + | if a.widgetFactory != nil { | |||
| 561 | + | a.tabCreationWindow = created | |||
| 562 | + | defer func() { | |||
| 563 | + | a.tabCreationWindow = nil | |||
| 564 | + | }() | |||
| 565 | + | a.createWorkspaceView(ctx, output.Tab) | |||
| 566 | + | a.switchWorkspaceView(ctx, output.Tab.ID) | |||
| 567 | + | } | |||
| 568 | + | if created.mainWindow != nil { | |||
| 569 | + | created.mainWindow.Show() | |||
| 570 | + | } | |||
| 571 | + | return nil | |||
| 572 | + | } | |||
| 573 | + | ||||
| 454 | func (a *App) initialWindowURL() string { | 574 | func (a *App) initialWindowURL() string { | ||
| 455 | if a.deps != nil && a.deps.InitialURL != "" { | 575 | if a.deps != nil && a.deps.InitialURL != "" { | ||
| 456 | return a.deps.InitialURL | 576 | return a.deps.InitialURL | ||
| @@ -1504,6 +1624,7 @@ func (a *App) finalizeActivation(ctx context.Context) { | |||||
| 1504 | if a.mainWindow != nil { | 1624 | if a.mainWindow != nil { | ||
| 1505 | a.mainWindow.Show() | 1625 | a.mainWindow.Show() | ||
| 1506 | } | 1626 | } | ||
| 1627 | + | a.startBrowserLaunchRelayListener(ctx) | |||
| 1507 | log.Info().Msg("main window displayed") | 1628 | log.Info().Msg("main window displayed") | ||
| 1508 | 1629 | ||||
| 1509 | if a.deps != nil && len(a.deps.StartupCrashReports) > 0 { | 1630 | if a.deps != nil && len(a.deps.StartupCrashReports) > 0 { | ||
| @@ -1616,6 +1737,7 @@ func (a *App) onShutdown(ctx context.Context) { | |||||
| 1616 | log.Warn().Err(err).Msg("failed to close idle inhibitor") | 1737 | log.Warn().Err(err).Msg("failed to close idle inhibitor") | ||
| 1617 | } | 1738 | } | ||
| 1618 | } | 1739 | } | ||
| 1740 | + | a.closeBrowserLaunchRelayListener() | |||
| 1619 | 1741 | ||||
| 1620 | log.Info().Msg("application shutdown complete") | 1742 | log.Info().Msg("application shutdown complete") | ||
| 1621 | } | 1743 | } | ||
| @@ -1793,7 +1915,7 @@ func (a *App) initCoordinators(ctx context.Context) { | |||||
| 1793 | 1915 | ||||
| 1794 | // Wire window title updates when active pane's title changes | 1916 | // Wire window title updates when active pane's title changes | ||
| 1795 | a.contentCoord.SetOnWindowTitleChanged(func(title string) { | 1917 | a.contentCoord.SetOnWindowTitleChanged(func(title string) { | ||
| 1796 | - | a.updateWindowTitle(title) | 1918 | + | a.updateWindowTitle(title, a.browserWindowForTab(a.tabs.ActiveTabID)) |
| 1797 | }) | 1919 | }) | ||
| 1798 | 1920 | ||||
| 1799 | // Wire pane URI updates for session snapshots (searches all tabs) | 1921 | // Wire pane URI updates for session snapshots (searches all tabs) | ||
| @@ -2043,8 +2165,11 @@ func RunWithArgs(ctx context.Context, deps *Dependencies) int { | |||||
| 2043 | 2165 | ||||
| 2044 | // updateWindowTitle updates the window title with the given page title. | 2166 | // updateWindowTitle updates the window title with the given page title. | ||
| 2045 | // Format: "<Page Title> - Dumber" or just "Dumber" if title is empty. | 2167 | // Format: "<Page Title> - Dumber" or just "Dumber" if title is empty. | ||
| 2046 | - | func (a *App) updateWindowTitle(pageTitle string) { | 2168 | + | func (a *App) updateWindowTitle(pageTitle string, target *browserWindow) { |
| 2047 | - | if a.mainWindow == nil { | 2169 | + | if target == nil { |
| 2170 | + | target = a.browserWindowForMainWindow(a.mainWindow) | |||
| 2171 | + | } | |||
| 2172 | + | if target == nil || target.mainWindow == nil { | |||
| 2048 | return | 2173 | return | ||
| 2049 | } | 2174 | } | ||
| 2050 | 2175 | ||||
| @@ -2052,18 +2177,23 @@ func (a *App) updateWindowTitle(pageTitle string) { | |||||
| 2052 | if pageTitle != "" { | 2177 | if pageTitle != "" { | ||
| 2053 | title = pageTitle + " - Dumber" | 2178 | title = pageTitle + " - Dumber" | ||
| 2054 | } | 2179 | } | ||
| 2055 | - | a.mainWindow.SetTitle(title) | 2180 | + | target.mainWindow.SetTitle(title) |
| 2056 | } | 2181 | } | ||
| 2057 | 2182 | ||||
| 2058 | // updateWindowTitleFromActivePane updates the window title based on the current active pane. | 2183 | // updateWindowTitleFromActivePane updates the window title based on the current active pane. | ||
| 2059 | - | func (a *App) updateWindowTitleFromActivePane() { | 2184 | + | func (a *App) updateWindowTitleFromActivePane(tabID entity.TabID) { |
| 2060 | - | ws := a.activeWorkspace() | 2185 | + | var ws *entity.Workspace |
| 2186 | + | if tab := a.tabs.Find(tabID); tab != nil { | |||
| 2187 | + | ws = tab.Workspace | |||
| 2188 | + | } else { | |||
| 2189 | + | ws = a.activeWorkspace() | |||
| 2190 | + | } | |||
| 2061 | if ws == nil || a.contentCoord == nil { | 2191 | if ws == nil || a.contentCoord == nil { | ||
| 2062 | - | a.updateWindowTitle("") | 2192 | + | a.updateWindowTitle("", a.browserWindowForTab(tabID)) |
| 2063 | return | 2193 | return | ||
| 2064 | } | 2194 | } | ||
| 2065 | title := a.contentCoord.GetTitle(ws.ActivePaneID) | 2195 | title := a.contentCoord.GetTitle(ws.ActivePaneID) | ||
| 2066 | - | a.updateWindowTitle(title) | 2196 | + | a.updateWindowTitle(title, a.browserWindowForTab(tabID)) |
| 2067 | } | 2197 | } | ||
| 2068 | 2198 | ||||
| 2069 | // handleModeChange is called when the input mode changes. | 2199 | // handleModeChange is called when the input mode changes. | ||
| @@ -2176,14 +2306,19 @@ func (a *App) applyResizeModeBorder(ctx context.Context, ws *entity.Workspace) { | |||||
| 2176 | func (a *App) createWorkspaceView(ctx context.Context, tab *entity.Tab) { | 2306 | func (a *App) createWorkspaceView(ctx context.Context, tab *entity.Tab) { | ||
| 2177 | a.createWorkspaceViewWithoutAttach(ctx, tab) | 2307 | a.createWorkspaceViewWithoutAttach(ctx, tab) | ||
| 2178 | 2308 | ||||
| 2309 | + | target := a.browserWindowForTab(tab.ID) | |||
| 2310 | + | if target != nil { | |||
| 2311 | + | a.setBrowserWindowForTab(tab.ID, target) | |||
| 2312 | + | } | |||
| 2313 | + | ||||
| 2179 | // Attach to content area | 2314 | // Attach to content area | ||
| 2180 | wsView := a.workspaceViews[tab.ID] | 2315 | wsView := a.workspaceViews[tab.ID] | ||
| 2181 | - | if wsView != nil && a.mainWindow != nil { | 2316 | + | if wsView != nil && target != nil && target.mainWindow != nil { |
| 2182 | widget := wsView.Widget() | 2317 | widget := wsView.Widget() | ||
| 2183 | if widget != nil { | 2318 | if widget != nil { | ||
| 2184 | gtkWidget := widget.GtkWidget() | 2319 | gtkWidget := widget.GtkWidget() | ||
| 2185 | if gtkWidget != nil { | 2320 | if gtkWidget != nil { | ||
| 2186 | - | a.mainWindow.SetContent(gtkWidget) | 2321 | + | target.mainWindow.SetContent(gtkWidget) |
| 2187 | } | 2322 | } | ||
| 2188 | } | 2323 | } | ||
| 2189 | } | 2324 | } | ||
| @@ -2362,12 +2497,14 @@ func (a *App) switchWorkspaceView(ctx context.Context, tabID entity.TabID) { | |||||
| 2362 | } | 2497 | } | ||
| 2363 | 2498 | ||||
| 2364 | // Swap content (MainWindow.SetContent now properly removes old content) | 2499 | // Swap content (MainWindow.SetContent now properly removes old content) | ||
| 2365 | - | if a.mainWindow != nil { | 2500 | + | if target := a.browserWindowForTab(tabID); target != nil && target.mainWindow != nil { |
| 2501 | + | target.mainWindow.SetContent(gtkWidget) | |||
| 2502 | + | } else if a.mainWindow != nil { | |||
| 2366 | a.mainWindow.SetContent(gtkWidget) | 2503 | a.mainWindow.SetContent(gtkWidget) | ||
| 2367 | } | 2504 | } | ||
| 2368 | 2505 | ||||
| 2369 | // Update window title with the new active pane's title | 2506 | // Update window title with the new active pane's title | ||
| 2370 | - | a.updateWindowTitleFromActivePane() | 2507 | + | a.updateWindowTitleFromActivePane(tabID) |
| 2371 | a.syncFloatingFocus() | 2508 | a.syncFloatingFocus() | ||
| 2372 | 2509 | ||||
| 2373 | log.Debug().Str("tab_id", string(tabID)).Msg("workspace view switched") | 2510 | log.Debug().Str("tab_id", string(tabID)).Msg("workspace view switched") | ||
A internal/ui/app_browser_launch_test.go +99 -0
| @@ -0,0 +1,99 @@ | |||
| 1 | + | package ui | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "io" | |
| 6 | + | "reflect" | |
| 7 | + | "testing" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/dumber/internal/application/port" | |
| 10 | + | "github.com/bnema/dumber/internal/application/usecase" | |
| 11 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | type testBrowserLaunchRelay struct { | |
| 15 | + | listenCalls int | |
| 16 | + | closer *testCloser | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | func (r *testBrowserLaunchRelay) DeliverOpenFreshWindow(context.Context, string) (bool, error) { | |
| 20 | + | return false, nil | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | func (r *testBrowserLaunchRelay) Listen(_ context.Context, opener port.BrowserWindowOpener) (io.Closer, error) { | |
| 24 | + | r.listenCalls++ | |
| 25 | + | _ = opener | |
| 26 | + | return r.closer, nil | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | type testCloser struct { | |
| 30 | + | closed bool | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | func (c *testCloser) Close() error { | |
| 34 | + | c.closed = true | |
| 35 | + | return nil | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | func setDependencyField(t *testing.T, deps *Dependencies, field string, value any) { | |
| 39 | + | t.Helper() | |
| 40 | + | ||
| 41 | + | rv := reflect.ValueOf(deps).Elem() | |
| 42 | + | fv := rv.FieldByName(field) | |
| 43 | + | if !fv.IsValid() { | |
| 44 | + | t.Fatalf("Dependencies missing %s field", field) | |
| 45 | + | } | |
| 46 | + | fv.Set(reflect.ValueOf(value)) | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | func windowForTabCount(t *testing.T, app *App) int { | |
| 50 | + | t.Helper() | |
| 51 | + | ||
| 52 | + | rv := reflect.ValueOf(app).Elem() | |
| 53 | + | fv := rv.FieldByName("windowForTab") | |
| 54 | + | if !fv.IsValid() { | |
| 55 | + | t.Fatalf("App missing windowForTab field") | |
| 56 | + | } | |
| 57 | + | return fv.Len() | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | func TestApp_FinalizeActivationStartsBrowserLaunchRelayOnceAndClosesOnShutdown(t *testing.T) { | |
| 61 | + | relay := &testBrowserLaunchRelay{closer: &testCloser{}} | |
| 62 | + | deps := &Dependencies{} | |
| 63 | + | setDependencyField(t, deps, "BrowserLaunchRelay", relay) | |
| 64 | + | ||
| 65 | + | app := &App{deps: deps, cancel: func(error) {}} | |
| 66 | + | ||
| 67 | + | app.finalizeActivation(context.Background()) | |
| 68 | + | app.finalizeActivation(context.Background()) | |
| 69 | + | ||
| 70 | + | if relay.listenCalls != 1 { | |
| 71 | + | t.Fatalf("Listen calls = %d, want 1", relay.listenCalls) | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | app.onShutdown(context.Background()) | |
| 75 | + | ||
| 76 | + | if !relay.closer.closed { | |
| 77 | + | t.Fatalf("relay listener closer was not closed") | |
| 78 | + | } | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | func TestApp_OpenFreshWindowRecordsTabOwnership(t *testing.T) { | |
| 82 | + | created := &browserWindow{id: "window-1"} | |
| 83 | + | app := &App{ | |
| 84 | + | tabs: entity.NewTabList(), | |
| 85 | + | tabsUC: usecase.NewManageTabsUseCase(func() string { return "id-1" }), | |
| 86 | + | browserWindows: make(map[string]*browserWindow), | |
| 87 | + | browserWindowFactory: func(context.Context, string) (*browserWindow, error) { | |
| 88 | + | return created, nil | |
| 89 | + | }, | |
| 90 | + | } | |
| 91 | + | app.tabs.Add(entity.NewTab(entity.TabID("existing-tab"), entity.WorkspaceID("existing-workspace"), entity.NewPane(entity.PaneID("existing-pane")))) | |
| 92 | + | ||
| 93 | + | if err := app.OpenFreshWindow(context.Background(), "https://example.com"); err != nil { | |
| 94 | + | t.Fatalf("OpenFreshWindow returned error: %v", err) | |
| 95 | + | } | |
| 96 | + | if got := windowForTabCount(t, app); got != 1 { | |
| 97 | + | t.Fatalf("windowForTab length = %d, want 1", got) | |
| 98 | + | } | |
| 99 | + | } | |
| @@ -0,0 +1,99 @@ | |||||
| 1 | + | package ui | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "io" | |||
| 6 | + | "reflect" | |||
| 7 | + | "testing" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 10 | + | "github.com/bnema/dumber/internal/application/usecase" | |||
| 11 | + | "github.com/bnema/dumber/internal/domain/entity" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | type testBrowserLaunchRelay struct { | |||
| 15 | + | listenCalls int | |||
| 16 | + | closer *testCloser | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | func (r *testBrowserLaunchRelay) DeliverOpenFreshWindow(context.Context, string) (bool, error) { | |||
| 20 | + | return false, nil | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | func (r *testBrowserLaunchRelay) Listen(_ context.Context, opener port.BrowserWindowOpener) (io.Closer, error) { | |||
| 24 | + | r.listenCalls++ | |||
| 25 | + | _ = opener | |||
| 26 | + | return r.closer, nil | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | type testCloser struct { | |||
| 30 | + | closed bool | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | func (c *testCloser) Close() error { | |||
| 34 | + | c.closed = true | |||
| 35 | + | return nil | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | func setDependencyField(t *testing.T, deps *Dependencies, field string, value any) { | |||
| 39 | + | t.Helper() | |||
| 40 | + | ||||
| 41 | + | rv := reflect.ValueOf(deps).Elem() | |||
| 42 | + | fv := rv.FieldByName(field) | |||
| 43 | + | if !fv.IsValid() { | |||
| 44 | + | t.Fatalf("Dependencies missing %s field", field) | |||
| 45 | + | } | |||
| 46 | + | fv.Set(reflect.ValueOf(value)) | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | func windowForTabCount(t *testing.T, app *App) int { | |||
| 50 | + | t.Helper() | |||
| 51 | + | ||||
| 52 | + | rv := reflect.ValueOf(app).Elem() | |||
| 53 | + | fv := rv.FieldByName("windowForTab") | |||
| 54 | + | if !fv.IsValid() { | |||
| 55 | + | t.Fatalf("App missing windowForTab field") | |||
| 56 | + | } | |||
| 57 | + | return fv.Len() | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | func TestApp_FinalizeActivationStartsBrowserLaunchRelayOnceAndClosesOnShutdown(t *testing.T) { | |||
| 61 | + | relay := &testBrowserLaunchRelay{closer: &testCloser{}} | |||
| 62 | + | deps := &Dependencies{} | |||
| 63 | + | setDependencyField(t, deps, "BrowserLaunchRelay", relay) | |||
| 64 | + | ||||
| 65 | + | app := &App{deps: deps, cancel: func(error) {}} | |||
| 66 | + | ||||
| 67 | + | app.finalizeActivation(context.Background()) | |||
| 68 | + | app.finalizeActivation(context.Background()) | |||
| 69 | + | ||||
| 70 | + | if relay.listenCalls != 1 { | |||
| 71 | + | t.Fatalf("Listen calls = %d, want 1", relay.listenCalls) | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | app.onShutdown(context.Background()) | |||
| 75 | + | ||||
| 76 | + | if !relay.closer.closed { | |||
| 77 | + | t.Fatalf("relay listener closer was not closed") | |||
| 78 | + | } | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | func TestApp_OpenFreshWindowRecordsTabOwnership(t *testing.T) { | |||
| 82 | + | created := &browserWindow{id: "window-1"} | |||
| 83 | + | app := &App{ | |||
| 84 | + | tabs: entity.NewTabList(), | |||
| 85 | + | tabsUC: usecase.NewManageTabsUseCase(func() string { return "id-1" }), | |||
| 86 | + | browserWindows: make(map[string]*browserWindow), | |||
| 87 | + | browserWindowFactory: func(context.Context, string) (*browserWindow, error) { | |||
| 88 | + | return created, nil | |||
| 89 | + | }, | |||
| 90 | + | } | |||
| 91 | + | app.tabs.Add(entity.NewTab(entity.TabID("existing-tab"), entity.WorkspaceID("existing-workspace"), entity.NewPane(entity.PaneID("existing-pane")))) | |||
| 92 | + | ||||
| 93 | + | if err := app.OpenFreshWindow(context.Background(), "https://example.com"); err != nil { | |||
| 94 | + | t.Fatalf("OpenFreshWindow returned error: %v", err) | |||
| 95 | + | } | |||
| 96 | + | if got := windowForTabCount(t, app); got != 1 { | |||
| 97 | + | t.Fatalf("windowForTab length = %d, want 1", got) | |||
| 98 | + | } | |||
| 99 | + | } | |||
M internal/ui/browser_window.go +8 -8
| @@ -26,6 +26,11 @@ func (a *App) removeBrowserWindow(id string) { | |||
| 26 | 26 | if id == "" || a.browserWindows == nil { | |
| 27 | 27 | return | |
| 28 | 28 | } | |
| 29 | + | for tabID, bw := range a.windowForTab { | |
| 30 | + | if bw != nil && bw.id == id { | |
| 31 | + | delete(a.windowForTab, tabID) | |
| 32 | + | } | |
| 33 | + | } | |
| 29 | 34 | if bw := a.browserWindows[id]; bw != nil && bw.mainWindow == a.mainWindow { | |
| 30 | 35 | delete(a.browserWindows, id) | |
| 31 | 36 | for _, remaining := range a.browserWindows { | |
| @@ -40,21 +45,16 @@ func (a *App) removeBrowserWindow(id string) { | |||
| 40 | 45 | delete(a.browserWindows, id) | |
| 41 | 46 | } | |
| 42 | 47 | ||
| 43 | - | func (a *App) OpenFreshWindow(ctx context.Context, url string) (*browserWindow, error) { | |
| 48 | + | func (a *App) OpenFreshWindow(ctx context.Context, url string) error { | |
| 44 | 49 | dispatch := a.dispatchOnMainThread | |
| 45 | 50 | if dispatch == nil { | |
| 46 | 51 | dispatch = func(fn func()) { fn() } | |
| 47 | 52 | } | |
| 48 | 53 | ||
| 49 | - | var created *browserWindow | |
| 50 | 54 | var openErr error | |
| 51 | 55 | dispatch(func() { | |
| 52 | - | created, openErr = a.createBrowserWindow(ctx, url) | |
| 53 | - | if openErr != nil { | |
| 54 | - | return | |
| 55 | - | } | |
| 56 | - | a.registerBrowserWindow(created) | |
| 56 | + | openErr = a.openFreshWindow(ctx, url) | |
| 57 | 57 | }) | |
| 58 | 58 | ||
| 59 | - | return created, openErr | |
| 59 | + | return openErr | |
| 60 | 60 | } | |
| @@ -26,6 +26,11 @@ func (a *App) removeBrowserWindow(id string) { | |||||
| 26 | if id == "" || a.browserWindows == nil { | 26 | if id == "" || a.browserWindows == nil { | ||
| 27 | return | 27 | return | ||
| 28 | } | 28 | } | ||
| 29 | + | for tabID, bw := range a.windowForTab { | |||
| 30 | + | if bw != nil && bw.id == id { | |||
| 31 | + | delete(a.windowForTab, tabID) | |||
| 32 | + | } | |||
| 33 | + | } | |||
| 29 | if bw := a.browserWindows[id]; bw != nil && bw.mainWindow == a.mainWindow { | 34 | if bw := a.browserWindows[id]; bw != nil && bw.mainWindow == a.mainWindow { | ||
| 30 | delete(a.browserWindows, id) | 35 | delete(a.browserWindows, id) | ||
| 31 | for _, remaining := range a.browserWindows { | 36 | for _, remaining := range a.browserWindows { | ||
| @@ -40,21 +45,16 @@ func (a *App) removeBrowserWindow(id string) { | |||||
| 40 | delete(a.browserWindows, id) | 45 | delete(a.browserWindows, id) | ||
| 41 | } | 46 | } | ||
| 42 | 47 | ||||
| 43 | - | func (a *App) OpenFreshWindow(ctx context.Context, url string) (*browserWindow, error) { | 48 | + | func (a *App) OpenFreshWindow(ctx context.Context, url string) error { |
| 44 | dispatch := a.dispatchOnMainThread | 49 | dispatch := a.dispatchOnMainThread | ||
| 45 | if dispatch == nil { | 50 | if dispatch == nil { | ||
| 46 | dispatch = func(fn func()) { fn() } | 51 | dispatch = func(fn func()) { fn() } | ||
| 47 | } | 52 | } | ||
| 48 | 53 | ||||
| 49 | - | var created *browserWindow | |||
| 50 | var openErr error | 54 | var openErr error | ||
| 51 | dispatch(func() { | 55 | dispatch(func() { | ||
| 52 | - | created, openErr = a.createBrowserWindow(ctx, url) | 56 | + | openErr = a.openFreshWindow(ctx, url) |
| 53 | - | if openErr != nil { | |||
| 54 | - | return | |||
| 55 | - | } | |||
| 56 | - | a.registerBrowserWindow(created) | |||
| 57 | }) | 57 | }) | ||
| 58 | 58 | ||||
| 59 | - | return created, openErr | 59 | + | return openErr |
| 60 | } | 60 | } | ||
M internal/ui/browser_window_test.go +4 -5
| @@ -90,8 +90,7 @@ func TestOpenFreshWindow_DispatchesAndTracksBrowserWindow(t *testing.T) { | |||
| 90 | 90 | return &browserWindow{id: "window-1", initialURL: url}, nil | |
| 91 | 91 | } | |
| 92 | 92 | ||
| 93 | - | window, err := app.OpenFreshWindow(context.Background(), "https://example.com") | |
| 94 | - | if err != nil { | |
| 93 | + | if err := app.OpenFreshWindow(context.Background(), "https://example.com"); err != nil { | |
| 95 | 94 | t.Fatalf("OpenFreshWindow returned error: %v", err) | |
| 96 | 95 | } | |
| 97 | 96 | if !dispatched { | |
| @@ -103,10 +102,10 @@ func TestOpenFreshWindow_DispatchesAndTracksBrowserWindow(t *testing.T) { | |||
| 103 | 102 | if got := len(app.browserWindows); got != 1 { | |
| 104 | 103 | t.Fatalf("browserWindows length = %d, want 1", got) | |
| 105 | 104 | } | |
| 106 | - | if got := app.browserWindows[window.id]; got != window { | |
| 105 | + | if got := app.browserWindows["window-1"]; got == nil { | |
| 107 | 106 | t.Fatalf("browser window was not tracked") | |
| 108 | 107 | } | |
| 109 | - | if window.initialURL != "https://example.com" { | |
| 110 | - | t.Fatalf("browser window initialURL = %q, want %q", window.initialURL, "https://example.com") | |
| 108 | + | if app.browserWindows["window-1"].initialURL != "https://example.com" { | |
| 109 | + | t.Fatalf("browser window initialURL = %q, want %q", app.browserWindows["window-1"].initialURL, "https://example.com") | |
| 111 | 110 | } | |
| 112 | 111 | } | |
| @@ -90,8 +90,7 @@ func TestOpenFreshWindow_DispatchesAndTracksBrowserWindow(t *testing.T) { | |||||
| 90 | return &browserWindow{id: "window-1", initialURL: url}, nil | 90 | return &browserWindow{id: "window-1", initialURL: url}, nil | ||
| 91 | } | 91 | } | ||
| 92 | 92 | ||||
| 93 | - | window, err := app.OpenFreshWindow(context.Background(), "https://example.com") | 93 | + | if err := app.OpenFreshWindow(context.Background(), "https://example.com"); err != nil { |
| 94 | - | if err != nil { | |||
| 95 | t.Fatalf("OpenFreshWindow returned error: %v", err) | 94 | t.Fatalf("OpenFreshWindow returned error: %v", err) | ||
| 96 | } | 95 | } | ||
| 97 | if !dispatched { | 96 | if !dispatched { | ||
| @@ -103,10 +102,10 @@ func TestOpenFreshWindow_DispatchesAndTracksBrowserWindow(t *testing.T) { | |||||
| 103 | if got := len(app.browserWindows); got != 1 { | 102 | if got := len(app.browserWindows); got != 1 { | ||
| 104 | t.Fatalf("browserWindows length = %d, want 1", got) | 103 | t.Fatalf("browserWindows length = %d, want 1", got) | ||
| 105 | } | 104 | } | ||
| 106 | - | if got := app.browserWindows[window.id]; got != window { | 105 | + | if got := app.browserWindows["window-1"]; got == nil { |
| 107 | t.Fatalf("browser window was not tracked") | 106 | t.Fatalf("browser window was not tracked") | ||
| 108 | } | 107 | } | ||
| 109 | - | if window.initialURL != "https://example.com" { | 108 | + | if app.browserWindows["window-1"].initialURL != "https://example.com" { |
| 110 | - | t.Fatalf("browser window initialURL = %q, want %q", window.initialURL, "https://example.com") | 109 | + | t.Fatalf("browser window initialURL = %q, want %q", app.browserWindows["window-1"].initialURL, "https://example.com") |
| 111 | } | 110 | } | ||
| 112 | } | 111 | } | ||
M internal/ui/deps.go +2 -0
| @@ -104,6 +104,8 @@ type Dependencies struct { | |||
| 104 | 104 | // LaunchBrowserURL opens a URL in a new dumber browser window. | |
| 105 | 105 | // Optional: if nil, standalone omnibox web navigations are logged only. | |
| 106 | 106 | LaunchBrowserURL func(uri string) | |
| 107 | + | // BrowserLaunchRelay listens for in-process browser launch requests. | |
| 108 | + | BrowserLaunchRelay port.BrowserLaunchRelay | |
| 107 | 109 | } | |
| 108 | 110 | ||
| 109 | 111 | // Validate checks that all required dependencies are set. | |
| @@ -104,6 +104,8 @@ type Dependencies struct { | |||||
| 104 | // LaunchBrowserURL opens a URL in a new dumber browser window. | 104 | // LaunchBrowserURL opens a URL in a new dumber browser window. | ||
| 105 | // Optional: if nil, standalone omnibox web navigations are logged only. | 105 | // Optional: if nil, standalone omnibox web navigations are logged only. | ||
| 106 | LaunchBrowserURL func(uri string) | 106 | LaunchBrowserURL func(uri string) | ||
| 107 | + | // BrowserLaunchRelay listens for in-process browser launch requests. | |||
| 108 | + | BrowserLaunchRelay port.BrowserLaunchRelay | |||
| 107 | } | 109 | } | ||
| 108 | 110 | ||||
| 109 | // Validate checks that all required dependencies are set. | 111 | // Validate checks that all required dependencies are set. | ||