Notifications: Replace gtk app with iced applet

Currently this is just a pretty box that shows no notifications.
This commit is contained in:
13r0ck 2022-12-22 15:16:22 -07:00 committed by Ashley Wulber
parent 57deb0999b
commit 3ae4eb635a
10 changed files with 3425 additions and 916 deletions

View file

@ -1,6 +1,5 @@
[workspace]
members = [
"applets/cosmic-applet-notifications",
"applets/cosmic-applet-status-area",
"applets/cosmic-panel-button",
"libcosmic-applet",
@ -14,6 +13,7 @@ exclude = [
"applets/cosmic-applet-power",
"applets/cosmic-applet-time",
"applets/cosmic-app-list",
"applets/cosmic-applet-notifications",
]
[patch.crates-io]

File diff suppressed because it is too large Load diff

View file

@ -5,16 +5,6 @@ edition = "2021"
license = "GPL-3.0-or-later"
[dependencies]
cascade = "1"
futures = "0.3"
gtk4 = { git = "https://github.com/gtk-rs/gtk4-rs" }
adw = { git = "https://gitlab.gnome.org/World/Rust/libadwaita-rs", package = "libadwaita"}
libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false }
libcosmic-applet = { path = "../../libcosmic-applet" }
once_cell = "1.12"
relm4-macros = { git = "https://github.com/Relm4/Relm4.git", branch = "next" }
serde = "1"
zbus = "2.0.1"
zbus_names = "2"
zvariant = "3"
cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", features = ["gtk4"] }
icon-loader = { version = "0.3.6", features = ["gtk"] }
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["wayland", "applet"] }
nix = "0.24.1"

View file

