---
url: https://desktop.bas.dev/desktop-shell/menu.md
---
# Application menu

The page knows what has the focus, so the page builds the menu: a `MenuSpec` with a tree of `MenuNode`s, sent over IPC whenever the focus changes. The shell draws it natively and sends a picked command back by its id. Every label comes from the page, so the whole menu is in the language of the interface.

## The shape

```ts
import type { MenuNode, MenuSpec } from '@basmilius/desktop-shell/bridge';

type ShellAction = 'devtools';

const spec: MenuSpec<ShellAction> = {
    menus: [
        {
            id: 'file',
            label: 'File',
            items: [
                { kind: 'command', id: 'project.new', label: 'New project', accelerator: 'CommandOrControl+N' },
                { kind: 'separator' },
                { kind: 'role', role: 'quit', label: 'Quit' }
            ]
        }
    ]
};
```

| Kind | What the shell draws |
|---|---|
| `command` | An item that sends its `id` back. `checked` makes it a check box, `radio` one of a group. |
| `role` | A native role from `MENU_ROLES`, with the page's label. |
| `shell` | Something only the shell can carry out. The type parameter lists an app's own actions. |
| `separator` | A line. |
| `submenu` | A nested menu. |

## Drawing it

`menuTemplateOf` turns a spec into a template for `Menu.buildFromTemplate`, or returns `null` for anything that is no spec. `shellItem` draws the app's own actions; return `null` to leave one out.

```ts
import { createMenuCommands, createPageKeys, devToolsAccelerator, menuTemplateOf } from '@basmilius/desktop-shell';

const pageKeys = createPageKeys();
const runCommand = createMenuCommands({ pageKeys, window: () => BrowserWindow.getFocusedWindow() });

window.webContents.on('before-input-event', (_event, input) => pageKeys.saw(input));

onFromApp('menu:set', (_event, spec: MenuSpec<ShellAction>) => {
    const template = menuTemplateOf(spec, {
        run: runCommand,
        shellItem: (action, label) =>
            action === 'devtools' ? { label, accelerator: devToolsAccelerator(), click: () => window.webContents.toggleDevTools() } : null
    });
    if (template) {
        Menu.setApplicationMenu(Menu.buildFromTemplate(template));
    }
});
```

Off macOS an accelerator is only shown, never registered: a registered one would take a key such as Ctrl+W from the page before the page saw it.

## A key the page already had

On macOS the menu fires its accelerator even when the page answered the key itself. `createMenuCommands` drops a command Electron says its accelerator fired when the page had a key with Cmd or Ctrl in the last `PAGE_KEY_MS`, as `createPageKeys` saw it. A pick with the mouse, through accessibility or from inside the open menu always runs.

`keyBypassesPage` covers a page inside the app that never hands a key on, such as a `<webview>`: while it has the keyboard, the menu answers every key. A command reaches the page as `menu:run` with its id. Only a minimized window is shown first, so a pick from behind other work leaves the app where it is.

## The menu before the page's

`staticMenuTemplate` is the menu that stands until the page sends its own, and again after a reload or a crash, so Quit is always there. Its View menu has no reload and no zoom roles: their accelerators would be taken before the page sees them.

```ts
import { staticMenuTemplate } from '@basmilius/desktop-shell';

Menu.setApplicationMenu(
    Menu.buildFromTemplate(
        staticMenuTemplate({
            appName: app.name,
            toggleDevTools: () => window.webContents.toggleDevTools(),
            appItems: [{ label: `About ${app.name}…`, click: openAbout }],
            quitItems: []
        })
    )
);
```

`appItems` go at the top of the app menu on macOS; `quitItems` go right above Quit, in a File menu off macOS.
