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 and GTK menus on Linux. There is no Windows backend (Bunmaska is macOS + Linux only).

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. Passing null clears the stored application menu.

Note: in the current source, passing null updates the stored reference (so getApplicationMenu() returns null) but does not push an empty/cleared menu down to the native layer - only a non-null menu is realized and installed.

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

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, popup() blocks - AppKit runs a nested menu-tracking loop 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).

On macOS this 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).
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 - there is no Windows backend at all; &-mnemonic accelerator generation is a Windows/Linux Chromium feature Bunmaska does not implement.