detached ec8c3ee

refactor(ui): extract app window lifecycle helpers

commit
ec8c3ee6aefd4a40484572ed97980b5731fdc8c1
author
brice · · 2026-06-25 12:54 UTC
parent
c031fea
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +298 -258
M internal/ui/app.go +0 -258
@@ -446,78 +446,6 @@ func (a *App) prewarmWebViewPoolAsync(ctx context.Context) {
446446 pool.PrewarmAsync(ctx, 0)
447447 }
448448
449-func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*browserWindow, error) {
450- log := logging.FromContext(ctx)
451- log.Debug().
452- Str("url_host", logging.SafeURLHost(initialURL)).
453- Bool("using_factory", a.browserWindowFactory != nil).
454- Int("window_count_before", len(a.browserWindows)).
455- Msg("ui: create browser window started")
456-
457- if a.browserWindowFactory != nil {
458- created, err := a.browserWindowFactory(ctx, initialURL)
459- if err != nil {
460- log.Warn().Err(err).
461- Str("url_host", logging.SafeURLHost(initialURL)).
462- Msg("ui: browser window factory failed")
463- return nil, err
464- }
465- created.ensureTabs()
466- a.wireBrowserWindowActivationTracking(created)
467- created.initChrome(ctx, a)
468- return created, nil
469- }
470-
471- runtimeCfg := a.runtimeConfigSnapshot().UI
472- mainWindow, err := window.New(ctx, a.gtkApp, runtimeCfg.Workspace.TabBarPosition)
473- if err != nil {
474- log.Warn().Err(err).
475- Str("url_host", logging.SafeURLHost(initialURL)).
476- Msg("ui: GTK browser window shell creation failed")
477- return nil, err
478- }
479- browserWindow := &browserWindow{
480- id: a.generateWindowID(),
481- initialURL: initialURL,
482- tabs: entity.NewTabList(),
483- mainWindow: mainWindow,
484- }
485- a.wireBrowserWindowActivationTracking(browserWindow)
486- if a.mainWindow == nil {
487- a.mainWindow = mainWindow
488- }
489-
490- closeRequestCb := func(_ gtk.Window) bool {
491- log.Info().Msg("browser window close requested")
492- a.removeBrowserWindow(browserWindow.id)
493- return false
494- }
495- mainWindow.Window().ConnectCloseRequest(&closeRequestCb)
496-
497- a.initBrowserWindowOverlays(mainWindow, browserWindow, runtimeCfg)
498- a.wireBrowserWindowPermissionIndicator(browserWindow)
499- if a.kbDispatcher != nil {
500- a.initBrowserWindowInput(ctx, browserWindow)
501- }
502- if a.tabCoord != nil {
503- a.wireBrowserWindowTabBar(ctx, browserWindow)
504- }
505- browserWindow.initChrome(ctx, a)
506-
507- // Apply GTK CSS styling from theme manager.
508- if a.deps == nil || a.deps.Theme == nil {
509- return browserWindow, nil
510- }
511- if display := mainWindow.Window().GetDisplay(); display != nil {
512- a.deps.Theme.ApplyToDisplay(ctx, display)
513- }
514- log.Debug().
515- Str("window_id", browserWindow.id).
516- Str("url_host", logging.SafeURLHost(initialURL)).
517- Msg("ui: create browser window completed")
518- return browserWindow, nil
519-}
520-
521449 func (a *App) initBrowserWindowOverlays(mainWindow *window.MainWindow, browserWindow *browserWindow, runtimeCfg entity.RuntimeUIConfig) {
522450 // Create permission popup and dialog presenter
523451 if a.deps != nil && a.deps.PermissionUC != nil {
@@ -830,192 +758,6 @@ func (a *App) closeBrowserLaunchRelayListener() {
830758 a.browserLaunchRelayCloser = nil
831759 }
832760
833-func (a *App) openFreshWindow(ctx context.Context, url string) error {
834- log := logging.FromContext(ctx)
835- log.Debug().
836- Str("url_host", logging.SafeURLHost(url)).
837- Int("window_count_before", len(a.browserWindows)).
838- Bool("has_tab_coord", a.tabCoord != nil).
839- Bool("has_tabs_uc", a.tabsUC != nil).
840- Msg("ui: open fresh window started")
841-
842- created, err := a.createBrowserWindow(ctx, url)
843- if err != nil {
844- log.Warn().Err(err).
845- Str("url_host", logging.SafeURLHost(url)).
846- Int("window_count_after", len(a.browserWindows)).
847- Msg("ui: open fresh window could not create browser window")
848- return err
849- }
850- log.Debug().
851- Str("window_id", created.id).
852- Str("url_host", logging.SafeURLHost(url)).
853- Msg("ui: open fresh window created browser window shell")
854- a.registerBrowserWindow(created)
855- log.Debug().
856- Str("window_id", created.id).
857- Int("window_count_after_register", len(a.browserWindows)).
858- Msg("ui: open fresh window registered browser window")
859-
860- if len(a.browserWindows) == 1 {
861- log.Debug().Str("window_id", created.id).Msg("ui: activating first browser window")
862- a.activateBrowserWindow(created)
863- return nil
864- }
865-
866- var openErr error
867- path := "tabs_uc"
868- if a.tabCoord != nil {
869- path = "tab_coord"
870- log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tab coordinator")
871- openErr = a.openFreshWindowWithTabCoord(ctx, url, created)
872- } else if a.tabsUC == nil {
873- log.Debug().Str("window_id", created.id).Msg("ui: activating fresh browser window without tab use case")
874- a.activateBrowserWindow(created)
875- return nil
876- } else {
877- log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tabs use case")
878- openErr = a.openFreshWindowWithTabsUC(ctx, url, created)
879- }
880- if openErr != nil {
881- log.Warn().Err(openErr).
882- Str("window_id", created.id).
883- Str("path", path).
884- Int("window_count_after", len(a.browserWindows)).
885- Msg("ui: open fresh window initial tab creation failed")
886- return openErr
887- }
888- log.Debug().
889- Str("window_id", created.id).
890- Str("path", path).
891- Msg("ui: activating fresh browser window after tab creation")
892- a.activateBrowserWindow(created)
893- return nil
894-}
895-
896-func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, created *browserWindow) error {
897- log := logging.FromContext(ctx)
898- previousTarget := a.tabCoord.CurrentTarget()
899- target := a.ensureTabTargetForBrowserWindow(created)
900- a.tabCoord.SetCurrentTarget(target)
901- defer func() {
902- a.tabCoord.SetCurrentTarget(previousTarget)
903- }()
904-
905- log.Debug().
906- Str("window_id", created.id).
907- Str("url_host", logging.SafeURLHost(url)).
908- Msg("ui: tab coordinator create started for fresh window")
909- createdTab, createErr := a.tabCoord.Create(ctx, target, url)
910- if createErr != nil {
911- log.Warn().Err(createErr).
912- Str("window_id", created.id).
913- Msg("ui: tab coordinator create failed for fresh window; rolling back window")
914- a.removeBrowserWindow(created.id)
915- if created.mainWindow != nil {
916- created.mainWindow.Destroy()
917- }
918- return createErr
919- }
920- log.Debug().
921- Str("window_id", created.id).
922- Str("tab_id", string(createdTab.ID)).
923- Msg("ui: tab coordinator create completed for fresh window")
924- if a.workspaceViews[createdTab.ID] == nil {
925- log.Warn().
926- Str("window_id", created.id).
927- Str("tab_id", string(createdTab.ID)).
928- Msg("ui: fresh window tab has no workspace view; rolling back window")
929- if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil {
930- createdTabs.Remove(createdTab.ID)
931- }
932- if created.mainWindow != nil {
933- if tabBar := created.mainWindow.TabBar(); tabBar != nil {
934- tabBar.RemoveTab(createdTab.ID)
935- if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil {
936- tabBar.SetActive(activeTab.ID)
937- }
938- }
939- }
940- a.removeBrowserWindow(created.id)
941- if created.mainWindow != nil {
942- created.mainWindow.Destroy()
943- }
944- return fmt.Errorf("workspace view not created for tab %s", createdTab.ID)
945- }
946- a.setBrowserWindowForTab(createdTab.ID, created)
947- // Tab was already created in the per-window TabList via tabCoord.Create;
948- // active state is managed by TabList.SetActive.
949- if created.mainWindow != nil {
950- created.mainWindow.Show()
951- }
952- return nil
953-}
954-
955-func (a *App) openFreshWindowWithTabsUC(ctx context.Context, url string, created *browserWindow) error {
956- log := logging.FromContext(ctx)
957- log.Debug().
958- Str("window_id", created.id).
959- Str("url_host", logging.SafeURLHost(url)).
960- Msg("ui: tabs use case create started for fresh window")
961- output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{
962- TabList: a.tabs,
963- InitialURL: url,
964- })
965- if err != nil {
966- log.Warn().Err(err).
967- Str("window_id", created.id).
968- Msg("ui: tabs use case create failed for fresh window; rolling back window")
969- a.removeBrowserWindow(created.id)
970- if created.mainWindow != nil {
971- created.mainWindow.Destroy()
972- }
973- return err
974- }
975- log.Debug().
976- Str("window_id", created.id).
977- Str("tab_id", string(output.Tab.ID)).
978- Msg("ui: tabs use case create completed for fresh window")
979- a.setBrowserWindowForTab(output.Tab.ID, created)
980- // Mirror the tabsUC-created tab into the new browser window's tab list.
981- // This path uses the tabs usecase directly instead of tabCoord.Create,
982- // so the per-window TabList must be populated explicitly.
983- a.ensureTabListForBrowserWindow(created).Add(output.Tab)
984- if a.widgetFactory != nil {
985- a.createWorkspaceView(ctx, output.Tab)
986- a.switchWorkspaceView(ctx, output.Tab.ID)
987- }
988- if a.workspaceViews[output.Tab.ID] == nil {
989- log.Warn().
990- Str("window_id", created.id).
991- Str("tab_id", string(output.Tab.ID)).
992- Msg("ui: fresh window tabs use case tab has no workspace view; rolling back window")
993- if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil {
994- createdTabs.Remove(output.Tab.ID)
995- }
996- if a.tabs != nil {
997- a.tabs.Remove(output.Tab.ID)
998- }
999- if created.mainWindow != nil {
1000- if tabBar := created.mainWindow.TabBar(); tabBar != nil {
1001- tabBar.RemoveTab(output.Tab.ID)
1002- if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil {
1003- tabBar.SetActive(activeTab.ID)
1004- }
1005- }
1006- }
1007- a.removeBrowserWindow(created.id)
1008- if created.mainWindow != nil {
1009- created.mainWindow.Destroy()
1010- }
1011- return fmt.Errorf("workspace view not created for tab %s", output.Tab.ID)
1012- }
1013- if created.mainWindow != nil {
1014- created.mainWindow.Show()
1015- }
1016- return nil
1017-}
1018-
1019761 func (a *App) initialWindowURL() string {
1020762 if a.deps != nil {
1021763 return urlutil.ResolveBrowserStartupURL(a.deps.InitialURL)
@@ -446,78 +446,6 @@ func (a *App) prewarmWebViewPoolAsync(ctx context.Context) {
446 pool.PrewarmAsync(ctx, 0)446 pool.PrewarmAsync(ctx, 0)
447 }447 }
448 448
449-func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*browserWindow, error) {
450- log := logging.FromContext(ctx)
451- log.Debug().
452- Str("url_host", logging.SafeURLHost(initialURL)).
453- Bool("using_factory", a.browserWindowFactory != nil).
454- Int("window_count_before", len(a.browserWindows)).
455- Msg("ui: create browser window started")
456-
457- if a.browserWindowFactory != nil {
458- created, err := a.browserWindowFactory(ctx, initialURL)
459- if err != nil {
460- log.Warn().Err(err).
461- Str("url_host", logging.SafeURLHost(initialURL)).
462- Msg("ui: browser window factory failed")
463- return nil, err
464- }
465- created.ensureTabs()
466- a.wireBrowserWindowActivationTracking(created)
467- created.initChrome(ctx, a)
468- return created, nil
469- }
470-
471- runtimeCfg := a.runtimeConfigSnapshot().UI
472- mainWindow, err := window.New(ctx, a.gtkApp, runtimeCfg.Workspace.TabBarPosition)
473- if err != nil {
474- log.Warn().Err(err).
475- Str("url_host", logging.SafeURLHost(initialURL)).
476- Msg("ui: GTK browser window shell creation failed")
477- return nil, err
478- }
479- browserWindow := &browserWindow{
480- id: a.generateWindowID(),
481- initialURL: initialURL,
482- tabs: entity.NewTabList(),
483- mainWindow: mainWindow,
484- }
485- a.wireBrowserWindowActivationTracking(browserWindow)
486- if a.mainWindow == nil {
487- a.mainWindow = mainWindow
488- }
489-
490- closeRequestCb := func(_ gtk.Window) bool {
491- log.Info().Msg("browser window close requested")
492- a.removeBrowserWindow(browserWindow.id)
493- return false
494- }
495- mainWindow.Window().ConnectCloseRequest(&closeRequestCb)
496-
497- a.initBrowserWindowOverlays(mainWindow, browserWindow, runtimeCfg)
498- a.wireBrowserWindowPermissionIndicator(browserWindow)
499- if a.kbDispatcher != nil {
500- a.initBrowserWindowInput(ctx, browserWindow)
501- }
502- if a.tabCoord != nil {
503- a.wireBrowserWindowTabBar(ctx, browserWindow)
504- }
505- browserWindow.initChrome(ctx, a)
506-
507- // Apply GTK CSS styling from theme manager.
508- if a.deps == nil || a.deps.Theme == nil {
509- return browserWindow, nil
510- }
511- if display := mainWindow.Window().GetDisplay(); display != nil {
512- a.deps.Theme.ApplyToDisplay(ctx, display)
513- }
514- log.Debug().
515- Str("window_id", browserWindow.id).
516- Str("url_host", logging.SafeURLHost(initialURL)).
517- Msg("ui: create browser window completed")
518- return browserWindow, nil
519-}
520-
521 func (a *App) initBrowserWindowOverlays(mainWindow *window.MainWindow, browserWindow *browserWindow, runtimeCfg entity.RuntimeUIConfig) {449 func (a *App) initBrowserWindowOverlays(mainWindow *window.MainWindow, browserWindow *browserWindow, runtimeCfg entity.RuntimeUIConfig) {
522 // Create permission popup and dialog presenter450 // Create permission popup and dialog presenter
523 if a.deps != nil && a.deps.PermissionUC != nil {451 if a.deps != nil && a.deps.PermissionUC != nil {
@@ -830,192 +758,6 @@ func (a *App) closeBrowserLaunchRelayListener() {
830 a.browserLaunchRelayCloser = nil758 a.browserLaunchRelayCloser = nil
831 }759 }
832 760
833-func (a *App) openFreshWindow(ctx context.Context, url string) error {
834- log := logging.FromContext(ctx)
835- log.Debug().
836- Str("url_host", logging.SafeURLHost(url)).
837- Int("window_count_before", len(a.browserWindows)).
838- Bool("has_tab_coord", a.tabCoord != nil).
839- Bool("has_tabs_uc", a.tabsUC != nil).
840- Msg("ui: open fresh window started")
841-
842- created, err := a.createBrowserWindow(ctx, url)
843- if err != nil {
844- log.Warn().Err(err).
845- Str("url_host", logging.SafeURLHost(url)).
846- Int("window_count_after", len(a.browserWindows)).
847- Msg("ui: open fresh window could not create browser window")
848- return err
849- }
850- log.Debug().
851- Str("window_id", created.id).
852- Str("url_host", logging.SafeURLHost(url)).
853- Msg("ui: open fresh window created browser window shell")
854- a.registerBrowserWindow(created)
855- log.Debug().
856- Str("window_id", created.id).
857- Int("window_count_after_register", len(a.browserWindows)).
858- Msg("ui: open fresh window registered browser window")
859-
860- if len(a.browserWindows) == 1 {
861- log.Debug().Str("window_id", created.id).Msg("ui: activating first browser window")
862- a.activateBrowserWindow(created)
863- return nil
864- }
865-
866- var openErr error
867- path := "tabs_uc"
868- if a.tabCoord != nil {
869- path = "tab_coord"
870- log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tab coordinator")
871- openErr = a.openFreshWindowWithTabCoord(ctx, url, created)
872- } else if a.tabsUC == nil {
873- log.Debug().Str("window_id", created.id).Msg("ui: activating fresh browser window without tab use case")
874- a.activateBrowserWindow(created)
875- return nil
876- } else {
877- log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tabs use case")
878- openErr = a.openFreshWindowWithTabsUC(ctx, url, created)
879- }
880- if openErr != nil {
881- log.Warn().Err(openErr).
882- Str("window_id", created.id).
883- Str("path", path).
884- Int("window_count_after", len(a.browserWindows)).
885- Msg("ui: open fresh window initial tab creation failed")
886- return openErr
887- }
888- log.Debug().
889- Str("window_id", created.id).
890- Str("path", path).
891- Msg("ui: activating fresh browser window after tab creation")
892- a.activateBrowserWindow(created)
893- return nil
894-}
895-
896-func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, created *browserWindow) error {
897- log := logging.FromContext(ctx)
898- previousTarget := a.tabCoord.CurrentTarget()
899- target := a.ensureTabTargetForBrowserWindow(created)
900- a.tabCoord.SetCurrentTarget(target)
901- defer func() {
902- a.tabCoord.SetCurrentTarget(previousTarget)
903- }()
904-
905- log.Debug().
906- Str("window_id", created.id).
907- Str("url_host", logging.SafeURLHost(url)).
908- Msg("ui: tab coordinator create started for fresh window")
909- createdTab, createErr := a.tabCoord.Create(ctx, target, url)
910- if createErr != nil {
911- log.Warn().Err(createErr).
912- Str("window_id", created.id).
913- Msg("ui: tab coordinator create failed for fresh window; rolling back window")
914- a.removeBrowserWindow(created.id)
915- if created.mainWindow != nil {
916- created.mainWindow.Destroy()
917- }
918- return createErr
919- }
920- log.Debug().
921- Str("window_id", created.id).
922- Str("tab_id", string(createdTab.ID)).
923- Msg("ui: tab coordinator create completed for fresh window")
924- if a.workspaceViews[createdTab.ID] == nil {
925- log.Warn().
926- Str("window_id", created.id).
927- Str("tab_id", string(createdTab.ID)).
928- Msg("ui: fresh window tab has no workspace view; rolling back window")
929- if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil {
930- createdTabs.Remove(createdTab.ID)
931- }
932- if created.mainWindow != nil {
933- if tabBar := created.mainWindow.TabBar(); tabBar != nil {
934- tabBar.RemoveTab(createdTab.ID)
935- if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil {
936- tabBar.SetActive(activeTab.ID)
937- }
938- }
939- }
940- a.removeBrowserWindow(created.id)
941- if created.mainWindow != nil {
942- created.mainWindow.Destroy()
943- }
944- return fmt.Errorf("workspace view not created for tab %s", createdTab.ID)
945- }
946- a.setBrowserWindowForTab(createdTab.ID, created)
947- // Tab was already created in the per-window TabList via tabCoord.Create;
948- // active state is managed by TabList.SetActive.
949- if created.mainWindow != nil {
950- created.mainWindow.Show()
951- }
952- return nil
953-}
954-
955-func (a *App) openFreshWindowWithTabsUC(ctx context.Context, url string, created *browserWindow) error {
956- log := logging.FromContext(ctx)
957- log.Debug().
958- Str("window_id", created.id).
959- Str("url_host", logging.SafeURLHost(url)).
960- Msg("ui: tabs use case create started for fresh window")
961- output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{
962- TabList: a.tabs,
963- InitialURL: url,
964- })
965- if err != nil {
966- log.Warn().Err(err).
967- Str("window_id", created.id).
968- Msg("ui: tabs use case create failed for fresh window; rolling back window")
969- a.removeBrowserWindow(created.id)
970- if created.mainWindow != nil {
971- created.mainWindow.Destroy()
972- }
973- return err
974- }
975- log.Debug().
976- Str("window_id", created.id).
977- Str("tab_id", string(output.Tab.ID)).
978- Msg("ui: tabs use case create completed for fresh window")
979- a.setBrowserWindowForTab(output.Tab.ID, created)
980- // Mirror the tabsUC-created tab into the new browser window's tab list.
981- // This path uses the tabs usecase directly instead of tabCoord.Create,
982- // so the per-window TabList must be populated explicitly.
983- a.ensureTabListForBrowserWindow(created).Add(output.Tab)
984- if a.widgetFactory != nil {
985- a.createWorkspaceView(ctx, output.Tab)
986- a.switchWorkspaceView(ctx, output.Tab.ID)
987- }
988- if a.workspaceViews[output.Tab.ID] == nil {
989- log.Warn().
990- Str("window_id", created.id).
991- Str("tab_id", string(output.Tab.ID)).
992- Msg("ui: fresh window tabs use case tab has no workspace view; rolling back window")
993- if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil {
994- createdTabs.Remove(output.Tab.ID)
995- }
996- if a.tabs != nil {
997- a.tabs.Remove(output.Tab.ID)
998- }
999- if created.mainWindow != nil {
1000- if tabBar := created.mainWindow.TabBar(); tabBar != nil {
1001- tabBar.RemoveTab(output.Tab.ID)
1002- if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil {
1003- tabBar.SetActive(activeTab.ID)
1004- }
1005- }
1006- }
1007- a.removeBrowserWindow(created.id)
1008- if created.mainWindow != nil {
1009- created.mainWindow.Destroy()
1010- }
1011- return fmt.Errorf("workspace view not created for tab %s", output.Tab.ID)
1012- }
1013- if created.mainWindow != nil {
1014- created.mainWindow.Show()
1015- }
1016- return nil
1017-}
1018-
1019 func (a *App) initialWindowURL() string {761 func (a *App) initialWindowURL() string {
1020 if a.deps != nil {762 if a.deps != nil {
1021 return urlutil.ResolveBrowserStartupURL(a.deps.InitialURL)763 return urlutil.ResolveBrowserStartupURL(a.deps.InitialURL)
A internal/ui/app_window_lifecycle.go +270 -0
@@ -0,0 +1,270 @@
1+package ui
2+
3+import (
4+ "context"
5+ "fmt"
6+
7+ "github.com/bnema/dumber/internal/application/usecase"
8+ "github.com/bnema/dumber/internal/domain/entity"
9+ "github.com/bnema/dumber/internal/logging"
10+ "github.com/bnema/dumber/internal/ui/window"
11+ "github.com/bnema/puregotk/v4/gtk"
12+)
13+
14+func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*browserWindow, error) {
15+ log := logging.FromContext(ctx)
16+ log.Debug().
17+ Str("url_host", logging.SafeURLHost(initialURL)).
18+ Bool("using_factory", a.browserWindowFactory != nil).
19+ Int("window_count_before", len(a.browserWindows)).
20+ Msg("ui: create browser window started")
21+
22+ if a.browserWindowFactory != nil {
23+ created, err := a.browserWindowFactory(ctx, initialURL)
24+ if err != nil {
25+ log.Warn().Err(err).
26+ Str("url_host", logging.SafeURLHost(initialURL)).
27+ Msg("ui: browser window factory failed")
28+ return nil, err
29+ }
30+ created.ensureTabs()
31+ a.wireBrowserWindowActivationTracking(created)
32+ created.initChrome(ctx, a)
33+ return created, nil
34+ }
35+
36+ runtimeCfg := a.runtimeConfigSnapshot().UI
37+ mainWindow, err := window.New(ctx, a.gtkApp, runtimeCfg.Workspace.TabBarPosition)
38+ if err != nil {
39+ log.Warn().Err(err).
40+ Str("url_host", logging.SafeURLHost(initialURL)).
41+ Msg("ui: GTK browser window shell creation failed")
42+ return nil, err
43+ }
44+ browserWindow := &browserWindow{
45+ id: a.generateWindowID(),
46+ initialURL: initialURL,
47+ tabs: entity.NewTabList(),
48+ mainWindow: mainWindow,
49+ }
50+ a.wireBrowserWindowActivationTracking(browserWindow)
51+ if a.mainWindow == nil {
52+ a.mainWindow = mainWindow
53+ }
54+
55+ closeRequestCb := func(_ gtk.Window) bool {
56+ log.Info().Msg("browser window close requested")
57+ a.removeBrowserWindow(browserWindow.id)
58+ return false
59+ }
60+ mainWindow.Window().ConnectCloseRequest(&closeRequestCb)
61+
62+ a.initBrowserWindowOverlays(mainWindow, browserWindow, runtimeCfg)
63+ a.wireBrowserWindowPermissionIndicator(browserWindow)
64+ if a.kbDispatcher != nil {
65+ a.initBrowserWindowInput(ctx, browserWindow)
66+ }
67+ if a.tabCoord != nil {
68+ a.wireBrowserWindowTabBar(ctx, browserWindow)
69+ }
70+ browserWindow.initChrome(ctx, a)
71+
72+ // Apply GTK CSS styling from theme manager.
73+ if a.deps == nil || a.deps.Theme == nil {
74+ return browserWindow, nil
75+ }
76+ if display := mainWindow.Window().GetDisplay(); display != nil {
77+ a.deps.Theme.ApplyToDisplay(ctx, display)
78+ }
79+ log.Debug().
80+ Str("window_id", browserWindow.id).
81+ Str("url_host", logging.SafeURLHost(initialURL)).
82+ Msg("ui: create browser window completed")
83+ return browserWindow, nil
84+}
85+
86+func (a *App) openFreshWindow(ctx context.Context, url string) error {
87+ log := logging.FromContext(ctx)
88+ log.Debug().
89+ Str("url_host", logging.SafeURLHost(url)).
90+ Int("window_count_before", len(a.browserWindows)).
91+ Bool("has_tab_coord", a.tabCoord != nil).
92+ Bool("has_tabs_uc", a.tabsUC != nil).
93+ Msg("ui: open fresh window started")
94+
95+ created, err := a.createBrowserWindow(ctx, url)
96+ if err != nil {
97+ log.Warn().Err(err).
98+ Str("url_host", logging.SafeURLHost(url)).
99+ Int("window_count_after", len(a.browserWindows)).
100+ Msg("ui: open fresh window could not create browser window")
101+ return err
102+ }
103+ log.Debug().
104+ Str("window_id", created.id).
105+ Str("url_host", logging.SafeURLHost(url)).
106+ Msg("ui: open fresh window created browser window shell")
107+ a.registerBrowserWindow(created)
108+ log.Debug().
109+ Str("window_id", created.id).
110+ Int("window_count_after_register", len(a.browserWindows)).
111+ Msg("ui: open fresh window registered browser window")
112+
113+ if len(a.browserWindows) == 1 {
114+ log.Debug().Str("window_id", created.id).Msg("ui: activating first browser window")
115+ a.activateBrowserWindow(created)
116+ return nil
117+ }
118+
119+ var openErr error
120+ path := "tabs_uc"
121+ if a.tabCoord != nil {
122+ path = "tab_coord"
123+ log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tab coordinator")
124+ openErr = a.openFreshWindowWithTabCoord(ctx, url, created)
125+ } else if a.tabsUC == nil {
126+ log.Debug().Str("window_id", created.id).Msg("ui: activating fresh browser window without tab use case")
127+ a.activateBrowserWindow(created)
128+ return nil
129+ } else {
130+ log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tabs use case")
131+ openErr = a.openFreshWindowWithTabsUC(ctx, url, created)
132+ }
133+ if openErr != nil {
134+ log.Warn().Err(openErr).
135+ Str("window_id", created.id).
136+ Str("path", path).
137+ Int("window_count_after", len(a.browserWindows)).
138+ Msg("ui: open fresh window initial tab creation failed")
139+ return openErr
140+ }
141+ log.Debug().
142+ Str("window_id", created.id).
143+ Str("path", path).
144+ Msg("ui: activating fresh browser window after tab creation")
145+ a.activateBrowserWindow(created)
146+ return nil
147+}
148+
149+func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, created *browserWindow) error {
150+ log := logging.FromContext(ctx)
151+ previousTarget := a.tabCoord.CurrentTarget()
152+ target := a.ensureTabTargetForBrowserWindow(created)
153+ a.tabCoord.SetCurrentTarget(target)
154+ defer func() {
155+ a.tabCoord.SetCurrentTarget(previousTarget)
156+ }()
157+
158+ log.Debug().
159+ Str("window_id", created.id).
160+ Str("url_host", logging.SafeURLHost(url)).
161+ Msg("ui: tab coordinator create started for fresh window")
162+ createdTab, createErr := a.tabCoord.Create(ctx, target, url)
163+ if createErr != nil {
164+ log.Warn().Err(createErr).
165+ Str("window_id", created.id).
166+ Msg("ui: tab coordinator create failed for fresh window; rolling back window")
167+ a.removeBrowserWindow(created.id)
168+ if created.mainWindow != nil {
169+ created.mainWindow.Destroy()
170+ }
171+ return createErr
172+ }
173+ log.Debug().
174+ Str("window_id", created.id).
175+ Str("tab_id", string(createdTab.ID)).
176+ Msg("ui: tab coordinator create completed for fresh window")
177+ if a.workspaceViews[createdTab.ID] == nil {
178+ log.Warn().
179+ Str("window_id", created.id).
180+ Str("tab_id", string(createdTab.ID)).
181+ Msg("ui: fresh window tab has no workspace view; rolling back window")
182+ if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil {
183+ createdTabs.Remove(createdTab.ID)
184+ }
185+ if created.mainWindow != nil {
186+ if tabBar := created.mainWindow.TabBar(); tabBar != nil {
187+ tabBar.RemoveTab(createdTab.ID)
188+ if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil {
189+ tabBar.SetActive(activeTab.ID)
190+ }
191+ }
192+ }
193+ a.removeBrowserWindow(created.id)
194+ if created.mainWindow != nil {
195+ created.mainWindow.Destroy()
196+ }
197+ return fmt.Errorf("workspace view not created for tab %s", createdTab.ID)
198+ }
199+ a.setBrowserWindowForTab(createdTab.ID, created)
200+ // Tab was already created in the per-window TabList via tabCoord.Create;
201+ // active state is managed by TabList.SetActive.
202+ if created.mainWindow != nil {
203+ created.mainWindow.Show()
204+ }
205+ return nil
206+}
207+
208+func (a *App) openFreshWindowWithTabsUC(ctx context.Context, url string, created *browserWindow) error {
209+ log := logging.FromContext(ctx)
210+ log.Debug().
211+ Str("window_id", created.id).
212+ Str("url_host", logging.SafeURLHost(url)).
213+ Msg("ui: tabs use case create started for fresh window")
214+ output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{
215+ TabList: a.tabs,
216+ InitialURL: url,
217+ })
218+ if err != nil {
219+ log.Warn().Err(err).
220+ Str("window_id", created.id).
221+ Msg("ui: tabs use case create failed for fresh window; rolling back window")
222+ a.removeBrowserWindow(created.id)
223+ if created.mainWindow != nil {
224+ created.mainWindow.Destroy()
225+ }
226+ return err
227+ }
228+ log.Debug().
229+ Str("window_id", created.id).
230+ Str("tab_id", string(output.Tab.ID)).
231+ Msg("ui: tabs use case create completed for fresh window")
232+ a.setBrowserWindowForTab(output.Tab.ID, created)
233+ // Mirror the tabsUC-created tab into the new browser window's tab list.
234+ // This path uses the tabs usecase directly instead of tabCoord.Create,
235+ // so the per-window TabList must be populated explicitly.
236+ a.ensureTabListForBrowserWindow(created).Add(output.Tab)
237+ if a.widgetFactory != nil {
238+ a.createWorkspaceView(ctx, output.Tab)
239+ a.switchWorkspaceView(ctx, output.Tab.ID)
240+ }
241+ if a.workspaceViews[output.Tab.ID] == nil {
242+ log.Warn().
243+ Str("window_id", created.id).
244+ Str("tab_id", string(output.Tab.ID)).
245+ Msg("ui: fresh window tabs use case tab has no workspace view; rolling back window")
246+ if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil {
247+ createdTabs.Remove(output.Tab.ID)
248+ }
249+ if a.tabs != nil {
250+ a.tabs.Remove(output.Tab.ID)
251+ }
252+ if created.mainWindow != nil {
253+ if tabBar := created.mainWindow.TabBar(); tabBar != nil {
254+ tabBar.RemoveTab(output.Tab.ID)
255+ if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil {
256+ tabBar.SetActive(activeTab.ID)
257+ }
258+ }
259+ }
260+ a.removeBrowserWindow(created.id)
261+ if created.mainWindow != nil {
262+ created.mainWindow.Destroy()
263+ }
264+ return fmt.Errorf("workspace view not created for tab %s", output.Tab.ID)
265+ }
266+ if created.mainWindow != nil {
267+ created.mainWindow.Show()
268+ }
269+ return nil
270+}
@@ -0,0 +1,270 @@
1+package ui
2+
3+import (
4+ "context"
5+ "fmt"
6+
7+ "github.com/bnema/dumber/internal/application/usecase"
8+ "github.com/bnema/dumber/internal/domain/entity"
9+ "github.com/bnema/dumber/internal/logging"
10+ "github.com/bnema/dumber/internal/ui/window"
11+ "github.com/bnema/puregotk/v4/gtk"
12+)
13+
14+func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*browserWindow, error) {
15+ log := logging.FromContext(ctx)
16+ log.Debug().
17+ Str("url_host", logging.SafeURLHost(initialURL)).
18+ Bool("using_factory", a.browserWindowFactory != nil).
19+ Int("window_count_before", len(a.browserWindows)).
20+ Msg("ui: create browser window started")
21+
22+ if a.browserWindowFactory != nil {
23+ created, err := a.browserWindowFactory(ctx, initialURL)
24+ if err != nil {
25+ log.Warn().Err(err).
26+ Str("url_host", logging.SafeURLHost(initialURL)).
27+ Msg("ui: browser window factory failed")
28+ return nil, err
29+ }
30+ created.ensureTabs()
31+ a.wireBrowserWindowActivationTracking(created)
32+ created.initChrome(ctx, a)
33+ return created, nil
34+ }
35+
36+ runtimeCfg := a.runtimeConfigSnapshot().UI
37+ mainWindow, err := window.New(ctx, a.gtkApp, runtimeCfg.Workspace.TabBarPosition)
38+ if err != nil {
39+ log.Warn().Err(err).
40+ Str("url_host", logging.SafeURLHost(initialURL)).
41+ Msg("ui: GTK browser window shell creation failed")
42+ return nil, err
43+ }
44+ browserWindow := &browserWindow{
45+ id: a.generateWindowID(),
46+ initialURL: initialURL,
47+ tabs: entity.NewTabList(),
48+ mainWindow: mainWindow,
49+ }
50+ a.wireBrowserWindowActivationTracking(browserWindow)
51+ if a.mainWindow == nil {
52+ a.mainWindow = mainWindow
53+ }
54+
55+ closeRequestCb := func(_ gtk.Window) bool {
56+ log.Info().Msg("browser window close requested")
57+ a.removeBrowserWindow(browserWindow.id)
58+ return false
59+ }
60+ mainWindow.Window().ConnectCloseRequest(&closeRequestCb)
61+
62+ a.initBrowserWindowOverlays(mainWindow, browserWindow, runtimeCfg)
63+ a.wireBrowserWindowPermissionIndicator(browserWindow)
64+ if a.kbDispatcher != nil {
65+ a.initBrowserWindowInput(ctx, browserWindow)
66+ }
67+ if a.tabCoord != nil {
68+ a.wireBrowserWindowTabBar(ctx, browserWindow)
69+ }
70+ browserWindow.initChrome(ctx, a)
71+
72+ // Apply GTK CSS styling from theme manager.
73+ if a.deps == nil || a.deps.Theme == nil {
74+ return browserWindow, nil
75+ }
76+ if display := mainWindow.Window().GetDisplay(); display != nil {
77+ a.deps.Theme.ApplyToDisplay(ctx, display)
78+ }
79+ log.Debug().
80+ Str("window_id", browserWindow.id).
81+ Str("url_host", logging.SafeURLHost(initialURL)).
82+ Msg("ui: create browser window completed")
83+ return browserWindow, nil
84+}
85+
86+func (a *App) openFreshWindow(ctx context.Context, url string) error {
87+ log := logging.FromContext(ctx)
88+ log.Debug().
89+ Str("url_host", logging.SafeURLHost(url)).
90+ Int("window_count_before", len(a.browserWindows)).
91+ Bool("has_tab_coord", a.tabCoord != nil).
92+ Bool("has_tabs_uc", a.tabsUC != nil).
93+ Msg("ui: open fresh window started")
94+
95+ created, err := a.createBrowserWindow(ctx, url)
96+ if err != nil {
97+ log.Warn().Err(err).
98+ Str("url_host", logging.SafeURLHost(url)).
99+ Int("window_count_after", len(a.browserWindows)).
100+ Msg("ui: open fresh window could not create browser window")
101+ return err
102+ }
103+ log.Debug().
104+ Str("window_id", created.id).
105+ Str("url_host", logging.SafeURLHost(url)).
106+ Msg("ui: open fresh window created browser window shell")
107+ a.registerBrowserWindow(created)
108+ log.Debug().
109+ Str("window_id", created.id).
110+ Int("window_count_after_register", len(a.browserWindows)).
111+ Msg("ui: open fresh window registered browser window")
112+
113+ if len(a.browserWindows) == 1 {
114+ log.Debug().Str("window_id", created.id).Msg("ui: activating first browser window")
115+ a.activateBrowserWindow(created)
116+ return nil
117+ }
118+
119+ var openErr error
120+ path := "tabs_uc"
121+ if a.tabCoord != nil {
122+ path = "tab_coord"
123+ log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tab coordinator")
124+ openErr = a.openFreshWindowWithTabCoord(ctx, url, created)
125+ } else if a.tabsUC == nil {
126+ log.Debug().Str("window_id", created.id).Msg("ui: activating fresh browser window without tab use case")
127+ a.activateBrowserWindow(created)
128+ return nil
129+ } else {
130+ log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tabs use case")
131+ openErr = a.openFreshWindowWithTabsUC(ctx, url, created)
132+ }
133+ if openErr != nil {
134+ log.Warn().Err(openErr).
135+ Str("window_id", created.id).
136+ Str("path", path).
137+ Int("window_count_after", len(a.browserWindows)).
138+ Msg("ui: open fresh window initial tab creation failed")
139+ return openErr
140+ }
141+ log.Debug().
142+ Str("window_id", created.id).
143+ Str("path", path).
144+ Msg("ui: activating fresh browser window after tab creation")
145+ a.activateBrowserWindow(created)
146+ return nil
147+}
148+
149+func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, created *browserWindow) error {
150+ log := logging.FromContext(ctx)
151+ previousTarget := a.tabCoord.CurrentTarget()
152+ target := a.ensureTabTargetForBrowserWindow(created)
153+ a.tabCoord.SetCurrentTarget(target)
154+ defer func() {
155+ a.tabCoord.SetCurrentTarget(previousTarget)
156+ }()
157+
158+ log.Debug().
159+ Str("window_id", created.id).
160+ Str("url_host", logging.SafeURLHost(url)).
161+ Msg("ui: tab coordinator create started for fresh window")
162+ createdTab, createErr := a.tabCoord.Create(ctx, target, url)
163+ if createErr != nil {
164+ log.Warn().Err(createErr).
165+ Str("window_id", created.id).
166+ Msg("ui: tab coordinator create failed for fresh window; rolling back window")
167+ a.removeBrowserWindow(created.id)
168+ if created.mainWindow != nil {
169+ created.mainWindow.Destroy()
170+ }
171+ return createErr
172+ }
173+ log.Debug().
174+ Str("window_id", created.id).
175+ Str("tab_id", string(createdTab.ID)).
176+ Msg("ui: tab coordinator create completed for fresh window")
177+ if a.workspaceViews[createdTab.ID] == nil {
178+ log.Warn().
179+ Str("window_id", created.id).
180+ Str("tab_id", string(createdTab.ID)).
181+ Msg("ui: fresh window tab has no workspace view; rolling back window")
182+ if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil {
183+ createdTabs.Remove(createdTab.ID)
184+ }
185+ if created.mainWindow != nil {
186+ if tabBar := created.mainWindow.TabBar(); tabBar != nil {
187+ tabBar.RemoveTab(createdTab.ID)
188+ if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil {
189+ tabBar.SetActive(activeTab.ID)
190+ }
191+ }
192+ }
193+ a.removeBrowserWindow(created.id)
194+ if created.mainWindow != nil {
195+ created.mainWindow.Destroy()
196+ }
197+ return fmt.Errorf("workspace view not created for tab %s", createdTab.ID)
198+ }
199+ a.setBrowserWindowForTab(createdTab.ID, created)
200+ // Tab was already created in the per-window TabList via tabCoord.Create;
201+ // active state is managed by TabList.SetActive.
202+ if created.mainWindow != nil {
203+ created.mainWindow.Show()
204+ }
205+ return nil
206+}
207+
208+func (a *App) openFreshWindowWithTabsUC(ctx context.Context, url string, created *browserWindow) error {
209+ log := logging.FromContext(ctx)
210+ log.Debug().
211+ Str("window_id", created.id).
212+ Str("url_host", logging.SafeURLHost(url)).
213+ Msg("ui: tabs use case create started for fresh window")
214+ output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{
215+ TabList: a.tabs,
216+ InitialURL: url,
217+ })
218+ if err != nil {
219+ log.Warn().Err(err).
220+ Str("window_id", created.id).
221+ Msg("ui: tabs use case create failed for fresh window; rolling back window")
222+ a.removeBrowserWindow(created.id)
223+ if created.mainWindow != nil {
224+ created.mainWindow.Destroy()
225+ }
226+ return err
227+ }
228+ log.Debug().
229+ Str("window_id", created.id).
230+ Str("tab_id", string(output.Tab.ID)).
231+ Msg("ui: tabs use case create completed for fresh window")
232+ a.setBrowserWindowForTab(output.Tab.ID, created)
233+ // Mirror the tabsUC-created tab into the new browser window's tab list.
234+ // This path uses the tabs usecase directly instead of tabCoord.Create,
235+ // so the per-window TabList must be populated explicitly.
236+ a.ensureTabListForBrowserWindow(created).Add(output.Tab)
237+ if a.widgetFactory != nil {
238+ a.createWorkspaceView(ctx, output.Tab)
239+ a.switchWorkspaceView(ctx, output.Tab.ID)
240+ }
241+ if a.workspaceViews[output.Tab.ID] == nil {
242+ log.Warn().
243+ Str("window_id", created.id).
244+ Str("tab_id", string(output.Tab.ID)).
245+ Msg("ui: fresh window tabs use case tab has no workspace view; rolling back window")
246+ if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil {
247+ createdTabs.Remove(output.Tab.ID)
248+ }
249+ if a.tabs != nil {
250+ a.tabs.Remove(output.Tab.ID)
251+ }
252+ if created.mainWindow != nil {
253+ if tabBar := created.mainWindow.TabBar(); tabBar != nil {
254+ tabBar.RemoveTab(output.Tab.ID)
255+ if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil {
256+ tabBar.SetActive(activeTab.ID)
257+ }
258+ }
259+ }
260+ a.removeBrowserWindow(created.id)
261+ if created.mainWindow != nil {
262+ created.mainWindow.Destroy()
263+ }
264+ return fmt.Errorf("workspace view not created for tab %s", output.Tab.ID)
265+ }
266+ if created.mainWindow != nil {
267+ created.mainWindow.Show()
268+ }
269+ return nil
270+}
M internal/ui/browser_window_test.go +28 -0
@@ -459,6 +459,34 @@ func TestOpenFreshWindow_DispatchesAndTracksBrowserWindow(t *testing.T) {
459459 }
460460 }
461461
462+func TestOpenFreshWindow_FirstWindowRegistersShellWithoutCreatingTab(t *testing.T) {
463+ app := &App{
464+ tabs: entity.NewTabList(),
465+ tabsUC: usecase.NewManageTabsUseCase(func() string { return "unexpected-tab" }),
466+ browserWindows: make(map[string]*browserWindow),
467+ }
468+ app.dispatchOnMainThread = func(label string, fn func()) syncdispatch.SyncDispatchResult {
469+ fn()
470+ return syncdispatch.SyncDispatchResult{Label: label, Status: syncdispatch.SyncDispatchCompleted}
471+ }
472+ app.browserWindowFactory = func(context.Context, string) (*browserWindow, error) {
473+ return &browserWindow{id: "window-1", tabs: entity.NewTabList()}, nil
474+ }
475+
476+ if err := app.OpenFreshWindow(context.Background(), "https://example.com"); err != nil {
477+ t.Fatalf("OpenFreshWindow returned error: %v", err)
478+ }
479+ if got := len(app.browserWindows); got != 1 {
480+ t.Fatalf("browserWindows length = %d, want 1", got)
481+ }
482+ if app.lastFocusedWindowID != "window-1" {
483+ t.Fatalf("lastFocusedWindowID = %q, want window-1", app.lastFocusedWindowID)
484+ }
485+ if got := app.tabs.Count(); got != 0 {
486+ t.Fatalf("tabs count = %d, want 0; first shell should wait for createInitialTab", got)
487+ }
488+}
489+
462490 func TestOpenFreshWindow_PropagatesFactoryError(t *testing.T) {
463491 app := &App{browserWindows: make(map[string]*browserWindow)}
464492 app.dispatchOnMainThread = func(label string, fn func()) syncdispatch.SyncDispatchResult {
@@ -459,6 +459,34 @@ func TestOpenFreshWindow_DispatchesAndTracksBrowserWindow(t *testing.T) {
459 }459 }
460 }460 }
461 461
462+func TestOpenFreshWindow_FirstWindowRegistersShellWithoutCreatingTab(t *testing.T) {
463+ app := &App{
464+ tabs: entity.NewTabList(),
465+ tabsUC: usecase.NewManageTabsUseCase(func() string { return "unexpected-tab" }),
466+ browserWindows: make(map[string]*browserWindow),
467+ }
468+ app.dispatchOnMainThread = func(label string, fn func()) syncdispatch.SyncDispatchResult {
469+ fn()
470+ return syncdispatch.SyncDispatchResult{Label: label, Status: syncdispatch.SyncDispatchCompleted}
471+ }
472+ app.browserWindowFactory = func(context.Context, string) (*browserWindow, error) {
473+ return &browserWindow{id: "window-1", tabs: entity.NewTabList()}, nil
474+ }
475+
476+ if err := app.OpenFreshWindow(context.Background(), "https://example.com"); err != nil {
477+ t.Fatalf("OpenFreshWindow returned error: %v", err)
478+ }
479+ if got := len(app.browserWindows); got != 1 {
480+ t.Fatalf("browserWindows length = %d, want 1", got)
481+ }
482+ if app.lastFocusedWindowID != "window-1" {
483+ t.Fatalf("lastFocusedWindowID = %q, want window-1", app.lastFocusedWindowID)
484+ }
485+ if got := app.tabs.Count(); got != 0 {
486+ t.Fatalf("tabs count = %d, want 0; first shell should wait for createInitialTab", got)
487+ }
488+}
489+
462 func TestOpenFreshWindow_PropagatesFactoryError(t *testing.T) {490 func TestOpenFreshWindow_PropagatesFactoryError(t *testing.T) {
463 app := &App{browserWindows: make(map[string]*browserWindow)}491 app := &App{browserWindows: make(map[string]*browserWindow)}
464 app.dispatchOnMainThread = func(label string, fn func()) syncdispatch.SyncDispatchResult {492 app.dispatchOnMainThread = func(label string, fn func()) syncdispatch.SyncDispatchResult {

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