Tauri でグローバルシングルトンを実装する

# はじめに

特に何も実装しない状態だと、Tauri は複数ウィンドウの起動を許してしまう。
しかしながら、タスクベースだったり、ファイルIO があるなどのシステム影響があるものはなるべくグローバルなレベルでのインスタンスは一つに抑えたい。つまり、該当アプリケーションの起動は単一起動のみにしたい。
これをふるまわせるためにシングルトンとしての振る舞いを付与する方法についてのメモ。

## 前提: 環境

* Windows 11
* Rust v1.72.0
* Tauri v1.4.0

# Tauri でのシングルトン化について

Tauri でのシングルトン化を実現するためには、[single-instance](https://github.com/tauri-apps/plugins-workspace/tree/v1/plugins/single-instance) というプラグインを使う。

https://github.com/tauri-apps/plugins-workspace/tree/v1/plugins/single-instance

## インストール

Cargo.toml の依存関係に追加。

```toml:src_tauri/Cargo.toml
[dependencies]
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
```

:::note info
この signleton-instance のような公式プラグインは、個別にインストールするというよりは、 [plugins-workspace](https://github.com/tauri-apps/plugins-workspace) というパッケージとしてインストールすることを薦めている。そのため、Cargo.toml に記述するインストール元の Git レポジトリは plugins-workspace を指している。
:::

## プラグインを有効化する。

```diff_rust:src_tauri/src/main.rs
+use tauri::Manager;

+#[derive(Clone, serde::Serialize)]
+struct Payload {
+  args: Vec<String>,
+  cwd: String,
+}

fn main() {
    tauri::Builder::default()
+        .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
+            println!("{}, {argv:?}, {cwd}", app.package_info().name);
+
+            app.emit_all("single-instance", Payload { args: argv, cwd }).unwrap();
+        }))
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

この状態で実行すると、次のようなプリントがされる。

```
<アプリケーション名>, <引数 (列の最初は実行ファイルパス)>, <アプリケーションの実行位置>
```



# 別インスタンスを立ち上げようとする際の振る舞いを変える

例えば、既にシングルトン側のインスタンスを立ち上げていて、そのウィンドウを最小化していたり、別のウィンドウに隠れていたりする場合、そのウィンドウを前面にもってきたり、通常表示したりしたい。

そうした場合に、上記コードに以下のように変更を加える。

```diff_rust:src_tauri/src/main.rs
fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
            app.emit_all("single-instance", Payload { args: argv, cwd }).unwrap();

+            let singleton_window = app.get_window("main").unwrap();
+
+            // hide してた場合に show する。
+            singleton_window.show().unwrap();
+
+            // 最小化されてる際、unminimize する。
+            if singleton_window.is_minimized().unwrap() {
+                singleton_window.unminimize().unwrap();
+            }
+
+            // ウィンドウが focus されていない場合、focus する。
+            if !singleton_window.is_focused().unwrap() {
+                singleton_window.set_focus().unwrap();
        }))
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

二個目以上のインスタンスの場合、 `app.emit_all(...).unwrap()` 時にそのプロセスは終了してしまうので、その後の処理は呼び出されない。

シングルトン側の方は、その後の処理も呼び出されるので、追加した部分の処理が実行される。

![output-palette.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/e4631c19-a2c2-4e50-f3b5-bcf40143c593.gif)


これで、目的の振る舞いが達成できた。

# Tuari v2

## インストール

Tauri v2 からは、tauri コマンドの add 機能を使ってインストールする。

```powershell
cargo tauri add single-instance
```

https://v2.tauri.app/plugin/single-instance/

インストールした時点で、**lib.rs** に次の様に追加される。

```diff_rust:src-tauri/src/lib.rs
pub fn run() {
    tauri::Builder::default()
+       .plugin(tauri_plugin_single_instance::init())
        .plugin(tauri_plugin_shell::init())
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

## 実装

さらに、この `init()` に実装を行う。

```diff_rust:src-tauri/src/lib.rs
pub fn run() {
    tauri::Builder::default()
+       .plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
+           let _ = app
+               .get_webview_window("main")
+               .expect("no main window")
+               .set_focus();
+       }))
        .plugin(tauri_plugin_shell::init())
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

## マルチプラットフォームにおける対応

Tauri v2 では、正式に iOS/Andoroid もサポートされたため、それらの起動と切り分ける必要が出てくる。
特にこのグローバルシングルトン (シングルインスタンス) は、モバイルでは機能しないのでプラグインとして組み込まないように設定したい。
この場合は、ビルドを分けて対応する。

```diff_rust:src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    #[cfg(desktop)]
    {
        tauri::Builder::default()
            .plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
                let _ = app
                    .get_webview_window("main")
                    .expect("no main window")
                    .set_focus();
            }))
            .plugin(tauri_plugin_shell::init())
            .invoke_handler(tauri::generate_handler![greet])
            .run(tauri::generate_context!())
            .expect("error while running tauri application");
    }

    // iOS and Android
    #[cfg(mobile)]
    {
        tauri::Builder::default()
            .invoke_handler(tauri::generate_handler![greet])
            .run(tauri::generate_context!())
            .expect("error while running tauri application");
    }
}
```

# まとめ

けっこう、こうしたシングルトン化ってめんどくさかったりするが、さくっと実装できたと思う。

Tauri はこうした細かい振る舞いを続々盛り込むのに Tauri のプラグイン機構を公式レベルからふんだんに活用している。探すとまた色々出てくるので、また面白そうなものがあったらまとめたい。