From e3b32f8bce10a15e10105cbc7efc8b898ed53588 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 13 Nov 2023 16:28:48 -0500 Subject: [PATCH 1/3] wip: dbus activation & single instance support refactor: activation action with new trait for subcommand and args refactor: libcosmic can handle sending the activation request cleanup --- Cargo.toml | 7 +- src/app/core.rs | 3 + src/app/cosmic.rs | 15 ++ src/app/mod.rs | 347 +++++++++++++++++++++++++++++++++++++++++++- src/app/settings.rs | 4 + 5 files changed, 373 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 77684f4f480..bb75f9ff27c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,9 +18,9 @@ pipewire = ["ashpd?/pipewire"] # Enables keycode serialization serde-keycode = ["iced_core/serde"] # smol async runtime -smol = ["iced/smol"] +smol = ["iced/smol", "zbus?/async-io"] # Tokio async runtime -tokio = ["dep:tokio", "ashpd/tokio", "iced/tokio"] +tokio = ["dep:tokio", "ashpd/tokio", "iced/tokio", "zbus?/tokio"] # Wayland window support wayland = [ "ashpd?/wayland", @@ -40,6 +40,7 @@ winit_wgpu = ["winit", "wgpu"] xdg-portal = ["ashpd"] # XXX Use "a11y"; which is causing a panic currently applet = ["wayland", "tokio", "cosmic-panel-config", "ron"] +zbus = ["dep:zbus", "serde", "ron"] [dependencies] apply = "0.3.0" @@ -59,6 +60,8 @@ ashpd = { version = "0.5.0", default-features = false, optional = true } url = "2.4.0" unicode-segmentation = "1.6" css-color = "0.2.5" +zbus = {version = "3.14.1", default-features = false, optional = true} +serde = { version = "1.0.180", optional = true } [target.'cfg(unix)'.dependencies] freedesktop-icons = "0.2.4" diff --git a/src/app/core.rs b/src/app/core.rs index 591b79b63a4..02e18d3b9dd 100644 --- a/src/app/core.rs +++ b/src/app/core.rs @@ -62,6 +62,8 @@ pub struct Core { #[cfg(feature = "applet")] pub applet: crate::applet::Context, + + pub single_instance: bool, } impl Default for Core { @@ -104,6 +106,7 @@ impl Default for Core { }, #[cfg(feature = "applet")] applet: crate::applet::Context::default(), + single_instance: false, } } } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 040bf1675aa..b3d0548a4b2 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -55,6 +55,8 @@ pub enum Message { /// Capabilities the window manager supports #[cfg(feature = "wayland")] WmCapabilities(window::Id, WindowManagerCapabilities), + /// Activate the application + Activate(String), } #[derive(Default)] @@ -177,6 +179,12 @@ where }) .map(super::Message::Cosmic), window_events.map(super::Message::Cosmic), + #[cfg(feature = "zbus")] + self.app + .core() + .single_instance + .then(|| super::single_instance_subscription::()) + .unwrap_or_else(Subscription::none), ]) } @@ -356,6 +364,13 @@ impl Cosmic { }); } } + Message::Activate(token) => { + #[cfg(feature = "wayland")] + return iced_sctk::commands::activation::activate( + iced::window::Id::default(), + token, + ); + } } iced::Command::none() diff --git a/src/app/mod.rs b/src/app/mod.rs index 97f017d253b..fcfbc7c6b41 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -36,6 +36,8 @@ pub mod message { } } +use std::str::FromStr; + pub use self::command::Command; pub use self::core::Core; pub use self::settings::Settings; @@ -46,7 +48,15 @@ use apply::Apply; use iced::Subscription; use iced::{window, Application as IcedApplication}; pub use message::Message; - +use url::Url; +#[cfg(feature = "zbus")] +use { + iced_futures::futures::channel::mpsc::{Receiver, Sender}, + iced_futures::futures::SinkExt, + std::any::TypeId, + std::collections::HashMap, + zbus::{dbus_interface, dbus_proxy, zvariant::Value}, +}; /// Launch a COSMIC application with the given [`Settings`]. /// /// # Errors @@ -62,6 +72,7 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res core.set_scale_factor(settings.scale_factor); core.set_window_width(settings.size.0); core.set_window_height(settings.size.1); + core.single_instance = settings.single_instance; THEME.with(move |t| { let mut cosmic_theme = t.borrow_mut(); @@ -92,6 +103,7 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res size_limits: settings.size_limits, title: None, transparent: settings.transparent, + xdg_activation_token: std::env::var("XDG_ACTIVATION_TOKEN").ok(), ..SctkWindowSettings::default() }) }; @@ -110,6 +122,223 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res cosmic::Cosmic::::run(iced) } +#[cfg(feature = "zbus")] +#[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 = "zbus")] +#[derive(Debug, Default)] +pub struct DbusActivation(Option>); +#[cfg(feature = "zbus")] +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 = "zbus")] +#[dbus_proxy(interface = "org.freedesktop.DbusActivation")] +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 = "zbus")] +#[dbus_interface(interface = "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 = "zbus")] + +/// Launch a COSMIC application with the given [`Settings`]. +/// If the application is already running, the arguments will be passed to the +/// running instance. +/// # Errors +/// Returns error on application failure. +pub fn run_single_instance( + mut settings: Settings, + flags: App::Flags, +) -> iced::Result { + let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok(); + + let override_single = std::env::var("COSMIC_SINGLE_INSTANCE") + .map(|v| &v.to_lowercase() == "false" || &v == "0") + .unwrap_or_default(); + if override_single { + return run::(settings, flags); + } + + let path: String = format!("/{}", App::APP_ID.replace('.', "/")); + settings.single_instance = true; + + let Ok(conn) = zbus::blocking::Connection::session() else { + tracing::warn!("Failed to connect to dbus"); + return run::(settings, flags); + }; + + if DbusActivationInterfaceProxyBlocking::builder(&conn) + .destination(App::APP_ID) + .ok() + .and_then(|b| b.path(path).ok()) + .and_then(|b| b.destination(App::APP_ID).ok()) + .and_then(|b| b.build().ok()) + .is_some_and(|mut p| { + match { + let mut platform_data = HashMap::new(); + if let Some(activation_token) = activation_token { + platform_data.insert("activation-token", activation_token.into()); + } + if let Ok(startup_id) = std::env::var("DESKTOP_STARTUP_ID") { + platform_data.insert("desktop-startup-id", startup_id.into()); + } + if let Some(action) = flags.action() { + let action = action.to_string(); + p.activate_action(&action, flags.args(), platform_data) + } else { + p.activate(platform_data) + } + } { + Ok(()) => { + tracing::info!("Successfully activated another instance"); + true + } + Err(err) => { + tracing::warn!(?err, "Failed to activate another instance"); + false + } + } + }) + { + tracing::info!("Another instance is running"); + Ok(()) + } else { + run::(settings, flags) + } +} + +pub trait CosmicFlags { + type SubCommand: FromStr + ToString + std::fmt::Debug + Clone + Send + 'static; + type Args: TryFrom> + Into> + std::fmt::Debug + Clone + Send + 'static; + #[must_use] + fn action(&self) -> Option<&Self::SubCommand> { + None + } + + #[must_use] + fn args(&self) -> Vec<&str> { + Vec::new() + } +} /// An interactive cross-platform COSMIC application. #[allow(unused_variables)] @@ -120,9 +349,27 @@ where /// Default async executor to use with the app. type Executor: iced_futures::Executor; + #[cfg(feature = "zbus")] + /// Argument received [`Application::new`]. + type Flags: Clone + CosmicFlags; + + #[cfg(not(feature = "zbus"))] /// Argument received [`Application::new`]. type Flags: Clone; + #[cfg(feature = "zbus")] + /// Message type specific to our app. + type Message: Clone + + From< + DbusActivationDetails< + ::SubCommand, + ::Args, + >, + > + std::fmt::Debug + + Send + + 'static; + + #[cfg(not(feature = "zbus"))] /// Message type specific to our app. type Message: Clone + std::fmt::Debug + Send + 'static; @@ -378,3 +625,101 @@ impl ApplicationExt for App { .into() } } + +#[cfg(feature = "zbus")] +fn single_instance_subscription() -> Subscription> { + use iced_futures::futures::StreamExt; + + iced::subscription::channel( + TypeId::of::(), + 10, + |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(Message::Cosmic(cosmic::Message::Activate(token))) + .await + { + tracing::error!(?err, "Failed to send message"); + } + } + if let Some(msg) = match msg.msg { + DbusActivationDetails::Activate => { + Some(DbusActivationDetails::Activate) + } + DbusActivationDetails::Open { url } => { + Some(DbusActivationDetails::Open { url }) + } + DbusActivationDetails::ActivateAction { action, args } => { + if let (Ok(action), Ok(args)) = ( + ::SubCommand::from_str(&action), + ::Args::try_from(args), + ) { + Some(DbusActivationDetails::ActivateAction::< + ::SubCommand, + ::Args, + > { + action, + args, + }) + } else { + tracing::error!("Invalid action or args"); + None + } + } + } { + if let Err(err) = + output.send(Message::App(App::Message::from(msg))).await + { + tracing::error!(?err, "Failed to send message"); + } + } + } + } + } else { + tracing::warn!("Failed to connect to dbus for single instance"); + } + + loop { + iced::futures::pending!(); + } + }, + ) +} diff --git a/src/app/settings.rs b/src/app/settings.rs index 10ed4dd3bcb..1e4322355dd 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -62,6 +62,9 @@ pub struct Settings { /// Whether the application should exit when there are no open windows pub(crate) exit_on_close: bool, + + /// Only allow a single instance of the application to run + pub single_instance: bool, } impl Settings { @@ -97,6 +100,7 @@ impl Default for Settings { theme: crate::theme::system_preference(), transparent: false, exit_on_close: true, + single_instance: false, } } } From 49163c974d452b616d046b83aec91d6d0346e4c2 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 14 Nov 2023 16:55:06 -0500 Subject: [PATCH 2/3] feat: add helpers for getting and using activation tokens in applets refactor(applet): connect to privileged socket if available cleanup --- Cargo.toml | 8 +- src/app/cosmic.rs | 4 +- src/applet/mod.rs | 6 +- src/applet/token/mod.rs | 2 + src/applet/token/subscription.rs | 78 ++++++++++++ src/applet/token/wayland_handler.rs | 180 ++++++++++++++++++++++++++++ src/lib.rs | 5 +- src/process.rs | 31 +++++ src/widget/text_input/input.rs | 4 +- 9 files changed, 310 insertions(+), 8 deletions(-) create mode 100644 src/applet/token/mod.rs create mode 100644 src/applet/token/subscription.rs create mode 100644 src/applet/token/wayland_handler.rs create mode 100644 src/process.rs diff --git a/Cargo.toml b/Cargo.toml index bb75f9ff27c..b4209a2d445 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ animated-image = ["image", "dep:async-fs", "tokio?/io-util", "tokio?/fs"] debug = ["iced/debug"] # Enables pipewire support in ashpd, if ashpd is enabled pipewire = ["ashpd?/pipewire"] +# Enables process spawning helper +process = ["nix"] # Enables keycode serialization serde-keycode = ["iced_core/serde"] # smol async runtime @@ -27,7 +29,7 @@ wayland = [ "iced_runtime/wayland", "iced/wayland", "iced_sctk", - "sctk", + "cctk", ] # Render with wgpu wgpu = ["iced/wgpu", "iced_wgpu"] @@ -40,6 +42,7 @@ winit_wgpu = ["winit", "wgpu"] xdg-portal = ["ashpd"] # XXX Use "a11y"; which is causing a panic currently applet = ["wayland", "tokio", "cosmic-panel-config", "ron"] +applet-token = [] zbus = ["dep:zbus", "serde", "ron"] [dependencies] @@ -48,7 +51,7 @@ derive_setters = "0.1.5" lazy_static = "1.4.0" palette = "0.7.3" tokio = { version = "1.24.2", optional = true } -sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", optional = true, rev = "dc8c4a0" } +cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "5faec87", optional = true } slotmap = "1.0.6" fraction = "0.13.0" cosmic-config = { path = "cosmic-config" } @@ -60,6 +63,7 @@ ashpd = { version = "0.5.0", default-features = false, optional = true } url = "2.4.0" unicode-segmentation = "1.6" css-color = "0.2.5" +nix = { version = "0.26", optional = true } zbus = {version = "3.14.1", default-features = false, optional = true} serde = { version = "1.0.180", optional = true } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index b3d0548a4b2..257e43d218d 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -5,6 +5,8 @@ use super::{command, Application, ApplicationExt, Core, Subscription}; use crate::theme::{self, Theme, ThemeType, THEME}; use crate::widget::nav_bar; use crate::{keyboard_nav, Element}; +#[cfg(feature = "wayland")] +use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; #[cfg(feature = "wayland")] use iced::event::wayland::{self, WindowEvent}; @@ -15,8 +17,6 @@ use iced::window; use iced_runtime::command::Action; #[cfg(not(feature = "wayland"))] use iced_runtime::window::Action as WindowAction; -#[cfg(feature = "wayland")] -use sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; /// A message managed internally by COSMIC. #[derive(Clone, Debug)] diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 839a4eae991..04c87b2a464 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -1,12 +1,16 @@ +#[cfg(feature = "applet-token")] +pub mod token; + use crate::{ app::Core, + cctk::sctk, iced::{ self, alignment::{Horizontal, Vertical}, widget::Container, window, Color, Length, Limits, Rectangle, }, - iced_style, iced_widget, sctk, + iced_style, iced_widget, theme::{self, Button, THEME}, widget, Application, Element, Renderer, }; diff --git a/src/applet/token/mod.rs b/src/applet/token/mod.rs new file mode 100644 index 00000000000..fc4c09c96bf --- /dev/null +++ b/src/applet/token/mod.rs @@ -0,0 +1,2 @@ +pub mod subscription; +pub mod wayland_handler; diff --git a/src/applet/token/subscription.rs b/src/applet/token/subscription.rs new file mode 100644 index 00000000000..c48e12509a8 --- /dev/null +++ b/src/applet/token/subscription.rs @@ -0,0 +1,78 @@ +use crate::iced; +use crate::iced::subscription; +use crate::iced_futures::futures; +use cctk::sctk::reexports::calloop; +use futures::{ + channel::mpsc::{unbounded, UnboundedReceiver}, + SinkExt, StreamExt, +}; +use std::{fmt::Debug, hash::Hash, thread::JoinHandle}; + +use super::wayland_handler::wayland_handler; + +pub fn activation_token_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(TokenUpdate::Init(tx)).await; + State::Waiting(toplevel_rx, calloop_tx, handle) + } + State::Waiting(mut rx, tx, handle) => { + if handle.is_finished() { + _ = output.send(TokenUpdate::Finished).await; + return State::Finished; + } + if let Some(u) = rx.next().await { + _ = output.send(u).await; + State::Waiting(rx, tx, handle) + } else { + _ = output.send(TokenUpdate::Finished).await; + State::Finished + } + } + State::Finished => iced::futures::future::pending().await, + } +} + +#[derive(Clone, Debug)] +pub enum TokenUpdate { + Init(calloop::channel::Sender), + Finished, + ActivationToken { token: Option, exec: String }, +} + +#[derive(Clone, Debug)] +pub struct TokenRequest { + pub app_id: String, + pub exec: String, +} diff --git a/src/applet/token/wayland_handler.rs b/src/applet/token/wayland_handler.rs new file mode 100644 index 00000000000..cb795c8ad6d --- /dev/null +++ b/src/applet/token/wayland_handler.rs @@ -0,0 +1,180 @@ +use std::os::{ + fd::{FromRawFd, RawFd}, + unix::net::UnixStream, +}; + +use super::subscription::{TokenRequest, TokenUpdate}; +use cctk::{ + sctk::{ + self, + activation::{RequestData, RequestDataExt}, + reexports::{calloop, calloop_wayland_source::WaylandSource}, + seat::{SeatHandler, SeatState}, + }, + wayland_client::{ + self, + protocol::{wl_seat::WlSeat, wl_surface::WlSurface}, + }, +}; +use iced_futures::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, + queue_handle: QueueHandle, + registry_state: RegistryState, + activation_state: Option, + tx: UnboundedSender, + seat_state: SeatState, +} + +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(TokenUpdate::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) {} +} + +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(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(TokenUpdate::ActivationToken { token: None, exec }); + } + } + calloop::channel::Event::Closed => { + state.exit = true; + } + }) + .is_err() + { + return; + } + let registry_state = RegistryState::new(&globals); + let mut app_data = AppData { + exit: false, + tx, + seat_state: SeatState::new(&globals, &qh), + queue_handle: qh.clone(), + activation_state: ActivationState::bind::(&globals, &qh).ok(), + registry_state, + }; + + loop { + if app_data.exit { + break; + } + event_loop.dispatch(None, &mut app_data).unwrap(); + } +} + +sctk::delegate_activation!(AppData, ExecRequestData); +sctk::delegate_seat!(AppData); +sctk::delegate_registry!(AppData); diff --git a/src/lib.rs b/src/lib.rs index 2785e4e40a8..6a65e18d50e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,8 +55,11 @@ pub use iced_winit; pub mod icon_theme; pub mod keyboard_nav; +#[cfg(feature = "process")] +pub mod process; + #[cfg(feature = "wayland")] -pub use sctk; +pub use cctk; pub mod theme; pub use theme::{style, Theme}; diff --git a/src/process.rs b/src/process.rs new file mode 100644 index 00000000000..1f58531a2de --- /dev/null +++ b/src/process.rs @@ -0,0 +1,31 @@ +use std::process::{exit, Command, Stdio}; + +use nix::sys::wait::waitpid; +use nix::unistd::{fork, ForkResult}; + +/// Performs a double fork with setsid to spawn and detach a command. +pub fn spawn(mut command: Command) { + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + + unsafe { + match fork() { + Ok(ForkResult::Parent { child }) => { + let _res = waitpid(Some(child), None); + } + + Ok(ForkResult::Child) => { + let _res = nix::unistd::setsid(); + let _res = command.spawn(); + + exit(0); + } + + Err(why) => { + println!("failed to fork and spawn command: {}", why.desc()); + } + } + } +} diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 4cb948945ce..bbff1e254fc 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -41,9 +41,9 @@ use iced_runtime::command::platform_specific; use iced_runtime::Command; #[cfg(feature = "wayland")] -use iced_runtime::command::platform_specific::wayland::data_device::{DataFromMimeType, DndIcon}; +use cctk::sctk::reexports::client::protocol::wl_data_device_manager::DndAction; #[cfg(feature = "wayland")] -use sctk::reexports::client::protocol::wl_data_device_manager::DndAction; +use iced_runtime::command::platform_specific::wayland::data_device::{DataFromMimeType, DndIcon}; /// Creates a new [`TextInput`]. /// From 616d192b042f948abcabdff2b5a39e92df9ad570 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 17 Nov 2023 13:55:00 -0500 Subject: [PATCH 3/3] refactor: rename single-instance feature --- Cargo.toml | 2 +- src/app/cosmic.rs | 2 +- src/app/mod.rs | 24 ++++++++++++------------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b4209a2d445..678d44d3b87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ xdg-portal = ["ashpd"] # XXX Use "a11y"; which is causing a panic currently applet = ["wayland", "tokio", "cosmic-panel-config", "ron"] applet-token = [] -zbus = ["dep:zbus", "serde", "ron"] +single-instance = ["dep:zbus", "serde", "ron"] [dependencies] apply = "0.3.0" diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 257e43d218d..f820649f6c2 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -179,7 +179,7 @@ where }) .map(super::Message::Cosmic), window_events.map(super::Message::Cosmic), - #[cfg(feature = "zbus")] + #[cfg(feature = "single-instance")] self.app .core() .single_instance diff --git a/src/app/mod.rs b/src/app/mod.rs index fcfbc7c6b41..6ce284d2ac9 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -49,7 +49,7 @@ use iced::Subscription; use iced::{window, Application as IcedApplication}; pub use message::Message; use url::Url; -#[cfg(feature = "zbus")] +#[cfg(feature = "single-instance")] use { iced_futures::futures::channel::mpsc::{Receiver, Sender}, iced_futures::futures::SinkExt, @@ -122,7 +122,7 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res cosmic::Cosmic::::run(iced) } -#[cfg(feature = "zbus")] +#[cfg(feature = "single-instance")] #[derive(Debug, Clone)] pub struct DbusActivationMessage> { pub activation_token: Option, @@ -142,10 +142,10 @@ pub enum DbusActivationDetails> { args: Args, }, } -#[cfg(feature = "zbus")] +#[cfg(feature = "single-instance")] #[derive(Debug, Default)] pub struct DbusActivation(Option>); -#[cfg(feature = "zbus")] +#[cfg(feature = "single-instance")] impl DbusActivation { #[must_use] pub fn new() -> Self { @@ -159,7 +159,7 @@ impl DbusActivation { } } -#[cfg(feature = "zbus")] +#[cfg(feature = "single-instance")] #[dbus_proxy(interface = "org.freedesktop.DbusActivation")] pub trait DbusActivationInterface { /// Activate the application. @@ -181,7 +181,7 @@ pub trait DbusActivationInterface { ) -> zbus::Result<()>; } -#[cfg(feature = "zbus")] +#[cfg(feature = "single-instance")] #[dbus_interface(interface = "org.freedesktop.DbusActivation")] impl DbusActivation { async fn activate(&mut self, platform_data: HashMap<&str, Value<'_>>) { @@ -258,7 +258,7 @@ impl DbusActivation { } } -#[cfg(feature = "zbus")] +#[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 @@ -349,15 +349,15 @@ where /// Default async executor to use with the app. type Executor: iced_futures::Executor; - #[cfg(feature = "zbus")] + #[cfg(feature = "single-instance")] /// Argument received [`Application::new`]. type Flags: Clone + CosmicFlags; - #[cfg(not(feature = "zbus"))] + #[cfg(not(feature = "single-instance"))] /// Argument received [`Application::new`]. type Flags: Clone; - #[cfg(feature = "zbus")] + #[cfg(feature = "single-instance")] /// Message type specific to our app. type Message: Clone + From< @@ -369,7 +369,7 @@ where + Send + 'static; - #[cfg(not(feature = "zbus"))] + #[cfg(not(feature = "single-instance"))] /// Message type specific to our app. type Message: Clone + std::fmt::Debug + Send + 'static; @@ -626,7 +626,7 @@ impl ApplicationExt for App { } } -#[cfg(feature = "zbus")] +#[cfg(feature = "single-instance")] fn single_instance_subscription() -> Subscription> { use iced_futures::futures::StreamExt;