diff --git a/Cargo.toml b/Cargo.toml index 560cc40..102825d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,18 @@ gettext-rs = { version = "0.7", features = ["gettext-system"]} memchr = "2" thiserror = "1" xdg = "2.4.0" +udev = "0.6.3" +zbus = "2.2.0" +fork = "0.1.19" + +[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/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/examples/de_launch.rs b/examples/de_launch.rs new file mode 100644 index 0000000..a99f8f9 --- /dev/null +++ b/examples/de_launch.rs @@ -0,0 +1,12 @@ +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(&[]).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..5c633ff --- /dev/null +++ b/src/exec/dbus.rs @@ -0,0 +1,90 @@ +// 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::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], + action: Option, + ) -> 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 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()))); + } + } + } + + 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(()) + } + + pub(crate) fn is_bus_actionable(&self, conn: &Connection) -> bool { + let dbus_proxy = zbus::blocking::fdo::DBusProxy::new(conn); + + if dbus_proxy.is_err() { + return false; + } + + let dbus_proxy = dbus_proxy.unwrap(); + let dbus_names = dbus_proxy.list_activatable_names(); + + 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 new file mode 100644 index 0000000..ab5c98d --- /dev/null +++ b/src/exec/error.rs @@ -0,0 +1,46 @@ +// 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("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..c2f769d --- /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::{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"); + + 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..c5af65b --- /dev/null +++ b/src/exec/mod.rs @@ -0,0 +1,358 @@ +// Copyright 2021 System76 +// SPDX-License-Identifier: MPL-2.0 + +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; + +mod dbus; +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, None) + } else { + self.shell_launch(uris, None) + } + } + Err(_) => self.shell_launch(uris, None), + } + } + + 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, + }); + } + + exec.unwrap() + } + }; + + 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); + + if exec_args.is_empty() { + return Err(ExecError::EmptyExecString); + } + + let exec_args = exec_args.join(" "); + let shell = std::env::var("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) + } else { + cmd + } + .args(args) + .exec() + } else { + let mut cmd = Command::new(shell); + + if self.prefers_non_default_gpu() { + with_non_default_gpu(cmd) + } else { + cmd + } + .args(&["-c", &exec_args]) + .exec() + }; + } + + Ok(()) + } + + // Replace field code with their values and ignore deprecated and unknown field codes + fn get_args(&self, uris: &[&str], exec_args: Vec) -> Vec { + exec_args + .iter() + .filter_map(|arg| match arg { + ArgOrFieldCode::SingleFileName | ArgOrFieldCode::SingleUrl => { + uris.get(0).map(|filename| filename.to_string()) + } + ArgOrFieldCode::FileList | ArgOrFieldCode::UrlList => { + if !uris.is_empty() { + Some(uris.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()) + } + 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_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(&[]); + + 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(&[]); + + 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(&[]); + + 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(&[]); + + 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"]); + + 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(&[]); + + 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()]); + + 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")); + 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..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