wip: send dismiss requests and display notifications

This commit is contained in:
Ashley Wulber 2023-07-03 14:53:45 -04:00 committed by Ashley Wulber
parent c4566730ab
commit 060b316365
5 changed files with 330 additions and 15 deletions

View file

@ -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"

View file

@ -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<id::Toggler> = Lazy::new(id::Toggler::unique);
struct Notifications {
applet_helper: CosmicAppletHelper,
theme: Theme,
config: NotificationsConfig,
config_helper: Option<Config>,
icon_name: String,
popup: Option<window::Id>,
id_ctr: u128,
do_not_disturb: bool,
notifications: Vec<Vec<String>>,
notifications: Vec<Notification>,
timeline: Timeline,
dbus_sender: Option<Sender<subscriptions::dbus::Input>>,
}
#[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<Message>) {
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<Message> {
Subscription::batch(vec![
self.applet_helper.theme_subscription(0).map(Message::Theme),
config_subscription::<u64, NotificationsConfig>(
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<Element<Message>>) -> Button<Message, Renderer> {
fn row_button(
mut content: Vec<Element<Message>>,
) -> cosmic::iced::widget::Button<Message, Renderer> {
content.insert(0, Space::with_width(Length::Fixed(24.0)).into());
content.push(Space::with_width(Length::Fixed(24.0)).into());

View file

@ -1 +1,83 @@
// TODO connect as a client and send actions / dismissals
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<Input>),
Finished,
}
#[derive(Debug, Clone, Copy)]
pub enum Input {
Dismiss(u32),
}
#[derive(Debug, Clone)]
pub enum Output {
Ready(Sender<Input>),
}
pub fn proxy() -> Subscription<Output> {
struct SomeWorker;
subscription::channel(
std::any::TypeId::of::<SomeWorker>(),
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;
}
}
}
},
)
}

View file

@ -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<Vec<String>>;
/// 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<u32>;
/// 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<()>;
}

View file

@ -1 +1,3 @@
pub mod dbus;
mod dbus_proxy;
pub mod notifications;