Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

libcosmic: Add desktop-file helpers #281

Merged
merged 1 commit into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -107,7 +111,6 @@ path = "./iced/futures"

[dependencies.iced_accessibility]
path = "./iced/accessibility"

optional = true

[dependencies.iced_tiny_skia]
Expand Down
213 changes: 213 additions & 0 deletions src/desktop.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
pub exec: Option<String>,
pub icon: IconSource,
pub path: Option<PathBuf>,
pub categories: String,
pub desktop_actions: Vec<DesktopAction>,
pub prefers_dgpu: bool,
}

pub fn load_applications<'a>(
locale: impl Into<Option<&'a str>>,
include_no_display: bool,
) -> Vec<DesktopEntryData> {
load_applications_filtered(locale, |de| include_no_display || !de.no_display())
}

pub fn load_applications_for_app_ids<'a, 'b>(
locale: impl Into<Option<&'a str>>,
app_ids: impl Iterator<Item = &'b str>,
fill_missing_ones: bool,
) -> Vec<DesktopEntryData> {
let mut app_ids = app_ids.collect::<Vec<_>>();
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<Option<&'a str>>,
mut filter: F,
) -> Vec<DesktopEntryData> {
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<Option<&'a str>>,
path: impl AsRef<Path>,
) -> Option<DesktopEntryData> {
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<Option<&'a str>>,
path: impl Into<Option<PathBuf>>,
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::<Vec<_>>()
})
.unwrap_or_default(),
prefers_dgpu: de.prefers_non_default_gpu(),
}
}
}

pub fn spawn_desktop_exec<S, I, K, V>(exec: S, env_vars: I)
where
S: AsRef<str>,
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
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)
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading