Compare commits

..

No commits in common. "6a41b5f43f7e02cd7e776a9bbd8cf330ef79f48d" and "87bb6c6064535ed5d9bca88139e46580ffba12d3" have entirely different histories.

24 changed files with 1321 additions and 1374 deletions

View file

@ -1,8 +0,0 @@
- [ ] 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.

2077
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.7"
version = "1.2.4"
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 = "3"
resolver = "2"
[workspace.package]
edition = "2024"
edition = "2021"
[workspace.dependencies]
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"
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"
futures = "0.3.31"
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"
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"
[dependencies]
const_format = "0.2.34"
const_format = "0.2.33"
dirs.workspace = true
futures.workspace = true
serde.workspace = true
@ -49,5 +49,9 @@ 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.7"
version = "1.2.4"
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.20", default-features = false, features = [
tracing-subscriber = { version = "0.3.18", default-features = false, features = [
"std",
"fmt",
"env-filter",
"chrono",
] }
tracing-journald = "0.3.1"
tracing-journald = "0.3.0"
dirs.workspace = true
mimalloc = "0.1.48"
mimalloc = "0.1.43"
[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::{EnvFilter, fmt};
use tracing_subscriber::{fmt, EnvFilter};
let logdir = match dirs::state_dir() {
Some(dir) => dir.join("pop-launcher/"),

8
debian/changelog vendored
View file

@ -1,11 +1,3 @@
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.5"
async-pidfd = "0.1.4"
fork = "0.2.0"
freedesktop-desktop-entry = "0.7.19"
freedesktop-desktop-entry = "0.6.2"
human_format = "1.1.0"
human-sort = "0.2.2"
new_mime_guess = "4.0.4"
@ -18,31 +18,32 @@ regex.workspace = true
ron.workspace = true
serde.workspace = true
serde_json.workspace = true
slab = "0.4.11"
slab = "0.4.9"
strsim = "0.11.1"
tracing.workspace = true
urlencoding = "2.1.3"
zbus = "5.14.0"
zvariant = "5.10.0"
url = "2.5.7"
zbus = "4.4.0"
zvariant = "4.2.0"
url = "2.5.2"
sysfs-class = "0.1.3"
anyhow.workspace = true
flume.workspace = true
dirs.workspace = true
futures.workspace = true
bytes = "1.10.1"
bytes = "1.7.2"
recently-used-xbel = "1.1.0"
# dependencies cosmic toplevel
cctk = { path = "../../cosmic-protocols/client-toolkit", package = "cosmic-client-toolkit" }
sctk = { package = "smithay-client-toolkit", version = "0.20.0", features = [
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit" }
sctk = { package = "smithay-client-toolkit", version = "0.19.2", features = [
"calloop",
] }
# dependencies desktop entries
switcheroo-control = { path = "../../dbus-settings-bindings/switcheroo-control" }
switcheroo-control = { git = "https://github.com/pop-os/dbus-settings-bindings" }
[dependencies.reqwest]
version = "0.12.23"
version = "0.12.8"
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,18 +11,19 @@ use tracing::{debug, error, info, warn};
use crate::desktop_entries::utils::{get_description, is_session_cosmic};
use crate::send;
use futures::{
StreamExt,
channel::mpsc,
future::{Either, select},
future::{select, Either},
StreamExt,
};
use pop_launcher::{
IconSource, PluginResponse, PluginSearchResult, Request, async_stdin, async_stdout,
json_input_stream,
async_stdin, async_stdout, json_input_stream, IconSource, PluginResponse, PluginSearchResult,
Request,
};
use std::borrow::Cow;
use std::iter;
use tokio::io::{AsyncWrite, AsyncWriteExt};
use self::toplevel_handler::{ToplevelAction, toplevel_handler};
use self::toplevel_handler::{toplevel_handler, ToplevelAction};
pub async fn main() {
let mut tx = async_stdout();
@ -109,7 +110,7 @@ pub async fn main() {
struct App<W> {
locales: Vec<String>,
desktop_entries: Vec<DesktopEntry>,
desktop_entries: Vec<DesktopEntry<'static>>,
ids_to_ignore: Vec<u32>,
toplevels: Vec<Box<ToplevelInfo>>,
calloop_tx: calloop::channel::Sender<ToplevelAction>,
@ -124,9 +125,10 @@ impl<W: AsyncWrite + Unpin> App<W> {
let locales = fde::get_languages_from_env();
let desktop_entries = fde::Iter::new(fde::default_paths())
.map(|path| DesktopEntry::from_path(path, Some(&locales)))
.filter_map(Result::ok)
let paths = fde::Iter::new(fde::default_paths());
let desktop_entries = DesktopEntry::from_paths(paths, &locales)
.filter_map(|e| e.ok())
.collect::<Vec<_>>();
(
@ -175,49 +177,62 @@ 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 {
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())
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(),
)
} else {
Cow::Borrowed("application-x-executable")
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
}
})
};
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()
});
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")
};
send(&mut self.tx, response).await;
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, PluginResponse::Finished).await;
let _ = self.tx.flush();
let _ = self.tx.flush().await;
}
}

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::{Connection, QueueHandle, globals::registry_queue_init};
use wayland_client::{globals::registry_queue_init, Connection, QueueHandle};
#[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>,
desktop_entries: Vec<DesktopEntry<'static>>,
locales: Vec<String>,
tx: W,
gpus: Option<Vec<switcheroo_control::Gpu>>,
@ -69,85 +69,79 @@ impl<W: AsyncWrite + Unpin> App<W> {
let paths = fde::Iter::new(fde::default_paths());
let desktop_entries = paths
.flat_map(|path| DesktopEntry::from_path(path, Some(&locales)))
let desktop_entries = DesktopEntry::from_paths(paths, &locales)
.filter_map(|de| {
// 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()
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;
}
None => de.appid.as_str(),
};
if deduplicator.contains(appid) {
return None;
}
de.name(&self.locales)?;
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,
}
match de.exec() {
Some(exec) => match exec.split_ascii_whitespace().next() {
Some(exec) => {
if exec == "false" {
// 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;
}
}
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;
}
// 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<_>>();
@ -219,66 +213,33 @@ impl<W: AsyncWrite + Unpin> App<W> {
}
async fn search(&mut self, query: &str) {
let query = query.to_ascii_lowercase();
for (id, entry) in self.desktop_entries.iter().enumerate() {
let score = fde::matching::get_entry_score(query, entry, &self.locales, &[]);
let &mut Self {
ref desktop_entries,
ref locales,
ref mut tx,
..
} = self;
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 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, response).await;
}
}
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,
@ -294,7 +255,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, locales: &[String]) -> String {
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();

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::{fs, path::PathBuf};
use std::{convert::TryFrom, fs, path::PathBuf};
use tokio::io::{AsyncWrite, AsyncWriteExt};
use zbus::Connection;
use zvariant::{Signature, Type};
mod config;
pub use config::{Config, load};
pub use config::{load, Config};
const DEST: &str = "com.System76.PopShell";
const PATH: &str = "/com/System76/PopShell";
@ -26,7 +26,9 @@ struct Item {
}
impl Type for Item {
const SIGNATURE: &'static Signature = <((u32, u32), String, String, String)>::SIGNATURE;
fn signature() -> Signature<'static> {
Signature::try_from("((uu)sss)").expect("bad dbus signature")
}
}
pub async fn main() {
@ -141,7 +143,7 @@ impl<W: AsyncWrite + Unpin> App<W> {
if let Ok(entry) = fde::DesktopEntry::from_str(
path,
&data,
Some(&get_languages_from_env()),
&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::{RecentlyUsed, parse_file};
use recently_used_xbel::{parse_file, RecentlyUsed};
use slab::Slab;
use std::borrow::Cow;

View file

@ -49,7 +49,7 @@ impl App {
None => return,
};
use fork::{Fork, daemon};
use fork::{daemon, Fork};
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, Some(&get_languages_from_env()))
DesktopEntry::from_str(&path, &input, &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::{Config, Definition, load};
pub use config::{load, Config, Definition};
use regex::Regex;
mod config;

View file

@ -1,25 +1,28 @@
[package]
name = "pop-launcher-service"
version = "1.2.7"
version = "1.2.4"
license = "MPL-2.0"
edition.workspace = true
[dependencies]
anyhow.workspace = true
async-oneshot = "0.5.9"
async-trait = "0.1.89"
clap = { version = "4.5.48", features = ["derive"] }
async-trait = "0.1.83"
clap = { version = "4.5.27", features = ["derive"] }
dirs.workspace = true
futures.workspace = true
futures_codec = "0.4.1"
gen-z = "0.1.0"
num_cpus = "1.17.0"
num_cpus = "1.16.0"
pop-launcher = { path = "../" }
regex.workspace = true
ron.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_with = "3.14.1"
slab = "0.4.11"
serde_with = "3.11.0"
slab = "0.4.9"
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::{SinkExt, Stream, StreamExt, future};
use futures::{future, SinkExt, Stream, StreamExt};
use pop_launcher::{
ContextOption, IconSource, Indice, PluginResponse, PluginSearchResult, Request, Response,
SearchResult, json_input_stream, plugin_paths,
json_input_stream, plugin_paths, ContextOption, IconSource, Indice, PluginResponse,
PluginSearchResult, Request, Response, SearchResult,
};
use regex::Regex;
use slab::Slab;
@ -567,6 +567,45 @@ 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,
@ -665,120 +704,3 @@ 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, StreamExt, stream};
use futures::{stream, Stream, StreamExt};
use regex::Regex;
use std::path::PathBuf;

View file

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

View file

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

View file

@ -1,6 +1,6 @@
[package]
name = "pop-launcher-toolkit"
version = "1.2.7"
version = "1.2.4"
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.89"
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.20", default-features = false, features = [
async-trait = "0.1.83"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", default-features = false, features = [
"std",
"fmt",
"env-filter",
] }
dirs = "6.0.0"
dirs = "5.0.1"
futures = "0.3.31"
[dev-dependencies]
tokio = { version = "1", features = ["rt"] }
fork = "0.2.0"
fork = "0.1.23"
[[example]]
name = "man-pages-plugin"

View file

@ -1,13 +1,13 @@
// SPDX-License-Identifier: GPL-3.0-only
// Copyright © 2021 System76
use fork::{Fork, daemon};
use fork::{daemon, Fork};
use pop_launcher::{Indice, PluginResponse, PluginSearchResult};
use pop_launcher_toolkit::plugin_trait::{PluginExt, async_trait};
use pop_launcher_toolkit::plugin_trait::{async_trait, PluginExt};
use std::io;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::{Command, exit};
use std::process::{exit, Command};
// 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::{Indice, PluginResponse, Request, async_stdin, async_stdout, json_input_stream};
use pop_launcher::{async_stdin, async_stdout, json_input_stream, Indice, PluginResponse, Request};
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::{EnvFilter, fmt};
use tracing_subscriber::{fmt, EnvFilter};
fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_writer(file)