//! Phase 6 of the feature-complete roadmap: Margot soft-touch nudges. //! //! The nudge bus lives in the frontend (`nudgeBus.svelte.ts`) — it //! owns trigger subscription, suppression rules, and the hourly cap. //! This module is the thin Rust side that: //! //! - Guards delivery to the main window only (secondary windows like //! the task float can't fire notifications at the user — they're //! already beside the user's attention). //! - Delegates to `tauri-plugin-notification` for cross-platform OS //! delivery. The plugin handles the macOS Notification Center / //! Linux org.freedesktop.Notifications / Windows toast plumbing. //! //! Permission handling happens on the JS side via the plugin's //! `isPermissionGranted` / `requestPermission` API — before the first //! `deliver_nudge` call, the nudge bus prompts the user. If denied, //! subsequent calls return an error that the bus logs and swallows. //! //! No persistence for v1. Cooldown state is ephemeral; the roadmap's //! nudge-audit table is deferred until a real need emerges. use serde::Deserialize; use tauri_plugin_notification::NotificationExt; use crate::commands::security::ensure_main_window; #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DeliverNudgeInput { pub title: String, pub body: String, } /// Deliver a single OS notification. Does not apply suppression /// logic — the frontend nudge bus is responsible for cadence and /// cooldown, so this command is a blunt "push it now" primitive. /// /// Returns an error if the notification plugin refuses the call /// (typically because permission was denied by the user at the OS /// level). The nudge bus logs and swallows these — nudges are /// fire-and-forget and must never surface errors to the user. #[tauri::command] pub fn deliver_nudge( app: tauri::AppHandle, window: tauri::WebviewWindow, input: DeliverNudgeInput, ) -> Result<(), String> { ensure_main_window(&window)?; let title = input.title.trim(); let body = input.body.trim(); if title.is_empty() && body.is_empty() { // A blank nudge is worse than no nudge — stay quiet. return Ok(()); } app.notification() .builder() .title(if title.is_empty() { "Lumotia" } else { title }) .body(body) .show() .map_err(|e| format!("notification delivery failed: {e}")) }