# Commands and keyboard shortcuts

> Single command routing across toolbar, menu, tray, shortcut, and bridge entry points with keyboard binding constraints.

- Repository: vercel-labs/native
- GitHub: https://github.com/vercel-labs/native
- Human docs: https://grok-wiki.com/public/docs/vercel-labs-native-8ccc3580636a
- Complete Markdown: https://grok-wiki.com/public/docs/vercel-labs-native-8ccc3580636a/llms-full.txt

## Source Files

- `docs/src/app/keyboard-shortcuts/layout.tsx`
- `examples/command-app/build.zig`
- `examples/command-app/README.md`
- `src/runtime/builtin_bridge.zig`
- `src/embed/chrome.zig`

---

---
title: "Commands and keyboard shortcuts"
description: "Single command routing across toolbar, menu, tray, shortcut, and bridge entry points with keyboard binding constraints."
---

App actions converge on one `CommandEvent` path: menus, app shortcuts, toolbar and native controls, tray items, embed chrome taps, automation, and WebView bridge calls all dispatch a stable command id into `Event.command`. Keyboard shortcuts are registered bindings that fire that same path (source `.shortcut`) and also emit `Event.shortcut` plus a JavaScript `shortcut` event when the bridge is live.

## Architecture

```mermaid
flowchart TB
  subgraph entry["Entry points"]
    TB[Toolbar / native control]
    MN[Menu item]
    TR[Tray item]
    SC[App shortcut key chord]
    BR["Bridge: native-sdk.command.invoke"]
    RT["runtime.dispatchCommand"]
    CH[Mobile chrome tab / primary action]
  end

  subgraph platform["Platform events"]
    NC[native_command]
    MC[menu_command]
    TA[tray_action]
    SE[shortcut]
    BM[bridge_message]
  end

  subgraph runtime["Runtime flow"]
    DC["dispatchCommand + validateCommandName"]
    EV["Event.command / CommandEvent"]
    ES["Event.shortcut + JS emit"]
  end

  subgraph app["App handlers"]
    EH["App.event switch .command"]
    OC["UiApp Options.on_command → Msg"]
  end

  TB --> NC
  MN --> MC
  TR --> TA
  SC --> SE
  BR --> BM
  CH --> RT
  RT --> DC
  NC --> DC
  MC --> DC
  TA --> DC
  SE --> DC
  SE --> ES
  BM --> DC
  DC --> EV
  EV --> EH
  EV --> OC
```

Platform hosts deliver raw events; `src/runtime/flow.zig` maps them into a single `CommandEvent` (name, source, window, view label, optional tray item id) and runs `validateCommandName` before the app sees the event.

## Command model

### `CommandEvent` and sources

| Field | Type / default | Role |
| --- | --- | --- |
| `name` | `[]const u8` | Stable command id (action identity) |
| `source` | `CommandSource` (default `.runtime`) | Which entry point fired the action |
| `window_id` | `WindowId` (default `0`) | Native window that owned the input; may be `0` for window-less sources |
| `view_label` | `[]const u8` | Native control or child WebView label when applicable |
| `tray_item_id` | `TrayItemId` (default `0`) | Tray item for tray-sourced commands |

| `CommandSource` | Emitted by |
| --- | --- |
| `.runtime` | Direct `runtime.dispatchCommand` / embed `native_sdk_app_command` |
| `.menu` | Native menu item (`menu_command`) |
| `.shortcut` | Registered app keyboard shortcut |
| `.toolbar` | Native control under a `.toolbar` parent |
| `.tray` | Tray menu item |
| `.native_view` | Native control outside a toolbar ancestry |
| `.bridge` | `window.zero.commands.invoke` / `native-sdk.command.invoke` |

Match on `command.name` for behavior. Use `source` only when the app needs source-specific UX. Tray items without an explicit command fall back to the name `"tray.action"` while still setting `tray_item_id`.

### Command catalog

Manifest and runtime catalog entries share the same shape:

| Field | Default | Notes |
| --- | --- | --- |
| `id` | required | Command id |
| `title` | `""` | Display title for menus/catalog UI |
| `enabled` | `true` | Catalog flag (not a hard runtime gate on dispatch) |
| `checked` | `false` | Toggle/state presentation in catalog |

Limits (manifest / platform):

| Limit | Value |
| --- | --- |
| Max commands in catalog | `256` |
| Max command id / title bytes | `128` |
| Runtime `validateCommandName` max id bytes | `128` |

