Rust で Windows のトースト通知を表示する

# 動機

Rust で CLI アプリ作ってるときに Windows のトースト通知を表示したくなったので、どうやって表示するのか調べました。

# 環境

rustc 1.78.0
Windows 10
[windows-rs](https://crates.io/crates/windows) 0.54.0

# トースト通知とは

ウィンドウの右下に現れるアレです。

![toast.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353186/b1280b25-ffd0-b70c-0cb0-449a734ed91f.png)

# 手順

Microsoft 公式の[ドキュメント](https://learn.microsoft.com/ja-jp/windows/apps/design/shell/tiles-and-notifications/send-local-toast-other-apps#step-1-register-your-app-in-the-registry)があるので、おおよそこの流れに従います。

## 手順 1: レジストリにアプリを登録する

レジストリに`AUMID`キーを登録します。今回はレジストリエディタで手動で追加しました。`DisplayName`文字列値を追加するだけで良いようです。

![regedit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353186/bc4d9e5e-8fe7-712e-d48c-029bee29d626.png)

## 手順 2: COM アクティベーターを設定する

COM アクティベーターというのを設定するとトースト通知からデータを取得できるようですが、今回は使わないのでスキップ。

## 手順 3: トーストを送信する

公式ドキュメントの C++ コードを Rust に写経します。

```rust
// UTF-8 を UTF-16 の Vec<u16> に変換
let text = text.encode_utf16().collect::<Vec<_>>();

// トーストテンプレート生成
let content = XmlDocument::new()?;
content.LoadXml(h!("<toast><visual><binding template='ToastGeneric'><text></text></binding></visual></toast>"))?;

// テキストの挿入
content
    .SelectSingleNode(h!("//text[1]"))?
    .SetInnerText(&HSTRING::from_wide(&text)?)?;

// 通知の生成
let notification = ToastNotification::CreateToastNotification(&content)?;

// AUMID はレジストリに設定したものを使う
let toast =
    ToastNotificationManager::CreateToastNotifierWithId(h!("ToastExample.MyApp"))?;

// 通知の表示
toast.Show(&notification)?;
```

これで表示されるのかと思ったけどダメでした。通知の表示が終わる前にメインスレッドが終了するとダメみたいです。なので、もう少し工夫します。

## 手順 4: トースト表示を別スレッドで実行して終了を待つ

ということでトースト通知を表示する関数は下記のようになりました。

```rust
fn toast_show(text: String) -> Result<()> {
    let (tx, rx) = mpsc::channel();

    // スレッドを生成
    thread::spawn(move || -> Result<_> {
        let text = text.encode_utf16().collect::<Vec<_>>();

        let content = XmlDocument::new()?;
        content.LoadXml(h!("<toast><visual><binding template='ToastGeneric'><text></text></binding></visual></toast>"))?;
        content
            .SelectSingleNode(h!("//text[1]"))?
            .SetInnerText(&HSTRING::from_wide(&text)?)?;

        let notification = ToastNotification::CreateToastNotification(&content)?;

        // トースト通知が非表示なったときに呼ばれる
        let tx_clone = tx.clone();
        notification.Dismissed(&TypedEventHandler::new(move |_, _| {
            tx_clone.send(()).ok();
            Ok(())
        }))?;

        // トースト通知がクリックされたときに呼ばれる
        let tx_clone = tx.clone();
        notification.Activated(&TypedEventHandler::new(move |_, _| {
            tx_clone.send(()).ok();
            Ok(())
        }))?;

        // トースト通知の表示に失敗したときに呼ばれる？
        notification.Failed(&TypedEventHandler::new(move |_, _| {
            tx.send(()).ok();
            Ok(())
        }))?;

        let toast =
            ToastNotificationManager::CreateToastNotifierWithId(h!("ToastExample.MyApp"))?;
        toast.Show(&notification)?;
        Ok(())
    });

    // トースト通知の終了を待つ
    rx.recv()?;
    Ok(())
}
```

# まとめ

<details><summary>コピペ用サンプルコード</summary>

```toml:Cargo.toml
[package]
name = "toast"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0"

[dependencies.windows]
version = "0.54"
features = [
    "Data_Xml_Dom", 
    "UI_Notifications",
]
```

```rust:main.rs
use anyhow::Result;
use std::sync::mpsc;
use std::thread;
use windows::{
    core::{h, HSTRING},
    Data::Xml::Dom::XmlDocument,
    Foundation::TypedEventHandler,
    UI::Notifications::{ToastNotification, ToastNotificationManager},
};

fn toast_show(text: String) -> Result<()> {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || -> Result<_> {
        let text = text.encode_utf16().collect::<Vec<_>>();

        let content = XmlDocument::new()?;
        content.LoadXml(h!("<toast><visual><binding template='ToastGeneric'><text></text></binding></visual></toast>"))?;
        content
            .SelectSingleNode(h!("//text[1]"))?
            .SetInnerText(&HSTRING::from_wide(&text)?)?;

        let notification = ToastNotification::CreateToastNotification(&content)?;

        let tx_clone = tx.clone();
        notification.Dismissed(&TypedEventHandler::new(move |_, _| {
            tx_clone.send(()).ok();
            Ok(())
        }))?;

        let tx_clone = tx.clone();
        notification.Activated(&TypedEventHandler::new(move |_, _| {
            tx_clone.send(()).ok();
            Ok(())
        }))?;

        notification.Failed(&TypedEventHandler::new(move |_, _| {
            tx.send(()).ok();
            Ok(())
        }))?;

        let toast =
            ToastNotificationManager::CreateToastNotifierWithId(h!("ToastExample.MyApp"))?;
        toast.Show(&notification)?;
        Ok(())
    });

    rx.recv()?;
    Ok(())
}

fn main() -> Result<()> {
    toast_show("Hello, World!".into())?;
    Ok(())
}
```
</details>

[winrt-toast](https://crates.io/crates/winrt-toast) というクレートがあるので、そちらを使いましょう。