Compare commits
10 commits
87bb6c6064
...
6a41b5f43f
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a41b5f43f | |||
|
|
5b86851071 | ||
|
|
eead361cca | ||
|
|
904b690c27 | ||
|
|
092d78f7ca | ||
|
|
0e8aa22f97 | ||
|
|
16b5ae74b3 | ||
|
|
8a0b37bd36 | ||
|
|
8d9da92dba | ||
|
|
58a8f2db64 |
24 changed files with 1390 additions and 1337 deletions
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
2111
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
36
Cargo.toml
36
Cargo.toml
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
8
debian/changelog
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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| ¬_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| ¬_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() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
service/src/plugins/external/load.rs
vendored
2
service/src/plugins/external/load.rs
vendored
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
use crate::PluginConfig;
|
||||
|
||||
use futures::{stream, Stream, StreamExt};
|
||||
use futures::{Stream, StreamExt, stream};
|
||||
use regex::Regex;
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
|
|
|||
4
service/src/plugins/external/mod.rs
vendored
4
service/src/plugins/external/mod.rs
vendored
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue