diff --git a/Cargo.toml b/Cargo.toml index 8502bec..967355c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,11 @@ categories = [ "os::unix-apis" ] keywords = [ "freedesktop", "desktop", "entry" ] [dependencies] +dirs = "5.0.1" gettext-rs = { version = "0.7", features = ["gettext-system"]} memchr = "2" +textdistance = "1.0.2" +strsim = "0.11.1" thiserror = "1" xdg = "2.4.0" +log = "0.4.21" diff --git a/src/decoder.rs b/src/decoder.rs index 3c0c0ae..c255bfd 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -71,7 +71,10 @@ impl<'a> DesktopEntry<'a> { } /// Return an owned [`DesktopEntry`] - pub fn from_path(path: PathBuf, locales_filter: Option<&[L]>) -> Result, DecodeError> + pub fn from_path( + path: PathBuf, + locales_filter: Option<&[L]>, + ) -> Result, DecodeError> where L: AsRef, { @@ -125,7 +128,11 @@ fn process_line<'buf, 'local_ref, 'res: 'local_ref + 'buf, F, L>( let locale = &key[start + 1..key.len() - 1]; match locales_filter { - Some(locales_filter) if !locales_filter.iter().any(|l| l.as_ref() == locale) => return, + Some(locales_filter) + if !locales_filter.iter().any(|l| l.as_ref() == locale) => + { + return + } _ => (), } diff --git a/src/lib.rs b/src/lib.rs index eeea8b2..a1fcf12 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ mod decoder; mod iter; +pub mod matching; pub use decoder::DecodeError; pub use self::iter::Iter; diff --git a/src/matching.rs b/src/matching.rs new file mode 100644 index 0000000..724178d --- /dev/null +++ b/src/matching.rs @@ -0,0 +1,124 @@ +// Copyright 2021 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::DesktopEntry; + +impl<'a> DesktopEntry<'a> { + /// The returned value is between 0.0 and 1.0 (higher value means more similar). + /// You can use the `additional_haystack_values` parameter to add relevant string that are not part of the desktop entry. + pub fn match_query( + &'a self, + query: Q, + locales: &'a [L], + additional_haystack_values: &'a [&'a str], + ) -> f64 + 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), + ("GenericName", false), + ("Comment", false), + ("Categories", true), + ("Keywords", true), + ]; + + let mut normalized_values: Vec = Vec::new(); + + normalized_values.extend( + additional_haystack_values + .iter() + .map(|val| val.to_lowercase()), + ); + + let desktop_entry_group = self.groups.get("Desktop Entry"); + + 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 { + match locale_map.get(locale.as_ref()) { + Some(value) => { + 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]) { + add_value(&mut normalized_values, value, field.1); + at_least_one_locale = true; + } + } + } + } + } + + if !at_least_one_locale { + if let Some(domain) = &self.ubuntu_gettext_domain { + let gettext_value = crate::dgettext(domain, default_value); + if !gettext_value.is_empty() { + add_value(&mut normalized_values, &gettext_value, false); + } + } + } + } + } + } + + let query = query.as_ref().to_lowercase(); + + let query_espaced = query.split_ascii_whitespace().collect::>(); + + normalized_values + .into_iter() + .map(|de_field| { + let jaro_score = strsim::jaro_winkler(&query, &de_field); + + if query_espaced.iter().any(|query| de_field.contains(*query)) { + // provide a bonus if the query is contained in the de field + (jaro_score + 0.1).clamp(0.61, 1.) + } else { + jaro_score + } + }) + .max_by(|e1, e2| e1.total_cmp(e2)) + .unwrap_or(0.0) + } +} + +/// Return the corresponding [`DesktopEntry`] that match the given appid. +pub fn find_entry_from_appid<'a, I>(entries: I, appid: &str) -> Option<&'a DesktopEntry<'a>> +where + I: Iterator>, +{ + let normalized_appid = appid.to_lowercase(); + + entries.into_iter().find(|e| { + if e.appid.to_lowercase() == normalized_appid { + return true; + } + + if let Some(field) = e.startup_wm_class() { + if field.to_lowercase() == normalized_appid { + return true; + } + } + + false + }) +}