tauri-plugin-deeprespond-im 1.0.0-alpha

深应智能 IM 的 Tauri 插件:本地存储、安全存储、窗口间的锁与消息、系统事件、通知、角标、文件
//! Linux:Unity 启动器接口(com.canonical.Unity.LauncherEntry 的 Update 信号)。Ubuntu 的 Dock、KDE Plasma、
//! Dash to Dock、Plank 等支持;应用的 .desktop 文件名要与 app_uri 一致(system::desktop_entry)。桌面不支持时没有效果。

use std::collections::HashMap;

use zbus::zvariant::Value;

/// 发出信号的对象路径:任意固定的路径即可,按 app_uri 散列(与 libunity 的做法相同)。
pub(crate) fn entry_path(app_uri: &str) -> String {
    // FNV-1a:稳定、不依赖随机种子
    let mut hash: u64 = 0xcbf29ce484222325;
    for b in app_uri.bytes() {
        hash ^= u64::from(b);
        hash = hash.wrapping_mul(0x100000001b3);
    }
    format!("/com/canonical/unity/launcherentry/{hash}")
}

pub(crate) fn app_uri(desktop_entry: &str) -> String {
    format!("application://{desktop_entry}.desktop")
}

pub(super) async fn apply(desktop_entry: &str, count: Option<u64>) {
    let Some(conn) = crate::system::session_bus().await else { return };
    let uri = app_uri(desktop_entry);
    let count = count.unwrap_or(0);
    let mut props: HashMap<&str, Value<'_>> = HashMap::new();
    props.insert("count", Value::from(count.min(i64::MAX as u64) as i64));
    props.insert("count-visible", Value::from(count > 0));
    let path = entry_path(&uri);
    if let Err(err) = conn.emit_signal(None::<&str>, path.as_str(), "com.canonical.Unity.LauncherEntry", "Update", &(uri.as_str(), props)).await {
        log::debug!("启动器角标的信号发送失败:{err}");
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn launcher_entry_naming() {
        assert_eq!(app_uri("com.example.im"), "application://com.example.im.desktop");
        let path = entry_path("application://com.example.im.desktop");
        assert!(path.starts_with("/com/canonical/unity/launcherentry/"));
        assert_eq!(path, entry_path("application://com.example.im.desktop"));
        assert_ne!(path, entry_path("application://other.desktop"));
        // 是合法的 D-Bus 对象路径
        assert!(zbus::zvariant::ObjectPath::try_from(path.as_str()).is_ok());
    }

    /// 在独立的会话总线(dbus-daemon)上收听 Update 信号,核对内容;没有 dbus-daemon 的环境跳过。
    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn update_signal_on_session_bus() {
        use futures_util::StreamExt;
        use std::io::BufRead;
        use zbus::zvariant::OwnedValue;

        let Ok(mut daemon) = std::process::Command::new("dbus-daemon").args(["--session", "--nofork", "--print-address=1"]).stdout(std::process::Stdio::piped()).spawn() else {
            eprintln!("没有 dbus-daemon,跳过");
            return;
        };
        let mut address = String::new();
        std::io::BufReader::new(daemon.stdout.take().unwrap()).read_line(&mut address).unwrap();
        // 本进程中只有这个测试使用共用的会话总线连接
        std::env::set_var("DBUS_SESSION_BUS_ADDRESS", address.trim());
        let listener = zbus::connection::Builder::address(address.trim()).unwrap().build().await.unwrap();
        let rule = zbus::MatchRule::builder().msg_type(zbus::message::Type::Signal).interface("com.canonical.Unity.LauncherEntry").unwrap().member("Update").unwrap().build();
        let mut stream = zbus::MessageStream::for_match_rule(rule, &listener, None).await.unwrap();

        apply("com.example.im", Some(5)).await;
        apply("com.example.im", None).await;
        for expected in [5i64, 0] {
            let msg = tokio::time::timeout(std::time::Duration::from_secs(5), stream.next()).await.unwrap().unwrap().unwrap();
            let (uri, props): (String, HashMap<String, OwnedValue>) = msg.body().deserialize().unwrap();
            assert_eq!(uri, "application://com.example.im.desktop");
            assert_eq!(props.get("count").map(|v| v.downcast_ref::<i64>().unwrap()), Some(expected));
            assert_eq!(props.get("count-visible").map(|v| v.downcast_ref::<bool>().unwrap()), Some(expected > 0));
            assert_eq!(msg.header().path().map(|p| p.to_string()), Some(entry_path(&uri)));
        }
        let _ = daemon.kill();
        let _ = daemon.wait();
    }
}