fix: new matching algorithm for desktop entries

This commit is contained in:
wiiznokes 2024-05-31 05:05:36 +02:00 committed by GitHub
parent e1b78fd3a0
commit 9519b86ec9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 114 additions and 30 deletions

View file

@ -27,7 +27,13 @@ process = ["dep:nix"]
# Use rfd for file dialogs # Use rfd for file dialogs
rfd = ["dep:rfd"] rfd = ["dep:rfd"]
# Enables desktop files helpers # Enables desktop files helpers
desktop = ["process", "dep:freedesktop-desktop-entry", "dep:mime", "dep:shlex"] desktop = [
"process",
"dep:freedesktop-desktop-entry",
"dep:mime",
"dep:shlex",
"dep:textdistance",
]
# Enables keycode serialization # Enables keycode serialization
serde-keycode = ["iced_core/serde"] serde-keycode = ["iced_core/serde"]
# Prevents multiple separate process instances. # Prevents multiple separate process instances.
@ -80,8 +86,9 @@ mime = { version = "0.3.17", optional = true }
nix = { version = "0.27", features = ["process"], optional = true } nix = { version = "0.27", features = ["process"], optional = true }
palette = "0.7.3" palette = "0.7.3"
rfd = { version = "0.14.0", optional = true } rfd = { version = "0.14.0", optional = true }
serde = { version = "1.0.180", features = ["derive"]} serde = { version = "1.0.180", features = ["derive"] }
slotmap = "1.0.6" slotmap = "1.0.6"
textdistance = { version = "1.0.2", optional = true }
thiserror = "1.0.44" thiserror = "1.0.44"
tokio = { version = "1.24.2", optional = true } tokio = { version = "1.24.2", optional = true }
tracing = "0.1" tracing = "0.1"

View file

@ -1,7 +1,9 @@
pub use freedesktop_desktop_entry::DesktopEntry; pub use freedesktop_desktop_entry::DesktopEntry;
use iced_widget::canvas::path::lyon_path::geom::euclid::approxord::min;
pub use mime::Mime; pub use mime::Mime;
use std::{ use std::{
borrow::Cow, borrow::Cow,
cmp::max,
ffi::OsStr, ffi::OsStr,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
@ -80,46 +82,121 @@ pub fn app_id_or_fallback_matches(app_id: &str, entry: &DesktopEntryData) -> boo
|| app_id.to_lowercase() == entry.name.to_lowercase() || app_id.to_lowercase() == entry.name.to_lowercase()
} }
/// From 0 to 1.
/// 1 is a perfect match.
fn match_entry(id: &str, de: &DesktopEntry) -> f32 {
let cmp = |id, de| {
let lcsstr = textdistance::str::lcsstr(id, de);
lcsstr as f32 / (max(id.len(), de.len())) as f32
};
fn max_f32(a: f32, b: f32) -> f32 {
if a > b {
return a;
} else {
b
}
}
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 max_f32(
cmp(&id, &de_id),
max_f32(cmp(&id, &de_wm_class), cmp(&id, &de_name)),
);
}
pub fn load_applications_for_app_ids<'a, 'b>( pub fn load_applications_for_app_ids<'a, 'b>(
locale: impl Into<Option<&'a str>>, locale: impl Into<Option<&'a str>>,
app_ids: impl Iterator<Item = &'b str>, app_ids: impl Iterator<Item = &'b str>,
fill_missing_ones: bool, fill_missing_ones: bool,
include_no_display: bool, include_no_display: bool,
) -> Vec<DesktopEntryData> { ) -> Vec<DesktopEntryData> {
let mut app_ids = app_ids.collect::<Vec<_>>(); // need to be owned
let mut applications = load_applications_filtered(locale, |de| { let all_desktop_entries_string =
if !include_no_display && de.no_display() { freedesktop_desktop_entry::Iter::new(freedesktop_desktop_entry::default_paths())
return false; .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;
let mut second_max_score = 0.;
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 {
second_max_score = prev_max_score;
max_score = Some((score, de));
}
}
None => {
max_score = Some((score, de));
}
}
if score > 0.99 {
break;
}
} }
// If appid matches, or startup_wm_class matches...
if let Some(i) = app_ids.iter().position(|id| { let mut add_missing = false;
id == &de.appid match max_score {
|| id Some((max_score, de)) => {
.to_lowercase() let entropy = max_score - second_max_score;
.eq(&de.startup_wm_class().unwrap_or_default().to_lowercase())
}) { if max_score > 0.7 || entropy > 0.2 && max_score > 0.2 {
app_ids.remove(i); applications.push(DesktopEntryData::from_desktop_entry(
true locale,
// Fallback: If the name matches... Some(de.path.to_path_buf()),
} else if let Some(i) = app_ids.iter().position(|id| { de,
de.name(None) ));
.map(|n| n.to_lowercase() == id.to_lowercase()) } else {
.unwrap_or_default() add_missing = true;
}) { }
app_ids.remove(i); }
true None => {
} else { add_missing = true;
false }
} }
});
if fill_missing_ones && add_missing {
missing.push(id);
}
}
if fill_missing_ones { if fill_missing_ones {
applications.extend(app_ids.into_iter().map(|app_id| DesktopEntryData { applications.extend(missing.into_iter().map(|app_id| DesktopEntryData {
id: app_id.to_string(), id: app_id.to_string(),
name: app_id.to_string(), name: app_id.to_string(),
icon: IconSource::default(), icon: IconSource::default(),
..Default::default() ..Default::default()
})); }));
} }
applications applications
} }
@ -140,7 +217,7 @@ pub fn load_applications_filtered<'a, F: FnMut(&DesktopEntry) -> bool>(
Some(DesktopEntryData::from_desktop_entry( Some(DesktopEntryData::from_desktop_entry(
locale, locale,
path.clone(), path.clone(),
de, &de,
)) ))
}) })
}) })
@ -156,7 +233,7 @@ pub fn load_desktop_file<'a>(
std::fs::read_to_string(path).ok().and_then(|input| { std::fs::read_to_string(path).ok().and_then(|input| {
DesktopEntry::decode(path, &input) DesktopEntry::decode(path, &input)
.ok() .ok()
.map(|de| DesktopEntryData::from_desktop_entry(locale, PathBuf::from(path), de)) .map(|de| DesktopEntryData::from_desktop_entry(locale, PathBuf::from(path), &de))
}) })
} }
@ -164,7 +241,7 @@ impl DesktopEntryData {
fn from_desktop_entry<'a>( fn from_desktop_entry<'a>(
locale: impl Into<Option<&'a str>>, locale: impl Into<Option<&'a str>>,
path: impl Into<Option<PathBuf>>, path: impl Into<Option<PathBuf>>,
de: DesktopEntry, de: &DesktopEntry,
) -> DesktopEntryData { ) -> DesktopEntryData {
let locale = locale.into(); let locale = locale.into();