libcosmic: Add desktop-file helpers
This commit is contained in:
parent
1291a48d4d
commit
bf0508816b
3 changed files with 219 additions and 1 deletions
|
|
@ -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]
|
||||
|
|
|
|||
213
src/desktop.rs
Normal file
213
src/desktop.rs
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue