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",
"rust-analyzer.check.overrideCommand": ["just", "check-json"]
// "remote.containers.dockerPath": "podman",
// "rust-analyzer.check.overrideCommand": ["just", "check-json"]
}

23
Cargo.lock generated
View file

@ -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"

View file

@ -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:

View file

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

View file

@ -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<W> {
desktop_entries: Vec<(fde::PathSource, PathBuf)>,
locales: Vec<String>,
desktop_entries: Vec<DesktopEntry<'static>>,
ids_to_ignore: Vec<u32>,
toplevels: Vec<(ZcosmicToplevelHandleV1, ToplevelInfo)>,
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 _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 {
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<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 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;

View file

@ -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<W> {
entries: Vec<Item>,
locale: Option<String>,
locales: Vec<String>,
tx: W,
gpus: Option<Vec<switcheroo_control::Gpu>>,
}
impl<W: AsyncWrite + Unpin> App<W> {
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<W: AsyncWrite + Unpin> App<W> {
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<W: AsyncWrite + Unpin> App<W> {
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<W: AsyncWrite + Unpin> App<W> {
// 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<W: AsyncWrite + Unpin> App<W> {
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<W: AsyncWrite + Unpin> App<W> {
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::<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>> {
let connection = zbus::Connection::system().await.ok()?;
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
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<W: AsyncWrite + Unpin> App<W> {
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());
}

View file

@ -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()