wip: send dismiss requests and display notifications
This commit is contained in:
parent
c4566730ab
commit
060b316365
5 changed files with 330 additions and 15 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
59
cosmic-applet-notifications/src/subscriptions/dbus_proxy.rs
Normal file
59
cosmic-applet-notifications/src/subscriptions/dbus_proxy.rs
Normal 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<()>;
|
||||
}
|
||||
|
|
@ -1 +1,3 @@
|
|||
pub mod dbus;
|
||||
mod dbus_proxy;
|
||||
pub mod notifications;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue