libcosmic/src/desktop.rs
2024-05-30 17:45:29 +02:00

459 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String>,
pub exec: Option<String>,
pub icon: IconSource,
pub path: Option<PathBuf>,
pub categories: Vec<String>,
pub desktop_actions: Vec<DesktopAction>,
pub mime_types: Vec<Mime>,
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 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 <tituswormer@gmail.com>
*/
#[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<usize> = (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<Option<&'a str>>,
app_ids: impl Iterator<Item = &'b str>,
fill_missing_ones: bool,
include_no_display: bool,
) -> Vec<DesktopEntryData> {
// 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::<Vec<_>>();
let all_desktop_entries = all_desktop_entries_string
.iter()
.filter_map(|(path, content)| DesktopEntry::decode(&path, &content).ok())
.collect::<Vec<_>>();
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<Option<&'a str>>,
app_ids: impl Iterator<Item = &'b str>,
fill_missing_ones: bool,
include_no_display: bool,
) -> Vec<DesktopEntryData> {
let mut app_ids = app_ids.collect::<Vec<_>>();
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<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()
.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::<Vec<_>>()
})
.unwrap_or_default(),
mime_types: de
.mime_type()
.map(|mime_types| {
mime_types
.split_terminator(';')
.filter_map(|mime_type| mime_type.parse::<Mime>().ok())
.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);
}