fix: update freedesktop-desktop-entry

This commit is contained in:
Michael Aaron Murphy 2025-04-30 17:19:16 +02:00 committed by Michael Murphy
parent 58a8f2db64
commit 8d9da92dba
8 changed files with 833 additions and 610 deletions

1102
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "pop-launcher"
version = "1.2.4"
version = "1.2.5"
license = "MPL-2.0"
authors = ["Michael Aaron Murphy <michael@mmurphy.dev>"]
description = "Library for writing plugins and frontends for pop-launcher"
@ -15,22 +15,22 @@ resolver = "2"
edition = "2021"
[workspace.dependencies]
anyhow = "1.0.90"
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.129"
anyhow = "1.0.98"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
tracing = "0.1.40"
dirs = "5.0.1"
serde_with = "3.11.0"
dirs = "6.0.0"
serde_with = "3.12.0"
futures = "0.3.31"
flume = "0.11.0"
toml = "0.8.19"
toml = "0.8.22"
regex = "1.11.0"
ron = "0.8.1"
tokio = "1.40.0"
tokio-stream = "0.1.16"
ron = "0.9.0"
tokio = "1.44.2"
tokio-stream = "0.1.17"
[dependencies]
const_format = "0.2.33"
const_format = "0.2.34"
dirs.workspace = true
futures.workspace = true
serde.workspace = true
@ -49,7 +49,6 @@ features = ["io-std", "io-util"]
workspace = true
features = ["io-util"]
# [patch.crates-io]
# freedesktop-desktop-entry = { path = "../freedesktop-desktop-entry" }

View file

@ -9,7 +9,7 @@ publish = false
[dependencies]
async-pidfd = "0.1.4"
fork = "0.2.0"
freedesktop-desktop-entry = "0.6.2"
freedesktop-desktop-entry = "0.7.11"
human_format = "1.1.0"
human-sort = "0.2.2"
new_mime_guess = "4.0.4"

View file

