Context
CommandsToBubbleUp and CommandBridge currently only work for Command.Accept and Command.Activate:
TryBubbleUp — only called from DefaultAcceptHandler/DefaultActivateHandler. Unbound commands hit DefaultCommandNotBoundHandler which just raises CommandNotBound and stops.
CommandBridge — hard-codes Accepted/Activated event subscriptions. Cannot bridge arbitrary commands.
DefaultCommandNotBoundHandler — currently a one-liner that raises CommandNotBound. Has no dispatch, no TryBubbleUp, no composite view support.
Tabs needs directional commands (Command.Up/Down/Left/Right) from a focused TitleView to bubble up to the owning Tabs view. Upgrading the not-bound path to support the full lifecycle (dispatch, bubbling, composite views) makes CommandsToBubbleUp work for any command generically.
Phases
Phase A: Reorganize existing tests
Move all bubbling-focused tests from ViewCommandTests.cs to a new CommandBubblingTests.cs. No behavior changes — just reorganization. All tests must pass before and after.
Tests to move → Tests/UnitTestsParallelizable/ViewBase/CommandBubblingTests.cs:
From #region Command Propagation Tests (lines 355-923) — move entire region:
CommandsToBubbleUp_DefaultIsEmpty
Accept_Command_DoesNotBubbleByDefault
Activate_Command_DoesNotBubbleByDefault
CommandsToBubbleUp_CanDisableAllPropagation
CommandsToBubbleUp_CanBeCustomized
Activate_BubblingUp_Fires_Activated_On_SuperView
Activate_BubblingUp_Fires_Activated_In_Deep_Hierarchy
ConsumeDispatch_Blocks_Further_Bubbling
CommandsToBubbleUp_StopsWhenHandled
CommandsToBubbleUp_WorksInDeepHierarchy
CommandsToBubbleUp_StopsAtIntermediateHandler
- All 9 Padding bubble tests
- All 6 Border bubble tests
From #region DispatchDown Tests (lines 1689-1959) — move entire region + helper class
From #region IAcceptTarget Tests — move bubbling subset:
Button_BubblesUp_To_SuperView
IAcceptTarget_In_Deep_Hierarchy_BubblesUp (1/2/3)
IAcceptTarget_Handled_Does_Not_BubbleUp
From #region DefaultAcceptView Tests — move bubbling subset:
DefaultAcceptView_Peer_Accept_Bubbles_To_DefaultAcceptView
DefaultAcceptView_Non_IAcceptTarget_Peer_Accept_Bubbles_To_DefaultAcceptView
DefaultAcceptView_Peer_IAcceptTarget_NonDefault_Accept_Bubbles_To_DefaultAcceptView
DefaultAcceptView_Accept_DoesNotBubble_To_DefaultAcceptView_WhenHandled
NonIAcceptTarget_Without_CommandsToBubbleUp_DoesNotRedirect_To_DefaultAcceptView
From #region OnAccept/Accept tests:
Accept_Command_Bubbles_Up_To_SuperView
From #region Values Chain Tests:
Values_BubbleActivatedUp_Carries_Composite_Value_To_Ancestor
From #region Bridge Cancellation Bug (lines 2739-3008) — move entire region + helper classes
Phase B: Write failing tests proving the gap
Add tests to CommandBubblingTests.cs that demonstrate unbound commands cannot bubble today. These tests must fail initially, proving the limitation.
Unbound command bubbling (via DefaultCommandNotBoundHandler):
UnboundCommand_BubblesToSuperView_ViaCommandsToBubbleUp — subview invokes Command.Up (unbound), superView has CommandsToBubbleUp = [Command.Up]
UnboundCommand_DoesNotBubble_WhenNotInCommandsToBubbleUp
UnboundCommand_BubblesFromPaddingSubView_ToOwner
UnboundCommand_BubblesFromBorderSubView_ToOwner
UnboundCommand_BubblesInDeepHierarchy
UnboundCommand_StopsWhenHandled
UnboundCommand_BubblingUp_ReturnsNotHandled — notification-only semantics
Composite view support for unbound commands:
UnboundCommand_DispatchesToTarget_InCompositeView — composite view with dispatch target receives unbound command, verifies TryDispatchToTarget fires
UnboundCommand_CompositeView_RaisesPostEvent_AfterDispatch
CommandBridge for arbitrary commands:
CommandBridge_BridgesUnboundCommand — bridge connects Command.Up, remote fires it, owner receives it
CommandBridge_IgnoresUnregisteredCommand — bridge for Command.Up only, Command.Down does not bridge
Phase C: Upgrade CommandBridge to support arbitrary commands
CommandBridge currently hard-codes Accepted/Activated subscriptions. Generalize to support any command.
File: Terminal.Gui/Input/CommandBridge.cs
Phase D: Upgrade DefaultCommandNotBoundHandler
Upgrade RaiseCommandNotBound and DefaultCommandNotBoundHandler to follow the full Activate-like lifecycle:
RaiseCommandNotBound — add TryDispatchToTarget and TryBubbleUp:
1. OnCommandNotBound(args) → if handled, return true
2. CommandNotBound?.Invoke(args)
3. if (!handled) TryDispatchToTarget(ctx) ← NEW
4. if (!handled) TryBubbleUp(ctx, handled) ← NEW
5. return args.Handled
DefaultCommandNotBoundHandler — full lifecycle with dispatch, bubbling, composite view support, and BubbleActivatedUp. No SetFocus. Includes _dispatchState reset/tracking, dispatch-routing guard, RefreshValue, BubbleActivatedUp, and CommandWillBubbleToAncestor.
ArrangerButton.cs — remove AddCommand(Command.Up/Down/Left/Right, DefaultAcceptHandler). These fall through to the upgraded DefaultCommandNotBoundHandler.
Files:
Terminal.Gui/ViewBase/View.Command.cs
Terminal.Gui/ViewBase/Adornment/ArrangerButton.cs
Files Summary
Tests/UnitTestsParallelizable/ViewBase/CommandBubblingTests.cs — NEW (phases A+B)
Tests/UnitTestsParallelizable/ViewBase/ViewCommandTests.cs — remove moved tests (phase A)
Terminal.Gui/Input/CommandBridge.cs — generalize (phase C)
Terminal.Gui/ViewBase/View.Command.cs — upgrade handler + raise method (phase D)
Terminal.Gui/ViewBase/Adornment/ArrangerButton.cs — remove AddCommand calls (phase D)
Verification
After each phase:
dotnet test --project Tests/UnitTestsParallelizable --filter-class "*CommandBubblingTests"
dotnet test --project Tests/UnitTestsParallelizable --filter-class "*ViewCommandTests"
- Phase B: new tests must FAIL (proving the gap)
- Phase C+D: all tests must PASS
- Full suite: only pre-existing TabsTests failures
Context
CommandsToBubbleUpandCommandBridgecurrently only work forCommand.AcceptandCommand.Activate:TryBubbleUp— only called fromDefaultAcceptHandler/DefaultActivateHandler. Unbound commands hitDefaultCommandNotBoundHandlerwhich just raisesCommandNotBoundand stops.CommandBridge— hard-codesAccepted/Activatedevent subscriptions. Cannot bridge arbitrary commands.DefaultCommandNotBoundHandler— currently a one-liner that raisesCommandNotBound. Has no dispatch, noTryBubbleUp, no composite view support.Tabs needs directional commands (
Command.Up/Down/Left/Right) from a focusedTitleViewto bubble up to the owningTabsview. Upgrading the not-bound path to support the full lifecycle (dispatch, bubbling, composite views) makesCommandsToBubbleUpwork for any command generically.Phases
Phase A: Reorganize existing tests
Move all bubbling-focused tests from
ViewCommandTests.csto a newCommandBubblingTests.cs. No behavior changes — just reorganization. All tests must pass before and after.Tests to move →
Tests/UnitTestsParallelizable/ViewBase/CommandBubblingTests.cs:From
#region Command Propagation Tests(lines 355-923) — move entire region:CommandsToBubbleUp_DefaultIsEmptyAccept_Command_DoesNotBubbleByDefaultActivate_Command_DoesNotBubbleByDefaultCommandsToBubbleUp_CanDisableAllPropagationCommandsToBubbleUp_CanBeCustomizedActivate_BubblingUp_Fires_Activated_On_SuperViewActivate_BubblingUp_Fires_Activated_In_Deep_HierarchyConsumeDispatch_Blocks_Further_BubblingCommandsToBubbleUp_StopsWhenHandledCommandsToBubbleUp_WorksInDeepHierarchyCommandsToBubbleUp_StopsAtIntermediateHandlerFrom
#region DispatchDown Tests(lines 1689-1959) — move entire region + helper classFrom
#region IAcceptTarget Tests— move bubbling subset:Button_BubblesUp_To_SuperViewIAcceptTarget_In_Deep_Hierarchy_BubblesUp(1/2/3)IAcceptTarget_Handled_Does_Not_BubbleUpFrom
#region DefaultAcceptView Tests— move bubbling subset:DefaultAcceptView_Peer_Accept_Bubbles_To_DefaultAcceptViewDefaultAcceptView_Non_IAcceptTarget_Peer_Accept_Bubbles_To_DefaultAcceptViewDefaultAcceptView_Peer_IAcceptTarget_NonDefault_Accept_Bubbles_To_DefaultAcceptViewDefaultAcceptView_Accept_DoesNotBubble_To_DefaultAcceptView_WhenHandledNonIAcceptTarget_Without_CommandsToBubbleUp_DoesNotRedirect_To_DefaultAcceptViewFrom
#region OnAccept/Accept tests:Accept_Command_Bubbles_Up_To_SuperViewFrom
#region Values Chain Tests:Values_BubbleActivatedUp_Carries_Composite_Value_To_AncestorFrom
#region Bridge Cancellation Bug(lines 2739-3008) — move entire region + helper classesPhase B: Write failing tests proving the gap
Add tests to
CommandBubblingTests.csthat demonstrate unbound commands cannot bubble today. These tests must fail initially, proving the limitation.Unbound command bubbling (via DefaultCommandNotBoundHandler):
UnboundCommand_BubblesToSuperView_ViaCommandsToBubbleUp— subview invokesCommand.Up(unbound), superView hasCommandsToBubbleUp = [Command.Up]UnboundCommand_DoesNotBubble_WhenNotInCommandsToBubbleUpUnboundCommand_BubblesFromPaddingSubView_ToOwnerUnboundCommand_BubblesFromBorderSubView_ToOwnerUnboundCommand_BubblesInDeepHierarchyUnboundCommand_StopsWhenHandledUnboundCommand_BubblingUp_ReturnsNotHandled— notification-only semanticsComposite view support for unbound commands:
UnboundCommand_DispatchesToTarget_InCompositeView— composite view with dispatch target receives unbound command, verifiesTryDispatchToTargetfiresUnboundCommand_CompositeView_RaisesPostEvent_AfterDispatchCommandBridge for arbitrary commands:
CommandBridge_BridgesUnboundCommand— bridge connectsCommand.Up, remote fires it, owner receives itCommandBridge_IgnoresUnregisteredCommand— bridge forCommand.Uponly,Command.Downdoes not bridgePhase C: Upgrade
CommandBridgeto support arbitrary commandsCommandBridgecurrently hard-codesAccepted/Activatedsubscriptions. Generalize to support any command.File:
Terminal.Gui/Input/CommandBridge.csPhase D: Upgrade
DefaultCommandNotBoundHandlerUpgrade
RaiseCommandNotBoundandDefaultCommandNotBoundHandlerto follow the full Activate-like lifecycle:RaiseCommandNotBound— addTryDispatchToTargetandTryBubbleUp:DefaultCommandNotBoundHandler— full lifecycle with dispatch, bubbling, composite view support, andBubbleActivatedUp. NoSetFocus. Includes_dispatchStatereset/tracking, dispatch-routing guard,RefreshValue,BubbleActivatedUp, andCommandWillBubbleToAncestor.ArrangerButton.cs— removeAddCommand(Command.Up/Down/Left/Right, DefaultAcceptHandler). These fall through to the upgradedDefaultCommandNotBoundHandler.Files:
Terminal.Gui/ViewBase/View.Command.csTerminal.Gui/ViewBase/Adornment/ArrangerButton.csFiles Summary
Tests/UnitTestsParallelizable/ViewBase/CommandBubblingTests.cs— NEW (phases A+B)Tests/UnitTestsParallelizable/ViewBase/ViewCommandTests.cs— remove moved tests (phase A)Terminal.Gui/Input/CommandBridge.cs— generalize (phase C)Terminal.Gui/ViewBase/View.Command.cs— upgrade handler + raise method (phase D)Terminal.Gui/ViewBase/Adornment/ArrangerButton.cs— remove AddCommand calls (phase D)Verification
After each phase:
dotnet test --project Tests/UnitTestsParallelizable --filter-class "*CommandBubblingTests"dotnet test --project Tests/UnitTestsParallelizable --filter-class "*ViewCommandTests"