From 58c11b9e0917eb40467d095f5229b07d9a1237da Mon Sep 17 00:00:00 2001 From: Paul Delafosse Date: Thu, 2 Jun 2022 12:21:17 +0200 Subject: [PATCH 01/25] 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 | 1 + 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, 569 insertions(+) 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 f909711..155ac3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,7 @@ textdistance = "1.0.2" strsim = "0.11.1" 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..3776cb7 --- /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 b911c26..4982d94 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 mod decoder; +pub mod exec; mod iter; pub mod matching; 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 2b0fb2d76c5d5295b1a6508db8065e6ae4758692 Mon Sep 17 00:00:00 2001 From: Paul Delafosse Date: Thu, 2 Jun 2022 14:38:30 +0200 Subject: [PATCH 02/25] feat: launch desktop entries via dbus --- Cargo.toml | 14 ++- examples/de_launch.rs | 14 +++ src/exec/dbus.rs | 76 ++++++++++++++++ src/exec/error.rs | 4 + src/exec/mod.rs | 198 +++++++++++++++++++++++++++--------------- 5 files changed, 236 insertions(+), 70 deletions(-) create mode 100644 examples/de_launch.rs create mode 100644 src/exec/dbus.rs diff --git a/Cargo.toml b/Cargo.toml index 155ac3e..03558b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,17 @@ textdistance = "1.0.2" strsim = "0.11.1" thiserror = "1" xdg = "2.4.0" -udev = "0.6.3" +udev = "0.8.0" +zbus = "4.2.2" [dev-dependencies] -speculoos = "0.9.0" +speculoos = "0.11.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..244a122 --- /dev/null +++ b/examples/de_launch.rs @@ -0,0 +1,14 @@ +use freedesktop_desktop_entry::{get_languages_from_env, 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 locales = get_languages_from_env(); + let input = fs::read_to_string(&path).expect("Failed to read file"); + let de = DesktopEntry::from_path(path, &locales).expect("Error decoding desktop entry"); + de.launch(&[], false, &locales) + .expect("Failed to run desktop entry"); +} diff --git a/src/exec/dbus.rs b/src/exec/dbus.rs new file mode 100644 index 0000000..722068f --- /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::names::OwnedBusName; +use zbus::zvariant::{OwnedValue, Str}; +use zbus::{connection, interface}; + +#[interface(name = "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.as_ref())? + .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..a9f21f0 100644 --- a/src/exec/error.rs +++ b/src/exec/error.rs @@ -28,4 +28,8 @@ 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 3776cb7..6d66826 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -1,24 +1,49 @@ 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; +use zbus::blocking::Connection; +mod dbus; pub mod error; mod graphics; -impl DesktopEntry<'_> { +impl<'a> DesktopEntry<'a> { /// Execute the given desktop entry `Exec` key with either the default gpu or /// the alternative one if available. - pub fn launch( + pub fn launch( &self, - filename: Option<&str>, - filenames: &[&str], - url: Option<&str>, - urls: &[&str], + uris: &[&'a str], prefer_non_default_gpu: bool, - ) -> Result<(), ExecError> { + locales: &[L], + ) -> Result<(), ExecError> + where + L: AsRef, + { + 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, locales) + } + } + Err(_) => self.shell_launch(uris, prefer_non_default_gpu, locales), + } + } + + fn shell_launch( + &self, + uris: &[&str], + prefer_non_default_gpu: bool, + locales: &[L], + ) -> Result<(), ExecError> + where + L: AsRef, + { let exec = self.exec(); if exec.is_none() { return Err(ExecError::MissingExecKey(&self.path)); @@ -42,7 +67,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, locales); if exec_args.is_empty() { return Err(ExecError::EmptyExecString); @@ -63,8 +88,8 @@ impl DesktopEntry<'_> { cmd } .args(args) - .output()? - .status + .spawn()? + .try_wait()? } else { let mut cmd = Command::new(shell); @@ -74,65 +99,65 @@ 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 { - 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 + 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.get(0) { + final_args.push(Cow::Borrowed(arg)); } } - ArgOrFieldCode::SingleUrl => url.map(|url| url.to_string()), - ArgOrFieldCode::UrlList => { - if !urls.is_empty() { - Some(urls.join(" ")) - } else { - None + ArgOrFieldCode::FileList | ArgOrFieldCode::UrlList => { + uris.into_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::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 + if let Some(name) = self.name(locales) { + final_args.push(name.clone()); } } ArgOrFieldCode::DesktopFileLocation => { - Some(self.path.to_string_lossy().to_string()) + final_args.push(self.path.to_string_lossy()); + } + ArgOrFieldCode::Arg(arg) => { + final_args.push(Cow::Borrowed(&arg)); } - // Ignore deprecated field-codes - ArgOrFieldCode::Arg(arg) => Some(arg.to_string()), - }) - .collect() + } + } + + final_args } } @@ -216,7 +241,7 @@ fn detect_terminal() -> (PathBuf, &'static str) { mod test { use crate::exec::error::ExecError; use crate::exec::with_non_default_gpu; - use crate::DesktopEntry; + use crate::{get_languages_from_env, DesktopEntry}; use speculoos::prelude::*; use std::fs; use std::path::{Path, PathBuf}; @@ -225,9 +250,9 @@ mod test { #[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); + let locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, &locales).unwrap(); + let result = de.launch(&[], false, &locales); assert_that!(result) .is_err() @@ -237,9 +262,9 @@ mod test { #[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); + let locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, &locales).unwrap(); + let result = de.launch(&[], false, &locales); assert_that!(result) .is_err() @@ -250,9 +275,9 @@ mod 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); + let locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, &locales).unwrap(); + let result = de.launch(&[], false, &locales); assert_that!(result).is_ok(); } @@ -261,9 +286,9 @@ mod 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); + let locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, &locales).unwrap(); + let result = de.launch(&[], false, &locales); assert_that!(result).is_ok(); } @@ -272,9 +297,46 @@ mod 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); + let locales = get_languages_from_env(); + let de = DesktopEntry::from_path(path, &locales).unwrap(); + let result = de.launch(&[], 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, &locales).unwrap(); + let result = de.launch(&["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, &locales).unwrap(); + let result = de.launch(&["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, &locales).unwrap(); + let _result = de.launch(&[], false, &locales); + 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, &locales); assert_that!(result).is_ok(); } From 62ba13cb9f4164d0283172aff7cf107da3e07f96 Mon Sep 17 00:00:00 2001 From: Paul Delafosse Date: Thu, 2 Jun 2022 14:55:56 +0200 Subject: [PATCH 03/25] 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, 13 insertions(+), 3 deletions(-) diff --git a/examples/de_launch.rs b/examples/de_launch.rs index 244a122..ec7b3c1 100644 --- a/examples/de_launch.rs +++ b/examples/de_launch.rs @@ -11,4 +11,5 @@ fn main() { let de = DesktopEntry::from_path(path, &locales).expect("Error decoding desktop entry"); de.launch(&[], false, &locales) .expect("Failed to run desktop entry"); -} + +} \ No newline at end of file diff --git a/src/exec/dbus.rs b/src/exec/dbus.rs index 722068f..a7d579f 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 a9f21f0..cb4f1d5 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 6d66826..5f2260e 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 0fab5e5828917c27f71bc9e8b8226c12fce4a249 Mon Sep 17 00:00:00 2001 From: Paul Delafosse Date: Fri, 3 Jun 2022 08:44:13 +0200 Subject: [PATCH 04/25] refactor: use compile time env var for vulkan icd base path --- src/exec/dbus.rs | 2 +- src/exec/graphics.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/exec/dbus.rs b/src/exec/dbus.rs index a7d579f..bedb9e9 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 { From 53e82a19422161d41808ed760e74b7aa958e71d3 Mon Sep 17 00:00:00 2001 From: Paul Delafosse Date: Fri, 3 Jun 2022 09:05:38 +0200 Subject: [PATCH 05/25] refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead --- examples/de_launch.rs | 8 ++++- src/exec/dbus.rs | 3 +- src/exec/error.rs | 3 -- src/exec/mod.rs | 80 +++++++++++++++++++++++++++++++++---------- 4 files changed, 70 insertions(+), 24 deletions(-) diff --git a/examples/de_launch.rs b/examples/de_launch.rs index ec7b3c1..2e8454c 100644 --- a/examples/de_launch.rs +++ b/examples/de_launch.rs @@ -8,8 +8,14 @@ fn main() { let path = PathBuf::from(path); let locales = get_languages_from_env(); let input = fs::read_to_string(&path).expect("Failed to read file"); +<<<<<<< HEAD let de = DesktopEntry::from_path(path, &locales).expect("Error decoding desktop entry"); de.launch(&[], false, &locales) .expect("Failed to run desktop entry"); -} \ No newline at end of file +} +======= + let de = DesktopEntry::decode(path.as_path(), &input).expect("Error decoding desktop entry"); + de.launch(&[]).expect("Failed to run desktop entry"); +} +>>>>>>> 8a6e90a (refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead) diff --git a/src/exec/dbus.rs b/src/exec/dbus.rs index bedb9e9..f5f0954 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 cb4f1d5..bec75bf 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 5f2260e..4518e54 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -17,6 +17,7 @@ mod graphics; impl<'a> DesktopEntry<'a> { /// Execute the given desktop entry `Exec` key with either the default gpu or /// the alternative one if available. +<<<<<<< HEAD pub fn launch( &self, uris: &[&'a str], @@ -26,11 +27,15 @@ impl<'a> DesktopEntry<'a> { where L: AsRef, { +======= + pub fn launch(&self, uris: &[&str]) -> Result<(), ExecError> { +>>>>>>> 8a6e90a (refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead) 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 { +<<<<<<< HEAD self.shell_launch(uris, prefer_non_default_gpu, locales) } } @@ -47,21 +52,22 @@ impl<'a> DesktopEntry<'a> { where L: AsRef, { +======= + self.shell_launch(uris) + } + } + Err(_) => self.shell_launch(uris), + } + } + + fn shell_launch(&self, uris: &[&str]) -> Result<(), ExecError> { +>>>>>>> 8a6e90a (refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead) 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![]; @@ -85,25 +91,25 @@ impl<'a> DesktopEntry<'a> { 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 { @@ -251,6 +257,7 @@ mod test { use std::process::Command; #[test] +<<<<<<< HEAD fn should_return_unmatched_quote_error() { let path = PathBuf::from("tests/entries/unmatched-quotes.desktop"); let locales = get_languages_from_env(); @@ -268,6 +275,13 @@ mod test { let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); let result = de.launch(&[], false, &locales); +======= + 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(&[]); +>>>>>>> 8a6e90a (refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead) assert_that!(result) .is_err() @@ -278,9 +292,15 @@ mod 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"); +<<<<<<< HEAD let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); let result = de.launch(&[], false, &locales); +======= + let input = fs::read_to_string(&path).unwrap(); + let de = DesktopEntry::decode(path.as_path(), &input).unwrap(); + let result = de.launch(&[]); +>>>>>>> 8a6e90a (refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead) assert_that!(result).is_ok(); } @@ -289,9 +309,15 @@ mod 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"); +<<<<<<< HEAD let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); let result = de.launch(&[], false, &locales); +======= + let input = fs::read_to_string(&path).unwrap(); + let de = DesktopEntry::decode(path.as_path(), &input).unwrap(); + let result = de.launch(&[]); +>>>>>>> 8a6e90a (refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead) assert_that!(result).is_ok(); } @@ -300,9 +326,15 @@ mod 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"); +<<<<<<< HEAD let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); let result = de.launch(&[], false, &locales); +======= + let input = fs::read_to_string(&path).unwrap(); + let de = DesktopEntry::decode(path.as_path(), &input).unwrap(); + let result = de.launch(&[]); +>>>>>>> 8a6e90a (refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead) assert_that!(result).is_ok(); } @@ -311,9 +343,15 @@ mod 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"); +<<<<<<< HEAD let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); let result = de.launch(&["src/lib.rs"], false, &locales); +======= + let input = fs::read_to_string(&path).unwrap(); + let de = DesktopEntry::decode(path.as_path(), &input).unwrap(); + let result = de.launch(&["src/lib.rs"]); +>>>>>>> 8a6e90a (refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead) assert_that!(result).is_ok(); } @@ -322,9 +360,15 @@ mod 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"); +<<<<<<< HEAD let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); let result = de.launch(&["src/lib.rs"], false, &locales); +======= + let input = fs::read_to_string(&path).unwrap(); + let de = DesktopEntry::decode(path.as_path(), &input).unwrap(); + let result = de.launch(&[]); +>>>>>>> 8a6e90a (refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead) assert_that!(result).is_ok(); } From caa1db5e8e3d80d43598022049407969ef8cb3a5 Mon Sep 17 00:00:00 2001 From: Paul Delafosse Date: Mon, 6 Jun 2022 10:30:38 +0200 Subject: [PATCH 06/25] fix: fork process instead of spawning with std::process::Command --- Cargo.toml | 6 +++++ src/exec/dbus.rs | 6 +---- src/exec/graphics.rs | 5 +++- src/exec/mod.rs | 63 ++++++++++++++++++++++---------------------- 4 files changed, 43 insertions(+), 37 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 03558b8..883089b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,8 +18,14 @@ textdistance = "1.0.2" strsim = "0.11.1" thiserror = "1" xdg = "2.4.0" +<<<<<<< HEAD udev = "0.8.0" zbus = "4.2.2" +======= +udev = "0.6.3" +zbus = "2.2.0" +fork = "0.1.19" +>>>>>>> eaa21ff (fix: fork process instead of spawning with std::process::Command) [dev-dependencies] speculoos = "0.11.0" diff --git a/src/exec/dbus.rs b/src/exec/dbus.rs index f5f0954..75016bc 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 4518e54..6d44e15 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -4,8 +4,13 @@ use crate::exec::error::ExecError; use crate::exec::graphics::Gpus; use crate::DesktopEntry; +<<<<<<< HEAD use std::borrow::Cow; +======= +use fork::{daemon, Fork}; +>>>>>>> eaa21ff (fix: fork process instead of spawning with std::process::Command) use std::convert::TryFrom; +use std::os::unix::prelude::CommandExt; use std::path::PathBuf; use std::process::Command; use zbus::blocking::Connection; @@ -85,40 +90,32 @@ impl<'a> DesktopEntry<'a> { 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(()) @@ -383,7 +380,11 @@ mod test { let path = std::env::current_dir().unwrap(); let path = path.to_string_lossy(); let path = format!("file:///{path}"); +<<<<<<< HEAD let result = de.launch(&[path.as_str()], false, &locales); +======= + let result = de.launch(&[path.as_str()]); +>>>>>>> eaa21ff (fix: fork process instead of spawning with std::process::Command) assert_that!(result).is_ok(); } From c69c7bdd07920b1ce9423f8510242837a26a4a62 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Thu, 6 Jun 2024 01:45:42 +0200 Subject: [PATCH 07/25] hello --- src/exec/error.rs | 12 +++ src/exec/mod.rs | 125 ++++++++----------------- 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 +- 6 files changed, 56 insertions(+), 89 deletions(-) diff --git a/src/exec/error.rs b/src/exec/error.rs index bec75bf..96003cb 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 6d44e15..403094f 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -4,13 +4,8 @@ use crate::exec::error::ExecError; use crate::exec::graphics::Gpus; use crate::DesktopEntry; -<<<<<<< HEAD use std::borrow::Cow; -======= -use fork::{daemon, Fork}; ->>>>>>> eaa21ff (fix: fork process instead of spawning with std::process::Command) use std::convert::TryFrom; -use std::os::unix::prelude::CommandExt; use std::path::PathBuf; use std::process::Command; use zbus::blocking::Connection; @@ -22,7 +17,6 @@ mod graphics; impl<'a> DesktopEntry<'a> { /// Execute the given desktop entry `Exec` key with either the default gpu or /// the alternative one if available. -<<<<<<< HEAD pub fn launch( &self, uris: &[&'a str], @@ -32,15 +26,11 @@ impl<'a> DesktopEntry<'a> { where L: AsRef, { -======= - pub fn launch(&self, uris: &[&str]) -> Result<(), ExecError> { ->>>>>>> 8a6e90a (refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead) match Connection::session() { Ok(conn) => { if self.is_bus_actionable(&conn) { self.dbus_launch(&conn, uris) } else { -<<<<<<< HEAD self.shell_launch(uris, prefer_non_default_gpu, locales) } } @@ -57,22 +47,21 @@ impl<'a> DesktopEntry<'a> { where L: AsRef, { -======= - self.shell_launch(uris) - } - } - Err(_) => self.shell_launch(uris), - } - } - - fn shell_launch(&self, uris: &[&str]) -> Result<(), ExecError> { ->>>>>>> 8a6e90a (refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead) 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![]; @@ -90,32 +79,40 @@ impl<'a> DesktopEntry<'a> { 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); + 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 self.prefers_non_default_gpu() { - with_non_default_gpu(cmd) - } else { - cmd - } - .args(args) - .exec() + if prefer_non_default_gpu { + with_non_default_gpu(cmd) } else { - let mut cmd = Command::new(shell); + cmd + } + .args(&["-c", &exec_args]) + .spawn()? + .try_wait()? + }; - if self.prefers_non_default_gpu() { - with_non_default_gpu(cmd) - } else { - cmd - } - .args(&["-c", &exec_args]) - .exec() - }; + if let Some(status) = status { + if !status.success() { + return Err(ExecError::NonZeroStatusCode { + status: status.code(), + exec: exec.to_string(), + }); + } } Ok(()) @@ -254,7 +251,6 @@ mod test { use std::process::Command; #[test] -<<<<<<< HEAD fn should_return_unmatched_quote_error() { let path = PathBuf::from("tests/entries/unmatched-quotes.desktop"); let locales = get_languages_from_env(); @@ -272,13 +268,6 @@ mod test { let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); let result = de.launch(&[], false, &locales); -======= - 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(&[]); ->>>>>>> 8a6e90a (refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead) assert_that!(result) .is_err() @@ -289,15 +278,9 @@ mod 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"); -<<<<<<< HEAD let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); let result = de.launch(&[], false, &locales); -======= - let input = fs::read_to_string(&path).unwrap(); - let de = DesktopEntry::decode(path.as_path(), &input).unwrap(); - let result = de.launch(&[]); ->>>>>>> 8a6e90a (refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead) assert_that!(result).is_ok(); } @@ -306,15 +289,9 @@ mod 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"); -<<<<<<< HEAD let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); let result = de.launch(&[], false, &locales); -======= - let input = fs::read_to_string(&path).unwrap(); - let de = DesktopEntry::decode(path.as_path(), &input).unwrap(); - let result = de.launch(&[]); ->>>>>>> 8a6e90a (refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead) assert_that!(result).is_ok(); } @@ -323,15 +300,9 @@ mod 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"); -<<<<<<< HEAD let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); let result = de.launch(&[], false, &locales); -======= - let input = fs::read_to_string(&path).unwrap(); - let de = DesktopEntry::decode(path.as_path(), &input).unwrap(); - let result = de.launch(&[]); ->>>>>>> 8a6e90a (refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead) assert_that!(result).is_ok(); } @@ -340,15 +311,9 @@ mod 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"); -<<<<<<< HEAD let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); let result = de.launch(&["src/lib.rs"], false, &locales); -======= - let input = fs::read_to_string(&path).unwrap(); - let de = DesktopEntry::decode(path.as_path(), &input).unwrap(); - let result = de.launch(&["src/lib.rs"]); ->>>>>>> 8a6e90a (refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead) assert_that!(result).is_ok(); } @@ -357,15 +322,9 @@ mod 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"); -<<<<<<< HEAD let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); let result = de.launch(&["src/lib.rs"], false, &locales); -======= - let input = fs::read_to_string(&path).unwrap(); - let de = DesktopEntry::decode(path.as_path(), &input).unwrap(); - let result = de.launch(&[]); ->>>>>>> 8a6e90a (refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead) assert_that!(result).is_ok(); } @@ -380,11 +339,7 @@ mod test { let path = std::env::current_dir().unwrap(); let path = path.to_string_lossy(); let path = format!("file:///{path}"); -<<<<<<< HEAD let result = de.launch(&[path.as_str()], false, &locales); -======= - let result = de.launch(&[path.as_str()]); ->>>>>>> eaa21ff (fix: fork process instead of spawning with std::process::Command) assert_that!(result).is_ok(); } 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 From 477ce38f6fcf12c2ba0ebf26814949ba471f6781 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Thu, 6 Jun 2024 02:06:12 +0200 Subject: [PATCH 08/25] fix compile --- Cargo.toml | 15 --------------- examples/de_launch.rs | 7 ------- src/decoder.rs | 5 +---- src/exec/dbus.rs | 4 ++-- src/exec/error.rs | 4 +++- src/exec/graphics.rs | 13 +++++++------ src/exec/mod.rs | 6 ++---- src/iter.rs | 1 - 8 files changed, 15 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 883089b..76b903b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,23 +18,8 @@ textdistance = "1.0.2" strsim = "0.11.1" thiserror = "1" xdg = "2.4.0" -<<<<<<< HEAD udev = "0.8.0" zbus = "4.2.2" -======= -udev = "0.6.3" -zbus = "2.2.0" -fork = "0.1.19" ->>>>>>> eaa21ff (fix: fork process instead of spawning with std::process::Command) [dev-dependencies] speculoos = "0.11.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 index 2e8454c..244a122 100644 --- a/examples/de_launch.rs +++ b/examples/de_launch.rs @@ -8,14 +8,7 @@ fn main() { let path = PathBuf::from(path); let locales = get_languages_from_env(); let input = fs::read_to_string(&path).expect("Failed to read file"); -<<<<<<< HEAD let de = DesktopEntry::from_path(path, &locales).expect("Error decoding desktop entry"); de.launch(&[], false, &locales) .expect("Failed to run desktop entry"); - -} -======= - let de = DesktopEntry::decode(path.as_path(), &input).expect("Error decoding desktop entry"); - de.launch(&[]).expect("Failed to run desktop entry"); } ->>>>>>> 8a6e90a (refactor: remove prefer_non_default arg gpu and use desktop entry attribute instead) diff --git a/src/decoder.rs b/src/decoder.rs index 3fac2b8..23f3e31 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -68,10 +68,7 @@ impl<'a> DesktopEntry<'a> { } /// Return an owned [`DesktopEntry`] - pub fn from_path( - path: PathBuf, - locales: &[L], - ) -> Result, DecodeError> + pub fn from_path(path: PathBuf, locales: &[L]) -> Result, DecodeError> where L: AsRef, { diff --git a/src/exec/dbus.rs b/src/exec/dbus.rs index 75016bc..a716f52 100644 --- a/src/exec/dbus.rs +++ b/src/exec/dbus.rs @@ -7,10 +7,10 @@ use crate::DesktopEntry; use std::collections::HashMap; use zbus::blocking::Connection; use zbus::names::OwnedBusName; +use zbus::proxy; use zbus::zvariant::{OwnedValue, Str}; -use zbus::{connection, interface}; -#[interface(name = "org.freedesktop.Application")] +#[proxy(interface = "org.freedesktop.Application")] trait Application { fn activate(&self, platform_data: HashMap) -> zbus::Result<()>; fn activate_action( diff --git a/src/exec/error.rs b/src/exec/error.rs index 96003cb..bf32b61 100644 --- a/src/exec/error.rs +++ b/src/exec/error.rs @@ -8,6 +8,9 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum ExecError<'a> { + #[error("{0}")] + WrongFormat(String), + #[error("Exec string is empty")] EmptyExecString, @@ -43,5 +46,4 @@ pub enum ExecError<'a> { #[error("Failed to launch aplication via dbus: {0}")] DBusError(#[from] zbus::Error), - } diff --git a/src/exec/graphics.rs b/src/exec/graphics.rs index c2f769d..b46f77f 100644 --- a/src/exec/graphics.rs +++ b/src/exec/graphics.rs @@ -5,12 +5,12 @@ use std::collections::HashSet; use std::hash::{Hash, Hasher}; use std::io; use std::ops::Deref; -use std::path::{Path, PathBuf}; +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`)" -); +// 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 { @@ -96,7 +96,8 @@ impl Dev { .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()]; + // 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() { diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 403094f..10ec7ee 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -56,9 +56,7 @@ impl<'a> DesktopEntry<'a> { let exec = if let Some(unquoted_exec) = exec.strip_prefix('\"') { unquoted_exec .strip_suffix('\"') - .ok_or(ExecError::UnmatchedQuote { - exec: exec.to_string(), - })? + .ok_or(ExecError::WrongFormat("unmatched quote".into()))? } else { exec }; @@ -259,7 +257,7 @@ mod test { assert_that!(result) .is_err() - .matches(|err| matches!(err, ExecError::UnmatchedQuote { exec: _ })); + .matches(|err| matches!(err, ExecError::WrongFormat(..))); } #[test] diff --git a/src/iter.rs b/src/iter.rs index bdaedcc..1d90669 100644 --- a/src/iter.rs +++ b/src/iter.rs @@ -37,7 +37,6 @@ impl Iterator for Iter { Err(_) => continue, } } - return None; } From dc493c7ed555c305325ee669ab7a09f27bfa12e1 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Thu, 6 Jun 2024 02:54:21 +0200 Subject: [PATCH 09/25] upgrade the behavior of get_entry_score --- examples/de_launch.rs | 3 +- src/decoder.rs | 2 +- src/exec/dbus.rs | 2 +- src/exec/graphics.rs | 1 - src/exec/mod.rs | 14 +++++----- src/lib.rs | 28 +++++++------------ src/matching.rs | 64 ++++++++++++++++++++++++------------------- 7 files changed, 56 insertions(+), 58 deletions(-) diff --git a/examples/de_launch.rs b/examples/de_launch.rs index 244a122..a996d37 100644 --- a/examples/de_launch.rs +++ b/examples/de_launch.rs @@ -1,13 +1,12 @@ use freedesktop_desktop_entry::{get_languages_from_env, DesktopEntry}; +use std::env; 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 locales = get_languages_from_env(); - let input = fs::read_to_string(&path).expect("Failed to read file"); let de = DesktopEntry::from_path(path, &locales).expect("Error decoding desktop entry"); de.launch(&[], false, &locales) .expect("Failed to run desktop entry"); diff --git a/src/decoder.rs b/src/decoder.rs index 23f3e31..c45e7c1 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -191,7 +191,7 @@ where } /// Ex: if a locale equal fr_FR, add fr -fn add_generic_locales<'a, L: AsRef>(locales: &'a [L]) -> Vec<&'a str> { +fn add_generic_locales>(locales: &[L]) -> Vec<&str> { let mut v = Vec::with_capacity(locales.len() + 1); for l in locales { diff --git a/src/exec/dbus.rs b/src/exec/dbus.rs index a716f52..5d34749 100644 --- a/src/exec/dbus.rs +++ b/src/exec/dbus.rs @@ -42,7 +42,7 @@ impl DesktopEntry<'_> { } if !uris.is_empty() { - app_proxy.open(&uris, platform_data)?; + app_proxy.open(uris, platform_data)?; } else { app_proxy.activate(platform_data)?; } diff --git a/src/exec/graphics.rs b/src/exec/graphics.rs index b46f77f..412ee89 100644 --- a/src/exec/graphics.rs +++ b/src/exec/graphics.rs @@ -172,7 +172,6 @@ fn get_gpus() -> io::Result> { 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")) diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 10ec7ee..24d2e0b 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -99,7 +99,7 @@ impl<'a> DesktopEntry<'a> { } else { cmd } - .args(&["-c", &exec_args]) + .args(["-c", &exec_args]) .spawn()? .try_wait()? }; @@ -131,17 +131,17 @@ impl<'a> DesktopEntry<'a> { for arg in exec_args { match arg { ArgOrFieldCode::SingleFileName | ArgOrFieldCode::SingleUrl => { - if let Some(arg) = uris.get(0) { + if let Some(arg) = uris.first() { final_args.push(Cow::Borrowed(arg)); } } ArgOrFieldCode::FileList | ArgOrFieldCode::UrlList => { - uris.into_iter() + 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)); + final_args.push(Cow::Borrowed(icon)); } } ArgOrFieldCode::TranslatedName => { @@ -153,7 +153,7 @@ impl<'a> DesktopEntry<'a> { final_args.push(self.path.to_string_lossy()); } ArgOrFieldCode::Arg(arg) => { - final_args.push(Cow::Borrowed(&arg)); + final_args.push(Cow::Borrowed(arg)); } } } @@ -244,8 +244,8 @@ mod test { use crate::exec::with_non_default_gpu; use crate::{get_languages_from_env, DesktopEntry}; use speculoos::prelude::*; - use std::fs; - use std::path::{Path, PathBuf}; + + use std::path::PathBuf; use std::process::Command; #[test] diff --git a/src/lib.rs b/src/lib.rs index 4982d94..5d42427 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,7 +36,7 @@ pub struct DesktopEntry<'a> { impl DesktopEntry<'_> { /// Construct a new [`DesktopEntry`] from an appid. The name field will be /// set to that appid. - pub fn from_appid<'a>(appid: &'a str) -> DesktopEntry<'a> { + pub fn from_appid(appid: &str) -> DesktopEntry<'_> { let mut de = DesktopEntry { appid: Cow::Borrowed(appid), groups: Groups::default(), @@ -78,12 +78,10 @@ impl<'a> DesktopEntry<'a> { DesktopEntry { appid: Cow::Owned(self.appid.to_string()), groups: new_groups, - ubuntu_gettext_domain: if let Some(ubuntu_gettext_domain) = &self.ubuntu_gettext_domain - { - Some(Cow::Owned(ubuntu_gettext_domain.to_string())) - } else { - None - }, + ubuntu_gettext_domain: self + .ubuntu_gettext_domain + .as_ref() + .map(|ubuntu_gettext_domain| Cow::Owned(ubuntu_gettext_domain.to_string())), path: Cow::Owned(self.path.to_path_buf()), } } @@ -94,9 +92,9 @@ impl<'a> DesktopEntry<'a> { self.appid.as_ref() } - /// A desktop entry field is any field under the `[Desktop Entry]` section. + /// A desktop entry field if any field under the `[Desktop Entry]` section. pub fn desktop_entry(&'a self, key: &str) -> Option<&'a str> { - Self::entry(self.groups.get("Desktop Entry"), key).map(|e| e.as_ref()) + Self::entry(self.groups.get("Desktop Entry"), key) } pub fn desktop_entry_localized>( @@ -267,13 +265,7 @@ impl<'a> DesktopEntry<'a> { key: &str, locales: &[L], ) -> Option> { - let Some(group) = group else { - return None; - }; - - let Some((default_value, locale_map)) = group.get(key) else { - return None; - }; + let (default_value, locale_map) = group?.get(key)?; for locale in locales { match locale_map.get(locale.as_ref()) { @@ -288,9 +280,9 @@ impl<'a> DesktopEntry<'a> { } } if let Some(domain) = ubuntu_gettext_domain { - return Some(Cow::Owned(dgettext(domain, &default_value))); + return Some(Cow::Owned(dgettext(domain, default_value))); } - return Some(default_value.clone()); + Some(default_value.clone()) } } diff --git a/src/matching.rs b/src/matching.rs index 025ac55..7b9138f 100644 --- a/src/matching.rs +++ b/src/matching.rs @@ -2,6 +2,15 @@ use std::cmp::max; use crate::DesktopEntry; +#[inline] +fn add_value(v: &mut Vec, value: &str, is_multiple: bool) { + if is_multiple { + value.split(';').for_each(|e| v.push(e.to_lowercase())); + } else { + v.push(value.to_lowercase()); + } +} + /// The returned value is between 0.0 and 1.0 (higher value means more similar). /// You can use the `additional_values` parameter to add runtime string. pub fn get_entry_score<'a, Q, L>( @@ -14,53 +23,50 @@ where Q: AsRef, L: AsRef, { - // let the user do this ? - let query = query.as_ref().to_lowercase(); - - // todo: cache all this ? - - let fields = ["Name", "GenericName", "Comment", "Categories", "Keywords"]; - let fields_not_translatable = ["Exec", "StartupWMClass"]; + // (field name, is separated by ";") + let fields = [ + ("Name", false), + ("GenericName", false), + ("Comment", false), + ("Categories", true), + ("Keywords", true), + ]; let mut normalized_values: Vec = Vec::new(); normalized_values.extend(additional_values.iter().map(|val| val.to_lowercase())); - let de_id = entry.appid.to_lowercase(); - let de_wm_class = entry.startup_wm_class().unwrap_or_default().to_lowercase(); - - normalized_values.push(de_id); - normalized_values.push(de_wm_class); - let desktop_entry_group = entry.groups.get("Desktop Entry"); - for field in fields_not_translatable { - if let Some(e) = DesktopEntry::entry(desktop_entry_group, field) { - normalized_values.push(e.to_lowercase()); - } - } + for field in fields { + if let Some(group) = desktop_entry_group { + if let Some((default_value, locale_map)) = group.get(field.0) { + add_value(&mut normalized_values, default_value, field.1); + + let mut at_least_one_locale = false; - for locale in locales { - for field in fields { - if let Some(group) = desktop_entry_group { - if let Some((default_value, locale_map)) = group.get(field) { + for locale in locales { match locale_map.get(locale.as_ref()) { Some(value) => { - normalized_values.push(value.to_lowercase()); + add_value(&mut normalized_values, value, field.1); + at_least_one_locale = true; } None => { if let Some(pos) = locale.as_ref().find('_') { if let Some(value) = locale_map.get(&locale.as_ref()[..pos]) { - normalized_values.push(value.to_lowercase()); + add_value(&mut normalized_values, value, field.1); + at_least_one_locale = true; } } } } + } + if !at_least_one_locale { if let Some(domain) = &entry.ubuntu_gettext_domain { - let gettext_value = crate::dgettext(domain, &default_value); + let gettext_value = crate::dgettext(domain, default_value); if !gettext_value.is_empty() { - normalized_values.push(gettext_value.to_lowercase()); + add_value(&mut normalized_values, &gettext_value, false); } } } @@ -68,6 +74,8 @@ where } } + let query = query.as_ref().to_lowercase(); + normalized_values .into_iter() .map(|de| strsim::jaro_winkler(&query, &de)) @@ -86,9 +94,8 @@ fn compare_str<'a>(pattern: &'a str, de_value: &'a str) -> f64 { fn match_entry_from_id(pattern: &str, de: &DesktopEntry) -> f64 { let de_id = de.appid.to_lowercase(); let de_wm_class = de.startup_wm_class().unwrap_or_default().to_lowercase(); - let de_name = de.name(&[] as &[&str]).unwrap_or_default().to_lowercase(); - *[de_id, de_wm_class, de_name] + *[de_id, de_wm_class] .map(|de| compare_str(pattern, &de)) .iter() .max_by(|e1, e2| e1.total_cmp(e2)) @@ -119,6 +126,7 @@ impl Default for MatchAppIdOptions { /// Return the best match over all provided [`DesktopEntry`]. /// Use this to match over the values provided by the compositor, not the user. +/// First entries get the priority. pub fn get_best_match<'a, I>( patterns: &[I], entries: &'a [DesktopEntry<'a>], From ec76f187ae89020b42e7225466305816606c2e41 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Thu, 6 Jun 2024 03:12:49 +0200 Subject: [PATCH 10/25] add 2 tests --- src/lib.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5d42427..df81399 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -213,7 +213,7 @@ impl<'a> DesktopEntry<'a> { /// Name=Open a New Window /// ``` /// you will need to call - /// ```rust + /// ```ignore /// entry.action_entry("new-window", "Name") /// ``` pub fn action_entry(&'a self, action: &str, key: &str) -> Option<&'a str> { @@ -397,8 +397,29 @@ pub fn get_languages_from_env() -> Vec { } #[test] -fn locales_env_test() { +fn add_field() { + let appid = "appid"; + let de = DesktopEntry::from_appid(appid); + + assert_eq!(de.appid, appid); + assert_eq!(de.name(&[] as &[&str]).unwrap(), appid); + let s = get_languages_from_env(); println!("{:?}", s); } + +#[test] +fn env_with_locale() { + let locales = &["fr_FR"]; + + let de = DesktopEntry::from_path(PathBuf::from("tests/org.mozilla.firefox.desktop"), locales) + .unwrap(); + + assert_eq!(de.generic_name(locales).unwrap(), "Navigateur Web"); + + + let locales = &["nb"]; + + assert_eq!(de.generic_name(locales).unwrap(), "Web Browser"); +} From 0cdb67ff28bd5c89bb3312c878812fb93475e60c Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Thu, 6 Jun 2024 03:21:16 +0200 Subject: [PATCH 11/25] add launch_with_uris variant --- examples/de_launch.rs | 2 +- src/exec/mod.rs | 38 ++++++++++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/examples/de_launch.rs b/examples/de_launch.rs index a996d37..6205c8f 100644 --- a/examples/de_launch.rs +++ b/examples/de_launch.rs @@ -8,6 +8,6 @@ fn main() { let path = PathBuf::from(path); let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).expect("Error decoding desktop entry"); - de.launch(&[], false, &locales) + de.launch_with_uris(&[], false, &locales) .expect("Failed to run desktop entry"); } diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 24d2e0b..4cc86b3 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -15,9 +15,27 @@ pub mod error; mod graphics; impl<'a> DesktopEntry<'a> { + pub fn launch(&self, prefer_non_default_gpu: bool) -> Result<(), ExecError> + where + L: AsRef, + { + match Connection::session() { + Ok(conn) => { + if self.is_bus_actionable(&conn) { + self.dbus_launch(&conn, &[]) + } else { + self.shell_launch(&[], prefer_non_default_gpu, &[] as &[&str]) + } + } + Err(_) => self.shell_launch(&[], prefer_non_default_gpu, &[] as &[&str]), + } + } + /// Execute the given desktop entry `Exec` key with either the default gpu or /// the alternative one if available. - pub fn launch( + /// 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, @@ -253,7 +271,7 @@ mod test { let path = PathBuf::from("tests/entries/unmatched-quotes.desktop"); let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); - let result = de.launch(&[], false, &locales); + let result = de.launch_with_uris(&[], false, &locales); assert_that!(result) .is_err() @@ -265,7 +283,7 @@ mod test { let path = PathBuf::from("tests/entries/empty-exec.desktop"); let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); - let result = de.launch(&[], false, &locales); + let result = de.launch_with_uris(&[], false, &locales); assert_that!(result) .is_err() @@ -278,7 +296,7 @@ mod test { let path = PathBuf::from("tests/entries/alacritty-simple.desktop"); let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); - let result = de.launch(&[], false, &locales); + let result = de.launch_with_uris(&[], false, &locales); assert_that!(result).is_ok(); } @@ -289,7 +307,7 @@ mod test { let path = PathBuf::from("tests/entries/non-terminal-cmd.desktop"); let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); - let result = de.launch(&[], false, &locales); + let result = de.launch_with_uris(&[], false, &locales); assert_that!(result).is_ok(); } @@ -300,7 +318,7 @@ mod test { let path = PathBuf::from("tests/entries/non-terminal-cmd.desktop"); let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); - let result = de.launch(&[], false, &locales); + let result = de.launch_with_uris(&[], false, &locales); assert_that!(result).is_ok(); } @@ -311,7 +329,7 @@ mod test { let path = PathBuf::from("/usr/share/applications/nvim.desktop"); let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); - let result = de.launch(&["src/lib.rs"], false, &locales); + let result = de.launch_with_uris(&["src/lib.rs"], false, &locales); assert_that!(result).is_ok(); } @@ -322,7 +340,7 @@ mod test { let path = PathBuf::from("/usr/share/applications/org.gnome.Books.desktop"); let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); - let result = de.launch(&["src/lib.rs"], false, &locales); + let result = de.launch_with_uris(&["src/lib.rs"], false, &locales); assert_that!(result).is_ok(); } @@ -333,11 +351,11 @@ mod test { let path = PathBuf::from("/usr/share/applications/org.gnome.Nautilus.desktop"); let locales = get_languages_from_env(); let de = DesktopEntry::from_path(path, &locales).unwrap(); - let _result = de.launch(&[], false, &locales); + 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(&[path.as_str()], false, &locales); + let result = de.launch_with_uris(&[path.as_str()], false, &locales); assert_that!(result).is_ok(); } From 21f86a03c1fee44fa71b73c6d7b20ee70ecdfd77 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Thu, 6 Jun 2024 03:24:52 +0200 Subject: [PATCH 12/25] change export, remove test, add licence on top --- src/decoder.rs | 3 +++ src/lib.rs | 9 +++------ src/matching.rs | 3 +++ src/test.rs | 22 ---------------------- 4 files changed, 9 insertions(+), 28 deletions(-) delete mode 100644 src/test.rs diff --git a/src/decoder.rs b/src/decoder.rs index c45e7c1..cef1256 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -1,3 +1,6 @@ +// Copyright 2021 System76 +// SPDX-License-Identifier: MPL-2.0 + use std::{ borrow::Cow, collections::BTreeMap, diff --git a/src/lib.rs b/src/lib.rs index df81399..c8b8ab8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,14 +2,12 @@ // SPDX-License-Identifier: MPL-2.0 mod decoder; -pub mod exec; +mod exec; +pub use exec::error::ExecError; mod iter; pub mod matching; -#[cfg(test)] -mod test; - pub use self::iter::Iter; use std::borrow::Cow; use std::collections::BTreeMap; @@ -418,8 +416,7 @@ fn env_with_locale() { assert_eq!(de.generic_name(locales).unwrap(), "Navigateur Web"); - let locales = &["nb"]; - + assert_eq!(de.generic_name(locales).unwrap(), "Web Browser"); } diff --git a/src/matching.rs b/src/matching.rs index 7b9138f..65a37c6 100644 --- a/src/matching.rs +++ b/src/matching.rs @@ -1,3 +1,6 @@ +// Copyright 2021 System76 +// SPDX-License-Identifier: MPL-2.0 + use std::cmp::max; use crate::DesktopEntry; diff --git a/src/test.rs b/src/test.rs deleted file mode 100644 index 8588778..0000000 --- a/src/test.rs +++ /dev/null @@ -1,22 +0,0 @@ -use std::path::Path; - -use crate::DesktopEntry; - -#[test] -fn test() { - let path = Path::new("tests/org.mozilla.firefox.desktop"); - - let locales = &["fr", "fr_FR.UTF-8"]; - - if let Ok(entry) = DesktopEntry::from_path(path.to_path_buf(), locales) { - let e = DesktopEntry::localized_entry( - None, - entry.groups.get("Desktop Entry"), - "GenericName", - &["fr"], - ) - .unwrap(); - - assert_eq!(e, "Navigateur Web"); - } -} From e8459b7347c65fc0073fb74524037e8f161b1edf Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Thu, 6 Jun 2024 11:11:55 +0200 Subject: [PATCH 13/25] add actions support --- Cargo.toml | 1 + src/exec/dbus.rs | 99 +++++++++++++++++++++++++++++++++++------------- src/exec/mod.rs | 75 +++++++++++++++++++++++------------- 3 files changed, 122 insertions(+), 53 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 76b903b..e956ce9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ thiserror = "1" xdg = "2.4.0" udev = "0.8.0" zbus = "4.2.2" +log = "0.4.21" [dev-dependencies] speculoos = "0.11.0" diff --git a/src/exec/dbus.rs b/src/exec/dbus.rs index 5d34749..db5e2a2 100644 --- a/src/exec/dbus.rs +++ b/src/exec/dbus.rs @@ -10,47 +10,43 @@ 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: &[OwnedValue], + parameters: &[&str], 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]) -> Result<(), ExecError> { - let dbus_path = self.appid.replace('.', "/"); - let dbus_path = format!("/{dbus_path}"); - let app_proxy = ApplicationProxyBlocking::builder(conn) - .destination(self.appid.as_ref())? - .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()))); + 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 + } } - } - } - - if !uris.is_empty() { - app_proxy.open(uris, platform_data)?; - } else { - app_proxy.activate(platform_data)?; + Err(e) => { + log::error!("can't open dbus session: {}", e); + None + } + }, + false => None, } - - Ok(()) } - pub(crate) fn is_bus_actionable(&self, conn: &Connection) -> bool { + fn is_bus_actionable(&self, conn: &Connection) -> bool { let dbus_proxy = zbus::blocking::fdo::DBusProxy::new(conn); if dbus_proxy.is_err() { @@ -71,4 +67,55 @@ impl DesktopEntry<'_> { .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/mod.rs b/src/exec/mod.rs index 4cc86b3..d83b021 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -8,31 +8,20 @@ use std::borrow::Cow; use std::convert::TryFrom; use std::path::PathBuf; use std::process::Command; -use zbus::blocking::Connection; mod dbus; pub mod error; mod graphics; impl<'a> DesktopEntry<'a> { - pub fn launch(&self, prefer_non_default_gpu: bool) -> Result<(), ExecError> - where - L: AsRef, - { - match Connection::session() { - Ok(conn) => { - if self.is_bus_actionable(&conn) { - self.dbus_launch(&conn, &[]) - } else { - self.shell_launch(&[], prefer_non_default_gpu, &[] as &[&str]) - } - } - Err(_) => self.shell_launch(&[], prefer_non_default_gpu, &[] as &[&str]), + 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. + /// 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( @@ -44,20 +33,53 @@ impl<'a> DesktopEntry<'a> { where L: AsRef, { - match Connection::session() { - Ok(conn) => { - if self.is_bus_actionable(&conn) { - self.dbus_launch(&conn, uris) - } else { - self.shell_launch(uris, prefer_non_default_gpu, locales) - } - } - Err(_) => self.shell_launch(uris, prefer_non_default_gpu, locales), + 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), } } - fn shell_launch( + 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], @@ -65,7 +87,6 @@ impl<'a> DesktopEntry<'a> { where L: AsRef, { - let exec = self.exec(); if exec.is_none() { return Err(ExecError::MissingExecKey(&self.path)); } From 2eaacb879bc928d065f4ebe0de6e158f78f1a008 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:21:05 +0200 Subject: [PATCH 14/25] add not_show_in, directly return a Vec when field is a list --- Cargo.toml | 2 +- src/lib.rs | 62 +++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e956ce9..8901e74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "freedesktop-desktop-entry" -version = "0.5.4" +version = "0.6.0" authors = ["Michael Aaron Murphy "] edition = "2021" homepage = "https://github.com/pop-os/freedesktop-desktop-entry" diff --git a/src/lib.rs b/src/lib.rs index c8b8ab8..360f1fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -151,27 +151,35 @@ impl<'a> DesktopEntry<'a> { self.desktop_entry("Exec") } - /// Return categories separated by `;` - pub fn categories(&'a self) -> Option<&'a str> { + /// Return categories + pub fn categories(&'a self) -> Option> { self.desktop_entry("Categories") + .map(|e| e.split(';').collect()) } - /// Return keywords separated by `;` - pub fn keywords>(&'a self, locales: &[L]) -> Option> { - self.desktop_entry_localized("Keywords", locales) + /// Return keywords + pub fn keywords>(&'a self, locales: &[L]) -> Option>> { + self.localized_entry_splitted(self.groups.get("Desktop Entry"), "Keywords", locales) } - /// Return mime types separated by `;` - pub fn mime_type(&'a self) -> Option<&'a str> { + /// Return mime types + pub fn mime_type(&'a self) -> Option> { self.desktop_entry("MimeType") + .map(|e| e.split(';').collect()) } pub fn no_display(&'a self) -> bool { self.desktop_entry_bool("NoDisplay") } - pub fn only_show_in(&'a self) -> Option<&'a str> { + pub fn only_show_in(&'a self) -> Option> { self.desktop_entry("OnlyShowIn") + .map(|e| e.split(';').collect()) + } + + pub fn not_show_in(&'a self) -> Option> { + self.desktop_entry("NotShowIn") + .map(|e| e.split(';').collect()) } pub fn flatpak(&'a self) -> Option<&'a str> { @@ -198,9 +206,9 @@ impl<'a> DesktopEntry<'a> { self.desktop_entry("Type") } - /// Return actions separated by `;` - pub fn actions(&'a self) -> Option<&'a str> { + pub fn actions(&'a self) -> Option> { self.desktop_entry("Actions") + .map(|e| e.split(';').collect()) } /// An action is defined as `[Desktop Action actions-name]` where `action-name` @@ -282,6 +290,40 @@ impl<'a> DesktopEntry<'a> { } Some(default_value.clone()) } + + pub fn localized_entry_splitted>( + &self, + group: Option<&'a KeyMap<'a>>, + key: &str, + locales: &[L], + ) -> Option>> { + let (default_value, locale_map) = group?.get(key)?; + + for locale in locales { + match locale_map.get(locale.as_ref()) { + Some(value) => { + return Some(value.split(';').map(|e| Cow::Borrowed(e)).collect()); + } + None => { + if let Some(pos) = memchr::memchr(b'_', locale.as_ref().as_bytes()) { + if let Some(value) = locale_map.get(&locale.as_ref()[..pos]) { + return Some(value.split(';').map(|e| Cow::Borrowed(e)).collect()); + } + } + } + } + } + if let Some(domain) = &self.ubuntu_gettext_domain { + return Some( + dgettext(domain, default_value) + .split(';') + .map(|e| Cow::Owned(e.to_string())) + .collect(), + ); + } + + Some(default_value.split(';').map(|e| Cow::Borrowed(e)).collect()) + } } use std::fmt::{self, Display, Formatter}; From 7d349d9f61692cd554f8a23cc063ef2e8e05e1a6 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:50:11 +0200 Subject: [PATCH 15/25] add current_desktop, Eq and Hash --- src/lib.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 360f1fe..cf5fa52 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ pub mod matching; pub use self::iter::Iter; use std::borrow::Cow; use std::collections::BTreeMap; +use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; use xdg::BaseDirectories; @@ -23,7 +24,7 @@ pub type Locale<'a> = Cow<'a, str>; pub type LocaleMap<'a> = BTreeMap, Value<'a>>; pub type Value<'a> = Cow<'a, str>; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq)] pub struct DesktopEntry<'a> { pub appid: Cow<'a, str>, pub groups: Groups<'a>, @@ -31,6 +32,19 @@ pub struct DesktopEntry<'a> { pub ubuntu_gettext_domain: Option>, } +impl Hash for DesktopEntry<'_> { + fn hash(&self, state: &mut H) { + self.appid.hash(state); + } +} + +impl PartialEq for DesktopEntry<'_> { + fn eq(&self, other: &Self) -> bool { + self.appid == other.appid + } +} + + impl DesktopEntry<'_> { /// Construct a new [`DesktopEntry`] from an appid. The name field will be /// set to that appid. @@ -436,6 +450,17 @@ pub fn get_languages_from_env() -> Vec { l } +pub fn current_desktop() -> Option> { + std::env::var("XDG_CURRENT_DESKTOP").ok().map(|x| { + let x = x.to_ascii_lowercase(); + if x == "unity" { + vec!["gnome".to_string()] + } else { + x.split(':').map(|e| e.to_string()).collect() + } + }) +} + #[test] fn add_field() { let appid = "appid"; From 5c24bcb82e19edda7fcba3b93e70449d763f5018 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Fri, 7 Jun 2024 16:17:08 +0200 Subject: [PATCH 16/25] add logs, include name and exec, lower min score --- src/matching.rs | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/src/matching.rs b/src/matching.rs index 65a37c6..74ae661 100644 --- a/src/matching.rs +++ b/src/matching.rs @@ -3,6 +3,8 @@ use std::cmp::max; +use log::warn; + use crate::DesktopEntry; #[inline] @@ -95,14 +97,25 @@ fn compare_str<'a>(pattern: &'a str, de_value: &'a str) -> f64 { /// From 0 to 1. /// 1 is a perfect match. fn match_entry_from_id(pattern: &str, de: &DesktopEntry) -> f64 { - let de_id = de.appid.to_lowercase(); - let de_wm_class = de.startup_wm_class().unwrap_or_default().to_lowercase(); - *[de_id, de_wm_class] - .map(|de| compare_str(pattern, &de)) + let mut de_inputs = Vec::with_capacity(4); + de_inputs.push(de.appid.to_lowercase()); + if let Some(i) = de.startup_wm_class() { + de_inputs.push(i.to_lowercase()); + } + if let Some(i) = de.desktop_entry("Name") { + de_inputs.push(i.to_lowercase()); + } + + if let Some(i) = de.exec() { + de_inputs.push(i.to_lowercase()); + } + + de_inputs .iter() + .map(|de| compare_str(pattern, &de)) .max_by(|e1, e2| e1.total_cmp(e2)) - .unwrap_or(&0.0) + .unwrap_or(0.0) } #[derive(Debug, Clone)] @@ -121,7 +134,7 @@ pub struct MatchAppIdOptions { impl Default for MatchAppIdOptions { fn default() -> Self { Self { - min_score: 0.7, + min_score: 0.6, entropy: Some((0.15, 0.2)), } } @@ -143,6 +156,9 @@ where let normalized_patterns = patterns .iter() + .inspect(|e| { + warn!("searching with {}", e.as_ref()); + }) .map(|e| e.as_ref().to_lowercase()) .collect::>(); @@ -155,12 +171,25 @@ where match max_score { Some((prev_max_score, _)) => { + warn!( + "found {} for {}. Score: {}", + de.appid, + patterns[0].as_ref(), + score + ); + if prev_max_score < score { second_max_score = prev_max_score; max_score = Some((score, de)); } } None => { + warn!( + "found: {} for {}. Score: {}", + de.appid, + patterns[0].as_ref(), + score + ); max_score = Some((score, de)); } } From fc93a8436b48ee5e7ac1fc3945d7710353422434 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Fri, 7 Jun 2024 16:36:32 +0200 Subject: [PATCH 17/25] fix log --- src/matching.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/matching.rs b/src/matching.rs index 74ae661..fdded9d 100644 --- a/src/matching.rs +++ b/src/matching.rs @@ -171,14 +171,13 @@ where match max_score { Some((prev_max_score, _)) => { - warn!( - "found {} for {}. Score: {}", - de.appid, - patterns[0].as_ref(), - score - ); - if prev_max_score < score { + warn!( + "found {} for {}. Score: {}", + de.appid, + patterns[0].as_ref(), + score + ); second_max_score = prev_max_score; max_score = Some((score, de)); } From 8de5926ee0c20ec8753e51d17493c6979c683cca Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sat, 8 Jun 2024 12:22:03 +0200 Subject: [PATCH 18/25] remove exec + lower min score + remove exec --- examples/de_launch.rs | 13 -- src/exec/dbus.rs | 121 ------------- src/exec/error.rs | 49 ------ src/exec/graphics.rs | 210 ----------------------- src/exec/mod.rs | 389 ------------------------------------------ src/lib.rs | 2 - src/matching.rs | 10 +- 7 files changed, 3 insertions(+), 791 deletions(-) delete mode 100644 examples/de_launch.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 diff --git a/examples/de_launch.rs b/examples/de_launch.rs deleted file mode 100644 index 6205c8f..0000000 --- a/examples/de_launch.rs +++ /dev/null @@ -1,13 +0,0 @@ -use freedesktop_desktop_entry::{get_languages_from_env, DesktopEntry}; -use std::env; -use std::path::PathBuf; - -fn main() { - let args: Vec = env::args().collect(); - let path = &args.get(1).expect("Not enough arguments"); - let path = PathBuf::from(path); - let locales = get_languages_from_env(); - let de = DesktopEntry::from_path(path, &locales).expect("Error decoding desktop entry"); - de.launch_with_uris(&[], false, &locales) - .expect("Failed to run desktop entry"); -} 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 d83b021..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, &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, &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, &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, &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, &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, &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, &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, &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 cf5fa52..b68591b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,8 +2,6 @@ // SPDX-License-Identifier: MPL-2.0 mod decoder; -mod exec; -pub use exec::error::ExecError; mod iter; pub mod matching; diff --git a/src/matching.rs b/src/matching.rs index fdded9d..a647a22 100644 --- a/src/matching.rs +++ b/src/matching.rs @@ -97,9 +97,9 @@ fn compare_str<'a>(pattern: &'a str, de_value: &'a str) -> f64 { /// From 0 to 1. /// 1 is a perfect match. fn match_entry_from_id(pattern: &str, de: &DesktopEntry) -> f64 { - let mut de_inputs = Vec::with_capacity(4); de_inputs.push(de.appid.to_lowercase()); + if let Some(i) = de.startup_wm_class() { de_inputs.push(i.to_lowercase()); } @@ -107,10 +107,6 @@ fn match_entry_from_id(pattern: &str, de: &DesktopEntry) -> f64 { de_inputs.push(i.to_lowercase()); } - if let Some(i) = de.exec() { - de_inputs.push(i.to_lowercase()); - } - de_inputs .iter() .map(|de| compare_str(pattern, &de)) @@ -134,8 +130,8 @@ pub struct MatchAppIdOptions { impl Default for MatchAppIdOptions { fn default() -> Self { Self { - min_score: 0.6, - entropy: Some((0.15, 0.2)), + min_score: 0.15, + entropy: Some((0.15, 0.1)), } } } From 1ee14ea450408c379bb02e29501d656db396395c Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sat, 8 Jun 2024 12:25:33 +0200 Subject: [PATCH 19/25] remove some files specific to exec --- Cargo.toml | 7 +------ 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 ----- 6 files changed, 1 insertion(+), 45 deletions(-) delete mode 100644 tests/entries/alacritty-simple.desktop delete mode 100644 tests/entries/empty-exec.desktop delete mode 100644 tests/entries/non-terminal-cmd.desktop delete mode 100644 tests/entries/terminal-cmd.desktop delete mode 100644 tests/entries/unmatched-quotes.desktop diff --git a/Cargo.toml b/Cargo.toml index 8901e74..24dfe91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,9 +18,4 @@ textdistance = "1.0.2" strsim = "0.11.1" thiserror = "1" xdg = "2.4.0" -udev = "0.8.0" -zbus = "4.2.2" -log = "0.4.21" - -[dev-dependencies] -speculoos = "0.11.0" +log = "0.4.21" \ No newline at end of file diff --git a/tests/entries/alacritty-simple.desktop b/tests/entries/alacritty-simple.desktop deleted file mode 100644 index 704a72e..0000000 --- a/tests/entries/alacritty-simple.desktop +++ /dev/null @@ -1,19 +0,0 @@ -[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 deleted file mode 100644 index fb28c29..0000000 --- a/tests/entries/empty-exec.desktop +++ /dev/null @@ -1,5 +0,0 @@ -[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 deleted file mode 100644 index 3b84b75..0000000 --- a/tests/entries/non-terminal-cmd.desktop +++ /dev/null @@ -1,5 +0,0 @@ -[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 deleted file mode 100644 index 70cf76a..0000000 --- a/tests/entries/terminal-cmd.desktop +++ /dev/null @@ -1,5 +0,0 @@ -[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 deleted file mode 100644 index 6f8f6ef..0000000 --- a/tests/entries/unmatched-quotes.desktop +++ /dev/null @@ -1,5 +0,0 @@ -[Desktop Entry] -Exec="alacritty -e -Terminal=false -Type=Application -Name=InvalidCommand \ No newline at end of file From d70abc15dcfcc447e4967be85f5c4882b0f4bd3a Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sat, 8 Jun 2024 16:49:13 +0200 Subject: [PATCH 20/25] remove Name from match + add last part of id --- src/matching.rs | 50 ++++++++++- tests/org.kde.krita.desktop | 168 ++++++++++++++++++++++++++++++++++++ 2 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 tests/org.kde.krita.desktop diff --git a/src/matching.rs b/src/matching.rs index a647a22..1745bd6 100644 --- a/src/matching.rs +++ b/src/matching.rs @@ -98,14 +98,23 @@ fn compare_str<'a>(pattern: &'a str, de_value: &'a str) -> f64 { /// 1 is a perfect match. fn match_entry_from_id(pattern: &str, de: &DesktopEntry) -> f64 { let mut de_inputs = Vec::with_capacity(4); - de_inputs.push(de.appid.to_lowercase()); - if let Some(i) = de.startup_wm_class() { - de_inputs.push(i.to_lowercase()); + // todo: use https://crates.io/crates/unicase ? + + let id = de.appid.to_lowercase(); + + if let Some(last_part_of_id) = id.split('.').last() { + de_inputs.push(last_part_of_id.to_owned()); } - if let Some(i) = de.desktop_entry("Name") { + + de_inputs.push(id); + + if let Some(i) = de.startup_wm_class() { de_inputs.push(i.to_lowercase()); } + // if let Some(i) = de.desktop_entry("Name") { + // de_inputs.push(i.to_lowercase()); + // } de_inputs .iter() @@ -136,6 +145,8 @@ impl Default for MatchAppIdOptions { } } + + /// Return the best match over all provided [`DesktopEntry`]. /// Use this to match over the values provided by the compositor, not the user. /// First entries get the priority. @@ -212,3 +223,34 @@ where None } } + + +mod test { + use crate::{default_paths, get_languages_from_env, DesktopEntry, Iter}; + + use super::{get_best_match, MatchAppIdOptions}; + + #[test] + fn find_de() { + + + let entries = DesktopEntry::from_paths(Iter::new(default_paths()), &get_languages_from_env()).filter_map(|e| e.ok()).collect::>(); + + let e = get_best_match(&["firefox"], &entries, MatchAppIdOptions::default()); + + dbg!(e); + panic!() + } + +} + +#[test] +fn a() { + + let id = "org.gnome.gedit"; + + let a = id.split('.').last().unwrap(); + + dbg!(a); + panic!(); +} \ No newline at end of file diff --git a/tests/org.kde.krita.desktop b/tests/org.kde.krita.desktop new file mode 100644 index 0000000..e7aa8cc --- /dev/null +++ b/tests/org.kde.krita.desktop @@ -0,0 +1,168 @@ +[Desktop Entry] +Name=Krita +Name[af]=Krita +Name[ar]=كريتا +Name[bg]=Krita +Name[br]=Krita +Name[bs]=Krita +Name[ca]=Krita +Name[ca@valencia]=Krita +Name[cs]=Krita +Name[cy]=Krita +Name[da]=Krita +Name[de]=Krita +Name[el]=Krita +Name[en_GB]=Krita +Name[eo]=Krita +Name[es]=Krita +Name[et]=Krita +Name[eu]=Krita +Name[fi]=Krita +Name[fr]=Krita +Name[fy]=Krita +Name[ga]=Krita +Name[gl]=Krita +Name[he]=Krita +Name[hi]=क्रिता +Name[hne]=केरिता +Name[hr]=Krita +Name[hu]=Krita +Name[ia]=Krita +Name[id]=Krita +Name[is]=Krita +Name[it]=Krita +Name[ja]=Krita +Name[ka]=Krita +Name[kk]=Krita +Name[ko]=Krita +Name[lt]=Krita +Name[lv]=Krita +Name[mr]=क्रिटा +Name[ms]=Krita +Name[nds]=Krita +Name[ne]=क्रिता +Name[nl]=Krita +Name[nn]=Krita +Name[pl]=Krita +Name[pt]=Krita +Name[pt_BR]=Krita +Name[ro]=Krita +Name[ru]=Krita +Name[se]=Krita +Name[sk]=Krita +Name[sl]=Krita +Name[sv]=Krita +Name[ta]=கிரிட்டா +Name[tg]=Krita +Name[tr]=Krita +Name[ug]=Krita +Name[uk]=Krita +Name[uz]=Krita +Name[uz@cyrillic]=Krita +Name[wa]=Krita +Name[xh]=Krita +Name[x-test]=xxKritaxx +Name[zh_CN]=Krita +Name[zh_TW]=Krita +Exec=/usr/bin/flatpak run --branch=stable --arch=x86_64 --command=krita --file-forwarding org.kde.krita @@ %F @@ +GenericName=Digital Painting +GenericName[ar]=رسم رقمي +GenericName[bs]=Digitalno Bojenje +GenericName[ca]=Dibuix digital +GenericName[ca@valencia]=Dibuix digital +GenericName[cs]=Digitální malování +GenericName[da]=Digital tegning +GenericName[de]=Digitales Malen +GenericName[el]=Ψηφιακή ζωγραφική +GenericName[en_GB]=Digital Painting +GenericName[eo]=Cifereca Pentrado +GenericName[es]=Pintura digital +GenericName[et]=Digitaalne joonistamine +GenericName[eu]=Margolan digitala +GenericName[fi]=Digitaalimaalaus +GenericName[fr]=Peinture numérique +GenericName[gl]=Pintura dixital +GenericName[hi]=डिजिटल चित्रकारी +GenericName[hu]=Digitális festészet +GenericName[ia]=Pintura Digital +GenericName[id]=Pelukisan Digital +GenericName[is]=Stafræn málun +GenericName[it]=Pittura digitale +GenericName[ja]=デジタルペインティング +GenericName[ka]=ციფრული მხატვრობა +GenericName[kk]=Цифрлық сурет салу +GenericName[ko]=디지털 페인팅 +GenericName[lt]=Skaitmeninis piešimas +GenericName[mr]=डिजिटल पेंटिंग +GenericName[nl]=Digitaal schilderen +GenericName[nn]=Digital teikning +GenericName[pl]=Malowanie cyfrowe +GenericName[pt]=Pintura Digital +GenericName[pt_BR]=Pintura digital +GenericName[ro]=Pictură digitală +GenericName[ru]=Цифровая живопись +GenericName[sk]=Digitálne maľovanie +GenericName[sl]=Digitalno slikanje +GenericName[sv]=Digital målning +GenericName[tr]=Sayısal Boyama +GenericName[ug]=سىفىرلىق رەسىم سىزغۇ +GenericName[uk]=Цифрове малювання +GenericName[x-test]=xxDigital Paintingxx +GenericName[zh_CN]=数字绘画程序 +GenericName[zh_TW]=數位繪畫 +MimeType=application/x-krita;image/openraster;application/x-krita-paintoppreset; +Comment=Digital Painting +Comment[ar]=رسم رقمي +Comment[bs]=Digitalno Bojenje +Comment[ca]=Dibuix digital +Comment[ca@valencia]=Dibuix digital +Comment[cs]=Digitální malování +Comment[da]=Digital tegning +Comment[de]=Digitales Malen +Comment[el]=Ψηφιακή ζωγραφική +Comment[en_GB]=Digital Painting +Comment[eo]=Cifereca Pentrado +Comment[es]=Pintura digital +Comment[et]=Digitaalne joonistamine +Comment[eu]=Margolan digitala +Comment[fi]=Digitaalimaalaus +Comment[fr]=Peinture numérique +Comment[gl]=Pintura dixital. +Comment[hi]=डिजिटल चित्रकारी +Comment[hu]=Digitális festészet +Comment[ia]=Pintura Digital +Comment[id]=Pelukisan Digital +Comment[is]=Stafræn málun +Comment[it]=Pittura digitale +Comment[ja]=デジタルペインティング +Comment[ka]=ციფრული მხატვრობა +Comment[kk]=Цифрлық сурет салу +Comment[ko]=디지털 페인팅 +Comment[lt]=Skaitmeninis piešimas +Comment[mr]=डिजिटल पेंटिंग +Comment[nl]=Digitaal schilderen +Comment[nn]=Digital teikning +Comment[pl]=Malowanie cyfrowe +Comment[pt]=Pintura Digital +Comment[pt_BR]=Pintura digital +Comment[ro]=Pictură digitală +Comment[ru]=Цифровая живопись +Comment[sk]=Digitálne maľovanie +Comment[sl]=Digitalno slikanje +Comment[sv]=Digitalt målningsverktyg +Comment[tr]=Sayısal Boyama +Comment[ug]=سىفىرلىق رەسىم سىزغۇ +Comment[uk]=Цифрове малювання +Comment[x-test]=xxDigital Paintingxx +Comment[zh_CN]=自由开源的专业数字绘画程序 +Comment[zh_TW]=數位繪畫 +Type=Application +Icon=org.kde.krita +Categories=Qt;KDE;Graphics;2DGraphics;RasterGraphics; +X-KDE-NativeMimeType=application/x-krita +X-KDE-ExtraNativeMimeTypes= +StartupNotify=true +X-Krita-Version=28 +StartupWMClass=krita +InitialPreference=99 +X-Flatpak=org.kde.krita \ No newline at end of file From 251de84bff2d760325e8f5a5dc64e6f627ccb96f Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sat, 8 Jun 2024 17:28:46 +0200 Subject: [PATCH 21/25] replace all - with . --- src/matching.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/matching.rs b/src/matching.rs index 1745bd6..bc7bafe 100644 --- a/src/matching.rs +++ b/src/matching.rs @@ -104,13 +104,13 @@ fn match_entry_from_id(pattern: &str, de: &DesktopEntry) -> f64 { let id = de.appid.to_lowercase(); if let Some(last_part_of_id) = id.split('.').last() { - de_inputs.push(last_part_of_id.to_owned()); + de_inputs.push(last_part_of_id.replace('-', ".")); } - de_inputs.push(id); + de_inputs.push(id.replace('-', ".")); if let Some(i) = de.startup_wm_class() { - de_inputs.push(i.to_lowercase()); + de_inputs.push(i.to_lowercase().replace('-', ".")); } // if let Some(i) = de.desktop_entry("Name") { // de_inputs.push(i.to_lowercase()); @@ -163,10 +163,10 @@ where let normalized_patterns = patterns .iter() + .map(|e| e.as_ref().to_lowercase().replace('-', ".")) .inspect(|e| { - warn!("searching with {}", e.as_ref()); + warn!("searching with {}", e); }) - .map(|e| e.as_ref().to_lowercase()) .collect::>(); for de in entries { From 430409d35419e6ba988823f5cb2c4eae67f90499 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sat, 8 Jun 2024 17:54:56 +0200 Subject: [PATCH 22/25] remove replace, add exec field, add malus --- src/matching.rs | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/matching.rs b/src/matching.rs index bc7bafe..ab5fdd8 100644 --- a/src/matching.rs +++ b/src/matching.rs @@ -97,6 +97,7 @@ fn compare_str<'a>(pattern: &'a str, de_value: &'a str) -> f64 { /// From 0 to 1. /// 1 is a perfect match. fn match_entry_from_id(pattern: &str, de: &DesktopEntry) -> f64 { + // (pattern, malus) let mut de_inputs = Vec::with_capacity(4); // todo: use https://crates.io/crates/unicase ? @@ -104,25 +105,34 @@ fn match_entry_from_id(pattern: &str, de: &DesktopEntry) -> f64 { let id = de.appid.to_lowercase(); if let Some(last_part_of_id) = id.split('.').last() { - de_inputs.push(last_part_of_id.replace('-', ".")); + de_inputs.push((last_part_of_id.to_owned(), 0.)); } - de_inputs.push(id.replace('-', ".")); + de_inputs.push((id, 0.)); if let Some(i) = de.startup_wm_class() { - de_inputs.push(i.to_lowercase().replace('-', ".")); + de_inputs.push((i.to_lowercase(), 0.)); } // if let Some(i) = de.desktop_entry("Name") { // de_inputs.push(i.to_lowercase()); // } + if let Some(i) = de.exec() { + de_inputs.push((i.to_lowercase(), 0.5)); + } + de_inputs .iter() - .map(|de| compare_str(pattern, &de)) + .map(|de| (compare_str(pattern, &de.0) - de.1).max(0.)) .max_by(|e1, e2| e1.total_cmp(e2)) .unwrap_or(0.0) } + +// first match appid +// match startup_wm_class +// match + #[derive(Debug, Clone)] pub struct MatchAppIdOptions { /// Minimal score required to validate a match. @@ -163,7 +173,7 @@ where let normalized_patterns = patterns .iter() - .map(|e| e.as_ref().to_lowercase().replace('-', ".")) + .map(|e| e.as_ref().to_lowercase()) .inspect(|e| { warn!("searching with {}", e); }) @@ -236,10 +246,10 @@ mod test { let entries = DesktopEntry::from_paths(Iter::new(default_paths()), &get_languages_from_env()).filter_map(|e| e.ok()).collect::>(); - let e = get_best_match(&["firefox"], &entries, MatchAppIdOptions::default()); + let e = get_best_match(&["gnome-disks"], &entries, MatchAppIdOptions::default()); - dbg!(e); - panic!() + println!("found {}", e.unwrap().appid); + // panic!() } } @@ -247,10 +257,7 @@ mod test { #[test] fn a() { - let id = "org.gnome.gedit"; - - let a = id.split('.').last().unwrap(); + let res = compare_str("org.gnome.tweaks", "gnome.disks"); - dbg!(a); - panic!(); + println!("{res}") } \ No newline at end of file From 33fdccbafe8205112100adadfaa607da5df0c53a Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sat, 8 Jun 2024 17:58:11 +0200 Subject: [PATCH 23/25] Update matching.rs --- src/matching.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matching.rs b/src/matching.rs index ab5fdd8..e5a3da4 100644 --- a/src/matching.rs +++ b/src/matching.rs @@ -118,7 +118,7 @@ fn match_entry_from_id(pattern: &str, de: &DesktopEntry) -> f64 { // } if let Some(i) = de.exec() { - de_inputs.push((i.to_lowercase(), 0.5)); + de_inputs.push((i.to_lowercase(), 0.06)); } de_inputs From 6bc0995b01be95e935a988bc9d60f07a880ce5d6 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sat, 8 Jun 2024 20:58:49 +0200 Subject: [PATCH 24/25] clean code for release --- src/lib.rs | 7 +++---- src/matching.rs | 51 +++++++++++++++++-------------------------------- 2 files changed, 21 insertions(+), 37 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b68591b..17d4ffd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,6 @@ impl PartialEq for DesktopEntry<'_> { } } - impl DesktopEntry<'_> { /// Construct a new [`DesktopEntry`] from an appid. The name field will be /// set to that appid. @@ -314,12 +313,12 @@ impl<'a> DesktopEntry<'a> { for locale in locales { match locale_map.get(locale.as_ref()) { Some(value) => { - return Some(value.split(';').map(|e| Cow::Borrowed(e)).collect()); + return Some(value.split(';').map(Cow::Borrowed).collect()); } None => { if let Some(pos) = memchr::memchr(b'_', locale.as_ref().as_bytes()) { if let Some(value) = locale_map.get(&locale.as_ref()[..pos]) { - return Some(value.split(';').map(|e| Cow::Borrowed(e)).collect()); + return Some(value.split(';').map(Cow::Borrowed).collect()); } } } @@ -334,7 +333,7 @@ impl<'a> DesktopEntry<'a> { ); } - Some(default_value.split(';').map(|e| Cow::Borrowed(e)).collect()) + Some(default_value.split(';').map(Cow::Borrowed).collect()) } } diff --git a/src/matching.rs b/src/matching.rs index e5a3da4..dc7adf1 100644 --- a/src/matching.rs +++ b/src/matching.rs @@ -3,7 +3,7 @@ use std::cmp::max; -use log::warn; +use log::debug; use crate::DesktopEntry; @@ -100,22 +100,17 @@ fn match_entry_from_id(pattern: &str, de: &DesktopEntry) -> f64 { // (pattern, malus) let mut de_inputs = Vec::with_capacity(4); - // todo: use https://crates.io/crates/unicase ? - let id = de.appid.to_lowercase(); if let Some(last_part_of_id) = id.split('.').last() { de_inputs.push((last_part_of_id.to_owned(), 0.)); } - + de_inputs.push((id, 0.)); if let Some(i) = de.startup_wm_class() { de_inputs.push((i.to_lowercase(), 0.)); } - // if let Some(i) = de.desktop_entry("Name") { - // de_inputs.push(i.to_lowercase()); - // } if let Some(i) = de.exec() { de_inputs.push((i.to_lowercase(), 0.06)); @@ -128,11 +123,6 @@ fn match_entry_from_id(pattern: &str, de: &DesktopEntry) -> f64 { .unwrap_or(0.0) } - -// first match appid -// match startup_wm_class -// match - #[derive(Debug, Clone)] pub struct MatchAppIdOptions { /// Minimal score required to validate a match. @@ -155,8 +145,6 @@ impl Default for MatchAppIdOptions { } } - - /// Return the best match over all provided [`DesktopEntry`]. /// Use this to match over the values provided by the compositor, not the user. /// First entries get the priority. @@ -175,7 +163,7 @@ where .iter() .map(|e| e.as_ref().to_lowercase()) .inspect(|e| { - warn!("searching with {}", e); + debug!("searching with {}", e); }) .collect::>(); @@ -189,7 +177,7 @@ where match max_score { Some((prev_max_score, _)) => { if prev_max_score < score { - warn!( + debug!( "found {} for {}. Score: {}", de.appid, patterns[0].as_ref(), @@ -200,7 +188,7 @@ where } } None => { - warn!( + debug!( "found: {} for {}. Score: {}", de.appid, patterns[0].as_ref(), @@ -234,30 +222,27 @@ where } } - +#[cfg(test)] mod test { - use crate::{default_paths, get_languages_from_env, DesktopEntry, Iter}; + use crate::{default_paths, get_languages_from_env, matching::compare_str, DesktopEntry, Iter}; use super::{get_best_match, MatchAppIdOptions}; #[test] fn find_de() { - - - let entries = DesktopEntry::from_paths(Iter::new(default_paths()), &get_languages_from_env()).filter_map(|e| e.ok()).collect::>(); + let entries = + DesktopEntry::from_paths(Iter::new(default_paths()), &get_languages_from_env()) + .filter_map(|e| e.ok()) + .collect::>(); let e = get_best_match(&["gnome-disks"], &entries, MatchAppIdOptions::default()); println!("found {}", e.unwrap().appid); - // panic!() - } - -} - -#[test] -fn a() { - - let res = compare_str("org.gnome.tweaks", "gnome.disks"); + } + #[test] + fn a() { + let res = compare_str("org.gnome.tweaks", "gnome.disks"); - println!("{res}") -} \ No newline at end of file + println!("{res}") + } +} From bbc39fbc5f6a0fc7517e3fc4644899decbec7a8d Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sat, 8 Jun 2024 21:10:46 +0200 Subject: [PATCH 25/25] malus for last part of id --- src/matching.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/matching.rs b/src/matching.rs index dc7adf1..95e50ac 100644 --- a/src/matching.rs +++ b/src/matching.rs @@ -7,15 +7,6 @@ use log::debug; use crate::DesktopEntry; -#[inline] -fn add_value(v: &mut Vec, value: &str, is_multiple: bool) { - if is_multiple { - value.split(';').for_each(|e| v.push(e.to_lowercase())); - } else { - v.push(value.to_lowercase()); - } -} - /// The returned value is between 0.0 and 1.0 (higher value means more similar). /// You can use the `additional_values` parameter to add runtime string. pub fn get_entry_score<'a, Q, L>( @@ -28,6 +19,15 @@ where Q: AsRef, L: AsRef, { + #[inline] + fn add_value(v: &mut Vec, value: &str, is_multiple: bool) { + if is_multiple { + value.split(';').for_each(|e| v.push(e.to_lowercase())); + } else { + v.push(value.to_lowercase()); + } + } + // (field name, is separated by ";") let fields = [ ("Name", false), @@ -103,7 +103,7 @@ fn match_entry_from_id(pattern: &str, de: &DesktopEntry) -> f64 { let id = de.appid.to_lowercase(); if let Some(last_part_of_id) = id.split('.').last() { - de_inputs.push((last_part_of_id.to_owned(), 0.)); + de_inputs.push((last_part_of_id.to_owned(), 0.06)); } de_inputs.push((id, 0.));