@ -20,8 +20,6 @@ use pop_launcher::{
Request,
};
use std::borrow::Cow;
use std::iter;
use std::time::Instant;
use tokio::io::{AsyncWrite, AsyncWriteExt};
use self::toplevel_handler::{toplevel_handler, ToplevelAction};
@ -111,7 +109,7 @@ pub async fn main() {
struct App<W> {
locales: Vec<String>,
desktop_entries: Vec<DesktopEntry<'static>>,
desktop_entries: Vec<DesktopEntry>,
ids_to_ignore: Vec<u32>,
toplevels: Vec<Box<ToplevelInfo>>,
calloop_tx: calloop::channel::Sender<ToplevelAction>,
@ -126,10 +124,9 @@ impl<W: AsyncWrite + Unpin> App<W> {
let locales = fde::get_languages_from_env();
let paths = fde::Iter::new(fde::default_paths());
let desktop_entries = DesktopEntry::from_paths(paths, &locales)
.filter_map(|e| e.ok())
let desktop_entries = fde::Iter::new(fde::default_paths())
.map(|path| DesktopEntry::from_path(path, Some(&locales)))
.filter_map(Result::ok)
.collect::<Vec<_>>();
(
@ -178,82 +175,53 @@ impl<W: AsyncWrite + Unpin> App<W> {
}
async fn search(&mut self, query: &str) {
fn contains_pattern(needle: &str, haystack: &[&str]) -> bool {
let needle = needle.to_ascii_lowercase();
haystack.iter().all(|h| needle.contains(h))
}
let query = query.to_ascii_lowercase();
let haystack = query.split_ascii_whitespace().collect::<Vec<&str>>();
for info in self.toplevels.iter().rev() {
let entry = if query.is_empty() {
fde::matching::get_best_match(
&[&info.app_id, &info.title],
&self.desktop_entries,
fde::matching::MatchAppIdOptions::default(),
)
for info in &self.toplevels {
let retain = query.is_empty()
|| contains_pattern(&info.app_id, &haystack)
|| contains_pattern(&info.title, &haystack);
if !retain {
continue;
}
let appid = fde::unicase::Ascii::new(info.app_id.as_str());
let entry = fde::find_app_by_id(&self.desktop_entries, appid)
.map(ToOwned::to_owned)
.unwrap_or_else(|| fde::DesktopEntry::from_appid(appid.to_string()).to_owned());
let icon_name = if let Some(icon) = entry.icon() {
Cow::Owned(icon.to_owned())
} else {
let lowercase_title = info.title.to_lowercase();
let window_words = lowercase_title
.split_whitespace()
.chain(iter::once(info.app_id.as_str()))
.chain(iter::once(info.title.as_str()))
.collect::<Vec<_>>();
// if there's an exact appid match, use that instead
let exact_appid_match = self
.desktop_entries
.iter()
.find(|de| de.appid == info.app_id);
if exact_appid_match.is_some()
&& fde::matching::get_entry_score(
&query,
exact_appid_match.unwrap(),
&self.locales,
&window_words,
) > 0.8
{
exact_appid_match
} else {
fde::matching::get_best_match(
&window_words,
&self.desktop_entries,
fde::matching::MatchAppIdOptions::default(),
)
.and_then(|de| {
let score = fde::matching::get_entry_score(
&query,
de,
&self.locales,
&window_words,
);
if score > 0.8 {
Some(de)
} else {
None
}
})
}
Cow::Borrowed("application-x-executable")
};
if let Some(de) = entry {
let icon_name = if let Some(icon) = de.icon() {
Cow::Owned(icon.to_owned())
} else {
Cow::Borrowed("application-x-executable")
};
let response = PluginResponse::Append(PluginSearchResult {
// XXX protocol id may be re-used later
id: info.foreign_toplevel.id().protocol_id(),
window: Some((0, info.foreign_toplevel.id().protocol_id())),
description: info.title.clone(),
name: get_description(&entry, &self.locales),
icon: Some(IconSource::Name(icon_name)),
..Default::default()
});
let response = PluginResponse::Append(PluginSearchResult {
// XXX protocol id may be re-used later
id: info.foreign_toplevel.id().protocol_id(),
window: Some((0, info.foreign_toplevel.id().protocol_id())),
description: info.title.clone(),
name: get_description(de, &self.locales),
icon: Some(IconSource::Name(icon_name)),
..Default::default()
});
send(&mut self.tx, response).await;
}
send(
&mut self.tx,
response,
)
.await;
}
send(&mut self.tx, PluginResponse::Finished).await;
let _ = self.tx.flush().await;
let _ = self.tx.flush();
}
}

View file

@ -43,7 +43,7 @@ const EXCLUSIONS: &[&str] = &["GNOME Shell", "Initial Setup"];
struct App<W> {
current_desktop: Option<Vec<String>>,
is_desktop_cosmic: bool,
desktop_entries: Vec<DesktopEntry<'static>>,
desktop_entries: Vec<DesktopEntry>,
locales: Vec<String>,
tx: W,
gpus: Option<Vec<switcheroo_control::Gpu>>,
@ -69,79 +69,78 @@ impl<W: AsyncWrite + Unpin> App<W> {
let paths = fde::Iter::new(fde::default_paths());
let desktop_entries = DesktopEntry::from_paths(paths, &locales)
let desktop_entries = paths
.flat_map(|path| DesktopEntry::from_path(path, Some(&locales)))
.filter_map(|de| {
de.ok().and_then(|de| {
// Treat Flatpak and system apps differently in the cache so they don't
// override each other
let appid = de.flatpak().unwrap_or_else(|| de.appid.as_ref());
if deduplicator.contains(appid) {
return None;
}
// Treat Flatpak and system apps differently in the cache so they don't
// override each other
let appid = de.flatpak().unwrap_or_else(|| de.appid.as_ref());
if deduplicator.contains(appid) {
return None;
}
de.name(&self.locales)?;
de.name(&self.locales)?;
match de.exec() {
Some(exec) => match exec.split_ascii_whitespace().next() {
Some(exec) => {
if exec == "false" {
return None;
}
match de.exec() {
Some(exec) => match exec.split_ascii_whitespace().next() {
Some(exec) => {
if exec == "false" {
return None;
}
None => return None,
},
}
None => return None,
}
},
None => return None,
}
// Avoid showing the GNOME Shell entry entirely
if de
.name(&[] as &[&str])
.map_or(false, |v| EXCLUSIONS.contains(&v.as_ref()))
{
return None;
}
// Avoid showing the GNOME Shell entry entirely
if de
.name(&[] as &[&str])
.map_or(false, |v| EXCLUSIONS.contains(&v.as_ref()))
{
return None;
}
// Do not show if our desktop is defined in `NotShowIn`.
if let Some(not_show_in) = de.not_show_in() {
if let Some(current_desktop) = &self.current_desktop {
if not_show_in.iter().any(|not_show| {
current_desktop
.iter()
.any(|desktop| &not_show.to_ascii_lowercase() == desktop)
}) {
return None;
}
// Do not show if our desktop is defined in `NotShowIn`.
if let Some(not_show_in) = de.not_show_in() {
if let Some(current_desktop) = &self.current_desktop {
if not_show_in.iter().any(|not_show| {
current_desktop
.iter()
.any(|desktop| &not_show.to_ascii_lowercase() == desktop)
}) {
return None;
}
}
}
// Do not show if our desktop is not defined in `OnlyShowIn`.
if let Some(only_show_in) = de.only_show_in() {
if let Some(current_desktop) = &self.current_desktop {
if !only_show_in.iter().any(|show_in| {
current_desktop
.iter()
.any(|desktop| &show_in.to_ascii_lowercase() == desktop)
}) {
return None;
}
// Do not show if our desktop is not defined in `OnlyShowIn`.
if let Some(only_show_in) = de.only_show_in() {
if let Some(current_desktop) = &self.current_desktop {
if !only_show_in.iter().any(|show_in| {
current_desktop
.iter()
.any(|desktop| &show_in.to_ascii_lowercase() == desktop)
}) {
return None;
}
}
// Treat `OnlyShowIn` as an override otherwise do not show if `NoDisplay` is true
// Some desktop environments set `OnlyShowIn` and `NoDisplay = true` to
// indicate special entries
else if de.no_display() {
return None;
}
}
// Treat `OnlyShowIn` as an override otherwise do not show if `NoDisplay` is true
// Some desktop environments set `OnlyShowIn` and `NoDisplay = true` to
// indicate special entries
else if de.no_display() {
return None;
}
// Always cache already visited entries to allow overriding entries e.g. by
// placing a modified copy in ~/.local/share/applications/
//
// We only do this when we can add an entry to our list, otherwise we risk
// ignoring user overrides or valid applications due to shell URL handlers
deduplicator.insert(appid.to_owned());
// Always cache already visited entries to allow overriding entries e.g. by
// placing a modified copy in ~/.local/share/applications/
//
// We only do this when we can add an entry to our list, otherwise we risk
// ignoring user overrides or valid applications due to shell URL handlers
deduplicator.insert(appid.to_owned());
Some(de)
})
Some(de)
})
.collect::<Vec<_>>();
@ -213,33 +212,66 @@ impl<W: AsyncWrite + Unpin> App<W> {
}
async fn search(&mut self, query: &str) {
for (id, entry) in self.desktop_entries.iter().enumerate() {
let score = fde::matching::get_entry_score(query, entry, &self.locales, &[]);
let query = query.to_ascii_lowercase();
if score > 0.6 {
let response = PluginResponse::Append(PluginSearchResult {
id: id as u32,
name: entry.name(&self.locales).unwrap_or_default().to_string(),
description: get_description(entry, &self.locales),
keywords: entry
.keywords(&self.locales)
.map(|v| v.iter().map(|e| e.to_string()).collect()),
icon: entry
.icon()
.map(|e| Cow::Owned(e.to_string()))
.map(IconSource::Name),
exec: entry.exec().map(|e| e.to_string()),
..Default::default()
});
let &mut Self {
ref desktop_entries,
ref locales,
ref mut tx,
..
} = self;
send(&mut self.tx, response).await;
let mut items = Vec::with_capacity(16);
for (id, entry) in desktop_entries.iter().enumerate() {
let name = entry.name(locales).unwrap_or_default();
let keywords = entry.keywords(locales);
items.extend(name.split_ascii_whitespace().map(ToOwned::to_owned));
if let Some(keywords) = keywords.as_ref() {
items.extend(keywords.iter().map(|x| String::from(x.as_ref())));
}
if let Some(exec) = entry.exec() {
items.push(exec.to_owned());
}
for search_interest in items.drain(..) {
let search_interest = search_interest.to_ascii_lowercase();
let append = search_interest.starts_with(&*query)
|| query
.split_ascii_whitespace()
.any(|query| search_interest.contains(&*query))
|| strsim::jaro_winkler(&*query, &*search_interest) > 0.6;
if append {
let response = PluginResponse::Append(PluginSearchResult {
id: id as u32,
name: entry.name(&self.locales).unwrap_or_default().to_string(),
description: get_description(entry, &self.locales),
keywords: entry
.keywords(&self.locales)
.map(|v| v.iter().map(|e| e.to_string()).collect()),
icon: entry
.icon()
.map(|e| Cow::Owned(e.to_string()))
.map(IconSource::Name),
exec: entry.exec().map(|e| e.to_string()),
..Default::default()
});
send(tx, response).await;
break;
}
}
}
send(&mut self.tx, PluginResponse::Finished).await;
let _ = self.tx.flush();
}
async fn gnome_context(&self, entry: &DesktopEntry<'_>) -> Vec<ContextOption> {
async fn gnome_context(&self, entry: &DesktopEntry) -> Vec<ContextOption> {
if self.gpus.is_some() {
vec![ContextOption {
id: 0,
@ -255,7 +287,7 @@ impl<W: AsyncWrite + Unpin> App<W> {
}
}
async fn cosmic_context(&self, entry: &DesktopEntry<'_>) -> Vec<ContextOption> {
async fn cosmic_context(&self, entry: &DesktopEntry) -> Vec<ContextOption> {
let mut options = Vec::new();
if let Some(gpus) = self.gpus.as_ref() {

View file

@ -20,7 +20,7 @@ pub fn path_string(source: &PathSource) -> Cow<'static, str> {
}
}
pub fn get_description<'a>(de: &'a DesktopEntry<'a>, locales: &[String]) -> String {
pub fn get_description<'a>(de: &'a DesktopEntry, locales: &[String]) -> String {
let path_source = PathSource::guess_from(&de.path);
let desc_source = path_string(&path_source).to_string();

View file

@ -143,7 +143,7 @@ impl<W: AsyncWrite + Unpin> App<W> {
if let Ok(entry) = fde::DesktopEntry::from_str(
path,
&data,
&get_languages_from_env(),
Some(&get_languages_from_env()),
) {
if let Some(icon) = entry.icon() {
icon_name = Cow::Owned(icon.to_owned());

View file

@ -124,7 +124,7 @@ fn detect_terminal() -> (PathBuf, &'static str) {
freedesktop_desktop_entry::Iter::new(freedesktop_desktop_entry::default_paths())
.filter_map(|path| {
std::fs::read_to_string(&path).ok().and_then(|input| {
DesktopEntry::from_str(&path, &input, &get_languages_from_env())
DesktopEntry::from_str(&path, &input, Some(&get_languages_from_env()))
.ok()
.and_then(|de| {
if de.no_display()