// SPDX-License-Identifier: AGPL-3.0-or-later #[cfg(target_os = "linux")] use std::{collections::HashMap, time::Duration}; #[cfg(target_os = "linux")] use futures_lite::{FutureExt, StreamExt, future}; #[cfg(target_os = "linux")] use zbus::{ MatchRule, MessageStream, Proxy, message::Type as MessageType, zvariant::{OwnedObjectPath, Value}, }; #[cfg(target_os = "linux")] use crate::portal::{REQUEST_INTERFACE, mint_token, request_path}; pub const PORTAL_DESTINATION: &str = "org.freedesktop.portal.Desktop"; pub const PORTAL_PATH: &str = "/org/freedesktop/portal/desktop"; pub const BACKGROUND_INTERFACE: &str = "org.freedesktop.portal.Background"; #[cfg(target_os = "linux")] pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(5 * 60); #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct RequestOptions { pub reason: Option, pub autostart: bool, pub commandline: Vec, pub dbus_activatable: bool, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct RequestResult { pub response: u32, pub background: bool, pub autostart: bool, } impl RequestResult { pub fn cancelled(&self) -> bool { self.response != 0 } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum BackgroundError { DbusError, PortalTimeout, InvalidReply, SendFailed, } impl std::fmt::Display for BackgroundError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = match self { Self::DbusError => "DbusError", Self::PortalTimeout => "PortalTimeout", Self::InvalidReply => "InvalidReply", Self::SendFailed => "SendFailed", }; f.write_str(s) } } #[cfg(target_os = "linux")] pub fn request_background(options: RequestOptions) -> Result { future::block_on(request_background_async(options)) } #[cfg(target_os = "linux")] async fn request_background_async( options: RequestOptions, ) -> Result { let conn = zbus::Connection::session() .await .map_err(|_| BackgroundError::DbusError)?; let unique_owned = conn .unique_name() .ok_or(BackgroundError::DbusError)? .to_owned(); let unique_name = unique_owned.as_str().to_string(); let handle_token = mint_token("fluxer_bg"); let expected_path = request_path(&unique_name, &handle_token); let rule = MatchRule::builder() .msg_type(MessageType::Signal) .interface(REQUEST_INTERFACE) .map_err(|_| BackgroundError::DbusError)? .member("Response") .map_err(|_| BackgroundError::DbusError)? .path(expected_path.clone()) .map_err(|_| BackgroundError::DbusError)? .build(); let mut stream = MessageStream::for_match_rule(rule, &conn, Some(8)) .await .map_err(|_| BackgroundError::DbusError)?; send_call(&conn, &handle_token, &options, &expected_path).await?; loop { let timeout = async { async_io::Timer::after(REQUEST_TIMEOUT).await; None::> }; match stream.next().or(timeout).await { Some(Ok(message)) => { if let Some(parsed) = parse_request_response(&message) { return Ok(parsed); } } Some(Err(_)) => return Err(BackgroundError::DbusError), None => return Err(BackgroundError::PortalTimeout), } } } #[cfg(target_os = "linux")] fn parse_request_response(message: &zbus::Message) -> Option { let body = message.body(); let (response, results): (u32, HashMap) = body.deserialize().ok()?; Some(RequestResult { response, background: bool_result(&results, "background").unwrap_or(false), autostart: bool_result(&results, "autostart").unwrap_or(false), }) } #[cfg(target_os = "linux")] fn bool_result(results: &HashMap, key: &str) -> Option { results .get(key) .and_then(|value| bool_from_value(crate::kwin::value_of_owned(value))) } #[cfg(target_os = "linux")] fn bool_from_value(value: &Value<'_>) -> Option { match value { Value::Bool(v) => Some(*v), Value::Value(inner) => bool_from_value(inner), _ => None, } } #[cfg(target_os = "linux")] async fn send_call( conn: &zbus::Connection, handle_token: &str, options: &RequestOptions, expected_path: &str, ) -> Result<(), BackgroundError> { let proxy = Proxy::new(conn, PORTAL_DESTINATION, PORTAL_PATH, BACKGROUND_INTERFACE) .await .map_err(|_| BackgroundError::DbusError)?; let mut vardict: HashMap<&str, Value<'_>> = HashMap::new(); vardict.insert("handle_token", Value::new(handle_token)); vardict.insert("autostart", Value::new(options.autostart)); if let Some(reason) = options.reason.as_deref() { vardict.insert("reason", Value::new(reason)); } if !options.commandline.is_empty() { let commandline: Vec<&str> = options.commandline.iter().map(String::as_str).collect(); vardict.insert("commandline", Value::new(commandline)); } if options.dbus_activatable { vardict.insert("dbus-activatable", Value::new(true)); } let reply_path: OwnedObjectPath = proxy .call("RequestBackground", &("", vardict)) .await .map_err(|_| BackgroundError::SendFailed)?; if !reply_path.as_str().is_empty() && reply_path.as_str() != expected_path { return Err(BackgroundError::InvalidReply); } Ok(()) } #[cfg(not(target_os = "linux"))] pub fn request_background(_options: RequestOptions) -> Result { Err(BackgroundError::DbusError) } #[cfg(test)] mod tests { use super::*; #[test] fn default_request_does_not_autostart() { let opts = RequestOptions::default(); assert!(!opts.autostart); assert!(opts.commandline.is_empty()); } #[test] fn nonzero_response_is_cancelled() { let result = RequestResult { response: 1, background: false, autostart: false, }; assert!(result.cancelled()); } }