detached f108dcf

fix(ui): stabilize external link routing checks

commit
f108dcf2e607f20b29a7342eea617cc976484155
author
brice · · 2026-08-13 04:15 UTC
committer
brice · · 2026-08-13 04:39 UTC
parent
cf4e69e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +84 -81
M internal/ui/browser_window.go +34 -19
@@ -629,32 +629,17 @@ func (a *App) openExternalURLOnMainThread(ctx context.Context, url string, cfg e
629629 }
630630
631631 func (a *App) openExternalURLInFocusedWindow(ctx context.Context, url string, cfg entity.ExternalLinksConfig) error {
632- target := a.lastFocusedBrowserWindow()
633- if target == nil || target.id == "" || a.browserWindows[target.id] != target {
634- return fmt.Errorf("last-focused browser window is missing or stale")
635- }
636- activeTab := a.activeTabForBrowserWindow(target)
637- if activeTab == nil || activeTab.Workspace == nil || activeTab.Workspace.ActivePane() == nil {
638- return fmt.Errorf("last-focused browser window has no active tab and pane")
632+ target, activeTab, err := a.externalURLTarget()
633+ if err != nil {
634+ return err
639635 }
640636
641637 a.activateBrowserWindow(target)
642638 switch cfg.Behavior {
643639 case entity.ExternalLinkBehaviorTabbed:
644- if a.tabCoord == nil {
645- return fmt.Errorf("tab coordinator not available")
646- }
647- created, err := a.tabCoord.Create(ctx, a.tabTargetForBrowserWindow(target), url)
648- if err != nil {
649- a.rollbackExternalTabCreation(ctx, target, created)
640+ if err := a.createExternalURLTab(ctx, target, url); err != nil {
650641 return err
651642 }
652- if created == nil || created.Workspace == nil || created.Workspace.ActivePane() == nil ||
653- created.Workspace.ActivePane().Pane == nil || created.Workspace.ActivePane().Pane.URI != url ||
654- a.activeTabForBrowserWindow(target) != created || a.workspaceViews[created.ID] == nil {
655- a.rollbackExternalTabCreation(ctx, target, created)
656- return fmt.Errorf("created tab did not become an active UI target")
657- }
658643 case entity.ExternalLinkBehaviorSplit:
659644 if err := a.placeExternalURLInPane(ctx, activeTab.Workspace, url, func(ctx context.Context) error {
660645 return a.wsCoord.SplitWithURL(ctx, externalLinkSplitDirection(cfg.Placement), url)
@@ -678,6 +663,36 @@ func (a *App) openExternalURLInFocusedWindow(ctx context.Context, url string, cf
678663 return nil
679664 }
680665
666+func (a *App) externalURLTarget() (*browserWindow, *entity.Tab, error) {
667+ target := a.lastFocusedBrowserWindow()
668+ if target == nil || target.id == "" || a.browserWindows[target.id] != target {
669+ return nil, nil, fmt.Errorf("last-focused browser window is missing or stale")
670+ }
671+ activeTab := a.activeTabForBrowserWindow(target)
672+ if activeTab == nil || activeTab.Workspace == nil || activeTab.Workspace.ActivePane() == nil {
673+ return nil, nil, fmt.Errorf("last-focused browser window has no active tab and pane")
674+ }
675+ return target, activeTab, nil
676+}
677+
678+func (a *App) createExternalURLTab(ctx context.Context, target *browserWindow, url string) error {
679+ if a.tabCoord == nil {
680+ return fmt.Errorf("tab coordinator not available")
681+ }
682+ created, err := a.tabCoord.Create(ctx, a.tabTargetForBrowserWindow(target), url)
683+ if err != nil {
684+ a.rollbackExternalTabCreation(ctx, target, created)
685+ return err
686+ }
687+ if created == nil || created.Workspace == nil || created.Workspace.ActivePane() == nil ||
688+ created.Workspace.ActivePane().Pane == nil || created.Workspace.ActivePane().Pane.URI != url ||
689+ a.activeTabForBrowserWindow(target) != created || a.workspaceViews[created.ID] == nil {
690+ a.rollbackExternalTabCreation(ctx, target, created)
691+ return fmt.Errorf("created tab did not become an active UI target")
692+ }
693+ return nil
694+}
695+
681696 func (a *App) placeExternalURLInPane(ctx context.Context, ws *entity.Workspace, url string, place func(context.Context) error) error {
682697 if a.wsCoord == nil {
683698 return fmt.Errorf("workspace coordinator not available")
@@ -629,32 +629,17 @@ func (a *App) openExternalURLOnMainThread(ctx context.Context, url string, cfg e
629 }629 }
630 630
631 func (a *App) openExternalURLInFocusedWindow(ctx context.Context, url string, cfg entity.ExternalLinksConfig) error {631 func (a *App) openExternalURLInFocusedWindow(ctx context.Context, url string, cfg entity.ExternalLinksConfig) error {
632- target := a.lastFocusedBrowserWindow()632+ target, activeTab, err := a.externalURLTarget()
633- if target == nil || target.id == "" || a.browserWindows[target.id] != target {633+ if err != nil {
634- return fmt.Errorf("last-focused browser window is missing or stale")634+ return err
635- }
636- activeTab := a.activeTabForBrowserWindow(target)
637- if activeTab == nil || activeTab.Workspace == nil || activeTab.Workspace.ActivePane() == nil {
638- return fmt.Errorf("last-focused browser window has no active tab and pane")
639 }635 }
640 636
641 a.activateBrowserWindow(target)637 a.activateBrowserWindow(target)
642 switch cfg.Behavior {638 switch cfg.Behavior {
643 case entity.ExternalLinkBehaviorTabbed:639 case entity.ExternalLinkBehaviorTabbed:
644- if a.tabCoord == nil {640+ if err := a.createExternalURLTab(ctx, target, url); err != nil {
645- return fmt.Errorf("tab coordinator not available")
646- }
647- created, err := a.tabCoord.Create(ctx, a.tabTargetForBrowserWindow(target), url)
648- if err != nil {
649- a.rollbackExternalTabCreation(ctx, target, created)
650 return err641 return err
651 }642 }
652- if created == nil || created.Workspace == nil || created.Workspace.ActivePane() == nil ||
653- created.Workspace.ActivePane().Pane == nil || created.Workspace.ActivePane().Pane.URI != url ||
654- a.activeTabForBrowserWindow(target) != created || a.workspaceViews[created.ID] == nil {
655- a.rollbackExternalTabCreation(ctx, target, created)
656- return fmt.Errorf("created tab did not become an active UI target")
657- }
658 case entity.ExternalLinkBehaviorSplit:643 case entity.ExternalLinkBehaviorSplit:
659 if err := a.placeExternalURLInPane(ctx, activeTab.Workspace, url, func(ctx context.Context) error {644 if err := a.placeExternalURLInPane(ctx, activeTab.Workspace, url, func(ctx context.Context) error {
660 return a.wsCoord.SplitWithURL(ctx, externalLinkSplitDirection(cfg.Placement), url)645 return a.wsCoord.SplitWithURL(ctx, externalLinkSplitDirection(cfg.Placement), url)
@@ -678,6 +663,36 @@ func (a *App) openExternalURLInFocusedWindow(ctx context.Context, url string, cf
678 return nil663 return nil
679 }664 }
680 665
666+func (a *App) externalURLTarget() (*browserWindow, *entity.Tab, error) {
667+ target := a.lastFocusedBrowserWindow()
668+ if target == nil || target.id == "" || a.browserWindows[target.id] != target {
669+ return nil, nil, fmt.Errorf("last-focused browser window is missing or stale")
670+ }
671+ activeTab := a.activeTabForBrowserWindow(target)
672+ if activeTab == nil || activeTab.Workspace == nil || activeTab.Workspace.ActivePane() == nil {
673+ return nil, nil, fmt.Errorf("last-focused browser window has no active tab and pane")
674+ }
675+ return target, activeTab, nil
676+}
677+
678+func (a *App) createExternalURLTab(ctx context.Context, target *browserWindow, url string) error {
679+ if a.tabCoord == nil {
680+ return fmt.Errorf("tab coordinator not available")
681+ }
682+ created, err := a.tabCoord.Create(ctx, a.tabTargetForBrowserWindow(target), url)
683+ if err != nil {
684+ a.rollbackExternalTabCreation(ctx, target, created)
685+ return err
686+ }
687+ if created == nil || created.Workspace == nil || created.Workspace.ActivePane() == nil ||
688+ created.Workspace.ActivePane().Pane == nil || created.Workspace.ActivePane().Pane.URI != url ||
689+ a.activeTabForBrowserWindow(target) != created || a.workspaceViews[created.ID] == nil {
690+ a.rollbackExternalTabCreation(ctx, target, created)
691+ return fmt.Errorf("created tab did not become an active UI target")
692+ }
693+ return nil
694+}
695+
681 func (a *App) placeExternalURLInPane(ctx context.Context, ws *entity.Workspace, url string, place func(context.Context) error) error {696 func (a *App) placeExternalURLInPane(ctx context.Context, ws *entity.Workspace, url string, place func(context.Context) error) error {
682 if a.wsCoord == nil {697 if a.wsCoord == nil {
683 return fmt.Errorf("workspace coordinator not available")698 return fmt.Errorf("workspace coordinator not available")
M internal/ui/coordinator/workspace.go +48 -61
@@ -198,7 +198,10 @@ func (c *WorkspaceCoordinator) splitWithInitialURL(ctx context.Context, directio
198198
199199 // Update the workspace view
200200 if splitCtx.wsView != nil {
201- if err := c.applySplitToView(ctx, splitCtx.wsView, splitCtx.ws, output, direction, splitCtx.existingWidget, splitCtx.isStackSplit, oldActivePaneID); err != nil {
201+ if err := c.applySplitToView(
202+ ctx, splitCtx.wsView, splitCtx.ws, output, direction,
203+ splitCtx.existingWidget, splitCtx.isStackSplit, oldActivePaneID,
204+ ); err != nil {
202205 return err
203206 }
204207 splitCtx.wsView.NotifyNewPaneCreated(ctx)
@@ -1472,64 +1475,11 @@ func (c *WorkspaceCoordinator) StackPaneWithURL(ctx context.Context, initialURL
14721475 return err
14731476 }
14741477
1475- // Determine if we need to create a new stack or add to existing.
1476- var stackNode *entity.PaneNode
1477- var newPane *entity.Pane
1478- var newPaneID entity.PaneID
1479- var needsFirstPaneTitleUpdate bool
1480-
1481- if stackCtx.activeNode.Parent != nil && stackCtx.activeNode.Parent.IsStacked {
1482- // Already in a stack - use AddToStack use case
1483- stackNode = stackCtx.activeNode.Parent
1484- output, err := c.panesUC.AddToStack(ctx, stackCtx.ws, stackNode, nil, initialURL)
1485- if err != nil {
1486- log.Error().Err(err).Msg("failed to add pane to stack via use case")
1487- return err
1488- }
1489- newPane = output.NewPaneNode.Pane
1490- newPaneID = newPane.ID
1491- newPane.Title = defaultPaneTitle
1492- log.Debug().
1493- Int("stack_size", len(stackNode.Children)).
1494- Int("insert_index", output.StackIndex).
1495- Msg("added to existing stack via use case")
1496- } else if stackCtx.activeNode.IsStacked {
1497- // Active node is already a stack container - add to it
1498- stackNode = stackCtx.activeNode
1499- output, err := c.panesUC.AddToStack(ctx, stackCtx.ws, stackNode, nil, initialURL)
1500- if err != nil {
1501- log.Error().Err(err).Msg("failed to add pane to stack via use case")
1502- return err
1503- }
1504- newPane = output.NewPaneNode.Pane
1505- newPaneID = newPane.ID
1506- newPane.Title = defaultPaneTitle
1507- log.Debug().
1508- Int("stack_size", len(stackNode.Children)).
1509- Int("insert_index", output.StackIndex).
1510- Msg("added to stack container via use case")
1511- } else {
1512- // Need to create a new stack - use CreateStack use case.
1513- output, err := c.panesUC.CreateStack(ctx, stackCtx.ws, stackCtx.activeNode, initialURL)
1514- if err != nil {
1515- log.Error().Err(err).Msg("failed to create stack via use case")
1516- return err
1517- }
1518- stackNode = output.StackNode
1519- newPane = output.NewPane
1520- newPaneID = newPane.ID
1521- newPane.Title = defaultPaneTitle
1522- needsFirstPaneTitleUpdate = true
1523-
1524- // Update the original pane's title in the domain
1525- if output.OriginalNode != nil && output.OriginalNode.Pane != nil {
1526- output.OriginalNode.Pane.Title = stackCtx.originalTitle
1527- }
1528-
1529- log.Debug().
1530- Int("stack_size", len(stackNode.Children)).
1531- Msg("created new stack via use case")
1478+ stackNode, newPane, needsFirstPaneTitleUpdate, err := c.createOrAddStackPane(ctx, stackCtx, initialURL)
1479+ if err != nil {
1480+ return err
15321481 }
1482+ newPaneID := newPane.ID
15331483
15341484 // Create PaneView for the new pane
15351485 newPaneView := component.NewPaneView(ctx, c.widgetFactory, newPaneID, nil)
@@ -1537,9 +1487,10 @@ func (c *WorkspaceCoordinator) StackPaneWithURL(ctx context.Context, initialURL
15371487 stackCtx.wsView.RegisterPaneView(newPaneID, newPaneView)
15381488
15391489 // Add to the UI StackedView
1540- if err := c.stackedPaneMgr.AddPaneToStack(ctx, stackCtx.wsView, stackCtx.activePaneID, newPaneView, defaultPaneTitle); err != nil {
1541- log.Error().Err(err).Msg("failed to add pane to stack")
1542- return err
1490+ addErr := c.stackedPaneMgr.AddPaneToStack(ctx, stackCtx.wsView, stackCtx.activePaneID, newPaneView, defaultPaneTitle)
1491+ if addErr != nil {
1492+ log.Error().Err(addErr).Msg("failed to add pane to stack")
1493+ return addErr
15431494 }
15441495
15451496 // Update the first pane's title if we just converted from leaf to stacked
@@ -1603,6 +1554,42 @@ func (c *WorkspaceCoordinator) StackPaneWithURL(ctx context.Context, initialURL
16031554 return nil
16041555 }
16051556
1557+func (c *WorkspaceCoordinator) createOrAddStackPane(
1558+ ctx context.Context,
1559+ stackCtx *stackPaneContext,
1560+ initialURL string,
1561+) (*entity.PaneNode, *entity.Pane, bool, error) {
1562+ log := logging.FromContext(ctx)
1563+ stackNode := stackCtx.activeNode
1564+ if stackNode.Parent != nil && stackNode.Parent.IsStacked {
1565+ stackNode = stackNode.Parent
1566+ }
1567+ if stackNode.IsStacked {
1568+ output, err := c.panesUC.AddToStack(ctx, stackCtx.ws, stackNode, nil, initialURL)
1569+ if err != nil {
1570+ log.Error().Err(err).Msg("failed to add pane to stack via use case")
1571+ return nil, nil, false, err
1572+ }
1573+ newPane := output.NewPaneNode.Pane
1574+ newPane.Title = defaultPaneTitle
1575+ log.Debug().Int("stack_size", len(stackNode.Children)).Int("insert_index", output.StackIndex).
1576+ Msg("added to stack via use case")
1577+ return stackNode, newPane, false, nil
1578+ }
1579+
1580+ output, err := c.panesUC.CreateStack(ctx, stackCtx.ws, stackCtx.activeNode, initialURL)
1581+ if err != nil {
1582+ log.Error().Err(err).Msg("failed to create stack via use case")
1583+ return nil, nil, false, err
1584+ }
1585+ output.NewPane.Title = defaultPaneTitle
1586+ if output.OriginalNode != nil && output.OriginalNode.Pane != nil {
1587+ output.OriginalNode.Pane.Title = stackCtx.originalTitle
1588+ }
1589+ log.Debug().Int("stack_size", len(output.StackNode.Children)).Msg("created new stack via use case")
1590+ return output.StackNode, output.NewPane, true, nil
1591+}
1592+
16061593 func (c *WorkspaceCoordinator) prepareStackPane(
16071594 ctx context.Context,
16081595 ) (*stackPaneContext, error) {
@@ -198,7 +198,10 @@ func (c *WorkspaceCoordinator) splitWithInitialURL(ctx context.Context, directio
198 198
199 // Update the workspace view199 // Update the workspace view
200 if splitCtx.wsView != nil {200 if splitCtx.wsView != nil {
201- if err := c.applySplitToView(ctx, splitCtx.wsView, splitCtx.ws, output, direction, splitCtx.existingWidget, splitCtx.isStackSplit, oldActivePaneID); err != nil {201+ if err := c.applySplitToView(
202+ ctx, splitCtx.wsView, splitCtx.ws, output, direction,
203+ splitCtx.existingWidget, splitCtx.isStackSplit, oldActivePaneID,
204+ ); err != nil {
202 return err205 return err
203 }206 }
204 splitCtx.wsView.NotifyNewPaneCreated(ctx)207 splitCtx.wsView.NotifyNewPaneCreated(ctx)
@@ -1472,64 +1475,11 @@ func (c *WorkspaceCoordinator) StackPaneWithURL(ctx context.Context, initialURL
1472 return err1475 return err
1473 }1476 }
1474 1477
1475- // Determine if we need to create a new stack or add to existing.1478+ stackNode, newPane, needsFirstPaneTitleUpdate, err := c.createOrAddStackPane(ctx, stackCtx, initialURL)
1476- var stackNode *entity.PaneNode1479+ if err != nil {
1477- var newPane *entity.Pane1480+ return err
1478- var newPaneID entity.PaneID
1479- var needsFirstPaneTitleUpdate bool
1480-
1481- if stackCtx.activeNode.Parent != nil && stackCtx.activeNode.Parent.IsStacked {
1482- // Already in a stack - use AddToStack use case
1483- stackNode = stackCtx.activeNode.Parent
1484- output, err := c.panesUC.AddToStack(ctx, stackCtx.ws, stackNode, nil, initialURL)
1485- if err != nil {
1486- log.Error().Err(err).Msg("failed to add pane to stack via use case")
1487- return err
1488- }
1489- newPane = output.NewPaneNode.Pane
1490- newPaneID = newPane.ID
1491- newPane.Title = defaultPaneTitle
1492- log.Debug().
1493- Int("stack_size", len(stackNode.Children)).
1494- Int("insert_index", output.StackIndex).
1495- Msg("added to existing stack via use case")
1496- } else if stackCtx.activeNode.IsStacked {
1497- // Active node is already a stack container - add to it
1498- stackNode = stackCtx.activeNode
1499- output, err := c.panesUC.AddToStack(ctx, stackCtx.ws, stackNode, nil, initialURL)
1500- if err != nil {
1501- log.Error().Err(err).Msg("failed to add pane to stack via use case")
1502- return err
1503- }
1504- newPane = output.NewPaneNode.Pane
1505- newPaneID = newPane.ID
1506- newPane.Title = defaultPaneTitle
1507- log.Debug().
1508- Int("stack_size", len(stackNode.Children)).
1509- Int("insert_index", output.StackIndex).
1510- Msg("added to stack container via use case")
1511- } else {
1512- // Need to create a new stack - use CreateStack use case.
1513- output, err := c.panesUC.CreateStack(ctx, stackCtx.ws, stackCtx.activeNode, initialURL)
1514- if err != nil {
1515- log.Error().Err(err).Msg("failed to create stack via use case")
1516- return err
1517- }
1518- stackNode = output.StackNode
1519- newPane = output.NewPane
1520- newPaneID = newPane.ID
1521- newPane.Title = defaultPaneTitle
1522- needsFirstPaneTitleUpdate = true
1523-
1524- // Update the original pane's title in the domain
1525- if output.OriginalNode != nil && output.OriginalNode.Pane != nil {
1526- output.OriginalNode.Pane.Title = stackCtx.originalTitle
1527- }
1528-
1529- log.Debug().
1530- Int("stack_size", len(stackNode.Children)).
1531- Msg("created new stack via use case")
1532 }1481 }
1482+ newPaneID := newPane.ID
1533 1483
1534 // Create PaneView for the new pane1484 // Create PaneView for the new pane
1535 newPaneView := component.NewPaneView(ctx, c.widgetFactory, newPaneID, nil)1485 newPaneView := component.NewPaneView(ctx, c.widgetFactory, newPaneID, nil)
@@ -1537,9 +1487,10 @@ func (c *WorkspaceCoordinator) StackPaneWithURL(ctx context.Context, initialURL
1537 stackCtx.wsView.RegisterPaneView(newPaneID, newPaneView)1487 stackCtx.wsView.RegisterPaneView(newPaneID, newPaneView)
1538 1488
1539 // Add to the UI StackedView1489 // Add to the UI StackedView
1540- if err := c.stackedPaneMgr.AddPaneToStack(ctx, stackCtx.wsView, stackCtx.activePaneID, newPaneView, defaultPaneTitle); err != nil {1490+ addErr := c.stackedPaneMgr.AddPaneToStack(ctx, stackCtx.wsView, stackCtx.activePaneID, newPaneView, defaultPaneTitle)
1541- log.Error().Err(err).Msg("failed to add pane to stack")1491+ if addErr != nil {
1542- return err1492+ log.Error().Err(addErr).Msg("failed to add pane to stack")
1493+ return addErr
1543 }1494 }
1544 1495
1545 // Update the first pane's title if we just converted from leaf to stacked1496 // Update the first pane's title if we just converted from leaf to stacked
@@ -1603,6 +1554,42 @@ func (c *WorkspaceCoordinator) StackPaneWithURL(ctx context.Context, initialURL
1603 return nil1554 return nil
1604 }1555 }
1605 1556
1557+func (c *WorkspaceCoordinator) createOrAddStackPane(
1558+ ctx context.Context,
1559+ stackCtx *stackPaneContext,
1560+ initialURL string,
1561+) (*entity.PaneNode, *entity.Pane, bool, error) {
1562+ log := logging.FromContext(ctx)
1563+ stackNode := stackCtx.activeNode
1564+ if stackNode.Parent != nil && stackNode.Parent.IsStacked {
1565+ stackNode = stackNode.Parent
1566+ }
1567+ if stackNode.IsStacked {
1568+ output, err := c.panesUC.AddToStack(ctx, stackCtx.ws, stackNode, nil, initialURL)
1569+ if err != nil {
1570+ log.Error().Err(err).Msg("failed to add pane to stack via use case")
1571+ return nil, nil, false, err
1572+ }
1573+ newPane := output.NewPaneNode.Pane
1574+ newPane.Title = defaultPaneTitle
1575+ log.Debug().Int("stack_size", len(stackNode.Children)).Int("insert_index", output.StackIndex).
1576+ Msg("added to stack via use case")
1577+ return stackNode, newPane, false, nil
1578+ }
1579+
1580+ output, err := c.panesUC.CreateStack(ctx, stackCtx.ws, stackCtx.activeNode, initialURL)
1581+ if err != nil {
1582+ log.Error().Err(err).Msg("failed to create stack via use case")
1583+ return nil, nil, false, err
1584+ }
1585+ output.NewPane.Title = defaultPaneTitle
1586+ if output.OriginalNode != nil && output.OriginalNode.Pane != nil {
1587+ output.OriginalNode.Pane.Title = stackCtx.originalTitle
1588+ }
1589+ log.Debug().Int("stack_size", len(output.StackNode.Children)).Msg("created new stack via use case")
1590+ return output.StackNode, output.NewPane, true, nil
1591+}
1592+
1606 func (c *WorkspaceCoordinator) prepareStackPane(1593 func (c *WorkspaceCoordinator) prepareStackPane(
1607 ctx context.Context,1594 ctx context.Context,
1608 ) (*stackPaneContext, error) {1595 ) (*stackPaneContext, error) {
M internal/ui/coordinator/workspace_external_url_test.go +2 -1
@@ -9,6 +9,7 @@ import (
99 "github.com/bnema/dumber/internal/ui/component"
1010 contentcoord "github.com/bnema/dumber/internal/ui/coordinator/content"
1111 "github.com/bnema/dumber/internal/ui/layout"
12+ "github.com/bnema/puregotk/v4/gdk"
1213 "github.com/bnema/puregotk/v4/gtk"
1314 "github.com/stretchr/testify/assert"
1415 "github.com/stretchr/testify/require"
@@ -68,7 +69,7 @@ func TestWorkspaceCoordinator_StackPaneWithURLUsesExplicitURL(t *testing.T) {
6869
6970 func stackURLTestCoordinator(t *testing.T, legacyURL string) (*WorkspaceCoordinator, *entity.Workspace) {
7071 t.Helper()
71- if !gtk.InitCheck() {
72+ if !gtk.InitCheck() || gdk.DisplayGetDefault() == nil {
7273 t.Skip("GTK native display prerequisite unavailable")
7374 }
7475 ctx := context.Background()
@@ -9,6 +9,7 @@ import (
9 "github.com/bnema/dumber/internal/ui/component"9 "github.com/bnema/dumber/internal/ui/component"
10 contentcoord "github.com/bnema/dumber/internal/ui/coordinator/content"10 contentcoord "github.com/bnema/dumber/internal/ui/coordinator/content"
11 "github.com/bnema/dumber/internal/ui/layout"11 "github.com/bnema/dumber/internal/ui/layout"
12+ "github.com/bnema/puregotk/v4/gdk"
12 "github.com/bnema/puregotk/v4/gtk"13 "github.com/bnema/puregotk/v4/gtk"
13 "github.com/stretchr/testify/assert"14 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/require"15 "github.com/stretchr/testify/require"
@@ -68,7 +69,7 @@ func TestWorkspaceCoordinator_StackPaneWithURLUsesExplicitURL(t *testing.T) {
68 69
69 func stackURLTestCoordinator(t *testing.T, legacyURL string) (*WorkspaceCoordinator, *entity.Workspace) {70 func stackURLTestCoordinator(t *testing.T, legacyURL string) (*WorkspaceCoordinator, *entity.Workspace) {
70 t.Helper()71 t.Helper()
71- if !gtk.InitCheck() {72+ if !gtk.InitCheck() || gdk.DisplayGetDefault() == nil {
72 t.Skip("GTK native display prerequisite unavailable")73 t.Skip("GTK native display prerequisite unavailable")
73 }74 }
74 ctx := context.Background()75 ctx := context.Background()

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