Documentation menu

Menu

Build application and context menus in the main process - native NSMenu on macOS, GTK on Linux.

Process: Main

The Menu module lets you build application menu bars and context (popup) menus. The menu tree is held as plain JS objects and realized into native widgets on demand - a native NSMenu on macOS, GTK menus on Linux, and Win32 HMENU on Windows. Both context menus and the application menu bar work on all three. Role items route to the responder chain on macOS; Linux wires the editing and window roles; on Windows role items are inert labels today (no accelerator table). Accelerator text in labels is a follow-up off macOS.

Bunmaska exposes both Menu and a companion MenuItem class. You typically build menus declaratively with Menu.buildFromTemplate(...), but you can also construct items by hand and append/insert them.

import { Menu, MenuItem } from 'bunmaska';

Static methods

static buildFromTemplate(template: ReadonlyArray<MenuItemOptions | MenuItem>): Menu

Builds a Menu from an array of plain option objects (or already-constructed MenuItems). This is the usual entry point. Nested submenu arrays are expanded recursively, and role values are resolved to their default label/accelerator/native behavior.

import { Menu } from 'bunmaska';

const menu = Menu.buildFromTemplate([
  {
    label: 'File',
    submenu: [
      { label: 'New', accelerator: 'CmdOrCtrl+N', click: () => console.log('new') },
      { type: 'separator' },
      { role: 'quit' },
    ],
  },
  { role: 'editMenu' }, // macro role > expands to a full standard Edit submenu
]);

Menu.setApplicationMenu(menu);

static setApplicationMenu(menu: Menu | null): void

Sets menu as the application menu. On macOS this becomes the system menu bar; on Linux it is installed via the GTK realizer; on Windows it is a Win32 menu bar. Passing null removes the bar on all three platforms - the stored reference (getApplicationMenu() returns null) and the native side, including bars already installed on Linux.

import { Menu } from 'bunmaska';

const menu = Menu.buildFromTemplate([{ role: 'windowMenu' }]);
Menu.setApplicationMenu(menu);

static getApplicationMenu(): Menu | null

Returns the Menu previously passed to setApplicationMenu, or null if none has been set.

import { Menu } from 'bunmaska';

const current = Menu.getApplicationMenu();
if (current) {
  console.log(`menu has ${current.items.length} top-level items`);
}

Instance methods

append(item: MenuItem): void

Appends a MenuItem to the end of the menu.

import { Menu, MenuItem } from 'bunmaska';

const menu = new Menu();
menu.append(new MenuItem({ label: 'Open…', click: () => openFile() }));
menu.append(new MenuItem({ type: 'separator' }));
menu.append(new MenuItem({ role: 'quit' }));

insert(pos: number, item: MenuItem): void

Inserts menuItem at index pos. Unlike Electron, pos is clamped to the menu’s bounds (negative values insert at the start; out-of-range values append at the end) rather than throwing.

import { Menu, MenuItem } from 'bunmaska';

const menu = Menu.buildFromTemplate([{ role: 'copy' }, { role: 'paste' }]);
menu.insert(1, new MenuItem({ type: 'separator' }));

getMenuItemById(id: string): MenuItem | null

Returns the item whose id matches, searching submenus depth-first. Returns null if no item has that id.

import { Menu } from 'bunmaska';

const menu = Menu.buildFromTemplate([
  { label: 'View', submenu: [{ id: 'theme', label: 'Dark Mode', type: 'checkbox' }] },
]);

const item = menu.getMenuItemById('theme');
console.log(item?.checked);

popup(options?: MenuPopupOptions): void

MenuPopupOptions is exported from bunmaska. Shows the menu as a context/popup menu, anchored to a window. The target window is the window option if given, else the focused window, else the most-recently-created window; if none can be resolved it throws.

Differences from Electron worth knowing:

  • x / y are content-relative and default to the top-left (0, 0) - not the current mouse cursor position.
  • The only supported options are window, x, and y. There is no frame, positioningItem, sourceType, or callback.
  • On macOS and Windows, popup() blocks - a nested menu-tracking loop runs until the menu is dismissed. On Linux it is non-blocking.
import { Menu } from 'bunmaska';

const ctx = Menu.buildFromTemplate([
  { label: 'Inspect', click: () => inspect() },
  { type: 'separator' },
  { role: 'copy' },
]);

// Anchor to a specific window at a content-relative point.
ctx.popup({ window: win, x: 120, y: 64 });

