Windows と macOS でメニューを出し分ける

Recipe ID: menu-015

macOS と Windows/Linux では、標準的なメニュー構成が異なります。特に macOS では、メニューバーの一番左(Apple ロゴの右隣)にアプリケーション名が表示される「アプリメニュー」が必要です(About や Quit はここに含まれます)。

Tauri v2 では、JavaScript API (@tauri-apps/plugin-os) を使って実行中のプラットフォームを判定し、動的にメニュー項目を出し分けることができます。

前提条件

プラグインの追加

OS 情報を取得するために、os プラグインを導入します。

npm run tauri add os

src-tauri/src/lib.rs:

.plugin(tauri_plugin_os::init()) を追加します。os プラグインをインストールすると自動的に挿入されるはずですので確認してください。

pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_os::init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Permissions (権限) の設定

src-tauri/capabilities/default.json に以下の権限を追加します。

{
  "permissions": [
    ...,
    "core:menu:default",
    "os:default"
  ]
}

1. フロントエンドで実装する (TypeScript)

type() 関数でプラットフォームを取得し、macOS (macos) の場合のみアプリメニューを追加するロジックを組みます。

import { Menu } from '@tauri-apps/api/menu';
import { type } from '@tauri-apps/plugin-os';

async function createMenu() {
  const platform = await type(); // 'macos', 'windows', 'linux', ...
  const menuItems = [];

  // macOS の場合のみ、アプリケーションメニューを追加
  if (platform === 'macos') {
    const appMenu = {
      text: 'MyApp', // macOSではここには自動的にアプリ名が入る
      items: [
        { item: 'About', text: 'MyApp について' },
        { item: 'Separator' },
        { item: 'Quit', text: '終了' },
      ]
    };
    menuItems.push(appMenu);
  }

  // 共通のファイルメニュー
  const fileMenu = {
    text: 'ファイル',
    items: [
      {
        id: 'new',
        text: '新規作成',
        accelerator: 'CmdOrCtrl+N'
      },
      { item: 'Separator' },
      {
        id: 'close',
        text: '閉じる',
        accelerator: 'CmdOrCtrl+W'
      }
    ]
  };
  menuItems.push(fileMenu);

  // アプリケーションメニューとしてセット
  const menu = await Menu.new({ items: menuItems });
  await menu.setAsAppMenu();
}

createMenu();

2. バックエンドで実装する (Rust)

Rust の場合は #[cfg(target_os = "macos")] などの属性を使用して、コンパイル時にコードを分けるのが一般的で効率的です。

use tauri::menu::{MenuBuilder, SubmenuBuilder, PredefinedMenuItem, MenuItem};
use tauri::Manager;

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            // メニュービルダの開始
            let mut menu_builder = MenuBuilder::new(app);

            // macOS の場合のみアプリメニューを追加
            #[cfg(target_os = "macos")]
            {
                let app_menu = SubmenuBuilder::new(app, "MyApp")
                    .about(None)
                    .separator()
                    .quit()
                    .build()?;
                
                menu_builder = menu_builder.item(&app_menu);
            }

            // 共通: ファイルメニュー
            let file_menu = SubmenuBuilder::new(app, "ファイル")
                .item(&MenuItem::with_id(app, "new", "新規作成", true, Some("CmdOrCtrl+N"))?)
                .separator()
                .close_window() // Predefined の CloseWindow
                .build()?;
            
            menu_builder = menu_builder.item(&file_menu);

            // ビルドしてセット
            let menu = menu_builder.build()?;
            app.set_menu(menu)?;

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

補足

* CmdOrCtrl: アクセラレータの記述で CmdOrCtrl を使うことで、修飾キーの違い(macOSのCommand、WindowsのCtrl)も自動的に吸収されます。
* 条件付きコンパイル: Rust の #[cfg(...)] を使うと、対象外のOSではコード自体がコンパイルされないため、バイナリサイズや実行時のオーバーヘッドを削減できます。