From 25417a3409a793ad46b72654977098efee084d4f Mon Sep 17 00:00:00 2001 From: Paul Delafosse Date: Thu, 2 Jun 2022 12:21:17 +0200 Subject: [PATCH 1/7] feat: add launch function for desktop entries --- Cargo.toml | 4 + src/exec/error.rs | 31 +++ src/exec/graphics.rs | 207 ++++++++++++++++++ src/exec/mod.rs | 287 +++++++++++++++++++++++++ src/lib.rs | 12 +- tests/entries/alacritty-simple.desktop | 19 ++ tests/entries/empty-exec.desktop | 5 + tests/entries/non-terminal-cmd.desktop | 5 + tests/entries/terminal-cmd.desktop | 5 + tests/entries/unmatched-quotes.desktop | 5 + 10 files changed, 576 insertions(+), 4 deletions(-) create mode 100644 src/exec/error.rs create mode 100644 src/exec/graphics.rs create mode 100644 src/exec/mod.rs create mode 100644 tests/entries/alacritty-simple.desktop create mode 100644 tests/entries/empty-exec.desktop create mode 100644 tests/entries/non-terminal-cmd.desktop create mode 100644 tests/entries/terminal-cmd.desktop create mode 100644 tests/entries/unmatched-quotes.desktop diff --git a/Cargo.toml b/Cargo.toml index 560cc40..eb30d90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,7 @@ gettext-rs = { version = "0.7", features = ["gettext-system"]} memchr = "2" thiserror = "1" xdg = "2.4.0" +udev = "0.6.3" + +[dev-dependencies] +speculoos = "0.9.0" diff --git a/src/exec/error.rs b/src/exec/error.rs new file mode 100644 index 0000000..27b5f30 --- /dev/null +++ b/src/exec/error.rs @@ -0,0 +1,31 @@ +use std::env::VarError; +use std::io; +use std::path::Path; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ExecError<'a> { + #[error("Unmatched quote delimiter: '{exec}'")] + UnmatchedQuote { exec: String }, + + #[error("Exec string is empty")] + EmptyExecString, + + #[error("$SHELL environment variable is not set")] + ShellNotFound(#[from] VarError), + + #[error("Failed to run Exec command")] + IoError(#[from] io::Error), + + #[error("Exec command '{exec}' exited with status code '{status:?}'")] + NonZeroStatusCode { status: Option, exec: String }, + + #[error("Unknown field code: '{0}'")] + UnknownFieldCode(String), + + #[error("Deprecated field code: '{0}'")] + DeprecatedFieldCode(String), + + #[error("Exec key not found in desktop entry '{0:?}'")] + MissingExecKey(&'a Path), +} diff --git a/src/exec/graphics.rs b/src/exec/graphics.rs new file mode 100644 index 0000000..3709096 --- /dev/null +++ b/src/exec/graphics.rs @@ -0,0 +1,207 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: GPL-3.0-only + +use std::collections::HashSet; +use std::hash::{Hash, Hasher}; +use std::io; +use std::ops::Deref; +use std::path::{Path, PathBuf}; + +const VULKAN_ICD_PATH: &str = "/usr/share/vulkan/icd.d"; + +#[derive(Debug, Default)] +pub struct Gpus { + devices: Vec, + default: Option, +} + +impl Gpus { + // Get gpus via udev + pub fn load() -> Self { + let drivers = get_gpus(); + + let mut gpus = Gpus::default(); + for dev in drivers.unwrap() { + if dev.is_default { + gpus.default = Some(dev) + } else { + gpus.devices.push(dev) + } + } + + gpus + } + + /// `true` if there is at least one non default gpu + pub fn is_switchable(&self) -> bool { + self.default.is_some() && !self.devices.is_empty() + } + + /// Return the default gpu + pub fn get_default(&self) -> Option<&Dev> { + self.default.as_ref() + } + + /// Get the first non-default gpu, the current `PreferNonDefaultGpu` specification + /// Does not tell us which one should be used. Anyway most machine out there should have + /// only one discrete graphic card. + /// see: https://gitlab.freedesktop.org/xdg/xdg-specs/-/issues/59 + pub fn non_default(&self) -> Option<&Dev> { + self.devices.first() + } +} + +#[derive(Debug)] +pub struct Dev { + id: usize, + driver: Driver, + is_default: bool, + parent_path: PathBuf, +} + +impl Dev { + /// Get the environment variable to launch a program with the correct gpu settings + pub fn launch_options(&self) -> Vec<(String, String)> { + let dev_num = self.id.to_string(); + let mut options = vec![]; + + match self.driver { + Driver::Unknown | Driver::Amd(_) | Driver::Intel => { + options.push(("DRI_PRIME".into(), dev_num)) + } + Driver::Nvidia => { + options.push(("__GLX_VENDOR_LIBRARY_NAME".into(), "nvidia".into())); + options.push(("__NV_PRIME_RENDER_OFFLOAD".into(), "1".into())); + options.push(("__VK_LAYER_NV_optimus".into(), "NVIDIA_only".into())); + } + } + + match self.get_vulkan_icd_paths() { + Ok(vulkan_icd_paths) if !vulkan_icd_paths.is_empty() => { + options.push(("VK_ICD_FILENAMES".into(), vulkan_icd_paths.join(":"))) + } + Err(err) => eprintln!("Failed to open vulkan icd paths: {err}"), + _ => {} + } + + options + } + + // Lookup vulkan icd files and return the ones matching the driver in use + fn get_vulkan_icd_paths(&self) -> io::Result> { + let vulkan_icd_paths = dirs::data_dir() + .expect("local data dir does not exists") + .join("vulkan/icd.d"); + + let vulkan_icd_paths = &[Path::new(VULKAN_ICD_PATH), vulkan_icd_paths.as_path()]; + + let mut icd_paths = vec![]; + if let Some(driver) = self.driver.as_str() { + for path in vulkan_icd_paths { + if path.exists() { + for entry in path.read_dir()? { + let entry = entry?; + let path = entry.path(); + if path.is_file() { + let path_str = path.to_string_lossy(); + if path_str.contains(driver) { + icd_paths.push(path_str.to_string()) + } + } + } + } + } + } + + Ok(icd_paths) + } +} + +// Ensure we filter out "render" devices having the same parent as the card +impl Hash for Dev { + fn hash(&self, state: &mut H) { + state.write(self.parent_path.to_string_lossy().as_bytes()); + state.finish(); + } +} + +impl PartialEq for Dev { + fn eq(&self, other: &Self) -> bool { + self.parent_path == other.parent_path + } +} + +impl Eq for Dev {} + +#[derive(Debug)] +enum Driver { + Intel, + Amd(String), + Nvidia, + Unknown, +} + +impl Driver { + fn from_udev>(driver: Option) -> Driver { + match driver.as_deref() { + // For amd devices we need the name of the driver to get vulkan icd files + Some("radeon") => Driver::Amd("radeon".to_string()), + Some("amdgpu") => Driver::Amd("amdgpu".to_string()), + Some("nvidia") => Driver::Nvidia, + Some("iris") | Some("i915") | Some("i965") => Driver::Intel, + _ => Driver::Unknown, + } + } + + fn as_str(&self) -> Option<&str> { + match self { + Driver::Intel => Some("intel"), + Driver::Amd(driver) => Some(driver.as_str()), + Driver::Nvidia => Some("nvidia"), + Driver::Unknown => None, + } + } +} + +fn get_gpus() -> io::Result> { + let mut enumerator = udev::Enumerator::new()?; + let mut dev_map = HashSet::new(); + let mut drivers: Vec = enumerator + .scan_devices()? + .into_iter() + .filter(|dev| { + dev.devnode() + .map(|path| path.starts_with("/dev/dri")) + .unwrap_or(false) + }) + .filter_map(|dev| { + dev.parent().and_then(|parent| { + let id = dev.sysnum(); + let parent_path = parent.syspath().to_path_buf(); + let driver = parent.driver().map(|d| d.to_string_lossy().to_string()); + let driver = Driver::from_udev(driver); + + let is_default = parent + .attribute_value("boot_vga") + .map(|v| v == "1") + .unwrap_or(false); + + id.map(|id| Dev { + id, + driver, + is_default, + parent_path, + }) + }) + }) + .collect(); + + // Sort the devices by sysnum so we get card0, card1 first and ignore the other 3D devices + drivers.sort_by(|a, b| a.id.cmp(&b.id)); + + for dev in drivers { + dev_map.insert(dev); + } + + Ok(dev_map) +} diff --git a/src/exec/mod.rs b/src/exec/mod.rs new file mode 100644 index 0000000..817644d --- /dev/null +++ b/src/exec/mod.rs @@ -0,0 +1,287 @@ +use crate::exec::error::ExecError; +use crate::exec::graphics::Gpus; +use crate::DesktopEntry; +use std::convert::TryFrom; +use std::path::PathBuf; +use std::process::Command; + +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> { + let exec = self.exec(); + if exec.is_none() { + return Err(ExecError::MissingExecKey(self.path)); + } + + let exec = exec.unwrap(); + let exec = if let Some(unquoted_exec) = exec.strip_prefix('\"') { + unquoted_exec + .strip_suffix('\"') + .ok_or(ExecError::UnmatchedQuote { + exec: exec.to_string(), + })? + } else { + exec + }; + + let mut exec_args = vec![]; + + for arg in exec.split_ascii_whitespace() { + let arg = ArgOrFieldCode::try_from(arg)?; + exec_args.push(arg); + } + + let exec_args = self.get_args(filename, filenames, url, urls, exec_args); + + if exec_args.is_empty() { + return Err(ExecError::EmptyExecString); + } + + let exec_args = exec_args.join(" "); + let shell = std::env::var("SHELL")?; + + let status = if self.terminal() { + let (terminal, separator) = detect_terminal(); + let terminal = terminal.to_string_lossy(); + let args = format!("{terminal} {separator} {exec_args}"); + let args = ["-c", &args]; + let mut cmd = Command::new(shell); + if prefer_non_default_gpu { + with_non_default_gpu(cmd) + } else { + cmd + } + .args(args) + .output()? + .status + } else { + let mut cmd = Command::new(shell); + + if prefer_non_default_gpu { + with_non_default_gpu(cmd) + } else { + cmd + } + .args(&["-c", &exec_args]) + .output()? + .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 { + 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::SingleUrl => url.map(|url| url.to_string()), + ArgOrFieldCode::UrlList => { + if !urls.is_empty() { + Some(urls.join(" ")) + } else { + None + } + } + ArgOrFieldCode::IconKey => self.icon().map(ToString::to_string), + ArgOrFieldCode::TranslatedName => { + let locale = std::env::var("LANG").ok(); + if let Some(locale) = locale { + let locale = locale.split_once('.').map(|(locale, _)| locale); + self.name(locale).map(|locale| locale.to_string()) + } else { + None + } + } + ArgOrFieldCode::DesktopFileLocation => { + Some(self.path.to_string_lossy().to_string()) + } + // Ignore deprecated field-codes + ArgOrFieldCode::Arg(arg) => Some(arg.to_string()), + }) + .collect() + } +} + +fn with_non_default_gpu(mut cmd: Command) -> Command { + let gpus = Gpus::load(); + let gpu = if gpus.is_switchable() { + gpus.non_default() + } else { + gpus.get_default() + }; + + if let Some(gpu) = gpu { + for (opt, value) in gpu.launch_options() { + cmd.env(opt, value); + } + } + + cmd +} + +// either a command line argument or a field-code as described +// in https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables +enum ArgOrFieldCode<'a> { + SingleFileName, + FileList, + SingleUrl, + UrlList, + IconKey, + TranslatedName, + DesktopFileLocation, + Arg(&'a str), +} + +impl<'a> TryFrom<&'a str> for ArgOrFieldCode<'a> { + type Error = ExecError<'a>; + + fn try_from(value: &'a str) -> Result { + match value { + "%f" => Ok(ArgOrFieldCode::SingleFileName), + "%F" => Ok(ArgOrFieldCode::FileList), + "%u" => Ok(ArgOrFieldCode::SingleUrl), + "%U" => Ok(ArgOrFieldCode::UrlList), + "%i" => Ok(ArgOrFieldCode::IconKey), + "%c" => Ok(ArgOrFieldCode::TranslatedName), + "%k" => Ok(ArgOrFieldCode::DesktopFileLocation), + "%d" | "%D" | "%n" | "%N" | "%v" | "%m" => { + Err(ExecError::DeprecatedFieldCode(value.to_string())) + } + other if other.starts_with('%') => Err(ExecError::UnknownFieldCode(other.to_string())), + other => Ok(ArgOrFieldCode::Arg(other)), + } + } +} + +// Returns the default terminal emulator linked to `/usr/bin/x-terminal-emulator` +// or fallback to gnome terminal, then konsole +fn detect_terminal() -> (PathBuf, &'static str) { + use std::fs::read_link; + + const SYMLINK: &str = "/usr/bin/x-terminal-emulator"; + + if let Ok(found) = read_link(SYMLINK) { + let arg = if found.to_string_lossy().contains("gnome-terminal") { + "--" + } else { + "-e" + }; + + return (read_link(&found).unwrap_or(found), arg); + } + + let gnome_terminal = PathBuf::from("/usr/bin/gnome-terminal"); + if gnome_terminal.exists() { + (gnome_terminal, "--") + } else { + (PathBuf::from("/usr/bin/konsole"), "-e") + } +} + +#[cfg(test)] +mod test { + use crate::exec::error::ExecError; + use crate::exec::with_non_default_gpu; + use crate::DesktopEntry; + use speculoos::prelude::*; + use std::fs; + use std::path::{Path, PathBuf}; + use std::process::Command; + + #[test] + fn should_return_unmatched_quote_error() { + 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); + + assert_that!(result) + .is_err() + .matches(|err| matches!(err, ExecError::UnmatchedQuote { exec: _ })); + } + + #[test] + fn should_fail_if_exec_string_is_empty() { + 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); + + assert_that!(result) + .is_err() + .matches(|err| matches!(err, ExecError::EmptyExecString)); + } + + #[test] + #[ignore = "Needs a desktop environment and alacritty installed, run locally only"] + fn should_exec_simple_command() { + 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); + + assert_that!(result).is_ok(); + } + + #[test] + #[ignore = "Needs a desktop environment and alacritty and mesa-utils installed, run locally only"] + fn should_exec_complex_command() { + 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); + + assert_that!(result).is_ok(); + } + + #[test] + #[ignore = "Needs a desktop environment and alacritty and mesa-utils installed, run locally only"] + fn should_exec_terminal_command() { + 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); + + assert_that!(result).is_ok(); + } + + #[test] + fn should_build_command_with_gpu() { + let cmd = with_non_default_gpu(Command::new("glxgears")); + assert_that!(cmd.get_envs().collect::>()).is_not_empty(); + } +} diff --git a/src/lib.rs b/src/lib.rs index f0e0787..89c8a64 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ #[macro_use] extern crate thiserror; +pub mod exec; mod iter; pub use self::iter::Iter; @@ -288,7 +289,6 @@ pub enum PathSource { } impl PathSource { - /// Attempts to determine the PathSource for a given Path. /// Note that this is a best-effort guesting function, and its results should be treated as /// such (e.g.: non-canonical). @@ -304,15 +304,19 @@ impl PathSource { PathSource::SystemFlatpak } else if path.starts_with("/var/lib/snapd") { PathSource::SystemSnap - } else if path.starts_with("/nix/var/nix/profiles/default") || path.starts_with("/nix/store") { + } else if path.starts_with("/nix/var/nix/profiles/default") + || path.starts_with("/nix/store") + { PathSource::Nix } else if path.to_string_lossy().contains("/flatpak/") { PathSource::LocalFlatpak } else if path.starts_with(&data_home.as_path()) { PathSource::Local - } else if path.starts_with("/nix/var/nix/profiles/per-user") || path.to_string_lossy().contains(".nix") { + } else if path.starts_with("/nix/var/nix/profiles/per-user") + || path.to_string_lossy().contains(".nix") + { PathSource::LocalNix - } else { + } else { PathSource::Other(String::from("unknown")) } } diff --git a/tests/entries/alacritty-simple.desktop b/tests/entries/alacritty-simple.desktop new file mode 100644 index 0000000..704a72e --- /dev/null +++ b/tests/entries/alacritty-simple.desktop @@ -0,0 +1,19 @@ +[Desktop Entry] +Type=Application +TryExec=alacritty +Exec=alacritty +Icon=Alacritty +Terminal=false +Categories=System;TerminalEmulator; + +Name=Alacritty +GenericName=Terminal +Comment=A fast, cross-platform, OpenGL terminal emulator +StartupWMClass=Alacritty +Actions=New; + +X-Desktop-File-Install-Version=0.26 + +[Desktop Action New] +Name=New Terminal +Exec=alacritty diff --git a/tests/entries/empty-exec.desktop b/tests/entries/empty-exec.desktop new file mode 100644 index 0000000..e6bfa79 --- /dev/null +++ b/tests/entries/empty-exec.desktop @@ -0,0 +1,5 @@ +[Desktop Entry] +Exec= +Terminal=false +Type=Application +Name=Alacritty diff --git a/tests/entries/non-terminal-cmd.desktop b/tests/entries/non-terminal-cmd.desktop new file mode 100644 index 0000000..b8dc18b --- /dev/null +++ b/tests/entries/non-terminal-cmd.desktop @@ -0,0 +1,5 @@ +[Desktop Entry] +Exec=alacritty -e glxgears -info +Terminal=false +Type=Application +Name=Alacritty \ No newline at end of file diff --git a/tests/entries/terminal-cmd.desktop b/tests/entries/terminal-cmd.desktop new file mode 100644 index 0000000..cf26df6 --- /dev/null +++ b/tests/entries/terminal-cmd.desktop @@ -0,0 +1,5 @@ +[Desktop Entry] +Exec=glxgears -info +Terminal=true +Type=Application +Name=Alacritty \ No newline at end of file diff --git a/tests/entries/unmatched-quotes.desktop b/tests/entries/unmatched-quotes.desktop new file mode 100644 index 0000000..98bda6c --- /dev/null +++ b/tests/entries/unmatched-quotes.desktop @@ -0,0 +1,5 @@ +[Desktop Entry] +Exec="alacritty -e +Terminal=false +Type=Application +Name=Alacritty \ No newline at end of file From 3a8c65f2b922c13bf79a2cbada3a8453303b4f16 Mon Sep 17 00:00:00 2001 From: Paul Delafosse Date: Thu, 2 Jun 2022 14:38:30 +0200 Subject: [PATCH 2/7] 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 | 76 ++++++++++++++++++ src/exec/error.rs | 3 + src/exec/mod.rs | 118 ++++++++++++++++++---------- 6 files changed, 177 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..022584c --- /dev/null +++ b/src/exec/dbus.rs @@ -0,0 +1,76 @@ +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; + let dbus_names = dbus_proxy.list_activatable_names(); + + if dbus_names.is_err() { + return false; + } + + let dbus_names = dbus_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 817644d..ca51ad5 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() @@ -227,7 +223,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() @@ -239,7 +235,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() @@ -252,7 +248,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(); } @@ -263,7 +259,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(); } @@ -274,7 +270,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(); } From e82ca5b89aecc7152171f5bb2abbdce572fdffbc Mon Sep 17 00:00:00 2001 From: Paul Delafosse Date: Thu, 2 Jun 2022 14:55:56 +0200 Subject: [PATCH 3/7] chore: add MPD-2.0 license --- examples/de_launch.rs | 3 +-- src/exec/dbus.rs | 3 +++ src/exec/error.rs | 3 +++ src/exec/graphics.rs | 4 ++-- src/exec/mod.rs | 3 +++ 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/examples/de_launch.rs b/examples/de_launch.rs index 8621987..6ad5d01 100644 --- a/examples/de_launch.rs +++ b/examples/de_launch.rs @@ -8,6 +8,5 @@ fn main() { 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"); + de.launch(&[], false).expect("Failed to run desktop entry"); } diff --git a/src/exec/dbus.rs b/src/exec/dbus.rs index 022584c..9a14e8b 100644 --- a/src/exec/dbus.rs +++ b/src/exec/dbus.rs @@ -1,3 +1,6 @@ +// Copyright 2021 System76 +// SPDX-License-Identifier: MPL-2.0 + use crate::exec::error::ExecError; use crate::exec::graphics::Gpus; use crate::DesktopEntry; diff --git a/src/exec/error.rs b/src/exec/error.rs index 37645a2..7fd0320 100644 --- a/src/exec/error.rs +++ b/src/exec/error.rs @@ -1,3 +1,6 @@ +// Copyright 2021 System76 +// SPDX-License-Identifier: MPL-2.0 + use std::env::VarError; use std::io; use std::path::Path; diff --git a/src/exec/graphics.rs b/src/exec/graphics.rs index 3709096..9a0ff8c 100644 --- a/src/exec/graphics.rs +++ b/src/exec/graphics.rs @@ -1,5 +1,5 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: GPL-3.0-only +// Copyright 2021 System76 +// SPDX-License-Identifier: MPL-2.0 use std::collections::HashSet; use std::hash::{Hash, Hasher}; diff --git a/src/exec/mod.rs b/src/exec/mod.rs index ca51ad5..68b091f 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -1,3 +1,6 @@ +// Copyright 2021 System76 +// SPDX-License-Identifier: MPL-2.0 + use crate::exec::error::ExecError; use crate::exec::graphics::Gpus; use crate::DesktopEntry; From 8d06564f0a81e073309f8d6e7d0fa9a35189a0e9 Mon Sep 17 00:00:00 2001 From: Paul Delafosse Date: Fri, 3 Jun 2022 08:44:13 +0200 Subject: [PATCH 4/7] refactor: use compile time env var for vulkan icd base path --- README.md | 2 +- src/exec/dbus.rs | 2 +- src/exec/graphics.rs | 2 +- src/exec/mod.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a12f9bc..ae6ee3b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Freedesktop Desktop Entry Specification -This crate provides a library for efficiently parsing [Desktop Entry](https://specifications.freedesktop.org/desktop-entry-spec/latest/index.html) files. +This crate provides a library for efficiently parsing and launching [Desktop Entry](https://specifications.freedesktop.org/desktop-entry-spec/latest/index.html) files. ```rust use std::fs; diff --git a/src/exec/dbus.rs b/src/exec/dbus.rs index 9a14e8b..27f31b0 100644 --- a/src/exec/dbus.rs +++ b/src/exec/dbus.rs @@ -62,7 +62,7 @@ impl DesktopEntry<'_> { return false; } - let dbus_proxy = dbus_proxy; + let dbus_proxy = dbus_proxy.unwrap(); let dbus_names = dbus_proxy.list_activatable_names(); if dbus_names.is_err() { diff --git a/src/exec/graphics.rs b/src/exec/graphics.rs index 9a0ff8c..377e853 100644 --- a/src/exec/graphics.rs +++ b/src/exec/graphics.rs @@ -7,7 +7,7 @@ use std::io; use std::ops::Deref; use std::path::{Path, PathBuf}; -const VULKAN_ICD_PATH: &str = "/usr/share/vulkan/icd.d"; +const VULKAN_ICD_PATH: &str = std::env!("VULKAN_ICD_PATH", "must define system Vulkan ICD path (ex: `/usr/share/vulkan/icd.d`)"); #[derive(Debug, Default)] pub struct Gpus { diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 68b091f..fe12840 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -14,7 +14,7 @@ pub mod error; mod graphics; impl DesktopEntry<'_> { - /// Execute the given desktop entry `Exec` key with either the default gpu or + /// Launch the given desktop entry either via dbus or via its `Exec` key with the default gpu or /// the alternative one if available. pub fn launch(&self, uris: &[&str], prefer_non_default_gpu: bool) -> Result<(), ExecError> { match Connection::session() { From 8a6e90ab927ad00249d4a35b9b7734d6ae5b0823 Mon Sep 17 00:00:00 2001 From: Paul Delafosse Date: Fri, 3 Jun 2022 09:05:38 +0200 Subject: [PATCH 5/7] refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead --- examples/de_launch.rs | 2 +- src/exec/dbus.rs | 3 +-- src/exec/error.rs | 3 --- src/exec/mod.rs | 59 ++++++++++++++----------------------------- 4 files changed, 21 insertions(+), 46 deletions(-) diff --git a/examples/de_launch.rs b/examples/de_launch.rs index 6ad5d01..a99f8f9 100644 --- a/examples/de_launch.rs +++ b/examples/de_launch.rs @@ -8,5 +8,5 @@ fn main() { 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"); + de.launch(&[]).expect("Failed to run desktop entry"); } diff --git a/src/exec/dbus.rs b/src/exec/dbus.rs index 27f31b0..ecd270e 100644 --- a/src/exec/dbus.rs +++ b/src/exec/dbus.rs @@ -27,7 +27,6 @@ impl DesktopEntry<'_> { &self, conn: &Connection, uris: &[&str], - prefer_non_default_gpu: bool, ) -> Result<(), ExecError> { let dbus_path = self.appid.replace('.', "/"); let dbus_path = format!("/{dbus_path}"); @@ -37,7 +36,7 @@ impl DesktopEntry<'_> { .build()?; let mut platform_data = HashMap::new(); - if prefer_non_default_gpu { + if self.prefers_non_default_gpu() { let gpus = Gpus::load(); if let Some(gpu) = gpus.non_default() { for (opt, value) in gpu.launch_options() { diff --git a/src/exec/error.rs b/src/exec/error.rs index 7fd0320..dcac984 100644 --- a/src/exec/error.rs +++ b/src/exec/error.rs @@ -8,9 +8,6 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum ExecError<'a> { - #[error("Unmatched quote delimiter: '{exec}'")] - UnmatchedQuote { exec: String }, - #[error("Exec string is empty")] EmptyExecString, diff --git a/src/exec/mod.rs b/src/exec/mod.rs index fe12840..84d2b41 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -16,35 +16,26 @@ mod graphics; impl DesktopEntry<'_> { /// Launch the given desktop entry either via dbus or via its `Exec` key with the default gpu or /// the alternative one if available. - pub fn launch(&self, uris: &[&str], prefer_non_default_gpu: bool) -> Result<(), ExecError> { + pub fn launch(&self, uris: &[&str]) -> Result<(), ExecError> { match Connection::session() { Ok(conn) => { if self.is_bus_actionable(&conn) { - self.dbus_launch(&conn, uris, prefer_non_default_gpu) + self.dbus_launch(&conn, uris) } else { - self.shell_launch(uris, prefer_non_default_gpu) + self.shell_launch(uris) } } - Err(_) => self.shell_launch(uris, prefer_non_default_gpu), + Err(_) => self.shell_launch(uris), } } - fn shell_launch(&self, uris: &[&str], prefer_non_default_gpu: bool) -> Result<(), ExecError> { + fn shell_launch(&self, uris: &[&str]) -> Result<(), ExecError> { let exec = self.exec(); if exec.is_none() { return Err(ExecError::MissingExecKey(self.path)); } let exec = exec.unwrap(); - let exec = if let Some(unquoted_exec) = exec.strip_prefix('\"') { - unquoted_exec - .strip_suffix('\"') - .ok_or(ExecError::UnmatchedQuote { - exec: exec.to_string(), - })? - } else { - exec - }; let mut exec_args = vec![]; @@ -68,25 +59,25 @@ impl DesktopEntry<'_> { let args = format!("{terminal} {separator} {exec_args}"); let args = ["-c", &args]; let mut cmd = Command::new(shell); - if prefer_non_default_gpu { + if self.prefers_non_default_gpu() { with_non_default_gpu(cmd) } else { cmd } - .args(args) - .spawn()? - .try_wait()? + .args(args) + .spawn()? + .try_wait()? } else { let mut cmd = Command::new(shell); - if prefer_non_default_gpu { + if self.prefers_non_default_gpu() { with_non_default_gpu(cmd) } else { cmd } - .args(&["-c", &exec_args]) - .spawn()? - .try_wait()? + .args(&["-c", &exec_args]) + .spawn()? + .try_wait()? }; if let Some(status) = status { @@ -221,24 +212,12 @@ mod test { use std::path::{Path, PathBuf}; use std::process::Command; - #[test] - fn should_return_unmatched_quote_error() { - 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(&[], false); - - assert_that!(result) - .is_err() - .matches(|err| matches!(err, ExecError::UnmatchedQuote { exec: _ })); - } - #[test] fn should_fail_if_exec_string_is_empty() { 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(&[], false); + let result = de.launch(&[]); assert_that!(result) .is_err() @@ -251,7 +230,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(&[], false); + let result = de.launch(&[]); assert_that!(result).is_ok(); } @@ -262,7 +241,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(&[], false); + let result = de.launch(&[]); assert_that!(result).is_ok(); } @@ -273,7 +252,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(&[], false); + let result = de.launch(&[]); assert_that!(result).is_ok(); } @@ -284,7 +263,7 @@ mod test { 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); + let result = de.launch(&["src/lib.rs"]); assert_that!(result).is_ok(); } @@ -295,7 +274,7 @@ mod test { 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); + let result = de.launch(&[]); assert_that!(result).is_ok(); } From eaa21ff5d0c402e02c285d012687b3743b5df62d Mon Sep 17 00:00:00 2001 From: Paul Delafosse Date: Mon, 6 Jun 2022 10:30:38 +0200 Subject: [PATCH 6/7] fix: fork process instead of spawning with std::process::Command --- Cargo.toml | 1 + src/exec/dbus.rs | 6 +---- src/exec/graphics.rs | 5 +++- src/exec/mod.rs | 58 ++++++++++++++++++++------------------------ 4 files changed, 32 insertions(+), 38 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5277647..102825d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ thiserror = "1" xdg = "2.4.0" udev = "0.6.3" zbus = "2.2.0" +fork = "0.1.19" [dev-dependencies] speculoos = "0.9.0" diff --git a/src/exec/dbus.rs b/src/exec/dbus.rs index ecd270e..0230a4d 100644 --- a/src/exec/dbus.rs +++ b/src/exec/dbus.rs @@ -23,11 +23,7 @@ trait Application { } impl DesktopEntry<'_> { - pub(crate) fn dbus_launch( - &self, - conn: &Connection, - uris: &[&str], - ) -> Result<(), ExecError> { + pub(crate) fn dbus_launch(&self, conn: &Connection, uris: &[&str]) -> Result<(), ExecError> { let dbus_path = self.appid.replace('.', "/"); let dbus_path = format!("/{dbus_path}"); let app_proxy = ApplicationProxyBlocking::builder(conn) diff --git a/src/exec/graphics.rs b/src/exec/graphics.rs index 377e853..c2f769d 100644 --- a/src/exec/graphics.rs +++ b/src/exec/graphics.rs @@ -7,7 +7,10 @@ use std::io; use std::ops::Deref; use std::path::{Path, PathBuf}; -const VULKAN_ICD_PATH: &str = std::env!("VULKAN_ICD_PATH", "must define system Vulkan ICD path (ex: `/usr/share/vulkan/icd.d`)"); +const VULKAN_ICD_PATH: &str = std::env!( + "VULKAN_ICD_PATH", + "must define system Vulkan ICD path (ex: `/usr/share/vulkan/icd.d`)" +); #[derive(Debug, Default)] pub struct Gpus { diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 84d2b41..f9aef2c 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -4,7 +4,9 @@ use crate::exec::error::ExecError; use crate::exec::graphics::Gpus; use crate::DesktopEntry; +use fork::{daemon, Fork}; use std::convert::TryFrom; +use std::os::unix::prelude::CommandExt; use std::path::PathBuf; use std::process::Command; use zbus::blocking::Connection; @@ -53,40 +55,32 @@ impl DesktopEntry<'_> { let exec_args = exec_args.join(" "); let shell = std::env::var("SHELL")?; - let status = if self.terminal() { - let (terminal, separator) = detect_terminal(); - let terminal = terminal.to_string_lossy(); - let args = format!("{terminal} {separator} {exec_args}"); - let args = ["-c", &args]; - let mut cmd = Command::new(shell); - if self.prefers_non_default_gpu() { - with_non_default_gpu(cmd) - } else { - cmd - } - .args(args) - .spawn()? - .try_wait()? - } else { - let mut cmd = Command::new(shell); + if let Ok(Fork::Child) = daemon(true, false) { + if self.terminal() { + let (terminal, separator) = detect_terminal(); + let terminal = terminal.to_string_lossy(); + let args = format!("{terminal} {separator} {exec_args}"); + let args = ["-c", &args]; + let mut cmd = Command::new(shell); - if self.prefers_non_default_gpu() { - with_non_default_gpu(cmd) + if self.prefers_non_default_gpu() { + with_non_default_gpu(cmd) + } else { + cmd + } + .args(args) + .exec() } else { - cmd - } - .args(&["-c", &exec_args]) - .spawn()? - .try_wait()? - }; + let mut cmd = Command::new(shell); - if let Some(status) = status { - if !status.success() { - return Err(ExecError::NonZeroStatusCode { - status: status.code(), - exec: exec.to_string(), - }); - } + if self.prefers_non_default_gpu() { + with_non_default_gpu(cmd) + } else { + cmd + } + .args(&["-c", &exec_args]) + .exec() + }; } Ok(()) @@ -288,7 +282,7 @@ mod test { 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); + let result = de.launch(&[path.as_str()]); assert_that!(result).is_ok(); } From fd6e2f9a182d666bd6143e818e009925c59e066a Mon Sep 17 00:00:00 2001 From: Paul Delafosse Date: Mon, 6 Jun 2022 11:09:59 +0200 Subject: [PATCH 7/7] feat: add desktop entry actions --- src/exec/dbus.rs | 26 ++++++-- src/exec/error.rs | 12 ++++ src/exec/mod.rs | 83 ++++++++++++++++++++++---- tests/entries/empty-exec.desktop | 2 +- tests/entries/non-terminal-cmd.desktop | 2 +- tests/entries/terminal-cmd.desktop | 2 +- tests/entries/unmatched-quotes.desktop | 2 +- 7 files changed, 110 insertions(+), 19 deletions(-) diff --git a/src/exec/dbus.rs b/src/exec/dbus.rs index 0230a4d..5c633ff 100644 --- a/src/exec/dbus.rs +++ b/src/exec/dbus.rs @@ -23,7 +23,12 @@ trait Application { } impl DesktopEntry<'_> { - pub(crate) fn dbus_launch(&self, conn: &Connection, uris: &[&str]) -> Result<(), ExecError> { + pub(crate) fn dbus_launch( + &self, + conn: &Connection, + uris: &[&str], + action: Option, + ) -> Result<(), ExecError> { let dbus_path = self.appid.replace('.', "/"); let dbus_path = format!("/{dbus_path}"); let app_proxy = ApplicationProxyBlocking::builder(conn) @@ -41,10 +46,21 @@ impl DesktopEntry<'_> { } } - if !uris.is_empty() { - app_proxy.open(uris, platform_data)?; - } else { - app_proxy.activate(platform_data)?; + match action { + None => { + if !uris.is_empty() { + app_proxy.open(uris, platform_data)?; + } else { + app_proxy.activate(platform_data)?; + } + } + Some(action) => { + let parameters: Vec = uris + .iter() + .map(|uri| OwnedValue::from(Str::from(*uri))) + .collect(); + app_proxy.activate_action(&action, parameters.as_slice(), platform_data)? + } } Ok(()) diff --git a/src/exec/error.rs b/src/exec/error.rs index dcac984..ab5c98d 100644 --- a/src/exec/error.rs +++ b/src/exec/error.rs @@ -29,6 +29,18 @@ pub enum ExecError<'a> { #[error("Exec key not found in desktop entry '{0:?}'")] MissingExecKey(&'a Path), + #[error("Action '{action}' not found for desktop entry '{desktop_entry:?}'")] + ActionNotFound { + action: String, + desktop_entry: &'a Path, + }, + + #[error("Exec key not found for action :'{action}' in desktop entry '{desktop_entry:?}'")] + ActionExecKeyNotFound { + action: String, + desktop_entry: &'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 f9aef2c..c5af65b 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -16,28 +16,69 @@ pub mod error; mod graphics; impl DesktopEntry<'_> { + /// Launch the given desktop entry action either via dbus or via its `Exec` key with the default gpu or + /// the alternative one if available. + pub fn launch_action(&self, action: &str, uris: &[&str]) -> Result<(), ExecError> { + let has_action = self + .actions() + .map(|actions| actions.split(';').any(|act| act == action)) + .unwrap_or(false); + + if !has_action { + return Err(ExecError::ActionNotFound { + action: action.to_string(), + desktop_entry: self.path, + }); + } + + match Connection::session() { + Ok(conn) => { + if self.is_bus_actionable(&conn) { + self.dbus_launch(&conn, uris, Some(action.to_string())) + } else { + self.shell_launch(uris, Some(action.to_string())) + } + } + Err(_) => self.shell_launch(uris, Some(action.to_string())), + } + } + /// Launch the given desktop entry either via dbus or via its `Exec` key with the default gpu or /// the alternative one if available. pub fn launch(&self, uris: &[&str]) -> Result<(), ExecError> { match Connection::session() { Ok(conn) => { if self.is_bus_actionable(&conn) { - self.dbus_launch(&conn, uris) + self.dbus_launch(&conn, uris, None) } else { - self.shell_launch(uris) + self.shell_launch(uris, None) } } - Err(_) => self.shell_launch(uris), + Err(_) => self.shell_launch(uris, None), } } - fn shell_launch(&self, uris: &[&str]) -> Result<(), ExecError> { - let exec = self.exec(); - if exec.is_none() { - return Err(ExecError::MissingExecKey(self.path)); - } + fn shell_launch(&self, uris: &[&str], action: Option) -> Result<(), ExecError> { + let exec = match action { + None => { + let exec = self.exec(); + if exec.is_none() { + return Err(ExecError::MissingExecKey(self.path)); + } + exec.unwrap() + } + Some(action) => { + let exec = self.action_exec(&action); + if exec.is_none() { + return Err(ExecError::ActionExecKeyNotFound { + action, + desktop_entry: self.path, + }); + } - let exec = exec.unwrap(); + exec.unwrap() + } + }; let mut exec_args = vec![]; @@ -281,12 +322,34 @@ mod test { 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 path = format!("file://{path}"); let result = de.launch(&[path.as_str()]); assert_that!(result).is_ok(); } + #[test] + #[ignore = "Needs a desktop environment with alacritty installed, run locally only"] + fn should_launch_action() { + let path = PathBuf::from("/usr/share/applications/Alacritty.desktop"); + let input = fs::read_to_string(&path).unwrap(); + let de = DesktopEntry::decode(path.as_path(), &input).unwrap(); + let result = de.launch_action("New", &[]); + + assert_that!(result).is_ok(); + } + + #[test] + #[ignore = "Needs a desktop environment with Nautilus installed, run locally only"] + fn should_launch_action_via_dbus() { + 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 result = de.launch_action("new-window", &[]); + + assert_that!(result).is_ok(); + } + #[test] fn should_build_command_with_gpu() { let cmd = with_non_default_gpu(Command::new("glxgears")); diff --git a/tests/entries/empty-exec.desktop b/tests/entries/empty-exec.desktop index e6bfa79..fb28c29 100644 --- a/tests/entries/empty-exec.desktop +++ b/tests/entries/empty-exec.desktop @@ -2,4 +2,4 @@ Exec= Terminal=false Type=Application -Name=Alacritty +Name=NoExecKey diff --git a/tests/entries/non-terminal-cmd.desktop b/tests/entries/non-terminal-cmd.desktop index b8dc18b..3b84b75 100644 --- a/tests/entries/non-terminal-cmd.desktop +++ b/tests/entries/non-terminal-cmd.desktop @@ -2,4 +2,4 @@ Exec=alacritty -e glxgears -info Terminal=false Type=Application -Name=Alacritty \ No newline at end of file +Name=GlxGearNoTerminal \ No newline at end of file diff --git a/tests/entries/terminal-cmd.desktop b/tests/entries/terminal-cmd.desktop index cf26df6..70cf76a 100644 --- a/tests/entries/terminal-cmd.desktop +++ b/tests/entries/terminal-cmd.desktop @@ -2,4 +2,4 @@ Exec=glxgears -info Terminal=true Type=Application -Name=Alacritty \ No newline at end of file +Name=GlxGearTerminal \ No newline at end of file diff --git a/tests/entries/unmatched-quotes.desktop b/tests/entries/unmatched-quotes.desktop index 98bda6c..6f8f6ef 100644 --- a/tests/entries/unmatched-quotes.desktop +++ b/tests/entries/unmatched-quotes.desktop @@ -2,4 +2,4 @@ Exec="alacritty -e Terminal=false Type=Application -Name=Alacritty \ No newline at end of file +Name=InvalidCommand \ No newline at end of file