Learn · 03 of 09
Focus and keybindings.
Terminal apps are keyboard-first. TerminalUI ships two cooperating
systems for keyboard interaction:
- Focus —
@FocusState,
.focusable(...), and .focused(...)
determine which view receives typing, arrows, Tab, Enter, Escape.
- Commands —
.keyCommand(...) and
.paletteCommand(...) attach modifier-key shortcuts and
palette entries to scopes, not to the global app.
The two combine through one rule: a scope is active iff its
anchor is on the current focus chain. Tree presence is a
prerequisite; focus-chain membership is the activation condition.
Anatomy
1 — @FocusState
A property wrapper that exposes the current focus as a typed value.
The enum case (Field.search, Field.list) is
the focus identity; .focused($focusedField, equals: .search)
registers a view as the receiver of that focus value.
Setting focusedField = .search moves focus
programmatically. The runtime drives the focus chain;
@FocusState just observes and writes it.
2 — Panel = an ActionScope
Panel is the consumer-facing primitive that says "I want
an ActionScope here." It conforms to ActionScope, has
no built-in chrome (style with standard modifiers), and acts as the
focus-chain anchor for any commands attached to it.
Other built-in ActionScopes:
Scene (always on the chain while the scene is active),
and the presentation modifiers .alert,
.confirmationDialog, .sheet — each
conforms to ActionScope so commands attach naturally to
the modal.
3 — .keyCommand
Attaches a (key, modifiers) → action binding to the
receiving scope. Modifiers must be non-empty — single-key
shortcuts are framework-reserved (typing, arrows, Tab, Enter,
Escape are routed to focused widgets). Dispatch is
shallowest-wins along the focus chain: the runtime
walks root-to-leaf, the first matching scope takes the event, and
a disabled match consumes the event without running the action.
That precedence is the load-bearing rule. It means a deep
dependency cannot silently override a shortcut claimed by a
shell-level scope.
4 — .paletteCommand + .toolbar
.paletteCommand(name:…) attaches an
action-bearing entry to the same scope, with no key shortcut. It's
pure metadata + action, and it shows up in
EnvironmentValues.activePaletteCommands for
consumer-authored palette surfaces. The framework doesn't ship a
palette UI — wrap your own.
.toolbar(style:) declares the receiving scope as a
toolbar surface. Toolbar items hoisted from descendants via
.toolbarItem(...) bubble up the tree and land in the
nearest enclosing scope that called
.toolbar(style:). Commands flow top-down (declared at
scope roots); items flow bottom-up.
The focus-chain dispatch flow
For a key press with at least one modifier:
- The runtime walks the focus chain root-to-leaf.
- At each scope, it looks for a matching
(key, modifiers). - The first match wins. If enabled, fires the action; if
disabled, consumes the event silently.
- If no scope claims the binding, the event is ignored.
Where to go next