diff --git a/cosmic-applet-notifications/Cargo.toml b/cosmic-applet-notifications/Cargo.toml index 758f7b9c..05387c25 100644 --- a/cosmic-applet-notifications/Cargo.toml +++ b/cosmic-applet-notifications/Cargo.toml @@ -6,15 +6,16 @@ license = "GPL-3.0-or-later" [dependencies] anyhow = "1" -icon-loader = { version = "0.3.6", features = ["gtk"] } libcosmic.workspace = true cosmic-time.workspace = true cosmic-applet = { path = "../applet" } nix = "0.26" tokio = { version = "1.24.1", features = ["sync", "rt", "tracing", "macros", "net", "io-util", "io-std"] } cosmic-notifications-util = { git = "https://github.com/pop-os/cosmic-notifications" } +cosmic-notifications-config = { git = "https://github.com/pop-os/cosmic-notifications" } tracing = "0.1" ron = "0.8" sendfd = { version = "0.4", features = [ "tokio" ] } bytemuck = "1" tracing-subscriber = "0.3" +zbus = "3.14" diff --git a/cosmic-applet-notifications/src/main.rs b/cosmic-applet-notifications/src/main.rs index c3abd0f2..f0030a92 100644 --- a/cosmic-applet-notifications/src/main.rs +++ b/cosmic-applet-notifications/src/main.rs @@ -1,25 +1,32 @@ mod subscriptions; +use cosmic::cosmic_config::{config_subscription, Config, CosmicConfigEntry}; use cosmic::iced::wayland::popup::{destroy_popup, get_popup}; use cosmic::iced::{ widget::{button, column, row, text, Row, Space}, window, Alignment, Application, Color, Command, Length, Subscription, }; +use cosmic::iced_core::image; +use cosmic::iced_widget::button::StyleSheet; use cosmic_applet::{applet_button_theme, CosmicAppletHelper}; use cosmic::iced_style::application::{self, Appearance}; -use cosmic::iced_widget::Button; -use cosmic::theme::Svg; +use cosmic::iced_widget::{horizontal_space, scrollable, Column}; +use cosmic::theme::{Button, Svg}; use cosmic::widget::{divider, icon}; use cosmic::Renderer; use cosmic::{Element, Theme}; +use cosmic_notifications_config::NotificationsConfig; +use cosmic_notifications_util::{AppletEvent, Notification}; use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline}; -use cosmic_notifications_util::AppletEvent; -use tracing::info; +use std::borrow::Cow; use std::process; +use tokio::sync::mpsc::Sender; +use tracing::info; -pub fn main() -> cosmic::iced::Result { +#[tokio::main(flavor = "current_thread")] +pub async fn main() -> cosmic::iced::Result { tracing_subscriber::fmt::init(); info!("Notifications applet"); @@ -34,12 +41,14 @@ static DO_NOT_DISTURB: Lazy = Lazy::new(id::Toggler::unique); struct Notifications { applet_helper: CosmicAppletHelper, theme: Theme, + config: NotificationsConfig, + config_helper: Option, icon_name: String, popup: Option, id_ctr: u128, - do_not_disturb: bool, - notifications: Vec>, + notifications: Vec, timeline: Timeline, + dbus_sender: Option>, } #[derive(Debug, Clone)] @@ -50,7 +59,10 @@ enum Message { Ignore, Frame(Instant), Theme(Theme), - NotificationEvent(AppletEvent) + NotificationEvent(AppletEvent), + Config(NotificationsConfig), + DbusEvent(subscriptions::dbus::Output), + Dismissed(u32), } impl Application for Notifications { @@ -62,11 +74,30 @@ impl Application for Notifications { fn new(_flags: ()) -> (Notifications, Command) { let applet_helper = CosmicAppletHelper::default(); let theme = applet_helper.theme(); + let helper = Config::new( + cosmic_notifications_config::ID, + NotificationsConfig::version(), + ) + .ok(); + + let config: NotificationsConfig = helper + .as_ref() + .map(|helper| { + NotificationsConfig::get_entry(helper).unwrap_or_else(|(errors, config)| { + for err in errors { + tracing::error!("{:?}", err); + } + config + }) + }) + .unwrap_or_default(); ( Notifications { applet_helper, theme, icon_name: "notification-alert-symbolic".to_string(), + config_helper: helper, + config, ..Default::default() }, Command::none(), @@ -95,10 +126,25 @@ impl Application for Notifications { fn subscription(&self) -> Subscription { Subscription::batch(vec![ self.applet_helper.theme_subscription(0).map(Message::Theme), + config_subscription::( + 0, + cosmic_notifications_config::ID.into(), + NotificationsConfig::version(), + ) + .map(|(_, res)| match res { + Ok(config) => Message::Config(config), + Err((errors, config)) => { + for err in errors { + tracing::error!("{:?}", err); + } + Message::Config(config) + } + }), self.timeline .as_subscription() .map(|(_, now)| Message::Frame(now)), - subscriptions::notifications::notifications().map(Message::NotificationEvent) + subscriptions::dbus::proxy().map(Message::DbusEvent), + subscriptions::notifications::notifications().map(Message::NotificationEvent), ]) } @@ -132,7 +178,12 @@ impl Application for Notifications { } Message::DoNotDisturb(chain, b) => { self.timeline.set_chain(chain).start(); - self.do_not_disturb = b; + self.config.do_not_disturb = b; + if let Some(helper) = &self.config_helper { + if let Err(err) = self.config.write_entry(helper) { + tracing::error!("{:?}", err); + } + } Command::none() } Message::Settings => { @@ -144,6 +195,28 @@ impl Application for Notifications { Command::none() } Message::Ignore => Command::none(), + Message::Config(config) => { + self.config = config; + Command::none() + } + Message::Dismissed(id) => { + self.notifications.retain(|n| n.id != id); + if let Some(tx) = &self.dbus_sender { + let tx = tx.clone(); + tokio::spawn(async move { + if let Err(err) = tx.send(subscriptions::dbus::Input::Dismiss(id)).await { + tracing::error!("{:?}", err); + } + }); + } + Command::none() + } + Message::DbusEvent(e) => match e { + subscriptions::dbus::Output::Ready(tx) => { + self.dbus_sender.replace(tx); + Command::none() + } + }, } } @@ -158,7 +231,7 @@ impl Application for Notifications { DO_NOT_DISTURB, &self.timeline, String::from("Do Not Disturb"), - self.do_not_disturb, + self.config.do_not_disturb, Message::DoNotDisturb ) .width(Length::Fill)] @@ -176,7 +249,103 @@ impl Application for Notifications { ] .spacing(12) } else { - row![text("TODO: make app worky with notifications")] + let mut notifs = Vec::with_capacity(self.notifications.len()); + + for n in &self.notifications { + let summary = text(if n.summary.len() > 24 { + Cow::from(format!( + "{:.26}...", + n.summary.lines().next().unwrap_or_default() + )) + } else { + Cow::from(&n.summary) + }) + .size(18); + let urgency = n.urgency(); + + notifs.push( + cosmic::widget::button(Button::Custom { + active: Box::new(move |t| { + let style = if urgency > 1 { + Button::Primary + } else { + Button::Secondary + }; + let cosmic = t.cosmic(); + let mut a = t.active(&style); + a.border_radius = 8.0.into(); + a.background = Some(Color::from(cosmic.bg_color()).into()); + a.border_color = Color::from(cosmic.bg_divider()); + a.border_width = 1.0; + a + }), + hover: Box::new(move |t| { + let style = if urgency > 1 { + Button::Primary + } else { + Button::Secondary + }; + let cosmic = t.cosmic(); + let mut a = t.hovered(&style); + a.border_radius = 8.0.into(); + a.background = Some(Color::from(cosmic.bg_color()).into()); + a.border_color = Color::from(cosmic.bg_divider()); + a.border_width = 1.0; + a + }), + }) + .custom(vec![column!( + match n.image() { + Some(cosmic_notifications_util::Image::File(path)) => { + row![icon(path.as_path(), 32), summary] + .spacing(8) + .align_items(Alignment::Center) + } + Some(cosmic_notifications_util::Image::Name(name)) => { + row![icon(name.as_str(), 32), summary] + .spacing(8) + .align_items(Alignment::Center) + } + Some(cosmic_notifications_util::Image::Data { + width, + height, + data, + }) => { + let handle = + image::Handle::from_pixels(*width, *height, data.clone()); + row![icon(handle, 32), summary] + .spacing(8) + .align_items(Alignment::Center) + } + None => row![summary], + }, + text(if n.body.len() > 38 { + Cow::from(format!( + "{:.40}...", + n.body.lines().next().unwrap_or_default() + )) + } else { + Cow::from(&n.summary) + }) + .size(14), + horizontal_space(Length::Fixed(300.0)), + ) + .spacing(8) + .into()]) + .on_press(Message::Dismissed(n.id)) + .into(), + ); + } + + row!(scrollable( + Column::with_children(notifs) + .spacing(8) + .width(Length::Shrink) + .height(Length::Shrink), + ) + .width(Length::Shrink) + .height(Length::Fixed(400.0))) + .width(Length::Shrink) }; let main_content = column![ @@ -201,7 +370,9 @@ impl Application for Notifications { } // todo put into libcosmic doing so will fix the row_button's boarder radius -fn row_button(mut content: Vec>) -> Button { +fn row_button( + mut content: Vec>, +) -> cosmic::iced::widget::Button { content.insert(0, Space::with_width(Length::Fixed(24.0)).into()); content.push(Space::with_width(Length::Fixed(24.0)).into()); diff --git a/cosmic-applet-notifications/src/subscriptions/dbus.rs b/cosmic-applet-notifications/src/subscriptions/dbus.rs index 5f3296f6..326b9f6f 100644 --- a/cosmic-applet-notifications/src/subscriptions/dbus.rs +++ b/cosmic-applet-notifications/src/subscriptions/dbus.rs @@ -1 +1,83 @@ -// TODO connect as a client and send actions / dismissals \ No newline at end of file +use crate::subscriptions::dbus_proxy::NotificationsProxy; +use cosmic::{ + iced::{ + futures::{self, SinkExt}, + subscription, + }, + iced_futures::Subscription, +}; +use tokio::sync::mpsc::{channel, Receiver, Sender}; +use tracing::{error, warn}; +use zbus::Connection; + +#[derive(Debug)] +pub enum State { + Ready, + WaitingForNotificationEvent(Connection, Receiver), + Finished, +} + +#[derive(Debug, Clone, Copy)] +pub enum Input { + Dismiss(u32), +} + +#[derive(Debug, Clone)] +pub enum Output { + Ready(Sender), +} + +pub fn proxy() -> Subscription { + struct SomeWorker; + + subscription::channel( + std::any::TypeId::of::(), + 50, + |mut output| async move { + let mut state = State::Ready; + + loop { + match &mut state { + State::Ready => { + let (sender, receiver) = channel(10); + let Ok(conn) = Connection::session().await else { + error!("Failed to connect to session bus"); + state = State::Finished; + continue; + }; + if let Err(err) = output.send(Output::Ready(sender)).await { + error!("Failed to send sender: {}", err); + state = State::Finished; + continue; + } + + state = State::WaitingForNotificationEvent(conn, receiver); + } + State::WaitingForNotificationEvent(conn, rx) => { + let Ok(proxy) = NotificationsProxy::new(&conn).await else { + error!("Failed to create proxy from session connection"); + state = State::Finished; + continue; + }; + + match rx.recv().await { + Some(Input::Dismiss(id)) => { + if let Err(err) = proxy.close_notification(id).await { + error!("Failed to close notification: {}", err); + } + } + None => { + warn!("Notification event channel closed"); + state = State::Finished; + continue; + } + } + } + State::Finished => { + let () = futures::future::pending().await; + } + } + } + }, + ) +} diff --git a/cosmic-applet-notifications/src/subscriptions/dbus_proxy.rs b/cosmic-applet-notifications/src/subscriptions/dbus_proxy.rs new file mode 100644 index 00000000..30c7e173 --- /dev/null +++ b/cosmic-applet-notifications/src/subscriptions/dbus_proxy.rs @@ -0,0 +1,59 @@ +//! # DBus interface proxy for: `org.freedesktop.Notifications` +//! +//! This code was generated by `zbus-xmlgen` `3.1.1` from DBus introspection data. +//! Source: `Interface '/org/freedesktop/Notifications' from service 'org.freedesktop.Notifications' on session bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the +//! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html) +//! section of the zbus documentation. +//! +//! This DBus object implements +//! [standard DBus interfaces](https://dbus.freedesktop.org/doc/dbus-specification.html), +//! (`org.freedesktop.DBus.*`) for which the following zbus proxies can be used: +//! +//! * [`zbus::fdo::PeerProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! …consequently `zbus-xmlgen` did not generate code for the above interfaces. + +use zbus::dbus_proxy; + +#[dbus_proxy( + interface = "org.freedesktop.Notifications", + default_service = "org.freedesktop.Notifications", + default_path = "/org/freedesktop/Notifications" +)] +trait Notifications { + /// CloseNotification method + fn close_notification(&self, id: u32) -> zbus::Result<()>; + + /// GetCapabilities method + fn get_capabilities(&self) -> zbus::Result>; + + /// GetServerInformation method + fn get_server_information(&self) -> zbus::Result<(String, String, String, String)>; + + /// Notify method + fn notify( + &self, + app_name: &str, + replaces_id: u32, + app_icon: &str, + summary: &str, + body: &str, + actions: &[&str], + hints: std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, + expire_timeout: i32, + ) -> zbus::Result; + + /// ActionInvoked signal + #[dbus_proxy(signal)] + fn action_invoked(&self, id: u32, action_key: &str) -> zbus::Result<()>; + + /// NotificationClosed signal + #[dbus_proxy(signal)] + fn notification_closed(&self, id: u32, reason: u32) -> zbus::Result<()>; +} diff --git a/cosmic-applet-notifications/src/subscriptions/mod.rs b/cosmic-applet-notifications/src/subscriptions/mod.rs index c2a91bed..21fbf7aa 100644 --- a/cosmic-applet-notifications/src/subscriptions/mod.rs +++ b/cosmic-applet-notifications/src/subscriptions/mod.rs @@ -1 +1,3 @@ +pub mod dbus; +mod dbus_proxy; pub mod notifications;