From 06077d80c995c80044ba1e2c2c6ac05826ef6225 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 24 Jul 2024 14:32:01 +0200 Subject: [PATCH 1/3] init --- Cargo.toml | 5 + examples/launch.rs | 11 + src/decoder.rs | 11 +- src/exec/dbus.rs | 121 ++++++++ src/exec/error.rs | 49 ++++ src/exec/graphics.rs | 210 +++++++++++++ src/exec/mod.rs | 389 +++++++++++++++++++++++++ src/lib.rs | 10 +- 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 + 13 files changed, 841 insertions(+), 4 deletions(-) create mode 100644 examples/launch.rs create mode 100644 src/exec/dbus.rs 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 967355c..30a1f90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,8 @@ strsim = "0.11.1" thiserror = "1" xdg = "2.4.0" log = "0.4.21" +udev = "0.8.0" +zbus = "4.2.2" + +[dev-dependencies] +speculoos = "0.11.0" \ No newline at end of file diff --git a/examples/launch.rs b/examples/launch.rs new file mode 100644 index 0000000..98772ac --- /dev/null +++ b/examples/launch.rs @@ -0,0 +1,11 @@ +use freedesktop_desktop_entry::DesktopEntry; +use std::path::PathBuf; + +fn main() { + let path = PathBuf::from("tests/org.mozilla.firefox.desktop"); + + let de = DesktopEntry::from_path::<&str>(path, None).expect("Error decoding desktop entry"); + + de.launch_with_uris::<&str>(&[], false, &[]) + .expect("Failed to run desktop entry"); +} diff --git a/src/decoder.rs b/src/decoder.rs index 3c0c0ae..c255bfd 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -71,7 +71,10 @@ impl<'a> DesktopEntry<'a> { } /// Return an owned [`DesktopEntry`] - pub fn from_path(path: PathBuf, locales_filter: Option<&[L]>) -> Result, DecodeError> + pub fn from_path( + path: PathBuf, + locales_filter: Option<&[L]>, + ) -> Result, DecodeError> where L: AsRef, { @@ -125,7 +128,11 @@ fn process_line<'buf, 'local_ref, 'res: 'local_ref + 'buf, F, L>( let locale = &key[start + 1..key.len() - 1]; match locales_filter { - Some(locales_filter) if !locales_filter.iter().any(|l| l.as_ref() == locale) => return, + Some(locales_filter) + if !locales_filter.iter().any(|l| l.as_ref() == locale) => + { + return + } _ => (), } diff --git a/src/exec/dbus.rs b/src/exec/dbus.rs new file mode 100644 index 0000000..db5e2a2 --- /dev/null +++ b/src/exec/dbus.rs @@ -0,0 +1,121 @@ +// Copyright 2021 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::exec::error::ExecError; +use crate::exec::graphics::Gpus; +use crate::DesktopEntry; +use std::collections::HashMap; +use zbus::blocking::Connection; +use zbus::names::OwnedBusName; +use zbus::proxy; +use zbus::zvariant::{OwnedValue, Str}; + +// https://specifications.freedesktop.org/desktop-entry-spec/1.1/ar01s07.html +#[proxy(interface = "org.freedesktop.Application")] +trait Application { + fn activate(&self, platform_data: HashMap) -> zbus::Result<()>; + + fn open(&self, uris: &[&str], platform_data: HashMap) -> zbus::Result<()>; + + // XXX: https://gitlab.freedesktop.org/xdg/xdg-specs/-/issues/134 + fn activate_action( + &self, + action_name: &str, + parameters: &[&str], + platform_data: HashMap, + ) -> zbus::Result<()>; +} + +impl DesktopEntry<'_> { + pub(crate) fn should_launch_on_dbus(&self) -> Option { + match self.desktop_entry_bool("DBusActivatable") { + true => match Connection::session() { + Ok(conn) => { + if self.is_bus_actionable(&conn) { + Some(conn) + } else { + None + } + } + Err(e) => { + log::error!("can't open dbus session: {}", e); + None + } + }, + false => None, + } + } + + 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(); + + 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) + } + + pub(crate) fn dbus_launch(&self, conn: &Connection, uris: &[&str]) -> Result<(), ExecError> { + let app_proxy = self.get_app_proxy(conn)?; + let platform_data = self.get_platform_data(); + + if !uris.is_empty() { + app_proxy.open(uris, platform_data)?; + } else { + app_proxy.activate(platform_data)?; + } + + Ok(()) + } + + pub(crate) fn dbus_launch_action( + &self, + conn: &Connection, + action_name: &str, + uris: &[&str], + ) -> Result<(), ExecError> { + let app_proxy = self.get_app_proxy(conn)?; + let platform_data = self.get_platform_data(); + app_proxy.activate_action(action_name, uris, platform_data)?; + + Ok(()) + } + + fn get_app_proxy(&self, conn: &Connection) -> Result { + let dbus_path = self.appid.replace('.', "/").replace('-', "_"); + let dbus_path = format!("/{dbus_path}"); + let app_proxy = ApplicationProxyBlocking::builder(conn) + .destination(self.appid.as_ref())? + .path(dbus_path)? + .build()?; + Ok(app_proxy) + } + + // todo: XDG_ACTIVATION_TOKEN and DESKTOP_STARTUP_ID ? + // https://github.com/pop-os/libcosmic/blob/master/src/app/mod.rs + fn get_platform_data(&self) -> HashMap { + let mut platform_data = HashMap::new(); + if self.prefers_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()))); + } + } + } + platform_data + } +} diff --git a/src/exec/error.rs b/src/exec/error.rs new file mode 100644 index 0000000..bf32b61 --- /dev/null +++ b/src/exec/error.rs @@ -0,0 +1,49 @@ +// Copyright 2021 System76 +// SPDX-License-Identifier: MPL-2.0 + +use std::env::VarError; +use std::io; +use std::path::Path; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ExecError<'a> { + #[error("{0}")] + WrongFormat(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), + + #[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/graphics.rs b/src/exec/graphics.rs new file mode 100644 index 0000000..412ee89 --- /dev/null +++ b/src/exec/graphics.rs @@ -0,0 +1,210 @@ +// Copyright 2021 System76 +// SPDX-License-Identifier: MPL-2.0 + +use std::collections::HashSet; +use std::hash::{Hash, Hasher}; +use std::io; +use std::ops::Deref; +use std::path::PathBuf; + +// 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 { + 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"); + + // Path::new(VULKAN_ICD_PATH) + let vulkan_icd_paths = &[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()? + .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..16feb50 --- /dev/null +++ b/src/exec/mod.rs @@ -0,0 +1,389 @@ +// Copyright 2021 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::exec::error::ExecError; +use crate::exec::graphics::Gpus; +use crate::DesktopEntry; +use std::borrow::Cow; +use std::convert::TryFrom; +use std::path::PathBuf; +use std::process::Command; + +mod dbus; +pub mod error; +mod graphics; + +impl<'a> DesktopEntry<'a> { + pub fn launch(&self, prefer_non_default_gpu: bool) -> Result<(), ExecError> { + match self.should_launch_on_dbus() { + Some(conn) => self.dbus_launch(&conn, &[]), + None => self.shell_launch(self.exec(), &[], prefer_non_default_gpu, &[] as &[&str]), + } + } + + /// Execute the given desktop entry `Exec` key with either the default gpu or the alternative one if available. + /// Macros like `%f` (cf [.desktop spec](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables)) will + /// be subtitued using the `uris` parameter. + pub fn launch_with_uris( + &self, + uris: &[&'a str], + prefer_non_default_gpu: bool, + locales: &[L], + ) -> Result<(), ExecError> + where + L: AsRef, + { + match self.should_launch_on_dbus() { + Some(conn) => self.dbus_launch(&conn, uris), + None => self.shell_launch(self.exec(), uris, prefer_non_default_gpu, locales), + } + } + + pub fn launch_action( + &self, + action_name: &str, + prefer_non_default_gpu: bool, + ) -> Result<(), ExecError> { + match self.should_launch_on_dbus() { + Some(conn) => self.dbus_launch_action(&conn, action_name, &[]), + None => self.shell_launch( + self.action_exec(action_name), + &[], + prefer_non_default_gpu, + &[] as &[&str], + ), + } + } + + pub fn launch_action_with_uris( + &self, + action_name: &str, + uris: &[&'a str], + prefer_non_default_gpu: bool, + locales: &[L], + ) -> Result<(), ExecError> + where + L: AsRef, + { + match self.should_launch_on_dbus() { + Some(conn) => self.dbus_launch_action(&conn, action_name, uris), + None => self.shell_launch( + self.action_exec(action_name), + uris, + prefer_non_default_gpu, + locales, + ), + } + } + + // https://github.com/pop-os/libcosmic/blob/master/src/desktop.rs + fn shell_launch( + &'a self, + exec: Option<&'a str>, + uris: &[&str], + prefer_non_default_gpu: bool, + locales: &[L], + ) -> Result<(), ExecError> + where + L: AsRef, + { + 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::WrongFormat("unmatched quote".into()))? + } 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(uris, exec_args, locales); + + 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) + .spawn()? + .try_wait()? + } else { + let mut cmd = Command::new(shell); + + if prefer_non_default_gpu { + with_non_default_gpu(cmd) + } else { + cmd + } + .args(["-c", &exec_args]) + .spawn()? + .try_wait()? + }; + + 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( + &'a self, + uris: &[&'a str], + exec_args: Vec>, + locales: &[L], + ) -> Vec> + where + L: AsRef, + { + let mut final_args: Vec> = Vec::new(); + + for arg in exec_args { + match arg { + ArgOrFieldCode::SingleFileName | ArgOrFieldCode::SingleUrl => { + if let Some(arg) = uris.first() { + final_args.push(Cow::Borrowed(arg)); + } + } + ArgOrFieldCode::FileList | ArgOrFieldCode::UrlList => { + uris.iter() + .for_each(|uri| final_args.push(Cow::Borrowed(uri))); + } + ArgOrFieldCode::IconKey => { + if let Some(icon) = self.icon() { + final_args.push(Cow::Borrowed(icon)); + } + } + ArgOrFieldCode::TranslatedName => { + if let Some(name) = self.name(locales) { + final_args.push(name.clone()); + } + } + ArgOrFieldCode::DesktopFileLocation => { + final_args.push(self.path.to_string_lossy()); + } + ArgOrFieldCode::Arg(arg) => { + final_args.push(Cow::Borrowed(arg)); + } + } + } + + final_args + } +} + +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::{get_languages_from_env, DesktopEntry}; + use speculoos::prelude::*; + + use std::path::PathBuf; + use std::process::Command; + + #[test] + fn should_return_unmatched_quote_error() { + let path = PathBuf::from("tests/entries/unmatched-quotes.desktop"); + let locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); + let result = de.launch_with_uris(&[], false, &locales); + + assert_that!(result) + .is_err() + .matches(|err| matches!(err, ExecError::WrongFormat(..))); + } + + #[test] + fn should_fail_if_exec_string_is_empty() { + let path = PathBuf::from("tests/entries/empty-exec.desktop"); + let locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); + let result = de.launch_with_uris(&[], false, &locales); + + 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 locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); + let result = de.launch_with_uris(&[], false, &locales); + + 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 locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); + let result = de.launch_with_uris(&[], false, &locales); + + 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 locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); + let result = de.launch_with_uris(&[], false, &locales); + + 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 locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); + let result = de.launch_with_uris(&["src/lib.rs"], false, &locales); + + 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 locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); + let result = de.launch_with_uris(&["src/lib.rs"], false, &locales); + + 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 locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); + let _result = de.launch_with_uris(&[], false, &locales); + let path = std::env::current_dir().unwrap(); + let path = path.to_string_lossy(); + let path = format!("file:///{path}"); + let result = de.launch_with_uris(&[path.as_str()], false, &locales); + + 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 72e94d8..0fda1c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,9 @@ mod decoder; mod iter; +mod exec; +pub use exec::error::ExecError; + pub mod matching; pub use decoder::DecodeError; @@ -476,8 +479,11 @@ fn add_field() { fn env_with_locale() { let locales = &["fr_FR"]; - let de = DesktopEntry::from_path(PathBuf::from("tests/org.mozilla.firefox.desktop"), Some(locales)) - .unwrap(); + let de = DesktopEntry::from_path( + PathBuf::from("tests/org.mozilla.firefox.desktop"), + Some(locales), + ) + .unwrap(); assert_eq!(de.generic_name(locales).unwrap(), "Navigateur Web"); 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..fb28c29 --- /dev/null +++ b/tests/entries/empty-exec.desktop @@ -0,0 +1,5 @@ +[Desktop Entry] +Exec= +Terminal=false +Type=Application +Name=NoExecKey diff --git a/tests/entries/non-terminal-cmd.desktop b/tests/entries/non-terminal-cmd.desktop new file mode 100644 index 0000000..3b84b75 --- /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=GlxGearNoTerminal \ 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..70cf76a --- /dev/null +++ b/tests/entries/terminal-cmd.desktop @@ -0,0 +1,5 @@ +[Desktop Entry] +Exec=glxgears -info +Terminal=true +Type=Application +Name=GlxGearTerminal \ 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..6f8f6ef --- /dev/null +++ b/tests/entries/unmatched-quotes.desktop @@ -0,0 +1,5 @@ +[Desktop Entry] +Exec="alacritty -e +Terminal=false +Type=Application +Name=InvalidCommand \ No newline at end of file From 1bcdc76325859e25a7453111bfe6cf1464512e4f Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:17:26 +0200 Subject: [PATCH 2/3] ff --- NOTE.md | 132 +++++++++++++++++++++++++++++++++++++++++++++++ REQUEST_TOKEN.md | 44 ++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 NOTE.md create mode 100644 REQUEST_TOKEN.md diff --git a/NOTE.md b/NOTE.md new file mode 100644 index 0000000..136a108 --- /dev/null +++ b/NOTE.md @@ -0,0 +1,132 @@ + + +need: + +- option to select the GpuPreference: +pub enum GpuPreference { + Default, + NonDefault, + SpecificIdx(u32), +} + + +- request a token: + params: + - app_id + - window id + + + + + + + + + + + + + + + + + + + + + + + + +- launch: + + if token exist: + envs.push(("XDG_ACTIVATION_TOKEN".to_string(), token.clone())); + envs.push(("DESKTOP_STARTUP_ID".to_string(), token)); + + + env for gpu: + async fn try_get_gpu_envs(gpu: GpuPreference) -> Option> { + let connection = zbus::Connection::system().await.ok()?; + let proxy = switcheroo_control::SwitcherooControlProxy::new(&connection) + .await + .ok()?; + let gpus = proxy.get_gpus().await.ok()?; + match gpu { + GpuPreference::Default => gpus.into_iter().find(|gpu| gpu.default), + GpuPreference::NonDefault => gpus.into_iter().find(|gpu| !gpu.default), + GpuPreference::SpecificIdx(idx) => gpus.into_iter().nth(idx as usize), + } + .map(|gpu| gpu.environment) + } + + + + + + + + + + + + + + + + + +spawn + +pub fn spawn_desktop_exec(exec: S, env_vars: I) +where + S: AsRef, + I: IntoIterator, + K: AsRef, + V: AsRef, +{ + let mut exec = shlex::Shlex::new(exec.as_ref()); + let mut cmd = match exec.next() { + Some(cmd) if !cmd.contains('=') => std::process::Command::new(cmd), + _ => return, + }; + + for arg in exec { + // TODO handle "%" args here if necessary? + if !arg.starts_with('%') { + cmd.arg(arg); + } + } + + cmd.envs(env_vars); + + crate::process::spawn(cmd); +} + + +/// 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/REQUEST_TOKEN.md b/REQUEST_TOKEN.md new file mode 100644 index 0000000..3eb8e04 --- /dev/null +++ b/REQUEST_TOKEN.md @@ -0,0 +1,44 @@ + +```rs +platform_specific::wayland::activation::Action::RequestToken { app_id, window, message } => { + if let Some(activation_state) = self.state.activation_state.as_ref() { + let (seat_and_serial, surface) = if let Some(id) = window { + let surface = self.state.windows.iter().find(|w| w.id == id) + .map(|w| w.window.wl_surface().clone()) + .or_else(|| self.state.layer_surfaces.iter().find(|l| l.id == id) + .map(|l| l.surface.wl_surface().clone()) + ); + let seat_and_serial = surface.as_ref().and_then(|surface| { + self.state.seats.first().and_then(|seat| if seat.kbd_focus.as_ref().map(|focus| focus == surface).unwrap_or(false) { + seat.last_kbd_press.as_ref().map(|(_, serial)| (seat.seat.clone(), *serial)) + } else if seat.ptr_focus.as_ref().map(|focus| focus == surface).unwrap_or(false) { + seat.last_ptr_press.as_ref().map(|(_, _, serial)| (seat.seat.clone(), *serial)) + } else { + None + }) + }); + + (seat_and_serial, surface) + } else { + (None, None) + }; + + activation_state.request_token_with_data(&self.state.queue_handle, IcedRequestData::new( + RequestData { + app_id, + seat_and_serial, + surface, + }, + message, + )); + } else { + // if we don't have the global, we don't want to stall the app + sticky_exit_callback( + IcedSctkEvent::UserEvent(message(None)), + &self.state, + &mut control_flow, + &mut callback, + ) + } +}, +``` \ No newline at end of file From 9632b108c500df130903d6c28bb3e4161df505a7 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 24 Jul 2024 16:28:01 +0200 Subject: [PATCH 3/3] parse_exec --- .vscode/settings.json | 4 - Cargo.toml | 4 - NOTE.md | 132 ------ REQUEST_TOKEN.md | 44 -- examples/launch.rs | 11 - examples/specific_file.rs | 2 +- src/exec.rs | 260 ++++++++++++ src/exec/dbus.rs | 121 ------ src/exec/error.rs | 49 --- src/exec/graphics.rs | 210 ---------- src/exec/mod.rs | 389 ------------------ src/lib.rs | 4 +- {tests => tests_entries}/code.desktop | 0 ...bnkkaeohfgghhklpknlkffjgod-Default.desktop | 0 .../exec}/alacritty-simple.desktop | 0 .../exec}/empty-exec.desktop | 0 .../exec}/non-terminal-cmd.desktop | 0 .../exec}/terminal-cmd.desktop | 0 .../exec}/unmatched-quotes.desktop | 0 .../org.kde.krita.desktop | 0 .../org.mozilla.firefox.desktop | 0 21 files changed, 263 insertions(+), 967 deletions(-) delete mode 100644 .vscode/settings.json delete mode 100644 NOTE.md delete mode 100644 REQUEST_TOKEN.md delete mode 100644 examples/launch.rs create mode 100644 src/exec.rs delete mode 100644 src/exec/dbus.rs delete mode 100644 src/exec/error.rs delete mode 100644 src/exec/graphics.rs delete mode 100644 src/exec/mod.rs rename {tests => tests_entries}/code.desktop (100%) rename {tests => tests_entries}/com.brave.Browser.flextop.brave-cinhimbnkkaeohfgghhklpknlkffjgod-Default.desktop (100%) rename {tests/entries => tests_entries/exec}/alacritty-simple.desktop (100%) rename {tests/entries => tests_entries/exec}/empty-exec.desktop (100%) rename {tests/entries => tests_entries/exec}/non-terminal-cmd.desktop (100%) rename {tests/entries => tests_entries/exec}/terminal-cmd.desktop (100%) rename {tests/entries => tests_entries/exec}/unmatched-quotes.desktop (100%) rename {tests => tests_entries}/org.kde.krita.desktop (100%) rename {tests => tests_entries}/org.mozilla.firefox.desktop (100%) diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 37ed364..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rust-analyzer.cargo.features": [ - ] -} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 30a1f90..689b518 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,8 +19,4 @@ strsim = "0.11.1" thiserror = "1" xdg = "2.4.0" log = "0.4.21" -udev = "0.8.0" -zbus = "4.2.2" -[dev-dependencies] -speculoos = "0.11.0" \ No newline at end of file diff --git a/NOTE.md b/NOTE.md deleted file mode 100644 index 136a108..0000000 --- a/NOTE.md +++ /dev/null @@ -1,132 +0,0 @@ - - -need: - -- option to select the GpuPreference: -pub enum GpuPreference { - Default, - NonDefault, - SpecificIdx(u32), -} - - -- request a token: - params: - - app_id - - window id - - - - - - - - - - - - - - - - - - - - - - - - -- launch: - - if token exist: - envs.push(("XDG_ACTIVATION_TOKEN".to_string(), token.clone())); - envs.push(("DESKTOP_STARTUP_ID".to_string(), token)); - - - env for gpu: - async fn try_get_gpu_envs(gpu: GpuPreference) -> Option> { - let connection = zbus::Connection::system().await.ok()?; - let proxy = switcheroo_control::SwitcherooControlProxy::new(&connection) - .await - .ok()?; - let gpus = proxy.get_gpus().await.ok()?; - match gpu { - GpuPreference::Default => gpus.into_iter().find(|gpu| gpu.default), - GpuPreference::NonDefault => gpus.into_iter().find(|gpu| !gpu.default), - GpuPreference::SpecificIdx(idx) => gpus.into_iter().nth(idx as usize), - } - .map(|gpu| gpu.environment) - } - - - - - - - - - - - - - - - - - -spawn - -pub fn spawn_desktop_exec(exec: S, env_vars: I) -where - S: AsRef, - I: IntoIterator, - K: AsRef, - V: AsRef, -{ - let mut exec = shlex::Shlex::new(exec.as_ref()); - let mut cmd = match exec.next() { - Some(cmd) if !cmd.contains('=') => std::process::Command::new(cmd), - _ => return, - }; - - for arg in exec { - // TODO handle "%" args here if necessary? - if !arg.starts_with('%') { - cmd.arg(arg); - } - } - - cmd.envs(env_vars); - - crate::process::spawn(cmd); -} - - -/// 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/REQUEST_TOKEN.md b/REQUEST_TOKEN.md deleted file mode 100644 index 3eb8e04..0000000 --- a/REQUEST_TOKEN.md +++ /dev/null @@ -1,44 +0,0 @@ - -```rs -platform_specific::wayland::activation::Action::RequestToken { app_id, window, message } => { - if let Some(activation_state) = self.state.activation_state.as_ref() { - let (seat_and_serial, surface) = if let Some(id) = window { - let surface = self.state.windows.iter().find(|w| w.id == id) - .map(|w| w.window.wl_surface().clone()) - .or_else(|| self.state.layer_surfaces.iter().find(|l| l.id == id) - .map(|l| l.surface.wl_surface().clone()) - ); - let seat_and_serial = surface.as_ref().and_then(|surface| { - self.state.seats.first().and_then(|seat| if seat.kbd_focus.as_ref().map(|focus| focus == surface).unwrap_or(false) { - seat.last_kbd_press.as_ref().map(|(_, serial)| (seat.seat.clone(), *serial)) - } else if seat.ptr_focus.as_ref().map(|focus| focus == surface).unwrap_or(false) { - seat.last_ptr_press.as_ref().map(|(_, _, serial)| (seat.seat.clone(), *serial)) - } else { - None - }) - }); - - (seat_and_serial, surface) - } else { - (None, None) - }; - - activation_state.request_token_with_data(&self.state.queue_handle, IcedRequestData::new( - RequestData { - app_id, - seat_and_serial, - surface, - }, - message, - )); - } else { - // if we don't have the global, we don't want to stall the app - sticky_exit_callback( - IcedSctkEvent::UserEvent(message(None)), - &self.state, - &mut control_flow, - &mut callback, - ) - } -}, -``` \ No newline at end of file diff --git a/examples/launch.rs b/examples/launch.rs deleted file mode 100644 index 98772ac..0000000 --- a/examples/launch.rs +++ /dev/null @@ -1,11 +0,0 @@ -use freedesktop_desktop_entry::DesktopEntry; -use std::path::PathBuf; - -fn main() { - let path = PathBuf::from("tests/org.mozilla.firefox.desktop"); - - let de = DesktopEntry::from_path::<&str>(path, None).expect("Error decoding desktop entry"); - - de.launch_with_uris::<&str>(&[], false, &[]) - .expect("Failed to run desktop entry"); -} diff --git a/examples/specific_file.rs b/examples/specific_file.rs index 526080b..d352528 100644 --- a/examples/specific_file.rs +++ b/examples/specific_file.rs @@ -3,7 +3,7 @@ use std::path::Path; use freedesktop_desktop_entry::DesktopEntry; fn main() { - let path = Path::new("tests/org.mozilla.firefox.desktop"); + let path = Path::new("tests_entries/org.mozilla.firefox.desktop"); let locales = &["fr_FR", "en", "it"]; // if let Ok(bytes) = fs::read_to_string(path) { diff --git a/src/exec.rs b/src/exec.rs new file mode 100644 index 0000000..2d0f73b --- /dev/null +++ b/src/exec.rs @@ -0,0 +1,260 @@ +// Copyright 2021 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::DesktopEntry; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ExecError { + #[error("{0}")] + WrongFormat(String), + + #[error("Exec field is empty")] + ExecFieldIsEmpty, + + #[error("Exec key was not found")] + ExecFieldNotFound, +} + +impl<'a> DesktopEntry<'a> { + pub fn parse_exec(&self) -> Result, ExecError> { + self.get_args(self.exec(), &[], &[] as &[&str]) + } + + /// Macros like `%f` (cf [.desktop spec](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables)) will be subtitued using the `uris` parameter. + pub fn parse_exec_with_uris( + &self, + uris: &[&'a str], + locales: &[L], + ) -> Result, ExecError> + where + L: AsRef, + { + self.get_args(self.exec(), uris, locales) + } + + pub fn parse_exec_action(&self, action_name: &str) -> Result, ExecError> { + self.get_args(self.action_exec(action_name), &[], &[] as &[&str]) + } + + pub fn parse_exec_action_with_uris( + &self, + action_name: &str, + uris: &[&'a str], + locales: &[L], + ) -> Result, ExecError> + where + L: AsRef, + { + self.get_args(self.action_exec(action_name), uris, locales) + } + + fn get_args( + &'a self, + exec: Option<&'a str>, + uris: &[&'a str], + locales: &[L], + ) -> Result, ExecError> + where + L: AsRef, + { + let Some(exec) = exec else { + return Err(ExecError::ExecFieldNotFound); + }; + + if exec.contains('=') { + return Err(ExecError::WrongFormat("equal sign detected".into())); + } + + let exec = if let Some(without_prefix) = exec.strip_prefix('\"') { + without_prefix + .strip_suffix('\"') + .ok_or(ExecError::WrongFormat("unmatched quote".into()))? + } else { + exec + }; + + let mut args: Vec = Vec::new(); + + for arg in exec.split_ascii_whitespace() { + match ArgOrFieldCode::try_from(arg) { + Ok(arg) => match arg { + ArgOrFieldCode::SingleFileName | ArgOrFieldCode::SingleUrl => { + if let Some(arg) = uris.first() { + args.push(arg.to_string()); + } + } + ArgOrFieldCode::FileList | ArgOrFieldCode::UrlList => { + uris.iter().for_each(|uri| args.push(uri.to_string())); + } + ArgOrFieldCode::IconKey => { + if let Some(icon) = self.icon() { + args.push(icon.to_string()); + } + } + ArgOrFieldCode::TranslatedName => { + if let Some(name) = self.name(locales) { + args.push(name.to_string()); + } + } + ArgOrFieldCode::DesktopFileLocation => { + args.push(self.path.to_string_lossy().to_string()); + } + ArgOrFieldCode::Arg(arg) => { + args.push(arg.to_string()); + } + }, + Err(e) => { + log::error!("{}", e); + } + } + } + + if args.is_empty() { + return Err(ExecError::ExecFieldIsEmpty); + } + + Ok(args) + } +} + +// 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), +} + +#[derive(Debug, Error)] +enum ExecErrorInternal<'a> { + #[error("Unknown field code: '{0}'")] + UnknownFieldCode(&'a str), + + #[error("Deprecated field code: '{0}'")] + DeprecatedFieldCode(&'a str), +} + +impl<'a> TryFrom<&'a str> for ArgOrFieldCode<'a> { + type Error = ExecErrorInternal<'a>; + + // todo: handle escaping + 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(ExecErrorInternal::DeprecatedFieldCode(value)) + } + other if other.starts_with('%') => Err(ExecErrorInternal::UnknownFieldCode(other)), + other => Ok(ArgOrFieldCode::Arg(other)), + } + } +} + +#[cfg(test)] +mod test { + + use std::path::PathBuf; + + use crate::{get_languages_from_env, DesktopEntry}; + + use super::ExecError; + + #[test] + fn should_return_unmatched_quote_error() { + let path = PathBuf::from("tests_entries/exec/unmatched-quotes.desktop"); + let locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); + let result = de.parse_exec_with_uris(&[], &locales); + + assert!(matches!(result.unwrap_err(), ExecError::WrongFormat(..))); + } + + #[test] + fn should_fail_if_exec_string_is_empty() { + let path = PathBuf::from("tests_entries/exec/empty-exec.desktop"); + let locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); + let result = de.parse_exec_with_uris(&[], &locales); + + assert!(matches!(result.unwrap_err(), ExecError::ExecFieldIsEmpty)); + } + + #[test] + fn should_exec_simple_command() { + let path = PathBuf::from("tests_entries/exec/alacritty-simple.desktop"); + let locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); + let result = de.parse_exec_with_uris(&[], &locales); + + assert!(result.is_ok()); + } + + #[test] + fn should_exec_complex_command() { + let path = PathBuf::from("tests_entries/exec/non-terminal-cmd.desktop"); + let locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); + let result = de.parse_exec_with_uris(&[], &locales); + + assert!(result.is_ok()); + } + + #[test] + fn should_exec_terminal_command() { + let path = PathBuf::from("tests_entries/exec/non-terminal-cmd.desktop"); + let locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); + let result = de.parse_exec_with_uris(&[], &locales); + + assert!(result.is_ok()); + } + + #[test] + #[ignore = "Needs a desktop environment with nvim installed, run locally only"] + fn should_parse_exec_with_field_codes() { + let path = PathBuf::from("/usr/share/applications/nvim.desktop"); + let locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); + let result = de.parse_exec_with_uris(&["src/lib.rs"], &locales); + + assert!(result.is_ok()); + } + + #[test] + #[ignore = "Needs a desktop environment with gnome Books installed, run locally only"] + fn should_parse_exec_with_dbus() { + let path = PathBuf::from("/usr/share/applications/org.gnome.Books.desktop"); + let locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); + let result = de.parse_exec_with_uris(&["src/lib.rs"], &locales); + + assert!(result.is_ok()); + } + + #[test] + #[ignore = "Needs a desktop environment with Nautilus installed, run locally only"] + fn should_parse_exec_with_dbus_and_field_codes() { + let path = PathBuf::from("/usr/share/applications/org.gnome.Nautilus.desktop"); + let locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); + let _result = de.parse_exec_with_uris(&[], &locales); + let path = std::env::current_dir().unwrap(); + let path = path.to_string_lossy(); + let path = format!("file:///{path}"); + let result = de.parse_exec_with_uris(&[path.as_str()], &locales); + + assert!(result.is_ok()); + } +} diff --git a/src/exec/dbus.rs b/src/exec/dbus.rs deleted file mode 100644 index db5e2a2..0000000 --- a/src/exec/dbus.rs +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright 2021 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::exec::error::ExecError; -use crate::exec::graphics::Gpus; -use crate::DesktopEntry; -use std::collections::HashMap; -use zbus::blocking::Connection; -use zbus::names::OwnedBusName; -use zbus::proxy; -use zbus::zvariant::{OwnedValue, Str}; - -// https://specifications.freedesktop.org/desktop-entry-spec/1.1/ar01s07.html -#[proxy(interface = "org.freedesktop.Application")] -trait Application { - fn activate(&self, platform_data: HashMap) -> zbus::Result<()>; - - fn open(&self, uris: &[&str], platform_data: HashMap) -> zbus::Result<()>; - - // XXX: https://gitlab.freedesktop.org/xdg/xdg-specs/-/issues/134 - fn activate_action( - &self, - action_name: &str, - parameters: &[&str], - platform_data: HashMap, - ) -> zbus::Result<()>; -} - -impl DesktopEntry<'_> { - pub(crate) fn should_launch_on_dbus(&self) -> Option { - match self.desktop_entry_bool("DBusActivatable") { - true => match Connection::session() { - Ok(conn) => { - if self.is_bus_actionable(&conn) { - Some(conn) - } else { - None - } - } - Err(e) => { - log::error!("can't open dbus session: {}", e); - None - } - }, - false => None, - } - } - - 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(); - - 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) - } - - pub(crate) fn dbus_launch(&self, conn: &Connection, uris: &[&str]) -> Result<(), ExecError> { - let app_proxy = self.get_app_proxy(conn)?; - let platform_data = self.get_platform_data(); - - if !uris.is_empty() { - app_proxy.open(uris, platform_data)?; - } else { - app_proxy.activate(platform_data)?; - } - - Ok(()) - } - - pub(crate) fn dbus_launch_action( - &self, - conn: &Connection, - action_name: &str, - uris: &[&str], - ) -> Result<(), ExecError> { - let app_proxy = self.get_app_proxy(conn)?; - let platform_data = self.get_platform_data(); - app_proxy.activate_action(action_name, uris, platform_data)?; - - Ok(()) - } - - fn get_app_proxy(&self, conn: &Connection) -> Result { - let dbus_path = self.appid.replace('.', "/").replace('-', "_"); - let dbus_path = format!("/{dbus_path}"); - let app_proxy = ApplicationProxyBlocking::builder(conn) - .destination(self.appid.as_ref())? - .path(dbus_path)? - .build()?; - Ok(app_proxy) - } - - // todo: XDG_ACTIVATION_TOKEN and DESKTOP_STARTUP_ID ? - // https://github.com/pop-os/libcosmic/blob/master/src/app/mod.rs - fn get_platform_data(&self) -> HashMap { - let mut platform_data = HashMap::new(); - if self.prefers_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()))); - } - } - } - platform_data - } -} diff --git a/src/exec/error.rs b/src/exec/error.rs deleted file mode 100644 index bf32b61..0000000 --- a/src/exec/error.rs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2021 System76 -// SPDX-License-Identifier: MPL-2.0 - -use std::env::VarError; -use std::io; -use std::path::Path; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum ExecError<'a> { - #[error("{0}")] - WrongFormat(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), - - #[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/graphics.rs b/src/exec/graphics.rs deleted file mode 100644 index 412ee89..0000000 --- a/src/exec/graphics.rs +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright 2021 System76 -// SPDX-License-Identifier: MPL-2.0 - -use std::collections::HashSet; -use std::hash::{Hash, Hasher}; -use std::io; -use std::ops::Deref; -use std::path::PathBuf; - -// 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 { - 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"); - - // Path::new(VULKAN_ICD_PATH) - let vulkan_icd_paths = &[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()? - .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 deleted file mode 100644 index 16feb50..0000000 --- a/src/exec/mod.rs +++ /dev/null @@ -1,389 +0,0 @@ -// Copyright 2021 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::exec::error::ExecError; -use crate::exec::graphics::Gpus; -use crate::DesktopEntry; -use std::borrow::Cow; -use std::convert::TryFrom; -use std::path::PathBuf; -use std::process::Command; - -mod dbus; -pub mod error; -mod graphics; - -impl<'a> DesktopEntry<'a> { - pub fn launch(&self, prefer_non_default_gpu: bool) -> Result<(), ExecError> { - match self.should_launch_on_dbus() { - Some(conn) => self.dbus_launch(&conn, &[]), - None => self.shell_launch(self.exec(), &[], prefer_non_default_gpu, &[] as &[&str]), - } - } - - /// Execute the given desktop entry `Exec` key with either the default gpu or the alternative one if available. - /// Macros like `%f` (cf [.desktop spec](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables)) will - /// be subtitued using the `uris` parameter. - pub fn launch_with_uris( - &self, - uris: &[&'a str], - prefer_non_default_gpu: bool, - locales: &[L], - ) -> Result<(), ExecError> - where - L: AsRef, - { - match self.should_launch_on_dbus() { - Some(conn) => self.dbus_launch(&conn, uris), - None => self.shell_launch(self.exec(), uris, prefer_non_default_gpu, locales), - } - } - - pub fn launch_action( - &self, - action_name: &str, - prefer_non_default_gpu: bool, - ) -> Result<(), ExecError> { - match self.should_launch_on_dbus() { - Some(conn) => self.dbus_launch_action(&conn, action_name, &[]), - None => self.shell_launch( - self.action_exec(action_name), - &[], - prefer_non_default_gpu, - &[] as &[&str], - ), - } - } - - pub fn launch_action_with_uris( - &self, - action_name: &str, - uris: &[&'a str], - prefer_non_default_gpu: bool, - locales: &[L], - ) -> Result<(), ExecError> - where - L: AsRef, - { - match self.should_launch_on_dbus() { - Some(conn) => self.dbus_launch_action(&conn, action_name, uris), - None => self.shell_launch( - self.action_exec(action_name), - uris, - prefer_non_default_gpu, - locales, - ), - } - } - - // https://github.com/pop-os/libcosmic/blob/master/src/desktop.rs - fn shell_launch( - &'a self, - exec: Option<&'a str>, - uris: &[&str], - prefer_non_default_gpu: bool, - locales: &[L], - ) -> Result<(), ExecError> - where - L: AsRef, - { - 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::WrongFormat("unmatched quote".into()))? - } 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(uris, exec_args, locales); - - 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) - .spawn()? - .try_wait()? - } else { - let mut cmd = Command::new(shell); - - if prefer_non_default_gpu { - with_non_default_gpu(cmd) - } else { - cmd - } - .args(["-c", &exec_args]) - .spawn()? - .try_wait()? - }; - - 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( - &'a self, - uris: &[&'a str], - exec_args: Vec>, - locales: &[L], - ) -> Vec> - where - L: AsRef, - { - let mut final_args: Vec> = Vec::new(); - - for arg in exec_args { - match arg { - ArgOrFieldCode::SingleFileName | ArgOrFieldCode::SingleUrl => { - if let Some(arg) = uris.first() { - final_args.push(Cow::Borrowed(arg)); - } - } - ArgOrFieldCode::FileList | ArgOrFieldCode::UrlList => { - uris.iter() - .for_each(|uri| final_args.push(Cow::Borrowed(uri))); - } - ArgOrFieldCode::IconKey => { - if let Some(icon) = self.icon() { - final_args.push(Cow::Borrowed(icon)); - } - } - ArgOrFieldCode::TranslatedName => { - if let Some(name) = self.name(locales) { - final_args.push(name.clone()); - } - } - ArgOrFieldCode::DesktopFileLocation => { - final_args.push(self.path.to_string_lossy()); - } - ArgOrFieldCode::Arg(arg) => { - final_args.push(Cow::Borrowed(arg)); - } - } - } - - final_args - } -} - -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::{get_languages_from_env, DesktopEntry}; - use speculoos::prelude::*; - - use std::path::PathBuf; - use std::process::Command; - - #[test] - fn should_return_unmatched_quote_error() { - let path = PathBuf::from("tests/entries/unmatched-quotes.desktop"); - let locales = get_languages_from_env(); - let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); - let result = de.launch_with_uris(&[], false, &locales); - - assert_that!(result) - .is_err() - .matches(|err| matches!(err, ExecError::WrongFormat(..))); - } - - #[test] - fn should_fail_if_exec_string_is_empty() { - let path = PathBuf::from("tests/entries/empty-exec.desktop"); - let locales = get_languages_from_env(); - let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); - let result = de.launch_with_uris(&[], false, &locales); - - 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 locales = get_languages_from_env(); - let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); - let result = de.launch_with_uris(&[], false, &locales); - - 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 locales = get_languages_from_env(); - let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); - let result = de.launch_with_uris(&[], false, &locales); - - 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 locales = get_languages_from_env(); - let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); - let result = de.launch_with_uris(&[], false, &locales); - - 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 locales = get_languages_from_env(); - let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); - let result = de.launch_with_uris(&["src/lib.rs"], false, &locales); - - 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 locales = get_languages_from_env(); - let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); - let result = de.launch_with_uris(&["src/lib.rs"], false, &locales); - - 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 locales = get_languages_from_env(); - let de = DesktopEntry::from_path(path, Some(&locales)).unwrap(); - let _result = de.launch_with_uris(&[], false, &locales); - let path = std::env::current_dir().unwrap(); - let path = path.to_string_lossy(); - let path = format!("file:///{path}"); - let result = de.launch_with_uris(&[path.as_str()], false, &locales); - - 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 0fda1c1..2879c6d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ mod decoder; mod iter; mod exec; -pub use exec::error::ExecError; +pub use exec::ExecError; pub mod matching; pub use decoder::DecodeError; @@ -480,7 +480,7 @@ fn env_with_locale() { let locales = &["fr_FR"]; let de = DesktopEntry::from_path( - PathBuf::from("tests/org.mozilla.firefox.desktop"), + PathBuf::from("tests_entries/org.mozilla.firefox.desktop"), Some(locales), ) .unwrap(); diff --git a/tests/code.desktop b/tests_entries/code.desktop similarity index 100% rename from tests/code.desktop rename to tests_entries/code.desktop diff --git a/tests/com.brave.Browser.flextop.brave-cinhimbnkkaeohfgghhklpknlkffjgod-Default.desktop b/tests_entries/com.brave.Browser.flextop.brave-cinhimbnkkaeohfgghhklpknlkffjgod-Default.desktop similarity index 100% rename from tests/com.brave.Browser.flextop.brave-cinhimbnkkaeohfgghhklpknlkffjgod-Default.desktop rename to tests_entries/com.brave.Browser.flextop.brave-cinhimbnkkaeohfgghhklpknlkffjgod-Default.desktop diff --git a/tests/entries/alacritty-simple.desktop b/tests_entries/exec/alacritty-simple.desktop similarity index 100% rename from tests/entries/alacritty-simple.desktop rename to tests_entries/exec/alacritty-simple.desktop diff --git a/tests/entries/empty-exec.desktop b/tests_entries/exec/empty-exec.desktop similarity index 100% rename from tests/entries/empty-exec.desktop rename to tests_entries/exec/empty-exec.desktop diff --git a/tests/entries/non-terminal-cmd.desktop b/tests_entries/exec/non-terminal-cmd.desktop similarity index 100% rename from tests/entries/non-terminal-cmd.desktop rename to tests_entries/exec/non-terminal-cmd.desktop diff --git a/tests/entries/terminal-cmd.desktop b/tests_entries/exec/terminal-cmd.desktop similarity index 100% rename from tests/entries/terminal-cmd.desktop rename to tests_entries/exec/terminal-cmd.desktop diff --git a/tests/entries/unmatched-quotes.desktop b/tests_entries/exec/unmatched-quotes.desktop similarity index 100% rename from tests/entries/unmatched-quotes.desktop rename to tests_entries/exec/unmatched-quotes.desktop diff --git a/tests/org.kde.krita.desktop b/tests_entries/org.kde.krita.desktop similarity index 100% rename from tests/org.kde.krita.desktop rename to tests_entries/org.kde.krita.desktop diff --git a/tests/org.mozilla.firefox.desktop b/tests_entries/org.mozilla.firefox.desktop similarity index 100% rename from tests/org.mozilla.firefox.desktop rename to tests_entries/org.mozilla.firefox.desktop