improv: use freedesktop_entry to search applications
This commit is contained in:
parent
65c1742a88
commit
2449943863
9 changed files with 146 additions and 99 deletions
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
|
@ -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
23
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
5
justfile
5
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:
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
39
plugins/src/desktop_entries/utils.rs
Normal file
39
plugins/src/desktop_entries/utils.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue