car-ffi-common 0.15.2

Shared logic for FFI bindings (NAPI, PyO3) — JSON wrappers for verify, multi-agent, scheduler
Documentation
//! Local notification helpers exposed through FFI / JSON-RPC.
//!
//! Platform hosts should prefer their native in-process notification
//! services. This module gives daemon clients a structured API for the
//! same user intent instead of forcing callers to tunnel through the
//! generic AppleScript bridge.

use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize)]
pub struct LocalNotificationArgs {
    pub title: String,
    pub body: String,
    #[serde(default)]
    pub subtitle: Option<String>,
    #[serde(default)]
    pub sound: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct LocalNotificationResult {
    pub delivered: bool,
    pub platform: &'static str,
    pub backend: &'static str,
}

/// Deliver a user-visible local notification when the current platform
/// supports it. On macOS this uses the system `display notification`
/// Apple Event through `osascript`; iOS delivery is owned by the signed
/// Swift host app, not the daemon process.
pub async fn local(args_json: &str) -> Result<String, String> {
    let args: LocalNotificationArgs =
        serde_json::from_str(args_json).map_err(|e| format!("invalid args: {e}"))?;
    validate(&args)?;

    #[cfg(target_os = "macos")]
    {
        deliver_macos(args).await?;
        return serde_json::to_string(&LocalNotificationResult {
            delivered: true,
            platform: "macos",
            backend: "user-notification-apple-event",
        })
        .map_err(|e| format!("serialize: {e}"));
    }

    #[cfg(target_os = "ios")]
    {
        let _ = args;
        return Err(
            "local notifications on iOS must be delivered by the signed host app via UserNotifications"
                .to_string(),
        );
    }

    #[cfg(not(any(target_os = "macos", target_os = "ios")))]
    {
        let _ = args;
        Err("local notifications are not supported on this platform".to_string())
    }
}

fn validate(args: &LocalNotificationArgs) -> Result<(), String> {
    if args.title.trim().is_empty() {
        return Err("title is required".to_string());
    }
    if args.body.trim().is_empty() {
        return Err("body is required".to_string());
    }
    Ok(())
}

#[cfg(target_os = "macos")]
async fn deliver_macos(args: LocalNotificationArgs) -> Result<(), String> {
    let script = r#"
on run argv
  set notificationTitle to item 1 of argv
  set notificationBody to item 2 of argv
  set notificationSubtitle to item 3 of argv
  set notificationSound to item 4 of argv

  if notificationSubtitle is "" and notificationSound is "" then
    display notification notificationBody with title notificationTitle
  else if notificationSubtitle is "" then
    display notification notificationBody with title notificationTitle sound name notificationSound
  else if notificationSound is "" then
    display notification notificationBody with title notificationTitle subtitle notificationSubtitle
  else
    display notification notificationBody with title notificationTitle subtitle notificationSubtitle sound name notificationSound
  end if
end run
"#;

    let subtitle = args.subtitle.unwrap_or_default();
    let sound = args.sound.unwrap_or_default();
    let argv = [
        args.title.as_str(),
        args.body.as_str(),
        subtitle.as_str(),
        sound.as_str(),
    ];

    car_automation::applescript::run_with_args(
        script,
        car_automation::applescript::Language::AppleScript,
        &argv,
        Some(std::time::Duration::from_secs(10)),
    )
    .await
    .map(|_| ())
    .map_err(|e| format!("{e}"))
}