Rejected ids: empty, oversize, `"."` / `".."`, or containing `0`, `/`, `\`, newline, CR, or tab.

`app.zon` example (shared catalog + bindings):

```zon
.commands = .{
    .{ .id = "app.sync", .title = "Sync" },
},
.shortcuts = .{
    .{ .id = "app.sync", .key = "s", .modifiers = .{ "primary" } },
},
.menus = .{
    .{
        .title = "View",
        .items = .{
            .{ .label = "Sync", .command = "app.sync", .key = "s", .modifiers = .{ "primary" } },
        },
    },
},
```

Generated runners load `.commands` into runtime options. Native code can read the active catalog without reparsing the manifest:

```zig
var buffer: [32]native_sdk.Command = undefined;
const commands = runtime.listCommands(&buffer);
```

### Handling commands

Raw `App` event handler:

```zig
fn event(context: *anyopaque, runtime: *native_sdk.Runtime, event_value: native_sdk.Event) anyerror!void {
    _ = context;
    switch (event_value) {
        .command => |command| {
            if (std.mem.eql(u8, command.name, "app.sync")) {
                // one handler for every entry point
            }
        },
        else => {},
    }
}
```

`UiApp` maps command names into TEA messages via `Options.on_command`:

```zig
.on_command = struct {
    fn map(name: []const u8) ?Msg {
        if (std.mem.eql(u8, name, "app.sync")) return .sync;
        return null;
    }
}.map,
```

When `window_id` is `0` (for example some tray/menu paths), `UiApp` dispatches against the canvas window so the model still receives a coherent window context.

## Entry points

| Entry point | How it binds | Platform event → source |
| --- | --- | --- |
| Toolbar button | Shell view `command` under parent `kind = toolbar` | `native_command` → `.toolbar` |
| Other native control | Shell/view `command` without toolbar ancestry | `native_command` → `.native_view` |
| App / window menu | `MenuItem.command` (+ optional key chord) | `menu_command` → `.menu` |
| Tray item | `TrayMenuItem.command` (or fallback `"tray.action"`) | `tray_action` → `.tray` |
| App shortcut | `Shortcut.id` matches command id | `shortcut` → `.shortcut` **and** `Event.shortcut` |
| Bridge | `window.zero.commands.invoke(name)` | `bridge_message` → `.bridge` |
| Embed chrome | Tab / primary action `id` as command id | host command path → `.runtime` |
| Automation | `shortcut <id>` or bridge invoke payloads | same command path |

Shell views declare commands on controls:

```zon
.{
    .label = "sync-button",
    .kind = "button",
    .parent = "toolbar",
    .text = "Sync",
    .command = "app.sync",
},
```

`commandSourceForNativeView` walks the view parent chain; if any ancestor is `.toolbar`, the source is `.toolbar`, otherwise `.native_view`.

Mobile platform chrome (tabs / primary action) uses the same command path: projected system controls call `native_sdk_app_command` with the declared id; selection state stays in the model, not in the host bar.

## Keyboard shortcuts

### Registration

Declare in `app.zon`:

```zon
.shortcuts = .{
    .{ .id = "app.sync", .key = "s", .modifiers = .{ "primary" } },
    .{ .id = "command.palette", .key = "p", .modifiers = .{ "primary", "shift" } },
},
```

Generated runners register manifest shortcuts automatically. Override only when building the list in Zig (`RunOptions.shortcuts` or `Runtime.init` `.shortcuts`).

Zig form:

```zig
const shortcuts = [_]native_sdk.Shortcut{
    .{ .id = "app.sync", .key = "s", .modifiers = .{ .primary = true } },
};
```

### Modifiers

| Manifest / field | Meaning |
| --- | --- |
| `primary` | Command on macOS; Control on Windows and Linux |
| `command` | Explicit Command (macOS-oriented) |
| `control` | Explicit Control |
| `option` / `alt` | Option/Alt |
| `shift` | Shift |

At least one modifier is required for single-character keys and for `space`, `enter`, `tab`, and `backspace`, so registration cannot steal ordinary typing. Named navigation keys (`escape`, arrows) may omit modifiers.

### Keys

**Single-character portable keys:** letters, digits, and unshifted punctuation `=`, `-`, `,`, `.`, `/`, `;`, `'`, `[`, `]`, `\`, `` ` ``.

**Named keys:** `escape`, `enter`, `tab`, `space`, `backspace`, `arrowleft`, `arrowright`, `arrowup`, `arrowdown` (case-insensitive).

### Limits and validation

| Constraint | Value / rule |
| --- | --- |
| Max shortcuts | `64` |
| Max shortcut id bytes | `64` |
| Max key bytes | `32` |
| Duplicate id | rejected (`DuplicateShortcut`) |
| Same key + colliding modifiers (per target platforms) | rejected |
| Invalid key / missing required modifier | `InvalidShortcut` |

Hosts validate and install the table at set time (`validateShortcut` on macOS/Linux/Windows/null platforms). Capability flag for apps that use them: `"shortcuts"` in `app.zon` capabilities.

### Delivery

When a registered chord matches the focused native window:

1. `dispatchCommand` with `source = .shortcut`, `name = shortcut.id`
2. `Event.shortcut` (`ShortcutEvent`: `id`, `key`, `modifiers`, `window_id`)
3. Optional JS emit on `window.zero` (`shortcut` detail)

Zig:

```zig
.shortcut => |shortcut| {
    // optional: react to chord details
    _ = shortcut.id;
},
.command => |command| {
    // primary action path; source == .shortcut for keyboard
},
```

JavaScript:

```ts
window.zero.on("shortcut", (event) => {
  // event.id, event.command (alias of id), event.key, event.windowId, event.modifiers
});
```

Prefer mapping the shared `command` event (or `on_command`) for application logic so menu/toolbar/bridge stay equivalent. Use `Event.shortcut` / JS `shortcut` only when the chord itself matters.

