Compare commits

...

10 commits

Author SHA1 Message Date
6a41b5f43f chore: use local COSMIC support crates 2026-05-25 17:37:39 +02:00
Jeremy Soller
5b86851071 Add pull request template 2026-02-13 12:35:27 -07:00
Michael Aaron Murphy
eead361cca
release: 1.2.7 2025-10-01 18:37:18 +02:00
Michael Aaron Murphy
904b690c27 fix: flatpaks with multiple entries only show one 2025-10-01 13:49:17 +02:00
Ashley Wulber
092d78f7ca fix: introduce decay factor for keyword weight based on index, and shrink the description match weight.
We could tweak the values to be smaller if this seems to affect anything else.
2025-09-30 13:13:47 -04:00
Michael Aaron Murphy
0e8aa22f97
release: 1.2.6 2025-09-25 19:58:14 +02:00
Vukašin Vojinović
16b5ae74b3 chore: update dependencies
Also removes some unused dependencies.
2025-09-25 11:46:48 -06:00
Vukašin Vojinović
8a0b37bd36 chore: Rust 2024 edition 2025-09-25 11:46:48 -06:00
Michael Aaron Murphy
8d9da92dba fix: update freedesktop-desktop-entry 2025-05-01 14:24:41 +02:00
ellieplayswow
58a8f2db64 Updating cosmic_toplevel plugin to check for an exact appid match, falling back to best match instead 2025-03-24 13:11:27 -06:00
24 changed files with 1390 additions and 1337 deletions

8
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,8 @@
- [ ] I have disclosed use of any AI generated code in my commit messages.
- If you are using an LLM, and do not fully understand the changes it is making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex code that lacks enough context for a proper fix or feature inclusion. This results in considerably longer code reviews. Due to this, AI authored or partially authored PRs may be closed without comment.
- [ ] I understand these changes in full and will be able to respond to review comments.
- [ ] My change is accurately described in the commit message.
- [ ] My contribution is tested and working as described.
- [ ] I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions.

2111
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "pop-launcher"
version = "1.2.4"
version = "1.2.7"
license = "MPL-2.0"
authors = ["Michael Aaron Murphy <michael@mmurphy.dev>"]
description = "Library for writing plugins and frontends for pop-launcher"
@ -9,28 +9,28 @@ edition.workspace = true
[workspace]
members = ["bin", "plugins", "service", "toolkit"]
resolver = "2"
resolver = "3"
[workspace.package]
edition = "2021"
edition = "2024"
[workspace.dependencies]
anyhow = "1.0.90"
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.129"
tracing = "0.1.40"
dirs = "5.0.1"
serde_with = "3.11.0"
anyhow = "1.0.100"
serde = { version = "1.0.226", features = ["derive"] }
serde_json = "1.0.145"
tracing = "0.1.41"
dirs = "6.0.0"
serde_with = "3.14.1"
futures = "0.3.31"
flume = "0.11.0"
toml = "0.8.19"
regex = "1.11.0"
ron = "0.8.1"
tokio = "1.40.0"
tokio-stream = "0.1.16"
flume = "0.11.1"
toml = "0.9.7"
regex = "1.11.2"
ron = "0.11.0"
tokio = "1.47.1"
tokio-stream = "0.1.17"
[dependencies]
const_format = "0.2.33"
const_format = "0.2.34"
dirs.workspace = true
futures.workspace = true
serde.workspace = true
@ -49,9 +49,5 @@ features = ["io-std", "io-util"]
workspace = true
features = ["io-util"]
# [patch.crates-io]
# freedesktop-desktop-entry = { path = "../freedesktop-desktop-entry" }
[patch."https://github.com/pop-os/cosmic-protocols"]
"cosmic-client-toolkit" = { git = "https://github.com/pop-os//cosmic-protocols" }

View file

