use std::{fmt, os::fd::AsFd, str::FromStr};
use futures_util::Stream;
use serde::{self, Deserialize, Serialize};
use zbus::zvariant::{
OwnedValue, Type, Value,
as_value::{self, optional},
};
use super::Icon;
use crate::{Error, proxy::Proxy};
#[derive(Debug, Clone, PartialEq, Eq, Type)]
#[zvariant(signature = "s")]
pub enum Category {
#[doc(alias = "im.message")]
ImMessage,
#[doc(alias = "alarm.ringing")]
AlarmRinging,
#[doc(alias = "call.incoming")]
IncomingCall,
#[doc(alias = "call.ongoing")]
OngoingCall,
#[doc(alias = "call.missed")]
MissedCall,
#[doc(alias = "weather.warning.extreme")]
ExtremeWeather,
#[doc(alias = "cellbroadcast.danger.extreme")]
CellNetworkExtremeDanger,
#[doc(alias = "cellbroadcast.danger.severe")]
CellNetworkSevereDanger,
#[doc(alias = "cellbroadcast.amber-alert")]
CellNetworkAmberAlert,
#[doc(alias = "cellbroadcast.test")]
CellNetworkBroadcastTest,
#[doc(alias = "os.battery.low")]
LowBattery,
#[doc(alias = "browser.web-notification")]
WebNotification,
Other(String),
}
impl Serialize for Category {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let category_str = match self {
Self::ImMessage => "im.message",
Self::AlarmRinging => "alarm.ringing",
Self::IncomingCall => "call.incoming",
Self::OngoingCall => "call.ongoing",
Self::MissedCall => "call.missed",
Self::ExtremeWeather => "weather.warning.extreme",
Self::CellNetworkExtremeDanger => "cellbroadcast.danger.extreme",
Self::CellNetworkSevereDanger => "cellbroadcast.danger.severe",
Self::CellNetworkAmberAlert => "cellbroadcast.amber-alert",
Self::CellNetworkBroadcastTest => "cellbroadcast.test",
Self::LowBattery => "os.battery.low",
Self::WebNotification => "browser.web-notification",
Self::Other(other) => other.as_str(),
};
serializer.serialize_str(category_str)
}
}
impl FromStr for Category {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"im.message" => Ok(Self::ImMessage),
"alarm.ringing" => Ok(Self::AlarmRinging),
"call.incoming" => Ok(Self::IncomingCall),
"call.ongoing" => Ok(Self::OngoingCall),
"call.missed" => Ok(Self::MissedCall),
"weather.warning.extreme" => Ok(Self::ExtremeWeather),
"cellbroadcast.danger.extreme" => Ok(Self::CellNetworkExtremeDanger),
"cellbroadcast.danger.severe" => Ok(Self::CellNetworkSevereDanger),
"cellbroadcast.amber-alert" => Ok(Self::CellNetworkAmberAlert),
"cellbroadcast.test" => Ok(Self::CellNetworkBroadcastTest),
"os.battery.low" => Ok(Self::LowBattery),
"browser.web-notification" => Ok(Self::WebNotification),
_ => Ok(Self::Other(s.to_owned())),
}
}
}
impl<'de> Deserialize<'de> for Category {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let category = String::deserialize(deserializer)?;
category
.parse::<Self>()
.map_err(|_e| serde::de::Error::custom("Failed to parse category"))
}
}
#[cfg_attr(feature = "glib", derive(glib::Enum))]
#[cfg_attr(feature = "glib", enum_type(name = "AshpdPriority"))]
#[derive(Debug, Copy, Clone, Serialize, PartialEq, Eq, Type)]
#[zvariant(signature = "s")]
#[serde(rename_all = "lowercase")]
pub enum Priority {
Low,
Normal,
High,
Urgent,
}
impl fmt::Display for Priority {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Low => write!(f, "Low"),
Self::Normal => write!(f, "Normal"),
Self::High => write!(f, "High"),
Self::Urgent => write!(f, "Urgent"),
}
}
}
impl AsRef<str> for Priority {
fn as_ref(&self) -> &str {
match self {
Self::Low => "Low",
Self::Normal => "Normal",
Self::High => "High",
Self::Urgent => "Urgent",
}
}
}
impl From<Priority> for &'static str {
fn from(d: Priority) -> Self {
match d {
Priority::Low => "Low",
Priority::Normal => "Normal",
Priority::High => "High",
Priority::Urgent => "Urgent",
}
}
}
impl FromStr for Priority {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Low" | "low" => Ok(Priority::Low),
"Normal" | "normal" => Ok(Priority::Normal),
"High" | "high" => Ok(Priority::High),
"Urgent" | "urgent" => Ok(Priority::Urgent),
_ => Err(Error::ParseError("Failed to parse priority, invalid value")),
}
}
}
#[cfg_attr(feature = "glib", derive(glib::Enum))]
#[cfg_attr(feature = "glib", enum_type(name = "AshpdNotificationDisplayHint"))]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Type, Serialize)]
#[zvariant(signature = "s")]
#[serde(rename_all = "kebab-case")]
pub enum DisplayHint {
#[doc(alias = "transient")]
Transient,
#[doc(alias = "tray")]
Tray,
#[doc(alias = "persistent")]
Persistent,
#[doc(alias = "hide-on-lockscreen")]
HideOnLockScreen,
#[doc(alias = "hide-content-on-lockscreen")]
HideContentOnLockScreen,
#[doc(alias = "show-as-new")]
ShowAsNew,
}
#[derive(Serialize, Type, Debug)]
#[zvariant(signature = "dict")]
#[serde(rename_all = "kebab-case")]
pub struct Notification {
#[serde(with = "as_value")]
title: String,
#[serde(with = "optional", skip_serializing_if = "Option::is_none")]
body: Option<String>,
#[serde(with = "optional", skip_serializing_if = "Option::is_none")]
markup_body: Option<String>,
#[serde(with = "optional", skip_serializing_if = "Option::is_none")]
icon: Option<Icon>,
#[serde(with = "optional", skip_serializing_if = "Option::is_none")]
priority: Option<Priority>,
#[serde(with = "optional", skip_serializing_if = "Option::is_none")]
default_action: Option<String>,
#[serde(with = "optional", skip_serializing_if = "Option::is_none")]
default_action_target: Option<OwnedValue>,
#[serde(with = "as_value", skip_serializing_if = "Vec::is_empty")]
buttons: Vec<Button>,
#[serde(with = "optional", skip_serializing_if = "Option::is_none")]
category: Option<Category>,
#[serde(with = "as_value", skip_serializing_if = "Vec::is_empty")]
display_hint: Vec<DisplayHint>,
#[serde(with = "optional", skip_serializing_if = "Option::is_none")]
sound: Option<OwnedValue>,
}
impl Notification {
pub fn new(title: &str) -> Self {
Self {
title: title.to_owned(),
body: None,
markup_body: None,
priority: None,
icon: None,
default_action: None,
default_action_target: None,
buttons: Vec::new(),
category: None,
display_hint: Vec::new(),
sound: None,
}
}
#[must_use]
pub fn body<'a>(mut self, body: impl Into<Option<&'a str>>) -> Self {
self.body = body.into().map(ToOwned::to_owned);
self
}
#[must_use]
pub fn markup_body<'a>(mut self, markup_body: impl Into<Option<&'a str>>) -> Self {
self.markup_body = markup_body.into().map(ToOwned::to_owned);
self
}
#[must_use]
pub fn icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
self.icon = icon.into();
self
}
#[must_use]
pub fn sound<S>(mut self, sound: impl Into<Option<S>>) -> Self
where
S: AsFd,
{
self.sound = sound.into().map(|s| {
zbus::zvariant::Value::from(zbus::zvariant::Fd::from(s.as_fd()))
.try_to_owned()
.unwrap()
});
self
}
#[must_use]
pub fn category(mut self, category: impl Into<Option<Category>>) -> Self {
self.category = category.into();
self
}
#[must_use]
pub fn display_hint(mut self, hints: impl IntoIterator<Item = DisplayHint>) -> Self {
self.display_hint = hints.into_iter().collect();
self
}
#[must_use]
pub fn priority(mut self, priority: impl Into<Option<Priority>>) -> Self {
self.priority = priority.into();
self
}
#[must_use]
pub fn default_action<'a>(mut self, default_action: impl Into<Option<&'a str>>) -> Self {
self.default_action = default_action.into().map(ToOwned::to_owned);
self
}
#[must_use]
pub fn default_action_target<'a, T: Into<Value<'a>>>(
mut self,
default_action_target: impl Into<Option<T>>,
) -> Self {
self.default_action_target = default_action_target
.into()
.map(|t| t.into().try_to_owned().unwrap());
self
}
#[must_use]
pub fn button(mut self, button: Button) -> Self {
self.buttons.push(button);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Type)]
#[zvariant(signature = "s")]
pub enum ButtonPurpose {
#[doc(alias = "im.reply-with-text")]
ImReplyWithText,
#[doc(alias = "call.accept")]
CallAccept,
#[doc(alias = "call.decline")]
CallDecline,
#[doc(alias = "call.hang-up")]
CallHangup,
#[doc(alias = "call.enable-speakerphone")]
CallEnableSpeakerphone,
#[doc(alias = "call.disable-speakerphone")]
CallDisableSpeakerphone,
#[doc(alias = "system.custom-alert")]
SystemCustomAlert,
Other(String),
}
impl Serialize for ButtonPurpose {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let purpose = match self {
Self::ImReplyWithText => "im.reply-with-text",
Self::CallAccept => "call.accept",
Self::CallDecline => "call.decline",
Self::CallHangup => "call.hang-up",
Self::CallEnableSpeakerphone => "call.enable-speakerphone",
Self::CallDisableSpeakerphone => "call.disable-speakerphone",
Self::SystemCustomAlert => "system.custom-alert",
Self::Other(other) => other.as_str(),
};
serializer.serialize_str(purpose)
}
}
impl FromStr for ButtonPurpose {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"im.reply-with-text" => Ok(Self::ImReplyWithText),
"call.accept" => Ok(Self::CallAccept),
"call.decline" => Ok(Self::CallDecline),
"call.hang-up" => Ok(Self::CallHangup),
"call.enable-speakerphone" => Ok(Self::CallEnableSpeakerphone),
"call.disable-speakerphone" => Ok(Self::CallDisableSpeakerphone),
"system.custom-alert" => Ok(Self::SystemCustomAlert),
_ => Ok(Self::Other(s.to_owned())),
}
}
}
impl<'de> Deserialize<'de> for ButtonPurpose {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let purpose = String::deserialize(deserializer)?;
purpose
.parse::<Self>()
.map_err(|_e| serde::de::Error::custom("Failed to parse purpose"))
}
}
#[derive(Serialize, Type, Debug)]
#[zvariant(signature = "dict")]
pub struct Button {
#[serde(with = "as_value")]
label: String,
#[serde(with = "as_value")]
action: String,
#[serde(with = "optional", skip_serializing_if = "Option::is_none")]
target: Option<OwnedValue>,
#[serde(with = "optional", skip_serializing_if = "Option::is_none")]
purpose: Option<ButtonPurpose>,
}
impl Button {
pub fn new(label: &str, action: &str) -> Self {
Self {
label: label.to_owned(),
action: action.to_owned(),
target: None,
purpose: None,
}
}
#[must_use]
pub fn target<'a, T: Into<Value<'a>>>(mut self, target: impl Into<Option<T>>) -> Self {
self.target = target.into().map(|t| t.into().try_to_owned().unwrap());
self
}
#[must_use]
pub fn purpose(mut self, purpose: impl Into<Option<ButtonPurpose>>) -> Self {
self.purpose = purpose.into();
self
}
}
#[derive(Debug, Deserialize, Type)]
pub struct Action(String, String, Vec<OwnedValue>);
impl Action {
pub fn id(&self) -> &str {
&self.0
}
pub fn name(&self) -> &str {
&self.1
}
pub fn parameter(&self) -> &[OwnedValue] {
&self.2
}
}
#[derive(Deserialize, Type, Debug, OwnedValue)]
#[zvariant(signature = "dict")]
#[serde(rename_all = "kebab-case")]
struct SupportedOptions {
#[serde(default, with = "as_value")]
category: Vec<String>,
#[serde(default, with = "as_value")]
button_purpose: Vec<String>,
}
#[derive(Debug)]
#[doc(alias = "org.freedesktop.portal.Notification")]
pub struct NotificationProxy(Proxy<'static>);
impl NotificationProxy {
pub async fn new() -> Result<Self, Error> {
let proxy = Proxy::new_desktop("org.freedesktop.portal.Notification").await?;
Ok(Self(proxy))
}
pub async fn with_connection(connection: zbus::Connection) -> Result<Self, Error> {
let proxy =
Proxy::new_desktop_with_connection(connection, "org.freedesktop.portal.Notification")
.await?;
Ok(Self(proxy))
}
pub fn version(&self) -> u32 {
self.0.version()
}
#[doc(alias = "ActionInvoked")]
#[doc(alias = "XdpPortal::notification-action-invoked")]
pub async fn receive_action_invoked(&self) -> Result<impl Stream<Item = Action>, Error> {
self.0.signal("ActionInvoked").await
}
#[doc(alias = "AddNotification")]
#[doc(alias = "xdp_portal_add_notification")]
pub async fn add_notification(
&self,
id: &str,
notification: Notification,
) -> Result<(), Error> {
self.0.call("AddNotification", &(id, notification)).await
}
#[doc(alias = "RemoveNotification")]
#[doc(alias = "xdp_portal_remove_notification")]
pub async fn remove_notification(&self, id: &str) -> Result<(), Error> {
self.0.call("RemoveNotification", &(id)).await
}
#[doc(alias = "SupportedOptions")]
pub async fn supported_options(&self) -> Result<(Vec<Category>, Vec<ButtonPurpose>), Error> {
let options = self
.0
.property_versioned::<SupportedOptions>("SupportedOptions", 2)
.await?;
let categories = options
.category
.into_iter()
.map(|c| Category::from_str(&c).unwrap())
.collect();
let purposes = options
.button_purpose
.into_iter()
.map(|c| ButtonPurpose::from_str(&c).unwrap())
.collect();
Ok((categories, purposes))
}
}
impl std::ops::Deref for NotificationProxy {
type Target = zbus::Proxy<'static>;
fn deref(&self) -> &Self::Target {
&self.0
}
}