diff --git a/Cargo.toml b/Cargo.toml index cbe64aca..3e8929df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ pipewire = ["ashpd?/pipewire"] process = ["dep:nix"] # Use rfd for file dialogs rfd = ["dep:rfd"] +# Enables desktop files helpers +desktop = ["process", "dep:freedesktop-desktop-entry", "dep:shlex"] # Enables keycode serialization serde-keycode = ["iced_core/serde"] # Prevents multiple separate process instances. @@ -79,6 +81,8 @@ zbus = {version = "3.14.1", default-features = false, optional = true} [target.'cfg(unix)'.dependencies] freedesktop-icons = "0.2.5" +freedesktop-desktop-entry = { version = "0.5.0", optional = true } +shlex = { version = "1.3.0", optional = true } [dependencies.cosmic-theme] path = "cosmic-theme" @@ -107,7 +111,6 @@ path = "./iced/futures" [dependencies.iced_accessibility] path = "./iced/accessibility" - optional = true [dependencies.iced_tiny_skia] diff --git a/src/desktop.rs b/src/desktop.rs new file mode 100644 index 00000000..9ec539f1 --- /dev/null +++ b/src/desktop.rs @@ -0,0 +1,213 @@ +pub use freedesktop_desktop_entry::DesktopEntry; +use std::{ + borrow::Cow, + 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: String, + pub desktop_actions: 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 load_applications_for_app_ids<'a, 'b>( + locale: impl Into>, + app_ids: impl Iterator, + fill_missing_ones: bool, +) -> Vec { + let mut app_ids = app_ids.collect::>(); + let mut applications = load_applications_filtered(locale, |de| { + if let Some(i) = app_ids + .iter() + .position(|id| id == &de.appid || id.eq(&de.startup_wm_class().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().to_string(), + 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(), + 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) +} diff --git a/src/lib.rs b/src/lib.rs index 258b4e2f..2f22aac1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,6 +55,8 @@ pub use iced_winit; pub mod icon_theme; pub mod keyboard_nav; +#[cfg(feature = "desktop")] +pub mod desktop; #[cfg(feature = "process")] pub mod process;