Command Deep Dive
Table of Contents
- See Also
- Getting Started
- Architecture Overview
- Command Routing
- Value Propagation
- Default Handlers
- Dispatch (Composite Pattern)
- Command Bubbling
- CommandBridge
- How To
- Shortcut Dispatch
- Selector Dispatch
- View Command Behaviors
- Command Route Tracing
See Also
Getting Started
Terminal.Gui uses the Command enum as a standardized vocabulary for user actions. Views declare what they do using Commands rather than ad-hoc event names — this enables a consistent, localizable, and composable command system across the entire toolkit.
The enum defines over 50 commands spanning several categories:
| Category | Examples | Purpose |
|---|---|---|
| Lifecycle | Activate, Accept, HotKey |
Core view interaction (toggle, confirm, focus) |
| Editing | Cut, Copy, Paste, Undo, Redo |
Clipboard and text editing |
| Movement | Up, Down, PageUp, WordRight |
Cursor and selection navigation |
| Selection | SelectAll, UpExtend, ToggleExtend |
Extending selection ranges |
| Semantic | Save, Open, New, Context, Refresh |
Application-level actions |
| Navigation | NextTabStop, PreviousTabGroup |
Focus movement between views |
The three lifecycle commands — Activate, Accept, and HotKey — drive the event system that most application code interacts with:
| Command | What It Means | Common Triggers |
|---|---|---|
| Activate | Change state or toggle (e.g., check a checkbox, select a list item) | Space, mouse click |
| Accept | Confirm or submit (e.g., press a button, submit a dialog) | Enter, double-click |
| HotKey | Focus and activate via keyboard shortcut | Alt+letter, Shortcut.Key |
Declarative Command Binding
The real power of Command is declarative binding. Shortcut and MenuItem can be constructed with just a target view and a command — the framework automatically resolves the key binding, display text, and help text from localized resources:
// Declarative: "this menu item invokes Cut on the editor"
MenuItem cutItem = new (editor, Command.Cut);
// Automatically:
// Key = Ctrl+X (from editor's key bindings)
// Title = "Cu_t" (from GlobalResources "cmdCut")
// HelpText = "Cut to clipboard" (from GlobalResources "cmdCut_Help")
MenuItem saveItem = new (editor, Command.Save);
// Key = Ctrl+S
// Title = "_Save"
// HelpText = "Save file"
This means views can advertise their capabilities via AddCommand, and menus/shortcuts can bind to them without hardcoding strings or key bindings. Localization comes for free — translating the resource strings is all that's needed.
Views register their command handlers declaratively too:
// Inside a custom View's constructor
AddCommand (Command.Copy, () => Copy ());
AddCommand (Command.Cut, () => Cut ());
AddCommand (Command.Context, () => ShowContextMenu ());
Responding to Button Clicks
The most common pattern is subscribing to a view's Accepted event to react when the user confirms an action:
Button okButton = new () { Text = "OK" };
okButton.Accepted += (_, args) =>
{
MessageBox.Query ("Result", "You clicked OK!", "Close");
};
Responding to State Changes
Use the Activated event to react when a view's state changes (e.g., a checkbox is toggled):
CheckBox darkMode = new () { Text = "Dark Mode" };
darkMode.Activated += (_, args) =>
{
// args.Value?.Value contains the view's current value
bool isChecked = args.Value?.Value is CheckState.Checked;
ApplyTheme (isChecked);
};
Cancelling an Action
Use the Activating or Accepting event to prevent an action before it happens. Set args.Cancel = true to cancel:
TextField nameField = new ();
nameField.Accepting += (_, args) =>
{
if (string.IsNullOrEmpty (nameField.Text))
{
args.Cancel = true; // Prevent Accept — name is required
MessageBox.ErrorQuery ("Error", "Name cannot be empty.", "OK");
}
};
Listening to Events from SubViews
By default, events don't propagate up the view hierarchy. To receive events from SubViews, set CommandsToBubbleUp on the ancestor:
Window myWindow = new () { Title = "My App" };
myWindow.CommandsToBubbleUp = [Command.Activate, Command.Accept];
// Now myWindow.Activated fires when ANY SubView activates
myWindow.Activated += (_, args) =>
{
// Identify which SubView fired via TryGetSource
if (args.Value?.TryGetSource (out View? source) is true)
{
// source is the originating view
}
// Or find a specific value type in the chain
if (args.Value?.Value is CheckState state)
{
// A CheckBox (or a view containing one) was toggled
}
};
The Two-Phase Pattern
Every command follows a two-phase pattern:
- Pre-event (
Activating/Accepting) — Fires before the action. Handlers can cancel by settingargs.Cancel = true. - Post-event (
Activated/Accepted) — Fires after the action completes. The view's state has already changed.
User presses Space on CheckBox
→ Activating fires (cancellable)
→ CheckBox toggles its state
→ Activated fires (state already changed, ctx.Value available)
The rest of this document covers the internal architecture. For common recipes, skip to How To.
Architecture Overview
The Command system provides a standardized framework for view actions (selecting, accepting, activating). It integrates with keyboard/mouse input handling and uses the Cancellable Work Pattern for extensibility and cancellation. As commands propagate through the view hierarchy, each <xref:Terminal.Gui.IValue>-implementing view appends its value to the Values chain, enabling ancestors to inspect the full value history (see Value Propagation).
Central concepts:
- Activate — Change view state or prepare for interaction (toggle checkbox, focus menu item)
- Accept — Confirm an action or state (submit dialog, execute menu command)
- HotKey — Set focus and activate (Alt+F, Shortcut.Key)
| Aspect | Activate | Accept | HotKey |
|---|---|---|---|
| Triggers | Space, mouse click, arrow keys | Enter, double-click | HotKey letter, Shortcut.Key |
| Pre-event | OnActivating / Activating | OnAccepting / Accepting | OnHandlingHotKey / HandlingHotKey |
| Post-event | OnActivated / Activated | OnAccepted / Accepted | OnHotKeyCommand / HotKeyCommand |
| Bubbling | Opt-in via CommandsToBubbleUp | Opt-in via CommandsToBubbleUp + DefaultAcceptView | Opt-in via CommandsToBubbleUp |
flowchart TD
input[User input] --> invoke[View.InvokeCommand]
invoke --> |Activate| act[RaiseActivating → TryDispatch → TryBubbleUp]
invoke --> |Accept| acc[RaiseAccepting → TryDispatch → TryBubbleUp]
invoke --> |HotKey| hk[RaiseHandlingHotKey]
act --> |handled| act_stop[dispatch consumed → RaiseActivated → return true]
act --> |not handled + BubblingUp| act_notify[RaiseActivated for plain views → return false]
act --> |not handled + Direct| act_focus[SetFocus + RaiseActivated → return true]
acc --> |handled| acc_stop[dispatch consumed → RaiseAccepted → return true]
acc --> |not handled| acc_default{DefaultAcceptView?}
acc_default --> |yes| acc_redirect[DispatchDown to DefaultAcceptView]
acc_default --> |no| acc_accepted[RaiseAccepted]
acc_redirect --> acc_accepted
hk --> |handled| hk_cancel[return false - key not consumed]
hk --> |not handled| hk_focus[SetFocus + RaiseHotKeyCommand + InvokeCommand Activate]
Command Routing
Commands propagate through the view hierarchy via CommandRouting, which describes the current routing phase:
public enum CommandRouting
{
Direct, // Programmatic or from this view's own bindings
BubblingUp, // Propagating upward through SuperView chain
DispatchingDown, // SuperView dispatching downward to a SubView
Bridged, // Crossing a non-containment boundary via CommandBridge
}
ICommandContext carries the routing mode, source view (weak reference), binding, and accumulated values:
public interface ICommandContext
{
Command Command { get; }
WeakReference<View>? Source { get; }
ICommandBinding? Binding { get; }
CommandRouting Routing { get; }
IReadOnlyList<object?> Values { get; }
object? Value { get; }
}
Values— Append-only chain of values accumulated as the command propagates. Each IValue-implementing view appends its value viaWithValue(). Ordered innermost (originator) to outermost.Value— Convenience accessor returningValues[^1](the most recently appended value), ornullif empty.
CommandContext is an immutable record struct. Use WithCommand(), WithRouting(), or WithValue() to create modified copies.
Value Propagation
As a command flows through the view hierarchy, Values accumulates a chain of values from each <xref:Terminal.Gui.IValue>-implementing view that participates. This enables ancestors to inspect values from any layer — not just the outermost composite.
How Values Accumulate
- Origin — The originating view (e.g., CheckBox) processes the command. If the originating view implements <xref:Terminal.Gui.IValue>, its value is captured at the start of command invocation and placed in the initial
Valueschain. - Dispatch target refresh — When a composite view dispatches to an inner target,
RefreshValue()re-reads the target'sIValue.GetValue()and appends it viaWithValue(). - Composite post-mutation — After
RaiseActivated, aConsumeDispatchcomposite (e.g., OptionSelector) may have updated its own value. The framework appends the composite's post-mutation value soctx.Valuereflects the composite's semantic value. - Ancestor notification —
BubbleActivatedUpwalks the SuperView chain, preservingValuesat each hop. If an ancestor has a dispatch target that is the command source, its refreshed value is also appended.
Value vs Values
| Accessor | Returns | Use When |
|---|---|---|
Value |
Values[^1] (last appended) |
You only need the outermost composite's value |
Values |
Full ordered chain | You need to find a specific inner value by type or position |
Example Chain
Consider a CheckBox inside an OptionSelector inside a MenuItem:
CheckBox (CheckState.Checked)
→ OptionSelector (int? selectedIndex)
→ MenuItem (Title string)
When the Activated event reaches an ancestor:
ctx.Values[0]=CheckState.Checked(dispatch target refresh)ctx.Values[1]=2(OptionSelector's post-mutation index)ctx.Values[2]="Dark"(MenuItem's value via bridge)ctx.Value="Dark"(last appended = outermost)
Use LINQ to find a specific type anywhere in the chain:
if (ctx.Values?.FirstOrDefault (v => v is Schemes) is Schemes scheme)
{
// Found the Schemes value regardless of its position
}
Struct Value Semantics
CommandContext is a readonly record struct. Each call to WithValue() creates a new copy — it does not mutate the original. This means:
- A caller's local variable is unaffected by
WithValue()calls insideRaiseActivatedor other methods that receive a copy. BubbleActivatedUpreceives exactly the values the caller appended — no double-counting from innerWithValue()calls on separate copies.
Performance
WithValue() uses [..Values, value], which copies the entire list on each call — O(N²) total for N appends. This is acceptable for typical UI hierarchies (3–5 levels). If extreme depths are ever needed, consider an immutable linked list or builder.
Default Handlers
View registers four default command handlers in SetupCommands():
DefaultActivateHandler (Activate)
Bound to Key.Space and MouseFlags.LeftButtonReleased.
- Resets
_lastDispatchOccurredto prevent stale state from prior invocations - Calls RaiseActivating (OnActivating → Activating event →
TryDispatchToTarget→ TryBubbleUp) - If RaiseActivating returns
true(handled/consumed):- If dispatch occurred (
_lastDispatchOccurred), calls RaiseActivated for composite view completion - Returns
true
- If dispatch occurred (
- If routing is
BubblingUp:- Plain views (no dispatch target): fires RaiseActivated to complete two-phase notification
- Relay-dispatch views (e.g., Shortcut): skips — deferred completion fires RaiseActivated later
- Consume-dispatch views: already completed in step 3
- Returns
false(notification, not consumption)
- Otherwise (Direct invocation): calls
SetFocus(), RaiseActivated, returnstrue
DefaultAcceptHandler (Accept)
Bound to Key.Enter.
- Resets
_lastDispatchOccurred - Calls RaiseAccepting (OnAccepting → Accepting event →
TryDispatchToTarget→ TryBubbleUp) - If handled and (dispatch occurred OR routing is
Bridged), calls RaiseAccepted - If not handled, redirects to DefaultAcceptView via
DispatchDown(unless Accept will also bubble to an ancestor — prevents double-path) - For
BubblingUpwith a dispatch target, calls RaiseAccepted - Calls RaiseAccepted
- Returns
trueif: redirected, will bubble to ancestor, routing isBubblingUp, or view is IAcceptTarget
DefaultHotKeyHandler (HotKey)
Bound to HotKey.
- Calls RaiseHandlingHotKey
- If handled, returns
false(allows key through as text input — e.g., TextField with HotKey_E) - Calls
SetFocus(), RaiseHotKeyCommand, thenInvokeCommand(Command.Activate, ctx?.Binding) - Returns
true
DefaultCommandNotBoundHandler (NotBound)
Invoked when an unregistered command is triggered. Raises CommandNotBound event.
Dispatch (Composite Pattern)
Composite views (Shortcut, Selectors, MenuBar) delegate commands to a primary SubView. The framework provides this via three virtual members:
GetDispatchTarget
protected virtual View? GetDispatchTarget (ICommandContext? ctx) => null;
Override to return the SubView that should receive dispatched commands. Returns null to skip dispatch.
| View | Target |
|---|---|
| Shortcut | CommandView |
| OptionSelector / FlagSelector | Focused (inner CheckBox) |
| MenuBar | Focused |
ConsumeDispatch
protected virtual bool ConsumeDispatch => false;
Controls whether dispatch consumes the command:
false(relay) — Shortcut: dispatches to CommandView viaDispatchDown, but the originator continues its own activation. Shortcut uses deferred completion (fires RaiseActivated after CommandView.Activated).true(consume) — Selectors, MenuBar: marks the command as handled after dispatch. The composite view fires RaiseActivated/RaiseAccepted itself. Inner SubView activations are implementation details that don't propagate.
TryDispatchToTarget
Called by RaiseActivating and RaiseAccepting after the OnActivating/Activating (or OnAccepting/Accepting) have had a chance to cancel. Guards:
- Routing is
DispatchingDown→ skip (prevents re-entry when command is already dispatching down) - Routing is
Bridged→ skip (bridge brings commands up from a non-containment boundary; dispatching down into the owner's CommandView would be incorrect) - Relay + no binding → skip (programmatic invocation — no user interaction to forward)
- Source is within target → skip (prevents loops)
For consume dispatch: on BubblingUp, consumes without dispatching (the composite handles state). On direct invocation, forwards via DispatchDown.
For relay dispatch: dispatches via DispatchDown if source is not within the target.
Command Bubbling
CommandsToBubbleUp
Opt-in property specifying which commands bubble from SubViews to this view:
public IReadOnlyList<Command> CommandsToBubbleUp { get; set; } = [];
| View | CommandsToBubbleUp |
|---|---|
| Shortcut | [Command.Activate, Command.Accept] |
| Bar / Menu | [Command.Accept, Command.Activate] |
| Dialog | [Command.Accept] |
| SelectorBase | [Command.Activate, Command.Accept] |
TryBubbleUp
Called by RaiseActivating, RaiseAccepting, and RaiseHandlingHotKey when the command is not handled. Steps:
- If already handled → return
true - If routing is
DispatchingDown→ returnfalse(prevents re-entry) - For Accept: handles DefaultAcceptView + IAcceptTarget redirect logic
- If command is in
SuperView.CommandsToBubbleUp→ invoke on SuperView withRouting = BubblingUp - Handles
Paddingedge cases (checks Padding's parent)
Bubbling is a notification, not consumption. The SuperView's return value is propagated, but relay views continue their own processing regardless.
DispatchDown
Dispatches a command downward to a SubView with bubbling suppressed:
protected bool? DispatchDown (View target, ICommandContext? ctx)
Creates a CommandContext with Routing = CommandRouting.DispatchingDown and invokes on the target. TryBubbleUp checks for DispatchingDown and skips bubbling, preventing infinite recursion.
DefaultAcceptView and IAcceptTarget
DefaultAcceptView identifies the SubView that receives Accept when no other handles it. Defaults to the first IAcceptTarget { IsDefault: true } SubView (typically a Button).
IAcceptTarget affects flow in three ways:
- Resolution: DefaultAcceptView searches for
IAcceptTarget { IsDefault: true } - Return value:
DefaultAcceptHandlerreturnstruefor IAcceptTarget views - Redirect: Non-default IAcceptTarget sources bubble up when a DefaultAcceptView exists
CommandBridge
CommandBridge routes commands across non-containment boundaries (e.g., MenuItem.SubMenu ↔ parentMenuItem, MenuBarItem ↔ PopoverMenu). The bridge subscribes to the remote view's Accepted/Activated events and re-enters the full command pipeline on the owner via InvokeCommand:
CommandBridge bridge = CommandBridge.Connect (owner, remote, Command.Accept, Command.Activate);
// remote.Accepted → owner.InvokeCommand (Accept, Routing = Bridged)
// remote.Activated → owner.InvokeCommand (Activate, Routing = Bridged)
bridge.Dispose (); // tears down subscriptions
Because the bridge calls InvokeCommand (not RaiseAccepted/RaiseActivated), bridged commands flow through the full pipeline: RaiseActivating/RaiseAccepting → TryDispatchToTarget → TryBubbleUp → RaiseActivated/RaiseAccepted. This enables bridged commands to propagate through the owner's SuperView hierarchy.
TryDispatchToTarget has a Bridged routing guard to prevent the bridged command from dispatching down into the owner's CommandView — the bridge brings commands up, not down.
The bridge preserves the Values chain across the boundary: Values = e.Context?.Values ?? []. This ensures that values accumulated in the remote view's hierarchy are visible to the owner's Activated/Accepted subscribers and to any further bubbling.
Both references are weak — the bridge does not prevent GC. The bridge is one-way; create two bridges for bidirectional routing.
Important
Cancellation does not work across a bridge. Because the bridge subscribes to the remote view's post-events (Activated/Accepted), the remote view's OnActivated/OnAccepted has already fired — and any state change has already occurred — before the bridge relays the command to the owner. If the owner (or an ancestor) sets args.Handled = true in Activating/Accepting, it will stop further propagation on the owner's side, but it cannot undo or prevent the state change that already happened on the remote side.
The framework detects this situation and emits a BridgedCancellation trace warning (visible when Trace.EnabledCategories includes TraceCategory.Command). If you need cancellation semantics, use direct containment (SuperView/SubView with CommandsToBubbleUp) instead of a bridge.
How To
Subscribe to Activated Events
To react when a view (or any of its descendants) completes an activation, subscribe to the Activated event. To receive bubbled events from SubViews, set CommandsToBubbleUp:
// Opt in to receive Activate commands bubbled from SubViews
myWindow.CommandsToBubbleUp = [Command.Activate];
myWindow.Activated += (_, args) =>
{
// Pattern 1: Identify the originator by type or Id using TryGetSource
if (args.Value?.TryGetSource (out View? source) is true
&& source is CheckBox { Id: "bordersCheckbox" } bordersCheckbox)
{
// Handle the specific originator
myWindow.BorderStyle = args.Value?.Value as CheckState? == CheckState.Checked
? LineStyle.Double
: LineStyle.None;
return;
}
// Pattern 2: Search the Values chain by type — finds a value
// regardless of its position in the hierarchy
if (args.Value?.Values?.FirstOrDefault (v => v is Schemes) is Schemes scheme)
{
myWindow.SchemeName = scheme.ToString ();
}
};
Pattern 1 uses TryGetSource() to identify which view originated the command. This is useful when multiple SubViews bubble the same command and you need to distinguish them.
Pattern 2 searches Values by type using LINQ. This is the idiomatic way to find a specific value in a deep hierarchy without caring about its position in the chain. For example, an OptionSelector inside a MenuItem inside a PopoverMenu produces a chain with multiple values — searching by type avoids fragile index-based access.
Tip
See the PopoverMenus scenario in UICatalog for a working example of both patterns. See also Menus.cs for Menu-specific event handling.
Build a Composite View (Consume Dispatch)
To build a composite view that owns its SubViews' state (like OptionSelector):
- Override GetDispatchTarget to return the SubView that should receive commands.
- Override ConsumeDispatch to return
true— the composite handles the command; inner activations don't propagate. - Implement IValue<TValue> (or IValue<TValue>) to expose the composite's semantic value.
- Apply state changes in OnActivated.
public class MySelector : View, IValue<int?>
{
protected override View? GetDispatchTarget (ICommandContext? ctx) => Focused;
protected override bool ConsumeDispatch => true;
protected override void OnActivated (ICommandContext? ctx)
{
base.OnActivated (ctx);
// Apply state changes here — the framework appends
// GetValue() to ctx.Values after this method returns.
}
public int? GetTypedValue () => _selectedIndex;
public object? GetValue () => GetTypedValue ();
}
Tip
See OptionSelector and FlagSelector for complete implementations.
Build a Relay View
To build a relay view that forwards commands to an inner target without consuming them (like Shortcut):
- Override GetDispatchTarget to return the target SubView (e.g.,
CommandView). - Leave ConsumeDispatch as
false(default). - Use deferred completion: subscribe to the target's
Activatedevent and call RaiseActivated from the callback.
public class MyRelay : View
{
protected override View? GetDispatchTarget (ICommandContext? ctx) => _commandView;
// ConsumeDispatch defaults to false — relay pattern.
// The framework dispatches via DispatchDown, then the
// originator continues its own activation.
}
Tip
See Shortcut for the complete relay pattern with deferred completion.
Bridge Commands Across Non-Containment Boundaries
When a view references another view that is not a SubView (e.g., a MenuItem that owns a SubMenu), use CommandBridge to relay commands across the boundary:
// Bridge Activate and Accept from SubMenu → this MenuItem
_subMenuBridge = CommandBridge.Connect (this, subMenu, Command.Activate, Command.Accept);
// Tear down when no longer needed
_subMenuBridge.Dispose ();
The bridge preserves the Values chain, so values accumulated in the remote view's hierarchy are visible to the owner's subscribers. The bridge uses weak references — it does not prevent GC.
Warning
Cancellation (Activating/Accepting with args.Handled = true) does not propagate back across a bridge — the remote view's state has already changed. See the CommandBridge section for details.
Tip
See MenuItem.SubMenu in MenuItem.cs for a working example of bridging across non-containment boundaries.
Shortcut Dispatch
Shortcut is a composite view with three SubViews: CommandView, HelpView, KeyView. It overrides:
- GetDispatchTarget → returns
CommandView - ConsumeDispatch →
false(relay pattern)
The framework handles dispatch automatically via TryDispatchToTarget:
- Commands from
CommandView→ source is within target → dispatch skipped (CommandView already processed) - Commands from Shortcut/HelpView/KeyView → DispatchDown to CommandView
- Programmatic invocation (no binding) → relay guard skips dispatch
Deferred completion: Shortcut subscribes to CommandView.Activated. When CommandView completes (e.g., CheckBox toggles), Shortcut's callback fires RaiseActivated. This ensures Action sees the updated CommandView state.
OnActivated invokes Action, then dispatches to TargetView (or falls back to application-bound key commands):
protected override void OnActivated (ICommandContext? ctx)
{
base.OnActivated (ctx);
Action?.Invoke ();
ICommandContext? targetCtx = ctx;
if (Command != Command.NotBound && ctx is CommandContext cc)
{
targetCtx = cc.WithCommand (Command);
}
InvokeOnTargetOrApp (targetCtx);
}
Selector Dispatch
OptionSelector and FlagSelector override:
- GetDispatchTarget → returns
Focused(the active inner CheckBox) - ConsumeDispatch →
true(consume pattern)
When an inner CheckBox activates (via click/space), the command bubbles up to the selector. TryDispatchToTarget consumes it (BubblingUp + ConsumeDispatch=true). The selector fires RaiseActivated to perform state mutation. The inner CheckBox activation does not propagate to the selector's SuperView.
View Command Behaviors
| View | Space | Enter | HotKey | Pressed | Released | Clicked | DoubleClicked |
|---|---|---|---|---|---|---|---|
| View (base) | Activate | Accept | HotKey | Not bound | Activate | Not bound | Not bound |
| Button | Accept | Accept | HotKey → Accept | Configurable via MouseHoldRepeat |
Configurable via MouseHoldRepeat |
Accept | Accept |
| CheckBox | Activate (advances state) | Accept | HotKey | Not bound | Not bound (removed) | Activate | Accept |
| ListView | Activate (marks item) | Accept | HotKey | Not bound | Not bound | Activate | Accept |
| TableView | Not bound | Accept (CellActivationKey) | HotKey | Not bound | Not bound | Activate | Not bound |
| TreeView | Not bound | Activate (ObjectActivationKey) | HotKey | Not bound | Not bound | OnMouseEvent (node selection) | OnMouseEvent (ObjectActivationButton) |
| TextField | Removed (text input) | Accept | HotKey (cancels if focused) | OnMouseEvent (set cursor) | OnMouseEvent (end drag) | Not bound | OnMouseEvent (select word) |
| TextView | Removed (text input) | NewLine or Accept | HotKey | Not bound | Not bound | Not bound | Not bound |
| OptionSelector | Forwards to CheckBox SubView | Accept | Restores focus, advances Active | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews |
| FlagSelector | Removed (forwards to SubView) | Removed (forwards to SubView) | Restores focus (no-op if focused) | Not bound (cleared) | Not bound (cleared) | Not bound (cleared) | Not bound (cleared) |
| HexView | Removed | Removed | Not bound | Not bound | Not bound | Activate | Activate |
| ColorPicker | Not bound | Not bound | Not bound | Not bound | Not bound | Not bound (removed) | Accept |
| Label | Not bound | Not bound | Forwards to next focusable peer | Not bound | Not bound | Not bound | Not bound |
| TabView | Not bound | Not bound | HotKey | Handled by SubViews | Handled by SubViews | Handled by SubViews | Not bound |
| NumericUpDown | Handled by SubViews | Handled by SubViews | HotKey | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews |
| Dialog | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews |
| Wizard | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews |
| FileDialog | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews |
| DatePicker | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews |
| DropDownList | Handled by SubViews | Handled by SubViews | HotKey | OnMouseEvent (toggle) | Handled by SubViews | Handled by SubViews | Handled by SubViews |
| Shortcut | Activate (dispatch to CommandView) | Accept (dispatch to CommandView) | HotKey → Activate | Not bound | Activate | Not bound | Not bound |
| MenuItem | Inherited from Shortcut | Inherited from Shortcut | HotKey → Activate | Not bound | Activate | Not bound | Not bound |
| Menu / Bar | Activate (dispatches to focused MenuItem) | Handled by MenuItems/Shortcuts | Handled by MenuItems/Shortcuts | Handled by MenuItems/Shortcuts | Handled by MenuItems/Shortcuts | Handled by MenuItems/Shortcuts | Handled by MenuItems/Shortcuts |
| MenuBar | Handled by SubViews (ConsumeDispatch) | Handled by SubViews (ConsumeDispatch) | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews |
| ScrollBar | Not bound | Not bound | Not bound | OnMouseEvent | OnMouseEvent | OnMouseEvent | Not bound |
| ProgressBar / SpinnerView | N/A | N/A | N/A | N/A | N/A | N/A | N/A |
Table Notation
Command.X— Bound via KeyBinding or MouseBinding- OnMouseEvent (desc) — Handled via
OnMouseEventoverride - Handled by SubViews — Composite view delegates to SubViews
- Not bound — Not handled by this view
- N/A — Display-only view (
CanFocus = false)
Key Points
View base: Space → Activate, Enter → Accept,
LeftButtonReleased→ Activate. Subclasses override or extend.Button: Implements IAcceptTarget. All interactions map to Accept.
Selector views: Use
ConsumeDispatch=true. Inner CheckBox commands are consumed; don't propagate to SuperView.Text input views: Remove
Key.Spacebinding for text entry. TextField cancels HotKey when focused (allows typing the HotKey character).Mouse columns: Pressed →
LeftButtonPressed, Released →LeftButtonReleased, Clicked → synthesized press+release, DoubleClicked → timing-based. See Mouse Deep Dive.Shortcut/MenuItem: Use relay dispatch (
ConsumeDispatch=false). Commands propagate through GetDispatchTarget →CommandView. MenuItem inherits from Shortcut.MenuBar: Uses consume dispatch (
ConsumeDispatch=true, GetDispatchTarget →Focused). Being redesigned — see source for current behavior.
Command Route Tracing
For debugging command routing issues, Terminal.Gui provides a tracing system via Tracing.Trace. Command tracing captures detailed information about command flow through the view hierarchy.
Enabling Tracing
using Terminal.Gui.Tracing;
// Enable tracing via flags-based API
Trace.EnabledCategories = TraceCategory.Command | TraceCategory.Mouse;
// For testing, use scoped tracing (thread-safe, per async context)
using (Trace.PushScope (TraceCategory.Command))
{
view.InvokeCommand (Command.Activate);
// Tracing enabled only in this scope
}
In UICatalog, use the Logging menu → Command Trace checkbox to toggle tracing at runtime.
Trace Output
When enabled, trace entries are logged via Logging.Trace with the format:
[Phase] Arrow Command @ ViewId (Method) - Message
- Phase:
Entry,Exit,Routing,Event, orHandler - Arrow:
↑(BubblingUp),↓(DispatchingDown),↔(Bridged),•(Direct)
Example output:
[Entry] • Activate @ Button("OK"){X=10,Y=5} (DefaultActivateHandler)
[Routing] ↑ Activate @ Button("OK"){X=10,Y=5} (TryBubbleUp) - BubblingUp to Dialog("Confirm")
[Event] • Activate @ Button("OK"){X=10,Y=5} (RaiseActivated)
See Logging - View Event Tracing for custom backends, testing patterns, and performance details.