From 24499438636ad1930328cadcccbe2c09bc58b3b3 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 5 Jun 2024 23:25:49 +0200 Subject: [PATCH] improv: use freedesktop_entry to search applications --- .vscode/settings.json | 4 +- Cargo.lock | 23 +++--- justfile | 5 +- plugins/Cargo.toml | 6 +- plugins/src/cosmic_toplevel/mod.rs | 107 +++++++++++++++------------ plugins/src/desktop_entries/mod.rs | 54 ++++++-------- plugins/src/desktop_entries/utils.rs | 39 ++++++++++ plugins/src/pop_shell/mod.rs | 4 +- plugins/src/terminal/mod.rs | 3 +- 9 files changed, 146 insertions(+), 99 deletions(-) create mode 100644 plugins/src/desktop_entries/utils.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index 2c971df..dbe54be 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { - "remote.containers.dockerPath": "podman", - "rust-analyzer.check.overrideCommand": ["just", "check-json"] + // "remote.containers.dockerPath": "podman", + // "rust-analyzer.check.overrideCommand": ["just", "check-json"] } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index d7f3207..458c31c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -572,15 +572,6 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "dirs" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" -dependencies = [ - "dirs-sys 0.3.7", -] - [[package]] name = "dirs" version = "4.0.0" @@ -786,13 +777,15 @@ dependencies = [ [[package]] name = "freedesktop-desktop-entry" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c201444ddafb5506fe85265b48421664ff4617e3b7090ef99e42a0070c1aead0" +checksum = "4fefe79ec93a6aeaa938981fe3e11b4ed1b2f9deacc6bb631585bc48252d1bfa" dependencies = [ - "dirs 3.0.2", + "dirs 5.0.1", "gettext-rs", "memchr", + "strsim 0.11.1", + "textdistance", "thiserror", "xdg", ] @@ -2350,6 +2343,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "textdistance" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d321c8576c2b47e43953e9cce236550d4cd6af0a6ce518fe084340082ca6037b" + [[package]] name = "thiserror" version = "1.0.58" diff --git a/justfile b/justfile index 7f3b8e6..58d9ff8 100644 --- a/justfile +++ b/justfile @@ -2,6 +2,9 @@ ID := 'pop-launcher' plugins := 'calc desktop_entries files find pop_shell pulse recent scripts terminal web cosmic_toplevel' rootdir := '' +debug := '0' + +target-dir := if debug == '1' { 'target/debug' } else { 'target/release' } base-dir := if rootdir == '' { env_var('HOME') / '.local' @@ -57,7 +60,7 @@ install: install-bin install-plugins install-scripts # Install pop-launcher binary install-bin: - install -Dm0755 target/release/pop-launcher-bin {{bin-path}} + install -Dm0755 {{target-dir}}/pop-launcher-bin {{bin-path}} # Install pop-launcher plugins install-plugins: diff --git a/plugins/Cargo.toml b/plugins/Cargo.toml index ca3c8b5..49dfc1c 100644 --- a/plugins/Cargo.toml +++ b/plugins/Cargo.toml @@ -9,7 +9,7 @@ publish = false [dependencies] async-pidfd = "0.1.4" fork = "0.1.23" -freedesktop-desktop-entry = "0.5.2" +freedesktop-desktop-entry = "0.6.0" human_format = "1.1.0" human-sort = "0.2.2" new_mime_guess = "4.0.1" @@ -36,7 +36,9 @@ recently-used-xbel = "1.0.0" # dependencies cosmic toplevel cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit" } -sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "3bed072", features = ["calloop"] } +sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "3bed072", features = [ + "calloop", +] } # dependencies desktop entries switcheroo-control = { git = "https://github.com/pop-os/dbus-settings-bindings" } diff --git a/plugins/src/cosmic_toplevel/mod.rs b/plugins/src/cosmic_toplevel/mod.rs index e948938..4c7b9b1 100644 --- a/plugins/src/cosmic_toplevel/mod.rs +++ b/plugins/src/cosmic_toplevel/mod.rs @@ -3,9 +3,11 @@ mod toplevel_handler; use cctk::wayland_client::Proxy; use cctk::{cosmic_protocols, sctk::reexports::calloop, toplevel_info::ToplevelInfo}; use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1; - -use crate::send; +use fde::DesktopEntry; use freedesktop_desktop_entry as fde; + +use crate::desktop_entries::utils::get_description; +use crate::send; use futures::{ channel::mpsc, future::{select, Either}, @@ -16,7 +18,6 @@ use pop_launcher::{ Request, }; use std::borrow::Cow; -use std::{fs, path::PathBuf}; use tokio::io::{AsyncWrite, AsyncWriteExt}; use self::toplevel_handler::{toplevel_handler, ToplevelAction, ToplevelEvent}; @@ -90,7 +91,8 @@ pub async fn main() { } struct App { - desktop_entries: Vec<(fde::PathSource, PathBuf)>, + locales: Vec, + desktop_entries: Vec>, ids_to_ignore: Vec, toplevels: Vec<(ZcosmicToplevelHandleV1, ToplevelInfo)>, calloop_tx: calloop::channel::Sender, @@ -103,12 +105,19 @@ impl App { let (calloop_tx, calloop_rx) = calloop::channel::channel(); let _handle = std::thread::spawn(move || toplevel_handler(toplevels_tx, calloop_rx)); + 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()) + .collect::>(); + ( Self { + locales, + desktop_entries, ids_to_ignore: Vec::new(), - desktop_entries: fde::Iter::new(fde::default_paths()) - .map(|path| (fde::PathSource::guess_from(&path), path)) - .collect(), toplevels: Vec::new(), calloop_tx, tx, @@ -150,55 +159,57 @@ impl App { } 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::>(); - for item in &self.toplevels { - let retain = query.is_empty() - || contains_pattern(&item.1.app_id.to_ascii_lowercase(), &haystack) - || contains_pattern(&item.1.title.to_ascii_lowercase(), &haystack); + for (handle, info) in &self.toplevels { + let entry = if query.is_empty() { + fde::matching::get_best_match( + &[&info.app_id, &info.title], + &self.desktop_entries, + fde::matching::MatchAppIdOptions::default(), + ) + } else { + fde::matching::get_best_match( + &[&info.app_id, &info.title], + &self.desktop_entries, + fde::matching::MatchAppIdOptions::default(), + ) + .and_then(|de| { + let score = fde::matching::get_entry_score( + &query, + de, + &self.locales, + &[&info.app_id, &info.title], + ); - if !retain { - continue; - } - - let mut icon_name = Cow::Borrowed("application-x-executable"); - - for (_, path) in &self.desktop_entries { - if let Ok(data) = fs::read_to_string(&path) { - if let Ok(entry) = fde::DesktopEntry::decode(&path, &data) { - if item.1.app_id == entry.appid - || entry - .startup_wm_class() - .is_some_and(|class| class == item.1.app_id) - { - if let Some(icon) = entry.icon() { - icon_name = Cow::Owned(icon.to_owned()); - } - break; - } + if score > 0.6 { + Some(de) + } else { + None } - } - } + }) + }; - send( - &mut self.tx, - PluginResponse::Append(PluginSearchResult { + 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: item.0.id().protocol_id(), - window: Some((0, item.0.id().clone().protocol_id())), - name: item.1.app_id.clone(), - description: item.1.title.clone(), + id: handle.id().protocol_id(), + window: Some((0, handle.id().clone().protocol_id())), + // XXX: why this is inversed for this plugin ???? + description: info.title.clone(), + name: get_description(de, &self.locales), icon: Some(IconSource::Name(icon_name)), ..Default::default() - }), - ) - .await; + }); + + send(&mut self.tx, response).await; + } } send(&mut self.tx, PluginResponse::Finished).await; diff --git a/plugins/src/desktop_entries/mod.rs b/plugins/src/desktop_entries/mod.rs index c21cc14..3b00cd7 100644 --- a/plugins/src/desktop_entries/mod.rs +++ b/plugins/src/desktop_entries/mod.rs @@ -2,13 +2,20 @@ // Copyright © 2021 System76 use crate::*; -use freedesktop_desktop_entry::{default_paths, DesktopEntry, Iter as DesktopIter, PathSource}; + +use freedesktop_desktop_entry as fde; +use freedesktop_desktop_entry::{ + default_paths, get_languages_from_env, DesktopEntry, Iter as DesktopIter, PathSource, +}; use futures::StreamExt; use pop_launcher::*; use std::borrow::Cow; use std::hash::{Hash, Hasher}; use std::path::PathBuf; use tokio::io::AsyncWrite; +use utils::path_string; + +pub(crate) mod utils; #[derive(Debug, Eq)] struct Item { @@ -65,21 +72,16 @@ const EXCLUSIONS: &[&str] = &["GNOME Shell", "Initial Setup"]; struct App { entries: Vec, - locale: Option, + locales: Vec, tx: W, gpus: Option>, } impl App { fn new(tx: W) -> Self { - let lang = std::env::var("LANG").ok(); - Self { entries: Vec::new(), - locale: lang - .as_ref() - .and_then(|l| l.split('.').next()) - .map(String::from), + locales: fde::get_languages_from_env(), tx, gpus: None, } @@ -88,8 +90,6 @@ impl App { async fn reload(&mut self) { self.entries.clear(); - let locale = self.locale.as_ref().map(String::as_ref); - let mut deduplicator = std::collections::HashSet::new(); let current = current_desktop(); @@ -100,7 +100,9 @@ impl App { for path in DesktopIter::new(default_paths()) { let src = PathSource::guess_from(&path); if let Ok(bytes) = std::fs::read_to_string(&path) { - if let Ok(entry) = DesktopEntry::decode(&path, &bytes) { + if let Ok(entry) = + DesktopEntry::from_str(&path, &bytes, &get_languages_from_env()) + { // Do not show if our desktop is defined in `NotShowIn`. if let Some(not_show_in) = entry.desktop_entry("NotShowIn") { let current = ward::ward!(current.as_ref(), else { continue }); @@ -134,7 +136,7 @@ impl App { // Avoid showing the GNOME Shell entry entirely if entry - .name(None) + .name(&[] as &[&str]) .map_or(false, |v| EXCLUSIONS.contains(&v.as_ref())) { continue; @@ -145,21 +147,21 @@ impl App { continue; } - if let Some((name, exec)) = entry.name(locale).zip(entry.exec()) { + if let Some((name, exec)) = entry.name(&self.locales).zip(entry.exec()) { if let Some(exec) = exec.split_ascii_whitespace().next() { if exec == "false" { continue; } let item = Item { - appid: entry.appid.to_owned(), + appid: entry.appid.to_string(), name: name.to_string(), description: entry - .comment(locale) + .comment(&self.locales) .as_deref() .unwrap_or("") .to_owned(), - keywords: entry.keywords().map(|keywords| { + keywords: entry.keywords(&self.locales).map(|keywords| { keywords.split(';').map(String::from).collect() }), icon: Some( @@ -178,7 +180,11 @@ impl App { actions .split(';') .filter_map(|action| { - entry.action_entry_localized(action, "Name", None) + entry.action_entry_localized( + action, + "Name", + &self.locales, + ) }) .map(Cow::into_owned) .collect::>() @@ -378,20 +384,6 @@ fn current_desktop() -> Option { }) } -fn path_string(source: &PathSource) -> Cow<'static, str> { - match source { - PathSource::Local | PathSource::LocalDesktop => "Local".into(), - PathSource::LocalFlatpak => "Flatpak".into(), - PathSource::LocalNix => "Nix".into(), - PathSource::Nix => "Nix (System)".into(), - PathSource::System => "System".into(), - PathSource::SystemLocal => "Local (System)".into(), - PathSource::SystemFlatpak => "Flatpak (System)".into(), - PathSource::SystemSnap => "Snap (System)".into(), - PathSource::Other(other) => Cow::Owned(other.clone()), - } -} - async fn try_get_gpus() -> Option> { let connection = zbus::Connection::system().await.ok()?; let proxy = switcheroo_control::SwitcherooControlProxy::new(&connection) diff --git a/plugins/src/desktop_entries/utils.rs b/plugins/src/desktop_entries/utils.rs new file mode 100644 index 0000000..ba649a2 --- /dev/null +++ b/plugins/src/desktop_entries/utils.rs @@ -0,0 +1,39 @@ +//! Reusable functions for desktop entries + +use std::borrow::Cow; + +use freedesktop_desktop_entry::{DesktopEntry, PathSource}; + +// todo: subscriptions with notify + +pub fn path_string(source: &PathSource) -> Cow<'static, str> { + match source { + PathSource::Local | PathSource::LocalDesktop => "Local".into(), + PathSource::LocalFlatpak => "Flatpak".into(), + PathSource::LocalNix => "Nix".into(), + PathSource::Nix => "Nix (System)".into(), + PathSource::System => "System".into(), + PathSource::SystemLocal => "Local (System)".into(), + PathSource::SystemFlatpak => "Flatpak (System)".into(), + PathSource::SystemSnap => "Snap (System)".into(), + PathSource::Other(other) => Cow::Owned(other.clone()), + } +} + +pub fn get_description<'a>(de: &'a DesktopEntry<'a>, locales: &[String]) -> String { + let path_source = PathSource::guess_from(&de.path); + + let desc_source = path_string(&path_source).to_string(); + + + match de.comment(locales) { + Some(desc) => { + if desc.is_empty() { + desc_source + } else { + format!("{} - {}", desc_source, desc) + } + } + None => desc_source, + } +} diff --git a/plugins/src/pop_shell/mod.rs b/plugins/src/pop_shell/mod.rs index 6470bc7..e944701 100644 --- a/plugins/src/pop_shell/mod.rs +++ b/plugins/src/pop_shell/mod.rs @@ -2,7 +2,7 @@ // Copyright © 2021 System76 use crate::*; -use freedesktop_desktop_entry as fde; +use freedesktop_desktop_entry::{self as fde, get_languages_from_env}; use futures::StreamExt; use pop_launcher::*; use serde::{Deserialize, Serialize}; @@ -139,7 +139,7 @@ impl App { if let Some(name) = path.file_stem() { if desktop_entry == name { if let Ok(data) = fs::read_to_string(path) { - if let Ok(entry) = fde::DesktopEntry::decode(path, &data) { + if let Ok(entry) = fde::DesktopEntry::from_str(path, &data, &get_languages_from_env()) { if let Some(icon) = entry.icon() { icon_name = Cow::Owned(icon.to_owned()); } diff --git a/plugins/src/terminal/mod.rs b/plugins/src/terminal/mod.rs index 51bc1b4..235f83e 100644 --- a/plugins/src/terminal/mod.rs +++ b/plugins/src/terminal/mod.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only // Copyright © 2021 System76 +use freedesktop_desktop_entry::get_languages_from_env; use futures::prelude::*; use pop_launcher::*; use std::path::PathBuf; @@ -123,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::decode(&path, &input).ok().and_then(|de| { + DesktopEntry::from_str(&path, &input, &get_languages_from_env()).ok().and_then(|de| { if de.no_display() || de .categories()