OS のネイティブ通知を送る(許可の確認・通知音)

notification プラグインの sendNotification() と Rust の NotificationExt で OS の通知を出す。許可を確かめる順序と、Windows で無音になりやすい sound の指定も示す。

ダイアログ・通知 対象: Tauri 2.x 更新日: 読了目安: 約9分 dlg-009
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 許可を確かめてから送る
  4. 通知音を付ける
  5. 2. バックエンドから実装する (Rust)
  6. 動作確認
  7. よくあるエラーと対処法
  8. OS ごとの違いと注意点
  9. 関連レシピ

処理の完了や新着を、ウィンドウが裏にあっても気付けるように OS の通知で知らせるには、notification プラグインを使います。JS では sendNotification()、Rust では NotificationExt で送ります。つまずきやすいのは許可の確認と通知音の指定で、そのまま書くと「Windows で通知が出ない」「音が鳴らない」になりがちなので、この 2 点を中心に説明します。

前提条件

npm run tauri add notification

npm パッケージ、クレート、lib.rs の .plugin(tauri_plugin_notification::init())、権限 notification:default がまとめて入ります。通知の権限は core:default に含まれません。絞るなら notification:allow-notify(送信)、notification:allow-is-permission-granted(確認)、notification:allow-request-permission(要求)の 3 つです。Rust からだけ送る場合も確認の権限は残します。macOS と Linux では、ページの読み込みのたびにプラグインが許可の状態を問い合わせるので、無いとコンソールに権限エラーが出ます。

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "notification:default"
  ]
}

知らせ方は、ユーザーの手をどこまで止めるかで選びます。

知らせ方向いている場面
OS の通知(このレシピ)ウィンドウが裏にあっても気付かせる。作業は止めない
ダイアログ閉じるまで操作を止める(dlg-001)
タスクバーの点滅戻ってきたときに気付かせる(win-020)
OS の警告音入力ミスなどを音だけで知らせる(dlg-014)

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

許可を確かめてから送る

isPermissionGranted() で確かめ、false なら requestPermission() を呼んでから送ります。デスクトップでは確認画面は出ず、requestPermission() は常に 'granted' を返します。それでも省けないのは、Windows ではページを読み込むたびに isPermissionGranted() が false から始まるためです。「許可済みのときだけ送る」書き方では、Windows で一度も通知が出ません。iOS / Android では OS の確認画面が出ます。

sendNotification() は結果を返さないので、表示されたかどうかは分かりません。文字列だけを渡すと、本文ではなくタイトルになります。

import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification';
import { getCurrentWindow } from '@tauri-apps/api/window';

let granted = false;

// 確かめてから、足りなければ要求する(許可されたら覚えておく)
async function ensurePermission(): Promise<boolean> {
  if (!granted) {
    granted = (await isPermissionGranted()) || (await requestPermission()) === 'granted';
  }
  return granted;
}

// ウィンドウを見ているときは送らない(画面の表示で足りる)
export async function notify(title: string, body: string): Promise<boolean> {
  if (await getCurrentWindow().isFocused()) return false;
  if (!(await ensurePermission())) return false; // モバイルで拒否された
  sendNotification({ title, body });
  return true;
}

デスクトップで使われるのは title・body・icon・sound だけで、schedule(予約)などは無視されます。時刻を決めるなら setTimeout などで待ってから送ります。

通知音を付ける

sound に指定する値は OS ごとに違い、Windows は指定しないと無音です。

OS指定する値例
WindowsWindows の通知音の名前'Default'、'IM'、'Mail'、'Reminder'、'SMS'
macOSシステムのサウンド名か、アプリに同梱した音'Ping'、'Blow'
Linuxサウンドテーマの名前'message-new-instant'
iOS / Androidアプリに入れた音のリソース名

Windows では、通知音の名前として受け付けられない値(音声ファイルのパスや小文字の 'default' など)も無音になります。古い記事の sound: 'default' が鳴らないのはこのためです。

import { sendNotification } from '@tauri-apps/plugin-notification';

// Windows の標準の通知音を付ける(macOS なら 'Ping' などに替える)
sendNotification({
  title: 'ダウンロード完了',
  body: 'report.pdf を保存しました。',
  sound: 'Default',
});

JS で OS ごとに切り替えるなら os プラグインの platform()(OS の種類を取得する)で判別するか、2 章のように Rust で選びます。アプリ独自の音にしたいときは、sound を付けずに WebView で音声ファイルを鳴らします。エラー時の OS の警告音は通知音とは別物で、システム全体のアラート音を鳴らす で扱います。

import { sendNotification } from '@tauri-apps/plugin-notification';

const chime = new Audio('/sounds/done.mp3'); // public/sounds/done.mp3 に置く

