From 19f7923b842f0015d2303fb89450d7f34870cc06 Mon Sep 17 00:00:00 2001 From: Paul Delafosse Date: Thu, 2 Jun 2022 14:38:30 +0200 Subject: [PATCH] feat: launch desktop entries via dbus --- Cargo.toml | 10 +++ examples/de_launch.rs | 13 +++ examples/{example.rs => de_list.rs} | 0 src/exec/dbus.rs | 70 +++++++++++++++++ src/exec/error.rs | 3 + src/exec/mod.rs | 118 ++++++++++++++++++---------- 6 files changed, 171 insertions(+), 43 deletions(-) create mode 100644 examples/de_launch.rs rename examples/{example.rs => de_list.rs} (100%) create mode 100644 src/exec/dbus.rs diff --git a/Cargo.toml b/Cargo.toml index eb30d90..5277647 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,16 @@ memchr = "2" thiserror = "1" xdg = "2.4.0" udev = "0.6.3" +zbus = "2.2.0" [dev-dependencies] speculoos = "0.9.0" + +[[example]] +name = "de-launch" +path = "examples/de_launch.rs" + + +[[example]] +name = "de-list" +path = "examples/de_list.rs" diff --git a/examples/de_launch.rs b/examples/de_launch.rs new file mode 100644 index 0000000..8621987 --- /dev/null +++ b/examples/de_launch.rs @@ -0,0 +1,13 @@ +use freedesktop_desktop_entry::DesktopEntry; +use std::path::PathBuf; +use std::{env, fs}; + +fn main() { + let args: Vec = env::args().collect(); + let path = &args.get(1).expect("Not enough arguments"); + let path = PathBuf::from(path); + let input = fs::read_to_string(&path).expect("Failed to read file"); + let de = DesktopEntry::decode(path.as_path(), &input).expect("Error decoding desktop entry"); + de.launch(&[], false) + .expect("Failed to run desktop entry"); +} diff --git a/examples/example.rs b/examples/de_list.rs similarity index 100% rename from examples/example.rs rename to examples/de_list.rs diff --git a/src/exec/dbus.rs b/src/exec/dbus.rs new file mode 100644 index 0000000..84b700c --- /dev/null +++ b/src/exec/dbus.rs @@ -0,0 +1,70 @@ +use crate::exec::error::ExecError; +use crate::exec::graphics::Gpus; +use crate::DesktopEntry; +use std::collections::HashMap; +use zbus::blocking::Connection; +use zbus::dbus_proxy; +use zbus::names::OwnedBusName; +use zbus::zvariant::{OwnedValue, Str}; + +#[dbus_proxy(interface = "org.freedesktop.Application")] +trait Application { + fn activate(&self, platform_data: HashMap) -> zbus::Result<()>; + fn activate_action( + &self, + action_name: &str, + parameters: &[OwnedValue], + platform_data: HashMap, + ) -> zbus::Result<()>; + fn open(&self, uris: &[&str], platform_data: HashMap) -> zbus::Result<()>; +} + +impl DesktopEntry<'_> { + pub(crate) fn dbus_launch( + &self, + conn: &Connection, + uris: &[&str], + prefer_non_default_gpu: bool, + ) -> Result<(), ExecError> { + let dbus_path = self.appid.replace('.', "/"); + let dbus_path = format!("/{dbus_path}"); + let app_proxy = ApplicationProxyBlocking::builder(conn) + .destination(self.appid)? + .path(dbus_path.as_str())? + .build()?; + + let mut platform_data = HashMap::new(); + if prefer_non_default_gpu { + let gpus = Gpus::load(); + if let Some(gpu) = gpus.non_default() { + for (opt, value) in gpu.launch_options() { + platform_data.insert(opt, OwnedValue::from(Str::from(value.as_str()))); + } + } + } + + if !uris.is_empty() { + app_proxy.open(uris, platform_data)?; + } else { + app_proxy.activate(platform_data)?; + } + + Ok(()) + } + + pub(crate) fn is_bus_actionable(&self, conn: &Connection) -> bool { + let dbus_proxy = zbus::blocking::fdo::DBusProxy::new(conn); + + if dbus_proxy.is_err() { + return false; + } + + let dbus_proxy = dbus_proxy.unwrap(); + let dbus_names = dbus_proxy.list_activatable_names().unwrap(); + + dbus_names + .into_iter() + .map(OwnedBusName::into_inner) + .any(|name| name.as_str() == self.appid) + } +} diff --git a/src/exec/error.rs b/src/exec/error.rs index 27b5f30..37645a2 100644 --- a/src/exec/error.rs +++ b/src/exec/error.rs @@ -28,4 +28,7 @@ pub enum ExecError<'a> { #[error("Exec key not found in desktop entry '{0:?}'")] MissingExecKey(&'a Path), + + #[error("Failed to launch aplication via dbus: {0}")] + DBusError(#[from] zbus::Error), } diff --git a/src/exec/mod.rs b/src/exec/mod.rs index d021f7a..9764aa4 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -4,21 +4,29 @@ use crate::DesktopEntry; use std::convert::TryFrom; use std::path::PathBuf; use std::process::Command; +use zbus::blocking::Connection; +mod dbus; pub mod error; mod graphics; impl DesktopEntry<'_> { /// Execute the given desktop entry `Exec` key with either the default gpu or /// the alternative one if available. - pub fn launch( - &self, - filename: Option<&str>, - filenames: &[&str], - url: Option<&str>, - urls: &[&str], - prefer_non_default_gpu: bool, - ) -> Result<(), ExecError> { + pub fn launch(&self, uris: &[&str], prefer_non_default_gpu: bool) -> Result<(), ExecError> { + match Connection::session() { + Ok(conn) => { + if self.is_bus_actionable(&conn) { + self.dbus_launch(&conn, uris, prefer_non_default_gpu) + } else { + self.shell_launch(uris, prefer_non_default_gpu) + } + } + Err(_) => self.shell_launch(uris, prefer_non_default_gpu), + } + } + + fn shell_launch(&self, uris: &[&str], prefer_non_default_gpu: bool) -> Result<(), ExecError> { let exec = self.exec(); if exec.is_none() { return Err(ExecError::MissingExecKey(self.path)); @@ -42,7 +50,7 @@ impl DesktopEntry<'_> { exec_args.push(arg); } - let exec_args = self.get_args(filename, filenames, url, urls, exec_args); + let exec_args = self.get_args(uris, exec_args); if exec_args.is_empty() { return Err(ExecError::EmptyExecString); @@ -63,8 +71,8 @@ impl DesktopEntry<'_> { cmd } .args(args) - .output()? - .status + .spawn()? + .try_wait()? } else { let mut cmd = Command::new(shell); @@ -74,44 +82,33 @@ impl DesktopEntry<'_> { cmd } .args(&["-c", &exec_args]) - .output()? - .status + .spawn()? + .try_wait()? }; - if !status.success() { - return Err(ExecError::NonZeroStatusCode { - status: status.code(), - exec: exec.to_string(), - }); + if let Some(status) = status { + if !status.success() { + return Err(ExecError::NonZeroStatusCode { + status: status.code(), + exec: exec.to_string(), + }); + } } Ok(()) } // Replace field code with their values and ignore deprecated and unknown field codes - fn get_args( - &self, - filename: Option<&str>, - filenames: &[&str], - url: Option<&str>, - urls: &[&str], - exec_args: Vec, - ) -> Vec { + fn get_args(&self, uris: &[&str], exec_args: Vec) -> Vec { exec_args .iter() .filter_map(|arg| match arg { - ArgOrFieldCode::SingleFileName => filename.map(|filename| filename.to_string()), - ArgOrFieldCode::FileList => { - if !filenames.is_empty() { - Some(filenames.join(" ")) - } else { - None - } + ArgOrFieldCode::SingleFileName | ArgOrFieldCode::SingleUrl => { + uris.get(0).map(|filename| filename.to_string()) } - ArgOrFieldCode::SingleUrl => url.map(|url| url.to_string()), - ArgOrFieldCode::UrlList => { - if !urls.is_empty() { - Some(urls.join(" ")) + ArgOrFieldCode::FileList | ArgOrFieldCode::UrlList => { + if !uris.is_empty() { + Some(uris.join(" ")) } else { None } @@ -129,7 +126,6 @@ impl DesktopEntry<'_> { ArgOrFieldCode::DesktopFileLocation => { Some(self.path.to_string_lossy().to_string()) } - // Ignore deprecated field-codes ArgOrFieldCode::Arg(arg) => Some(arg.to_string()), }) .collect() @@ -222,7 +218,7 @@ mod test { let path = PathBuf::from("tests/entries/unmatched-quotes.desktop"); let input = fs::read_to_string(&path).unwrap(); let de = DesktopEntry::decode(path.as_path(), &input).unwrap(); - let result = de.launch(None, &[], None, &[], false); + let result = de.launch(&[], false); assert_that!(result) .is_err() @@ -234,7 +230,7 @@ mod test { let path = PathBuf::from("tests/entries/empty-exec.desktop"); let input = fs::read_to_string(&path).unwrap(); let de = DesktopEntry::decode(Path::new(path.as_path()), &input).unwrap(); - let result = de.launch(None, &[], None, &[], false); + let result = de.launch(&[], false); assert_that!(result) .is_err() @@ -247,7 +243,7 @@ mod test { let path = PathBuf::from("tests/entries/alacritty-simple.desktop"); let input = fs::read_to_string(&path).unwrap(); let de = DesktopEntry::decode(path.as_path(), &input).unwrap(); - let result = de.launch(None, &[], None, &[], false); + let result = de.launch(&[], false); assert_that!(result).is_ok(); } @@ -258,7 +254,7 @@ mod test { let path = PathBuf::from("tests/entries/non-terminal-cmd.desktop"); let input = fs::read_to_string(&path).unwrap(); let de = DesktopEntry::decode(path.as_path(), &input).unwrap(); - let result = de.launch(None, &[], None, &[], false); + let result = de.launch(&[], false); assert_that!(result).is_ok(); } @@ -269,7 +265,43 @@ mod test { let path = PathBuf::from("tests/entries/non-terminal-cmd.desktop"); let input = fs::read_to_string(&path).unwrap(); let de = DesktopEntry::decode(path.as_path(), &input).unwrap(); - let result = de.launch(None, &[], None, &[], false); + let result = de.launch(&[], false); + + assert_that!(result).is_ok(); + } + + #[test] + #[ignore = "Needs a desktop environment with nvim installed, run locally only"] + fn should_launch_with_field_codes() { + let path = PathBuf::from("/usr/share/applications/nvim.desktop"); + let input = fs::read_to_string(&path).unwrap(); + let de = DesktopEntry::decode(path.as_path(), &input).unwrap(); + let result = de.launch(&["src/lib.rs"], false); + + assert_that!(result).is_ok(); + } + + #[test] + #[ignore = "Needs a desktop environment with gnome Books installed, run locally only"] + fn should_launch_with_dbus() { + let path = PathBuf::from("/usr/share/applications/org.gnome.Books.desktop"); + let input = fs::read_to_string(&path).unwrap(); + let de = DesktopEntry::decode(path.as_path(), &input).unwrap(); + let result = de.launch(&[], false); + + assert_that!(result).is_ok(); + } + + #[test] + #[ignore = "Needs a desktop environment with Nautilus installed, run locally only"] + fn should_launch_with_dbus_and_field_codes() { + let path = PathBuf::from("/usr/share/applications/org.gnome.Nautilus.desktop"); + let input = fs::read_to_string(&path).unwrap(); + let de = DesktopEntry::decode(path.as_path(), &input).unwrap(); + let path = std::env::current_dir().unwrap(); + let path = path.to_string_lossy(); + let path = format!("file:///{path}"); + let result = de.launch(&[path.as_str()], false); assert_that!(result).is_ok(); }