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])| API | Operations |
|---|---|
app | quit (graceful application shutdown) |
dialog | openFile, openDirectory, saveFile |
clipboard | readText, writeText |
notification | show |
shell | openExternal (safe URL schemes only), showItemInFolder |
systemPermission | macOS status, request for camera, microphone, screen recording, and accessibility |
appWindow | getLabel, setTitle, setSize, minimize, toggleMaximize, show, hide, focus, close, setAlwaysOnTop, and state queries |
windows | open, list, show, hide, focus, close for declared labels |
tray | create, 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()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:
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:
| Action | Behavior |
|---|---|
<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.