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] [package]
name = "pop-launcher" name = "pop-launcher"
version = "1.2.4" version = "1.2.7"
license = "MPL-2.0" license = "MPL-2.0"
authors = ["Michael Aaron Murphy <michael@mmurphy.dev>"] authors = ["Michael Aaron Murphy <michael@mmurphy.dev>"]
description = "Library for writing plugins and frontends for pop-launcher" description = "Library for writing plugins and frontends for pop-launcher"
@ -9,28 +9,28 @@ edition.workspace = true
[workspace] [workspace]
members = ["bin", "plugins", "service", "toolkit"] members = ["bin", "plugins", "service", "toolkit"]
resolver = "2" resolver = "3"
[workspace.package] [workspace.package]
edition = "2021" edition = "2024"
[workspace.dependencies] [workspace.dependencies]
anyhow = "1.0.90" anyhow = "1.0.100"
serde = { version = "1.0.210", features = ["derive"] } serde = { version = "1.0.226", features = ["derive"] }
serde_json = "1.0.129" serde_json = "1.0.145"
tracing = "0.1.40" tracing = "0.1.41"
dirs = "5.0.1" dirs = "6.0.0"
serde_with = "3.11.0" serde_with = "3.14.1"
futures = "0.3.31" futures = "0.3.31"
flume = "0.11.0" flume = "0.11.1"
toml = "0.8.19" toml = "0.9.7"
regex = "1.11.0" regex = "1.11.2"
ron = "0.8.1" ron = "0.11.0"
tokio = "1.40.0" tokio = "1.47.1"
tokio-stream = "0.1.16" tokio-stream = "0.1.17"
[dependencies] [dependencies]
const_format = "0.2.33" const_format = "0.2.34"
dirs.workspace = true dirs.workspace = true
futures.workspace = true futures.workspace = true
serde.workspace = true serde.workspace = true
@ -49,9 +49,5 @@ features = ["io-std", "io-util"]
workspace = true workspace = true
features = ["io-util"] features = ["io-util"]
# [patch.crates-io] # [patch.crates-io]
# freedesktop-desktop-entry = { path = "../freedesktop-desktop-entry" } # 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] [package]
name = "pop-launcher-bin" name = "pop-launcher-bin"
version = "1.2.4" version = "1.2.7"
license = "GPL-3.0-only" license = "GPL-3.0-only"
edition.workspace = true edition.workspace = true
publish = false publish = false
@ -10,15 +10,15 @@ publish = false
[dependencies] [dependencies]
pop-launcher-toolkit = { path = "../toolkit" } pop-launcher-toolkit = { path = "../toolkit" }
tracing.workspace = true tracing.workspace = true
tracing-subscriber = { version = "0.3.18", default-features = false, features = [ tracing-subscriber = { version = "0.3.20", default-features = false, features = [
"std", "std",
"fmt", "fmt",
"env-filter", "env-filter",
"chrono", "chrono",
] } ] }
tracing-journald = "0.3.0" tracing-journald = "0.3.1"
dirs.workspace = true dirs.workspace = true
mimalloc = "0.1.43" mimalloc = "0.1.48"
[dependencies.tokio] [dependencies.tokio]
workspace = true workspace = true

View file

