Skip to content

Commit 2449943

Browse files
authored
improv: use freedesktop_entry to search applications
1 parent 65c1742 commit 2449943

File tree

9 files changed

+146
-99
lines changed

9 files changed

+146
-99
lines changed

.vscode/settings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"remote.containers.dockerPath": "podman",
3-
"rust-analyzer.check.overrideCommand": ["just", "check-json"]
2+
// "remote.containers.dockerPath": "podman",
3+
// "rust-analyzer.check.overrideCommand": ["just", "check-json"]
44
}

Cargo.lock

Lines changed: 11 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

justfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ ID := 'pop-launcher'
22
plugins := 'calc desktop_entries files find pop_shell pulse recent scripts terminal web cosmic_toplevel'
33

44
rootdir := ''
5+
debug := '0'
6+
7+
target-dir := if debug == '1' { 'target/debug' } else { 'target/release' }
58

69
base-dir := if rootdir == '' {
710
env_var('HOME') / '.local'
@@ -57,7 +60,7 @@ install: install-bin install-plugins install-scripts
5760

5861
# Install pop-launcher binary
5962
install-bin:
60-
install -Dm0755 target/release/pop-launcher-bin {{bin-path}}
63+
install -Dm0755 {{target-dir}}/pop-launcher-bin {{bin-path}}
6164

6265
# Install pop-launcher plugins
6366
install-plugins:

plugins/Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ publish = false
99
[dependencies]
1010
async-pidfd = "0.1.4"
1111
fork = "0.1.23"
12-
freedesktop-desktop-entry = "0.5.2"
12+
freedesktop-desktop-entry = "0.6.0"
1313
human_format = "1.1.0"
1414
human-sort = "0.2.2"
1515
new_mime_guess = "4.0.1"
@@ -36,7 +36,9 @@ recently-used-xbel = "1.0.0"
3636

3737
# dependencies cosmic toplevel
3838
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit" }
39-
sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "3bed072", features = ["calloop"] }
39+
sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "3bed072", features = [
40+
"calloop",
41+
] }
4042

4143
# dependencies desktop entries
4244
switcheroo-control = { git = "https://github.com/pop-os/dbus-settings-bindings" }

plugins/src/cosmic_toplevel/mod.rs

Lines changed: 59 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ mod toplevel_handler;
33
use cctk::wayland_client::Proxy;
44
use cctk::{cosmic_protocols, sctk::reexports::calloop, toplevel_info::ToplevelInfo};
55
use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1;
6+
use fde::DesktopEntry;
7+
use freedesktop_desktop_entry as fde;
68

