diff --git a/Cargo.lock b/Cargo.lock index f7efac66..3d2ea1c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -887,9 +887,12 @@ version = "0.1.0" dependencies = [ "anyhow", "bytemuck", + "cosmic-client-toolkit", "cosmic-notifications-config", "cosmic-notifications-util", + "cosmic-protocols", "cosmic-time", + "futures", "i18n-embed 0.13.9", "i18n-embed-fl 0.6.7", "libcosmic", diff --git a/cosmic-applet-notifications/Cargo.toml b/cosmic-applet-notifications/Cargo.toml index e722d47b..be937ed1 100644 --- a/cosmic-applet-notifications/Cargo.toml +++ b/cosmic-applet-notifications/Cargo.toml @@ -36,4 +36,7 @@ i18n-embed = { version = "0.13.4", features = [ i18n-embed-fl = "0.6.4" rust-embed = "6.3.0" rust-embed-utils = "7.5.0" +cctk.workspace = true +cosmic-protocols.workspace = true url = "2.4.0" +futures = "0.3.21" diff --git a/cosmic-applet-notifications/data/com.system76.CosmicAppletNotifications.desktop b/cosmic-applet-notifications/data/com.system76.CosmicAppletNotifications.desktop index 841cb1f2..bd8bda84 100644 --- a/cosmic-applet-notifications/data/com.system76.CosmicAppletNotifications.desktop +++ b/cosmic-applet-notifications/data/com.system76.CosmicAppletNotifications.desktop @@ -10,3 +10,4 @@ Icon=com.system76.CosmicAppletNotifications NoDisplay=true X-CosmicApplet=true X-NotificationsApplet=true +X-HostWaylandDisplay=true diff --git a/cosmic-applet-notifications/src/main.rs b/cosmic-applet-notifications/src/main.rs index 2dafbc52..69225b1d 100644 --- a/cosmic-applet-notifications/src/main.rs +++ b/cosmic-applet-notifications/src/main.rs @@ -1,8 +1,7 @@ mod localize; mod subscriptions; -use cosmic::applet::token::subscription::{ - activation_token_subscription, TokenRequest, TokenUpdate, -}; +mod wayland_handler; +mod wayland_subscription; use cosmic::applet::{menu_button, menu_control_padding, padded_control}; use cosmic::cctk::sctk::reexports::calloop; use cosmic::cosmic_config::{config_subscription, Config, CosmicConfigEntry}; @@ -28,6 +27,7 @@ use std::collections::HashMap; use std::path::PathBuf; use tokio::sync::mpsc::Sender; use tracing::info; +use wayland_subscription::{WaylandRequest, WaylandUpdate}; #[tokio::main(flavor = "current_thread")] pub async fn main() -> cosmic::iced::Result { @@ -42,18 +42,25 @@ pub async fn main() -> cosmic::iced::Result { static DO_NOT_DISTURB: Lazy = Lazy::new(id::Toggler::unique); +#[derive(Debug, Clone, Copy)] +pub enum Popup { + NewNotifications, + UserActivated, +} + #[derive(Default)] struct Notifications { core: cosmic::app::Core, config: NotificationsConfig, config_helper: Option, icon_name: String, - popup: Option, + popup: Option<(window::Id, Popup)>, // notifications: Vec, timeline: Timeline, dbus_sender: Option>, cards: Vec<(id::Cards, Vec, bool, String, String, String)>, - token_tx: Option>, + wayland_tx: Option>, + on_active_output: bool, } impl Notifications { @@ -93,8 +100,8 @@ enum Message { Dismissed(u32), ClearAll(String), CardsToggled(String, bool), - Token(TokenUpdate), OpenSettings, + WaylandUpdate(wayland_subscription::WaylandUpdate), } impl cosmic::Application for Notifications { @@ -131,6 +138,7 @@ impl cosmic::Application for Notifications { core, config_helper: helper, config, + on_active_output: false, ..Default::default() }; _self.update_icon(); @@ -170,7 +178,7 @@ impl cosmic::Application for Notifications { .map(|(_, now)| Message::Frame(now)), subscriptions::dbus::proxy().map(Message::DbusEvent), subscriptions::notifications::notifications().map(Message::NotificationEvent), - activation_token_subscription(0).map(Message::Token), + wayland_subscription::wayland_subscription(0).map(Message::WaylandUpdate), ]) } @@ -183,11 +191,11 @@ impl cosmic::Application for Notifications { self.timeline.now(now); } Message::TogglePopup => { - if let Some(p) = self.popup.take() { + if let Some((p, _popup_type)) = self.popup.take() { return destroy_popup(p); } else { let new_id = window::Id::unique(); - self.popup.replace(new_id); + self.popup.replace((new_id, Popup::UserActivated)); let mut popup_settings = self.core.applet.get_popup_settings( window::Id::MAIN, @@ -238,6 +246,25 @@ impl cosmic::Application for Notifications { fl!("clear-all"), )); } + // create new notification popup if none exists + if self.popup.is_none() { + let new_id = window::Id::unique(); + self.popup.replace((new_id, Popup::NewNotifications)); + + let mut popup_settings = self.core.applet.get_popup_settings( + window::Id::MAIN, + new_id, + None, + None, + None, + ); + popup_settings.positioner.size_limits = Limits::NONE + .min_width(1.0) + .max_width(444.0) + .min_height(100.0) + .max_height(900.0); + return get_popup(popup_settings); + } } Message::Config(config) => { self.config = config; @@ -307,27 +334,27 @@ impl cosmic::Application for Notifications { self.update_cards(id); } Message::CloseRequested(id) => { - if Some(id) == self.popup { + if Some(id) == self.popup.as_ref().map(|(id, _)| *id) { self.popup = None; } } Message::OpenSettings => { let exec = "cosmic-settings notifications".to_string(); - if let Some(tx) = self.token_tx.as_ref() { - let _ = tx.send(TokenRequest { + if let Some(tx) = self.wayland_tx.as_ref() { + let _ = tx.send(WaylandRequest::TokenRequest { app_id: Self::APP_ID.to_string(), exec, }); } } - Message::Token(u) => match u { - TokenUpdate::Init(tx) => { - self.token_tx = Some(tx); + Message::WaylandUpdate(u) => match u { + WaylandUpdate::Init(tx) => { + self.wayland_tx = Some(tx); } - TokenUpdate::Finished => { - self.token_tx = None; + WaylandUpdate::Finished => { + self.wayland_tx = None; } - TokenUpdate::ActivationToken { token, .. } => { + WaylandUpdate::ActivationToken { token, .. } => { let mut cmd = std::process::Command::new("cosmic-settings"); cmd.arg("notifications"); if let Some(token) = token { @@ -336,6 +363,9 @@ impl cosmic::Application for Notifications { } cosmic::process::spawn(cmd); } + WaylandUpdate::ActiveOutput(active) => { + self.on_active_output = active; + } }, }; self.update_icon(); @@ -506,13 +536,18 @@ impl cosmic::Application for Notifications { notifs.push(card_list.into()); } - row!(scrollable( + let ret = row!(scrollable( Column::with_children(notifs) .spacing(8) .height(Length::Shrink), ) .height(Length::Shrink)) - .padding(menu_control_padding()) + .padding(menu_control_padding()); + + if matches!(self.popup, Some((_, Popup::NewNotifications))) { + return self.core.applet.popup_container(ret).into(); + } + ret }; let main_content = column![ diff --git a/cosmic-applet-notifications/src/wayland_handler.rs b/cosmic-applet-notifications/src/wayland_handler.rs new file mode 100644 index 00000000..15c1153e --- /dev/null +++ b/cosmic-applet-notifications/src/wayland_handler.rs @@ -0,0 +1,303 @@ +use crate::wayland_subscription::{WaylandRequest, WaylandUpdate}; +use std::os::{ + fd::{FromRawFd, RawFd}, + unix::net::UnixStream, +}; + +use cctk::{ + sctk::{ + self, + activation::{RequestData, RequestDataExt}, + output::{OutputHandler, OutputState}, + reexports::{calloop, calloop_wayland_source::WaylandSource}, + seat::{SeatHandler, SeatState}, + }, + toplevel_info::{ToplevelInfoHandler, ToplevelInfoState}, + wayland_client::{ + self, + protocol::{wl_output::WlOutput, wl_seat::WlSeat, wl_surface::WlSurface}, + }, +}; +use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1; +use futures::channel::mpsc::UnboundedSender; +use sctk::{ + activation::{ActivationHandler, ActivationState}, + registry::{ProvidesRegistryState, RegistryState}, +}; +use wayland_client::{globals::registry_queue_init, Connection, QueueHandle}; + +struct AppData { + exit: bool, + tx: UnboundedSender, + queue_handle: QueueHandle, + registry_state: RegistryState, + activation_state: Option, + toplevel_info_state: ToplevelInfoState, + seat_state: SeatState, + output_state: OutputState, + output: WlOutput, + active_output: bool, +} + +impl AppData { + fn active_output(&self) -> bool { + self.toplevel_info_state.toplevels().any(|toplevel| { + let Some(info) = toplevel.1 else { + return false; + }; + info.output.iter().any(|o| *o == self.output) + && info + .state + .contains(&zcosmic_toplevel_handle_v1::State::Activated) + }) + } +} + +impl ProvidesRegistryState for AppData { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + + sctk::registry_handlers!(); +} + +struct ExecRequestData { + data: RequestData, + exec: String, +} + +impl RequestDataExt for ExecRequestData { + fn app_id(&self) -> Option<&str> { + self.data.app_id() + } + + fn seat_and_serial(&self) -> Option<(&WlSeat, u32)> { + self.data.seat_and_serial() + } + + fn surface(&self) -> Option<&WlSurface> { + self.data.surface() + } +} + +impl ActivationHandler for AppData { + type RequestData = ExecRequestData; + fn new_token(&mut self, token: String, data: &ExecRequestData) { + let _ = self.tx.unbounded_send(WaylandUpdate::ActivationToken { + token: Some(token), + exec: data.exec.clone(), + }); + } +} + +impl SeatHandler for AppData { + fn seat_state(&mut self) -> &mut sctk::seat::SeatState { + &mut self.seat_state + } + + fn new_seat(&mut self, _: &Connection, _: &QueueHandle, _: WlSeat) {} + + fn new_capability( + &mut self, + _: &Connection, + _: &QueueHandle, + _: WlSeat, + _: sctk::seat::Capability, + ) { + } + + fn remove_capability( + &mut self, + _: &Connection, + _: &QueueHandle, + _: WlSeat, + _: sctk::seat::Capability, + ) { + } + + fn remove_seat(&mut self, _: &Connection, _: &QueueHandle, _: WlSeat) {} +} + +impl ToplevelInfoHandler for AppData { + fn toplevel_info_state(&mut self) -> &mut ToplevelInfoState { + &mut self.toplevel_info_state + } + + fn new_toplevel( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1, + ) { + let new_active_output = self.active_output(); + if new_active_output != self.active_output { + self.active_output = new_active_output; + let _ = self + .tx + .unbounded_send(WaylandUpdate::ActiveOutput(new_active_output)); + } + } + + fn update_toplevel( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1, + ) { + let new_active_output = self.active_output(); + if new_active_output != self.active_output { + self.active_output = new_active_output; + let _ = self + .tx + .unbounded_send(WaylandUpdate::ActiveOutput(new_active_output)); + } + } + + fn toplevel_closed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1, + ) { + let new_active_output = self.active_output(); + if new_active_output != self.active_output { + self.active_output = new_active_output; + let _ = self + .tx + .unbounded_send(WaylandUpdate::ActiveOutput(new_active_output)); + } + } +} + +pub(crate) fn wayland_handler( + tx: UnboundedSender, + rx: calloop::channel::Channel, +) { + let socket = std::env::var("X_PRIVILEGED_WAYLAND_SOCKET") + .ok() + .and_then(|fd| { + fd.parse::() + .ok() + .map(|fd| unsafe { UnixStream::from_raw_fd(fd) }) + }); + + let conn = if let Some(socket) = socket { + Connection::from_socket(socket).unwrap() + } else { + Connection::connect_to_env().unwrap() + }; + let (globals, event_queue) = registry_queue_init(&conn).unwrap(); + + let mut event_loop = calloop::EventLoop::::try_new().unwrap(); + let qh = event_queue.handle(); + let wayland_source = WaylandSource::new(conn, event_queue); + let handle = event_loop.handle(); + wayland_source + .insert(handle.clone()) + .expect("Failed to insert wayland source."); + + if handle + .insert_source(rx, |event, _, state| match event { + calloop::channel::Event::Msg(req) => match req { + WaylandRequest::TokenRequest { app_id, exec } => { + if let Some(activation_state) = state.activation_state.as_ref() { + activation_state.request_token_with_data( + &state.queue_handle, + ExecRequestData { + data: RequestData { + app_id: Some(app_id), + seat_and_serial: state + .seat_state + .seats() + .next() + .map(|seat| (seat, 0)), + surface: None, + }, + exec, + }, + ); + } else { + let _ = state + .tx + .unbounded_send(WaylandUpdate::ActivationToken { token: None, exec }); + } + } + }, + calloop::channel::Event::Closed => { + state.exit = true; + } + }) + .is_err() + { + return; + } + + let registry_state = RegistryState::new(&globals); + let Ok(applet_output) = std::env::var("COSMIC_PANEL_OUTPUT") else { + return; + }; + let output_state = OutputState::new(&globals, &qh); + let Some(my_output) = output_state.outputs().find(|o| { + output_state + .info(o) + .map(|info| info.name.as_ref() == Some(&applet_output)) + .unwrap_or_default() + }) else { + return; + }; + let mut app_data = AppData { + exit: false, + tx, + queue_handle: qh.clone(), + activation_state: ActivationState::bind::(&globals, &qh).ok(), + seat_state: SeatState::new(&globals, &qh), + toplevel_info_state: ToplevelInfoState::new(®istry_state, &qh), + registry_state, + output_state, + output: my_output, + active_output: false, + }; + + loop { + if app_data.exit { + break; + } + event_loop.dispatch(None, &mut app_data).unwrap(); + } +} + +impl OutputHandler for AppData { + fn output_state(&mut self) -> &mut OutputState { + &mut self.output_state + } + + fn new_output( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _output: wayland_client::protocol::wl_output::WlOutput, + ) { + } + + fn update_output( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _output: wayland_client::protocol::wl_output::WlOutput, + ) { + } + + fn output_destroyed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _output: wayland_client::protocol::wl_output::WlOutput, + ) { + } +} + +sctk::delegate_activation!(AppData, ExecRequestData); +sctk::delegate_seat!(AppData); +sctk::delegate_output!(AppData); +sctk::delegate_registry!(AppData); +cctk::delegate_toplevel_info!(AppData); diff --git a/cosmic-applet-notifications/src/wayland_subscription.rs b/cosmic-applet-notifications/src/wayland_subscription.rs new file mode 100644 index 00000000..a09aa8cb --- /dev/null +++ b/cosmic-applet-notifications/src/wayland_subscription.rs @@ -0,0 +1,84 @@ +//! # DBus interface proxy for: `org.freedesktop.UPower.KbdBacklight` +//! +//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data. +//! Source: `Interface '/org/freedesktop/UPower/KbdBacklight' from service 'org.freedesktop.UPower' on system bus`. +use cctk::sctk::reexports::calloop; +use cosmic::iced; +use cosmic::iced::subscription; +use futures::{ + channel::mpsc::{unbounded, UnboundedReceiver}, + SinkExt, StreamExt, +}; +use std::{fmt::Debug, hash::Hash, thread::JoinHandle}; + +use crate::wayland_handler::wayland_handler; + +pub fn wayland_subscription( + id: I, +) -> iced::Subscription { + subscription::channel(id, 50, move |mut output| async move { + let mut state = State::Ready; + + loop { + state = start_listening(state, &mut output).await; + } + }) +} + +pub enum State { + Ready, + Waiting( + UnboundedReceiver, + calloop::channel::Sender, + JoinHandle<()>, + ), + Finished, +} + +async fn start_listening( + state: State, + output: &mut futures::channel::mpsc::Sender, +) -> State { + match state { + State::Ready => { + let (calloop_tx, calloop_rx) = calloop::channel::channel(); + let (toplevel_tx, toplevel_rx) = unbounded(); + let handle = std::thread::spawn(move || { + wayland_handler(toplevel_tx, calloop_rx); + }); + let tx = calloop_tx.clone(); + _ = output.send(WaylandUpdate::Init(tx)).await; + State::Waiting(toplevel_rx, calloop_tx, handle) + } + State::Waiting(mut rx, tx, handle) => { + if handle.is_finished() { + _ = output.send(WaylandUpdate::Finished).await; + return State::Finished; + } + match rx.next().await { + Some(u) => { + _ = output.send(u).await; + State::Waiting(rx, tx, handle) + } + None => { + _ = output.send(WaylandUpdate::Finished).await; + State::Finished + } + } + } + State::Finished => iced::futures::future::pending().await, + } +} + +#[derive(Clone, Debug)] +pub enum WaylandUpdate { + Init(calloop::channel::Sender), + Finished, + ActiveOutput(bool), + ActivationToken { token: Option, exec: String }, +} + +#[derive(Clone, Debug)] +pub enum WaylandRequest { + TokenRequest { app_id: String, exec: String }, +}