improv: use freedesktop_entry to search applications

This commit is contained in:
wiiznokes 2024-06-05 23:25:49 +02:00 committed by GitHub
parent 65c1742a88
commit 2449943863
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 146 additions and 99 deletions

View file

@ -1,4 +1,4 @@
{ {
"remote.containers.dockerPath": "podman", // "remote.containers.dockerPath": "podman",
"rust-analyzer.check.overrideCommand": ["just", "check-json"] // "rust-analyzer.check.overrideCommand": ["just", "check-json"]
} }

23
Cargo.lock generated
View file

@ -572,15 +572,6 @@ dependencies = [
"crypto-common", "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]] [[package]]
name = "dirs" name = "dirs"
version = "4.0.0" version = "4.0.0"
@ -786,13 +777,15 @@ dependencies = [
[[package]] [[package]]
name = "freedesktop-desktop-entry" name = "freedesktop-desktop-entry"
version = "0.5.2" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c201444ddafb5506fe85265b48421664ff4617e3b7090ef99e42a0070c1aead0" checksum = "4fefe79ec93a6aeaa938981fe3e11b4ed1b2f9deacc6bb631585bc48252d1bfa"
dependencies = [ dependencies = [
"dirs 3.0.2", "dirs 5.0.1",
"gettext-rs", "gettext-rs",
"memchr", "memchr",
"strsim 0.11.1",
"textdistance",
"thiserror", "thiserror",
"xdg", "xdg",
] ]
@ -2350,6 +2343,12 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.58" version = "1.0.58"

View file

@ -2,6 +2,9 @@ ID := 'pop-launcher'
plugins := 'calc desktop_entries files find pop_shell pulse recent scripts terminal web cosmic_toplevel' plugins := 'calc desktop_entries files find pop_shell pulse recent scripts terminal web cosmic_toplevel'
rootdir := '' rootdir := ''
debug := '0'
target-dir := if debug == '1' { 'target/debug' } else { 'target/release' }
base-dir := if rootdir == '' { base-dir := if rootdir == '' {
env_var('HOME') / '.local' env_var('HOME') / '.local'
@ -57,7 +60,7 @@ install: install-bin install-plugins install-scripts
# Install pop-launcher binary # Install pop-launcher binary
install-bin: 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 pop-launcher plugins
install-plugins: install-plugins:

View file

@ -9,7 +9,7 @@ publish = false
[dependencies] [dependencies]
async-pidfd = "0.1.4" async-pidfd = "0.1.4"
fork = "0.1.23" fork = "0.1.23"
freedesktop-desktop-entry = "0.5.2" freedesktop-desktop-entry = "0.6.0"
human_format = "1.1.0" human_format = "1.1.0"
human-sort = "0.2.2" human-sort = "0.2.2"
new_mime_guess = "4.0.1" new_mime_guess = "4.0.1"
@ -36,7 +36,9 @@ recently-used-xbel = "1.0.0"
# dependencies cosmic toplevel # dependencies cosmic toplevel
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit" } 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 # dependencies desktop entries
switcheroo-control = { git = "https://github.com/pop-os/dbus-settings-bindings" } switcheroo-control = { git = "https://github.com/pop-os/dbus-settings-bindings" }

View file

@ -3,9 +3,11 @@ mod toplevel_handler;
use cctk::wayland_client::Proxy; use cctk::wayland_client::Proxy;
use cctk::{cosmic_protocols, sctk::reexports::calloop, toplevel_info::ToplevelInfo}; use cctk::{cosmic_protocols, sctk::reexports::calloop, toplevel_info::ToplevelInfo};
use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1; use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
use fde::DesktopEntry;
use crate::send;
use freedesktop_desktop_entry as fde; use freedesktop_desktop_entry as fde;
use crate::desktop_entries::utils::get_description;
use crate::send;
use futures::{ use futures::{
channel::mpsc, channel::mpsc,
future::{select, Either}, future::{select, Either},
@ -16,7 +18,6 @@ use pop_launcher::{
Request, Request,
}; };
use std::borrow::Cow; use std::borrow::Cow;
use std::{fs, path::PathBuf};
use tokio::io::{AsyncWrite, AsyncWriteExt}; use tokio::io::{AsyncWrite, AsyncWriteExt};
use self::toplevel_handler::{toplevel_handler, ToplevelAction, ToplevelEvent}; use self::toplevel_handler::{toplevel_handler, ToplevelAction, ToplevelEvent};
@ -90,7 +91,8 @@ pub async fn main() {
} }
struct App<W> { struct App<W> {
desktop_entries: Vec<(fde::PathSource, PathBuf)>, locales: Vec<String>,
desktop_entries: Vec<DesktopEntry<'static>>,
ids_to_ignore: Vec<u32>, ids_to_ignore: Vec<u32>,
toplevels: Vec<(ZcosmicToplevelHandleV1, ToplevelInfo)>, toplevels: Vec<(ZcosmicToplevelHandleV1, ToplevelInfo)>,
calloop_tx: calloop::channel::Sender<ToplevelAction>, calloop_tx: calloop::channel::Sender<ToplevelAction>,
@ -103,12 +105,19 @@ impl<W: AsyncWrite + Unpin> App<W> {
let (calloop_tx, calloop_rx) = calloop::channel::channel(); let (calloop_tx, calloop_rx) = calloop::channel::channel();
let _handle = std::thread::spawn(move || toplevel_handler(toplevels_tx, calloop_rx)); 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::<Vec<_>>();
( (
Self { Self {
locales,
desktop_entries,
ids_to_ignore: Vec::new(), 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(), toplevels: Vec::new(),
calloop_tx, calloop_tx,
tx, tx,
@ -150,55 +159,57 @@ impl<W: AsyncWrite + Unpin> App<W> {
} }
async fn search(&mut self, query: &str) { 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 query = query.to_ascii_lowercase();
let haystack = query.split_ascii_whitespace().collect::<Vec<&str>>();
for item in &self.toplevels { for (handle, info) in &self.toplevels {
let retain = query.is_empty() let entry = if query.is_empty() {
|| contains_pattern(&item.1.app_id.to_ascii_lowercase(), &haystack) fde::matching::get_best_match(
|| contains_pattern(&item.1.title.to_ascii_lowercase(), &haystack); &[&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 { if score > 0.6 {
continue; Some(de)
} } else {
None
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;
}
} }
} })
} };
send( if let Some(de) = entry {
&mut self.tx, let icon_name = if let Some(icon) = de.icon() {
PluginResponse::Append(PluginSearchResult { Cow::Owned(icon.to_owned())
} else {
Cow::Borrowed("application-x-executable")
};
let response = PluginResponse::Append(PluginSearchResult {
// XXX protocol id may be re-used later // XXX protocol id may be re-used later
id: item.0.id().protocol_id(), id: handle.id().protocol_id(),
window: Some((0, item.0.id().clone().protocol_id())), window: Some((0, handle.id().clone().protocol_id())),
name: item.1.app_id.clone(), // XXX: why this is inversed for this plugin ????
description: item.1.title.clone(), description: info.title.clone(),
name: get_description(de, &self.locales),
icon: Some(IconSource::Name(icon_name)), icon: Some(IconSource::Name(icon_name)),
..Default::default() ..Default::default()
}), });
)
.await; send(&mut self.tx, response).await;
}
} }
send(&mut self.tx, PluginResponse::Finished).await; send(&mut self.tx, PluginResponse::Finished).await;

View file

@ -2,13 +2,20 @@
// Copyright © 2021 System76 // Copyright © 2021 System76
use crate::*; 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 futures::StreamExt;
use pop_launcher::*; use pop_launcher::*;
use std::borrow::Cow; use std::borrow::Cow;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::path::PathBuf; use std::path::PathBuf;
use tokio::io::AsyncWrite; use tokio::io::AsyncWrite;
use utils::path_string;
pub(crate) mod utils;
#[derive(Debug, Eq)] #[derive(Debug, Eq)]
struct Item { struct Item {
@ -65,21 +72,16 @@ const EXCLUSIONS: &[&str] = &["GNOME Shell", "Initial Setup"];
struct App<W> { struct App<W> {
entries: Vec<Item>, entries: Vec<Item>,
locale: Option<String>, locales: Vec<String>,
tx: W, tx: W,
gpus: Option<Vec<switcheroo_control::Gpu>>, gpus: Option<Vec<switcheroo_control::Gpu>>,
} }
impl<W: AsyncWrite + Unpin> App<W> { impl<W: AsyncWrite + Unpin> App<W> {
fn new(tx: W) -> Self { fn new(tx: W) -> Self {
let lang = std::env::var("LANG").ok();
Self { Self {
entries: Vec::new(), entries: Vec::new(),
locale: lang locales: fde::get_languages_from_env(),
.as_ref()
.and_then(|l| l.split('.').next())
.map(String::from),
tx, tx,
gpus: None, gpus: None,
} }
@ -88,8 +90,6 @@ impl<W: AsyncWrite + Unpin> App<W> {
async fn reload(&mut self) { async fn reload(&mut self) {
self.entries.clear(); self.entries.clear();
let locale = self.locale.as_ref().map(String::as_ref);
let mut deduplicator = std::collections::HashSet::new(); let mut deduplicator = std::collections::HashSet::new();
let current = current_desktop(); let current = current_desktop();
@ -100,7 +100,9 @@ impl<W: AsyncWrite + Unpin> App<W> {
for path in DesktopIter::new(default_paths()) { for path in DesktopIter::new(default_paths()) {
let src = PathSource::guess_from(&path); let src = PathSource::guess_from(&path);
if let Ok(bytes) = std::fs::read_to_string(&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`. // Do not show if our desktop is defined in `NotShowIn`.
if let Some(not_show_in) = entry.desktop_entry("NotShowIn") { if let Some(not_show_in) = entry.desktop_entry("NotShowIn") {
let current = ward::ward!(current.as_ref(), else { continue }); let current = ward::ward!(current.as_ref(), else { continue });
@ -134,7 +136,7 @@ impl<W: AsyncWrite + Unpin> App<W> {
// Avoid showing the GNOME Shell entry entirely // Avoid showing the GNOME Shell entry entirely
if entry if entry
.name(None) .name(&[] as &[&str])
.map_or(false, |v| EXCLUSIONS.contains(&v.as_ref())) .map_or(false, |v| EXCLUSIONS.contains(&v.as_ref()))
{ {
continue; continue;
@ -145,21 +147,21 @@ impl<W: AsyncWrite + Unpin> App<W> {
continue; 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 let Some(exec) = exec.split_ascii_whitespace().next() {
if exec == "false" { if exec == "false" {
continue; continue;
} }
let item = Item { let item = Item {
appid: entry.appid.to_owned(), appid: entry.appid.to_string(),
name: name.to_string(), name: name.to_string(),
description: entry description: entry
.comment(locale) .comment(&self.locales)
.as_deref() .as_deref()
.unwrap_or("") .unwrap_or("")
.to_owned(), .to_owned(),
keywords: entry.keywords().map(|keywords| { keywords: entry.keywords(&self.locales).map(|keywords| {
keywords.split(';').map(String::from).collect() keywords.split(';').map(String::from).collect()
}), }),
icon: Some( icon: Some(
@ -178,7 +180,11 @@ impl<W: AsyncWrite + Unpin> App<W> {
actions actions
.split(';') .split(';')
.filter_map(|action| { .filter_map(|action| {
entry.action_entry_localized(action, "Name", None) entry.action_entry_localized(
action,
"Name",
&self.locales,
)
}) })
.map(Cow::into_owned) .map(Cow::into_owned)
.collect::<Vec<_>>() .collect::<Vec<_>>()
@ -378,20 +384,6 @@ fn current_desktop() -> Option<String> {
}) })
} }
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<Vec<switcheroo_control::Gpu>> { async fn try_get_gpus() -> Option<Vec<switcheroo_control::Gpu>> {
let connection = zbus::Connection::system().await.ok()?; let connection = zbus::Connection::system().await.ok()?;
let proxy = switcheroo_control::SwitcherooControlProxy::new(&connection) let proxy = switcheroo_control::SwitcherooControlProxy::new(&connection)

View file

@ -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,
}
}

View file

@ -2,7 +2,7 @@
// Copyright © 2021 System76 // Copyright © 2021 System76
use crate::*; use crate::*;
use freedesktop_desktop_entry as fde; use freedesktop_desktop_entry::{self as fde, get_languages_from_env};
use futures::StreamExt; use futures::StreamExt;
use pop_launcher::*; use pop_launcher::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -139,7 +139,7 @@ impl<W: AsyncWrite + Unpin> App<W> {
if let Some(name) = path.file_stem() { if let Some(name) = path.file_stem() {
if desktop_entry == name { if desktop_entry == name {
if let Ok(data) = fs::read_to_string(path) { 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() { if let Some(icon) = entry.icon() {
icon_name = Cow::Owned(icon.to_owned()); icon_name = Cow::Owned(icon.to_owned());
} }

View file

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
// Copyright © 2021 System76 // Copyright © 2021 System76
use freedesktop_desktop_entry::get_languages_from_env;
use futures::prelude::*; use futures::prelude::*;
use pop_launcher::*; use pop_launcher::*;
use std::path::PathBuf; use std::path::PathBuf;
@ -123,7 +124,7 @@ fn detect_terminal() -> (PathBuf, &'static str) {
freedesktop_desktop_entry::Iter::new(freedesktop_desktop_entry::default_paths()) freedesktop_desktop_entry::Iter::new(freedesktop_desktop_entry::default_paths())
.filter_map(|path| { .filter_map(|path| {
std::fs::read_to_string(&path).ok().and_then(|input| { 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() if de.no_display()
|| de || de
.categories() .categories()