9+
use crate::desktop_entries::utils::get_description;
710
use crate::send;
8-
use freedesktop_desktop_entry as fde;
911
use futures::{
1012
channel::mpsc,
1113
future::{select, Either},
@@ -16,7 +18,6 @@ use pop_launcher::{
1618
Request,
1719
};
1820
use std::borrow::Cow;
19-
use std::{fs, path::PathBuf};
2021
use tokio::io::{AsyncWrite, AsyncWriteExt};
2122

2223
use self::toplevel_handler::{toplevel_handler, ToplevelAction, ToplevelEvent};
@@ -90,7 +91,8 @@ pub async fn main() {
9091
}
9192

9293
struct App<W> {
93-
desktop_entries: Vec<(fde::PathSource, PathBuf)>,
94+
locales: Vec<String>,
95+
desktop_entries: Vec<DesktopEntry<'static>>,
9496
ids_to_ignore: Vec<u32>,
9597
toplevels: Vec<(ZcosmicToplevelHandleV1, ToplevelInfo)>,
9698
calloop_tx: calloop::channel::Sender<ToplevelAction>,
@@ -103,12 +105,19 @@ impl<W: AsyncWrite + Unpin> App<W> {
103105
let (calloop_tx, calloop_rx) = calloop::channel::channel();
104106
let _handle = std::thread::spawn(move || toplevel_handler(toplevels_tx, calloop_rx));
105107

108+
let locales = fde::get_languages_from_env();
109+
110+
let paths = fde::Iter::new(fde::default_paths());
111+
112+
let desktop_entries = DesktopEntry::from_paths(paths, &locales)
113+
.filter_map(|e| e.ok())
114+
.collect::<Vec<_>>();
115+
106116
(
107117
Self {
118+
locales,
119+
desktop_entries,
108120
ids_to_ignore: Vec::new(),
109-
desktop_entries: fde::Iter::new(fde::default_paths())
110-
.map(|path| (fde::PathSource::guess_from(&path), path))
111-
.collect(),
112121
toplevels: Vec::new(),
113122
calloop_tx,
114123
tx,
@@ -150,55 +159,57 @@ impl<W: AsyncWrite + Unpin> App<W> {
150159
}
151160

152161
async fn search(&mut self, query: &str) {
153-
fn contains_pattern(needle: &str, haystack: &[&str]) -> bool {
154-
let needle = needle.to_ascii_lowercase();
155-
haystack.iter().all(|h| needle.contains(h))
156-
}
157-
158162
let query = query.to_ascii_lowercase();
159-
let haystack = query.split_ascii_whitespace().collect::<Vec<&str>>();
160163

161-
for item in &self.toplevels {
162-
let retain = query.is_empty()
163-
|| contains_pattern(&item.1.app_id.to_ascii_lowercase(), &haystack)
164-
|| contains_pattern(&item.1.title.to_ascii_lowercase(), &haystack);
165-
166-
if !retain {
167-
continue;
168-
}
169-
170-
let mut icon_name = Cow::Borrowed("application-x-executable");
171-
172-
for (_, path) in &self.desktop_entries {
173-
if let Ok(data) = fs::read_to_string(&path) {
174-
if let Ok(entry) = fde::DesktopEntry::decode(&path, &data) {
175-
if item.1.app_id == entry.appid
176-
|| entry
177-
.startup_wm_class()
178-
.is_some_and(|class| class == item.1.app_id)
179-
{
180-
if let Some(icon) = entry.icon() {
181-
icon_name = Cow::Owned(icon.to_owned());
182-
}
183-
break;
184-
}
164+
for (handle, info) in &self.toplevels {
165+
let entry = if query.is_empty() {
166+
fde::matching::get_best_match(
167+
&[&info.app_id, &info.title],
168+
&self.desktop_entries,
169+
fde::matching::MatchAppIdOptions::default(),
170+
)
171+
} else {
172+
fde::matching::get_best_match(
173+
&[&info.app_id, &info.title],
174+
&self.desktop_entries,
175+
fde::matching::MatchAppIdOptions::default(),
176+
)
177+
.and_then(|de| {
178+
let score = fde::matching::get_entry_score(
179+
&query,
180+
de,
181+
&self.locales,
182+
&[&info.app_id, &info.title],
183+
);
184+
185+
if score > 0.6 {
186+
Some(de)
187+
} else {
188+
None
185189
}
186-
}
187-
}
190+
})
191+
};
192+
193+
if let Some(de) = entry {
194+
let icon_name = if let Some(icon) = de.icon() {
195+
Cow::Owned(icon.to_owned())
196+
} else {
197+
Cow::Borrowed("application-x-executable")
198+
};
188199

189-
send(
190-
&mut self.tx,
191-
PluginResponse::Append(PluginSearchResult {
200+
let response = PluginResponse::Append(PluginSearchResult {
192201
// XXX protocol id may be re-used later
193-
id: item.0.id().protocol_id(),
194-
window: Some((0, item.0.id().clone().protocol_id())),
195-
name: item.1.app_id.clone(),
196-
description: item.1.title.clone(),
202+
id: handle.id().protocol_id(),
203+
window: Some((0, handle.id().clone().protocol_id())),
204+
// XXX: why this is inversed for this plugin ????
205+
description: info.title.clone(),
206+
name: get_description(de, &self.locales),
197207
icon: Some(IconSource::Name(icon_name)),
198208
..Default::default()
199-
}),
200-
)
201-
.await;
209+
});
210+
211+
send(&mut self.tx, response).await;
212+
}
202213
}
203214

204215
send(&mut self.tx, PluginResponse::Finished).await;

plugins/src/desktop_entries/mod.rs

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@
22
// Copyright © 2021 System76
33

44
use crate::*;
5-
use freedesktop_desktop_entry::{default_paths, DesktopEntry, Iter as DesktopIter, PathSource};
5+
6+
use freedesktop_desktop_entry as fde;
7+
use freedesktop_desktop_entry::{
8+
default_paths, get_languages_from_env, DesktopEntry, Iter as DesktopIter, PathSource,
9+
};
610
use futures::StreamExt;
711
use pop_launcher::*;
812
use std::borrow::Cow;
913
use std::hash::{Hash, Hasher};
1014
use std::path::PathBuf;
1115
use tokio::io::AsyncWrite;
16+
use utils::path_string;
17+
18+
pub(crate) mod utils;
1219

1320
#[derive(Debug, Eq)]
1421
struct Item {
@@ -65,21 +72,16 @@ const EXCLUSIONS: &[&str] = &["GNOME Shell", "Initial Setup"];
6572

6673
struct App<W> {
6774
entries: Vec<Item>,
68-
locale: Option<String>,
75+
locales: Vec<String>,
6976
tx: W,
7077
gpus: Option<Vec<switcheroo_control::Gpu>>,
7178
}
7279

7380
impl<W: AsyncWrite + Unpin> App<W> {
7481
fn new(tx: W) -> Self {
75-
let lang = std::env::var("LANG").ok();
76-
7782
Self {
7883
entries: Vec::new(),
79-
locale: lang
80-
.as_ref()
81-
.and_then(|l| l.split('.').next())
82-
.map(String::from),
84+
locales: fde::get_languages_from_env(),
8385
tx,
8486
gpus: None,
8587
}
@@ -88,8 +90,6 @@ impl<W: AsyncWrite + Unpin> App<W> {
8890
async fn reload(&mut self) {
8991
self.entries.clear();
9092

91-
let locale = self.locale.as_ref().map(String::as_ref);
92-
9393
let mut deduplicator = std::collections::HashSet::new();
9494

9595
let current = current_desktop();
@@ -100,7 +100,9 @@ impl<W: AsyncWrite + Unpin> App<W> {
100100
for path in DesktopIter::new(default_paths()) {
101101
let src = PathSource::guess_from(&path);
102102
if let Ok(bytes) = std::fs::read_to_string(&path) {
103-
if let Ok(entry) = DesktopEntry::decode(&path, &bytes) {
103+
if let Ok(entry) =
104+
DesktopEntry::from_str(&path, &bytes, &get_languages_from_env())
105+
{
104106
// Do not show if our desktop is defined in `NotShowIn`.
105107
if let Some(not_show_in) = entry.desktop_entry("NotShowIn") {
106108
let current = ward::ward!(current.as_ref(), else { continue });
@@ -134,7 +136,7 @@ impl<W: AsyncWrite + Unpin> App<W> {
134136

135137
// Avoid showing the GNOME Shell entry entirely
136138
if entry
137-
.name(None)
139+
.name(&[] as &[&str])
138140
.map_or(false, |v| EXCLUSIONS.contains(&v.as_ref()))
139141
{
140142
continue;
@@ -145,21 +147,21 @@ impl<W: AsyncWrite + Unpin> App<W> {
145147
continue;
146148
}
147149

148-
if let Some((name, exec)) = entry.name(locale).zip(entry.exec()) {
150+
if let Some((name, exec)) = entry.name(&self.locales).zip(entry.exec()) {
149151
if let Some(exec) = exec.split_ascii_whitespace().next() {
150152
if exec == "false" {
151153
continue;
152154
}
153155

154156
let item = Item {
155-
appid: entry.appid.to_owned(),
157+
appid: entry.appid.to_string(),
156158
name: name.to_string(),
157159
description: entry
158-
.comment(locale)
160+
.comment(&self.locales)
159161
.as_deref()
160162
.unwrap_or("")
161163
.to_owned(),
162-
keywords: entry.keywords().map(|keywords| {
164+
keywords: entry.keywords(&self.locales).map(|keywords| {
163165
keywords.split(';').map(String::from).collect()
164166
}),
165167
icon: Some(
@@ -178,7 +180,11 @@ impl<W: AsyncWrite + Unpin> App<W> {
178180
actions
179181
.split(';')
180182
.filter_map(|action| {
181-
entry.action_entry_localized(action, "Name", None)
183+
entry.action_entry_localized(
184+
action,
185+
"Name",
186+
&self.locales,
187+
)
182188
})
183189
.map(Cow::into_owned)
184190
.collect::<Vec<_>>()
@@ -378,20 +384,6 @@ fn current_desktop() -> Option<String> {
378384
})
379385
}
380386

381-
fn path_string(source: &PathSource) -> Cow<'static, str> {
382-
match source {
383-
PathSource::Local | PathSource::LocalDesktop => "Local".into(),
384-
PathSource::LocalFlatpak => "Flatpak".into(),
385-
PathSource::LocalNix => "Nix".into(),
386-
PathSource::Nix => "Nix (System)".into(),
387-
PathSource::System => "System".into(),
388-
PathSource::SystemLocal => "Local (System)".into(),
389-
PathSource::SystemFlatpak => "Flatpak (System)".into(),
390-
PathSource::SystemSnap => "Snap (System)".into(),
391-
PathSource::Other(other) => Cow::Owned(other.clone()),
392-
}
393-
}
394-
395387
async fn try_get_gpus() -> Option<Vec<switcheroo_control::Gpu>> {
396388
let connection = zbus::Connection::system().await.ok()?;
397389
let proxy = switcheroo_control::SwitcherooControlProxy::new(&connection)

0 commit comments

Comments
 (0)