1use std::{fmt, os::fd::AsFd, str::FromStr};
54
55use futures_util::Stream;
56use serde::{self, Deserialize, Serialize};
57use zbus::zvariant::{DeserializeDict, OwnedValue, SerializeDict, Type, Value};
58
59use super::Icon;
60use crate::{proxy::Proxy, Error};
61
62#[derive(Debug, Clone, PartialEq, Eq, Type)]
63#[zvariant(signature = "s")]
64pub enum Category {
66 #[doc(alias = "im.message")]
68 ImMessage,
69 #[doc(alias = "alarm.ringing")]
71 AlarmRinging,
72 #[doc(alias = "call.incoming")]
74 IncomingCall,
75 #[doc(alias = "call.ongoing")]
77 OngoingCall,
78 #[doc(alias = "call.missed")]
80 MissedCall,
81 #[doc(alias = "weather.warning.extreme")]
83 ExtremeWeather,
84 #[doc(alias = "cellbroadcast.danger.extreme")]
86 CellNetworkExtremeDanger,
87 #[doc(alias = "cellbroadcast.danger.severe")]
89 CellNetworkSevereDanger,
90 #[doc(alias = "cellbroadcast.amber-alert")]
92 CellNetworkAmberAlert,
93 #[doc(alias = "cellbroadcast.test")]
95 CellNetworkBroadcastTest,
96 #[doc(alias = "os.battery.low")]
98 LowBattery,
99 #[doc(alias = "browser.web-notification")]
101 WebNotification,
102 Other(String),
104}
105
106impl Serialize for Category {
107 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
108 where
109 S: serde::Serializer,
110 {
111 let category_str = match self {
112 Self::ImMessage => "im.message",
113 Self::AlarmRinging => "alarm.ringing",
114 Self::IncomingCall => "call.incoming",
115 Self::OngoingCall => "call.ongoing",
116 Self::MissedCall => "call.missed",
117 Self::ExtremeWeather => "weather.warning.extreme",
118 Self::CellNetworkExtremeDanger => "cellbroadcast.danger.extreme",
119 Self::CellNetworkSevereDanger => "cellbroadcast.danger.severe",
120 Self::CellNetworkAmberAlert => "cellbroadcast.amber-alert",
121 Self::CellNetworkBroadcastTest => "cellbroadcast.test",
122 Self::LowBattery => "os.battery.low",
123 Self::WebNotification => "browser.web-notification",
124 Self::Other(other) => other.as_str(),
125 };
126 serializer.serialize_str(category_str)
127 }
128}
129
130impl FromStr for Category {
131 type Err = crate::Error;
132
133 fn from_str(s: &str) -> Result<Self, Self::Err> {
134 match s {
135 "im.message" => Ok(Self::ImMessage),
136 "alarm.ringing" => Ok(Self::AlarmRinging),
137 "call.incoming" => Ok(Self::IncomingCall),
138 "call.ongoing" => Ok(Self::OngoingCall),
139 "call.missed" => Ok(Self::MissedCall),
140 "weather.warning.extreme" => Ok(Self::ExtremeWeather),
141 "cellbroadcast.danger.extreme" => Ok(Self::CellNetworkExtremeDanger),
142 "cellbroadcast.danger.severe" => Ok(Self::CellNetworkSevereDanger),
143 "cellbroadcast.amber-alert" => Ok(Self::CellNetworkAmberAlert),
144 "cellbroadcast.test" => Ok(Self::CellNetworkBroadcastTest),
145 "os.battery.low" => Ok(Self::LowBattery),
146 "browser.web-notification" => Ok(Self::WebNotification),
147 _ => Ok(Self::Other(s.to_owned())),
148 }
149 }
150}
151
152impl<'de> Deserialize<'de> for Category {
153 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
154 where
155 D: serde::Deserializer<'de>,
156 {
157 let category = String::deserialize(deserializer)?;
158 category
159 .parse::<Self>()
160 .map_err(|_e| serde::de::Error::custom("Failed to parse category"))
161 }
162}
163
164#[cfg_attr(feature = "glib", derive(glib::Enum))]
165#[cfg_attr(feature = "glib", enum_type(name = "AshpdPriority"))]
166#[derive(Debug, Copy, Clone, Serialize, PartialEq, Eq, Type)]
167#[zvariant(signature = "s")]
168#[serde(rename_all = "lowercase")]
169pub enum Priority {
171 Low,
173 Normal,
175 High,
177 Urgent,
179}
180
181impl fmt::Display for Priority {
182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183 match self {
184 Self::Low => write!(f, "Low"),
185 Self::Normal => write!(f, "Normal"),
186 Self::High => write!(f, "High"),
187 Self::Urgent => write!(f, "Urgent"),
188 }
189 }
190}
191
192impl AsRef<str> for Priority {
193 fn as_ref(&self) -> &str {
194 match self {
195 Self::Low => "Low",
196 Self::Normal => "Normal",
197 Self::High => "High",
198 Self::Urgent => "Urgent",
199 }
200 }
201}
202
203impl From<Priority> for &'static str {
204 fn from(d: Priority) -> Self {
205 match d {
206 Priority::Low => "Low",
207 Priority::Normal => "Normal",
208 Priority::High => "High",
209 Priority::Urgent => "Urgent",
210 }
211 }
212}
213
214impl FromStr for Priority {
215 type Err = Error;
216
217 fn from_str(s: &str) -> Result<Self, Self::Err> {
218 match s {
219 "Low" | "low" => Ok(Priority::Low),
220 "Normal" | "normal" => Ok(Priority::Normal),
221 "High" | "high" => Ok(Priority::High),
222 "Urgent" | "urgent" => Ok(Priority::Urgent),
223 _ => Err(Error::ParseError("Failed to parse priority, invalid value")),
224 }
225 }
226}
227
228#[cfg_attr(feature = "glib", derive(glib::Enum))]
229#[cfg_attr(feature = "glib", enum_type(name = "AshpdNotificationDisplayHint"))]
230#[derive(Debug, Copy, Clone, PartialEq, Eq, Type)]
231#[zvariant(signature = "s")]
232pub enum DisplayHint {
234 #[doc(alias = "transient")]
236 Transient,
237 #[doc(alias = "tray")]
239 Tray,
240 #[doc(alias = "persistent")]
242 Persistent,
243 #[doc(alias = "hide-on-lockscreen")]
245 HideOnLockScreen,
246 #[doc(alias = "hide-content-on-lockscreen")]
248 HideContentOnLockScreen,
249 #[doc(alias = "show-as-new")]
251 ShowAsNew,
252}
253
254impl Serialize for DisplayHint {
255 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
256 where
257 S: serde::Serializer,
258 {
259 let purpose = match self {
260 Self::Transient => "transient",
261 Self::Tray => "tray",
262 Self::Persistent => "persistent",
263 Self::HideOnLockScreen => "hide-on-lockscreen",
264 Self::HideContentOnLockScreen => "hide-content-on-lockscreen",
265 Self::ShowAsNew => "show-as-new",
266 };
267 serializer.serialize_str(purpose)
268 }
269}
270
271#[derive(SerializeDict, Type, Debug)]
272#[zvariant(signature = "dict")]
274pub struct Notification {
275 title: String,
277 body: Option<String>,
279 #[zvariant(rename = "markup-body")]
280 markup_body: Option<String>,
281 icon: Option<Icon>,
283 priority: Option<Priority>,
285 #[zvariant(rename = "default-action")]
288 default_action: Option<String>,
289 #[zvariant(rename = "default-action-target")]
291 default_action_target: Option<OwnedValue>,
292 buttons: Option<Vec<Button>>,
294 category: Option<Category>,
295 #[zvariant(rename = "display-hint")]
296 display_hints: Option<Vec<DisplayHint>>,
297 sound: Option<OwnedValue>,
298}
299
300impl Notification {
301 pub fn new(title: &str) -> Self {
307 Self {
308 title: title.to_owned(),
309 body: None,
310 markup_body: None,
311 priority: None,
312 icon: None,
313 default_action: None,
314 default_action_target: None,
315 buttons: None,
316 category: None,
317 display_hints: None,
318 sound: None,
319 }
320 }
321
322 #[must_use]
324 pub fn body<'a>(mut self, body: impl Into<Option<&'a str>>) -> Self {
325 self.body = body.into().map(ToOwned::to_owned);
326 self
327 }
328
329 #[must_use]
331 pub fn markup_body<'a>(mut self, markup_body: impl Into<Option<&'a str>>) -> Self {
332 self.markup_body = markup_body.into().map(ToOwned::to_owned);
333 self
334 }
335
336 #[must_use]
338 pub fn icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
339 self.icon = icon.into();
340 self
341 }
342
343 #[must_use]
345 pub fn sound<S>(mut self, sound: impl Into<Option<S>>) -> Self
346 where
347 S: AsFd,
348 {
349 self.sound = sound.into().map(|s| {
350 zbus::zvariant::Value::from(zbus::zvariant::Fd::from(s.as_fd()))
351 .try_to_owned()
352 .unwrap()
353 });
354 self
355 }
356
357 #[must_use]
359 pub fn category(mut self, category: impl Into<Option<Category>>) -> Self {
360 self.category = category.into();
361 self
362 }
363
364 #[must_use]
365 pub fn display_hint(mut self, hints: impl IntoIterator<Item = DisplayHint>) -> Self {
367 self.display_hints = Some(hints.into_iter().collect());
368 self
369 }
370
371 #[must_use]
373 pub fn priority(mut self, priority: impl Into<Option<Priority>>) -> Self {
374 self.priority = priority.into();
375 self
376 }
377
378 #[must_use]
380 pub fn default_action<'a>(mut self, default_action: impl Into<Option<&'a str>>) -> Self {
381 self.default_action = default_action.into().map(ToOwned::to_owned);
382 self
383 }
384
385 #[must_use]
387 pub fn default_action_target<'a, T: Into<Value<'a>>>(
388 mut self,
389 default_action_target: impl Into<Option<T>>,
390 ) -> Self {
391 self.default_action_target = default_action_target
392 .into()
393 .map(|t| t.into().try_to_owned().unwrap());
394 self
395 }
396
397 #[must_use]
399 pub fn button(mut self, button: Button) -> Self {
400 match self.buttons {
401 Some(ref mut buttons) => buttons.push(button),
402 None => {
403 self.buttons.replace(vec![button]);
404 }
405 };
406 self
407 }
408}
409
410#[derive(Debug, Clone, PartialEq, Eq, Type)]
411#[zvariant(signature = "s")]
412pub enum ButtonPurpose {
414 #[doc(alias = "im.reply-with-text")]
416 ImReplyWithText,
417 #[doc(alias = "call.accept")]
419 CallAccept,
420 #[doc(alias = "call.decline")]
422 CallDecline,
423 #[doc(alias = "call.hang-up")]
425 CallHangup,
426 #[doc(alias = "call.enable-speakerphone")]
428 CallEnableSpeakerphone,
429 #[doc(alias = "call.disable-speakerphone")]
431 CallDisableSpeakerphone,
432 #[doc(alias = "system.custom-alert")]
434 SystemCustomAlert,
435 Other(String),
437}
438
439impl Serialize for ButtonPurpose {
440 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
441 where
442 S: serde::Serializer,
443 {
444 let purpose = match self {
445 Self::ImReplyWithText => "im.reply-with-text",
446 Self::CallAccept => "call.accept",
447 Self::CallDecline => "call.decline",
448 Self::CallHangup => "call.hang-up",
449 Self::CallEnableSpeakerphone => "call.enable-speakerphone",
450 Self::CallDisableSpeakerphone => "call.disable-speakerphone",
451 Self::SystemCustomAlert => "system.custom-alert",
452 Self::Other(other) => other.as_str(),
453 };
454 serializer.serialize_str(purpose)
455 }
456}
457
458impl FromStr for ButtonPurpose {
459 type Err = crate::Error;
460
461 fn from_str(s: &str) -> Result<Self, Self::Err> {
462 match s {
463 "im.reply-with-text" => Ok(Self::ImReplyWithText),
464 "call.accept" => Ok(Self::CallAccept),
465 "call.decline" => Ok(Self::CallDecline),
466 "call.hang-up" => Ok(Self::CallHangup),
467 "call.enable-speakerphone" => Ok(Self::CallEnableSpeakerphone),
468 "call.disable-speakerphone" => Ok(Self::CallDisableSpeakerphone),
469 "system.custom-alert" => Ok(Self::SystemCustomAlert),
470 _ => Ok(Self::Other(s.to_owned())),
471 }
472 }
473}
474
475impl<'de> Deserialize<'de> for ButtonPurpose {
476 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
477 where
478 D: serde::Deserializer<'de>,
479 {
480 let purpose = String::deserialize(deserializer)?;
481 purpose
482 .parse::<Self>()
483 .map_err(|_e| serde::de::Error::custom("Failed to parse purpose"))
484 }
485}
486
487#[derive(SerializeDict, Type, Debug)]
488#[zvariant(signature = "dict")]
490pub struct Button {
491 label: String,
493 action: String,
496 target: Option<OwnedValue>,
498 purpose: Option<ButtonPurpose>,
499}
500
501impl Button {
502 pub fn new(label: &str, action: &str) -> Self {
510 Self {
511 label: label.to_owned(),
512 action: action.to_owned(),
513 target: None,
514 purpose: None,
515 }
516 }
517
518 #[must_use]
520 pub fn target<'a, T: Into<Value<'a>>>(mut self, target: impl Into<Option<T>>) -> Self {
521 self.target = target.into().map(|t| t.into().try_to_owned().unwrap());
522 self
523 }
524
525 #[must_use]
527 pub fn purpose(mut self, purpose: impl Into<Option<ButtonPurpose>>) -> Self {
528 self.purpose = purpose.into();
529 self
530 }
531}
532
533#[derive(Debug, Deserialize, Type)]
534pub struct Action(String, String, Vec<OwnedValue>);
536
537impl Action {
538 pub fn id(&self) -> &str {
540 &self.0
541 }
542
543 pub fn name(&self) -> &str {
545 &self.1
546 }
547
548 pub fn parameter(&self) -> &Vec<OwnedValue> {
550 &self.2
551 }
552}
553
554#[derive(DeserializeDict, Type, Debug, OwnedValue)]
555#[zvariant(signature = "dict")]
556struct SupportedOptions {
558 category: Vec<String>,
559 #[zvariant(rename = "button-purpose")]
560 button_purpose: Vec<String>,
561}
562
563#[derive(Debug)]
582#[doc(alias = "org.freedesktop.portal.Notification")]
583pub struct NotificationProxy<'a>(Proxy<'a>);
584
585impl<'a> NotificationProxy<'a> {
586 pub async fn new() -> Result<NotificationProxy<'a>, Error> {
588 let proxy = Proxy::new_desktop("org.freedesktop.portal.Notification").await?;
589 Ok(Self(proxy))
590 }
591
592 #[doc(alias = "ActionInvoked")]
598 #[doc(alias = "XdpPortal::notification-action-invoked")]
599 pub async fn receive_action_invoked(&self) -> Result<impl Stream<Item = Action>, Error> {
600 self.0.signal("ActionInvoked").await
601 }
602
603 #[doc(alias = "AddNotification")]
618 #[doc(alias = "xdp_portal_add_notification")]
619 pub async fn add_notification(
620 &self,
621 id: &str,
622 notification: Notification,
623 ) -> Result<(), Error> {
624 self.0.call("AddNotification", &(id, notification)).await
625 }
626
627 #[doc(alias = "RemoveNotification")]
637 #[doc(alias = "xdp_portal_remove_notification")]
638 pub async fn remove_notification(&self, id: &str) -> Result<(), Error> {
639 self.0.call("RemoveNotification", &(id)).await
640 }
641
642 pub async fn supported_options(&self) -> Result<(Vec<Category>, Vec<ButtonPurpose>), Error> {
653 let options = self
654 .0
655 .property_versioned::<SupportedOptions>("SupportedOptions", 2)
656 .await?;
657 let categories = options
658 .category
659 .into_iter()
660 .map(|c| Category::from_str(&c).unwrap())
661 .collect();
662 let purposes = options
663 .button_purpose
664 .into_iter()
665 .map(|c| ButtonPurpose::from_str(&c).unwrap())
666 .collect();
667 Ok((categories, purposes))
668 }
669}
670
671impl<'a> std::ops::Deref for NotificationProxy<'a> {
672 type Target = zbus::Proxy<'a>;
673
674 fn deref(&self) -> &Self::Target {
675 &self.0
676 }
677}