pub use freedesktop_desktop_entry::DesktopEntry; use iced_widget::canvas::path::lyon_path::geom::euclid::approxord::min; pub use mime::Mime; use std::{ borrow::Cow, cmp::max, ffi::OsStr, path::{Path, PathBuf} }; #[derive(Debug, Clone, PartialEq, Eq)] pub enum IconSource { Name(String), Path(PathBuf), } impl IconSource { pub fn from_unknown(icon: &str) -> Self { let icon_path = Path::new(icon); if icon_path.is_absolute() && icon_path.exists() { Self::Path(icon_path.into()) } else { Self::Name(icon.into()) } } pub fn as_cosmic_icon(&self) -> crate::widget::icon::Icon { match self { Self::Name(name) => crate::widget::icon::from_name(name.as_str()) .size(128) .fallback(Some(crate::widget::icon::IconFallback::Names(vec![ "application-default".into(), "application-x-executable".into(), ]))) .into(), Self::Path(path) => crate::widget::icon(crate::widget::icon::from_path(path.clone())), } } } impl Default for IconSource { fn default() -> Self { Self::Name("application-default".to_string()) } } #[derive(Debug, Clone, PartialEq)] pub struct DesktopAction { pub name: String, pub exec: String, } #[derive(Debug, Clone, PartialEq, Default)] pub struct DesktopEntryData { pub id: String, pub name: String, pub wm_class: Option, pub exec: Option, pub icon: IconSource, pub path: Option, pub categories: Vec, pub desktop_actions: Vec, pub mime_types: Vec, pub prefers_dgpu: bool, } pub fn load_applications<'a>( locale: impl Into>, include_no_display: bool, ) -> Vec { load_applications_filtered(locale, |de| include_no_display || !de.no_display()) } pub fn app_id_or_fallback_matches(app_id: &str, entry: &DesktopEntryData) -> bool { let lowercase_wm_class = match entry.wm_class.as_ref() { Some(s) => Some(s.to_lowercase()), None => None, }; app_id == entry.id || Some(app_id.to_lowercase()) == lowercase_wm_class || app_id.to_lowercase() == entry.name.to_lowercase() } /// lower is better fn match_entry(id: &str, de: &DesktopEntry) -> f32 { /** * `levenshtein-rs` - levenshtein * * MIT licensed. * * Copyright (c) 2016 Titus Wormer */ #[must_use] pub fn levenshtein(a: &str, b: &str) -> usize { let mut result = 0; /* Shortcut optimizations / degenerate cases. */ if a == b { return result; } let length_a = a.chars().count(); let length_b = b.chars().count(); if length_a == 0 { return length_b; } if length_b == 0 { return length_a; } /* Initialize the vector. * * This is why it’s fast, normally a matrix is used, * here we use a single vector. */ let mut cache: Vec = (1..).take(length_a).collect(); let mut distance_a; let mut distance_b; /* Loop. */ for (index_b, code_b) in b.chars().enumerate() { result = index_b; distance_a = index_b; for (index_a, code_a) in a.chars().enumerate() { distance_b = if code_a == code_b { distance_a } else { distance_a + 1 }; distance_a = cache[index_a]; result = if distance_a > result { if distance_b > result { result + 1 } else { distance_b } } else if distance_b > distance_a { distance_a + 1 } else { distance_b }; cache[index_a] = result; } } result } let cmp = |id, de| { let score = levenshtein(id, de); let max_len = ((id.len() as i32 - de.len() as i32)).abs(); score as f32 / max_len as f32 }; let id = id.to_lowercase(); 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(None).unwrap_or_default().to_lowercase(); return min(cmp(&id, &de_id), min(cmp(&id, &de_wm_class), cmp(&id, &de_name))); // let score = levenshtein(&id, &de_id); // if score == 0 -> return 0 // if len == 0 -> return +inf // if len is big, we want to lighten the weight of a high score // return score as f32 / id.len() as f32; // if id == de.appid // || id // .to_lowercase() // .eq(&de.startup_wm_class().unwrap_or_default().to_lowercase()) // { // return 100; // } // if de // .name(None) // .map(|n| n.to_lowercase() == id.to_lowercase()) // .unwrap_or_default() // { // return 100; // } // return 0; } pub fn load_applications_for_app_ids2<'a, 'b>( locale: impl Into>, app_ids: impl Iterator, fill_missing_ones: bool, include_no_display: bool, ) -> Vec { // need to be owned let all_desktop_entries_string = freedesktop_desktop_entry::Iter::new(freedesktop_desktop_entry::default_paths()) .filter_map(|path| { std::fs::read_to_string(&path) .ok() .map(|content| (path, content)) }) .collect::>(); let all_desktop_entries = all_desktop_entries_string .iter() .filter_map(|(path, content)| DesktopEntry::decode(&path, &content).ok()) .collect::>(); let mut applications = Vec::new(); let mut missing = Vec::new(); let locale = locale.into(); for id in app_ids { let mut max_score = None; for de in &all_desktop_entries { if !include_no_display && de.no_display() { continue; } let score = match_entry(id, de); match max_score { Some((prev_max_score, _)) => { if prev_max_score < score { max_score = Some((score, de)); } } None => { max_score = Some((score, de)); } } if score == 0.0 { break; } } let mut add_missing = false; match max_score { Some((score, de)) => { if score > 0.3 { let d = DesktopEntryData::from_desktop_entry( locale, Some(de.path.to_path_buf()), de, ); applications.push(d); } else { add_missing = true; } } None => { add_missing = true; } } if fill_missing_ones && add_missing { missing.push(id); } } if fill_missing_ones { applications.extend(missing.into_iter().map(|app_id| DesktopEntryData { id: app_id.to_string(), name: app_id.to_string(), icon: IconSource::default(), ..Default::default() })); } applications } pub fn load_applications_for_app_ids<'a, 'b>( locale: impl Into>, app_ids: impl Iterator, fill_missing_ones: bool, include_no_display: bool, ) -> Vec { let mut app_ids = app_ids.collect::>(); dbg!(&app_ids); let mut applications = load_applications_filtered(locale, |de| { if !include_no_display && de.no_display() { return false; } // If appid matches, or startup_wm_class matches... if let Some(i) = app_ids.iter().position(|id| { id == &de.appid || id .to_lowercase() .eq(&de.startup_wm_class().unwrap_or_default().to_lowercase()) }) { app_ids.remove(i); true // Fallback: If the name matches... } else if let Some(i) = app_ids.iter().position(|id| { de.name(None) .map(|n| n.to_lowercase() == id.to_lowercase()) .unwrap_or_default() }) { app_ids.remove(i); true } else { false } }); if fill_missing_ones { applications.extend(app_ids.into_iter().map(|app_id| DesktopEntryData { id: app_id.to_string(), name: app_id.to_string(), icon: IconSource::default(), ..Default::default() })); } applications } pub fn load_applications_filtered<'a, F: FnMut(&DesktopEntry) -> bool>( locale: impl Into>, mut filter: F, ) -> Vec { let locale = locale.into(); freedesktop_desktop_entry::Iter::new(freedesktop_desktop_entry::default_paths()) .filter_map(|path| { std::fs::read_to_string(&path).ok().and_then(|input| { DesktopEntry::decode(&path, &input).ok().and_then(|de| { if !filter(&de) { return None; } Some(DesktopEntryData::from_desktop_entry( locale, path.clone(), &de, )) }) }) }) .collect() } pub fn load_desktop_file<'a>( locale: impl Into>, path: impl AsRef, ) -> Option { let path = path.as_ref(); std::fs::read_to_string(path).ok().and_then(|input| { DesktopEntry::decode(path, &input) .ok() .map(|de| DesktopEntryData::from_desktop_entry(locale, PathBuf::from(path), &de)) }) } impl DesktopEntryData { fn from_desktop_entry<'a>( locale: impl Into>, path: impl Into>, de: &DesktopEntry, ) -> DesktopEntryData { let locale = locale.into(); let name = de .name(locale) .unwrap_or(Cow::Borrowed(de.appid)) .to_string(); // check if absolute path exists and otherwise treat it as a name let icon = de.icon().unwrap_or(de.appid); let icon_path = Path::new(icon); let icon = if icon_path.is_absolute() && icon_path.exists() { IconSource::Path(icon_path.into()) } else { IconSource::Name(icon.into()) }; DesktopEntryData { id: de.appid.to_string(), wm_class: de.startup_wm_class().map(ToString::to_string), exec: de.exec().map(ToString::to_string), name, icon, path: path.into(), categories: de .categories() .unwrap_or_default() .split_terminator(';') .map(std::string::ToString::to_string) .collect(), desktop_actions: de .actions() .map(|actions| { actions .split(';') .filter_map(|action| { let name = de.action_entry_localized(action, "Name", locale); let exec = de.action_entry(action, "Exec"); if let (Some(name), Some(exec)) = (name, exec) { Some(DesktopAction { name: name.to_string(), exec: exec.to_string(), }) } else { None } }) .collect::>() }) .unwrap_or_default(), mime_types: de .mime_type() .map(|mime_types| { mime_types .split_terminator(';') .filter_map(|mime_type| mime_type.parse::().ok()) .collect::>() }) .unwrap_or_default(), prefers_dgpu: de.prefers_non_default_gpu(), } } } pub fn spawn_desktop_exec(exec: S, env_vars: I) where S: AsRef, I: IntoIterator, K: AsRef, V: AsRef, { let mut exec = shlex::Shlex::new(exec.as_ref()); let mut cmd = match exec.next() { Some(cmd) if !cmd.contains('=') => std::process::Command::new(cmd), _ => return, }; for arg in exec { // TODO handle "%" args here if necessary? if !arg.starts_with('%') { cmd.arg(arg); } } cmd.envs(env_vars); crate::process::spawn(cmd); }