diff --git a/plugins/applications/src/lib.rs b/plugins/applications/src/lib.rs index 3dca60f..cb7275e 100644 --- a/plugins/applications/src/lib.rs +++ b/plugins/applications/src/lib.rs @@ -71,14 +71,12 @@ pub fn handler(selection: Match, state: &State) -> HandleResult { Command::new("sh") .arg("-c") .arg(&entry.exec) - .current_dir(if let Some(path) = &entry.path { - if path.exists() { path } else { current_dir } - } else { - current_dir + .current_dir(match &entry.path { + Some(path) if path.exists() => path, + _ => current_dir, }) .spawn() - } - { + } { eprintln!("Error running desktop entry: {}", why); } @@ -113,24 +111,36 @@ pub fn get_matches(input: RString, state: &State) -> RVec { .entries .iter() .filter_map(|(entry, id)| { - let app_score = match &entry.desc { - None => matcher.fuzzy_match(&entry.name, &input).unwrap_or(0), - Some(val) => matcher - .fuzzy_match(&format!("{} {}", &val, &entry.name).to_string(), &input) - .unwrap_or(0), - }; - - let keyword_score = entry - .keywords - .iter() + // Can be replaced by `Iterator::intersperse` once the API becomes stable. + macro_rules! prefix_sep { + ($i:expr) => { + $i.as_deref() + .map(|s| [" ", s].into_iter()) + .into_iter() + .flatten() + }; + } + + let app_names = ([&*entry.name].into_iter()) + .chain(prefix_sep!(entry.display_name)) + .chain(prefix_sep!(entry.desc)) + .collect::(); + + let app_score = matcher.fuzzy_match(&app_names, &input).unwrap_or(0); + + let keyword_score = (entry.keywords.iter()) + .chain(entry.display_keywords.iter().flat_map(|k| k.iter())) .map(|keyword| matcher.fuzzy_match(keyword, &input).unwrap_or(0)) .sum::(); let mut score = (app_score * 25 + keyword_score) - entry.offset; // prioritize actions + if entry.display_name.is_some() { + score *= 2; + } if entry.desc.is_some() { - score = score * 2; + score *= 2; } if score > 0 { @@ -147,7 +157,9 @@ pub fn get_matches(input: RString, state: &State) -> RVec { entries .into_iter() .map(|(entry, id, _)| Match { - title: entry.name.clone().into(), + title: (entry.display_name.clone()) + .unwrap_or_else(|| entry.name.clone()) + .into(), description: entry.desc.clone().map(|desc| desc.into()).into(), use_pango: false, icon: ROption::RSome(entry.icon.clone().into()), diff --git a/plugins/applications/src/scrubber.rs b/plugins/applications/src/scrubber.rs index d72065c..9b9b935 100644 --- a/plugins/applications/src/scrubber.rs +++ b/plugins/applications/src/scrubber.rs @@ -7,7 +7,9 @@ pub struct DesktopEntry { pub exec: String, pub path: Option, pub name: String, + pub display_name: Option, pub keywords: Vec, + pub display_keywords: Option>, pub desc: Option, pub icon: String, pub term: bool, @@ -19,7 +21,20 @@ const FIELD_CODE_LIST: &[&str] = &[ ]; impl DesktopEntry { - fn from_dir_entry(entry: &fs::DirEntry, config: &Config) -> Vec { + fn display_keys<'a>( + key: &'a str, + lang_choices: &'a [&str], + ) -> impl Iterator + 'a { + lang_choices + .iter() + .map(move |lang| format!("{key}[{lang}]")) + } + + fn from_dir_entry( + entry: &fs::DirEntry, + config: &Config, + lang_choices: Option<&[&str]>, + ) -> Vec { if entry.path().extension() == Some(OsStr::new("desktop")) { let content = match fs::read_to_string(entry.path()) { Ok(content) => content, @@ -78,6 +93,11 @@ impl DesktopEntry { }, path: map.get("Path").map(PathBuf::from), name: map.get("Name")?.to_string(), + display_name: lang_choices.and_then(|lang_choices| { + Self::display_keys("Name", lang_choices) + .find_map(|key| map.get(&*key)) + .map(ToString::to_string) + }), keywords: map .get("Keywords") .map(|keywords| { @@ -87,6 +107,16 @@ impl DesktopEntry { .collect::>() }) .unwrap_or_default(), + display_keywords: lang_choices.and_then(|lang_choices| { + Self::display_keys("Keywords", lang_choices) + .find_map(|key| map.get(&*key)) + .map(|keywords| { + keywords + .split(';') + .map(|s| s.to_owned()) + .collect::>() + }) + }), desc: None, icon: map .get("Icon") @@ -137,6 +167,11 @@ impl DesktopEntry { Some(name) => name.to_string(), None => continue, }, + display_name: lang_choices.and_then(|lang_choices| { + Self::display_keys("Name", lang_choices) + .find_map(|key| map.get(&*key)) + .map(ToString::to_string) + }), keywords: map .get("Keywords") .map(|keywords| { @@ -146,6 +181,16 @@ impl DesktopEntry { .collect::>() }) .unwrap_or_default(), + display_keywords: lang_choices.and_then(|lang_choices| { + Self::display_keys("Keywords", lang_choices) + .find_map(|key| map.get(&*key)) + .map(|keywords| { + keywords + .split(';') + .map(|s| s.to_owned()) + .collect::>() + }) + }), desc: Some(entry.name.clone()), icon: entry.icon.clone(), term: map @@ -166,6 +211,20 @@ impl DesktopEntry { } } +fn lang_choices(lang: &str) -> Vec<&str> { + // example: en_US.UTF-8 + let whole = lang; + // example: en_US + let Some((prefix, _)) = whole.split_once('.') else { + return vec![whole]; + }; + // example: en + let Some((short, _)) = prefix.split_once('_') else { + return vec![whole, prefix]; + }; + vec![whole, prefix, short] +} + pub fn scrubber(config: &Config) -> Result, Box> { // Create iterator over all the files in the XDG_DATA_DIRS // XDG compliancy is cool @@ -181,6 +240,9 @@ pub fn scrubber(config: &Config) -> Result, Box = match env::var("XDG_DATA_DIRS") { Ok(data_dirs) => { // The vec for all the DirEntry objects @@ -212,7 +274,7 @@ pub fn scrubber(config: &Config) -> Result, Box entry, Err(_why) => return None, }; - let entries = DesktopEntry::from_dir_entry(&entry, config); + let entries = DesktopEntry::from_dir_entry(&entry, config, lang_choices.as_deref()); Some( entries .into_iter() @@ -232,7 +294,8 @@ pub fn scrubber(config: &Config) -> Result, Box entry, Err(_why) => return None, }; - let entries = DesktopEntry::from_dir_entry(&entry, config); + let entries = + DesktopEntry::from_dir_entry(&entry, config, lang_choices.as_deref()); Some( entries .into_iter()