### Widget focus vs app shortcuts

Canvas keyboard routing is separate from app shortcuts:

1. Focused widget bound handler  
2. Structural consume (editable text kinds consume keys; control intents consume their keys)  
3. `UiApp` `Options.on_key` fallback for unclaimed `key_down`

App-level chrome shortcuts deliberately require modifiers on character keys so they do not compete with typing. Bare media-style keys belong in `on_key`, which yields to every consuming widget.

### Platform notes

- Shortcuts are scoped to the native window that receives the key event.
- System WebView hosts on macOS, Linux, and Windows install shortcut tables through the platform backends.
- Chromium (CEF) shortcut delivery is supported on the macOS CEF host; use the system WebView backend where CEF is unavailable if native shortcut events are required.

## Bridge command API

When `js_window_api` is on and the built-in bridge policy allows the call, WebView content uses the same command path.

| Surface | Request | Permission |
| --- | --- | --- |
| `window.zero.commands.invoke(name \| { name \| id })` | `native-sdk.command.invoke` | `command` |
| `window.zero.commands.list()` | `native-sdk.command.list` | `command` |

Invoke payload accepts `name` or `id`. Success result mirrors `CommandEvent`:

```json
{
  "name": "app.sync",
  "source": "bridge",
  "windowId": 1,
  "viewLabel": "",
  "trayItemId": 0
}
```

List result is the catalog array:

```json
[{ "id": "app.sync", "title": "Sync", "enabled": true, "checked": false }]
```

Denied origins or missing permission return bridge errors (for example `"Command API is not permitted"`). `command-app` enables this with:

- `permissions = .{ "command" }`
- `builtin_bridge` policies for `native-sdk.command.invoke` / `.list`
- `security.navigation.allowed_origins` including the WebView origin (e.g. `zero://inline`)

## Reference example: one command, five entry points

`examples/command-app` wires a single id `app.sync` through toolbar, menu, tray, primary+`s` shortcut, and WebView bridge, then updates a status label from one handler.

Run:

```sh
zig build run -Dplatform=macos -Dweb-engine=system
```

Headless suite (null platform):

```sh
zig build test -Dplatform=null
```

The example test dispatches, in order: toolbar `native_command`, `menu_command`, tray action, platform `shortcut`, and bridge invoke — asserting sources `.toolbar`, `.menu`, `.tray`, `.shortcut`, `.bridge` and a successful list response containing `"id":"app.sync"`.

## Automation

The automation protocol accepts `shortcut <id>` (for example `shortcut app.refresh`), which drives the same registered shortcut / command path used by the live host. Bridge-shaped automation can also post `native-sdk.command.invoke` payloads. Prefer command ids that already exist in the manifest catalog so UI, bridge, and agents share one vocabulary.

## Constraints and failure modes

| Condition | Result |
| --- | --- |
| Empty / path-like / oversize command name | `error.InvalidCommand` at `dispatchCommand` |
| Shortcut without required modifier | `error.InvalidShortcut` at registration / manifest validation |
| Duplicate shortcut id or colliding chord | `error.DuplicateShortcut` |
| More than 64 shortcuts | manifest / host rejection |
| Bridge invoke without `command` permission or allowed origin | bridge error response, no app command |
| Tray item with command but `id == 0` or separator | tray validation failure |
| Handler error inside `App.event` | recorded dispatch error; app continues (tests may use `.propagate`) |

## Verification checklist

- Declare the command once in `.commands` and reuse that id on menu, shortcut, toolbar, tray, and bridge.
- Confirm headless multi-source routing (`command-app` pattern or equivalent platform event injections).
- Confirm bridge: `commands.list()` returns the catalog; `commands.invoke` returns `source: "bridge"`.
- Confirm shortcut: chord updates the same UI path as the toolbar button; typing unmodified character keys still reaches text fields.
- Confirm capability/permission flags when packaging: `"shortcuts"`, `"menus"`, `"tray"`, `"command"` as used.

## Related pages

<CardGroup>
  <Card title="Menus, dialogs, and tray" href="/menus-dialogs-tray">
    OS menus and tray items that bind to the same command ids.
  </Card>
  <Card title="Bridge and builtin commands" href="/bridge">
    Host-to-content bridge payloads, async dispatch, and permission boundaries.
  </Card>
  <Card title="App model" href="/app-model">
    Model / Msg / update loop and how UiApp maps commands into messages.
  </Card>
  <Card title="State and data flow" href="/state-data-flow">
    Message dispatch and effects after a command is accepted.
  </Card>
  <Card title="Capabilities" href="/capabilities">
    Guarded OS services and the command permission boundary.
  </Card>
  <Card title="Automation" href="/automation">
    Agent command surface including shortcut and bridge invoke.
  </Card>
  <Card title="Native UI markup" href="/native-ui">
    Widget keyboard focus rules that outrank app shortcuts for typing.
  </Card>
  <Card title="Windows and surfaces" href="/windows-surfaces">
    Window-scoped shortcut delivery and multi-window command window_id.
  </Card>
</CardGroup>
