Murasaki

Native APIs

What's exposed to your app from Murasaki's native surface today.

Murasaki's native window, menus, and OS integration are powered by @murasakijs/native — a self-authored Rust binding (tao/wry/muda) — so you never write Rust. This page covers what's actually reachable from your React app today.

Window & native menu bar

Window shape—size, title, route, visibility, permissions, and macOS vibrancy—is declared in murasaki.config.ts. window is the primary main window; windows contains labeled secondary windows. See Windows & permissions for the full model.

On macOS, the standard App/Edit/Window menu bar (About, Services, Hide, Quit, Edit, Window) is generated for you and localized from config.locales — no code required.

Context menu

The right-click context menu is declared with useContextMenu — a hook, not markup — and posts to the Rust side, which pops a real NSMenu / HMENU on macOS and Windows. See the dedicated Context Menu guide.

Dialogs, clipboard, notifications, shell, and window controls

Import renderer-safe native capabilities from murasaki/native. Every call is Promise-based and uses a request-correlated IPC message handled directly by the Rust host:

'use client'

import { app, appWindow, clipboard, dialog, notification, shell, systemPermission, tray, windows } from 'murasaki/native'

const files = await dialog.openFile({
  multiple: true,
  filters: [{ name: 'Images', extensions: ['png', 'jpg'] }],
})
await clipboard.writeText(files.join('\n'))
await notification.show({ title: 'Files selected', body: `${files.length} files` })
await appWindow.setTitle('Import complete')
console.log(await appWindow.getLabel())
await windows.open('preview')
await shell.showItemInFolder(files[0])
APIOperations
appquit (graceful application shutdown)
dialogopenFile, openDirectory, saveFile
clipboardreadText, writeText
notificationshow
shellopenExternal (safe URL schemes only), showItemInFolder
systemPermissionmacOS status, request for camera, microphone, screen recording, and accessibility
appWindowgetLabel, setTitle, setSize, minimize, toggleMaximize, show, hide, focus, close, setAlwaysOnTop, and state queries
windowsopen, list, show, hide, focus, close for declared labels
traycreate, remove, setTooltip, setIcon, setMenu, onClick, onMenuItem

app.quit() and the root quit() helper both require app:quit; an unprivileged secondary renderer cannot terminate the whole application.

0.50 migration: quit() used to post an unpermissioned raw IPC message. Existing non-updater apps that call it must add app:quit to that window. Enabling the built-in updater grants it to the primary window only so the verified install/restart handshake remains backwards-compatible.

These APIs run in trusted renderer code, not directly in src/main.ts. The bridge accepts messages only from the app's exact origin and exposes a fixed, default-deny command allowlist. Grant only the commands each renderer uses through window.capabilities / windows[label].capabilities. Argument and path/URL scopes are still planned, so do not load untrusted remote content into a privileged app window.

Tray icon

Create one system tray icon from a client component. It uses config.icon by default, or an explicit 8-bit RGB/RGBA PNG. Every operation is independently permissioned:

import { tray } from 'murasaki/native'

await tray.create({
  tooltip: 'Sync is running',
  template: true,
  // macOS status-item menu / Windows system-tray menu
  menu: [
    { id: 'open', label: 'Open Murasaki' },
    { separator: true },
    { id: 'quit', label: 'Quit' },
  ],
  menuOnLeftClick: navigator.userAgent.includes('Mac OS X'),
  menuOnRightClick: true,
})
const unsubscribe = tray.onClick(({ button, double }) => {
  console.log({ button, double })
})
const unsubscribeMenu = tray.onMenuItem(async (id) => {
  if (id === 'open') await appWindow.show()
  if (id === 'quit') await app.quit()
})

await tray.setTooltip('Sync complete')
await tray.setIcon('/absolute/path/to/synced.png')
await tray.setMenu([{ id: 'open', label: 'Open Murasaki' }])
// Later: unsubscribe(); unsubscribeMenu(); await tray.remove()
murasaki.config.ts
export default defineConfig({
  // ...
  capabilities: [
    'tray:create',
    'tray:setTooltip',
    'tray:setIcon',
    'tray:setMenu',
    'tray:remove',
    'window:show',
    'app:quit',
  ],
})

template is macOS-specific. Creating a second tray icon replaces the first. Tray menu items are event-driven: every clickable item needs a unique id, and privileged actions such as quitting still pass through their own native capability. Closing the renderer that created the process-wide icon removes it. Global shortcuts and Linux tray support are not available yet.

System permissions

OS consent is separate from Murasaki's renderer capability allowlist. For a packaged macOS app, declare purpose text and optional launch-time prompts in config. Purpose strings are written into Info.plist; missing camera or microphone descriptions are rejected before a prompt can crash the app:

murasaki.config.ts
export default defineConfig({
  // ...
  systemPermissions: {
    macOS: {
      camera: {
        usageDescription: 'Use your camera for video calls.',
        requestOnLaunch: true,
      },
      microphone: {
        usageDescription: 'Use your microphone for voice calls.',
      },
      screenRecording: { requestOnLaunch: false },
      accessibility: { requestOnLaunch: false },
    },
  },
  capabilities: [
    'systemPermission:status',
    'systemPermission:request',
  ],
})

For contextual prompts from a client component:

const status = await systemPermission.status('microphone')
if (status === 'notDetermined') {
  await systemPermission.request('microphone')
}

Camera and microphone prompts complete asynchronously at the OS level, so request() may initially return notDetermined; query status() again when the app regains focus before enabling the protected feature.

requestOnLaunch applies to packaged macOS apps. Test TCC behavior from a packaged build because development runs under the terminal/Node host identity. On Windows, unpackaged desktop camera/microphone consent is requested by the device API when it is used, not through a generic application-start prompt; Murasaki therefore reports unsupported instead of pretending permission was granted. Screen recording and accessibility return notGranted when macOS cannot distinguish first-use from denial.

Built-in menu actions

An item's action can be one of the built-in <Action.*/> elements instead of a function — these run a native role (handled by the OS itself) or a small client-side behavior:

ActionBehavior
<Action.Copy />, <Action.Paste />, <Action.Cut />, <Action.SelectAll />, <Action.Undo />, <Action.Redo />Native OS edit roles
<Action.Quit />Native quit role
<Action.Reload />Reloads the window (location.reload())
<Action.Navigate to="/path" />Client-side navigation via the router
<Action.Run action={fn} />Runs a plain function (same as passing the function directly)

Auto-update

useUpdate() checks for, downloads, and installs updates — its check/download logic runs in Node and is reached over the same local HTTP server that serves the rest of the app (like Server Actions and API routes), not the IPC bridge the context menu and app menu use. <UpdateButton /> (also from murasaki, styled with @murasakijs/ui) is a ready-made button that wraps it:

import { useUpdate } from 'murasaki'

const { status, latest, check, download, install } = useUpdate()

status moves through idle → checking → available → downloading → ready (or not-available / error). Setup is two commands — see the dedicated Auto-update guide for the manifest format, the security model, and a GitHub Actions release workflow.

Global shortcuts and argument/path-scoped capability rules are not public yet. Check the platform feature status before designing around them.

Next

Improve this page on GitHub

On this page