From 8578f4fcf629b312efdd9fda0ab743be52bb60a4 Mon Sep 17 00:00:00 2001 From: Maximiliano Sandoval R Date: Sun, 5 Jun 2022 00:02:26 +0200 Subject: [PATCH] Add activation_token Fixes: https://github.com/bilelmoussaoui/ashpd/issues/55 --- Cargo.toml | 2 +- src/activation_token/gtk3.rs | 22 ++++ src/activation_token/gtk4.rs | 26 +++++ src/activation_token/mod.rs | 116 +++++++++++++++++++++ src/activation_token/wayland.rs | 178 ++++++++++++++++++++++++++++++++ src/desktop/dynamic_launcher.rs | 14 ++- src/desktop/open_uri.rs | 61 +++++++---- src/lib.rs | 3 + 8 files changed, 400 insertions(+), 22 deletions(-) create mode 100644 src/activation_token/gtk3.rs create mode 100644 src/activation_token/gtk4.rs create mode 100644 src/activation_token/mod.rs create mode 100644 src/activation_token/wayland.rs diff --git a/Cargo.toml b/Cargo.toml index d5e561a4f..6cd20f094 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ tracing = {version = "0.1", optional = true} libc = {version = "0.2.94", optional = true} raw-window-handle = {version = "0.4", optional = true} wayland-client = {version = "0.30.0-beta.8", optional = true} -wayland-protocols = {version = "0.30.0-beta.8", optional = true, features = ["unstable", "client"]} +wayland-protocols = {version = "0.30.0-beta.8", optional = true, features = ["unstable", "client", "staging"]} wayland-backend = {version = "0.1.0-beta.8", optional = true, features = ["client_system"]} async-std = {version = "1.11", optional = true} tokio = {version = "1.17", features = ["fs", "io-util"], optional = true, default-features = false} diff --git a/src/activation_token/gtk3.rs b/src/activation_token/gtk3.rs new file mode 100644 index 000000000..4db482bea --- /dev/null +++ b/src/activation_token/gtk3.rs @@ -0,0 +1,22 @@ +use super::wayland::WaylandActivationToken; +use gtk3::glib::translate::ToGlibPtr; +use gtk3::prelude::*; +use gtk3::{gdk, glib}; +use wayland_client::{backend::ObjectId, protocol::wl_surface::WlSurface, Proxy}; +use wayland_protocols::xdg::activation::v1::client::xdg_activation_token_v1::XdgActivationTokenV1; + +#[derive(Debug)] +pub struct Gtk3ActivationToken { + pub(crate) token: String, +} + +impl Gtk3ActivationToken { + pub fn from_window(window: &impl glib::IsA) -> Option { + let display = window.as_ref().display(); + match display.backend() { + gdk::Backend::Wayland => todo!(), + gdk::Backend::X11 => unimplemented!(), + _ => None, + } + } +} diff --git a/src/activation_token/gtk4.rs b/src/activation_token/gtk4.rs new file mode 100644 index 000000000..ef2c60222 --- /dev/null +++ b/src/activation_token/gtk4.rs @@ -0,0 +1,26 @@ +use super::wayland::WaylandActivationToken; +use gtk4::{gdk, glib, prelude::*}; + +#[derive(Debug)] +pub struct Gtk4ActivationToken { + pub(crate) token: String, +} + +impl Gtk4ActivationToken { + pub fn from_native>(native: &N) -> Option { + match native.backend() { + gdk::Backend::Wayland => todo!(), + gdk::Backend::X11 => unimplemented!(), + _ => None, + } + } +} + +impl From for Gtk4ActivationToken { + fn from(mut wl_token: WaylandActivationToken) -> Self { + let token = std::mem::take(&mut wl_token.token); + // NOTE Safe unwrap, WlActivationToken has a inner set at construction. + + Self { token } + } +} diff --git a/src/activation_token/mod.rs b/src/activation_token/mod.rs new file mode 100644 index 000000000..9a7deacba --- /dev/null +++ b/src/activation_token/mod.rs @@ -0,0 +1,116 @@ +#[cfg(all(feature = "gtk3", feature = "wayland"))] +mod gtk3; +#[cfg(all(feature = "gtk3", feature = "wayland"))] +pub use self::gtk3::Gtk3ActivationToken; + +#[cfg(feature = "gtk4")] +mod gtk4; +#[cfg(feature = "gtk4")] +pub use self::gtk4::Gtk4ActivationToken; + +#[cfg(any(feature = "wayland"))] +mod wayland; +#[cfg(feature = "wayland")] +pub use wayland::WaylandActivationToken; + +use serde::{ser::Serializer, Serialize}; +use zbus::zvariant::Type; + +// TODO +/// See https://wayland.app/protocols/xdg-activation-v1 +#[derive(Type)] +#[zvariant(signature = "s")] +#[derive(Debug)] +pub enum ActivationToken { + #[cfg(feature = "wayland")] + #[doc(hidden)] + Wayland(WaylandActivationToken), + #[cfg(feature = "gtk4")] + #[doc(hidden)] + Gtk4(Gtk4ActivationToken), + #[cfg(all(feature = "gtk3", feature = "wayland"))] + #[doc(hidden)] + Gtk3(Gtk3ActivationToken), + #[doc(hidden)] + Raw(String), + #[doc(hidden)] + None, +} + +impl Serialize for ActivationToken { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl Default for ActivationToken { + fn default() -> Self { + Self::None + } +} + +impl ActivationToken { + #[cfg(feature = "wayland")] + /// Create an instance of [`ActivationToken`] from a Wayland surface and the + /// application's id. + pub async fn from_wayland_surface( + app_id: &str, + surface: &wayland_client::protocol::wl_surface::WlSurface, + ) -> Option { + let token = WaylandActivationToken::from_surface(app_id, surface).await?; + + Some(Self::Wayland(token)) + } + + #[cfg(feature = "wayland")] + /// Create an instance of [`ActivationToken`] from a raw Wayland surface and + /// the application's id. + /// + /// # Safety + /// + /// Both pointers have to be valid surface and display pointers. You must + /// ensure the `display_ptr` lives longer than the returned + /// `ActivationToken`. + pub async unsafe fn from_wayland_raw( + app_id: &str, + surface_ptr: *mut std::ffi::c_void, + display_ptr: *mut std::ffi::c_void, + ) -> Option { + let token = WaylandActivationToken::from_raw(app_id, surface_ptr, display_ptr).await?; + + Some(Self::Wayland(token)) + } + + #[cfg(feature = "gtk4")] + // TODO Maybe name from_display. + /// Creates a [`ActivationToken`] from a [`gtk4::Native`](https://docs.gtk.org/gtk4/class.Native.html). + pub fn from_native>(native: &N) -> Option { + let token = Gtk4ActivationToken::from_native(native)?; + + Some(Self::Gtk4(token)) + } + + #[cfg(all(feature = "gtk3", feature = "wayland"))] + /// Creates a [`ActivationToken`] from a [`IsA`](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gdk/struct.Window.html). + pub fn from_window(window: &impl ::gtk3::glib::IsA<::gtk3::gdk::Root>) -> Option { + let token = Gtk3ActivationToken::from_window(window)?; + + Some(Self::Gtk3(token)) + } + + pub(crate) fn as_str(&self) -> &str { + match self { + #[cfg(feature = "wayland")] + Self::Wayland(activation_token) => activation_token.token.as_str(), + #[cfg(feature = "gtk4")] + Self::Gtk4(activation_token) => activation_token.token.as_str(), + #[cfg(all(feature = "gtk3", feature = "wayland"))] + Self::Gtk3(activation_token) => activation_token.token.as_str(), + Self::Raw(string) => string.as_str(), + Self::None => "", + } + } +} diff --git a/src/activation_token/wayland.rs b/src/activation_token/wayland.rs new file mode 100644 index 000000000..d5a9a1f18 --- /dev/null +++ b/src/activation_token/wayland.rs @@ -0,0 +1,178 @@ +use wayland_backend::sys::client::Backend; +use wayland_client::{ + protocol::{wl_registry, wl_surface::WlSurface}, + Proxy, QueueHandle, +}; +use wayland_protocols::xdg::activation::v1::client::{ + xdg_activation_token_v1::{Event, XdgActivationTokenV1}, + xdg_activation_v1::XdgActivationV1, +}; + +// Supported versions. +const XDG_ACTIVATION_V1_VERSION: u32 = 1; + +#[derive(Debug, Default)] +pub struct WaylandActivationToken { + pub(crate) token: String, + wl_activation: Option, + wl_token: Option, +} + +impl Drop for WaylandActivationToken { + fn drop(&mut self) { + if let Some(wl_token) = self.wl_token.take() { + wl_token.destroy(); + } + + if let Some(wl_activation) = self.wl_activation.take() { + wl_activation.destroy(); + } + } +} + +impl WaylandActivationToken { + // Can be changed to display. + pub async fn from_surface(app_id: &str, surface: &WlSurface) -> Option { + let backend = surface.backend().upgrade()?; + let conn = wayland_client::Connection::from_backend(backend); + + Self::new_inner(app_id, conn, surface).await + } + + pub async unsafe fn from_raw( + app_id: &str, + surface_ptr: *mut std::ffi::c_void, + display_ptr: *mut std::ffi::c_void, + ) -> Option { + if surface_ptr.is_null() || display_ptr.is_null() { + return None; + } + + let backend = Backend::from_foreign_display(display_ptr as *mut _); + let conn = wayland_client::Connection::from_backend(backend); + let obj_id = wayland_backend::sys::client::ObjectId::from_ptr( + WlSurface::interface(), + surface_ptr as *mut _, + ) + .ok()?; + + let surface = WlSurface::from_id(&conn, obj_id).ok()?; + + Self::new_inner(app_id, conn, &surface).await + } + + async fn new_inner( + app_id: &str, + conn: wayland_client::Connection, + surface: &WlSurface, + ) -> Option { + let (sender, receiver) = futures::channel::oneshot::channel::>(); + + // Cheap clone, protocol objects are essentially smart pointers + let surface = surface.clone(); + let app_id = app_id.to_owned(); + std::thread::spawn(move || match wayland_export_token(&app_id, conn, &surface) { + Ok(window_handle) => sender.send(Some(window_handle)).unwrap(), + Err(_err) => { + #[cfg(feature = "tracing")] + tracing::info!("Could not get wayland window identifier: {_err}"); + sender.send(None).unwrap(); + } + }); + + receiver.await.unwrap() + } +} + +impl wayland_client::Dispatch for WaylandActivationToken { + fn event( + state: &mut Self, + _proxy: &XdgActivationTokenV1, + event: ::Event, + _data: &(), + _connhandle: &wayland_client::Connection, + _qhandle: &QueueHandle, + ) { + if let Event::Done { token } = event { + state.token = token; + } + } +} + +impl wayland_client::Dispatch for WaylandActivationToken { + fn event( + state: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _: &(), + _: &wayland_client::Connection, + qhandle: &QueueHandle, + ) { + if let wl_registry::Event::Global { + name, + interface, + version, + } = event + { + if &interface == "xdg_activation_v1" { + #[cfg(feature = "tracing")] + tracing::info!("Found wayland interface {interface} v{version}"); + let activation = registry + .bind::( + name, + version.min(XDG_ACTIVATION_V1_VERSION), + qhandle, + (), + ) + .unwrap(); + state.wl_activation = Some(activation); + } + } + } +} + +impl wayland_client::Dispatch for WaylandActivationToken { + fn event( + _state: &mut Self, + _activation: &XdgActivationV1, + _event: wayland_protocols::xdg::activation::v1::client::xdg_activation_v1::Event, + _: &(), + _: &wayland_client::Connection, + _qhandle: &QueueHandle, + ) { + } +} + +fn wayland_export_token( + app_id: &str, + conn: wayland_client::Connection, + surface: &WlSurface, +) -> Result> { + let display = conn.display(); + let mut event_queue = conn.new_event_queue(); + let mut state = WaylandActivationToken::default(); + let qhandle = event_queue.handle(); + display.get_registry(&qhandle, ())?; + event_queue.roundtrip(&mut state)?; + + if let Some(ref activation) = state.wl_activation { + let wl_token = activation.get_activation_token(&qhandle, ())?; + // TODO is this an APP ID in the traditional sense? + wl_token.set_app_id(app_id.to_string()); + wl_token.set_surface(surface); + // TODO wl_token.set_serial(serial, &seat); + // Requests a token. + wl_token.commit(); + + event_queue.roundtrip(&mut state)?; + state.wl_token = Some(wl_token); + }; + + if !state.token.is_empty() { + Ok(state) + } else { + #[cfg(feature = "tracing")] + tracing::error!("Failed to get a response from the wayland server"); + Err(Box::new(crate::Error::NoResponse)) + } +} diff --git a/src/desktop/dynamic_launcher.rs b/src/desktop/dynamic_launcher.rs index f54ce0d32..544a3bf2d 100644 --- a/src/desktop/dynamic_launcher.rs +++ b/src/desktop/dynamic_launcher.rs @@ -45,6 +45,7 @@ use zbus::zvariant::{self, SerializeDict, Type}; use super::{HandleToken, Icon, DESTINATION, PATH}; use crate::{ + activation_token::ActivationToken, helpers::{call_method, call_request_method, session_connection}, Error, WindowIdentifier, }; @@ -253,9 +254,16 @@ impl<'a> DynamicLauncherProxy<'a> { /// See also [`Launch`](https://flatpak.github.io/xdg-desktop-portal/index.html#gdbus-method-org-freedesktop-portal-DynamicLauncher.Launch). #[doc(alias = "Launch")] #[doc(alias = "xdp_portal_dynamic_launcher_launch")] - pub async fn launch(&self, desktop_file_id: &str) -> Result<(), Error> { - // TODO: handle activation_token - let options: HashMap<&str, zvariant::Value<'_>> = HashMap::new(); + pub async fn launch( + &self, + desktop_file_id: &str, + activation_token: Option, + ) -> Result<(), Error> { + let options: HashMap<&str, zvariant::Value<'_>> = if let Some(token) = activation_token { + HashMap::from([("activation_token", token.as_str().to_owned().into())]) + } else { + HashMap::new() + }; call_method(self.inner(), "Launch", &(desktop_file_id, &options)).await } diff --git a/src/desktop/open_uri.rs b/src/desktop/open_uri.rs index 346e9462f..9e6ac41a7 100644 --- a/src/desktop/open_uri.rs +++ b/src/desktop/open_uri.rs @@ -9,7 +9,7 @@ //! //! async fn run() -> ashpd::Result<()> { //! let file = File::open("/home/bilelmoussaoui/adwaita-day.jpg").unwrap(); -//! open_uri::open_file(&WindowIdentifier::default(), &file, false, true).await?; +//! open_uri::open_file(&WindowIdentifier::default(), &file, false, true, None).await?; //! Ok(()) //! } //! ``` @@ -27,7 +27,7 @@ //! let proxy = OpenURIProxy::new().await?; //! //! proxy -//! .open_file(&WindowIdentifier::default(), &file, false, true) +//! .open_file(&WindowIdentifier::default(), &file, false, true, None) //! .await?; //! Ok(()) //! } @@ -42,7 +42,7 @@ //! //! async fn run() -> ashpd::Result<()> { //! let directory = File::open("/home/bilelmoussaoui/Downloads").unwrap(); -//! open_uri::open_directory(&WindowIdentifier::default(), &directory).await?; +//! open_uri::open_directory(&WindowIdentifier::default(), &directory, None).await?; //! Ok(()) //! } //! ``` @@ -60,7 +60,7 @@ //! let proxy = OpenURIProxy::new().await?; //! //! proxy -//! .open_directory(&WindowIdentifier::default(), &directory) +//! .open_directory(&WindowIdentifier::default(), &directory, None) //! .await?; //! Ok(()) //! } @@ -102,6 +102,7 @@ use zbus::zvariant::{DeserializeDict, Fd, SerializeDict, Type}; use super::{HandleToken, DESTINATION, PATH}; use crate::{ + activation_token::ActivationToken, helpers::{call_basic_response_method, session_connection}, Error, WindowIdentifier, }; @@ -112,14 +113,14 @@ use crate::{ struct OpenDirOptions { /// A string that will be used as the last element of the handle. handle_token: HandleToken, - // Token to activate the chosen application. + /// Activation token of the application. activation_token: Option, } impl OpenDirOptions { - #[allow(dead_code)] - pub fn set_activation_token(&mut self, activation_token: &str) { - self.activation_token = Some(activation_token.to_owned()); + /// Sets the activation token for the application. + pub fn set_activation_token(&mut self, activation_token: &ActivationToken) { + self.activation_token = Some(activation_token.as_str().to_owned()); } } @@ -138,7 +139,7 @@ struct OpenFileOptions { /// Whether to ask the user to choose an app. If this is not passed, or /// false, the portal may use a default or pick the last choice. ask: Option, - // Token to activate the chosen application. + /// Activation token of the application. activation_token: Option, } @@ -155,9 +156,9 @@ impl OpenFileOptions { self } - #[allow(dead_code)] - pub fn set_activation_token(&mut self, activation_token: &str) { - self.activation_token = Some(activation_token.to_owned()); + /// Sets the activation token for the application. + pub fn set_activation_token(&mut self, activation_token: &ActivationToken) { + self.activation_token = Some(activation_token.as_str().to_owned()); } } @@ -194,6 +195,7 @@ impl<'a> OpenURIProxy<'a> { /// /// * `identifier` - Identifier for the application window. /// * `directory` - File descriptor for a file. + /// * `activation_token` - A token that can be used to activate the chosen application. /// /// # Specifications /// @@ -204,8 +206,12 @@ impl<'a> OpenURIProxy<'a> { &self, identifier: &WindowIdentifier, directory: &impl AsRawFd, + activation_token: Option<&ActivationToken>, ) -> Result<(), Error> { - let options = OpenDirOptions::default(); + let mut options = OpenDirOptions::default(); + if let Some(token) = activation_token { + options.set_activation_token(token); + }; call_basic_response_method( self.inner(), &options.handle_token, @@ -224,6 +230,7 @@ impl<'a> OpenURIProxy<'a> { /// * `writeable` - Whether the file should be writeable or not. /// * `ask` - Whether to always ask the user which application to use or /// not. + /// * `activation_token` - A token that can be used to activate the chosen application. /// /// # Specifications /// @@ -235,8 +242,12 @@ impl<'a> OpenURIProxy<'a> { file: &impl AsRawFd, writeable: bool, ask: bool, + activation_token: Option<&ActivationToken>, ) -> Result<(), Error> { - let options = OpenFileOptions::default().ask(ask).writeable(writeable); + let mut options = OpenFileOptions::default().ask(ask).writeable(writeable); + if let Some(token) = activation_token { + options.set_activation_token(token); + }; call_basic_response_method( self.inner(), &options.handle_token, @@ -255,6 +266,7 @@ impl<'a> OpenURIProxy<'a> { /// * `writeable` - Whether the file should be writeable or not. /// * `ask` - Whether to always ask the user which application to use or /// not. + /// * `activation_token` - A token that can be used to activate the chosen application. /// /// *Note* that `file` uris are explicitly not supported by this method. /// Use [`Self::open_file`] or [`Self::open_directory`] instead. @@ -270,8 +282,12 @@ impl<'a> OpenURIProxy<'a> { uri: &url::Url, writeable: bool, ask: bool, + activation_token: Option<&ActivationToken>, ) -> Result<(), Error> { - let options = OpenFileOptions::default().ask(ask).writeable(writeable); + let mut options = OpenFileOptions::default().ask(ask).writeable(writeable); + if let Some(token) = activation_token { + options.set_activation_token(token); + }; call_basic_response_method( self.inner(), &options.handle_token, @@ -289,9 +305,12 @@ pub async fn open_uri( uri: &url::Url, writeable: bool, ask: bool, + activation_token: Option<&ActivationToken>, ) -> Result<(), Error> { let proxy = OpenURIProxy::new().await?; - proxy.open_uri(identifier, uri, writeable, ask).await?; + proxy + .open_uri(identifier, uri, writeable, ask, activation_token) + .await?; Ok(()) } @@ -301,9 +320,12 @@ pub async fn open_file( file: &impl AsRawFd, writeable: bool, ask: bool, + activation_token: Option<&ActivationToken>, ) -> Result<(), Error> { let proxy = OpenURIProxy::new().await?; - proxy.open_file(identifier, file, writeable, ask).await?; + proxy + .open_file(identifier, file, writeable, ask, activation_token) + .await?; Ok(()) } @@ -312,8 +334,11 @@ pub async fn open_file( pub async fn open_directory( identifier: &WindowIdentifier, directory: &impl AsRawFd, + activation_token: Option<&ActivationToken>, ) -> Result<(), Error> { let proxy = OpenURIProxy::new().await?; - proxy.open_directory(identifier, directory).await?; + proxy + .open_directory(identifier, directory, activation_token) + .await?; Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 27940c8de..e4610f4d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,9 @@ mod helpers; pub use enumflags2; pub use zbus::{self, zvariant}; +mod activation_token; +pub use self::activation_token::ActivationToken; + /// Check whether the application is running inside a sandbox. /// /// The function checks whether the file `/.flatpak-info` exists, or if the app