🎄

Tauriでメニューバーにアプリを常駐しよう[Rust編]

2024/12/04に公開

はじめに

以前[Rust]はじめてのデスクトップアプリ開発 with React × Tauriという記事を書いたのですが、
今年はその Tauri が待望の v2.0 にアップデートされました。

https://v2.tauri.app/blog/tauri-20/#enhancements

目玉機能であるモバイル対応が Stable になり、個人的には激アツなアップデートとなりました。
今回はそのうちの一つである、System Tray APIを使って、
メニューバーに常駐するアプリケーションを作成してみたいと思います。

(筆者のアドカレの準備不足により)
JavaScript の System Tray API については想定の挙動を実現できてないため、
続編として別途記事にさせていただきます、、!

本記事の中では Rust の API 編として記載させていただきます。

セットアップ

まずは任意のディレクトリで Tauri のプロジェクトを作成します。
今回は例をシンプルにするため、npm と React(TypeScript)を使うことにします。

npm create tauri-app@latest

https://v2.tauri.app/start/

// 以下実行結果
npm create tauri-app@latest
Need to install the following packages:
create-tauri-app@4.5.7
Ok to proceed? (y) y
✔ Project name · system-tray-sample
✔ Identifier · com.system-tray-sample.app
✔ Choose which language to use for your frontend · TypeScript / JavaScript - (pnpm, yarn, npm, deno, bun)
✔ Choose your package manager · npm
✔ Choose your UI template · React - (https://react.dev/)
✔ Choose your UI flavor · TypeScript
// cd <作成したプロジェクト名>
cd system-tray-sample

// モジュールのインストール
npm install

Tauri を起動して、下記のような画面が表示されれば準備完了です。

npm run tauri dev

Cargo.toml の編集

まずプロジェクト名/src-tauri/Cargo.tomldependencies セクションにある
tauri = { version = "2", features = [] }tray-iconを追加します

toml
- tauri = { version = "2", features = [] }
+ tauri = { version = "2", features = [ "tray-icon" ] }

メニューバーにアイコンを表示

次に、TrayIconBuilderのインスタンスを使ってメニューバーにアイコンを表示させます。
lib.rsを下記のように編集します。

lib.rs
+use tauri::tray::TrayIconBuilder;

// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
+        .setup(|app| {
+            let _tray = TrayIconBuilder::new()
+                .icon(app.default_window_icon().unwrap().clone())
+                .build(app)?;
+            Ok(())
+        })
        .plugin(tauri_plugin_shell::init())
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

この状態で Tauri を立ち上げると、メニューバーに Tauri のデフォルトのアイコンが表示されます。

npm run tauri dev

現時点ではメニューを追加していないため、
クリックしても何も起きません。
メニューを追加して、任意の処理を実行できるようにしていきます。

https://docs.rs/tauri/2.1.1/tauri/tray/struct.TrayIconBuilder.html#method.on_menu_event

メニューの追加

menu::{Menu, MenuItem}をインポートし、
メニューとそのアイテムを追加していきます。

lib.rs
- use tauri::tray::TrayIconBuilder;
+use tauri::{
+    menu::{Menu, MenuItem},
+    tray::TrayIconBuilder,
+};

// 省略
    tauri::Builder::default()
        .setup(|app| {
+            // メニューの追加
+            let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
+            let menu = Menu::with_items(app, &[&quit_i])?;
+            let _tray = TrayIconBuilder::new()
+                .menu(&menu)
+                .menu_on_left_click(true)
                .icon(app.default_window_icon().unwrap().clone())
                .build(app)?;
            Ok(())
        })
npm run tauri dev

メニューバーのアイコンをクリックした際に、Quitというメニューが表示されるようになりました。

https://docs.rs/tauri/2.1.1/tauri/menu/struct.Menu.html
https://docs.rs/tauri/2.1.1/tauri/menu/struct.MenuItem.html

ハンドラーの追加

続いて、メニューアイテムのハンドラーを追加していきます。
on_menu_eventメソッドを追加し、
メニューアイテムの ID をパターンマッチングして、任意の処理を追加します。

今回はクリックした時にアプリケーションを終了するようにしておきます。

lib.rs
 let _tray = TrayIconBuilder::new()
                .menu(&menu)
                .menu_on_left_click(true)
+                .on_menu_event(|app, event| match event.id.as_ref() {
+                    "quit" => {
+                        println!("quit menu item was clicked");
+                        app.exit(0);
+                    }
+                    _ => {
+                        println!("menu item {:?} not handled", event.id);
+                    }
+                })
                .icon(app.default_window_icon().unwrap().clone())
                .build(app)?;

https://docs.rs/tauri/2.1.1/tauri/tray/struct.TrayIconBuilder.html#method.on_menu_event

セパレーターの追加

最後に、セパレーターを追加してメニューアイテムの見た目に区切りを入れてみます。
セパレーターはmenu::PredefinedMenuItemより追加できます。

lib.rs
+use tauri::{
-    menu::{Menu, MenuItem},
+    menu::{Menu, MenuItem, PredefinedMenuItem},
    tray::TrayIconBuilder,
};

// 省略
let hide_i = MenuItem::with_id(app, "hide", "Hide", true, None::<&str>)?;
+            let separator = PredefinedMenuItem::separator(app)?;
+            let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
-            let menu = Menu::with_items(app, &[&quit_i])?;
+            let menu = Menu::with_items(app, &[&hide_i, &separator, &quit_i])?;
            let _tray = TrayIconBuilder::new()
                .menu(&menu)
                .menu_on_left_click(true)
                .on_menu_event(|app, event| match event.id.as_ref() {
                    "quit" => {
                        println!("quit menu item was clicked");
                        app.exit(0);
                    }
+                    "hide" => {
+                        println!("hide menu item was clicked");
+                        app.hide().unwrap();
+                    }
                    _ => {
                        println!("menu item {:?} not handled", event.id);
                    }
                })

上記のように編集することで、セパレーターの見た目を追加できました。

https://docs.rs/tauri/2.1.1/tauri/menu/struct.PredefinedMenuItem.html


以上で Rust 側の記述は終了です。

lib.rsの最終形は以下のような形になっています。

lib.rs
use tauri::{
    menu::{Menu, MenuItem, PredefinedMenuItem},
    tray::TrayIconBuilder,
};

// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            // メニューの追加
            let hide_i = MenuItem::with_id(app, "hide", "Hide", true, None::<&str>)?;
            let separator = PredefinedMenuItem::separator(app)?;
            let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
            let menu = Menu::with_items(app, &[&hide_i, &separator, &quit_i])?;
            let _tray = TrayIconBuilder::new()
                .menu(&menu)
                .menu_on_left_click(true)
                .on_menu_event(|app, event| match event.id.as_ref() {
                    "quit" => {
                        println!("quit menu item was clicked");
                        app.exit(0);
                    }
                    "hide" => {
                        println!("hide menu item was clicked");
                        app.hide().unwrap();
                    }
                    _ => {
                        println!("menu item {:?} not handled", event.id);
                    }
                })
                .icon(app.default_window_icon().unwrap().clone())
                .build(app)?;
            Ok(())
        })
        .plugin(tauri_plugin_shell::init())
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

まとめ

Tauri 2.0 の System Tray API を使って、メニューバーに常駐するアプリを作成することができました!
次回 JavaScript 側の API でもリベンジしてみたいと思います!

余談

Mac の場合、メニューバーにあるアプリは
⌘+ドラッグで位置を変えることができます

Aidemy Tech Blog

Discussion