@ -1,6 +1,6 @@
[package]
name = "pop-launcher-bin"
version = "1.2.4"
version = "1.2.7"
license = "GPL-3.0-only"
edition.workspace = true
publish = false
@ -10,15 +10,15 @@ publish = false
[dependencies]
pop-launcher-toolkit = { path = "../toolkit" }
tracing.workspace = true
tracing-subscriber = { version = "0.3.18", default-features = false, features = [
tracing-subscriber = { version = "0.3.20", default-features = false, features = [
"std",
"fmt",
"env-filter",
"chrono",
] }
tracing-journald = "0.3.0"
tracing-journald = "0.3.1"
dirs.workspace = true
mimalloc = "0.1.43"
mimalloc = "0.1.48"
[dependencies.tokio]
workspace = true

View file

@ -42,7 +42,7 @@ async fn main() {
fn init_logging(cmd: &str) {
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter};
use tracing_subscriber::{EnvFilter, fmt};
let logdir = match dirs::state_dir() {
Some(dir) => dir.join("pop-launcher/"),

8
debian/changelog vendored
View file

@ -1,3 +1,11 @@
pop-launcher (1.2.6) jammy; urgency=medium
* Update all cargo dependencies as of 2025-09-25
* Fixes for some desktop entries
* Fixes for the cosmic beta
-- Michael Murphy <michael@mmurphy.dev> Thu, 25 Sep 2025 19:55:10 +0200
pop-launcher (1.2.4) jammy; urgency=medium
* Update all cargo dependencies as of 2024-10-18

View file

@ -7,9 +7,9 @@ edition.workspace = true
publish = false
[dependencies]
async-pidfd = "0.1.4"
async-pidfd = "0.1.5"
fork = "0.2.0"
freedesktop-desktop-entry = "0.6.2"
freedesktop-desktop-entry = "0.7.19"
human_format = "1.1.0"
human-sort = "0.2.2"
new_mime_guess = "4.0.4"
@ -18,32 +18,31 @@ regex.workspace = true
ron.workspace = true
serde.workspace = true
serde_json.workspace = true
slab = "0.4.9"
slab = "0.4.11"
strsim = "0.11.1"
tracing.workspace = true
urlencoding = "2.1.3"
zbus = "4.4.0"
zvariant = "4.2.0"
url = "2.5.2"
sysfs-class = "0.1.3"
zbus = "5.14.0"
zvariant = "5.10.0"
url = "2.5.7"
anyhow.workspace = true
flume.workspace = true
dirs.workspace = true
futures.workspace = true
bytes = "1.7.2"
bytes = "1.10.1"
recently-used-xbel = "1.1.0"
# dependencies cosmic toplevel
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit" }
sctk = { package = "smithay-client-toolkit", version = "0.19.2", features = [
cctk = { path = "../../cosmic-protocols/client-toolkit", package = "cosmic-client-toolkit" }
sctk = { package = "smithay-client-toolkit", version = "0.20.0", features = [
"calloop",
] }
# dependencies desktop entries
switcheroo-control = { git = "https://github.com/pop-os/dbus-settings-bindings" }
switcheroo-control = { path = "../../dbus-settings-bindings/switcheroo-control" }
[dependencies.reqwest]
version = "0.12.8"
version = "0.12.23"
default-features = false
features = ["rustls-tls"]

View file

@ -139,7 +139,7 @@ async fn qcalc(regex: &mut Regex, expression: &str, decimal_comma: bool) -> Opti
String::from("qalc command is not installed")
} else {
format!("qalc command failed to spawn: {}", why)
})
});
}
};

View file

