diff --git a/.scripts/ci/check-license-header.js b/.scripts/ci/check-license-header.js index 2f7b7909..65be6eca 100644 --- a/.scripts/ci/check-license-header.js +++ b/.scripts/ci/check-license-header.js @@ -27,6 +27,7 @@ const ignore = [ "api-iife.js", "init-iife.js", ".build", + "notify_rust" ]; async function checkFile(file) { diff --git a/Cargo.lock b/Cargo.lock index 5be00666..6f967522 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -402,6 +402,17 @@ dependencies = [ "slab", ] +[[package]] +name = "async-fs" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc19683171f287921f2405677dd2ed2549c3b3bda697a563ebc3a121ace2aba1" +dependencies = [ + "async-lock 3.3.0", + "blocking", + "futures-lite 2.2.0", +] + [[package]] name = "async-io" version = "1.13.0" @@ -649,6 +660,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -1344,6 +1361,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.11" @@ -1359,6 +1395,12 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1485,6 +1527,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" +[[package]] +name = "dbus" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +dependencies = [ + "libc", + "libdbus-sys", + "winapi 0.3.9", +] + [[package]] name = "deep-link-example" version = "0.0.0" @@ -1803,8 +1856,11 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" dependencies = [ + "humantime", + "is-terminal", "log", "regex", + "termcolor", ] [[package]] @@ -1888,6 +1944,22 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -2375,6 +2447,16 @@ dependencies = [ "polyval 0.6.1", ] +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.28.1" @@ -2663,6 +2745,16 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "half" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2815,6 +2907,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.28" @@ -2944,8 +3042,12 @@ dependencies = [ "bytemuck", "byteorder", "color_quant", + "exr", + "gif", + "jpeg-decoder", "num-traits", "png", + "qoi", "tiff", ] @@ -3210,6 +3312,9 @@ name = "jpeg-decoder" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +dependencies = [ + "rayon", +] [[package]] name = "js-sys" @@ -3306,6 +3411,12 @@ dependencies = [ "spin 0.5.2", ] +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libappindicator" version = "0.9.0" @@ -3336,6 +3447,15 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libdbus-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +dependencies = [ + "pkg-config", +] + [[package]] name = "libloading" version = "0.7.4" @@ -3825,19 +3945,6 @@ dependencies = [ "walkdir 2.4.0", ] -[[package]] -name = "notify-rust" -version = "4.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "827c5edfa80235ded4ab3fe8e9dc619b4f866ef16fe9b1c6b8a7f8692c0f2226" -dependencies = [ - "log", - "mac-notification-sys", - "serde", - "tauri-winrt-notification", - "zbus 3.10.0", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -4596,6 +4703,15 @@ dependencies = [ "psl-types", ] +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -4776,6 +4892,26 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544" +[[package]] +name = "rayon" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "read-progress-stream" version = "1.0.0" @@ -6578,19 +6714,26 @@ dependencies = [ name = "tauri-plugin-notification" version = "2.0.0-beta.0" dependencies = [ + "chrono", + "dbus", + "env_logger", + "image", + "lazy_static", "log", - "notify-rust", + "mac-notification-sys", "rand 0.8.5", "serde", "serde_json", "serde_repr", "tauri", "tauri-plugin", + "tauri-winrt-notification", "thiserror", "time", "url", "win7-notifications", "windows-version", + "zbus 4.0.1", ] [[package]] @@ -6918,6 +7061,15 @@ dependencies = [ "utf-8", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thin-slice" version = "0.1.1" @@ -8515,9 +8667,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b8e3d6ae3342792a6cc2340e4394334c7402f3d793b390d2c5494a4032b3030" dependencies = [ "async-broadcast 0.7.0", + "async-executor", + "async-fs", + "async-io 2.3.1", + "async-lock 3.3.0", "async-process", "async-recursion", + "async-task", "async-trait", + "blocking", "derivative", "enumflags2", "event-listener 5.1.0", @@ -8680,6 +8838,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + [[package]] name = "zvariant" version = "3.11.0" diff --git a/plugins/notification/Cargo.toml b/plugins/notification/Cargo.toml index 3535a214..5d6e307c 100644 --- a/plugins/notification/Cargo.toml +++ b/plugins/notification/Cargo.toml @@ -27,12 +27,26 @@ time = { version = "0.3", features = [ "serde", "parsing", "formatting" ] } url = { version = "2", features = [ "serde" ] } serde_repr = "0.1" -[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] -notify-rust = "4.5" - [target."cfg(windows)".dependencies] win7-notifications = { version = "0.3.1", optional = true } windows-version = { version = "0.1", optional = true } +[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] +dbus = { version = "0.9", optional = true } +lazy_static = { version = "1", optional = true } +image = { version = "0.24", optional = true } +zbus = { version = "4", optional = true } +log = "0.4" +env_logger ={ version ="0.10", optional = true } + +[target.'cfg(target_os="macos")'.dependencies] +mac-notification-sys = "0.6" +chrono = { version = "0.4", optional = true} + +[target.'cfg(target_os="windows")'.dependencies] +winrt-notification = { package = "tauri-winrt-notification", version = "0.1" } + [features] +default = [ "zbus", "async" ] +async = [] windows7-compat = [ "win7-notifications", "windows-version" ] diff --git a/plugins/notification/src/desktop.rs b/plugins/notification/src/desktop.rs index 8015885e..be3c348e 100644 --- a/plugins/notification/src/desktop.rs +++ b/plugins/notification/src/desktop.rs @@ -160,7 +160,7 @@ mod imp { deprecated = "This function does not work on Windows 7. Use `Self::notify` instead." )] pub fn show(self) -> crate::Result<()> { - let mut notification = notify_rust::Notification::new(); + let mut notification = crate::notify_rust::Notification::new(); if let Some(body) = self.body { notification.body(&body); } @@ -186,7 +186,7 @@ mod imp { } #[cfg(target_os = "macos")] { - let _ = notify_rust::set_application(if cfg!(feature = "custom-protocol") { + let _ = crate::notify_rust::set_application(if cfg!(feature = "custom-protocol") { &self.identifier } else { "com.apple.Terminal" diff --git a/plugins/notification/src/lib.rs b/plugins/notification/src/lib.rs index 4df17b87..61cb6a7e 100644 --- a/plugins/notification/src/lib.rs +++ b/plugins/notification/src/lib.rs @@ -32,6 +32,8 @@ mod commands; mod error; mod models; +mod notify_rust; + pub use error::{Error, Result}; #[cfg(desktop)] diff --git a/plugins/notification/src/notify_rust/error.rs b/plugins/notification/src/notify_rust/error.rs new file mode 100644 index 00000000..cf8f0448 --- /dev/null +++ b/plugins/notification/src/notify_rust/error.rs @@ -0,0 +1,161 @@ +#![allow(missing_docs)] + +#[cfg(all(feature = "images", unix, not(target_os = "macos")))] +use crate::image::ImageError; +use std::{fmt, num}; +/// Convenient wrapper around `std::Result`. +pub type Result = ::std::result::Result; + +#[cfg(target_os = "macos")] +pub use crate::macos::{ApplicationError, MacOsError, NotificationError}; + +/// The Error type. +#[derive(Debug)] +pub struct Error { + kind: ErrorKind, +} + +/// The kind of an error. +#[derive(Debug)] +#[non_exhaustive] +pub enum ErrorKind { + /// only here for backwards compatibility + Msg(String), + + #[cfg(all(feature = "dbus", unix, not(target_os = "macos")))] + Dbus(dbus::Error), + + #[cfg(all(feature = "zbus", unix, not(target_os = "macos")))] + Zbus(zbus::Error), + + #[cfg(target_os = "macos")] + MacNotificationSys(mac_notification_sys::error::Error), + + Parse(num::ParseIntError), + + SpecVersion(String), + + Conversion(String), + + #[cfg(all(feature = "images", unix, not(target_os = "macos")))] + Image(ImageError), + + ImplementationMissing, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.kind { + #[cfg(all(feature = "dbus", unix, not(target_os = "macos")))] + ErrorKind::Dbus(ref e) => write!(f, "{}", e), + + #[cfg(all(feature = "zbus", unix, not(target_os = "macos")))] + ErrorKind::Zbus(ref e) => write!(f, "{}", e), + + #[cfg(target_os = "macos")] + ErrorKind::MacNotificationSys(ref e) => write!(f, "{}", e), + + ErrorKind::Parse(ref e) => write!(f, "Parsing Error: {}", e), + ErrorKind::Conversion(ref e) => write!(f, "Conversion Error: {}", e), + ErrorKind::SpecVersion(ref e) | ErrorKind::Msg(ref e) => write!(f, "{}", e), + #[cfg(all(feature = "images", unix, not(target_os = "macos")))] + ErrorKind::Image(ref e) => write!(f, "{}", e), + ErrorKind::ImplementationMissing => write!( + f, + r#"No Dbus implementation available, please compile with either feature ="z" or feature="d""# + ), + } + } +} + +impl std::error::Error for Error {} + +impl From<&str> for Error { + fn from(e: &str) -> Error { + Error { + kind: ErrorKind::Msg(e.into()), + } + } +} + +#[cfg(all(feature = "dbus", unix, not(target_os = "macos")))] +impl From for Error { + fn from(e: dbus::Error) -> Error { + Error { + kind: ErrorKind::Dbus(e), + } + } +} + +#[cfg(all(feature = "zbus", unix, not(target_os = "macos")))] +impl From for Error { + fn from(e: zbus::Error) -> Error { + Error { + kind: ErrorKind::Zbus(e), + } + } +} + +#[cfg(target_os = "macos")] +impl From for Error { + fn from(e: mac_notification_sys::error::Error) -> Error { + Error { + kind: ErrorKind::MacNotificationSys(e), + } + } +} + +#[cfg(all(feature = "images", unix, not(target_os = "macos")))] +impl From for Error { + fn from(e: ImageError) -> Error { + Error { + kind: ErrorKind::Image(e), + } + } +} + +impl From for Error { + fn from(e: num::ParseIntError) -> Error { + Error { + kind: ErrorKind::Parse(e), + } + } +} + +impl From for Error { + fn from(kind: ErrorKind) -> Error { + Error { kind } + } +} + +/// Just the usual bail macro +#[macro_export] +#[doc(hidden)] +macro_rules! bail { + ($e:expr) => { + return Err($e.into()); + }; + ($fmt:expr, $($arg:tt)+) => { + return Err(format!($fmt, $($arg)+).into()); + }; +} + +/// Exits a function early with an `Error` if the condition is not satisfied. +/// +/// Similar to `assert!`, `ensure!` takes a condition and exits the function +/// if the condition fails. Unlike `assert!`, `ensure!` returns an `Error`, +/// it does not panic. +#[macro_export(local_inner_macros)] +#[doc(hidden)] +macro_rules! ensure { + ($cond:expr, $e:expr) => { + if !($cond) { + bail!($e); + } + }; + ($cond:expr, $fmt:expr, $($arg:tt)*) => { + if !($cond) { + bail!($fmt, $($arg)*); + } + }; +} diff --git a/plugins/notification/src/notify_rust/hints.rs b/plugins/notification/src/notify_rust/hints.rs new file mode 100644 index 00000000..2d43b022 --- /dev/null +++ b/plugins/notification/src/notify_rust/hints.rs @@ -0,0 +1,245 @@ +#![cfg_attr(rustfmt, rustfmt_skip)] + +#[cfg(all(feature = "zbus", unix, not(target_os = "macos")))] +use zbus::zvariant; + +#[cfg(all(unix, not(target_os = "macos")))] +pub(crate) mod message; + +#[cfg(all(feature = "images", any(feature = "dbus", feature = "zbus"), unix, not(target_os = "macos")))] +use super::image::Image; + +#[cfg(all(feature = "images", feature = "zbus", unix, not(target_os = "macos")))] +use super::image::image_spec_str; +use super::Urgency; + +#[cfg(all(feature = "zbus", unix, not(target_os = "macos")))] use super::notification::Notification; +#[cfg(all(feature = "zbus", unix, not(target_os = "macos")))] use std::collections::HashMap; + +mod constants; + +#[cfg(all(unix, not(target_os = "macos")))] +#[derive(Eq, PartialEq, Hash, Clone, Debug)] +pub(crate) enum CustomHintType { + Int, + String, +} + +/// `Hints` allow you to pass extra information to the server. +/// +/// Many of these are standardized by either: +/// +/// * +/// * +/// +/// Which of these are actually implemented depends strongly on the Notification server you talk to. +/// Usually the [`get_capabilities()`](`crate::get_capabilities`) gives some clues, but the standards usually mention much more +/// than is actually available. +/// +/// you pass these to [`Notification::hint`] +#[derive(Eq, PartialEq, Hash, Clone, Debug)] +pub enum Hint { + /// If true, server may interpret action identifiers as named icons and display those. + ActionIcons(bool), + + /// Check out: + /// + /// * + /// * + Category(String), + + /// Name of the DesktopEntry representing the calling application. In case of "firefox.desktop" + /// use "firefox". May be used to retrieve the correct icon. + DesktopEntry(String), + + /// Image as raw data + #[cfg(all(feature = "images", unix, not(target_os = "macos")))] + ImageData(Image), + + /// Display the image at this path. + ImagePath(String), + + /// This does not work on all servers, however timeout=0 will do the job + Resident(bool), + + /// Play the sound at this path. + SoundFile(String), + + /// A themeable named sound from the freedesktop.org [sound naming specification](http://0pointer.de/public/sound-naming-spec.html) to play when the notification pops up. Similar to icon-name, only for sounds. An example would be "message-new-instant". + SoundName(String), + + /// Suppress the notification sound. + SuppressSound(bool), + + /// When set the server will treat the notification as transient and by-pass the server's persistence capability, if it should exist. + Transient(bool), + + /// Lets the notification point to a certain 'x' position on the screen. + /// Requires `Y`. + X(i32), + + /// Lets the notification point to a certain 'y' position on the screen. + /// Requires `X`. + Y(i32), + + /// Pass me a Urgency, either Low, Normal or Critical + Urgency(Urgency), + + /// If you want to pass something entirely different. + Custom(String, String), + + /// A custom numerical (integer) hint + CustomInt(String, i32), + + /// Only used by this NotificationServer implementation + Invalid // TODO find a better solution to this +} + +impl Hint { + /// Get the `bool` representation of this hint. + pub fn as_bool(&self) -> Option { + match *self { + | Hint::ActionIcons(inner) + | Hint::Resident(inner) + | Hint::SuppressSound(inner) + | Hint::Transient(inner) => Some(inner), + _ => None + } + } + + /// Get the `i32` representation of this hint. + pub fn as_i32(&self) -> Option { + match *self { + Hint::X(inner) | Hint::Y(inner) => Some(inner), + _ => None + } + } + + /// Get the `&str` representation of this hint. + pub fn as_str(&self) -> Option<&str> { + match *self { + Hint::DesktopEntry(ref inner) | + Hint::ImagePath(ref inner) | + Hint::SoundFile(ref inner) | + Hint::SoundName(ref inner) => Some(inner), + _ => None + } + } + + /// convenience converting a name and value into a hint + pub fn from_key_val(name: &str, value: &str) -> Result { + match (name,value){ + (constants::ACTION_ICONS,val) => val.parse::().map(Hint::ActionIcons).map_err(|e|e.to_string()), + (constants::CATEGORY, val) => Ok(Hint::Category(val.to_owned())), + (constants::DESKTOP_ENTRY, val) => Ok(Hint::DesktopEntry(val.to_owned())), + (constants::IMAGE_PATH, val) => Ok(Hint::ImagePath(val.to_owned())), + (constants::RESIDENT, val) => val.parse::().map(Hint::Resident).map_err(|e|e.to_string()), + (constants::SOUND_FILE, val) => Ok(Hint::SoundFile(val.to_owned())), + (constants::SOUND_NAME, val) => Ok(Hint::SoundName(val.to_owned())), + (constants::SUPPRESS_SOUND, val) => val.parse::().map(Hint::SuppressSound).map_err(|e|e.to_string()), + (constants::TRANSIENT, val) => val.parse::().map(Hint::Transient).map_err(|e|e.to_string()), + (constants::X, val) => val.parse::().map(Hint::X).map_err(|e|e.to_string()), + (constants::Y, val) => val.parse::().map(Hint::Y).map_err(|e|e.to_string()), + _ => Err(String::from("unknown name")) + } + } +} + +#[cfg(all(unix, not(target_os = "macos")))] +impl Hint {} + +#[cfg(all(feature = "zbus", unix, not(target_os = "macos")))] +#[test] +fn test_hints_to_map() { + + // custom value should only be there once if the names are identical + + let n1 = crate::Notification::new() + .hint(Hint::Custom("foo".into(), "bar1".into())) + .hint(Hint::Custom("foo".into(), "bar2".into())) + .hint(Hint::Custom("f00".into(), "bar3".into())) + .finalize(); + + assert_eq!(hints_to_map(&n1), maplit::hashmap!{ + "foo" => zvariant::Value::Str("bar2".into()), + "f00" => zvariant::Value::Str("bar3".into()) + }); +} + +#[cfg(all(feature = "zbus", unix, not(target_os = "macos")))] +pub(crate) fn hints_to_map(notification: &Notification) -> HashMap::<&str, zvariant::Value<'_>> { + notification + .get_hints() + .map(Into::into) + .collect() +} + +#[cfg(all(feature = "zbus", unix, not(target_os = "macos")))] +impl<'a> From<&'a Hint> for (&'a str, zvariant::Value<'a>) { + fn from(val: &'a Hint) -> Self { + use self::constants::*; + match val { + Hint::ActionIcons(value) => (ACTION_ICONS , zvariant::Value::Bool(*value)), // bool + Hint::Category(value) => (CATEGORY , zvariant::Value::Str(value.as_str().into())), + Hint::DesktopEntry(value) => (DESKTOP_ENTRY , zvariant::Value::Str(value.as_str().into())), + + #[cfg(all(feature = "zbus", feature = "images", unix, not(target_os = "macos")))] + //Hint::ImageData(image) => (image_spec(*crate::SPEC_VERSION).as_str(), ImagePayload::from(*image).into()), + Hint::ImageData(image) => ( + image_spec_str(*crate::SPEC_VERSION), + zvariant::Value::Structure( + image.to_tuple().into() + ) + ), + + + Hint::ImagePath(value) => (IMAGE_PATH , zvariant::Value::Str(value.as_str().into())), + Hint::Resident(value) => (RESIDENT , zvariant::Value::Bool(*value)), // bool + Hint::SoundFile(value) => (SOUND_FILE , zvariant::Value::Str(value.as_str().into())), + Hint::SoundName(value) => (SOUND_NAME , zvariant::Value::Str(value.as_str().into())), + Hint::SuppressSound(value) => (SUPPRESS_SOUND , zvariant::Value::Bool(*value)), + Hint::Transient(value) => (TRANSIENT , zvariant::Value::Bool(*value)), + Hint::X(value) => (X , zvariant::Value::I32(*value)), + Hint::Y(value) => (Y , zvariant::Value::I32(*value)), + Hint::Urgency(value) => (URGENCY , zvariant::Value::U8(*value as u8)), + Hint::Custom(key, val) => (key.as_str() , zvariant::Value::Str(val.as_str().into())), + Hint::CustomInt(key, val) => (key.as_str() , zvariant::Value::I32(*val)), + Hint::Invalid => (INVALID , zvariant::Value::Str(INVALID.into())) + } + } +} + + +#[cfg(all(feature = "dbus", unix, not(target_os = "macos")))] +impl<'a, A: dbus::arg::RefArg> From<(&'a String, &'a A)> for Hint { + fn from(pair: (&String, &A)) -> Self { + + let (key, variant) = pair; + match (key.as_ref(), variant.as_u64(), variant.as_i64(), variant.as_str().map(String::from)) { + + (constants::ACTION_ICONS, Some(1), _, _ ) => Hint::ActionIcons(true), + (constants::ACTION_ICONS, _, _, _ ) => Hint::ActionIcons(false), + (constants::URGENCY, level, _, _ ) => Hint::Urgency(level.into()), + (constants::CATEGORY, _, _, Some(name) ) => Hint::Category(name), + + (constants::DESKTOP_ENTRY, _, _, Some(entry)) => Hint::DesktopEntry(entry), + (constants::IMAGE_PATH, _, _, Some(path) ) => Hint::ImagePath(path), + (constants::RESIDENT, Some(1), _, _ ) => Hint::Resident(true), + (constants::RESIDENT, _, _, _ ) => Hint::Resident(false), + + (constants::SOUND_FILE, _, _, Some(path) ) => Hint::SoundFile(path), + (constants::SOUND_NAME, _, _, Some(name) ) => Hint::SoundName(name), + (constants::SUPPRESS_SOUND, Some(1), _, _ ) => Hint::SuppressSound(true), + (constants::SUPPRESS_SOUND, _, _, _ ) => Hint::SuppressSound(false), + (constants::TRANSIENT, Some(1), _, _ ) => Hint::Transient(true), + (constants::TRANSIENT, _, _, _ ) => Hint::Transient(false), + (constants::X, _, Some(x), _ ) => Hint::X(x as i32), + (constants::Y, _, Some(y), _ ) => Hint::Y(y as i32), + + other => { + eprintln!("Invalid Hint {:#?} ", other); + Hint::Invalid + } + } + } +} diff --git a/plugins/notification/src/notify_rust/hints/constants.rs b/plugins/notification/src/notify_rust/hints/constants.rs new file mode 100644 index 00000000..a7c666c1 --- /dev/null +++ b/plugins/notification/src/notify_rust/hints/constants.rs @@ -0,0 +1,17 @@ +#![allow(dead_code)] + +pub const ACTION_ICONS: &str = "action-icons"; +pub const CATEGORY: &str = "category"; +pub const DESKTOP_ENTRY: &str = "desktop-entry"; +pub const IMAGE_PATH: &str = "image-path"; +pub const RESIDENT: &str = "resident"; +pub const SOUND_FILE: &str = "sound-file"; +pub const SOUND_NAME: &str = "sound-name"; +pub const SUPPRESS_SOUND: &str = "suppress-sound"; +pub const TRANSIENT: &str = "transient"; +pub const X: &str = "x"; +pub const Y: &str = "y"; +pub const URGENCY: &str = "urgency"; + + +pub const INVALID: &str = "invalid"; \ No newline at end of file diff --git a/plugins/notification/src/notify_rust/hints/message.rs b/plugins/notification/src/notify_rust/hints/message.rs new file mode 100644 index 00000000..4e8d0e70 --- /dev/null +++ b/plugins/notification/src/notify_rust/hints/message.rs @@ -0,0 +1,159 @@ +//! `Hints` allow you to pass extra information to the server. +//! +//! Many of these are standardized by either: +//! +//! [galago-project spec](http://www.galago-project.org/specs/notification/0.9/x344.html) or +//! [gnome notification-spec](https://developer.gnome.org/notification-spec/#hints) +//! +//! Which of these are actually implemented depends strongly on the Notification server you talk to. +//! Usually the `get_capabilities()` gives some clues, but the standards usually mention much more +//! than is actually available. +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(dead_code, unused_imports)] + + +use super::{Hint, constants::*}; +use super::Urgency; + +#[cfg(all(feature = "images", unix, not(target_os = "macos")))] +use super::image::*; + +use std::collections::{HashMap, HashSet}; +#[cfg(feature = "dbus")] +use dbus::arg::{messageitem::MessageItem, RefArg}; + +/// All currently implemented `Hints` that can be sent. +/// +/// as found on +#[derive(Eq, PartialEq, Hash, Clone, Debug)] +pub(crate) struct HintMessage(Hint); + +#[cfg(feature = "dbus")] +impl HintMessage { + pub fn wrap_hint(hint: Hint) -> (MessageItem, MessageItem) { + Self::from(hint).into() + } +} + +impl From for HintMessage { + fn from(hint: Hint) -> Self { + HintMessage(hint) + } +} + +impl std::ops::Deref for HintMessage { + type Target = Hint; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(feature = "dbus")] +impl<'a, A: RefArg> From<(&'a String, &'a A)> for HintMessage { + fn from(pair: (&String, &A)) -> Self { + + let (key, variant) = pair; + match (key.as_ref(), variant.as_u64(), variant.as_i64(), variant.as_str().map(String::from)) { + + (ACTION_ICONS, Some(1), _, _ ) => Hint::ActionIcons(true), + (ACTION_ICONS, _, _, _ ) => Hint::ActionIcons(false), + (URGENCY, level, _, _ ) => Hint::Urgency(level.into()), + (CATEGORY, _, _, Some(name) ) => Hint::Category(name), + + (DESKTOP_ENTRY, _, _, Some(entry)) => Hint::DesktopEntry(entry), + (IMAGE_PATH, _, _, Some(path) ) => Hint::ImagePath(path), + (RESIDENT, Some(1), _, _ ) => Hint::Resident(true), + (RESIDENT, _, _, _ ) => Hint::Resident(false), + + (SOUND_FILE, _, _, Some(path) ) => Hint::SoundFile(path), + (SOUND_NAME, _, _, Some(name) ) => Hint::SoundName(name), + (SUPPRESS_SOUND, Some(1), _, _ ) => Hint::SuppressSound(true), + (SUPPRESS_SOUND, _, _, _ ) => Hint::SuppressSound(false), + (TRANSIENT, Some(1), _, _ ) => Hint::Transient(true), + (TRANSIENT, _, _, _ ) => Hint::Transient(false), + (X, _, Some(x), _ ) => Hint::X(x as i32), + (Y, _, Some(y), _ ) => Hint::Y(y as i32), + + other => { + eprintln!("Invalid Hint{:#?} ", other); + Hint::Invalid + } + }.into() + } +} + +#[cfg(feature = "dbus")] +impl From for (MessageItem, MessageItem) { + fn from(hint: HintMessage) -> Self { + + let (key, value): (String, MessageItem) = match hint.0 { + Hint::ActionIcons(value) => (ACTION_ICONS .to_owned(), MessageItem::Bool(value)), // bool + Hint::Category(ref value) => (CATEGORY .to_owned(), MessageItem::Str(value.clone())), + Hint::DesktopEntry(ref value) => (DESKTOP_ENTRY .to_owned(), MessageItem::Str(value.clone())), + #[cfg(all(feature = "images", unix, not(target_os ="macos")))] + Hint::ImageData(image) => (image_spec(*crate::SPEC_VERSION), ImageMessage::from(image).into()), + Hint::ImagePath(ref value) => (IMAGE_PATH .to_owned(), MessageItem::Str(value.clone())), + Hint::Resident(value) => (RESIDENT .to_owned(), MessageItem::Bool(value)), // bool + Hint::SoundFile(ref value) => (SOUND_FILE .to_owned(), MessageItem::Str(value.clone())), + Hint::SoundName(ref value) => (SOUND_NAME .to_owned(), MessageItem::Str(value.clone())), + Hint::SuppressSound(value) => (SUPPRESS_SOUND .to_owned(), MessageItem::Bool(value)), + Hint::Transient(value) => (TRANSIENT .to_owned(), MessageItem::Bool(value)), + Hint::X(value) => (X .to_owned(), MessageItem::Int32(value)), + Hint::Y(value) => (Y .to_owned(), MessageItem::Int32(value)), + Hint::Urgency(value) => (URGENCY .to_owned(), MessageItem::Byte(value as u8)), + Hint::Custom(ref key, ref val) => (key .to_owned(), MessageItem::Str(val.to_owned ())), + Hint::CustomInt(ref key, val) => (key .to_owned(), MessageItem::Int32(val)), + Hint::Invalid => ("invalid" .to_owned(), MessageItem::Str("Invalid".to_owned())) + }; + + (MessageItem::Str(key), MessageItem::Variant(Box::new(value))) + } +} + + +// TODO: deprecated, Prefer the DBus Arg and RefArg APIs +#[cfg(feature = "dbus")] +impl From<(&MessageItem, &MessageItem)> for HintMessage { + fn from ((key, mut value): (&MessageItem, &MessageItem)) -> Self { + use Hint as Hint; + + // If this is a variant, consider the thing inside it + // If it's a nested variant, keep drilling down until we get a real value + while let MessageItem::Variant(inner) = value { + value = inner; + } + + let is_stringy = value.inner::<&str>().is_ok(); + + match key.inner::<&str>() { + Ok(CATEGORY) => value.inner::<&str>().map(String::from).map(Hint::Category), + Ok(ACTION_ICONS) => value.inner().map(Hint::ActionIcons), + Ok(DESKTOP_ENTRY) => value.inner::<&str>().map(String::from).map(Hint::DesktopEntry), + Ok(IMAGE_PATH) => value.inner::<&str>().map(String::from).map(Hint::ImagePath), + Ok(RESIDENT) => value.inner().map(Hint::Resident), + Ok(SOUND_FILE) => value.inner::<&str>().map(String::from).map(Hint::SoundFile), + Ok(SOUND_NAME) => value.inner::<&str>().map(String::from).map(Hint::SoundName), + Ok(SUPPRESS_SOUND) => value.inner().map(Hint::SuppressSound), + Ok(TRANSIENT) => value.inner().map(Hint::Transient), + Ok(X) => value.inner().map(Hint::X), + Ok(Y) => value.inner().map(Hint::Y), + Ok(URGENCY) => value.inner().map(|i| match i { + 0 => Urgency::Low, + 2 => Urgency::Critical, + _ => Urgency::Normal + }).map(Hint::Urgency), + Ok(k) if is_stringy => value.inner::<&str>().map(|v| Hint::Custom(k.to_string(), v.to_string())), + Ok(k) => value.inner().map(|v| Hint::CustomInt(k.to_string(), v)), + _ => Err(()), + }.unwrap_or(Hint::Invalid) + .into() + } +} + + +#[allow(missing_docs)] +#[cfg(feature = "dbus")] +pub(crate) fn hints_from_variants(hints: &HashMap) -> HashSet { + hints.iter().map(Into::into).collect() +} diff --git a/plugins/notification/src/notify_rust/hints/tests.rs b/plugins/notification/src/notify_rust/hints/tests.rs new file mode 100644 index 00000000..23759c4a --- /dev/null +++ b/plugins/notification/src/notify_rust/hints/tests.rs @@ -0,0 +1,85 @@ +#![cfg(all(test, unix, not(target_os = "macos")))] + +use dbus::arg::messageitem::MessageItem as Item; +use ctor::ctor; + +use super::*; +use self::Hint as Hint; +use super::Urgency::*; + + +#[ctor] +fn init_color_backtrace() { + color_backtrace::install(); +} + +#[test] +fn hint_to_item() { + let category = &Hint::Category("test-me".to_owned()); + let (k, v) = category.into(); + + let test_k = Item::Str("category".into()); + let test_v = Item::Variant(Box::new(Item::Str("test-me".into()))); + + assert_eq!(k, test_k); + assert_eq!(v, test_v); +} + +#[test] +fn urgency() { + let low = &Hint::Urgency(Low); + let (k, v) = low.into(); + + let test_k = Item::Str("urgency".into()); + let test_v = Item::Variant(Box::new(Item::Byte(0))); + + assert_eq!(k, test_k); + assert_eq!(v, test_v); +} + +#[test] +fn simple_hint_to_item() { + let old_hint = &Hint::Custom("foo".into(), "bar".into()); + + let (k, v) = old_hint.into(); + let hint: Hint = (&k, &v).into(); + + assert_eq!(old_hint, &hint); +} + +#[test] +#[cfg(all(feature = "images", unix, not(target_os = "macos")))] +fn imagedata_hint_to_item() { + let hint = &Hint::ImageData(Image::from_rgb(1, 1, vec![0, 0, 0]).unwrap()); + let item: MessageItem = hint.into(); + let test_item = Item::DictEntry( + Box::new(Item::Str(image_spec(*::SPEC_VERSION))), + Box::new(Item::Variant(Box::new(Item::Struct(vec![ + Item::Int32(1), + Item::Int32(1), + Item::Int32(3), + Item::Bool(false), + Item::Int32(8), + Item::Int32(3), + Item::Array(dbus::MessageItemArray::new(vec![ + Item::Byte(0), + Item::Byte(0), + Item::Byte(0), + ],"ay".into()).unwrap()) + ])))) + ); + assert_eq!(item, test_item); +} + +#[test] +#[cfg(all(feature = "images", unix, not(target_os = "macos")))] +fn imagedata_hint_to_item_with_spec() { + let key = image_spec(Version::new(1, 0)); + assert_eq!(key, String::from("icon_data")); + + let key = image_spec(Version::new(1, 1)); + assert_eq!(key, String::from("image_data")); + + let key = image_spec(Version::new(1, 2)); + assert_eq!(key, String::from("image-data")); +} diff --git a/plugins/notification/src/notify_rust/image.rs b/plugins/notification/src/notify_rust/image.rs new file mode 100644 index 00000000..a2ee3c5d --- /dev/null +++ b/plugins/notification/src/notify_rust/image.rs @@ -0,0 +1,229 @@ +#[cfg(feature = "dbus")] +use dbus::arg::messageitem::{MessageItem, MessageItemArray}; +pub use image::DynamicImage; + +use std::cmp::Ordering; +use std::convert::TryFrom; +use std::error::Error; +use std::fmt; +use std::path::Path; + +use crate::miniver::Version; + +mod constants { + pub const IMAGE_DATA: &str = "image-data"; + pub const IMAGE_DATA_1_1: &str = "image_data"; + pub const IMAGE_DATA_1_0: &str = "icon_data"; +} + +/// Image representation for images. Send via `Notification::image_data()` +#[derive(PartialEq, Eq, Debug, Clone, Hash)] +pub struct Image { + width: i32, + height: i32, + rowstride: i32, + alpha: bool, + bits_per_sample: i32, + channels: i32, + data: Vec, +} + +impl Image { + fn from_raw_data( + width: i32, + height: i32, + data: Vec, + channels: i32, + bits_per_sample: i32, + alpha: bool, + ) -> Result { + const MAX_SIZE: i32 = 0x0fff_ffff; + if width > MAX_SIZE || height > MAX_SIZE { + return Err(ImageError::TooBig); + } + + if data.len() != (width * height * channels) as usize { + Err(ImageError::WrongDataSize) + } else { + Ok(Self { + width, + height, + bits_per_sample, + channels, + data, + rowstride: width * channels, + alpha, + }) + } + } + + /// Creates an image from a raw vector of bytes + pub fn from_rgb(width: i32, height: i32, data: Vec) -> Result { + let channels = 3i32; + let bits_per_sample = 8; + Self::from_raw_data(width, height, data, channels, bits_per_sample, false) + } + + /// Creates an image from a raw vector of bytes with alpha + pub fn from_rgba(width: i32, height: i32, data: Vec) -> Result { + let channels = 4i32; + let bits_per_sample = 8; + Self::from_raw_data(width, height, data, channels, bits_per_sample, true) + } + + /// Attempts to open the given path as image + pub fn open + Sized>(path: T) -> Result { + let dyn_img = image::open(&path).map_err(ImageError::CantOpen)?; + Image::try_from(dyn_img) + } + + #[cfg(all(feature = "images", feature = "zbus"))] + pub(crate) fn to_tuple(&self) -> (i32, i32, i32, bool, i32, i32, Vec) { + ( + self.width, + self.height, + self.rowstride, + self.alpha, + self.bits_per_sample, + self.channels, + self.data.clone(), + ) + } +} + +impl TryFrom for Image { + type Error = ImageError; + + fn try_from(dyn_img: DynamicImage) -> Result { + match dyn_img { + DynamicImage::ImageRgb8(img) => Self::try_from(img), + DynamicImage::ImageRgba8(img) => Self::try_from(img), + _ => Err(ImageError::CantConvert), + } + } +} + +impl TryFrom for Image { + type Error = ImageError; + + fn try_from(img: image::RgbImage) -> Result { + let (width, height) = img.dimensions(); + let image_data = img.into_raw(); + Image::from_rgb(width as i32, height as i32, image_data) + } +} + +impl TryFrom for Image { + type Error = ImageError; + + fn try_from(img: image::RgbaImage) -> Result { + let (width, height) = img.dimensions(); + let image_data = img.into_raw(); + Image::from_rgba(width as i32, height as i32, image_data) + } +} + +/// Errors that can occur when creating an Image +#[derive(Debug)] +pub enum ImageError { + /// The given image is too big. DBus only has 32 bits for width / height + TooBig, + /// The given bytes don't match the width, height and channel count + WrongDataSize, + /// Can't open given path + CantOpen(image::ImageError), + /// Can't convert from given input + CantConvert, +} + +impl Error for ImageError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + use ImageError::*; + match self { + TooBig | WrongDataSize | CantConvert => None, + CantOpen(e) => Some(e), + } + } +} + +impl fmt::Display for ImageError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use ImageError::*; + match self { + TooBig => writeln!( + f, + "The given image is too big. DBus only has 32 bits for width / height" + ), + WrongDataSize => writeln!( + f, + "The given bytes don't match the width, height and channel count" + ), + CantOpen(e) => writeln!(f, "Can't open given path {}", e), + CantConvert => writeln!(f, "Can't convert from given input"), + } + } +} + +/// matching image data key for each spec version +#[cfg(feature = "dbus")] +pub(crate) fn image_spec(version: Version) -> String { + match version.cmp(&Version::new(1, 1)) { + Ordering::Less => constants::IMAGE_DATA_1_0.to_owned(), + Ordering::Equal => constants::IMAGE_DATA_1_1.to_owned(), + Ordering::Greater => constants::IMAGE_DATA.to_owned(), + } +} + +/// matching image data key for each spec version +#[cfg(feature = "zbus")] +pub(crate) fn image_spec_str(version: Version) -> &'static str { + match version.cmp(&Version::new(1, 1)) { + Ordering::Less => constants::IMAGE_DATA_1_0, + Ordering::Equal => constants::IMAGE_DATA_1_1, + Ordering::Greater => constants::IMAGE_DATA, + } +} + +#[cfg(feature = "dbus")] +pub struct ImageMessage(Image); + +#[cfg(feature = "dbus")] +impl From for ImageMessage { + fn from(hint: Image) -> Self { + ImageMessage(hint) + } +} + +impl From for ImageError { + fn from(image_error: image::ImageError) -> Self { + ImageError::CantOpen(image_error) + } +} + +#[cfg(feature = "dbus")] +impl std::ops::Deref for ImageMessage { + type Target = Image; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(feature = "dbus")] +impl From for MessageItem { + fn from(img_msg: ImageMessage) -> Self { + let img = img_msg.0; + + let bytes = img.data.into_iter().map(MessageItem::Byte).collect(); + + MessageItem::Struct(vec![ + MessageItem::Int32(img.width), + MessageItem::Int32(img.height), + MessageItem::Int32(img.rowstride), + MessageItem::Bool(img.alpha), + MessageItem::Int32(img.bits_per_sample), + MessageItem::Int32(img.channels), + MessageItem::Array(MessageItemArray::new(bytes, "ay".into()).unwrap()), + ]) + } +} diff --git a/plugins/notification/src/notify_rust/macos.rs b/plugins/notification/src/notify_rust/macos.rs new file mode 100644 index 00000000..c886c0a7 --- /dev/null +++ b/plugins/notification/src/notify_rust/macos.rs @@ -0,0 +1,61 @@ +use super::{error::*, notification::Notification}; + +pub use mac_notification_sys::error::{ApplicationError, Error as MacOsError, NotificationError}; + +use std::ops::{Deref, DerefMut}; + +/// A handle to a shown notification. +/// +/// This keeps a connection alive to ensure actions work on certain desktops. +#[derive(Debug)] +pub struct NotificationHandle { + notification: Notification, +} + +impl NotificationHandle { + #[allow(missing_docs)] + pub fn new(notification: Notification) -> NotificationHandle { + NotificationHandle { notification } + } +} + +impl Deref for NotificationHandle { + type Target = Notification; + + fn deref(&self) -> &Notification { + &self.notification + } +} + +/// Allow to easily modify notification properties +impl DerefMut for NotificationHandle { + fn deref_mut(&mut self) -> &mut Notification { + &mut self.notification + } +} + +pub(crate) fn show_notification(notification: &Notification) -> Result { + mac_notification_sys::Notification::default() + .title(notification.summary.as_str()) + .message(¬ification.body) + .maybe_subtitle(notification.subtitle.as_deref()) + .maybe_sound(notification.sound_name.as_deref()) + .send()?; + + Ok(NotificationHandle::new(notification.clone())) +} + +pub(crate) fn schedule_notification( + notification: &Notification, + delivery_date: f64, +) -> Result { + mac_notification_sys::Notification::default() + .title(notification.summary.as_str()) + .message(¬ification.body) + .maybe_subtitle(notification.subtitle.as_deref()) + .maybe_sound(notification.sound_name.as_deref()) + .delivery_date(delivery_date) + .send()?; + + Ok(NotificationHandle::new(notification.clone())) +} diff --git a/plugins/notification/src/notify_rust/miniver.rs b/plugins/notification/src/notify_rust/miniver.rs new file mode 100644 index 00000000..5c8deb83 --- /dev/null +++ b/plugins/notification/src/notify_rust/miniver.rs @@ -0,0 +1,75 @@ +use super::error::*; +use std::str::FromStr; + +#[derive(Copy, Clone, Eq, Debug)] +pub struct Version { + pub major: u64, + pub minor: u64, +} + +impl Version { + #[allow(dead_code)] + pub fn new(major: u64, minor: u64) -> Self { + Self { major, minor } + } +} + +impl FromStr for Version { + type Err = Error; + fn from_str(s: &str) -> Result { + let vv = s.split('.').collect::>(); + match (vv.first(), vv.get(1)) { + (Some(maj), Some(min)) => Ok(Version { + major: maj.parse()?, + minor: min.parse()?, + }), + _ => Err(ErrorKind::SpecVersion(s.into()).into()), + } + } +} + +use std::cmp; + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Version) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for Version { + fn eq(&self, other: &Version) -> bool { + self.major == other.major && self.minor == other.minor + } +} + +impl Ord for Version { + fn cmp(&self, other: &Version) -> cmp::Ordering { + match self.major.cmp(&other.major) { + cmp::Ordering::Equal => {} + r => return r, + } + match self.minor.cmp(&other.minor) { + cmp::Ordering::Equal => {} + r => return r, + } + cmp::Ordering::Equal + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn version_parsing() { + assert_eq!("1.3".parse::().unwrap(), Version::new(1, 3)); + } + + #[test] + fn version_comparison() { + assert!(Version::new(1, 3) >= Version::new(1, 2)); + assert!(Version::new(1, 2) >= Version::new(1, 2)); + assert!(Version::new(1, 2) == Version::new(1, 2)); + assert!(Version::new(1, 1) <= Version::new(1, 2)); + } +} diff --git a/plugins/notification/src/notify_rust/mod.rs b/plugins/notification/src/notify_rust/mod.rs new file mode 100644 index 00000000..2ca73dc4 --- /dev/null +++ b/plugins/notification/src/notify_rust/mod.rs @@ -0,0 +1,236 @@ +//! Desktop Notifications for Rust. +//! +//! Desktop notifications are popup messages generated to notify the user of certain events. +//! +//! ## Platform Support +//! +//! This library was originally conceived with the [XDG](https://en.wikipedia.org/wiki/XDG) notification specification in mind. +//! Since version 3.3 this crate also builds on macOS, however the semantics of the [XDG](https://en.wikipedia.org/wiki/XDG) specification and macOS `NSNotifications` +//! are quite different. +//! Therefore only a very small subset of functions is supported on macOS. +//! Certain methods don't have any effect there, others will explicitly fail to compile, +//! in these cases you will have to add platform specific toggles to your code. +//! For more see [platform differences](#platform-differences) +//! +//! # Examples +//! +//! ## Example 1: Simple Notification +//! +//! ```no_run +//! # use notify_rust::*; +//! Notification::new() +//! .summary("Firefox News") +//! .body("This will almost look like a real firefox notification.") +//! .icon("firefox") +//! .timeout(Timeout::Milliseconds(6000)) //milliseconds +//! .show().unwrap(); +//! ``` +//! +//! ## Example 2: Persistent Notification +//! +//! ```no_run +//! # use notify_rust::*; +//! Notification::new() +//! .summary("Category:email") +//! .body("This has nothing to do with emails.\nIt should not go away until you acknowledge it.") +//! .icon("thunderbird") +//! .appname("thunderbird") +//! .hint(Hint::Category("email".to_owned())) +//! .hint(Hint::Resident(true)) // this is not supported by all implementations +//! .timeout(Timeout::Never) // this however is +//! .show().unwrap(); +//! ``` +//! +//! Careful! There are no checks whether you use hints twice. +//! It is possible to set `urgency=Low` AND `urgency=Critical`, in which case the behavior of the server is undefined. +//! +//! ## Example 3: Ask the user to do something +//! +//! ```no_run +//! # use notify_rust::*; +//! # #[cfg(all(unix, not(target_os = "macos")))] +//! Notification::new().summary("click me") +//! .action("default", "default") +//! .action("clicked", "click here") +//! .hint(Hint::Resident(true)) +//! .show() +//! .unwrap() +//! .wait_for_action(|action| match action { +//! "default" => println!("you clicked \"default\""), +//! "clicked" => println!("that was correct"), +//! // here "__closed" is a hard coded keyword +//! "__closed" => println!("the notification was closed"), +//! _ => () +//! }); +//! ``` +//! +//! ## Minimal Example +//! +//! You can omit almost everything +//! +//! ```no_run +//! # use notify_rust::Notification; +//! Notification::new().show(); +//! ``` +//! +//! more [examples](https://github.com/hoodie/notify-rust/tree/main/examples) in the repository. +//! +//! # Platform Differences +//!
+//! ✔︎ = works
+//! ❌ = will not compile +//! +//! ## `Notification` +//! | method | XDG | macOS | windows | +//! |---------------------|-------|-------|---------| +//! | `fn appname(...)` | ✔︎ | | | +//! | `fn summary(...)` | ✔︎ | ✔︎ | ✔︎ | +//! | `fn subtitle(...)` | | ✔︎ | ✔︎ | +//! | `fn body(...)` | ✔︎ | ✔︎ | ✔︎ | +//! | `fn icon(...)` | ✔︎ | | | +//! | `fn auto_icon(...)`| ✔︎ | | | +//! | `fn hint(...)` | ✔︎ | ❌ | ❌ | +//! | `fn timeout(...)` | ✔︎ | | ✔︎ | +//! | `fn urgency(...)` | ✔︎ | ❌ | ❌ | +//! | `fn action(...)` | ✔︎ | | | +//! | `fn id(...)` | ✔︎ | | | +//! | `fn finalize(...)` | ✔︎ | ✔︎ | ✔︎ | +//! | `fn show(...)` | ✔︎ | ✔︎ | ✔︎ | +//! +//! ## `NotificationHandle` +//! +//! | method | XDG | macOS | windows | +//! |--------------------------|-----|-------|---------| +//! | `fn wait_for_action(...)`| ✔︎ | ❌ | ❌ | +//! | `fn close(...)` | ✔︎ | ❌ | ❌ | +//! | `fn on_close(...)` | ✔︎ | ❌ | ❌ | +//! | `fn update(...)` | ✔︎ | ❌ | ❌ | +//! | `fn id(...)` | ✔︎ | ❌ | ❌ | +//! +//! ## Functions +//! +//! | | XDG | macOS | windows | +//! |--------------------------------------------|-----|-------|---------| +//! | `fn get_capabilities(...)` | ✔︎ | ❌ | ❌ | +//! | `fn get_server_information(...)` | ✔︎ | ❌ | ❌ | +//! | `fn set_application(...)` | ❌ | ✔︎ | ❌ | +//! | `fn get_bundle_identifier_or_default(...)` | ❌ | ✔︎ | ❌ | +//! +//! +//! ### Toggles +//! +//! Please use `target_os` toggles if you plan on using methods labeled with ❌. +//! +//! ```ignore +//! #[cfg(target_os = "macos")] +//! // or +//! // #### #[cfg(all(unix, not(target_os = "macos")))] +//! ``` +//!
+//! + +#![deny( + missing_copy_implementations, + trivial_casts, + trivial_numeric_casts, + unsafe_code, + unused_import_braces, + unused_qualifications +)] +#![warn( + missing_docs, + clippy::doc_markdown, + clippy::semicolon_if_nothing_returned, + clippy::single_match_else, + clippy::inconsistent_struct_constructor, + clippy::map_unwrap_or, + clippy::match_same_arms +)] + +#[cfg(all(feature = "dbus", unix, not(target_os = "macos")))] +extern crate dbus; + +#[cfg(target_os = "macos")] +extern crate mac_notification_sys; + +#[cfg(target_os = "windows")] +extern crate winrt_notification; + +#[macro_use] +#[cfg(all(feature = "images", unix, not(target_os = "macos")))] +extern crate lazy_static; + +pub mod error; +mod hints; +mod miniver; +mod notification; +mod timeout; +pub(crate) mod urgency; + +#[cfg(target_os = "macos")] +mod macos; + +#[cfg(target_os = "windows")] +mod windows; + +#[cfg(all(unix, not(target_os = "macos")))] +mod xdg; + +#[cfg(all(feature = "images", unix, not(target_os = "macos")))] +mod image; + +#[cfg(all(feature = "server", feature = "dbus", unix, not(target_os = "macos")))] +pub mod server; + +#[cfg(target_os = "macos")] +pub use mac_notification_sys::{get_bundle_identifier_or_default, set_application}; + +#[cfg(target_os = "macos")] +pub use macos::NotificationHandle; + +#[cfg(all( + any(feature = "dbus", feature = "zbus"), + unix, + not(target_os = "macos") +))] +pub use xdg::{ + dbus_stack, get_capabilities, get_server_information, handle_action, ActionResponse, + CloseHandler, CloseReason, DbusStack, NotificationHandle, +}; + +#[cfg(all(feature = "server", unix, not(target_os = "macos")))] +pub use xdg::stop_server; + +pub use hints::Hint; + +#[cfg(all(feature = "images", unix, not(target_os = "macos")))] +pub use image::{Image, ImageError}; + +#[cfg_attr( + target_os = "macos", + deprecated(note = "Urgency is not supported on macOS") +)] +pub use urgency::Urgency; + +pub use {notification::Notification, timeout::Timeout}; + +#[cfg(all(feature = "images", unix, not(target_os = "macos")))] +lazy_static! { + /// Read once at runtime. Needed for Images + pub static ref SPEC_VERSION: miniver::Version = + get_server_information() + .and_then(|info| info.spec_version.parse::()) + .unwrap_or_else(|_| miniver::Version::new(1,1)); +} +/// Return value of `get_server_information()`. +#[derive(Debug)] +pub struct ServerInformation { + /// The product name of the server. + pub name: String, + /// The vendor name. + pub vendor: String, + /// The server's version string. + pub version: String, + /// The specification version the server is compliant with. + pub spec_version: String, +} diff --git a/plugins/notification/src/notify_rust/notification.rs b/plugins/notification/src/notify_rust/notification.rs new file mode 100644 index 00000000..c032eedf --- /dev/null +++ b/plugins/notification/src/notify_rust/notification.rs @@ -0,0 +1,520 @@ +#[cfg(all(unix, not(target_os = "macos")))] +use super::{ + hints::{CustomHintType, Hint}, + urgency::Urgency, + xdg, +}; + +#[cfg(all(unix, not(target_os = "macos"), feature = "images"))] +use super::image::Image; + +#[cfg(all(unix, target_os = "macos"))] +use super::macos; +#[cfg(target_os = "windows")] +use super::windows; + +use super::{error::*, timeout::Timeout}; + +#[cfg(all(unix, not(target_os = "macos")))] +use std::collections::{HashMap, HashSet}; + +// Returns the name of the current executable, used as a default for `Notification.appname`. +fn exe_name() -> String { + std::env::current_exe() + .unwrap() + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_owned() +} + +/// Desktop notification. +/// +/// A desktop notification is configured via builder pattern, before it is launched with `show()`. +/// +/// # Example +/// ``` no_run +/// # use notify_rust::*; +/// # fn _doc() -> Result<(), Box> { +/// Notification::new() +/// .summary("☝️ A notification") +/// .show()?; +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct Notification { + /// Filled by default with executable name. + pub appname: String, + + /// Single line to summarize the content. + pub summary: String, + + /// Subtitle for macOS + pub subtitle: Option, + + /// Multiple lines possible, may support simple markup, + /// check out `get_capabilities()` -> `body-markup` and `body-hyperlinks`. + pub body: String, + + /// Use a file:// URI or a name in an icon theme, must be compliant freedesktop.org. + pub icon: String, + + /// Check out `Hint` + /// + /// # warning + /// this does not hold all hints, [`Hint::Custom`] and [`Hint::CustomInt`] are held elsewhere, + // /// please access hints via [`Notification::get_hints`]. + #[cfg(all(unix, not(target_os = "macos")))] + pub hints: HashSet, + + #[cfg(all(unix, not(target_os = "macos")))] + pub(crate) hints_unique: HashMap<(String, CustomHintType), Hint>, + + /// See `Notification::actions()` and `Notification::action()` + pub actions: Vec, + + #[cfg(target_os = "macos")] + pub(crate) sound_name: Option, + + #[cfg(target_os = "windows")] + pub(crate) sound_name: Option, + + #[cfg(target_os = "windows")] + pub(crate) path_to_image: Option, + + #[cfg(target_os = "windows")] + pub(crate) app_id: Option, + + #[cfg(all(unix, not(target_os = "macos")))] + pub(crate) bus: xdg::NotificationBus, + + /// Lifetime of the Notification in ms. Often not respected by server, sorry. + pub timeout: Timeout, // both gnome and galago want allow for -1 + + /// Only to be used on the receive end. Use Notification hand for updating. + pub(crate) id: Option, +} + +impl Notification { + /// Constructs a new Notification. + /// + /// Most fields are empty by default, only `appname` is initialized with the name of the current + /// executable. + /// The appname is used by some desktop environments to group notifications. + pub fn new() -> Notification { + Notification::default() + } + + /// This is for testing purposes only and will not work with actual implementations. + #[cfg(all(unix, not(target_os = "macos")))] + #[doc(hidden)] + #[deprecated(note = "this is a test only feature")] + pub fn at_bus(sub_bus: &str) -> Notification { + let bus = xdg::NotificationBus::custom(sub_bus) + .ok_or("invalid subpath") + .unwrap(); + Notification { + bus, + ..Notification::default() + } + } + + /// Overwrite the appname field used for Notification. + /// + /// # Platform Support + /// Please note that this method has no effect on macOS. Here you can only set the application via [`set_application()`](fn.set_application.html) + pub fn appname(&mut self, appname: &str) -> &mut Notification { + self.appname = appname.to_owned(); + self + } + + /// Set the `summary`. + /// + /// Often acts as title of the notification. For more elaborate content use the `body` field. + pub fn summary(&mut self, summary: &str) -> &mut Notification { + self.summary = summary.to_owned(); + self + } + + /// Set the `subtitle`. + /// + /// This is only useful on macOS, it's not part of the XDG specification and will therefore be eaten by gremlins under your CPU 😈🤘. + pub fn subtitle(&mut self, subtitle: &str) -> &mut Notification { + self.subtitle = Some(subtitle.to_owned()); + self + } + + /// Manual wrapper for `Hint::ImageData` + #[cfg(all(feature = "images", unix, not(target_os = "macos")))] + pub fn image_data(&mut self, image: Image) -> &mut Notification { + self.hint(Hint::ImageData(image)); + self + } + + /// Wrapper for `Hint::ImagePath` + #[cfg(all(unix, not(target_os = "macos")))] + pub fn image_path(&mut self, path: &str) -> &mut Notification { + self.hint(Hint::ImagePath(path.to_string())); + self + } + + /// Wrapper for `NotificationHint::ImagePath` + #[cfg(target_os = "windows")] + pub fn image_path(&mut self, path: &str) -> &mut Notification { + self.path_to_image = Some(path.to_string()); + self + } + + /// app's System.AppUserModel.ID + #[cfg(target_os = "windows")] + pub fn app_id(&mut self, app_id: &str) -> &mut Notification { + self.app_id = Some(app_id.to_string()); + self + } + + /// Wrapper for `Hint::ImageData` + #[cfg(all(feature = "images", unix, not(target_os = "macos")))] + pub fn image + Sized>( + &mut self, + path: T, + ) -> Result<&mut Notification> { + let img = Image::open(&path)?; + self.hint(Hint::ImageData(img)); + Ok(self) + } + + /// Wrapper for `Hint::SoundName` + #[cfg(all(unix, not(target_os = "macos")))] + pub fn sound_name(&mut self, name: &str) -> &mut Notification { + self.hint(Hint::SoundName(name.to_owned())); + self + } + + /// Set the `sound_name` for the `NSUserNotification` + #[cfg(any(target_os = "macos", target_os = "windows"))] + pub fn sound_name(&mut self, name: &str) -> &mut Notification { + self.sound_name = Some(name.to_owned()); + self + } + + /// Set the content of the `body` field. + /// + /// Multiline textual content of the notification. + /// Each line should be treated as a paragraph. + /// Simple html markup should be supported, depending on the server implementation. + pub fn body(&mut self, body: &str) -> &mut Notification { + self.body = body.to_owned(); + self + } + + /// Set the `icon` field. + /// + /// You can use common icon names here, usually those in `/usr/share/icons` + /// can all be used. + /// You can also use an absolute path to file. + /// + /// # Platform support + /// macOS does not have support manually setting the icon. However you can pretend to be another app using [`set_application()`](fn.set_application.html) + pub fn icon(&mut self, icon: &str) -> &mut Notification { + self.icon = icon.to_owned(); + self + } + + /// Set the `icon` field automatically. + /// + /// This looks at your binary's name and uses it to set the icon. + /// + /// # Platform support + /// macOS does not support manually setting the icon. However you can pretend to be another app using [`set_application()`](fn.set_application.html) + pub fn auto_icon(&mut self) -> &mut Notification { + self.icon = exe_name(); + self + } + + /// Adds a hint. + /// + /// This method will add a hint to the internal hint [`HashSet`]. + /// Hints must be of type [`Hint`]. + /// + /// Many of these are again wrapped by more convenient functions such as: + /// + /// * `sound_name(...)` + /// * `urgency(...)` + /// * [`image(...)`](#method.image) or + /// * [`image_data(...)`](#method.image_data) + /// * [`image_path(...)`](#method.image_path) + /// + /// ```no_run + /// # use notify_rust::Notification; + /// # use notify_rust::Hint; + /// Notification::new().summary("Category:email") + /// .body("This should not go away until you acknowledge it.") + /// .icon("thunderbird") + /// .appname("thunderbird") + /// .hint(Hint::Category("email".to_owned())) + /// .hint(Hint::Resident(true)) + /// .show(); + /// ``` + /// + /// # Platform support + /// Most of these hints don't even have an effect on the big XDG Desktops, they are completely tossed on macOS. + #[cfg(all(unix, not(target_os = "macos")))] + pub fn hint(&mut self, hint: Hint) -> &mut Notification { + match hint { + Hint::CustomInt(k, v) => { + self.hints_unique + .insert((k.clone(), CustomHintType::Int), Hint::CustomInt(k, v)); + } + Hint::Custom(k, v) => { + self.hints_unique + .insert((k.clone(), CustomHintType::String), Hint::Custom(k, v)); + } + _ => { + self.hints.insert(hint); + } + } + self + } + + #[cfg(all(unix, not(target_os = "macos")))] + pub(crate) fn get_hints(&self) -> impl Iterator { + self.hints.iter().chain(self.hints_unique.values()) + } + + /// Set the `timeout`. + /// + /// Accepts multiple types that implement `Into`. + /// + /// ## `i31` + /// + /// This sets the time (in milliseconds) from the time the notification is displayed until it is + /// closed again by the Notification Server. + /// According to [specification](https://developer.gnome.org/notification-spec/) + /// -1 will leave the timeout to be set by the server and + /// 0 will cause the notification never to expire. + + /// ## [Duration](`std::time::Duration`) + /// + /// When passing a [`Duration`](`std::time::Duration`) we will try convert it into milliseconds. + /// + /// + /// ``` + /// # use std::time::Duration; + /// # use notify_rust::Timeout; + /// assert_eq!(Timeout::from(Duration::from_millis(2000)), Timeout::Milliseconds(2000)); + /// ``` + /// ### Caveats! + /// + /// 1. If the duration is zero milliseconds then the original behavior will apply and the notification will **Never** timeout. + /// 2. Should the number of milliseconds not fit within an [`i32`] then we will fall back to the default timeout. + /// ``` + /// # use std::time::Duration; + /// # use notify_rust::Timeout; + /// assert_eq!(Timeout::from(Duration::from_millis(0)), Timeout::Never); + /// assert_eq!(Timeout::from(Duration::from_millis(u64::MAX)), Timeout::Default); + /// ``` + /// + /// # Platform support + /// This only works on XDG Desktops, macOS does not support manually setting the timeout. + pub fn timeout>(&mut self, timeout: T) -> &mut Notification { + self.timeout = timeout.into(); + self + } + + /// Set the `urgency`. + /// + /// Pick between Medium, Low and High. + /// + /// # Platform support + /// Most Desktops on linux and bsd are far too relaxed to pay any attention to this. + /// In macOS this does not exist + #[cfg(all(unix, not(target_os = "macos")))] + pub fn urgency(&mut self, urgency: Urgency) -> &mut Notification { + self.hint(Hint::Urgency(urgency)); // TODO impl as T where T: Into + self + } + + /// Set `actions`. + /// + /// To quote + /// + /// > Actions are sent over as a list of pairs. + /// > Each even element in the list (starting at index 0) represents the identifier for the action. + /// > Each odd element in the list is the localized string that will be displayed to the user.y + /// + /// There is nothing fancy going on here yet. + /// **Careful! This replaces the internal list of actions!** + /// + /// (xdg only) + #[deprecated(note = "please use .action() only")] + pub fn actions(&mut self, actions: Vec) -> &mut Notification { + self.actions = actions; + self + } + + /// Add an action. + /// + /// This adds a single action to the internal list of actions. + /// + /// (xdg only) + pub fn action(&mut self, identifier: &str, label: &str) -> &mut Notification { + self.actions.push(identifier.to_owned()); + self.actions.push(label.to_owned()); + self + } + + /// Set an Id ahead of time + /// + /// Setting the id ahead of time allows overriding a known other notification. + /// Though if you want to update a notification, it is easier to use the `update()` method of + /// the `NotificationHandle` object that `show()` returns. + /// + /// (xdg only) + pub fn id(&mut self, id: u32) -> &mut Notification { + self.id = Some(id); + self + } + + /// Finalizes a Notification. + /// + /// Part of the builder pattern, returns a complete copy of the built notification. + pub fn finalize(&self) -> Notification { + self.clone() + } + + /// Schedules a Notification + /// + /// Sends a Notification at the specified date. + #[cfg(all(target_os = "macos", feature = "chrono"))] + pub fn schedule( + &self, + delivery_date: chrono::DateTime, + ) -> Result { + macos::schedule_notification(self, delivery_date.timestamp() as f64) + } + + /// Schedules a Notification + /// + /// Sends a Notification at the specified timestamp. + /// This is a raw `f64`, if that is a bit too raw for you please activate the feature `"chrono"`, + /// then you can use `Notification::schedule()` instead, which accepts a `chrono::DateTime`. + #[cfg(target_os = "macos")] + pub fn schedule_raw(&self, timestamp: f64) -> Result { + macos::schedule_notification(self, timestamp) + } + + /// Sends Notification to D-Bus. + /// + /// Returns a handle to a notification + #[cfg(all(unix, not(target_os = "macos")))] + pub fn show(&self) -> Result { + xdg::show_notification(self) + } + + /// Sends Notification to D-Bus. + /// + /// Returns a handle to a notification + #[cfg(all(unix, not(target_os = "macos")))] + #[cfg(all(feature = "async", feature = "zbus"))] + pub async fn show_async(&self) -> Result { + xdg::show_notification_async(self).await + } + + /// Sends Notification to D-Bus. + /// + /// Returns a handle to a notification + #[cfg(all(unix, not(target_os = "macos")))] + #[cfg(feature = "async")] + // #[cfg(test)] + pub async fn show_async_at_bus(&self, sub_bus: &str) -> Result { + let bus = super::xdg::NotificationBus::custom(sub_bus).ok_or("invalid subpath")?; + super::xdg::show_notification_async_at_bus(self, bus).await + } + + /// Sends Notification to `NSUserNotificationCenter`. + /// + /// Returns an `Ok` no matter what, since there is currently no way of telling the success of + /// the notification. + #[cfg(target_os = "macos")] + pub fn show(&self) -> Result { + macos::show_notification(self) + } + + /// Sends Notification to `NSUserNotificationCenter`. + /// + /// Returns an `Ok` no matter what, since there is currently no way of telling the success of + /// the notification. + #[cfg(target_os = "windows")] + pub fn show(&self) -> Result<()> { + windows::show_notification(self) + } + + /// Wraps `show()` but prints notification to stdout. + #[cfg(all(unix, not(target_os = "macos")))] + #[deprecated = "this was never meant to be public API"] + pub fn show_debug(&mut self) -> Result { + println!( + "Notification:\n{appname}: ({icon}) {summary:?} {body:?}\nhints: [{hints:?}]\n", + appname = self.appname, + summary = self.summary, + body = self.body, + hints = self.hints, + icon = self.icon, + ); + self.show() + } +} + +impl Default for Notification { + #[cfg(all(unix, not(target_os = "macos")))] + fn default() -> Notification { + Notification { + appname: exe_name(), + summary: String::new(), + subtitle: None, + body: String::new(), + icon: String::new(), + hints: HashSet::new(), + hints_unique: HashMap::new(), + actions: Vec::new(), + timeout: Timeout::Default, + bus: Default::default(), + id: None, + } + } + + #[cfg(target_os = "macos")] + fn default() -> Notification { + Notification { + appname: exe_name(), + summary: String::new(), + subtitle: None, + body: String::new(), + icon: String::new(), + actions: Vec::new(), + timeout: Timeout::Default, + sound_name: Default::default(), + id: None, + } + } + + #[cfg(target_os = "windows")] + fn default() -> Notification { + Notification { + appname: exe_name(), + summary: String::new(), + subtitle: None, + body: String::new(), + icon: String::new(), + actions: Vec::new(), + timeout: Timeout::Default, + sound_name: Default::default(), + id: None, + path_to_image: None, + app_id: None, + } + } +} diff --git a/plugins/notification/src/notify_rust/server.rs b/plugins/notification/src/notify_rust/server.rs new file mode 100644 index 00000000..29182390 --- /dev/null +++ b/plugins/notification/src/notify_rust/server.rs @@ -0,0 +1,238 @@ +//! **Experimental** server taking the place of your Desktop Environment's Notification Server. +//! +//! This is not nearly meant for anything but testing, as it only prints notifications to stdout. +//! It does not respond properly either yet. +//! +//! This server will not replace an already running notification server. +//! + +#![allow(unused_imports, unused_variables, dead_code)] + +use std::cell::Cell; +use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, Mutex}; + +#[cfg(feature = "dbus")] +use dbus::{ + arg::{self, RefArg}, + ffidisp::{BusType, Connection, NameFlag}, + tree::{self, Factory, Interface, MTFn, MTSync, Tree}, + Path, +}; + +use super::{Hint, Notification, Timeout}; +use crate::xdg::{NOTIFICATION_NAMESPACE, NOTIFICATION_OBJECTPATH}; + +static DBUS_ERROR_FAILED: &str = "org.freedesktop.DBus.Error.Failed"; +/// Version of the crate equals the version server. +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// An **experimental** notification server. +/// See [the module level documentation](index.html) for more. +#[derive(Debug, Default)] +pub struct NotificationServer { + /// Counter for generating notification ids + counter: Mutex>, + + /// A flag that stops the server + stopped: Mutex>, +} + +impl NotificationServer { + fn count_up(&self) { + if let Ok(counter) = self.counter.lock() { + counter.set(counter.get() + 1); + } + } + + fn stop(&self) { + if let Ok(stop) = self.stopped.lock() { + stop.set(true); + } + } + + fn is_stopped(&self) -> bool { + if let Ok(stop) = self.stopped.lock() { + stop.get() + } else { + true + } + } + + /// Create a new `NotificationServer` instance. + pub fn create() -> Arc { + Arc::new(NotificationServer::default()) + } + // pub fn notify_mothod(&mut self, closure: F) + // -> Method + // where F: Fn(&Notification) + // { + + // fn handle_notification + + /// Start listening for incoming notifications + pub fn start(me: &Arc, closure: F) + where + F: Fn(&Notification), + { + let connection = Connection::get_private(BusType::Session).unwrap(); + + connection.release_name(NOTIFICATION_NAMESPACE).unwrap(); + connection + .register_name(NOTIFICATION_NAMESPACE, NameFlag::ReplaceExisting as u32) + .unwrap(); + connection + .register_object_path(NOTIFICATION_OBJECTPATH) + .unwrap(); + + let mytex = Arc::new(Mutex::new(me.clone())); + + let factory = Factory::new_fn::<()>(); // D::Tree = () + let tree = factory.tree(()).add( + factory + .object_path(NOTIFICATION_OBJECTPATH, ()) + .introspectable() + .add( + factory + .interface(NOTIFICATION_NAMESPACE, ()) + .add_m(method_notify(&factory, closure)) + .add_m(method_close_notification(&factory)) + .add_m(Self::stop_server(mytex.clone(), &factory)) + // .add_signal(method_notification_closed(&factory)) + // .add_signal(method_action_invoked(&factory)) + .add_m(method_get_capabilities(&factory)) + .add_m(method_get_server_information(&factory)), + ), + ); + + connection.add_handler(tree); + + while !me.is_stopped() { + // Wait for incoming messages. This will block up to one second. + // Discard the result - relevant messages have already been handled. + if let Some(received) = connection.incoming(1000).next() { + println!("RECEIVED {:?}", received); + } + } + } + + fn stop_server( + me: Arc>>, + factory: &Factory, + ) -> tree::Method, ()> { + factory + .method("Stop", (), move |minfo| { + if let Ok(me) = me.lock() { + me.stop(); + println!("STOPPING"); + Ok(vec![]) + } else { + Err(tree::MethodErr::failed(&String::from("nope!"))) + } + }) + .out_arg(("", "u")) + } +} + +fn hints_from_variants(hints: &HashMap) -> HashSet { + hints.iter().map(Into::into).collect() +} + +fn method_notify( + factory: &Factory, + on_notification: F, +) -> tree::Method, ()> +where + F: Fn(&Notification), +{ + factory + .method("Notify", (), move |minfo| { + let mut i = minfo.msg.iter_init(); + let appname: String = i.read()?; + let replaces_id: u32 = i.read()?; + let icon: String = i.read()?; + let summary: String = i.read()?; + let body: String = i.read()?; + let actions: Vec = i.read()?; + let hints: ::std::collections::HashMap>> = + i.read()?; + let timeout: i32 = i.read()?; + println!("hints {:?} ", hints); + + // let arg0 = try!(d.notify(app_name, replaces_id, app_icon, summary, body, actions, hints, timeout)); + let notification = Notification { + appname, + icon, + summary, + body, + actions, + hints: hints_from_variants(&hints), + timeout: Timeout::from(timeout), + id: if replaces_id == 0 { + None + } else { + Some(replaces_id) + }, + subtitle: None, + }; + + on_notification(¬ification); + + let arg0 = 43; + let rm = minfo.msg.method_return(); + let rm = rm.append1(arg0); + Ok(vec![rm]) + }) + .in_arg(("app_name", "s")) + .in_arg(("replaces_id", "u")) + .in_arg(("app_icon", "s")) + .in_arg(("summary", "s")) + .in_arg(("body", "s")) + .in_arg(("actions", "as")) + .in_arg(("hints", "a{sv}")) + .in_arg(("timeout", "i")) + .out_arg(("", "u")) +} + +fn method_close_notification(factory: &Factory) -> tree::Method, ()> { + factory + .method("CloseNotification", (), |minfo| { + let i = minfo.msg.iter_init(); + let rm = minfo.msg.method_return(); + Ok(vec![rm]) + }) + .in_arg(("id", "u")) +} + +fn method_get_capabilities(factory: &Factory) -> tree::Method, ()> { + factory + .method("GetCapabilities", (), |minfo| { + let caps: Vec = vec![]; + let rm = minfo.msg.method_return(); + let rm = rm.append1(caps); + Ok(vec![rm]) + }) + .out_arg(("caps", "as")) +} + +fn method_get_server_information(factory: &Factory) -> tree::Method, ()> { + factory + .method("GetServerInformation", (), |minfo| { + let (name, vendor, version, spec_version) = ( + "notify-rust", + "notify-rust", + env!("CARGO_PKG_VERSION"), + "0.0.0", + ); + let rm = minfo.msg.method_return(); + let rm = rm.append1(name); + let rm = rm.append1(vendor); + let rm = rm.append1(version); + let rm = rm.append1(spec_version); + Ok(vec![rm]) + }) + .out_arg(("name", "s")) + .out_arg(("vendor", "s")) + .out_arg(("version", "s")) + .out_arg(("spec_version", "s")) +} diff --git a/plugins/notification/src/notify_rust/timeout.rs b/plugins/notification/src/notify_rust/timeout.rs new file mode 100644 index 00000000..295c97ee --- /dev/null +++ b/plugins/notification/src/notify_rust/timeout.rs @@ -0,0 +1,108 @@ +use std::{convert::TryInto, num::ParseIntError, str::FromStr, time::Duration}; + +/// Describes the timeout of a notification +/// +/// # `FromStr` +/// You can also parse a `Timeout` from a `&str`. +/// ``` +/// # use notify_rust::Timeout; +/// assert_eq!("default".parse(), Ok(Timeout::Default)); +/// assert_eq!("never".parse(), Ok(Timeout::Never)); +/// assert_eq!("42".parse(), Ok(Timeout::Milliseconds(42))); +/// ``` +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Timeout { + /// Expires according to server default. + /// + /// Whatever that might be... + Default, + + /// Do not expire, user will have to close this manually. + Never, + + /// Expire after n milliseconds. + Milliseconds(u32), +} + +impl Default for Timeout { + fn default() -> Self { + Timeout::Default + } +} + +#[test] +fn timeout_from_i32() { + assert_eq!(Timeout::from(234), Timeout::Milliseconds(234)); + assert_eq!(Timeout::from(-234), Timeout::Default); + assert_eq!(Timeout::from(0), Timeout::Never); +} + +impl From for Timeout { + fn from(int: i32) -> Timeout { + use std::cmp::Ordering::*; + match int.cmp(&0) { + Greater => Timeout::Milliseconds(int as u32), + Less => Timeout::Default, + Equal => Timeout::Never, + } + } +} + +impl From for Timeout { + fn from(duration: Duration) -> Timeout { + if duration.is_zero() { + Timeout::Never + } else if duration.as_millis() > u32::MAX.into() { + Timeout::Default + } else { + Timeout::Milliseconds(duration.as_millis().try_into().unwrap_or(u32::MAX)) + } + } +} + +impl From for i32 { + fn from(timeout: Timeout) -> Self { + match timeout { + Timeout::Default => -1, + Timeout::Never => 0, + Timeout::Milliseconds(ms) => ms as i32, + } + } +} + +impl FromStr for Timeout { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + match s { + "default" => Ok(Timeout::Default), + "never" => Ok(Timeout::Never), + milliseconds => Ok(Timeout::Milliseconds(u32::from_str(milliseconds)?)), + } + } +} + +pub struct TimeoutMessage(Timeout); + +impl From for TimeoutMessage { + fn from(hint: Timeout) -> Self { + TimeoutMessage(hint) + } +} + +impl std::ops::Deref for TimeoutMessage { + type Target = Timeout; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(all(feature = "dbus", unix, not(target_os = "macos")))] +impl TryFrom<&dbus::arg::messageitem::MessageItem> for TimeoutMessage { + type Error = (); + + fn try_from(mi: &dbus::arg::messageitem::MessageItem) -> Result { + mi.inner::().map(|i| TimeoutMessage(i.into())) + } +} diff --git a/plugins/notification/src/notify_rust/urgency.rs b/plugins/notification/src/notify_rust/urgency.rs new file mode 100644 index 00000000..9c9ba96e --- /dev/null +++ b/plugins/notification/src/notify_rust/urgency.rs @@ -0,0 +1,88 @@ +use super::error::ErrorKind; +use std::convert::TryFrom; + +/// Levels of Urgency. +/// +/// # Specification +/// > Developers must use their own judgement when deciding the urgency of a notification. Typically, if the majority of programs are using the same level for a specific type of urgency, other applications should follow them. +/// > +/// > For low and normal urgencies, server implementations may display the notifications how they choose. They should, however, have a sane expiration timeout dependent on the urgency level. +/// > +/// > **Critical notifications should not automatically expire**, as they are things that the user will most likely want to know about. They should only be closed when the user dismisses them, for example, by clicking on the notification. +/// +/// — see [Galago](http://www.galago-project.org/specs/notification/0.9/x320.html) or [Gnome](https://developer.gnome.org/notification-spec/#urgency-levels) specification. +/// +/// # Example +/// ```no_run +/// # use notify_rust::*; +/// # fn _doc() -> Result<(), Box> { +/// Notification::new() +/// .summary("oh no") +/// .icon("dialog-warning") +/// .urgency(Urgency::Critical) +/// .show()?; +/// # Ok(()) +/// # } +/// ``` +/// +#[derive(Eq, PartialEq, Hash, Copy, Clone, Debug)] +pub enum Urgency { + /// The behavior for `Low` urgency depends on the notification server. + Low = 0, + /// The behavior for `Normal` urgency depends on the notification server. + Normal = 1, + /// A critical notification will not time out. + Critical = 2, +} + +impl TryFrom<&str> for Urgency { + type Error = super::error::Error; + + #[rustfmt::skip] + fn try_from(string: &str) -> Result { + match string.to_lowercase().as_ref() { + "low" | + "lo" => Ok(Urgency::Low), + "normal" | + "medium" => Ok(Urgency::Normal), + "critical" | + "high" | + "hi" => Ok(Urgency::Critical), + _ => Err(ErrorKind::Conversion(format!("invalid input {:?}", string)).into()) + } + } +} + +impl From> for Urgency { + fn from(maybe_int: Option) -> Urgency { + match maybe_int { + Some(0) => Urgency::Low, + Some(x) if x >= 2 => Urgency::Critical, + _ => Urgency::Normal, + } + } +} + +// TODO: remove this in v5.0 +#[cfg(not(feature = "server"))] +impl From for Urgency { + fn from(int: u64) -> Urgency { + match int { + 0 => Urgency::Low, + 1 => Urgency::Normal, + 2..=std::u64::MAX => Urgency::Critical, + } + } +} + +// TODO: make this the default in v5.0 +#[cfg(feature = "server")] +impl From for Urgency { + fn from(int: u8) -> Urgency { + match int { + 0 => Urgency::Low, + 1 => Urgency::Normal, + 2..=std::u8::MAX => Urgency::Critical, + } + } +} diff --git a/plugins/notification/src/notify_rust/windows.rs b/plugins/notification/src/notify_rust/windows.rs new file mode 100644 index 00000000..c1c76864 --- /dev/null +++ b/plugins/notification/src/notify_rust/windows.rs @@ -0,0 +1,40 @@ +use winrt_notification::Toast; + +pub use crate::{error::*, notification::Notification, timeout::Timeout}; + +use std::{path::Path, str::FromStr}; + +pub(crate) fn show_notification(notification: &Notification) -> Result<()> { + let sound = match ¬ification.sound_name { + Some(chosen_sound_name) => winrt_notification::Sound::from_str(chosen_sound_name).ok(), + None => None, + }; + + let duration = match notification.timeout { + Timeout::Default => winrt_notification::Duration::Short, + Timeout::Never => winrt_notification::Duration::Long, + Timeout::Milliseconds(t) => { + if t >= 25000 { + winrt_notification::Duration::Long + } else { + winrt_notification::Duration::Short + } + } + }; + + let powershell_app_id = &Toast::POWERSHELL_APP_ID.to_string(); + let app_id = ¬ification.app_id.as_ref().unwrap_or(powershell_app_id); + let mut toast = Toast::new(app_id) + .title(¬ification.summary) + .text1(notification.subtitle.as_ref().map_or("", AsRef::as_ref)) // subtitle + .text2(¬ification.body) + .sound(sound) + .duration(duration); + if let Some(image_path) = ¬ification.path_to_image { + toast = toast.image(Path::new(&image_path), ""); + } + + toast + .show() + .map_err(|e| Error::from(ErrorKind::Msg(format!("{:?}", e)))) +} diff --git a/plugins/notification/src/notify_rust/xdg/bus.rs b/plugins/notification/src/notify_rust/xdg/bus.rs new file mode 100644 index 00000000..6195f3ad --- /dev/null +++ b/plugins/notification/src/notify_rust/xdg/bus.rs @@ -0,0 +1,68 @@ +use crate::xdg::NOTIFICATION_DEFAULT_BUS; + +fn skip_first_slash(s: &str) -> &str { + if let Some('/') = s.chars().next() { + &s[1..] + } else { + s + } +} + +use std::path::PathBuf; + +type BusNameType = std::borrow::Cow<'static, str>; + +#[derive(Clone, Debug)] +pub struct NotificationBus(BusNameType); + +impl Default for NotificationBus { + #[cfg(feature = "zbus")] + fn default() -> Self { + Self( + zbus::names::WellKnownName::from_static_str(NOTIFICATION_DEFAULT_BUS) + .unwrap() + .to_string() + .into(), + ) + } + + #[cfg(all(feature = "dbus", not(feature = "zbus")))] + fn default() -> Self { + Self( + dbus::strings::BusName::from_slice(NOTIFICATION_DEFAULT_BUS) + .unwrap() + .to_string() + .into(), + ) + } +} + +impl NotificationBus { + fn namespaced_custom(custom_path: &str) -> Option { + // abusing path for semantic join + skip_first_slash( + PathBuf::from("/de/hoodie/Notification") + .join(custom_path) + .to_str()?, + ) + .replace('/', ".") + .into() + } + + #[cfg(feature = "zbus")] + pub fn custom(custom_path: &str) -> Option { + let name = + zbus::names::WellKnownName::try_from(Self::namespaced_custom(custom_path)?).ok()?; + Some(Self(name.to_string().into())) + } + + #[cfg(all(feature = "dbus", not(feature = "zbus")))] + pub fn custom(custom_path: &str) -> Option { + let name = dbus::strings::BusName::new(Self::namespaced_custom(custom_path)?).ok()?; + Some(Self(name.to_string().into())) + } + + pub fn into_name(self) -> BusNameType { + self.0 + } +} diff --git a/plugins/notification/src/notify_rust/xdg/dbus_rs.rs b/plugins/notification/src/notify_rust/xdg/dbus_rs.rs new file mode 100644 index 00000000..16e5f5b3 --- /dev/null +++ b/plugins/notification/src/notify_rust/xdg/dbus_rs.rs @@ -0,0 +1,328 @@ +use dbus::{ + arg::messageitem::{MessageItem, MessageItemArray}, + ffidisp::{BusType, Connection, ConnectionItem}, + Message, +}; + +use super::{ + bus::NotificationBus, ActionResponse, ActionResponseHandler, CloseReason, + NOTIFICATION_INTERFACE, +}; + +use crate::{ + error::*, + hints::message::HintMessage, + notification::Notification, + xdg::{ServerInformation, NOTIFICATION_OBJECTPATH}, +}; + +pub mod bus { + + use crate::xdg::NOTIFICATION_DEFAULT_BUS; + + fn skip_first_slash(s: &str) -> &str { + if let Some('/') = s.chars().next() { + &s[1..] + } else { + s + } + } + + use std::path::PathBuf; + + type BusNameType = dbus::strings::BusName<'static>; + + #[derive(Clone, Debug)] + pub struct NotificationBus(BusNameType); + + impl Default for NotificationBus { + fn default() -> Self { + Self(dbus::strings::BusName::from_slice(NOTIFICATION_DEFAULT_BUS).unwrap()) + } + } + + impl NotificationBus { + fn namespaced_custom(custom_path: &str) -> Option { + // abusing path for semantic join + skip_first_slash( + PathBuf::from("/de/hoodie/Notification") + .join(custom_path) + .to_str()?, + ) + .replace('/', ".") + .into() + } + + pub fn custom(custom_path: &str) -> Option { + let name = dbus::strings::BusName::new(Self::namespaced_custom(custom_path)?).ok()?; + Some(Self(name)) + } + + pub fn into_name(self) -> BusNameType { + self.0 + } + } +} + +/// A handle to a shown notification. +/// +/// This keeps a connection alive to ensure actions work on certain desktops. +#[derive(Debug)] +pub struct DbusNotificationHandle { + pub(crate) id: u32, + pub(crate) connection: Connection, + pub(crate) notification: Notification, +} + +impl DbusNotificationHandle { + pub(crate) fn new( + id: u32, + connection: Connection, + notification: Notification, + ) -> DbusNotificationHandle { + DbusNotificationHandle { + id, + connection, + notification, + } + } + + pub fn wait_for_action(self, invocation_closure: impl ActionResponseHandler) { + wait_for_action_signal(&self.connection, self.id, invocation_closure); + } + + pub fn close(self) { + let mut message = build_message("CloseNotification", Default::default()); + message.append_items(&[self.id.into()]); + let _ = self.connection.send(message); // If closing fails there's nothing we could do anyway + } + + pub fn on_close(self, closure: F) + where + F: FnOnce(CloseReason), + { + self.wait_for_action(|action: &ActionResponse| { + if let ActionResponse::Closed(reason) = action { + closure(*reason); + } + }); + } + + pub fn update(&mut self) { + self.id = send_notification_via_connection(&self.notification, self.id, &self.connection) + .unwrap(); + } +} + +pub fn send_notification_via_connection( + notification: &Notification, + id: u32, + connection: &Connection, +) -> Result { + send_notification_via_connection_at_bus(notification, id, connection, Default::default()) +} + +pub fn send_notification_via_connection_at_bus( + notification: &Notification, + id: u32, + connection: &Connection, + bus: NotificationBus, +) -> Result { + let mut message = build_message("Notify", bus); + let timeout: i32 = notification.timeout.into(); + message.append_items(&[ + notification.appname.to_owned().into(), // appname + id.into(), // notification to update + notification.icon.to_owned().into(), // icon + notification.summary.to_owned().into(), // summary (title) + notification.body.to_owned().into(), // body + pack_actions(notification), // actions + pack_hints(notification)?, // hints + timeout.into(), // timeout + ]); + + let reply = connection.send_with_reply_and_block(message, 2000)?; + + match reply.get_items().first() { + Some(MessageItem::UInt32(ref id)) => Ok(*id), + _ => Ok(0), + } +} + +pub fn connect_and_send_notification( + notification: &Notification, +) -> Result { + let bus = notification.bus.clone(); + connect_and_send_notification_at_bus(notification, bus) +} + +pub fn connect_and_send_notification_at_bus( + notification: &Notification, + bus: NotificationBus, +) -> Result { + let connection = Connection::get_private(BusType::Session)?; + let inner_id = notification.id.unwrap_or(0); + let id = send_notification_via_connection_at_bus(notification, inner_id, &connection, bus)?; + + Ok(DbusNotificationHandle::new( + id, + connection, + notification.clone(), + )) +} + +fn build_message(method_name: &str, bus: NotificationBus) -> Message { + Message::new_method_call( + bus.into_name(), + NOTIFICATION_OBJECTPATH, + NOTIFICATION_INTERFACE, + method_name, + ) + .unwrap_or_else(|_| panic!("Error building message call {:?}.", method_name)) +} + +pub fn pack_hints(notification: &Notification) -> Result { + if !notification.hints.is_empty() || !notification.hints_unique.is_empty() { + let hints = notification + .get_hints() + .cloned() + .map(HintMessage::wrap_hint) + .collect::>(); + + if let Ok(array) = MessageItem::new_dict(hints) { + return Ok(array); + } + } + + Ok(MessageItem::Array( + MessageItemArray::new(vec![], "a{sv}".into()).unwrap(), + )) +} + +pub fn pack_actions(notification: &Notification) -> MessageItem { + if !notification.actions.is_empty() { + let mut actions = vec![]; + for action in ¬ification.actions { + actions.push(action.to_owned().into()); + } + if let Ok(array) = MessageItem::new_array(actions) { + return array; + } + } + + MessageItem::Array(MessageItemArray::new(vec![], "as".into()).unwrap()) +} + +pub fn get_capabilities() -> Result> { + let mut capabilities = vec![]; + + let message = build_message("GetCapabilities", Default::default()); + let connection = Connection::get_private(BusType::Session)?; + let reply = connection.send_with_reply_and_block(message, 2000)?; + + if let Some(MessageItem::Array(items)) = reply.get_items().first() { + for item in items.iter() { + if let MessageItem::Str(ref cap) = *item { + capabilities.push(cap.clone()); + } + } + } + + Ok(capabilities) +} + +fn unwrap_message_string(item: Option<&MessageItem>) -> String { + match item { + Some(MessageItem::Str(value)) => value.to_owned(), + _ => "".to_owned(), + } +} + +#[allow(clippy::get_first)] +pub fn get_server_information() -> Result { + let message = build_message("GetServerInformation", Default::default()); + let connection = Connection::get_private(BusType::Session)?; + let reply = connection.send_with_reply_and_block(message, 2000)?; + + let items = reply.get_items(); + + Ok(ServerInformation { + name: unwrap_message_string(items.get(0)), + vendor: unwrap_message_string(items.get(1)), + version: unwrap_message_string(items.get(2)), + spec_version: unwrap_message_string(items.get(3)), + }) +} + +/// Listens for the `ActionInvoked(UInt32, String)` Signal. +/// +/// No need to use this, check out `Notification::show_and_wait_for_action(FnOnce(action:&str))` +pub fn handle_action(id: u32, func: impl ActionResponseHandler) { + let connection = Connection::get_private(BusType::Session).unwrap(); + wait_for_action_signal(&connection, id, func); +} + +// Listens for the `ActionInvoked(UInt32, String)` signal. +fn wait_for_action_signal(connection: &Connection, id: u32, handler: impl ActionResponseHandler) { + connection + .add_match(&format!( + "interface='{}',member='ActionInvoked'", + NOTIFICATION_INTERFACE + )) + .unwrap(); + connection + .add_match(&format!( + "interface='{}',member='NotificationClosed'", + NOTIFICATION_INTERFACE + )) + .unwrap(); + + for item in connection.iter(1000) { + if let ConnectionItem::Signal(message) = item { + let items = message.get_items(); + + let (path, interface, member) = ( + message.path().map_or_else(String::new, |p| { + p.into_cstring().to_string_lossy().into_owned() + }), + message.interface().map_or_else(String::new, |p| { + p.into_cstring().to_string_lossy().into_owned() + }), + message.member().map_or_else(String::new, |p| { + p.into_cstring().to_string_lossy().into_owned() + }), + ); + match (path.as_str(), interface.as_str(), member.as_str()) { + // match (protocol.unwrap(), iface.unwrap(), member.unwrap()) { + // Action Invoked + (path, interface, "ActionInvoked") + if path == NOTIFICATION_OBJECTPATH && interface == NOTIFICATION_INTERFACE => + { + if let (&MessageItem::UInt32(nid), MessageItem::Str(ref action)) = + (&items[0], &items[1]) + { + if nid == id { + handler.call(&ActionResponse::Custom(action)); + break; + } + } + } + + // Notification Closed + (path, interface, "NotificationClosed") + if path == NOTIFICATION_OBJECTPATH && interface == NOTIFICATION_INTERFACE => + { + if let (&MessageItem::UInt32(nid), &MessageItem::UInt32(reason)) = + (&items[0], &items[1]) + { + if nid == id { + handler.call(&ActionResponse::Closed(reason.into())); + break; + } + } + } + (..) => (), + } + } + } +} diff --git a/plugins/notification/src/notify_rust/xdg/mod.rs b/plugins/notification/src/notify_rust/xdg/mod.rs new file mode 100644 index 00000000..370d88b2 --- /dev/null +++ b/plugins/notification/src/notify_rust/xdg/mod.rs @@ -0,0 +1,604 @@ +//! This module contains `XDG` and `DBus` specific code. +//! +//! it should not be available under any platform other than `(unix, not(target_os = "macos"))` + +#[cfg(feature = "dbus")] +use dbus::ffidisp::Connection as DbusConnection; +#[cfg(feature = "zbus")] +use zbus::{block_on, zvariant}; + +use super::{error::*, notification::Notification}; + +use std::ops::{Deref, DerefMut}; + +#[cfg(feature = "dbus")] +mod dbus_rs; +#[cfg(all(feature = "dbus", not(feature = "zbus")))] +use dbus_rs::bus; + +#[cfg(feature = "zbus")] +mod zbus_rs; +#[cfg(all(feature = "zbus", not(feature = "dbus")))] +use zbus_rs::bus; + +#[cfg(all(feature = "dbus", feature = "zbus"))] +mod bus; + +// #[cfg(all(feature = "server", feature = "dbus", unix, not(target_os = "macos")))] +// pub mod server_dbus; + +// #[cfg(all(feature = "server", feature = "zbus", unix, not(target_os = "macos")))] +// pub mod server_zbus; + +// #[cfg(all(feature = "server", unix, not(target_os = "macos")))] +// pub mod server; + +#[cfg(not(feature = "debug_namespace"))] +#[doc(hidden)] +pub static NOTIFICATION_DEFAULT_BUS: &str = "org.freedesktop.Notifications"; + +#[cfg(feature = "debug_namespace")] +#[doc(hidden)] +// #[deprecated] +pub static NOTIFICATION_DEFAULT_BUS: &str = "de.hoodie.Notifications"; + +#[doc(hidden)] +pub static NOTIFICATION_INTERFACE: &str = "org.freedesktop.Notifications"; + +#[doc(hidden)] +pub static NOTIFICATION_OBJECTPATH: &str = "/org/freedesktop/Notifications"; + +pub(crate) use bus::NotificationBus; + +#[derive(Debug)] +enum NotificationHandleInner { + #[cfg(feature = "dbus")] + Dbus(dbus_rs::DbusNotificationHandle), + + #[cfg(feature = "zbus")] + Zbus(zbus_rs::ZbusNotificationHandle), +} + +/// A handle to a shown notification. +/// +/// This keeps a connection alive to ensure actions work on certain desktops. +#[derive(Debug)] +pub struct NotificationHandle { + inner: NotificationHandleInner, +} + +#[allow(dead_code)] +impl NotificationHandle { + #[cfg(feature = "dbus")] + pub(crate) fn for_dbus( + id: u32, + connection: DbusConnection, + notification: Notification, + ) -> NotificationHandle { + NotificationHandle { + inner: dbus_rs::DbusNotificationHandle::new(id, connection, notification).into(), + } + } + + #[cfg(feature = "zbus")] + pub(crate) fn for_zbus( + id: u32, + connection: zbus::Connection, + notification: Notification, + ) -> NotificationHandle { + NotificationHandle { + inner: zbus_rs::ZbusNotificationHandle::new(id, connection, notification).into(), + } + } + + /// Waits for the user to act on a notification and then calls + /// `invocation_closure` with the name of the corresponding action. + pub fn wait_for_action(self, invocation_closure: F) + where + F: FnOnce(&str), + { + match self.inner { + #[cfg(feature = "dbus")] + NotificationHandleInner::Dbus(inner) => { + inner.wait_for_action(|action: &ActionResponse| match action { + ActionResponse::Custom(action) => invocation_closure(action), + ActionResponse::Closed(_reason) => invocation_closure("__closed"), // FIXME: remove backward compatibility with 5.0 + }); + } + + #[cfg(feature = "zbus")] + NotificationHandleInner::Zbus(inner) => { + block_on( + inner.wait_for_action(|action: &ActionResponse| match action { + ActionResponse::Custom(action) => invocation_closure(action), + ActionResponse::Closed(_reason) => invocation_closure("__closed"), // FIXME: remove backward compatibility with 5.0 + }), + ); + } + }; + } + + /// Manually close the notification + /// + /// # Example + /// + /// ```no_run + /// # use notify_rust::*; + /// let handle: NotificationHandle = Notification::new() + /// .summary("oh no") + /// .hint(notify_rust::Hint::Transient(true)) + /// .body("I'll be here till you close me!") + /// .hint(Hint::Resident(true)) // does not work on kde + /// .timeout(Timeout::Never) // works on kde and gnome + /// .show() + /// .unwrap(); + /// // ... and then later + /// handle.close(); + /// ``` + pub fn close(self) { + match self.inner { + #[cfg(feature = "dbus")] + NotificationHandleInner::Dbus(inner) => inner.close(), + #[cfg(feature = "zbus")] + NotificationHandleInner::Zbus(inner) => block_on(inner.close()), + } + } + + /// Executes a closure after the notification has closed. + /// + /// ## Example 1: *I don't care about why it closed* (the good ole API) + /// + /// ```no_run + /// # use notify_rust::Notification; + /// Notification::new().summary("Time is running out") + /// .body("This will go away.") + /// .icon("clock") + /// .show() + /// .unwrap() + /// .on_close(|| println!("closed")); + /// ``` + /// + /// ## Example 2: *I **do** care about why it closed* (added in v4.5.0) + /// + /// ```no_run + /// # use notify_rust::Notification; + /// Notification::new().summary("Time is running out") + /// .body("This will go away.") + /// .icon("clock") + /// .show() + /// .unwrap() + /// .on_close(|reason| println!("closed: {:?}", reason)); + /// ``` + pub fn on_close(self, handler: impl CloseHandler) { + match self.inner { + #[cfg(feature = "dbus")] + NotificationHandleInner::Dbus(inner) => { + inner.wait_for_action(|action: &ActionResponse| { + if let ActionResponse::Closed(reason) = action { + handler.call(*reason); + } + }); + } + #[cfg(feature = "zbus")] + NotificationHandleInner::Zbus(inner) => { + block_on(inner.wait_for_action(|action: &ActionResponse| { + if let ActionResponse::Closed(reason) = action { + handler.call(*reason); + } + })); + } + }; + } + + /// Replace the original notification with an updated version + /// ## Example + /// ```no_run + /// # use notify_rust::Notification; + /// let mut notification = Notification::new().summary("Latest News") + /// .body("Bayern Dortmund 3:2") + /// .show() + /// .unwrap(); + /// + /// std::thread::sleep_ms(1_500); + /// + /// notification.summary("Latest News (Correction)") + /// .body("Bayern Dortmund 3:3"); + /// + /// notification.update(); + /// ``` + /// Watch out for different implementations of the + /// notification server! On plasma5 for instance, you should also change the appname, so the old + /// message is really replaced and not just amended. Xfce behaves well, all others have not + /// been tested by the developer. + pub fn update(&mut self) { + match self.inner { + #[cfg(feature = "dbus")] + NotificationHandleInner::Dbus(ref mut inner) => inner.update(), + #[cfg(feature = "zbus")] + NotificationHandleInner::Zbus(ref mut inner) => inner.update(), + } + } + + /// Returns the Handle's id. + pub fn id(&self) -> u32 { + match self.inner { + #[cfg(feature = "dbus")] + NotificationHandleInner::Dbus(ref inner) => inner.id, + #[cfg(feature = "zbus")] + NotificationHandleInner::Zbus(ref inner) => inner.id, + } + } +} + +/// Required for `DerefMut` +impl Deref for NotificationHandle { + type Target = Notification; + + fn deref(&self) -> &Notification { + match self.inner { + #[cfg(feature = "dbus")] + NotificationHandleInner::Dbus(ref inner) => &inner.notification, + #[cfg(feature = "zbus")] + NotificationHandleInner::Zbus(ref inner) => &inner.notification, + } + } +} + +/// Allow you to easily modify notification properties +impl DerefMut for NotificationHandle { + fn deref_mut(&mut self) -> &mut Notification { + match self.inner { + #[cfg(feature = "dbus")] + NotificationHandleInner::Dbus(ref mut inner) => &mut inner.notification, + #[cfg(feature = "zbus")] + NotificationHandleInner::Zbus(ref mut inner) => &mut inner.notification, + } + } +} + +#[cfg(feature = "dbus")] +impl From for NotificationHandleInner { + fn from(handle: dbus_rs::DbusNotificationHandle) -> NotificationHandleInner { + NotificationHandleInner::Dbus(handle) + } +} + +#[cfg(feature = "zbus")] +impl From for NotificationHandleInner { + fn from(handle: zbus_rs::ZbusNotificationHandle) -> NotificationHandleInner { + NotificationHandleInner::Zbus(handle) + } +} + +#[cfg(feature = "dbus")] +impl From for NotificationHandle { + fn from(handle: dbus_rs::DbusNotificationHandle) -> NotificationHandle { + NotificationHandle { + inner: handle.into(), + } + } +} + +#[cfg(feature = "zbus")] +impl From for NotificationHandle { + fn from(handle: zbus_rs::ZbusNotificationHandle) -> NotificationHandle { + NotificationHandle { + inner: handle.into(), + } + } +} + +// here be public functions + +// TODO: breaking change, wait for 5.0 +// #[cfg(all(feature = "dbus", feature = "zbus"))] +//compile_error!("the z and d features are mutually exclusive"); + +#[cfg(all( + not(any(feature = "dbus", feature = "zbus")), + unix, + not(target_os = "macos") +))] +compile_error!("you have to build with either zbus or dbus turned on"); + +/// Which Dbus implementation are we using? +#[derive(Copy, Clone, Debug)] +pub enum DbusStack { + /// using [dbus-rs](https://docs.rs/dbus-rs) + Dbus, + /// using [zbus](https://docs.rs/zbus) + Zbus, +} + +#[cfg(all(feature = "dbus", feature = "zbus"))] +const DBUS_SWITCH_VAR: &str = "DBUSRS"; + +#[cfg(all(feature = "zbus", not(feature = "dbus")))] +pub(crate) fn show_notification(notification: &Notification) -> Result { + block_on(zbus_rs::connect_and_send_notification(notification)).map(Into::into) +} + +#[cfg(all(feature = "async", feature = "zbus"))] +pub(crate) async fn show_notification_async( + notification: &Notification, +) -> Result { + zbus_rs::connect_and_send_notification(notification) + .await + .map(Into::into) +} + +#[cfg(all(feature = "async", feature = "zbus"))] +pub(crate) async fn show_notification_async_at_bus( + notification: &Notification, + bus: NotificationBus, +) -> Result { + zbus_rs::connect_and_send_notification_at_bus(notification, bus) + .await + .map(Into::into) +} + +#[cfg(all(feature = "dbus", not(feature = "zbus")))] +pub(crate) fn show_notification(notification: &Notification) -> Result { + dbus_rs::connect_and_send_notification(notification).map(Into::into) +} + +#[cfg(all(feature = "dbus", feature = "zbus"))] +pub(crate) fn show_notification(notification: &Notification) -> Result { + if std::env::var(DBUS_SWITCH_VAR).is_ok() { + dbus_rs::connect_and_send_notification(notification).map(Into::into) + } else { + block_on(zbus_rs::connect_and_send_notification(notification)).map(Into::into) + } +} + +/// Get the currently used [`DbusStack`] +/// +/// (zbus only) +#[cfg(all(feature = "zbus", not(feature = "dbus")))] +pub fn dbus_stack() -> Option { + Some(DbusStack::Zbus) +} + +/// Get the currently used [`DbusStack`] +/// +/// (dbus-rs only) +#[cfg(all(feature = "dbus", not(feature = "zbus")))] +pub fn dbus_stack() -> Option { + Some(DbusStack::Dbus) +} + +/// Get the currently used [`DbusStack`] +/// +/// both dbus-rs and zbus, switch via `$ZBUS_NOTIFICATION` +#[cfg(all(feature = "dbus", feature = "zbus"))] +pub fn dbus_stack() -> Option { + Some(if std::env::var(DBUS_SWITCH_VAR).is_ok() { + DbusStack::Dbus + } else { + DbusStack::Zbus + }) +} + +/// Get the currently used [`DbusStack`] +/// +/// neither zbus nor dbus-rs are configured +#[cfg(all(not(feature = "dbus"), not(feature = "zbus")))] +pub fn dbus_stack() -> Option { + None +} + +/// Get list of all capabilities of the running notification server. +/// +/// (zbus only) +#[cfg(all(feature = "zbus", not(feature = "dbus")))] +pub fn get_capabilities() -> Result> { + block_on(zbus_rs::get_capabilities()) +} + +/// Get list of all capabilities of the running notification server. +/// +/// (dbus-rs only) +#[cfg(all(feature = "dbus", not(feature = "zbus")))] +pub fn get_capabilities() -> Result> { + dbus_rs::get_capabilities() +} + +/// Get list of all capabilities of the running notification server. +/// +/// both dbus-rs and zbus, switch via `$ZBUS_NOTIFICATION` +#[cfg(all(feature = "dbus", feature = "zbus"))] +pub fn get_capabilities() -> Result> { + if std::env::var(DBUS_SWITCH_VAR).is_ok() { + dbus_rs::get_capabilities() + } else { + block_on(zbus_rs::get_capabilities()) + } +} + +/// Returns a struct containing `ServerInformation`. +/// +/// This struct contains `name`, `vendor`, `version` and `spec_version` of the notification server +/// running. +/// +/// (zbus only) +#[cfg(all(feature = "zbus", not(feature = "dbus")))] +pub fn get_server_information() -> Result { + block_on(zbus_rs::get_server_information()) +} + +/// Returns a struct containing `ServerInformation`. +/// +/// This struct contains `name`, `vendor`, `version` and `spec_version` of the notification server +/// running. +/// +/// (dbus-rs only) +#[cfg(all(feature = "dbus", not(feature = "zbus")))] +pub fn get_server_information() -> Result { + dbus_rs::get_server_information() +} + +/// Returns a struct containing `ServerInformation`. +/// +/// This struct contains `name`, `vendor`, `version` and `spec_version` of the notification server +/// running. +/// +/// both dbus-rs and zbus, switch via `$ZBUS_NOTIFICATION` +#[cfg(all(feature = "dbus", feature = "zbus"))] +pub fn get_server_information() -> Result { + if std::env::var(DBUS_SWITCH_VAR).is_ok() { + dbus_rs::get_server_information() + } else { + block_on(zbus_rs::get_server_information()) + } +} + +/// Return value of `get_server_information()`. +#[derive(Debug, serde::Deserialize)] +#[cfg_attr(feature = "zbus", derive(zvariant::Type))] +pub struct ServerInformation { + /// The product name of the server. + pub name: String, + /// The vendor name. + pub vendor: String, + /// The server's version string. + pub version: String, + /// The specification version the server is compliant with. + pub spec_version: String, +} + +/// Strictly internal. +/// The NotificationServer implemented here exposes a "Stop" function. +/// stops the notification server +#[cfg(all(feature = "server", unix, not(target_os = "macos")))] +#[doc(hidden)] +pub fn stop_server() { + #[cfg(feature = "dbus")] + dbus_rs::stop_server() +} + +/// Listens for the `ActionInvoked(UInt32, String)` Signal. +/// +/// No need to use this, check out [`NotificationHandle::wait_for_action`] +/// (xdg only) +#[cfg(all(feature = "zbus", not(feature = "dbus")))] +// #[deprecated(note="please use [`NotificationHandle::wait_for_action`]")] +pub fn handle_action(id: u32, func: F) +where + F: FnOnce(&ActionResponse), +{ + block_on(zbus_rs::handle_action(id, func)); +} + +/// Listens for the `ActionInvoked(UInt32, String)` Signal. +/// +/// No need to use this, check out [`NotificationHandle::wait_for_action`] +/// (xdg only) +#[cfg(all(feature = "dbus", not(feature = "zbus")))] +// #[deprecated(note="please use `NotificationHandle::wait_for_action`")] +pub fn handle_action(id: u32, func: F) +where + F: FnOnce(&ActionResponse), +{ + dbus_rs::handle_action(id, func); +} + +/// Listens for the `ActionInvoked(UInt32, String)` Signal. +/// +/// No need to use this, check out [`NotificationHandle::wait_for_action`] +/// both dbus-rs and zbus, switch via `$ZBUS_NOTIFICATION` +#[cfg(all(feature = "dbus", feature = "zbus"))] +// #[deprecated(note="please use `NotificationHandle::wait_for_action`")] +pub fn handle_action(id: u32, func: F) +where + F: FnOnce(&ActionResponse), +{ + if std::env::var(DBUS_SWITCH_VAR).is_ok() { + dbus_rs::handle_action(id, func); + } else { + block_on(zbus_rs::handle_action(id, func)); + } +} + +/// Reason passed to `NotificationClosed` Signal +/// +/// ## Specification +/// As listed under [Table 8. `NotificationClosed` Parameters](https://specifications.freedesktop.org/notification-spec/latest/ar01s09.html#idm46350804042704) +#[derive(Copy, Clone, Debug)] +pub enum CloseReason { + /// The notification expired + Expired, + /// The notification was dismissed by the user + Dismissed, + /// The notification was closed by a call to `CloseNotification` + CloseAction, + /// Undefined/Reserved reason + Other(u32), +} + +impl From for CloseReason { + fn from(raw_reason: u32) -> Self { + match raw_reason { + 1 => CloseReason::Expired, + 2 => CloseReason::Dismissed, + 3 => CloseReason::CloseAction, + other => CloseReason::Other(other), + } + } +} + +/// Helper Trait implemented by `Fn()` +pub trait ActionResponseHandler { + fn call(self, response: &ActionResponse); +} + +// impl ActionResponseHandler for F +impl ActionResponseHandler for F +where + F: FnOnce(&ActionResponse), +{ + fn call(self, res: &ActionResponse) { + (self)(res); + } +} + +/// Response to an action +pub enum ActionResponse<'a> { + /// Custom Action configured by the Notification. + Custom(&'a str), + + /// The Notification was closed. + Closed(CloseReason), +} + +impl<'a> From<&'a str> for ActionResponse<'a> { + fn from(raw: &'a str) -> Self { + Self::Custom(raw) + } +} + +/// Your handy callback for the `Close` signal of your Notification. +/// +/// This is implemented by `Fn()` and `Fn(CloseReason)`, so there is probably no good reason for you to manually implement this trait. +/// Should you find one anyway, please notify me and I'll gladly remove this obviously redundant comment. +pub trait CloseHandler { + /// This is called with the [`CloseReason`]. + fn call(&self, reason: CloseReason); +} + +impl CloseHandler for F +where + F: Fn(CloseReason), +{ + fn call(&self, reason: CloseReason) { + self(reason); + } +} + +impl CloseHandler<()> for F +where + F: Fn(), +{ + fn call(&self, _: CloseReason) { + self(); + } +} diff --git a/plugins/notification/src/notify_rust/xdg/zbus_rs.rs b/plugins/notification/src/notify_rust/xdg/zbus_rs.rs new file mode 100644 index 00000000..7bf7017a --- /dev/null +++ b/plugins/notification/src/notify_rust/xdg/zbus_rs.rs @@ -0,0 +1,285 @@ +use super::super::{error::*, notification::Notification, xdg}; +use zbus::{export::futures_util::TryStreamExt, MatchRule}; + +use super::{bus::NotificationBus, ActionResponse, ActionResponseHandler, CloseReason}; + +pub mod bus { + + use super::super::super::xdg::NOTIFICATION_DEFAULT_BUS; + + fn skip_first_slash(s: &str) -> &str { + if let Some('/') = s.chars().next() { + &s[1..] + } else { + s + } + } + + use std::path::PathBuf; + + type BusNameType = zbus::names::WellKnownName<'static>; + + #[derive(Clone, Debug)] + pub struct NotificationBus(BusNameType); + + impl Default for NotificationBus { + #[cfg(feature = "zbus")] + fn default() -> Self { + Self(zbus::names::WellKnownName::from_static_str(NOTIFICATION_DEFAULT_BUS).unwrap()) + } + } + + impl NotificationBus { + fn namespaced_custom(custom_path: &str) -> Option { + // abusing path for semantic join + skip_first_slash( + PathBuf::from("/de/hoodie/Notification") + .join(custom_path) + .to_str()?, + ) + .replace('/', ".") + .into() + } + + pub fn custom(custom_path: &str) -> Option { + let name = + zbus::names::WellKnownName::try_from(Self::namespaced_custom(custom_path)?).ok()?; + Some(Self(name)) + } + + pub fn into_name(self) -> BusNameType { + self.0 + } + } +} + +/// A handle to a shown notification. +/// +/// This keeps a connection alive to ensure actions work on certain desktops. +#[derive(Debug)] +pub struct ZbusNotificationHandle { + pub(crate) id: u32, + pub(crate) connection: zbus::Connection, + pub(crate) notification: Notification, +} + +impl ZbusNotificationHandle { + pub(crate) fn new( + id: u32, + connection: zbus::Connection, + notification: Notification, + ) -> ZbusNotificationHandle { + ZbusNotificationHandle { + id, + connection, + notification, + } + } + + pub async fn wait_for_action(self, invocation_closure: impl ActionResponseHandler) { + wait_for_action_signal(&self.connection, self.id, invocation_closure).await; + } + + pub async fn close_fallible(self) -> Result<()> { + self.connection + .call_method( + Some(self.notification.bus.clone().into_name()), + xdg::NOTIFICATION_OBJECTPATH, + Some(xdg::NOTIFICATION_INTERFACE), + "CloseNotification", + &(self.id), + ) + .await?; + Ok(()) + } + + pub async fn close(self) { + self.close_fallible().await.unwrap(); + } + + pub fn on_close(self, closure: F) + where + F: FnOnce(CloseReason), + { + zbus::block_on(self.wait_for_action(|action: &ActionResponse| { + if let ActionResponse::Closed(reason) = action { + closure(*reason); + } + })); + } + + pub fn update_fallible(&mut self) -> Result<()> { + self.id = zbus::block_on(send_notification_via_connection( + &self.notification, + self.id, + &self.connection, + ))?; + Ok(()) + } + + pub fn update(&mut self) { + self.update_fallible().unwrap(); + } +} + +async fn send_notification_via_connection( + notification: &Notification, + id: u32, + connection: &zbus::Connection, +) -> Result { + send_notification_via_connection_at_bus(notification, id, connection, Default::default()).await +} + +async fn send_notification_via_connection_at_bus( + notification: &Notification, + id: u32, + connection: &zbus::Connection, + bus: NotificationBus, +) -> Result { + let reply: u32 = connection + .call_method( + Some(bus.into_name()), + xdg::NOTIFICATION_OBJECTPATH, + Some(xdg::NOTIFICATION_INTERFACE), + "Notify", + &( + ¬ification.appname, + id, + ¬ification.icon, + ¬ification.summary, + ¬ification.body, + ¬ification.actions, + super::super::hints::hints_to_map(notification), + i32::from(notification.timeout), + ), + ) + .await? + .body() + .deserialize()?; + Ok(reply) +} + +pub async fn connect_and_send_notification( + notification: &Notification, +) -> Result { + let bus = notification.bus.clone(); + connect_and_send_notification_at_bus(notification, bus).await +} + +pub(crate) async fn connect_and_send_notification_at_bus( + notification: &Notification, + bus: NotificationBus, +) -> Result { + let connection = zbus::Connection::session().await?; + let inner_id = notification.id.unwrap_or(0); + let id = + send_notification_via_connection_at_bus(notification, inner_id, &connection, bus).await?; + + Ok(ZbusNotificationHandle::new( + id, + connection, + notification.clone(), + )) +} + +pub async fn get_capabilities_at_bus(bus: NotificationBus) -> Result> { + let connection = zbus::Connection::session().await?; + let info: Vec = connection + .call_method( + Some(bus.into_name()), + xdg::NOTIFICATION_OBJECTPATH, + Some(xdg::NOTIFICATION_INTERFACE), + "GetCapabilities", + &(), + ) + .await? + .body() + .deserialize()?; + Ok(info) +} + +pub async fn get_capabilities() -> Result> { + get_capabilities_at_bus(Default::default()).await +} + +pub async fn get_server_information_at_bus(bus: NotificationBus) -> Result { + let connection = zbus::Connection::session().await?; + let info: xdg::ServerInformation = connection + .call_method( + Some(bus.into_name()), + xdg::NOTIFICATION_OBJECTPATH, + Some(xdg::NOTIFICATION_INTERFACE), + "GetServerInformation", + &(), + ) + .await? + .body() + .deserialize()?; + + Ok(info) +} + +pub async fn get_server_information() -> Result { + get_server_information_at_bus(Default::default()).await +} + +/// Listens for the `ActionInvoked(UInt32, String)` Signal. +/// +/// No need to use this, check out `Notification::show_and_wait_for_action(FnOnce(action:&str))` +pub async fn handle_action(id: u32, func: impl ActionResponseHandler) { + let connection = zbus::Connection::session().await.unwrap(); + wait_for_action_signal(&connection, id, func).await; +} + +async fn wait_for_action_signal( + connection: &zbus::Connection, + id: u32, + handler: impl ActionResponseHandler, +) { + let action_signal_rule = MatchRule::builder() + .msg_type(zbus::MessageType::Signal) + .interface(xdg::NOTIFICATION_INTERFACE) + .unwrap() + .member("ActionInvoked") + .unwrap() + .build(); + + let proxy = zbus::fdo::DBusProxy::new(connection).await.unwrap(); + proxy.add_match_rule(action_signal_rule).await.unwrap(); + + let close_signal_rule = MatchRule::builder() + .msg_type(zbus::MessageType::Signal) + .interface(xdg::NOTIFICATION_INTERFACE) + .unwrap() + .member("NotificationClosed") + .unwrap() + .build(); + proxy.add_match_rule(close_signal_rule).await.unwrap(); + + while let Ok(Some(msg)) = zbus::MessageStream::from(connection).try_next().await { + let header = msg.header(); + if let zbus::MessageType::Signal = header.message_type() { + match header.member() { + Some(name) if name == "ActionInvoked" => { + match msg.body().deserialize::<(u32, String)>() { + Ok((nid, action)) if nid == id => { + handler.call(&ActionResponse::Custom(&action)); + break; + } + _ => {} + } + } + Some(name) if name == "NotificationClosed" => { + match msg.body().deserialize::<(u32, u32)>() { + Ok((nid, reason)) if nid == id => { + handler.call(&ActionResponse::Closed(reason.into())); + break; + } + _ => {} + } + } + _ => {} + } + } + } +}