use std::collections::HashMap;
use zbus::zvariant::Value;
pub(crate) fn entry_path(app_uri: &str) -> String {
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"));
assert!(zbus::zvariant::ObjectPath::try_from(path.as_str()).is_ok());
}
#[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();
}
}