@ -11,19 +11,18 @@ use tracing::{debug, error, info, warn};
use crate::desktop_entries::utils::{get_description, is_session_cosmic};
use crate::send;
use futures::{
channel::mpsc,
future::{select, Either},
StreamExt,
channel::mpsc,
future::{Either, select},
};
use pop_launcher::{
async_stdin, async_stdout, json_input_stream, IconSource, PluginResponse, PluginSearchResult,
Request,
IconSource, PluginResponse, PluginSearchResult, Request, async_stdin, async_stdout,
json_input_stream,
};
use std::borrow::Cow;
use std::iter;
use tokio::io::{AsyncWrite, AsyncWriteExt};
use self::toplevel_handler::{toplevel_handler, ToplevelAction};
use self::toplevel_handler::{ToplevelAction, toplevel_handler};
pub async fn main() {
let mut tx = async_stdout();
@ -110,7 +109,7 @@ pub async fn main() {
struct App<W> {
locales: Vec<String>,
desktop_entries: Vec<DesktopEntry<'static>>,
desktop_entries: Vec<DesktopEntry>,
ids_to_ignore: Vec<u32>,
toplevels: Vec<Box<ToplevelInfo>>,
calloop_tx: calloop::channel::Sender<ToplevelAction>,
@ -125,10 +124,9 @@ impl<W: AsyncWrite + Unpin> App<W> {
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())
let desktop_entries = fde::Iter::new(fde::default_paths())
.map(|path| DesktopEntry::from_path(path, Some(&locales)))
.filter_map(Result::ok)
.collect::<Vec<_>>();
(
@ -177,62 +175,49 @@ 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 info in self.toplevels.iter().rev() {
let entry = if query.is_empty() {
fde::matching::get_best_match(
&[&info.app_id, &info.title],
&self.desktop_entries,
fde::matching::MatchAppIdOptions::default(),
)
for info in &self.toplevels {
let retain = query.is_empty()
|| contains_pattern(&info.app_id, &haystack)
|| contains_pattern(&info.title, &haystack);
if !retain {
continue;
}
let appid = fde::unicase::Ascii::new(info.app_id.as_str());
let entry = fde::find_app_by_id(&self.desktop_entries, appid)
.map(ToOwned::to_owned)
.unwrap_or_else(|| fde::DesktopEntry::from_appid(appid.to_string()).to_owned());
let icon_name = if let Some(icon) = entry.icon() {
Cow::Owned(icon.to_owned())
} else {
let lowercase_title = info.title.to_lowercase();
let window_words = lowercase_title
.split_whitespace()
.chain(iter::once(info.app_id.as_str()))
.chain(iter::once(info.title.as_str()))
.collect::<Vec<_>>();
fde::matching::get_best_match(
&window_words,
&self.desktop_entries,
fde::matching::MatchAppIdOptions::default(),
)
.and_then(|de| {
let score =
fde::matching::get_entry_score(&query, de, &self.locales, &window_words);
if score > 0.8 {
Some(de)
} else {
None
}
})
Cow::Borrowed("application-x-executable")
};
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: info.foreign_toplevel.id().protocol_id(),
window: Some((0, info.foreign_toplevel.id().protocol_id())),
description: info.title.clone(),
name: get_description(&entry, &self.locales),
icon: Some(IconSource::Name(icon_name)),
..Default::default()
});
let response = PluginResponse::Append(PluginSearchResult {
// XXX protocol id may be re-used later
id: info.foreign_toplevel.id().protocol_id(),
window: Some((0, info.foreign_toplevel.id().protocol_id())),
description: info.title.clone(),
name: get_description(de, &self.locales),
icon: Some(IconSource::Name(icon_name)),
..Default::default()
});
send(&mut self.tx, response).await;
}
send(&mut self.tx, response).await;
}
send(&mut self.tx, PluginResponse::Finished).await;
let _ = self.tx.flush().await;
let _ = self.tx.flush();
}
}

View file

@ -22,7 +22,7 @@ use cosmic_protocols::{
use futures::channel::mpsc::UnboundedSender;
use sctk::registry::{ProvidesRegistryState, RegistryState};
use tracing::warn;
use wayland_client::{globals::registry_queue_init, Connection, QueueHandle};
use wayland_client::{Connection, QueueHandle, globals::registry_queue_init};
#[derive(Debug, Clone)]
pub enum ToplevelAction {

View file

@ -43,7 +43,7 @@ const EXCLUSIONS: &[&str] = &["GNOME Shell", "Initial Setup"];
struct App<W> {
current_desktop: Option<Vec<String>>,
is_desktop_cosmic: bool,
desktop_entries: Vec<DesktopEntry<'static>>,
desktop_entries: Vec<DesktopEntry>,
locales: Vec<String>,
tx: W,
gpus: Option<Vec<switcheroo_control::Gpu>>,
@ -69,79 +69,85 @@ impl<W: AsyncWrite + Unpin> App<W> {
let paths = fde::Iter::new(fde::default_paths());
let desktop_entries = DesktopEntry::from_paths(paths, &locales)
let desktop_entries = paths
.flat_map(|path| DesktopEntry::from_path(path, Some(&locales)))
.filter_map(|de| {
de.ok().and_then(|de| {
// Treat Flatpak and system apps differently in the cache so they don't
// override each other
let appid = de.flatpak().unwrap_or_else(|| de.appid.as_ref());
if deduplicator.contains(appid) {
return None;
// Cache flatpak apps separately from non-flatpak apps.
let _flatpak_appid;
let appid = match de.flatpak() {
Some(base_id) => {
_flatpak_appid = [base_id, ".", de.appid.as_str()].concat();
_flatpak_appid.as_str()
}
None => de.appid.as_str(),
};
de.name(&self.locales)?;
if deduplicator.contains(appid) {
return None;
}
match de.exec() {
Some(exec) => match exec.split_ascii_whitespace().next() {
Some(exec) => {
if exec == "false" {
return None;
}
de.name(&self.locales)?;
match de.exec() {
Some(exec) => match exec.split_ascii_whitespace().next() {
Some(exec) => {
if exec == "false" {
return None;
}
None => return None,
},
}
None => return None,
}
},
None => return None,
}
// Avoid showing the GNOME Shell entry entirely
if de
.name(&[] as &[&str])
.map_or(false, |v| EXCLUSIONS.contains(&v.as_ref()))
{
return None;
}
// Avoid showing the GNOME Shell entry entirely
if de
.name(&[] as &[&str])
.map_or(false, |v| EXCLUSIONS.contains(&v.as_ref()))
{
return None;
}
// Do not show if our desktop is defined in `NotShowIn`.
if let Some(not_show_in) = de.not_show_in() {
if let Some(current_desktop) = &self.current_desktop {
if not_show_in.iter().any(|not_show| {
current_desktop
.iter()
.any(|desktop| &not_show.to_ascii_lowercase() == desktop)
}) {
return None;
}
// Do not show if our desktop is defined in `NotShowIn`.
if let Some(not_show_in) = de.not_show_in() {
if let Some(current_desktop) = &self.current_desktop {
if not_show_in.iter().any(|not_show| {
current_desktop
.iter()
.any(|desktop| &not_show.to_ascii_lowercase() == desktop)
}) {
return None;
}
}
}
// Do not show if our desktop is not defined in `OnlyShowIn`.
if let Some(only_show_in) = de.only_show_in() {
if let Some(current_desktop) = &self.current_desktop {
if !only_show_in.iter().any(|show_in| {
current_desktop
.iter()
.any(|desktop| &show_in.to_ascii_lowercase() == desktop)
}) {
return None;
}
// Do not show if our desktop is not defined in `OnlyShowIn`.
if let Some(only_show_in) = de.only_show_in() {
if let Some(current_desktop) = &self.current_desktop {
if !only_show_in.iter().any(|show_in| {
current_desktop
.iter()
.any(|desktop| &show_in.to_ascii_lowercase() == desktop)
}) {
return None;
}
}
// Treat `OnlyShowIn` as an override otherwise do not show if `NoDisplay` is true
// Some desktop environments set `OnlyShowIn` and `NoDisplay = true` to
// indicate special entries
else if de.no_display() {
return None;
}
}
// Treat `OnlyShowIn` as an override otherwise do not show if `NoDisplay` is true
// Some desktop environments set `OnlyShowIn` and `NoDisplay = true` to
// indicate special entries
else if de.no_display() {
return None;
}
// Always cache already visited entries to allow overriding entries e.g. by
// placing a modified copy in ~/.local/share/applications/
//
// We only do this when we can add an entry to our list, otherwise we risk
// ignoring user overrides or valid applications due to shell URL handlers
deduplicator.insert(appid.to_owned());
// Always cache already visited entries to allow overriding entries e.g. by
// placing a modified copy in ~/.local/share/applications/
//
// We only do this when we can add an entry to our list, otherwise we risk
// ignoring user overrides or valid applications due to shell URL handlers
deduplicator.insert(appid.to_owned());
Some(de)
})
Some(de)
})
.collect::<Vec<_>>();
@ -213,33 +219,66 @@ impl<W: AsyncWrite + Unpin> App<W> {
}
async fn search(&mut self, query: &str) {
for (id, entry) in self.desktop_entries.iter().enumerate() {
let score = fde::matching::get_entry_score(query, entry, &self.locales, &[]);
let query = query.to_ascii_lowercase();
if score > 0.6 {
let response = PluginResponse::Append(PluginSearchResult {
id: id as u32,
name: entry.name(&self.locales).unwrap_or_default().to_string(),
description: get_description(entry, &self.locales),
keywords: entry
.keywords(&self.locales)
.map(|v| v.iter().map(|e| e.to_string()).collect()),
icon: entry
.icon()
.map(|e| Cow::Owned(e.to_string()))
.map(IconSource::Name),
exec: entry.exec().map(|e| e.to_string()),
..Default::default()
});
let &mut Self {
ref desktop_entries,
ref locales,
ref mut tx,
..
} = self;
send(&mut self.tx, response).await;
let mut items = Vec::with_capacity(16);
for (id, entry) in desktop_entries.iter().enumerate() {
let name = entry.name(locales).unwrap_or_default();
let keywords = entry.keywords(locales);
items.extend(name.split_ascii_whitespace().map(ToOwned::to_owned));
if let Some(keywords) = keywords.as_ref() {
items.extend(keywords.iter().map(|x| String::from(x.as_ref())));
}
if let Some(exec) = entry.exec() {
items.push(exec.to_owned());
}
for search_interest in items.drain(..) {
let search_interest = search_interest.to_ascii_lowercase();
let append = search_interest.starts_with(&*query)
|| query
.split_ascii_whitespace()
.any(|query| search_interest.contains(&*query))
|| strsim::jaro_winkler(&*query, &*search_interest) > 0.6;
if append {
let response = PluginResponse::Append(PluginSearchResult {
id: id as u32,
name: entry.name(&self.locales).unwrap_or_default().to_string(),
description: get_description(entry, &self.locales),
keywords: entry
.keywords(&self.locales)
.map(|v| v.iter().map(|e| e.to_string()).collect()),
icon: entry
.icon()
.map(|e| Cow::Owned(e.to_string()))
.map(IconSource::Name),
exec: entry.exec().map(|e| e.to_string()),
..Default::default()
});
send(tx, response).await;
break;
}
}
}
send(&mut self.tx, PluginResponse::Finished).await;
let _ = self.tx.flush();
}
async fn gnome_context(&self, entry: &DesktopEntry<'_>) -> Vec<ContextOption> {
async fn gnome_context(&self, entry: &DesktopEntry) -> Vec<ContextOption> {
if self.gpus.is_some() {
vec![ContextOption {
id: 0,
@ -255,7 +294,7 @@ impl<W: AsyncWrite + Unpin> App<W> {
}
}
async fn cosmic_context(&self, entry: &DesktopEntry<'_>) -> Vec<ContextOption> {
async fn cosmic_context(&self, entry: &DesktopEntry) -> Vec<ContextOption> {
let mut options = Vec::new();
if let Some(gpus) = self.gpus.as_ref() {

View file

@ -20,7 +20,7 @@ pub fn path_string(source: &PathSource) -> Cow<'static, str> {
}
}
pub fn get_description<'a>(de: &'a DesktopEntry<'a>, locales: &[String]) -> String {
pub fn get_description<'a>(de: &'a DesktopEntry, locales: &[String]) -> String {
let path_source = PathSource::guess_from(&de.path);
let desc_source = path_string(&path_source).to_string();

View file

@ -6,13 +6,13 @@ use freedesktop_desktop_entry::{self as fde, get_languages_from_env};
use futures::StreamExt;
use pop_launcher::*;
use serde::{Deserialize, Serialize};
use std::{convert::TryFrom, fs, path::PathBuf};
use std::{fs, path::PathBuf};
use tokio::io::{AsyncWrite, AsyncWriteExt};
use zbus::Connection;
use zvariant::{Signature, Type};
mod config;
pub use config::{load, Config};
pub use config::{Config, load};
const DEST: &str = "com.System76.PopShell";
const PATH: &str = "/com/System76/PopShell";
@ -26,9 +26,7 @@ struct Item {
}
impl Type for Item {
fn signature() -> Signature<'static> {
Signature::try_from("((uu)sss)").expect("bad dbus signature")
}
const SIGNATURE: &'static Signature = <((u32, u32), String, String, String)>::SIGNATURE;
}
pub async fn main() {
@ -143,7 +141,7 @@ impl<W: AsyncWrite + Unpin> App<W> {
if let Ok(entry) = fde::DesktopEntry::from_str(
path,
&data,
&get_languages_from_env(),
Some(&get_languages_from_env()),
) {
if let Some(icon) = entry.icon() {
icon_name = Cow::Owned(icon.to_owned());

View file

@ -3,7 +3,7 @@
use futures::prelude::*;
use pop_launcher::*;
use recently_used_xbel::{parse_file, RecentlyUsed};
use recently_used_xbel::{RecentlyUsed, parse_file};
use slab::Slab;
use std::borrow::Cow;

View file

@ -49,7 +49,7 @@ impl App {
None => return,
};
use fork::{daemon, Fork};
use fork::{Fork, daemon};
crate::send(&mut self.out, PluginResponse::Close).await;
@ -124,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::from_str(&path, &input, &get_languages_from_env())
DesktopEntry::from_str(&path, &input, Some(&get_languages_from_env()))
.ok()
.and_then(|de| {
if de.no_display()

View file

@ -12,7 +12,7 @@ use url::Url;
use pop_launcher::*;
pub use config::{load, Config, Definition};
pub use config::{Config, Definition, load};
use regex::Regex;
mod config;

View file

@ -1,28 +1,25 @@
[package]
name = "pop-launcher-service"
version = "1.2.4"
version = "1.2.7"
license = "MPL-2.0"
edition.workspace = true
[dependencies]
anyhow.workspace = true
async-oneshot = "0.5.9"
async-trait = "0.1.83"
clap = { version = "4.5.27", features = ["derive"] }
async-trait = "0.1.89"
clap = { version = "4.5.48", features = ["derive"] }
dirs.workspace = true
futures.workspace = true
futures_codec = "0.4.1"
gen-z = "0.1.0"
num_cpus = "1.16.0"
num_cpus = "1.17.0"
pop-launcher = { path = "../" }
regex.workspace = true
ron.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_with = "3.11.0"
slab = "0.4.9"
serde_with = "3.14.1"
slab = "0.4.11"
strsim = "0.11.1"
toml.workspace = true
tracing.workspace = true
flume.workspace = true

View file

@ -17,10 +17,10 @@ use crate::priority::Priority;
use crate::recent::RecentUseStorage;
use clap::Parser;
use flume::{Receiver, Sender};
use futures::{future, SinkExt, Stream, StreamExt};
use futures::{SinkExt, Stream, StreamExt, future};
use pop_launcher::{
json_input_stream, plugin_paths, ContextOption, IconSource, Indice, PluginResponse,
PluginSearchResult, Request, Response, SearchResult,
ContextOption, IconSource, Indice, PluginResponse, PluginSearchResult, Request, Response,
SearchResult, json_input_stream, plugin_paths,
};
use regex::Regex;
use slab::Slab;
@ -567,45 +567,6 @@ impl<O: futures::Sink<Response> + Unpin> Service<O> {
} else {
active_search.sort_by(|a, b| {
// Weight is calculated between 0.0 and 1.0, with higher values being most similar
fn calculate_weight(meta: &PluginSearchResult, query: &str) -> f64 {
let mut weight: f64 = 0.0;
let name = meta.name.to_ascii_lowercase();
let description = meta.description.to_ascii_lowercase();
let exec = meta
.exec
.as_ref()
.map(|exec| exec.to_ascii_lowercase())
.unwrap_or_default();
for name in name.split_ascii_whitespace().flat_map(|x| x.split('_')) {
if name.starts_with(query) {
return 1.0;
}
}
if exec.contains(query) {
if exec.starts_with(query) {
return 1.0;
}
weight = strsim::jaro_winkler(query, &exec) - 0.1;
}
weight
.max(strsim::jaro_winkler(&name, query))
.max(strsim::jaro_winkler(&description, query) - 0.1)
.max(match meta.keywords.as_ref() {
Some(keywords) => keywords
.iter()
.flat_map(|word| word.split_ascii_whitespace())
.fold(0.0, |acc, keyword| {
let keyword = keyword.to_ascii_lowercase();
acc.max(strsim::jaro_winkler(query, &keyword) - 0.1)
}),
None => 0.0,
})
}
let plug1 = match plugins.get(a.0) {
Some(plug) => plug,
@ -704,3 +665,120 @@ fn serialize_out<E: serde::Serialize>(output: &mut io::StdoutLock, event: &E) {
let _res = output.write_all(&vec);
}
}
fn calculate_weight(meta: &PluginSearchResult, query: &str) -> f64 {
let mut weight: f64 = 0.0;
let name = meta.name.to_ascii_lowercase();
let description = meta.description.to_ascii_lowercase();
let exec = meta
.exec
.as_ref()
.map(|exec| exec.to_ascii_lowercase())
.unwrap_or_default();
for name in name.split_ascii_whitespace().flat_map(|x| x.split('_')) {
if name.starts_with(query) {
return 1.0;
}
}
if exec.contains(query) {
if exec.starts_with(query) {
return 1.0;
}
weight = strsim::jaro_winkler(query, &exec) - 0.1;
}
weight
.max(strsim::jaro_winkler(&name, query))
.max(match meta.keywords.as_ref() {
Some(keywords) => keywords
.iter()
.flat_map(|word| word.split_ascii_whitespace())
.enumerate()
.fold(0.0, |acc, (i, keyword)| {
let keyword = keyword.to_ascii_lowercase();
let mut v = acc.max(strsim::jaro_winkler(query, &keyword) - 0.1);
// small decay factor for keywords later in the list.
v *= (90. + 10usize.saturating_sub(i) as f64) / 100.;
v
}),
None => 0.0,
})
// deprioritize description matches the most
.max(strsim::jaro_winkler(&description, query) * 0.9 - 0.1)
}
#[cfg(test)]
mod tests {
use super::*;
use pop_launcher::PluginSearchResult;
#[test]
fn test_script_calculate_weight() {
// Test queries for each of the prompt's entries
let entries = vec![
(
"Enter BIOS",
PluginSearchResult {
id: 0,
name: "Enter BIOS".to_string(),
description: "Reboot into BIOS".to_string(),
icon: None,
window: None,
exec: None,
keywords: Some(vec![
"bios".to_string(),
"uefi".to_string(),
"reboot".to_string(),
"restart".to_string(),
]),
},
),
(
"Restart",
PluginSearchResult {
id: 3,
name: "Restart".to_string(),
description: "Reboot the system".to_string(),
icon: None,
window: None,
exec: None,
keywords: Some(vec![
"power".to_string(),
"reboot".to_string(),
"restart".to_string(),
]),
},
),
];
let query_reboot = "reboot";
let weights_reboot: Vec<f64> = entries
.iter()
.map(|(_, entry)| calculate_weight(entry, query_reboot))
.collect();
let idx_restart = entries.iter().position(|(n, _)| *n == "Restart").unwrap();
let idx_bios = entries
.iter()
.position(|(n, _)| *n == "Enter BIOS")
.unwrap();
assert!(
weights_reboot[idx_restart] > weights_reboot[idx_bios],
"Restart should be top for 'reboot', then Enter BIOS"
);
assert!(
weights_reboot[idx_restart] >= 0.85,
"Restart should be high for 'reboot'"
);
assert!(
weights_reboot[idx_bios] >= 0.85,
"Enter BIOS should be high for 'reboot'"
);
}
}

View file

@ -3,7 +3,7 @@
use crate::PluginConfig;
use futures::{stream, Stream, StreamExt};
use futures::{Stream, StreamExt, stream};
use regex::Regex;
use std::path::PathBuf;

View file

@ -8,8 +8,8 @@ use std::{
path::PathBuf,
process::Stdio,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
atomic::{AtomicBool, Ordering},
},
};
@ -22,7 +22,7 @@ use tokio::{
process::{Child, Command},
task::JoinHandle,
};
use tracing::{event, Level};
use tracing::{Level, event};
pub struct ExternalPlugin {
id: usize,

View file

@ -1,5 +1,5 @@
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::{hash_map::DefaultHasher, HashMap};
use std::collections::{HashMap, hash_map::DefaultHasher};
use std::hash::{Hash, Hasher};
const SHORTTERM_CAP: usize = 20;

View file

@ -1,6 +1,6 @@
[package]
name = "pop-launcher-toolkit"
version = "1.2.4"
version = "1.2.7"
description = "A wrapper around pop-launcher, pop-launcher-service and pop-launcher-plugins types for writing plugins and frontends for pop-launcher."
edition.workspace = true
@ -10,19 +10,19 @@ edition.workspace = true
pop-launcher-plugins = { path = "../plugins" }
pop-launcher-service = { path = "../service" }
pop-launcher = { path = "../" }
async-trait = "0.1.83"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", default-features = false, features = [
async-trait = "0.1.89"
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.20", default-features = false, features = [
"std",
"fmt",
"env-filter",
] }
dirs = "5.0.1"
dirs = "6.0.0"
futures = "0.3.31"
[dev-dependencies]
tokio = { version = "1", features = ["rt"] }
fork = "0.1.23"
fork = "0.2.0"
[[example]]
name = "man-pages-plugin"

View file

@ -1,13 +1,13 @@
// SPDX-License-Identifier: GPL-3.0-only
// Copyright © 2021 System76
use fork::{daemon, Fork};
use fork::{Fork, daemon};
use pop_launcher::{Indice, PluginResponse, PluginSearchResult};
use pop_launcher_toolkit::plugin_trait::{async_trait, PluginExt};
use pop_launcher_toolkit::plugin_trait::{PluginExt, async_trait};
use std::io;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::{exit, Command};
use std::process::{Command, exit};
// This example demonstrate how to write a pop-launcher plugin using the `PluginExt` helper trait.
// We are going to build a plugin to display man pages descriptions and open them on activation.

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: MPL-2.0
use futures::StreamExt;
use pop_launcher::{async_stdin, async_stdout, json_input_stream, Indice, PluginResponse, Request};
use pop_launcher::{Indice, PluginResponse, Request, async_stdin, async_stdout, json_input_stream};
pub use async_trait::async_trait;
use pop_launcher_plugins as plugins;
@ -122,7 +122,7 @@ where
);
if let Ok(file) = logfile {
use tracing_subscriber::{fmt, EnvFilter};
use tracing_subscriber::{EnvFilter, fmt};
fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_writer(file)