closePopup(window?: BrowserWindow): void

Closes a popup menu. With a window argument it targets that window; otherwise it targets the window the popup was opened on (falling back to the focused window).

Works on all three platforms. On macOS and Windows it only does something useful re-entrantly - for example, from inside an item’s own click handler - because popup() itself blocks until the menu is dismissed. On Linux it pops the popover down.

import { Menu } from 'bunmaska';

const menu = Menu.buildFromTemplate([
  { label: 'Dismiss me', click: () => menu.closePopup() },
]);
menu.popup({ window: win });

Properties

readonly items: MenuItem[]

The menu’s items, in order. Each MenuItem may nest a Menu in its submenu property.

import { Menu } from 'bunmaska';

const menu = Menu.buildFromTemplate([{ role: 'copy' }, { role: 'paste' }]);
for (const item of menu.items) {
  console.log(item.label, item.accelerator);
}

The MenuItem class

new MenuItem(options: MenuItemOptions)

Constructs a single menu item. All properties are read-only after construction. Supported MenuItemOptions:

  • label (string)
  • type ('normal' | 'separator' | 'submenu' | 'checkbox' | 'radio')
  • id (string) - for getMenuItemById
  • enabled (boolean, default true)
  • checked (boolean) - renders a checkmark on checkbox/radio items
  • accelerator (string) - a single-key accelerator like 'CmdOrCtrl+Q' (the bare key plus modifiers; multi-key chords are not parsed)
  • role (a role or macro role - see below)
  • click (() => void) - receives no arguments (no menuItem/browserWindow/event like Electron)
  • submenu (Menu or an array of MenuItemOptions)
import { MenuItem } from 'bunmaska';

const item = new MenuItem({
  id: 'wrap',
  label: 'Word Wrap',
  type: 'checkbox',
  checked: true,
  accelerator: 'Alt+Z',
  click: () => toggleWrap(),
});

Roles

A role gives an item a default label, accelerator, and native behavior with no explicit click. If both a role and a click are supplied, the role wins.

Item-level roles: undo, redo, cut, copy, paste, pasteAndMatchStyle, delete, selectAll, minimize, close, zoom, quit, togglefullscreen, about, hide, hideOthers, unhide.

Macro roles (each expands into a whole standard submenu): editMenu, windowMenu.

Platform notes from the source:

  • macOS wires every role to its standard first-responder selector (e.g. copy:, terminate:), routed up the responder chain.
  • Linux dispatches editing roles (undo/redo/cut/copy/paste/delete/selectAll/pasteAndMatchStyle) as WebKitGTK editing commands and window roles (minimize/close/zoom/togglefullscreen) as GTK window ops. Roles with neither - quit, about, hide, hideOthers, unhide - have no Linux menu-click wiring yet (their keyboard shortcuts still work natively via WebKit).
  • Windows role items are inert labels today - no accelerator table is installed, so neither the click nor the shortcut is wired.
import { Menu } from 'bunmaska';

// Macro roles save you from hand-writing the standard Edit / Window menus.
const menu = Menu.buildFromTemplate([{ role: 'editMenu' }, { role: 'windowMenu' }]);
Menu.setApplicationMenu(menu);

Not in Bunmaska (yet)

  • Events - Electron’s 'menu-will-show' and 'menu-will-close' are not emitted; Menu is not an EventEmitter here.
  • Menu.sendActionToFirstResponder(action) macOS - not implemented; use a role to get first-responder behavior instead.
  • menu.popup extras - no frame, positioningItem macOS, sourceType Windows/Linux, or callback option. x/y default to the top-left, not the mouse cursor.
  • click callback arguments - handlers receive nothing; there is no (menuItem, browserWindow, event) signature, no KeyboardEvent modifier flags.
  • Dynamic MenuItem mutation - items are read-only after construction. There are no settable enabled / checked / visible / label properties, no MenuItem.sublabel, icon, toolTip, acceleratorWorksWhenHidden, registerAccelerator, sharingItem, or commandId.
  • Deferred roles - appMenu, viewMenu, fileMenu, recentDocuments, shareMenu, services, startSpeaking/stopSpeaking, toggleDevTools, reload/forceReload, resetZoom/zoomIn/zoomOut, and the window-control roles (front, window, help) are not available. Only the role list above is supported.
  • Windows accelerator text - the menu bar and context menus work on Windows (Win32 HMENU), but accelerator/&-mnemonic text is not rendered into the labels yet, and role items are inert labels (no accelerator table).