Table of Contents

Command Deep Dive

Table of Contents

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 commandsActivate, 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:

  1. Pre-event (Activating / Accepting) — Fires before the action. Handlers can cancel by setting args.Cancel = true.
  2. 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 via WithValue(). Ordered innermost (originator) to outermost.
  • Value — Convenience accessor returning Values[^1] (the most recently appended value), or null if 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

  1. 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 Values chain.
  2. Dispatch target refresh — When a composite view dispatches to an inner target, RefreshValue() re-reads the target's IValue.GetValue() and appends it via WithValue().
  3. Composite post-mutation — After RaiseActivated, a ConsumeDispatch composite (e.g., OptionSelector) may have updated its own value. The framework appends the composite's post-mutation value so ctx.Value reflects the composite's semantic value.
  4. Ancestor notificationBubbleActivatedUp walks the SuperView chain, preserving Values at 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 inside RaiseActivated or other methods that receive a copy.
  • BubbleActivatedUp receives exactly the values the caller appended — no double-counting from inner WithValue() 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.

  1. Resets _lastDispatchOccurred to prevent stale state from prior invocations
  2. Calls RaiseActivating (OnActivatingActivating event → TryDispatchToTargetTryBubbleUp)
  3. If RaiseActivating returns true (handled/consumed):
    • If dispatch occurred (_lastDispatchOccurred), calls RaiseActivated for composite view completion
    • Returns true
  4. 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)
  5. Otherwise (Direct invocation): calls SetFocus(), RaiseActivated, returns true

DefaultAcceptHandler (Accept)

Bound to Key.Enter.

  1. Resets _lastDispatchOccurred
  2. Calls RaiseAccepting (OnAcceptingAccepting event → TryDispatchToTargetTryBubbleUp)
  3. If handled and (dispatch occurred OR routing is Bridged), calls RaiseAccepted
  4. If not handled, redirects to DefaultAcceptView via DispatchDown (unless Accept will also bubble to an ancestor — prevents double-path)
  5. For BubblingUp with a dispatch target, calls RaiseAccepted
  6. Calls RaiseAccepted
  7. Returns true if: redirected, will bubble to ancestor, routing is BubblingUp, or view is IAcceptTarget

DefaultHotKeyHandler (HotKey)

Bound to HotKey.

  1. Calls RaiseHandlingHotKey
  2. If handled, returns false (allows key through as text input — e.g., TextField with HotKey _E)
  3. Calls SetFocus(), RaiseHotKeyCommand, then InvokeCommand(Command.Activate, ctx?.Binding)
  4. 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 via DispatchDown, 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:

  1. If already handled → return true
  2. If routing is DispatchingDown → return false (prevents re-entry)
  3. For Accept: handles DefaultAcceptView + IAcceptTarget redirect logic
  4. If command is in SuperView.CommandsToBubbleUp → invoke on SuperView with Routing = BubblingUp
  5. Handles Padding edge 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:

  1. Resolution: DefaultAcceptView searches for IAcceptTarget { IsDefault: true }
  2. Return value: DefaultAcceptHandler returns true for IAcceptTarget views
  3. 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/RaiseAcceptingTryDispatchToTargetTryBubbleUpRaiseActivated/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):

  1. Override GetDispatchTarget to return the SubView that should receive commands.
  2. Override ConsumeDispatch to return true — the composite handles the command; inner activations don't propagate.
  3. Implement IValue<TValue> (or IValue<TValue>) to expose the composite's semantic value.
  4. 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):

  1. Override GetDispatchTarget to return the target SubView (e.g., CommandView).
  2. Leave ConsumeDispatch as false (default).
  3. Use deferred completion: subscribe to the target's Activated event 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:

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:

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 HotKeyAccept 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) HotKeyActivate Not bound Activate Not bound Not bound
MenuItem Inherited from Shortcut Inherited from Shortcut HotKeyActivate 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 OnMouseEvent override
  • Handled by SubViews — Composite view delegates to SubViews
  • Not bound — Not handled by this view
  • N/A — Display-only view (CanFocus = false)

Key Points

  1. View base: Space → Activate, Enter → Accept, LeftButtonReleasedActivate. Subclasses override or extend.

  2. Button: Implements IAcceptTarget. All interactions map to Accept.

  3. Selector views: Use ConsumeDispatch=true. Inner CheckBox commands are consumed; don't propagate to SuperView.

  4. Text input views: Remove Key.Space binding for text entry. TextField cancels HotKey when focused (allows typing the HotKey character).

  5. Mouse columns: Pressed → LeftButtonPressed, Released → LeftButtonReleased, Clicked → synthesized press+release, DoubleClicked → timing-based. See Mouse Deep Dive.

  6. Shortcut/MenuItem: Use relay dispatch (ConsumeDispatch=false). Commands propagate through GetDispatchTargetCommandView. MenuItem inherits from Shortcut.

  7. MenuBar: Uses consume dispatch (ConsumeDispatch=true, GetDispatchTargetFocused). 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, or Handler
  • 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.