Initial port of notifications to an applet

This commit is contained in:
Ian Douglas Scott 2022-06-10 15:29:45 -07:00
parent 25a8e8353e
commit 04ce88e4ce
17 changed files with 231 additions and 28 deletions

View file

@ -0,0 +1,16 @@
[package]
name = "cosmic-applet-notifications"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0-or-later"
[dependencies]
cascade = "1"
futures = "0.3"
gtk4 = "0.4.6"
once_cell = "1.12"
serde = "1"
zbus = "2.0.1"
zbus_names = "2"
zvariant = "3"
cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", features = ["gtk4"]}

View file

@ -0,0 +1,10 @@
[Desktop Entry]
Name=Cosmic Applet Notifications
Type=Application
Exec=cosmic-applet-notifications
Terminal=false
Categories=GNOME;GTK;
Keywords=Gnome;GTK;
# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
Icon=com.system76.CosmicAppletNotifications
NoDisplay=true

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="128px" height="128px" viewBox="0 0 128 128" version="1.1">
<defs>
<filter id="alpha" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">
<feColorMatrix type="matrix" in="SourceGraphic" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<mask id="mask0">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip1">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10632" clip-path="url(#clip1)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 123.503906 236 C 123.503906 268.863281 96.863281 295.503906 64 295.503906 C 31.136719 295.503906 4.496094 268.863281 4.496094 236 C 4.496094 203.136719 31.136719 176.496094 64 176.496094 C 96.863281 176.496094 123.503906 203.136719 123.503906 236 Z M 123.503906 236 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<mask id="mask1">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip2">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10635" clip-path="url(#clip2)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 29.195312 180.496094 L 98.804688 180.496094 C 103.609375 180.496094 107.503906 184.046875 107.503906 188.425781 L 107.503906 283.574219 C 107.503906 287.953125 103.609375 291.503906 98.804688 291.503906 L 29.195312 291.503906 C 24.390625 291.503906 20.496094 287.953125 20.496094 283.574219 L 20.496094 188.425781 C 20.496094 184.046875 24.390625 180.496094 29.195312 180.496094 Z M 29.195312 180.496094 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<mask id="mask2">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip3">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10638" clip-path="url(#clip3)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 20.417969 184.496094 L 107.582031 184.496094 C 111.957031 184.496094 115.503906 188.042969 115.503906 192.417969 L 115.503906 279.582031 C 115.503906 283.957031 111.957031 287.503906 107.582031 287.503906 L 20.417969 287.503906 C 16.042969 287.503906 12.496094 283.957031 12.496094 279.582031 L 12.496094 192.417969 C 12.496094 188.042969 16.042969 184.496094 20.417969 184.496094 Z M 20.417969 184.496094 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<mask id="mask3">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip4">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10641" clip-path="url(#clip4)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 16.425781 200.496094 L 111.574219 200.496094 C 115.953125 200.496094 119.503906 204.390625 119.503906 209.195312 L 119.503906 278.804688 C 119.503906 283.609375 115.953125 287.503906 111.574219 287.503906 L 16.425781 287.503906 C 12.046875 287.503906 8.496094 283.609375 8.496094 278.804688 L 8.496094 209.195312 C 8.496094 204.390625 12.046875 200.496094 16.425781 200.496094 Z M 16.425781 200.496094 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
</defs>
<g id="surface10578">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(94.117647%,94.117647%,94.117647%);fill-opacity:1;stroke:none;"/>
<use xlink:href="#surface10632" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask0)"/>
<use xlink:href="#surface10635" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask1)"/>
<use xlink:href="#surface10638" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask2)"/>
<use xlink:href="#surface10641" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask3)"/>
<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(38.431373%,62.7451%,91.764706%);stroke-opacity:1;stroke-miterlimit:4;" d="M 0 289 L 128 289 " transform="matrix(1,0,0,1,0,-172)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -0,0 +1,53 @@
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

@ -0,0 +1,31 @@
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

@ -0,0 +1,58 @@
use cascade::cascade;
use gtk4::{glib, prelude::*};
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;
fn main() {
gtk4::init().unwrap();
// XXX Implement DBus service somewhere other than applet?
let notifications = Notifications::new();
let provider = gtk4::CssProvider::new();
provider.load_from_data(include_bytes!("style.css"));
gtk4::StyleContext::add_provider_for_display(
&gtk4::gdk::Display::default().expect("Could not connect to a display."),
&provider,
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
let notification_list = NotificationList::new(&notifications);
let popover = cascade! {
gtk4::Popover::new();
..set_child(Some(&notification_list));
};
let menu_button = cascade! {
gtk4::MenuButton::new();
..set_popover(Some(&popover));
};
// XXX show in correct place
cascade! {
NotificationPopover::new(&notifications);
..set_parent(&menu_button);
};
gtk4::Window::builder()
.decorated(false)
.child(&menu_button)
.resizable(false)
.width_request(1)
.height_request(1)
.css_classes(vec!["root_window".to_string()])
.build()
.show();
let main_loop = glib::MainLoop::new(None, false);
main_loop.run();
}

View file

@ -0,0 +1,113 @@
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

@ -0,0 +1,136 @@
use cascade::cascade;
use gtk4::{
glib::{self, clone},
prelude::*,
subclass::prelude::*,
};
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) -> Self {
let obj = glib::Object::new::<Self>(&[]).unwrap();
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

@ -0,0 +1,117 @@
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

@ -0,0 +1,416 @@
#![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",
&[NotificationId::static_type().into()],
glib::Type::UNIT.into(),
)
.build(),
Signal::builder(
"notification-closed",
&[NotificationId::static_type().into()],
glib::Type::UNIT.into(),
)
.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());
if 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
})
}
}

View file

@ -0,0 +1,33 @@
.loading-overlay {
background-color: #2f2f2f;
opacity: 0.85;
}
image.panel_icon {
padding-left: 0px;
padding-right: 0px;
padding-top: 0px;
padding-bottom: 0px;
}
button.panel_icon {
border-radius: 12px;
transition: 100ms;
padding: 4px;
border-color: transparent;
background: transparent;
outline-color: transparent;
}
button.panel_icon:hover {
border-radius: 12px;
transition: 100ms;
padding: 4px;
border-color: rgba(255, 255, 255, 0.1);
outline-color: rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.1);
}
window.root_window {
background: transparent;
}