export async function notifyWithChime(title: string, body: string) {
  sendNotification({ title, body }); // 通知の音と重ならないよう sound は付けない
  try {
    await chime.play();
  } catch (e) {
    console.warn('音を再生できません:', e); // ユーザーの操作前は再生を止められることがある
  }
}

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

NotificationExt を use すると、AppHandle などから notification().builder() で組み立てられます。JS の権限とは関係なく送れるので、トレイに常駐する アプリでウィンドウを隠していても使えます。title を省略すると productName になります。show() は OS に渡した時点で Ok を返すので、表示されたかどうかは分かりません。音は cfg! で OS ごとに選びます。Rust の request_permission() もデスクトップでは常に Granted なので、デスクトップ専用なら確認は要りません。

use tauri::Manager;
use tauri_plugin_notification::NotificationExt;

/// OS ごとの通知音(Windows は指定しないと無音)
fn notification_sound() -> Option<&'static str> {
    if cfg!(target_os = "windows") {
        Some("Default")
    } else if cfg!(target_os = "macos") {
        Some("Ping")
    } else if cfg!(target_os = "linux") {
        Some("message-new-instant")
    } else {
        None // iOS / Android はアプリに入れた音のリソース名を指定する
    }
}

/// 通知を送る。OS に渡せた時点で Ok になる
fn notify(app: &tauri::AppHandle, title: &str, body: &str) -> Result<(), String> {
    let mut builder = app.notification().builder().title(title).body(body);
    if let Some(sound) = notification_sound() {
        builder = builder.sound(sound);
    }
    builder.show().map_err(|e| e.to_string())
}

/// 時間のかかる処理をして、終わったら知らせる
#[tauri::command]
async fn export_all(app: tauri::AppHandle) -> Result<(), String> {
    let count = tauri::async_runtime::spawn_blocking(|| {
        std::thread::sleep(std::time::Duration::from_secs(5)); // 実際の処理の代わり
        120
    })
    .await
    .map_err(|e| e.to_string())?;

    // ウィンドウを見ているなら、画面の表示で足りる
    let focused = app
        .get_webview_window("main")
        .and_then(|w| w.is_focused().ok())
        .unwrap_or(false);
    if !focused {
        notify(&app, "エクスポート完了", &format!("{count} 件を書き出しました。"))?;
    }
    Ok(())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_notification::init())
        .invoke_handler(tauri::generate_handler![export_all])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';

await invoke('export_all'); // 約 5 秒後、ウィンドウが裏にあれば通知が出る

動作確認

npm run tauri dev で起動し、次の関数をボタンから呼びます(1 章の notify() はウィンドウが前面だと送らないので、ここでは直接送ります)。

import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification';

export async function testNotification() {
  console.log('before:', await isPermissionGranted());
  console.log('request:', await requestPermission());
  console.log('after:', await isPermissionGranted());
  window.setTimeout(() => {
    sendNotification({ title: 'テスト', body: '3 秒後の通知です', sound: 'Default' });
  }, 3000);
}

Windows では次のように出て、3 秒後に音付きの通知が出ます。macOS と Linux では最初から true です。

before: false
request: granted
after: true

開発中の通知は、送り主がアプリではなく Windows では「Windows PowerShell」、macOS では「ターミナル」になります。インストーラーで入れたアプリならアプリの名前で出ます。Windows で src-tauri\target\release の exe を直接起動した場合も、開発中と同じ表示です。

よくあるエラーと対処法

  • 「notification.notify not allowed. Permissions associated with this command: notification:allow-notify, notification:default」: 権限の追加漏れです。sendNotification() の失敗は try/catch で捕まえられず、捕捉されない Promise のエラーとしてコンソールに出ます。リリースビルドでは「Command plugin:notification|notify not allowed by ACL」です。
  • Windows で通知が一度も出ない: isPermissionGranted() が true のときだけ送っています。requestPermission() まで呼びます。それでも出ないなら、設定の「通知」で送り主がオフか、応答不可(集中モード)でないかを確かめます。
  • 通知は出るが音が鳴らない: Windows で sound が無いか、受け付けられない値です。OS の設定で通知の音を切っている場合も鳴りません。
  • Rust で「state() called before manage()」で始まるパニックになる: .plugin(tauri_plugin_notification::init()) がありません。クレートを手で追加して登録を忘れた場合に起きます(tauri add なら登録まで行われます)。

OS ごとの違いと注意点

  • Windows: 開発中は通知のオン・オフも Windows PowerShell の項目で切り替わります。
  • macOS: 開発中の通知が出ないときは、システム設定の「通知」でターミナルが許可されているかを確かめます。
  • Linux: 表示のされ方や音は、デスクトップ環境の通知の設定に左右されます。
  • iOS / Android: 拒否されたら、端末の設定から許可するよう画面で案内します。
  • 共通: デスクトップでは、通知のクリックをアプリで受け取れません。見逃されることもあるので、大事な内容はアプリの画面にも残します。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する