diff --git a/src/action.rs b/src/action.rs index f7ddd6f1..b7162896 100644 --- a/src/action.rs +++ b/src/action.rs @@ -3,6 +3,8 @@ #[cfg(feature = "winit")] use crate::app; +#[cfg(feature = "single-instance")] +use crate::dbus_activation; pub const fn app(message: M) -> Action { Action::App(message) @@ -24,9 +26,9 @@ pub enum Action { #[cfg(feature = "winit")] /// Internal messages to be handled by libcosmic. Cosmic(app::Action), - #[cfg(all(feature = "winit", feature = "single-instance"))] + #[cfg(feature = "single-instance")] /// Dbus activation messages - DbusActivation(app::DbusActivationMessage), + DbusActivation(dbus_activation::Message), /// Do nothing None, } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 5e3e3fb5..919e6042 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -354,7 +354,7 @@ where #[cfg(feature = "single-instance")] if self.app.core().single_instance { - subscriptions.push(super::single_instance_subscription::()); + subscriptions.push(crate::dbus_activation::subscription::()); } Subscription::batch(subscriptions) diff --git a/src/app/mod.rs b/src/app/mod.rs index 5541bdca..1ebc63d1 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -27,15 +27,6 @@ use iced::window; use iced::{Length, Subscription}; pub use settings::Settings; use std::borrow::Cow; -use url::Url; -#[cfg(feature = "single-instance")] -use { - iced_futures::futures::channel::mpsc::{Receiver, Sender}, - iced_futures::futures::SinkExt, - std::any::TypeId, - std::collections::HashMap, - zbus::{interface, proxy, zvariant::Value}, -}; pub(crate) fn iced_settings( settings: Settings, @@ -146,142 +137,6 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res } } -#[cfg(feature = "single-instance")] -#[derive(Debug, Clone)] -pub struct DbusActivationMessage> { - pub activation_token: Option, - pub desktop_startup_id: Option, - pub msg: DbusActivationDetails, -} - -#[derive(Debug, Clone)] -pub enum DbusActivationDetails> { - Activate, - Open { - url: Vec, - }, - /// action can be deserialized as Flags - ActivateAction { - action: Action, - args: Args, - }, -} -#[cfg(feature = "single-instance")] -#[derive(Debug, Default)] -pub struct DbusActivation(Option>); -#[cfg(feature = "single-instance")] -impl DbusActivation { - #[must_use] - pub fn new() -> Self { - Self(None) - } - - pub fn rx(&mut self) -> Receiver { - let (tx, rx) = iced_futures::futures::channel::mpsc::channel(10); - self.0 = Some(tx); - rx - } -} - -#[cfg(feature = "single-instance")] -#[proxy(interface = "org.freedesktop.DbusActivation", assume_defaults = true)] -pub trait DbusActivationInterface { - /// Activate the application. - fn activate(&mut self, platform_data: HashMap<&str, Value<'_>>) -> zbus::Result<()>; - - /// Open the given URIs. - fn open( - &mut self, - uris: Vec<&str>, - platform_data: HashMap<&str, Value<'_>>, - ) -> zbus::Result<()>; - - /// Activate the given action. - fn activate_action( - &mut self, - action_name: &str, - parameter: Vec<&str>, - platform_data: HashMap<&str, Value<'_>>, - ) -> zbus::Result<()>; -} - -#[cfg(feature = "single-instance")] -#[interface(name = "org.freedesktop.DbusActivation")] -impl DbusActivation { - async fn activate(&mut self, platform_data: HashMap<&str, Value<'_>>) { - if let Some(tx) = &mut self.0 { - let _ = tx - .send(DbusActivationMessage { - activation_token: platform_data.get("activation-token").and_then(|t| match t { - Value::Str(t) => Some(t.to_string()), - _ => None, - }), - desktop_startup_id: platform_data.get("desktop-startup-id").and_then( - |t| match t { - Value::Str(t) => Some(t.to_string()), - _ => None, - }, - ), - msg: DbusActivationDetails::Activate, - }) - .await; - } - } - - async fn open(&mut self, uris: Vec<&str>, platform_data: HashMap<&str, Value<'_>>) { - if let Some(tx) = &mut self.0 { - let _ = tx - .send(DbusActivationMessage { - activation_token: platform_data.get("activation-token").and_then(|t| match t { - Value::Str(t) => Some(t.to_string()), - _ => None, - }), - desktop_startup_id: platform_data.get("desktop-startup-id").and_then( - |t| match t { - Value::Str(t) => Some(t.to_string()), - _ => None, - }, - ), - msg: DbusActivationDetails::Open { - url: uris.iter().filter_map(|u| Url::parse(u).ok()).collect(), - }, - }) - .await; - } - } - - async fn activate_action( - &mut self, - action_name: &str, - parameter: Vec<&str>, - platform_data: HashMap<&str, Value<'_>>, - ) { - if let Some(tx) = &mut self.0 { - let _ = tx - .send(DbusActivationMessage { - activation_token: platform_data.get("activation-token").and_then(|t| match t { - Value::Str(t) => Some(t.to_string()), - _ => None, - }), - desktop_startup_id: platform_data.get("desktop-startup-id").and_then( - |t| match t { - Value::Str(t) => Some(t.to_string()), - _ => None, - }, - ), - msg: DbusActivationDetails::ActivateAction { - action: action_name.to_string(), - args: parameter - .iter() - .map(std::string::ToString::to_string) - .collect(), - }, - }) - .await; - } - } -} - #[cfg(feature = "single-instance")] /// Launch a COSMIC application with the given [`Settings`]. /// If the application is already running, the arguments will be passed to the @@ -293,6 +148,8 @@ where App::Flags: CosmicFlags, App::Message: Clone + std::fmt::Debug + Send + 'static, { + use std::collections::HashMap; + let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok(); let override_single = std::env::var("COSMIC_SINGLE_INSTANCE") @@ -309,7 +166,7 @@ where return run::(settings, flags); }; - if DbusActivationInterfaceProxyBlocking::builder(&conn) + if crate::dbus_activation::DbusActivationInterfaceProxyBlocking::builder(&conn) .destination(App::APP_ID) .ok() .and_then(|b| b.path(path).ok()) @@ -576,7 +433,7 @@ where /// Handles dbus activation messages #[cfg(feature = "single-instance")] - fn dbus_activation(&mut self, msg: DbusActivationMessage) -> Task { + fn dbus_activation(&mut self, msg: crate::dbus_activation::Message) -> Task { Task::none() } } @@ -935,75 +792,6 @@ impl ApplicationExt for App { } } -#[cfg(feature = "single-instance")] -fn single_instance_subscription() -> Subscription> -{ - use iced_futures::futures::StreamExt; - iced_futures::Subscription::run_with_id( - TypeId::of::(), - iced::stream::channel(10, move |mut output| async move { - let mut single_instance: DbusActivation = DbusActivation::new(); - let mut rx = single_instance.rx(); - if let Ok(builder) = zbus::ConnectionBuilder::session() { - let path: String = format!("/{}", App::APP_ID.replace('.', "/")); - if let Ok(conn) = builder.build().await { - // XXX Setup done this way seems to be more reliable. - // - // the docs for serve_at seem to imply it will replace the - // existing interface at the requested path, but it doesn't - // seem to work that way all the time. The docs for - // object_server().at() imply it won't replace the existing - // interface. - // - // request_name is used either way, with the builder or - // with the connection, but it must be done after the - // object server is setup. - if conn.object_server().at(path, single_instance).await != Ok(true) { - tracing::error!("Failed to serve dbus"); - std::process::exit(1); - } - if conn.request_name(App::APP_ID).await.is_err() { - tracing::error!("Failed to serve dbus"); - std::process::exit(1); - } - - #[cfg(feature = "smol")] - let handle = { - std::thread::spawn(move || { - let conn_clone = _conn.clone(); - - zbus::block_on(async move { - loop { - conn_clone.executor().tick().await; - } - }) - }) - }; - while let Some(mut msg) = rx.next().await { - if let Some(token) = msg.activation_token.take() { - if let Err(err) = output - .send(crate::Action::Cosmic(Action::Activate(token))) - .await - { - tracing::error!(?err, "Failed to send message"); - } - } - if let Err(err) = output.send(crate::Action::DbusActivation(msg)).await { - tracing::error!(?err, "Failed to send message"); - } - } - } - } else { - tracing::warn!("Failed to connect to dbus for single instance"); - } - - loop { - iced::futures::pending!(); - } - }), - ) -} - const EMBEDDED_FONTS: &[&[u8]] = &[ include_bytes!("../../res/open-sans/OpenSans-Light.ttf"), include_bytes!("../../res/open-sans/OpenSans-Regular.ttf"), diff --git a/src/dbus_activation.rs b/src/dbus_activation.rs new file mode 100644 index 00000000..84b5d000 --- /dev/null +++ b/src/dbus_activation.rs @@ -0,0 +1,214 @@ +// Copyright 2024 System76 +// SPDX-License-Identifier: MPL-2.0 + +use { + crate::ApplicationExt, + iced::Subscription, + iced_futures::futures::{ + channel::mpsc::{Receiver, Sender}, + SinkExt, + }, + std::{any::TypeId, collections::HashMap}, + url::Url, + zbus::{interface, proxy, zvariant::Value}, +}; + +pub fn subscription() -> Subscription> { + use iced_futures::futures::StreamExt; + iced_futures::Subscription::run_with_id( + TypeId::of::(), + iced::stream::channel(10, move |mut output| async move { + let mut single_instance: DbusActivation = DbusActivation::new(); + let mut rx = single_instance.rx(); + if let Ok(builder) = zbus::ConnectionBuilder::session() { + let path: String = format!("/{}", App::APP_ID.replace('.', "/")); + if let Ok(conn) = builder.build().await { + // XXX Setup done this way seems to be more reliable. + // + // the docs for serve_at seem to imply it will replace the + // existing interface at the requested path, but it doesn't + // seem to work that way all the time. The docs for + // object_server().at() imply it won't replace the existing + // interface. + // + // request_name is used either way, with the builder or + // with the connection, but it must be done after the + // object server is setup. + if conn.object_server().at(path, single_instance).await != Ok(true) { + tracing::error!("Failed to serve dbus"); + std::process::exit(1); + } + if conn.request_name(App::APP_ID).await.is_err() { + tracing::error!("Failed to serve dbus"); + std::process::exit(1); + } + + #[cfg(feature = "smol")] + let handle = { + std::thread::spawn(move || { + let conn_clone = _conn.clone(); + + zbus::block_on(async move { + loop { + conn_clone.executor().tick().await; + } + }) + }) + }; + while let Some(mut msg) = rx.next().await { + if let Some(token) = msg.activation_token.take() { + if let Err(err) = output + .send(crate::Action::Cosmic(crate::app::Action::Activate(token))) + .await + { + tracing::error!(?err, "Failed to send message"); + } + } + if let Err(err) = output.send(crate::Action::DbusActivation(msg)).await { + tracing::error!(?err, "Failed to send message"); + } + } + } + } else { + tracing::warn!("Failed to connect to dbus for single instance"); + } + + loop { + iced::futures::pending!(); + } + }), + ) +} + +#[derive(Debug, Clone)] +pub struct Message> { + pub activation_token: Option, + pub desktop_startup_id: Option, + pub msg: Details, +} + +#[derive(Debug, Clone)] +pub enum Details> { + Activate, + Open { + url: Vec, + }, + /// action can be deserialized as Flags + ActivateAction { + action: Action, + args: Args, + }, +} + +#[derive(Debug, Default)] +pub struct DbusActivation(Option>); + +impl DbusActivation { + #[must_use] + pub fn new() -> Self { + Self(None) + } + + pub fn rx(&mut self) -> Receiver { + let (tx, rx) = iced_futures::futures::channel::mpsc::channel(10); + self.0 = Some(tx); + rx + } +} + +#[proxy(interface = "org.freedesktop.DbusActivation", assume_defaults = true)] +pub trait DbusActivationInterface { + /// Activate the application. + fn activate(&mut self, platform_data: HashMap<&str, Value<'_>>) -> zbus::Result<()>; + + /// Open the given URIs. + fn open( + &mut self, + uris: Vec<&str>, + platform_data: HashMap<&str, Value<'_>>, + ) -> zbus::Result<()>; + + /// Activate the given action. + fn activate_action( + &mut self, + action_name: &str, + parameter: Vec<&str>, + platform_data: HashMap<&str, Value<'_>>, + ) -> zbus::Result<()>; +} + +#[interface(name = "org.freedesktop.DbusActivation")] +impl DbusActivation { + async fn activate(&mut self, platform_data: HashMap<&str, Value<'_>>) { + if let Some(tx) = &mut self.0 { + let _ = tx + .send(Message { + activation_token: platform_data.get("activation-token").and_then(|t| match t { + Value::Str(t) => Some(t.to_string()), + _ => None, + }), + desktop_startup_id: platform_data.get("desktop-startup-id").and_then( + |t| match t { + Value::Str(t) => Some(t.to_string()), + _ => None, + }, + ), + msg: Details::Activate, + }) + .await; + } + } + + async fn open(&mut self, uris: Vec<&str>, platform_data: HashMap<&str, Value<'_>>) { + if let Some(tx) = &mut self.0 { + let _ = tx + .send(Message { + activation_token: platform_data.get("activation-token").and_then(|t| match t { + Value::Str(t) => Some(t.to_string()), + _ => None, + }), + desktop_startup_id: platform_data.get("desktop-startup-id").and_then( + |t| match t { + Value::Str(t) => Some(t.to_string()), + _ => None, + }, + ), + msg: Details::Open { + url: uris.iter().filter_map(|u| Url::parse(u).ok()).collect(), + }, + }) + .await; + } + } + + async fn activate_action( + &mut self, + action_name: &str, + parameter: Vec<&str>, + platform_data: HashMap<&str, Value<'_>>, + ) { + if let Some(tx) = &mut self.0 { + let _ = tx + .send(Message { + activation_token: platform_data.get("activation-token").and_then(|t| match t { + Value::Str(t) => Some(t.to_string()), + _ => None, + }), + desktop_startup_id: platform_data.get("desktop-startup-id").and_then( + |t| match t { + Value::Str(t) => Some(t.to_string()), + _ => None, + }, + ), + msg: Details::ActivateAction { + action: action_name.to_string(), + args: parameter + .iter() + .map(std::string::ToString::to_string) + .collect(), + }, + }) + .await; + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 397357d8..0515911d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,9 +34,6 @@ pub mod core; #[doc(inline)] pub use core::Core; -pub use iced::Task; -pub mod task; - pub mod config; #[doc(inline)] @@ -45,6 +42,11 @@ pub use cosmic_config; #[doc(inline)] pub use cosmic_theme; +#[cfg(feature = "single-instance")] +pub mod dbus_activation; +#[cfg(feature = "single-instance")] +pub use dbus_activation::DbusActivation; + #[cfg(feature = "desktop")] pub mod desktop; @@ -99,6 +101,9 @@ pub use cctk; pub mod surface; +pub use iced::Task; +pub mod task; + pub mod theme; #[doc(inline)] diff --git a/src/surface/action.rs b/src/surface/action.rs index 21709e74..af7cc2de 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: MPL-2.0 use super::Action; +#[cfg(feature = "winit")] use crate::Application; + use std::{any::Any, sync::Arc}; /// Used to produce a destroy popup message from within a widget.