ウィンドウと権限
複数のネイティブウィンドウを宣言し、各レンダラーへ必要最小限のコマンドだけを許可する。
Murasaki のウィンドウは murasaki.config.ts で宣言します。既存の window がプライマリウィンドウで、ラベルは常に main です。追加のウィンドウは、安定したラベルをキーとして windows に記述します。
import { defineConfig } from 'murasaki'
export default defineConfig({
appId: 'com.example.notes',
productName: 'Notes',
capabilities: ['clipboard:readText'], // main だけのfallback
window: {
route: '/',
width: 1100,
height: 760,
capabilities: ['window:getLabel', 'window:open', 'window:list', 'window:manage'],
},
windows: {
settings: {
route: '/settings',
title: 'Settings',
width: 720,
height: 560,
// secondaryのvisibleは既定false
capabilities: [],
},
preview: {
route: '/preview',
visible: false,
capabilities: ['clipboard:readText'],
},
report: {
route: '/report',
createOnLaunch: false,
capabilities: [],
},
},
})宣言されたウィンドウの createOnLaunch はデフォルトで true です。起動時に作成されるセカンダリウィンドウは、visible: true を指定しない限り非表示で開始します。createOnLaunch: false にすると、信頼された Node Main が windows.create(label) を呼ぶまで、その宣言を休止状態のままにできます。セカンダリウィンドウで capabilities を省略すると、すべての権限が拒否された状態になり、トップレベルのリストは継承されません。プライマリウィンドウは常に起動時に作成され、window.capabilities ?? capabilities ?? [] の順に権限を解決します。
非表示のセカンダリウィンドウは、通常 main のような表示中のレンダラーから開いてください。WebKit や WebView2 は、非表示のままレンダラーを実行する場合もあれば、表示されるまで JavaScript の実行を遅延させる場合もあります。どちらのタイミングにも依存せず、非表示のレンダラー自身に windows.open() で自分自身を開かせる設計にはしないでください。ネイティブの可視性は OS のイベントループを通じて反映されるため、windows.list() でオープン後の状態を確認する必要がある場合は、最初の読み取りがコンポジターと同期していると仮定せず、visible が true になるまで待ってください。
Windows バックエンドの console オプションは、アプリケーションバックエンドコンソール全体を制御するため、プライマリウィンドウ専用です。セカンダリウィンドウの宣言に console を書くと、設定解決時に拒否されます。
ラベルは 1〜64 文字で、先頭は英数字、残りは英数字・.・_・- だけを利用できます。main は予約済みです。route は / で始まる同一オリジンのパスだけを受け付け、完全な URL、プロトコル相対 URL、バックスラッシュは設定解決時に拒否されます。
ウィンドウを識別・操作する
レンダラー自身のウィンドウには appWindow を、ラベルで指定する宣言済みのウィンドウには windows を使います。
'use client'
import { appWindow, windows } from 'murasaki/native'
const current = await appWindow.getLabel()
await windows.open('settings')
await windows.focus('settings')
const all = await windows.list()
const settings = all.find((item) => item.label === 'settings')
console.log({ current, settings })
await windows.hide('settings')windows.list() は WindowInfo[] を返します。
interface WindowInfo {
label: string
primary: boolean
visible: boolean
focused: boolean
minimized: boolean
maximized: boolean
}| API | ケイパビリティ | 動作 |
|---|---|---|
appWindow.getLabel() | window:getLabel | 現在のレンダラーのラベルを返す。 |
windows.open(label) | window:open | 生存中の宣言済みウィンドウを表示・復元してフォーカスする。 |
windows.list() | window:list | 生存中の宣言済みウィンドウすべての状態を返す。 |
windows.show(label) / hide(label) / focus(label) / close(label) | window:manage | 別の宣言済みウィンドウを操作する。 |
設定にないラベルは利用できません。任意のルートや実行時のウィンドウを動的に生成する API ではありません。windows.open(label) は表示のみを行う API であり、休止状態または破棄済みの宣言に対しては拒否されます。先に Node Main のウィンドウマネージャーでその宣言を作成してください。
OS の close コントロールと appWindow.close() はセカンダリウィンドウを非表示にするため、windows.open(label) で同じ宣言済みウィンドウを再表示できます。一方、windows.close(label) はセカンダリウィンドウのターゲットを明示的に破棄します。破棄したセカンダリウィンドウは、信頼された Node Main からだけ再生成できます。main のクローズは、引き続きアプリの終了リクエストとして扱われます。Node Mainも参照してください。
フレームレスウィンドウとカスタムタイトルバー
decorations: false を指定すると、すべてのプラットフォームで OS のウィンドウ枠を取り除けます。macOS の titleBarStyle: 'hidden' は、信号機ボタンを残したままタイトルテキストだけを隠し、WebView をその下まで広げます。
export default defineConfig({
appId: 'com.example.notes',
productName: 'Notes',
window: {
decorations: false, // 全platformでframeless
// titleBarStyle: 'hidden', // macOS専用。Windows/Linuxではconfig warning
// // 付きで無視されます
},
})ネイティブのタイトルバーがないと、OS 側にウィンドウをドラッグするための領域がなくなります。useWindowDrag() で、任意の領域をドラッグ可能にしてください。
'use client'
import { appWindow } from 'murasaki/native'
import { useWindowDrag } from 'murasaki'
function Titlebar() {
const drag = useWindowDrag()
return (
<header {...drag} style={{ WebkitUserSelect: 'none' }}>
<span>My App</span>
<button data-murasaki-no-drag onClick={() => appWindow.close()}>
×
</button>
</header>
)
}useWindowDrag() は、プライマリボタンの pointerdown でネイティブドラッグを開始します。インタラクティブなターゲット(button、input、a、select、textarea、または data-murasaki-no-drag が付いた要素)はスキップするため、ドラッグ可能な領域の内側にあるタイトルバーのコントロールも問題なく動作します。ネイティブレンダラーの外や、OS がドラッグを拒否した場合(例: マウスボタンが押されていない場合)は何もしません。
フルスクリーン、最大サイズ、モニター
| API | ケイパビリティ | 動作 |
|---|---|---|
appWindow.startDragging() | window:manage | OS のウィンドウドラッグを開始する。上記の useWindowDrag() を参照。 |
appWindow.setFullscreen(bool) / isFullscreen() | window:manage | ウィンドウの現在のモニターでボーダーレスフルスクリーンを開始・終了する。専用ビデオモードによる排他的フルスクリーンには未対応。 |
appWindow.setMaxSize({ width?, height? }) | window:manage | 最大の内側サイズを設定する。両方省略、または null を指定するとクリアされる。片方だけの指定は拒否され、両方指定するか、どちらも指定しない必要がある。 |
appWindow.getMonitors() | window:manage | ウィンドウから見えるすべての OS ディスプレイを物理ピクセルで返す。 |
これらは呼び出したレンダラー自身のウィンドウを操作するため、window:manage は許可の有無だけで判定します。上記の windows.show/hide/focus/close(label) と異なり、スコープ付きの許可が適用対象とする別ウィンドウの label 引数はありません。
window.fullscreen(windows.<label> 配下も同様)は、起動時の初期状態を設定します。window.maxWidth / maxHeight は初期の最大サイズを設定し、両方指定した場合は minWidth / minHeight 以上である必要があります。
export default defineConfig({
appId: 'com.example.notes',
productName: 'Notes',
window: { maxWidth: 1600, maxHeight: 1200 },
})getMonitors() は { monitors: WindowMonitorInfo[] } を resolve します。
interface WindowMonitorInfo {
name: string | null
isPrimary: boolean
isCurrent: boolean
x: number
y: number
width: number
height: number
scaleFactor: number
}プライマリウィンドウのクローズとアプリのライフサイクル
セカンダリウィンドウで OS のクローズコントロールまたは appWindow.close() を使うと、そのウィンドウだけが非表示になります。main のクローズはアプリの終了リクエストであり、beforeQuit() が実行され、その中でキャンセルできます。その後、時間上限付きの shutdown() を経て、ネイティブホストが終了します。ウィンドウ別のクローズフックは、今のところ用意されていません。
セッション分離
WebContext / プロファイルはウィンドウのラベルごとに分離されるため、起動時に作成されたラベルと動的に作成されたラベルの間で、cookie、Web Storage、Service Worker、SharedWorker が共有されることはありません。同じラベルをプロセス内で再作成した場合は、コンテキストが再利用されます。Windows では、永続的なラベルごとのプロファイルを、アプリ専用の WebView2 データディレクトリ以下に保存します。webview.incognito: true では、すべてのラベルが非永続的なコンテキストを使用します。
ウィンドウ別の最小権限
権限は呼び出しを行うレンダラーごとに評価されます。React の状態だけを編集する設定用のウィンドウには capabilities: [] を、クリップボードを読み取るプレビュー用のウィンドウには clipboard:readText だけを付与できます。ウィンドウ管理権限は、通常、信頼するプライマリレンダラーだけに置きます。
export default defineConfig({
appId: 'com.example.notes',
productName: 'Notes',
window: {
route: '/',
capabilities: [
'window:list',
{ permission: 'window:open', allow: { windows: ['settings'] } },
{ permission: 'window:manage', allow: { windows: ['settings'] } },
],
backendCapabilities: ['api:GET:/api/account'],
},
windows: {
settings: { route: '/settings', capabilities: [], backendCapabilities: ['api:GET:/api/settings'] },
importer: { route: '/import', capabilities: ['dialog:openFile'], backendCapabilities: [] },
},
})ウィンドウ別のケイパビリティ一覧は、侵害されたレンダラーが呼び出せるネイティブコマンド名を制限します。構造化された window:open / window:manage の許可では、完全一致するターゲットラベルも限定でき、deny は allow より優先されます。ダイアログのデフォルト値など、他のコマンド引数はまだスコープできません。権限を持つレンダラーにリモートコンテンツや実行可能なユーザー作成コンテンツを読み込ませず、広範な操作についてはアプリケーションレベルの意図も検証してください。
ネイティブの許可リストとバックエンドの許可リストは、それぞれ別の境界を保護します。各ネイティブウィンドウはラベルに紐づく HMAC アイデンティティを受け取り、backendCapabilities が Server Actions、'use main'、API Routes、アップデーターのルート、イベント、診断を制限します。セカンダリウィンドウは、デフォルトでバックエンドの許可を持ちません。ルートは同じ HTTP オリジンを使いますが、ブラウザプロファイルはウィンドウごとに分離されるため、あるウィンドウの Service Worker、SharedWorker、cookie、ストレージが、別のウィンドウのバックエンド権限を引き継ぐことはありません。プライマリウィンドウは、従来のアプリケーションプロファイルを維持します。セカンダリウィンドウのプロファイルは Windows / Linux と macOS 14+ で永続化され、カスタムの永続的な WebKit ストアを使えない macOS 11–13 では、ウィンドウ別の非永続的なストアへ安全側に倒れます。ただし、これは完全なレンダラーサンドボックスではなく、XSS は自身のウィンドウに許可されたリソースをすべて利用できます。ハンドラー内でもユーザー / セッション / オブジェクト単位の認証・認可を行い、信頼できない実行可能なコンテンツをアプリケーションウィンドウへ読み込まないでください。複数のウィンドウで共有する永続的な状態は、ブラウザストレージではなく Main / API ハンドラーを経由させてください。