@ -42,7 +42,7 @@ async fn main() {
fn init_logging(cmd: &str) { fn init_logging(cmd: &str) {
use tracing_subscriber::prelude::*; use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter}; use tracing_subscriber::{EnvFilter, fmt};
let logdir = match dirs::state_dir() { let logdir = match dirs::state_dir() {
Some(dir) => dir.join("pop-launcher/"), 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 pop-launcher (1.2.4) jammy; urgency=medium
* Update all cargo dependencies as of 2024-10-18 * Update all cargo dependencies as of 2024-10-18

View file

@ -7,9 +7,9 @@ edition.workspace = true
publish = false publish = false
[dependencies] [dependencies]
async-pidfd = "0.1.4" async-pidfd = "0.1.5"
fork = "0.2.0" fork = "0.2.0"
freedesktop-desktop-entry = "0.6.2" freedesktop-desktop-entry = "0.7.19"
human_format = "1.1.0" human_format = "1.1.0"
human-sort = "0.2.2" human-sort = "0.2.2"
new_mime_guess = "4.0.4" new_mime_guess = "4.0.4"
@ -18,32 +18,31 @@ regex.workspace = true
ron.workspace = true ron.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
slab = "0.4.9" slab = "0.4.11"
strsim = "0.11.1" strsim = "0.11.1"
tracing.workspace = true tracing.workspace = true
urlencoding = "2.1.3" urlencoding = "2.1.3"
zbus = "4.4.0" zbus = "5.14.0"
zvariant = "4.2.0" zvariant = "5.10.0"
url = "2.5.2" url = "2.5.7"
sysfs-class = "0.1.3"
anyhow.workspace = true anyhow.workspace = true
flume.workspace = true flume.workspace = true
dirs.workspace = true dirs.workspace = true
futures.workspace = true futures.workspace = true
bytes = "1.7.2" bytes = "1.10.1"
recently-used-xbel = "1.1.0" recently-used-xbel = "1.1.0"
# dependencies cosmic toplevel # dependencies cosmic toplevel
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit" } cctk = { path = "../../cosmic-protocols/client-toolkit", package = "cosmic-client-toolkit" }
sctk = { package = "smithay-client-toolkit", version = "0.19.2", features = [ sctk = { package = "smithay-client-toolkit", version = "0.20.0", features = [
"calloop", "calloop",
] } ] }
# dependencies desktop entries # dependencies desktop entries
switcheroo-control = { git = "https://github.com/pop-os/dbus-settings-bindings" } switcheroo-control = { path = "../../dbus-settings-bindings/switcheroo-control" }
[dependencies.reqwest] [dependencies.reqwest]
version = "0.12.8" version = "0.12.23"
default-features = false default-features = false
features = ["rustls-tls"] 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") String::from("qalc command is not installed")
} else { } else {
format!("qalc command failed to spawn: {}", why) 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::desktop_entries::utils::{get_description, is_session_cosmic};
use crate::send; use crate::send;
use futures::{ use futures::{
channel::mpsc,
future::{select, Either},
StreamExt, StreamExt,
channel::mpsc,
future::{Either, select},
}; };
use pop_launcher::{ use pop_launcher::{
async_stdin, async_stdout, json_input_stream, IconSource, PluginResponse, PluginSearchResult, IconSource, PluginResponse, PluginSearchResult, Request, async_stdin, async_stdout,
Request, json_input_stream,
}; };
use std::borrow::Cow; use std::borrow::Cow;
use std::iter;
use tokio::io::{AsyncWrite, AsyncWriteExt}; use tokio::io::{AsyncWrite, AsyncWriteExt};
use self::toplevel_handler::{toplevel_handler, ToplevelAction}; use self::toplevel_handler::{ToplevelAction, toplevel_handler};
pub async fn main() { pub async fn main() {
let mut tx = async_stdout(); let mut tx = async_stdout();
@ -110,7 +109,7 @@ pub async fn main() {
struct App<W> { struct App<W> {
locales: Vec<String>, locales: Vec<String>,
desktop_entries: Vec<DesktopEntry<'static>>, desktop_entries: Vec<DesktopEntry>,
ids_to_ignore: Vec<u32>, ids_to_ignore: Vec<u32>,
toplevels: Vec<Box<ToplevelInfo>>, toplevels: Vec<Box<ToplevelInfo>>,
calloop_tx: calloop::channel::Sender<ToplevelAction>, calloop_tx: calloop::channel::Sender<ToplevelAction>,
@ -125,10 +124,9 @@ impl<W: AsyncWrite + Unpin> App<W> {
let locales = fde::get_languages_from_env(); let locales = fde::get_languages_from_env();
let paths = fde::Iter::new(fde::default_paths()); let desktop_entries = fde::Iter::new(fde::default_paths())
.map(|path| DesktopEntry::from_path(path, Some(&locales)))
let desktop_entries = DesktopEntry::from_paths(paths, &locales) .filter_map(Result::ok)
.filter_map(|e| e.ok())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
( (
@ -177,62 +175,49 @@ impl<W: AsyncWrite + Unpin> App<W> {
} }
async fn search(&mut self, query: &str) { 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 query = query.to_ascii_lowercase();
let haystack = query.split_ascii_whitespace().collect::<Vec<&str>>();
for info in self.toplevels.iter().rev() { for info in &self.toplevels {
let entry = if query.is_empty() { let retain = query.is_empty()
fde::matching::get_best_match( || contains_pattern(&info.app_id, &haystack)
&[&info.app_id, &info.title], || contains_pattern(&info.title, &haystack);
&self.desktop_entries,
fde::matching::MatchAppIdOptions::default(), 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 { } else {
let lowercase_title = info.title.to_lowercase(); Cow::Borrowed("application-x-executable")
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
}
})
}; };
if let Some(de) = entry { let response = PluginResponse::Append(PluginSearchResult {
let icon_name = if let Some(icon) = de.icon() { // XXX protocol id may be re-used later
Cow::Owned(icon.to_owned()) id: info.foreign_toplevel.id().protocol_id(),
} else { window: Some((0, info.foreign_toplevel.id().protocol_id())),
Cow::Borrowed("application-x-executable") description: info.title.clone(),
}; name: get_description(&entry, &self.locales),
icon: Some(IconSource::Name(icon_name)),
..Default::default()
});
let response = PluginResponse::Append(PluginSearchResult { send(&mut self.tx, response).await;
// 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; 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 futures::channel::mpsc::UnboundedSender;
use sctk::registry::{ProvidesRegistryState, RegistryState}; use sctk::registry::{ProvidesRegistryState, RegistryState};
use tracing::warn; use tracing::warn;
use wayland_client::{globals::registry_queue_init, Connection, QueueHandle}; use wayland_client::{Connection, QueueHandle, globals::registry_queue_init};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ToplevelAction { pub enum ToplevelAction {

View file

@ -43,7 +43,7 @@ const EXCLUSIONS: &[&str] = &["GNOME Shell", "Initial Setup"];
struct App<W> { struct App<W> {
current_desktop: Option<Vec<String>>, current_desktop: Option<Vec<String>>,
is_desktop_cosmic: bool, is_desktop_cosmic: bool,
desktop_entries: Vec<DesktopEntry<'static>>, desktop_entries: Vec<DesktopEntry>,
locales: Vec<String>, locales: Vec<String>,
tx: W, tx: W,
gpus: Option<Vec<switcheroo_control::Gpu>>, 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 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| { .filter_map(|de| {
de.ok().and_then(|de| { // Cache flatpak apps separately from non-flatpak apps.
// Treat Flatpak and system apps differently in the cache so they don't let _flatpak_appid;
// override each other let appid = match de.flatpak() {
let appid = de.flatpak().unwrap_or_else(|| de.appid.as_ref()); Some(base_id) => {
if deduplicator.contains(appid) { _flatpak_appid = [base_id, ".", de.appid.as_str()].concat();
return None; _flatpak_appid.as_str()
} }
None => de.appid.as_str(),
};
de.name(&self.locales)?; if deduplicator.contains(appid) {
return None;
}
match de.exec() { de.name(&self.locales)?;
Some(exec) => match exec.split_ascii_whitespace().next() {
Some(exec) => { match de.exec() {
if exec == "false" { Some(exec) => match exec.split_ascii_whitespace().next() {
return None; Some(exec) => {
} if exec == "false" {
return None;
} }
None => return None, }
},
None => return None, None => return None,
} },
None => return None,
}
// Avoid showing the GNOME Shell entry entirely // Avoid showing the GNOME Shell entry entirely
if de if de
.name(&[] as &[&str]) .name(&[] as &[&str])
.map_or(false, |v| EXCLUSIONS.contains(&v.as_ref())) .map_or(false, |v| EXCLUSIONS.contains(&v.as_ref()))
{ {
return None; return None;
} }
// Do not show if our desktop is defined in `NotShowIn`. // Do not show if our desktop is defined in `NotShowIn`.
if let Some(not_show_in) = de.not_show_in() { if let Some(not_show_in) = de.not_show_in() {
if let Some(current_desktop) = &self.current_desktop { if let Some(current_desktop) = &self.current_desktop {
if not_show_in.iter().any(|not_show| { if not_show_in.iter().any(|not_show| {
current_desktop current_desktop
.iter() .iter()
.any(|desktop| &not_show.to_ascii_lowercase() == desktop) .any(|desktop| &not_show.to_ascii_lowercase() == desktop)
}) { }) {
return None; return None;
}
} }
} }
}
// Do not show if our desktop is not defined in `OnlyShowIn`. // Do not show if our desktop is not defined in `OnlyShowIn`.
if let Some(only_show_in) = de.only_show_in() { if let Some(only_show_in) = de.only_show_in() {
if let Some(current_desktop) = &self.current_desktop { if let Some(current_desktop) = &self.current_desktop {
if !only_show_in.iter().any(|show_in| { if !only_show_in.iter().any(|show_in| {
current_desktop current_desktop
.iter() .iter()
.any(|desktop| &show_in.to_ascii_lowercase() == desktop) .any(|desktop| &show_in.to_ascii_lowercase() == desktop)
}) { }) {
return None; return None;
}
} }
} }
// Treat `OnlyShowIn` as an override otherwise do not show if `NoDisplay` is true }
// Some desktop environments set `OnlyShowIn` and `NoDisplay = true` to // Treat `OnlyShowIn` as an override otherwise do not show if `NoDisplay` is true
// indicate special entries // Some desktop environments set `OnlyShowIn` and `NoDisplay = true` to
else if de.no_display() { // indicate special entries
return None; else if de.no_display() {
} return None;
}
// Always cache already visited entries to allow overriding entries e.g. by // Always cache already visited entries to allow overriding entries e.g. by
// placing a modified copy in ~/.local/share/applications/ // placing a modified copy in ~/.local/share/applications/
// //
// We only do this when we can add an entry to our list, otherwise we risk // 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 // ignoring user overrides or valid applications due to shell URL handlers
deduplicator.insert(appid.to_owned()); deduplicator.insert(appid.to_owned());
Some(de) Some(de)
})
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -213,33 +219,66 @@ impl<W: AsyncWrite + Unpin> App<W> {
} }
async fn search(&mut self, query: &str) { async fn search(&mut self, query: &str) {
for (id, entry) in self.desktop_entries.iter().enumerate() { let query = query.to_ascii_lowercase();
let score = fde::matching::get_entry_score(query, entry, &self.locales, &[]);
if score > 0.6 { let &mut Self {
let response = PluginResponse::Append(PluginSearchResult { ref desktop_entries,
id: id as u32, ref locales,
name: entry.name(&self.locales).unwrap_or_default().to_string(), ref mut tx,
description: get_description(entry, &self.locales), ..
keywords: entry } = self;
.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(&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; 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() { if self.gpus.is_some() {
vec![ContextOption { vec![ContextOption {
id: 0, 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(); let mut options = Vec::new();
if let Some(gpus) = self.gpus.as_ref() { 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 path_source = PathSource::guess_from(&de.path);
let desc_source = path_string(&path_source).to_string(); 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 futures::StreamExt;
use pop_launcher::*; use pop_launcher::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{convert::TryFrom, fs, path::PathBuf}; use std::{fs, path::PathBuf};
use tokio::io::{AsyncWrite, AsyncWriteExt}; use tokio::io::{AsyncWrite, AsyncWriteExt};
use zbus::Connection; use zbus::Connection;
use zvariant::{Signature, Type}; use zvariant::{Signature, Type};
mod config; mod config;
pub use config::{load, Config}; pub use config::{Config, load};
const DEST: &str = "com.System76.PopShell"; const DEST: &str = "com.System76.PopShell";
const PATH: &str = "/com/System76/PopShell"; const PATH: &str = "/com/System76/PopShell";
@ -26,9 +26,7 @@ struct Item {
} }
impl Type for Item { impl Type for Item {
fn signature() -> Signature<'static> { const SIGNATURE: &'static Signature = <((u32, u32), String, String, String)>::SIGNATURE;
Signature::try_from("((uu)sss)").expect("bad dbus signature")
}
} }
pub async fn main() { pub async fn main() {
@ -143,7 +141,7 @@ impl<W: AsyncWrite + Unpin> App<W> {
if let Ok(entry) = fde::DesktopEntry::from_str( if let Ok(entry) = fde::DesktopEntry::from_str(
path, path,
&data, &data,
&get_languages_from_env(), Some(&get_languages_from_env()),
) { ) {
if let Some(icon) = entry.icon() { if let Some(icon) = entry.icon() {
icon_name = Cow::Owned(icon.to_owned()); icon_name = Cow::Owned(icon.to_owned());

View file

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

View file

@ -49,7 +49,7 @@ impl App {
None => return, None => return,
}; };
use fork::{daemon, Fork}; use fork::{Fork, daemon};
crate::send(&mut self.out, PluginResponse::Close).await; 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()) freedesktop_desktop_entry::Iter::new(freedesktop_desktop_entry::default_paths())
.filter_map(|path| { .filter_map(|path| {
std::fs::read_to_string(&path).ok().and_then(|input| { 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() .ok()
.and_then(|de| { .and_then(|de| {
if de.no_display() if de.no_display()

View file

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

View file

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

View file

@ -17,10 +17,10 @@ use crate::priority::Priority;
use crate::recent::RecentUseStorage; use crate::recent::RecentUseStorage;
use clap::Parser; use clap::Parser;
use flume::{Receiver, Sender}; use flume::{Receiver, Sender};
use futures::{future, SinkExt, Stream, StreamExt}; use futures::{SinkExt, Stream, StreamExt, future};
use pop_launcher::{ use pop_launcher::{
json_input_stream, plugin_paths, ContextOption, IconSource, Indice, PluginResponse, ContextOption, IconSource, Indice, PluginResponse, PluginSearchResult, Request, Response,
PluginSearchResult, Request, Response, SearchResult, SearchResult, json_input_stream, plugin_paths,
}; };
use regex::Regex; use regex::Regex;
use slab::Slab; use slab::Slab;
@ -567,45 +567,6 @@ impl<O: futures::Sink<Response> + Unpin> Service<O> {
} else { } else {
active_search.sort_by(|a, b| { active_search.sort_by(|a, b| {
// Weight is calculated between 0.0 and 1.0, with higher values being most similar // 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) { let plug1 = match plugins.get(a.0) {
Some(plug) => plug, 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); 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 crate::PluginConfig;
use futures::{stream, Stream, StreamExt}; use futures::{Stream, StreamExt, stream};
use regex::Regex; use regex::Regex;
use std::path::PathBuf; use std::path::PathBuf;

View file

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

View file

@ -1,5 +1,5 @@
use serde::{Deserialize, Deserializer, Serialize, Serializer}; 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}; use std::hash::{Hash, Hasher};
const SHORTTERM_CAP: usize = 20; const SHORTTERM_CAP: usize = 20;

View file

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

View file

@ -1,13 +1,13 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
// Copyright © 2021 System76 // Copyright © 2021 System76
use fork::{daemon, Fork}; use fork::{Fork, daemon};
use pop_launcher::{Indice, PluginResponse, PluginSearchResult}; 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::io;
use std::os::unix::process::CommandExt; use std::os::unix::process::CommandExt;
use std::path::PathBuf; 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. // 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. // 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 // SPDX-License-Identifier: MPL-2.0
use futures::StreamExt; 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; pub use async_trait::async_trait;
use pop_launcher_plugins as plugins; use pop_launcher_plugins as plugins;
@ -122,7 +122,7 @@ where
); );
if let Ok(file) = logfile { if let Ok(file) = logfile {
use tracing_subscriber::{fmt, EnvFilter}; use tracing_subscriber::{EnvFilter, fmt};
fmt() fmt()
.with_env_filter(EnvFilter::from_default_env()) .with_env_filter(EnvFilter::from_default_env())
.with_writer(file) .with_writer(file)