@ -1,53 +0,0 @@
use futures::prelude::*;
use gtk4::glib::{self, clone};
use std::cell::Cell;
use zbus::fdo::{DBusProxy, RequestNameFlags, RequestNameReply};
use zbus_names::WellKnownName;
pub async fn create<
F: Fn(zbus::ConnectionBuilder<'static>) -> zbus::Result<zbus::ConnectionBuilder<'static>>,
>(
well_known_name: &'static str,
serve_cb: F,
) -> zbus::Result<zbus::Connection> {
let well_known_name = WellKnownName::try_from(well_known_name)?;
let connection = serve_cb(zbus::ConnectionBuilder::session()?)?
.build()
.await?;
let dbus_proxy = DBusProxy::new(&connection).await?;
let mut name_owner_changed_stream = dbus_proxy.receive_name_owner_changed().await?;
let flags = RequestNameFlags::AllowReplacement.into();
match dbus_proxy
.request_name(well_known_name.as_ref(), flags)
.await?
{
RequestNameReply::InQueue => {
eprintln!("Bus name '{}' already owned", well_known_name);
}
_ => {}
}
glib::MainContext::default().spawn_local(clone!(@strong connection => async move {
let have_bus_name = Cell::new(false);
let unique_name = connection.unique_name().map(|x| x.as_ref());
while let Some(evt) = name_owner_changed_stream.next().await {
let args = match evt.args() {
Ok(args) => args,
Err(_) => { continue; },
};
if args.name.as_ref() == well_known_name {
if args.new_owner.as_ref() == unique_name.as_ref() {
eprintln!("Acquired bus name: {}", well_known_name);
have_bus_name.set(true);
} else if have_bus_name.get() {
eprintln!("Lost bus name: {}", well_known_name);
have_bus_name.set(false);
}
}
}
}));
Ok(connection)
}

View file

@ -1,31 +0,0 @@
use once_cell::unsync::OnceCell;
/// Wrapper around `OnceCell` implementing `Deref`, and thus also panicking
/// when not set (or set twice).
///
/// To be used in place of `gtk::TemplateChild`, but without xml.
pub struct DerefCell<T>(OnceCell<T>);
impl<T> DerefCell<T> {
#[track_caller]
pub fn set(&self, value: T) {
if self.0.set(value).is_err() {
panic!("Initialized twice");
}
}
}
impl<T> Default for DerefCell<T> {
fn default() -> Self {
Self(OnceCell::default())
}
}
impl<T> std::ops::Deref for DerefCell<T> {
type Target = T;
#[track_caller]
fn deref(&self) -> &T {
self.0.get().unwrap()
}
}

View file

@ -1,49 +1,189 @@
use gtk4::{glib, prelude::*, PositionType};
use relm4_macros::view;
use cosmic_panel_config::PanelAnchor;
use cosmic::applet::CosmicAppletHelper;
use cosmic::iced::wayland::{
popup::{destroy_popup, get_popup},
SurfaceIdWrapper,
};
use cosmic::iced::{
executor,
widget::{button, column, horizontal_rule, row, text, Row, Space},
window, Alignment, Application, Color, Command, Length, Subscription,
};
mod dbus_service;
mod deref_cell;
mod notification_popover;
use notification_popover::NotificationPopover;
mod notification_list;
mod notification_widget;
use notification_list::NotificationList;
mod notifications;
use notifications::Notifications;
use cosmic::iced_style::application::{self, Appearance};
use cosmic::iced_style::svg;
use cosmic::theme::{self, Svg};
use cosmic::widget::icon;
use cosmic::widget::toggler;
use cosmic::Renderer;
use cosmic::{Element, Theme};
fn main() {
let _monitors = libcosmic::init();
use std::process;
// XXX Implement DBus service somewhere other than applet?
let notifications = Notifications::new();
pub fn main() -> cosmic::iced::Result {
let helper = CosmicAppletHelper::default();
Notifications::run(helper.window_settings())
}
let notification_list = NotificationList::new(&notifications);
#[derive(Default)]
struct Notifications {
applet_helper: CosmicAppletHelper,
theme: Theme,
icon_name: String,
popup: Option<window::Id>,
id_ctr: u32,
do_not_disturb: bool,
notifications: Vec<Vec<String>>,
}
view! {
window = libcosmic_applet::AppletWindow {
#[wrap(Some)]
set_child: applet_button = &libcosmic_applet::AppletButton {
set_button_icon_name: "user-invisible-symbolic", // TODO
set_popover_child: Some(&notification_list)
#[derive(Debug, Clone)]
enum Message {
TogglePopup,
DoNotDisturb(bool),
Settings,
Ignore,
}
impl Application for Notifications {
type Message = Message;
type Theme = Theme;
type Executor = executor::Default;
type Flags = ();
fn new(_flags: ()) -> (Notifications, Command<Message>) {
(
Notifications {
icon_name: "notification-alert-symbolic".to_string(),
..Default::default()
},
Command::none(),
)
}
fn title(&self) -> String {
String::from("Notifications")
}
fn theme(&self) -> Theme {
self.theme
}
fn close_requested(&self, _id: SurfaceIdWrapper) -> Self::Message {
Message::Ignore
}
fn style(&self) -> <Self::Theme as application::StyleSheet>::Style {
<Self::Theme as application::StyleSheet>::Style::Custom(|theme| Appearance {
background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
text_color: theme.cosmic().on_bg_color().into(),
})
}
fn subscription(&self) -> Subscription<Message> {
Subscription::none()
}
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::TogglePopup => {
if let Some(p) = self.popup.take() {
destroy_popup(p)
} else {
self.id_ctr += 1;
let new_id = window::Id::new(self.id_ctr);
self.popup.replace(new_id);
let popup_settings = self.applet_helper.get_popup_settings(
window::Id::new(0),
new_id,
(400, 300),
Some(60),
None,
);
get_popup(popup_settings)
}
}
Message::DoNotDisturb(b) => {
self.do_not_disturb = b;
Command::none()
}
Message::Settings => {
let _ = process::Command::new("cosmic-settings notifications").spawn();
Command::none()
}
Message::Ignore => Command::none(),
}
}
fn view(&self, id: SurfaceIdWrapper) -> Element<Message> {
match id {
SurfaceIdWrapper::LayerSurface(_) => unimplemented!(),
SurfaceIdWrapper::Window(_) => self
.applet_helper
.icon_button(&self.icon_name)
.on_press(Message::TogglePopup)
.into(),
SurfaceIdWrapper::Popup(_) => {
let do_not_disturb =
row![
toggler(String::from("Do Not Disturb"), self.do_not_disturb, |b| {
Message::DoNotDisturb(b)
})
.width(Length::Fill)
]
.padding([0, 24]);
let settings =
row_button(vec!["Notification Settings...".into()]).on_press(Message::Settings);
let notifications = if self.notifications.len() == 0 {
row![
Space::with_width(Length::Fill),
column![text_icon(&self.icon_name, 40), "No Notifications"]
.align_items(Alignment::Center),
Space::with_width(Length::Fill)
]
.spacing(12)
} else {
row![text("TODO: make app worky with notifications")]
};
let main_content = column![horizontal_rule(1), notifications, horizontal_rule(1)]
.padding([0, 24])
.spacing(12);
let content = column![]
.align_items(Alignment::Start)
.spacing(12)
.padding([12, 0])
.push(do_not_disturb)
.push(main_content)
.push(settings);
self.applet_helper.popup_container(content).into()
}
}
}
window.show();
// XXX show in correct place
let position = std::env::var("COSMIC_PANEL_ANCHOR")
.ok()
.and_then(|anchor| anchor.parse::<PanelAnchor>().ok())
.map(|anchor| match anchor {
PanelAnchor::Left => PositionType::Right,
PanelAnchor::Right => PositionType::Left,
PanelAnchor::Top => PositionType::Bottom,
PanelAnchor::Bottom => PositionType::Top,
});
let notification_popover = NotificationPopover::new(&notifications, position);
notification_popover.set_parent(&applet_button);
let main_loop = glib::MainLoop::new(None, false);
main_loop.run();
}
// todo put into libcosmic doing so will fix the row_button's boarder radius
fn row_button(
mut content: Vec<Element<Message>>,
) -> cosmic::iced_native::widget::Button<Message, Renderer> {
content.insert(0, Space::with_width(Length::Units(24)).into());
content.push(Space::with_width(Length::Units(24)).into());
button(
Row::with_children(content)
.spacing(5)
.align_items(Alignment::Center),
)
.width(Length::Fill)
.height(Length::Units(35))
.style(theme::Button::Text)
}
fn text_icon(name: &str, size: u16) -> cosmic::widget::Icon {
icon(name, size).style(Svg::Custom(|theme| svg::Appearance {
color: Some(theme.palette().text),
}))
}

View file

@ -1,113 +0,0 @@
use cascade::cascade;
use gtk4::{
glib::{self, clone},
prelude::*,
subclass::prelude::*,
};
use std::{cell::RefCell, collections::HashMap};
use crate::deref_cell::DerefCell;
use crate::notification_widget::NotificationWidget;
use crate::notifications::{Notification, NotificationId, Notifications};
#[derive(Default)]
pub struct NotificationListInner {
listbox: DerefCell<gtk4::ListBox>,
notifications: DerefCell<Notifications>,
ids: RefCell<Vec<glib::SignalHandlerId>>,
rows: RefCell<HashMap<NotificationId, gtk4::ListBoxRow>>,
}
#[glib::object_subclass]
impl ObjectSubclass for NotificationListInner {
const NAME: &'static str = "S76NotificationList";
type ParentType = gtk4::Widget;
type Type = NotificationList;
fn class_init(klass: &mut Self::Class) {
klass.set_layout_manager_type::<gtk4::BinLayout>();
}
}
impl ObjectImpl for NotificationListInner {
fn constructed(&self, obj: &NotificationList) {
let listbox = cascade! {
gtk4::ListBox::new();
..set_parent(obj);
..connect_row_activated(clone!(@weak obj => move |_, row| {
if let Some(id) = obj.id_for_row(row) {
let notifications = obj.inner().notifications.clone();
glib::MainContext::default().spawn_local(async move {
notifications.invoke_action(id, "default").await;
});
}
}));
};
self.listbox.set(listbox);
}
fn dispose(&self, obj: &NotificationList) {
self.listbox.unparent();
for i in obj.inner().ids.take().into_iter() {
obj.inner().notifications.disconnect(i);
}
}
}
impl WidgetImpl for NotificationListInner {}
glib::wrapper! {
pub struct NotificationList(ObjectSubclass<NotificationListInner>)
@extends gtk4::Widget;
}
impl NotificationList {
pub fn new(notifications: &Notifications) -> Self {
let obj = glib::Object::new::<Self>(&[]).unwrap();
obj.inner().notifications.set(notifications.clone());
*obj.inner().ids.borrow_mut() = vec![
notifications.connect_notification_received(clone!(@weak obj => move |notification| {
obj.handle_notification(&notification);
})),
notifications.connect_notification_closed(clone!(@weak obj => move |id| {
obj.remove_notification(id);
})),
];
obj
}
fn inner(&self) -> &NotificationListInner {
NotificationListInner::from_instance(self)
}
fn handle_notification(&self, notification: &Notification) {
let notification_widget = cascade! {
NotificationWidget::new(&*self.inner().notifications);
..set_notification(notification);
};
let row = cascade! {
gtk4::ListBoxRow::new();
..set_selectable(false);
..set_child(Some(&notification_widget));
};
self.inner().listbox.prepend(&row);
self.inner().rows.borrow_mut().insert(notification.id, row);
}
fn remove_notification(&self, id: NotificationId) {
if let Some(row) = self.inner().rows.borrow_mut().remove(&id) {
self.inner().listbox.remove(&row);
}
}
fn id_for_row(&self, row: &gtk4::ListBoxRow) -> Option<NotificationId> {
let rows = self.inner().rows.borrow();
Some(*rows.iter().find(|(_, i)| i == &row)?.0)
}
}

View file

@ -1,138 +0,0 @@
use cascade::cascade;
use gtk4::{
glib::{self, clone},
prelude::*,
subclass::prelude::*, PositionType,
};
use std::cell::RefCell;
use crate::deref_cell::DerefCell;
use crate::notification_widget::NotificationWidget;
use crate::notifications::{Notification, NotificationId, Notifications};
#[derive(Default)]
pub struct NotificationPopoverInner {
notification_widget: DerefCell<NotificationWidget>,
notifications: DerefCell<Notifications>,
ids: RefCell<Vec<glib::SignalHandlerId>>,
source: RefCell<Option<glib::SourceId>>,
}
#[glib::object_subclass]
impl ObjectSubclass for NotificationPopoverInner {
const NAME: &'static str = "S76NotificationPopover";
type ParentType = gtk4::Popover;
type Type = NotificationPopover;
}
impl ObjectImpl for NotificationPopoverInner {
fn constructed(&self, obj: &NotificationPopover) {
cascade! {
obj;
..set_autohide(false);
..set_has_arrow(false);
..set_offset(0, 12);
..add_controller(&cascade! {
gtk4::GestureClick::new();
..connect_released(clone!(@weak obj => move |_, n_press, _, _| {
if n_press != 1 {
return;
}
if let Some(id) = obj.id() {
let notifications = obj.inner().notifications.clone();
glib::MainContext::default().spawn_local(async move {
notifications.invoke_action(id, "default").await;
});
}
obj.popdown();
}));
});
..add_controller(&cascade! {
gtk4::EventControllerMotion::new();
..connect_enter(clone!(@weak obj => move |_, _, _| {
obj.stop_timer();
}));
..connect_leave(clone!(@weak obj => move |_| {
obj.start_timer();
}));
});
};
}
fn dispose(&self, obj: &NotificationPopover) {
for i in obj.inner().ids.take().into_iter() {
obj.inner().notifications.disconnect(i);
}
}
}
impl WidgetImpl for NotificationPopoverInner {}
impl PopoverImpl for NotificationPopoverInner {}
glib::wrapper! {
pub struct NotificationPopover(ObjectSubclass<NotificationPopoverInner>)
@extends gtk4::Popover, gtk4::Widget;
}
impl NotificationPopover {
pub fn new(notifications: &Notifications, position: Option<PositionType>) -> Self {
let obj = glib::Object::new::<Self>(&[]).unwrap();
if let Some(position) = position {
obj.set_position(position);
}
let notification_widget = cascade! {
NotificationWidget::new(notifications);
};
obj.set_child(Some(&notification_widget));
obj.inner().notification_widget.set(notification_widget);
obj.inner().notifications.set(notifications.clone());
*obj.inner().ids.borrow_mut() = vec![
notifications.connect_notification_received(clone!(@weak obj => move |notification| {
obj.handle_notification(&notification);
})),
notifications.connect_notification_closed(clone!(@weak obj => move |id| {
if obj.id() == Some(id) {
obj.popdown();
}
})),
];
obj
}
fn inner(&self) -> &NotificationPopoverInner {
NotificationPopoverInner::from_instance(self)
}
fn id(&self) -> Option<NotificationId> {
self.inner().notification_widget.id()
}
fn handle_notification(&self, notification: &Notification) {
self.inner()
.notification_widget
.set_notification(notification);
self.popup();
self.start_timer();
}
fn stop_timer(&self) {
if let Some(source) = self.inner().source.borrow_mut().take() {
source.remove();
}
}
fn start_timer(&self) {
self.stop_timer();
let source = glib::timeout_add_seconds_local(
1,
clone!(@weak self as self_ => @default-return Continue(false), move || {
self_.popdown();
*self_.inner().source.borrow_mut() = None;
Continue(false)
}),
);
*self.inner().source.borrow_mut() = Some(source);
}
}

View file

@ -1,117 +0,0 @@
use cascade::cascade;
use gtk4::{
glib::{self, clone},
pango,
prelude::*,
subclass::prelude::*,
};
use std::cell::Cell;
use crate::deref_cell::DerefCell;
use crate::notifications::{Notification, NotificationId, Notifications};
#[derive(Default)]
pub struct NotificationWidgetInner {
box_: DerefCell<gtk4::Box>,
summary_label: DerefCell<gtk4::Label>,
body_label: DerefCell<gtk4::Label>,
notifications: DerefCell<Notifications>,
id: Cell<Option<NotificationId>>,
}
#[glib::object_subclass]
impl ObjectSubclass for NotificationWidgetInner {
const NAME: &'static str = "S76NotificationWidget";
type ParentType = gtk4::Widget;
type Type = NotificationWidget;
fn class_init(klass: &mut Self::Class) {
klass.set_layout_manager_type::<gtk4::BinLayout>();
}
}
impl ObjectImpl for NotificationWidgetInner {
fn constructed(&self, obj: &NotificationWidget) {
let summary_label = cascade! {
gtk4::Label::new(None);
..set_width_chars(20);
..set_max_width_chars(20);
..set_attributes(Some(&cascade! {
pango::AttrList::new();
..insert(pango::AttrInt::new_weight(pango::Weight::Bold));
}));
};
let body_label = cascade! {
gtk4::Label::new(None);
..set_width_chars(20);
..set_max_width_chars(20);
};
let box_ = cascade! {
gtk4::Box::new(gtk4::Orientation::Horizontal, 6);
..set_parent(obj);
..append(&cascade! {
gtk4::Box::new(gtk4::Orientation::Vertical, 0);
..append(&summary_label);
..append(&body_label);
});
..append(&cascade! {
gtk4::Button::new();
..style_context().add_provider(&cascade! {
gtk4::CssProvider::new();
..load_from_data(b"button { min-width: 0; min-height: 0; padding: 4px 4px; }");
}, gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION);
..style_context().add_class("flat");
..set_valign(gtk4::Align::Start);
..set_child(Some(&cascade! {
gtk4::Image::from_icon_name("window-close-symbolic");
..set_pixel_size(8);
}));
..connect_clicked(clone!(@weak obj => move |_| {
if let Some(id) = obj.id() {
obj.inner().notifications.dismiss(id);
}
}));
});
};
self.box_.set(box_);
self.summary_label.set(summary_label);
self.body_label.set(body_label);
}
fn dispose(&self, _obj: &NotificationWidget) {
self.box_.unparent();
}
}
impl WidgetImpl for NotificationWidgetInner {}
glib::wrapper! {
pub struct NotificationWidget(ObjectSubclass<NotificationWidgetInner>)
@extends gtk4::Widget;
}
impl NotificationWidget {
pub fn new(notifications: &Notifications) -> Self {
let obj = glib::Object::new::<Self>(&[]).unwrap();
obj.inner().notifications.set(notifications.clone());
obj
}
fn inner(&self) -> &NotificationWidgetInner {
NotificationWidgetInner::from_instance(self)
}
pub fn set_notification(&self, notification: &Notification) {
self.inner().summary_label.set_label(&notification.summary);
self.inner().body_label.set_label(&notification.body);
self.inner().id.set(Some(notification.id));
}
pub fn id(&self) -> Option<NotificationId> {
self.inner().id.get()
}
}

View file

@ -1,410 +0,0 @@
#![allow(non_snake_case)]
use futures::channel::mpsc;
use futures::stream::StreamExt;
use gtk4::{
glib::{self, clone, subclass::Signal, SignalHandlerId},
prelude::*,
subclass::prelude::*,
};
use once_cell::sync::Lazy;
use once_cell::unsync::OnceCell;
use std::{
collections::HashMap,
convert::TryFrom,
fmt,
num::NonZeroU32,
sync::{Arc, Mutex},
};
use zbus::{dbus_interface, Result, SignalContext};
use zvariant::OwnedValue;
use crate::dbus_service;
use crate::deref_cell::DerefCell;
static PATH: &str = "/org/freedesktop/Notifications";
static INTERFACE: &str = "org.freedesktop.Notifications";
enum Event {
NotificationReceived(NotificationId),
CloseNotification(NotificationId),
}
pub struct NotificationsInterfaceInner {
next_id: Mutex<NotificationId>,
notifications: Mutex<HashMap<NotificationId, Arc<Notification>>>,
sender: mpsc::UnboundedSender<Event>,
}
#[derive(Clone)]
pub struct NotificationsInterface(Arc<NotificationsInterfaceInner>);
impl NotificationsInterface {
fn new() -> (Self, mpsc::UnboundedReceiver<Event>) {
let (sender, receiver) = mpsc::unbounded();
(
Self(Arc::new(NotificationsInterfaceInner {
next_id: Default::default(),
notifications: Default::default(),
sender,
})),
receiver,
)
}
fn next_id(&self) -> NotificationId {
let mut next_id = self.0.next_id.lock().unwrap();
let id = *next_id;
*next_id = NotificationId::new(u32::from(id).wrapping_add(1)).unwrap_or_default();
id
}
fn handle_notify(
&self,
app_name: String,
replaces_id: Option<NotificationId>,
app_icon: String,
summary: String,
body: String,
actions: Vec<String>,
hints: Hints,
_expire_timeout: i32,
) -> NotificationId {
// Ignores `expire-timeout`, like Gnome Shell
let id = replaces_id.unwrap_or_else(|| self.next_id());
let notification = Arc::new(Notification {
id,
app_name,
app_icon,
summary,
body,
actions,
hints,
});
self.0
.notifications
.lock()
.unwrap()
.insert(id, notification);
self.0
.sender
.unbounded_send(Event::NotificationReceived(id))
.unwrap();
id
}
}
// TODO: return value variable names in introspection data?
#[dbus_interface(name = "org.freedesktop.Notifications")]
impl NotificationsInterface {
fn Notify(
&self,
app_name: String,
replaces_id: u32,
app_icon: String,
summary: String,
body: String,
actions: Vec<String>,
hints: Hints,
expire_timeout: i32,
) -> u32 {
u32::from(self.handle_notify(
app_name,
NotificationId::new(replaces_id),
app_icon,
summary,
body,
actions,
hints,
expire_timeout,
))
}
async fn CloseNotification(&self, id: u32) {
if let Some(id) = NotificationId::new(id) {
self.0
.sender
.unbounded_send(Event::CloseNotification(id))
.unwrap();
}
// TODO error?
}
fn GetCapabilities(&self) -> Vec<&'static str> {
// TODO: body-markup, sound
vec!["actions", "body", "icon-static", "persistence"]
}
fn GetServerInformation(&self) -> (&'static str, &'static str, &'static str, &'static str) {
("cosmic-panel", "system76", env!("CARGO_PKG_VERSION"), "1.2")
}
#[dbus_interface(signal)]
async fn NotificationClosed(ctxt: &SignalContext<'_>, id: u32, reason: u32) -> Result<()>;
#[dbus_interface(signal)]
async fn ActionInvoked(ctxt: &SignalContext<'_>, id: u32, action_key: &str) -> Result<()>;
}
#[derive(Default)]
pub struct NotificationsInner {
interface: DerefCell<NotificationsInterface>,
connection: OnceCell<zbus::Connection>,
}
#[glib::object_subclass]
impl ObjectSubclass for NotificationsInner {
const NAME: &'static str = "S76Notifications";
type ParentType = glib::Object;
type Type = Notifications;
}
impl ObjectImpl for NotificationsInner {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![
Signal::builder("notification-received")
.param_types(Some(NotificationId::static_type()))
.build(),
Signal::builder("notification-closed")
.param_types(Some(NotificationId::static_type()))
.build(),
]
});
SIGNALS.as_ref()
}
}
glib::wrapper! {
pub struct Notifications(ObjectSubclass<NotificationsInner>);
}
#[derive(zvariant::Type, serde::Deserialize)]
struct Hints(HashMap<String, OwnedValue>);
#[allow(dead_code)]
impl Hints {
fn prop<T: TryFrom<OwnedValue>>(&self, name: &str) -> Option<T> {
T::try_from(self.0.get(name)?.clone()).ok()
}
fn actions_icon(&self) -> bool {
self.prop("actions-icon").unwrap_or(false)
}
fn category(&self) -> Option<String> {
self.prop("category")
}
fn desktop_entry(&self) -> Option<String> {
self.prop("desktop-entry")
}
fn image_data(&self) -> Option<(i32, i32, i32, bool, i32, i32, Vec<u8>)> {
self.prop("image-data")
.or_else(|| self.prop("image_data"))
.or_else(|| self.prop("icon_data"))
}
fn image_path(&self) -> Option<String> {
self.prop("image-path").or_else(|| self.prop("image_path"))
}
fn resident(&self) -> bool {
self.prop("resident").unwrap_or(false)
}
fn sound_file(&self) -> Option<String> {
self.prop("sound-file")
}
fn sound_name(&self) -> Option<String> {
self.prop("sound-name")
}
fn transient(&self) -> bool {
self.prop("transient").unwrap_or(false)
}
fn xy(&self) -> Option<(u8, u8)> {
Some((self.prop("x")?, self.prop("y")?))
}
fn urgency(&self) -> Option<u8> {
self.prop("urgency")
}
}
impl fmt::Debug for Hints {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut s = f.debug_struct("Hints");
for (k, v) in &self.0 {
if let Ok(v) = <&str>::try_from(v) {
s.field(k, &v);
} else if let Ok(v) = i32::try_from(v) {
s.field(k, &v);
} else if let Ok(v) = bool::try_from(v) {
s.field(k, &v);
} else if let Ok(v) = u8::try_from(v) {
s.field(k, &v);
} else {
s.field(k, v);
};
}
s.finish()
}
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, Hash, glib::Boxed, PartialEq, Eq)]
#[boxed_type(name = "S76NotificationId")]
pub struct NotificationId(NonZeroU32);
impl Default for NotificationId {
fn default() -> Self {
Self(NonZeroU32::new(1).unwrap())
}
}
impl From<NotificationId> for u32 {
fn from(id: NotificationId) -> u32 {
id.0.into()
}
}
impl NotificationId {
fn new(value: u32) -> Option<Self> {
NonZeroU32::new(value).map(Self)
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct Notification {
pub id: NotificationId,
pub app_name: String,
pub app_icon: String, // decode?
pub summary: String,
pub body: String,
pub actions: Vec<String>, // enum?
hints: Hints,
}
#[repr(u32)]
#[allow(dead_code)]
enum CloseReason {
Expire = 1,
Dismiss,
Call,
Undefined,
}
impl Notifications {
pub fn new() -> Self {
let notifications = glib::Object::new::<Self>(&[]).unwrap();
let (interface, mut receiver) = NotificationsInterface::new();
notifications.inner().interface.set(interface);
glib::MainContext::default().spawn_local(clone!(@strong notifications => async move {
let connection = match dbus_service::create(INTERFACE, |builder| builder.serve_at(PATH, notifications.inner().interface.clone())).await {
Ok(connection) => connection,
Err(err) => {
eprintln!("Failed to start `Notifications` service: {}", err);
return;
}
};
let _ = notifications.inner().connection.set(connection.clone());
while let Some(event) = receiver.next().await {
match event {
Event::NotificationReceived(id) => {
notifications.emit_by_name::<()>("notification-received", &[&id]);
}
Event::CloseNotification(id) => {
notifications.close_notification(id, CloseReason::Call).await
}
}
}
}));
notifications
}
fn inner(&self) -> &NotificationsInner {
NotificationsInner::from_instance(self)
}
async fn close_notification(&self, id: NotificationId, reason: CloseReason) {
self.inner()
.interface
.0
.notifications
.lock()
.unwrap()
.remove(&id);
self.emit_by_name::<()>("notification-closed", &[&id]);
if let Some(connection) = self.inner().connection.get() {
let ctxt = SignalContext::new(connection, PATH).unwrap(); // XXX unwrap?
let _ =
NotificationsInterface::NotificationClosed(&ctxt, id.into(), reason as u32).await;
}
}
pub fn dismiss(&self, id: NotificationId) {
glib::MainContext::default().spawn_local(clone!(@strong self as self_ => async move {
self_.close_notification(id, CloseReason::Dismiss).await
}));
}
pub async fn invoke_action(&self, id: NotificationId, action_key: &str) {
if let Some(connection) = self.inner().connection.get() {
let ctxt = SignalContext::new(connection, PATH).unwrap(); // XXX unwrap?
let _ = NotificationsInterface::ActionInvoked(&ctxt, id.into(), action_key).await;
}
}
pub fn get(&self, id: NotificationId) -> Option<Arc<Notification>> {
self.inner()
.interface
.0
.notifications
.lock()
.unwrap()
.get(&id)
.cloned()
}
pub fn connect_notification_received<F: Fn(Arc<Notification>) + 'static>(
&self,
cb: F,
) -> SignalHandlerId {
self.connect_local("notification-received", false, move |values| {
let obj = values[0].get::<Self>().unwrap();
let id = values[1].get().unwrap();
if let Some(notification) = obj.get(id) {
cb(notification);
}
None
})
}
pub fn connect_notification_closed<F: Fn(NotificationId) + 'static>(
&self,
cb: F,
) -> SignalHandlerId {
self.connect_local("notification-closed", false, move |values| {
let id = values[1].get().unwrap();
cb(id);
None
})
}
}