From 5a8368e273657f269f25edfc631aa2c057719d2a Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:37:37 +0100 Subject: [PATCH 1/9] add plugin spec in the readme --- README.md | 135 ++++++++++++++++++++++++++------ plugins/src/calc/plugin.desktop | 9 +++ 2 files changed, 118 insertions(+), 26 deletions(-) create mode 100644 plugins/src/calc/plugin.desktop diff --git a/README.md b/README.md index 54fefcb..1030e9e 100644 --- a/README.md +++ b/README.md @@ -49,31 +49,112 @@ just plugins="calc desktop_entries files find pop_shell pulse recent scripts ter ## Plugin Config -A plugin's metadata is defined `pop-launcher/plugins/{plugin}/plugin.ron`. - -```ron -( - name: "PluginName", - description: "Plugin Description: Example", - bin: ( - path: "name-of-executable-in-plugin-folder", - ), - icon: Name("icon-name-or-path"), - // Optional - query: ( - // Optional -- if we should isolate this plugin when the regex matches - isolate: true, - // Optional -- Plugin which searches on empty queries - persistent: true, - // Optional -- avoid sorting results from this plugin - no_sort: true, - // Optional -- pattern that a query must have to be sent to plugin - regex: "pattern", - // Optional -- the launcher should keep a history for this plugin - history: true, - ) -) -``` +A plugin's manifest file should be installed in `{plugin_directory}`. The syntax respect the [freedesktop specification](https://specifications.freedesktop.org/desktop-entry-spec/latest/index.html). An example can be found [here](./plugins/src/calc/plugin.desktop). + +This are the supported fields. The ones with no default are required. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionTypeDefaultTranslatable
NamePlugin desciptionlocalestringyes
DescriptionPlugin namelocalestringempty stringyes
IconIcon name, respecting the [icon theme spec](https://freedesktop.org/wiki/Specifications/icon-theme-spec/).iconstringno iconno
ExecExecutable file. Should either an absolute path, or a path relative to {plugin_directory}stringno
RegexActivate the plugin when this regex match the queryregexmatch all queryno
IsolateIsolate this plugin when the regex field matchesbooleanfalseno
IsolateWithIsolate this plugin when this regex matchesregexmatch nothingno
ShowOnEmptyQueryShow plugin's result on empty queriesbooleanfalseno
NoSortAvoid sorting results from this pluginbooleanfalseno
GenericQueryDefault query that will activate this pluginlocalestringempty stringno
LongLivedThe plugin should always run in the backgroundbooleanfalseno
HistoryThe launcher should keep a history for this pluginbooleanfalseno
PriorityResult priority of the pluginDefault, High or LowDefaultno
+ + ## Script Directories @@ -82,6 +163,7 @@ A plugin's metadata is defined `pop-launcher/plugins/{plugin}/plugin.ron`. - Distribution packaging: `/usr/lib/pop-launcher/scripts` Example script +
 #!/bin/sh
@@ -92,6 +174,7 @@ Example script
 # keywords: vpn start connect
 
 nmcli connection up "vpn-name"
+
 
@@ -231,7 +314,7 @@ Where `PluginSearchResult` is: `GpuPreference` is: ```ts -"Default" | "NonDefault" +"Default" | "NonDefault"; ``` And `IconSource` is either: diff --git a/plugins/src/calc/plugin.desktop b/plugins/src/calc/plugin.desktop new file mode 100644 index 0000000..73d90dc --- /dev/null +++ b/plugins/src/calc/plugin.desktop @@ -0,0 +1,9 @@ +[Plugin] +Name=Calculator with unit conversion (uses Qalculate! expressions) +Description=Syntax: = \nExample: = 10 J / (196x^2) = 4 kW/s +Exec=calc +Icon=x-office-spreadsheet +Regex=^[=\\-0-9|(-0-9)].* +GenericQuery==\ +Priority = High +IsolateWith = "^(=).*" \ No newline at end of file From 13e2b5a2b537d59dd962312dc161100b6daec284 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:36:23 +0100 Subject: [PATCH 2/9] impl parsing --- Cargo.lock | 69 ++++++++- Cargo.toml | 5 +- justfile | 2 +- plugins/Cargo.toml | 2 +- plugins/src/cosmic_toplevel/mod.rs | 126 ++++++++------- plugins/src/desktop_entries/mod.rs | 158 ++++++++++--------- plugins/src/pop_shell/mod.rs | 2 +- plugins/src/terminal/mod.rs | 2 +- service/Cargo.toml | 1 + service/src/lib.rs | 47 ++---- service/src/plugins/config.rs | 221 +++++++++++++++------------ service/src/plugins/external/load.rs | 24 ++- service/src/plugins/external/mod.rs | 16 +- service/src/plugins/mod.rs | 33 +--- 14 files changed, 385 insertions(+), 323 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c428600..a54a58b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,18 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -26,6 +38,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -364,6 +382,39 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +[[package]] +name = "cached" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9718806c4a2fe9e8a56fd736f97b340dd10ed1be8ed733ed50449f351dc33cae" +dependencies = [ + "ahash", + "cached_proc_macro", + "cached_proc_macro_types", + "hashbrown 0.14.5", + "once_cell", + "thiserror 1.0.69", + "web-time", +] + +[[package]] +name = "cached_proc_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f42a145ed2d10dce2191e1dcf30cfccfea9026660e143662ba5eec4017d5daa" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" + [[package]] name = "calloop" version = "0.13.0" @@ -763,17 +814,16 @@ dependencies = [ [[package]] name = "freedesktop-desktop-entry" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33809936d2fa9ac78750c5c04696a7aabdb09f928454957c77a2c8247f5ff98" +version = "0.7.5" dependencies = [ + "cached", "dirs", "gettext-rs", "log", "memchr", "strsim", "textdistance", - "thiserror 1.0.69", + "thiserror 2.0.4", "xdg", ] @@ -979,6 +1029,16 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + [[package]] name = "hashbrown" version = "0.15.2" @@ -1830,6 +1890,7 @@ dependencies = [ "async-trait", "dirs", "flume", + "freedesktop-desktop-entry", "futures", "futures_codec", "gen-z", diff --git a/Cargo.toml b/Cargo.toml index ede182d..8f9b9d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ regex = "1.11.0" ron = "0.8.1" tokio = "1.40.0" tokio-stream = "0.1.16" +freedesktop-desktop-entry = "0.7.5" [dependencies] const_format = "0.2.33" @@ -50,8 +51,8 @@ workspace = true features = ["io-util"] -# [patch.crates-io] -# freedesktop-desktop-entry = { path = "../freedesktop-desktop-entry" } +[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" } diff --git a/justfile b/justfile index 06793e9..f9bec0a 100644 --- a/justfile +++ b/justfile @@ -69,7 +69,7 @@ install-plugins: for plugin in {{plugins}}; do dest={{plugin-dir}}${plugin} mkdir -p ${dest} - install -Dm0644 plugins/src/${plugin}/*.ron ${dest} + install -Dm0644 plugins/src/${plugin}/*.desktop ${dest} ln -srf {{bin-path}} {{plugin-dir}}${plugin}/$(echo ${plugin} | sed 's/_/-/') done diff --git a/plugins/Cargo.toml b/plugins/Cargo.toml index 41e11d9..1e54122 100644 --- a/plugins/Cargo.toml +++ b/plugins/Cargo.toml @@ -9,7 +9,7 @@ publish = false [dependencies] async-pidfd = "0.1.4" fork = "0.2.0" -freedesktop-desktop-entry = "0.6.2" +freedesktop-desktop-entry.workspace = true human_format = "1.1.0" human-sort = "0.2.2" new_mime_guess = "4.0.4" diff --git a/plugins/src/cosmic_toplevel/mod.rs b/plugins/src/cosmic_toplevel/mod.rs index 38b2dfd..9414b90 100644 --- a/plugins/src/cosmic_toplevel/mod.rs +++ b/plugins/src/cosmic_toplevel/mod.rs @@ -21,7 +21,6 @@ use pop_launcher::{ Request, }; use std::borrow::Cow; -use std::iter; use tokio::io::{AsyncWrite, AsyncWriteExt}; use self::toplevel_handler::{toplevel_handler, ToplevelAction}; @@ -73,20 +72,42 @@ pub async fn main() { for (handle, info) in updates { match info { Some(info) => { - if let Some(pos) = app.toplevels.iter().position(|t| t.0 == handle) { + if let Some(pos) = app.toplevels.iter().position(|t| t.handle == handle) + { if info.state.contains(&State::Activated) { app.toplevels.remove(pos); - app.toplevels.push((handle, Box::new(info))); + app.toplevels.push(Box::new(TopLevel { + handle, + entry: fde::matching::find_entry_from_appid( + &app.desktop_entries, + &info.app_id, + ) + .map(|e| e.to_owned()) + .unwrap_or( + DesktopEntry::from_appid(&info.app_id).to_owned(), + ), + info, + })); } else { - app.toplevels[pos].1 = Box::new(info); + app.toplevels[pos].info = info; } } else { - app.toplevels.push((handle, Box::new(info))); + app.toplevels.push(Box::new(TopLevel { + handle, + entry: fde::matching::find_entry_from_appid( + &app.desktop_entries, + &info.app_id, + ) + .map(|e| e.to_owned()) + .unwrap_or(DesktopEntry::from_appid(&info.app_id).to_owned()), + info, + })); } } // no info means remove None => { - if let Some(pos) = app.toplevels.iter().position(|t| t.0 == handle) { + if let Some(pos) = app.toplevels.iter().position(|t| t.handle == handle) + { app.toplevels.remove(pos); // ignore requests for this id until after the next search app.ids_to_ignore.push(handle.id().protocol_id()); @@ -102,11 +123,17 @@ pub async fn main() { } } +struct TopLevel { + handle: ZcosmicToplevelHandleV1, + info: ToplevelInfo, + entry: DesktopEntry<'static>, +} + struct App { locales: Vec, desktop_entries: Vec>, ids_to_ignore: Vec, - toplevels: Vec<(ZcosmicToplevelHandleV1, Box)>, + toplevels: Vec>, calloop_tx: calloop::channel::Sender, tx: W, } @@ -119,11 +146,9 @@ impl App { 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()) - .collect::>(); + let desktop_entries = fde::Iter::new(fde::default_paths()) + .entries(Some(&locales)) + .collect(); ( Self { @@ -144,8 +169,8 @@ impl App { return; } if let Some(handle) = self.toplevels.iter().find_map(|t| { - if t.0.id().protocol_id() == id { - Some(t.0.clone()) + if t.handle.id().protocol_id() == id { + Some(t.handle.clone()) } else { None } @@ -160,8 +185,8 @@ impl App { return; } if let Some(handle) = self.toplevels.iter().find_map(|t| { - if t.0.id().protocol_id() == id { - Some(t.0.clone()) + if t.handle.id().protocol_id() == id { + Some(t.handle.clone()) } else { None } @@ -173,57 +198,30 @@ impl App { async fn search(&mut self, query: &str) { let query = query.to_ascii_lowercase(); - for (handle, 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 top_level in self.toplevels.iter().rev() { + let score = top_level.entry.match_query(&query, &self.locales, &[]); + + if score < 0.8 { + continue; + } + + let icon_name = if let Some(icon) = top_level.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::>(); - - 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: handle.id().protocol_id(), - window: Some((0, handle.id().clone().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; - } + let response = PluginResponse::Append(PluginSearchResult { + // XXX protocol id may be re-used later + id: top_level.handle.id().protocol_id(), + window: Some((0, top_level.handle.id().clone().protocol_id())), + description: top_level.info.title.clone(), + name: get_description(&top_level.entry, &self.locales), + icon: Some(IconSource::Name(icon_name)), + ..Default::default() + }); + + send(&mut self.tx, response).await; } send(&mut self.tx, PluginResponse::Finished).await; diff --git a/plugins/src/desktop_entries/mod.rs b/plugins/src/desktop_entries/mod.rs index d98144f..6575cc7 100644 --- a/plugins/src/desktop_entries/mod.rs +++ b/plugins/src/desktop_entries/mod.rs @@ -67,81 +67,76 @@ impl App { let mut deduplicator = std::collections::HashSet::new(); let locales = fde::get_languages_from_env(); - let paths = fde::Iter::new(fde::default_paths()); - - let desktop_entries = DesktopEntry::from_paths(paths, &locales) + self.desktop_entries = fde::Iter::new(fde::default_paths()) + .entries(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; - } - // Always cache already visited entries to allow overriding entries e.g. by - // placing a modified copy in ~/.local/share/applications/ - deduplicator.insert(appid.to_owned()); - - 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, - } + // 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; + } + // Always cache already visited entries to allow overriding entries e.g. by + // placing a modified copy in ~/.local/share/applications/ + deduplicator.insert(appid.to_owned()); - // Avoid showing the GNOME Shell entry entirely - if de - .name(&[] as &[&str]) - .map_or(false, |v| EXCLUSIONS.contains(&v.as_ref())) - { - return None; - } + de.name(&self.locales)?; - // 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) - }) { + match de.exec() { + Some(exec) => match exec.split_ascii_whitespace().next() { + Some(exec) => { + if exec == "false" { return None; } } - } + None => return None, + }, + None => 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; - } + // 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; } } - // 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; + } + + // 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; + } - Some(de) - }) + Some(de) }) - .collect::>(); - - self.desktop_entries = desktop_entries; + .collect(); self.gpus = try_get_gpus().await; } @@ -210,26 +205,27 @@ impl App { 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, &[]); - - 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 score = entry.match_query(query, &self.locales, &[]); - send(&mut self.tx, response).await; + if score < 0.6 { + continue; } + 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(&mut self.tx, response).await; } send(&mut self.tx, PluginResponse::Finished).await; diff --git a/plugins/src/pop_shell/mod.rs b/plugins/src/pop_shell/mod.rs index 8946f58..38f7e5d 100644 --- a/plugins/src/pop_shell/mod.rs +++ b/plugins/src/pop_shell/mod.rs @@ -143,7 +143,7 @@ impl App { 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()); diff --git a/plugins/src/terminal/mod.rs b/plugins/src/terminal/mod.rs index 9b76ab3..fc0377e 100644 --- a/plugins/src/terminal/mod.rs +++ b/plugins/src/terminal/mod.rs @@ -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() diff --git a/service/Cargo.toml b/service/Cargo.toml index e79a853..01a008b 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -24,6 +24,7 @@ strsim = "0.11.1" toml.workspace = true tracing.workspace = true flume.workspace = true +freedesktop-desktop-entry.workspace = true [dependencies.tokio] workspace = true diff --git a/service/src/lib.rs b/service/src/lib.rs index dafbf88..2ed6d17 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -10,9 +10,7 @@ pub use client::*; pub use plugins::config; pub use plugins::external::load; -use crate::plugins::{ - ExternalPlugin, HelpPlugin, Plugin, PluginConfig, PluginConnector, PluginPriority, PluginQuery, -}; +use crate::plugins::{ExternalPlugin, Plugin, PluginConfig, PluginConnector, PluginPriority}; use crate::priority::Priority; use crate::recent::RecentUseStorage; use flume::{Receiver, Sender}; @@ -21,7 +19,6 @@ use pop_launcher::{ json_input_stream, plugin_paths, ContextOption, IconSource, Indice, PluginResponse, PluginSearchResult, Request, Response, SearchResult, }; -use regex::Regex; use slab::Slab; use std::{ cmp::Ordering, @@ -142,8 +139,8 @@ impl + Unpin> Service { futures::pin_mut!(stream); - while let Some((exec, config, regex)) = stream.next().await { - tracing::info!("found plugin \"{}\"", exec.display()); + while let Some(config) = stream.next().await { + tracing::info!("found plugin \"{}\"", config.exec.path.display()); if self .plugins .iter() @@ -153,20 +150,14 @@ impl + Unpin> Service { continue; } - let name = String::from(config.name.as_ref()); + let name = config.name.clone(); + let exec = config.exec.clone(); - self.register_plugin(service_tx.clone(), config, regex, move |id, tx| { - ExternalPlugin::new(id, name.clone(), exec.clone(), Vec::new(), tx) + self.register_plugin(service_tx.clone(), config, move |id, tx| { + ExternalPlugin::new(id, name.clone(), exec.clone(), tx) }); } - self.register_plugin( - service_tx.clone(), - plugins::help::CONFIG, - Some(Regex::new(plugins::help::REGEX.as_ref()).expect("failed to compile help regex")), - HelpPlugin::new, - ); - let f1 = request_handler(input, service_tx); let f2 = self.response_handler(service_rx); @@ -265,7 +256,6 @@ impl + Unpin> Service { &mut self, service_tx: Sender, config: PluginConfig, - regex: Option, init: I, ) { let entry = self.plugins.vacant_entry(); @@ -273,16 +263,8 @@ impl + Unpin> Service { let init = std::sync::Arc::new(init); - let isolate_with = config - .query - .isolate_with - .as_ref() - .and_then(|expr| Regex::new(expr).ok()); - entry.insert(PluginConnector::new( config, - regex, - isolate_with, Box::new(move || { let (request_tx, request_rx) = flume::bounded(8); @@ -436,22 +418,22 @@ impl + Unpin> Service { for (key, plugin) in self.plugins.iter_mut() { // Avoid sending queries to plugins which are not matched - if let Some(regex) = plugin.regex.as_ref() { + if let Some(regex) = plugin.config.regex.as_ref() { if !regex.is_match(query) { continue; } } - if requires_persistence && !plugin.config.query.persistent { + if requires_persistence && !plugin.config.show_on_empty_query { continue; } - if plugin.config.query.isolate { + if plugin.config.isolate { isolated = Some(key); break; } - if let Some(regex) = plugin.isolate_regex.as_ref() { + if let Some(regex) = plugin.config.isolate_with.as_ref() { if regex.is_match(query) { isolated = Some(key); break; @@ -470,7 +452,7 @@ impl + Unpin> Service { .is_ok() { self.awaiting_results.insert(isolated); - self.no_sort = plugin.config.query.no_sort; + self.no_sort = plugin.config.no_sort; } } } else { @@ -581,7 +563,7 @@ impl + Unpin> Service { let get_prio = |sr: &PluginSearchResult, plg: &PluginConnector| -> Priority { let ex = sr.cache_identifier(); Priority { - plugin_priority: plg.config.query.priority, + plugin_priority: plg.config.priority, match_score: calculate_weight(sr, query), recent_score: ex.as_ref().map(|s| recent.get_recent(s)).unwrap_or(0.), freq_score: ex.as_ref().map(|s| recent.get_freq(s)).unwrap_or(0.), @@ -617,7 +599,8 @@ impl + Unpin> Service { icon: meta.icon.clone(), category_icon: plugins .get(*plugin) - .and_then(|conn| conn.config.icon.clone()), + .and_then(|conn| conn.config.icon.clone()) + .map(|icon| IconSource::Name(icon.into())), window: meta.window, } }); diff --git a/service/src/plugins/config.rs b/service/src/plugins/config.rs index cd2d465..d5e75b5 100644 --- a/service/src/plugins/config.rs +++ b/service/src/plugins/config.rs @@ -1,86 +1,38 @@ // Copyright 2021 System76 // SPDX-License-Identifier: MPL-2.0 +use anyhow::{anyhow, bail}; +use freedesktop_desktop_entry as fde; use regex::Regex; -use serde::Deserialize; -use std::{ - borrow::Cow, - path::{Path, PathBuf}, -}; +use std::path::{Path, PathBuf}; -#[derive(Debug, Default, Deserialize, Clone)] +#[derive(Debug, Default, Clone)] pub struct PluginConfig { - pub name: Cow<'static, str>, - pub description: Cow<'static, str>, - #[serde( - default, - skip_serializing_if = "Option::is_none", - with = "::serde_with::rust::unwrap_or_skip" - )] - pub bin: Option, - #[serde( - default, - skip_serializing_if = "Option::is_none", - with = "::serde_with::rust::unwrap_or_skip" - )] - pub icon: Option, - - #[serde(default)] - pub query: PluginQuery, - - #[serde(default)] - pub history: bool, - - #[serde(default)] - pub long_lived: bool, -} - -#[derive(Debug, Default, Deserialize, Clone)] -pub struct PluginBinary { - path: Cow<'static, str>, - - #[serde(default)] - #[allow(dead_code)] - args: Vec>, -} - -#[derive(Debug, Default, Deserialize, Clone)] -pub struct PluginQuery { - #[serde( - default, - skip_serializing_if = "Option::is_none", - with = "::serde_with::rust::unwrap_or_skip" - )] - pub help: Option>, - - #[serde(default)] + pub name: String, + pub description: Option, + pub icon: Option, + pub exec: PluginExec, + pub regex: Option, pub isolate: bool, - - #[serde( - default, - skip_serializing_if = "Option::is_none", - with = "::serde_with::rust::unwrap_or_skip" - )] - pub isolate_with: Option>, - - #[serde(default)] + pub isolate_with: Option, + pub show_on_empty_query: bool, pub no_sort: bool, - - #[serde(default)] - pub persistent: bool, - - #[serde(default)] + pub generic_query: Option, + pub long_lived: bool, + pub history: bool, pub priority: PluginPriority, +} - #[serde( - default, - skip_serializing_if = "Option::is_none", - with = "::serde_with::rust::unwrap_or_skip" - )] - pub regex: Option>, +#[derive(Debug, Default, Clone)] +pub struct PluginExec { + pub path: PathBuf, + pub args: Vec, } -#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, PartialOrd, Ord)] +#[derive(Debug, Default, Clone)] +pub struct PluginQuery {} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] pub enum PluginPriority { High = 0, Default = 1, @@ -93,36 +45,115 @@ impl Default for PluginPriority { } } -pub fn load(source: &Path, config_path: &Path) -> Option<(PathBuf, PluginConfig, Option)> { - if let Ok(config_bytes) = std::fs::read_to_string(config_path) { - let config = match ron::from_str::(&config_bytes) { - Ok(config) => config, - Err(why) => { - tracing::error!("malformed config at {}: {}", config_path.display(), why); - return None; - } +impl PluginConfig { + pub fn from_desktop_entry(source: &Path, config_path: &Path) -> anyhow::Result { + let locales = fde::get_languages_from_env(); + + let content = std::fs::read_to_string(config_path) + .map_err(|e| anyhow!("error reading config at {}: {:?}", config_path.display(), e))?; + + let desktop_entry = fde::DesktopEntry::from_str(config_path, &content, Some(&locales))?; + + let group = desktop_entry + .groups + .group("Plugin") + .ok_or(anyhow!("no Plugin group"))?; + + let mut config = PluginConfig::default(); + + config.name = group + .localized_entry("Name", &locales) + .ok_or(anyhow!("no Name field"))? + .to_string() + .into(); + + let exec = group + .localized_entry("Exec", &locales) + .ok_or(anyhow!("no Exec field"))?; + + let mut iter = exec.split(" "); + + let mut exec = PluginExec { + path: PathBuf::from(iter.next().unwrap()), + args: iter.map(|a| a.to_string()).collect(), }; - let exec = if let Some(bin) = config.bin.as_ref() { - if bin.path.starts_with('/') { - PathBuf::from((*bin.path).to_owned()) - } else { - source.join(bin.path.as_ref()) - } - } else { - tracing::error!( - "bin field is missing from config at {}", - config_path.display() - ); - return None; + if !exec.path.is_absolute() { + exec.path = source.join(&exec.path); }; - let regex = config.query.regex.as_ref().and_then(|p| Regex::new(p).ok()); + config.exec = exec.into(); + + if let Some(description) = group.entry("Description") { + config.description.replace(description.to_string()); + } + + if let Some(icon) = group.entry("Icon") { + config.icon.replace(icon.to_string()); + } + + if let Some(regex) = group.entry("Regex") { + match Regex::new(regex) { + Ok(regex) => { + config.regex.replace(regex); + } + Err(e) => bail!("can't parse regex: {e:?}"), + } + } + + if let Some(isolate) = group.entry_bool("Isolate") { + config.isolate = isolate; + } + + if let Some(regex) = group.entry("IsolateWith") { + match Regex::new(regex) { + Ok(regex) => { + config.isolate_with.replace(regex); + } + Err(e) => bail!("can't parse isolate_with: {e:?}"), + } + } + + if let Some(persistent) = group.entry_bool("ShowOnEmptyQuery") { + config.show_on_empty_query = persistent; + } + + if let Some(no_sort) = group.entry_bool("NoSort") { + config.no_sort = no_sort; + } + + if let Some(generic_query) = group.entry("GenericQuery") { + config.generic_query.replace(generic_query.to_string()); + } + + if let Some(long_lived) = group.entry_bool("LongLived") { + config.long_lived = long_lived; + } + + if let Some(history) = group.entry_bool("History") { + config.history = history; + } + + if let Some(priority) = group.entry("Priority") { + match priority { + "Default" => config.priority = PluginPriority::Default, + "High" => config.priority = PluginPriority::High, + "Low" => config.priority = PluginPriority::Low, + _ => {} + } + } - return Some((exec, config, regex)); + Ok(config) } +} - tracing::error!("I/O error reading config at {}", config_path.display()); +#[test] +fn a() { + let p = PluginConfig::from_desktop_entry( + Path::new("source"), + Path::new("../plugins/src/calc/plugin.desktop"), + ) + .unwrap(); - None + dbg!(&p); } diff --git a/service/src/plugins/external/load.rs b/service/src/plugins/external/load.rs index e8cf801..019717e 100644 --- a/service/src/plugins/external/load.rs +++ b/service/src/plugins/external/load.rs @@ -4,21 +4,35 @@ use crate::PluginConfig; use futures::{stream, Stream, StreamExt}; -use regex::Regex; use std::path::PathBuf; +use tracing::error; /// Fetches plugins installed on the system in parallel. /// /// Searches plugin paths from highest to least priority. User plugins will override /// distribution plugins. Plugins are loaded in the order they are found. -pub fn from_paths() -> impl Stream)> { +pub fn from_paths() -> impl Stream { stream::iter(crate::plugin_paths()) .flat_map(|path| from_path(path.to_path_buf())) .map(|(source, config)| { - tokio::task::spawn_blocking(move || crate::plugins::config::load(&source, &config)) + tokio::task::spawn_blocking(move || PluginConfig::from_desktop_entry(&source, &config)) }) .buffered(num_cpus::get()) - .filter_map(|x| async move { x.ok().flatten() }) + .filter_map(|x| async move { + match x { + Ok(plugin) => match plugin { + Ok(plugin) => Some(plugin), + Err(e) => { + error!("{e:?}"); + None + } + }, + Err(e) => { + error!("{e:?}"); + None + } + } + }) } /// Loads all plugin information found in the given path. @@ -31,7 +45,7 @@ pub fn from_path(path: PathBuf) -> impl Stream { continue; } - let config = source.join("plugin.ron"); + let config = source.join("plugin.desktop"); if !config.exists() { continue; } diff --git a/service/src/plugins/external/mod.rs b/service/src/plugins/external/mod.rs index 67620c3..81da276 100644 --- a/service/src/plugins/external/mod.rs +++ b/service/src/plugins/external/mod.rs @@ -5,7 +5,6 @@ pub mod load; use std::{ io, - path::PathBuf, process::Stdio, sync::{ atomic::{AtomicBool, Ordering}, @@ -24,12 +23,13 @@ use tokio::{ }; use tracing::{event, Level}; +use super::config::PluginExec; + pub struct ExternalPlugin { id: usize, tx: Sender, name: String, - pub cmd: PathBuf, - pub args: Vec, + exec: PluginExec, process: Option<(JoinHandle<()>, Child, async_oneshot::Sender<()>)>, detached: Arc, searching: Arc, @@ -39,16 +39,14 @@ impl ExternalPlugin { pub fn new( id: usize, name: String, - cmd: PathBuf, - args: Vec, + exec: PluginExec, tx: Sender, ) -> Self { Self { id, name, tx, - cmd, - args, + exec, process: None, detached: Arc::default(), searching: Arc::default(), @@ -58,8 +56,8 @@ impl ExternalPlugin { pub fn launch(&mut self) -> Option<&mut (JoinHandle<()>, Child, async_oneshot::Sender<()>)> { event!(Level::DEBUG, "{}: launching plugin", self.name()); - let child = Command::new(&self.cmd) - .args(&self.args) + let child = Command::new(&self.exec.path) + .args(&self.exec.args) .stdout(Stdio::piped()) .stdin(Stdio::piped()) .stderr(Stdio::inherit()) diff --git a/service/src/plugins/mod.rs b/service/src/plugins/mod.rs index 24c1078..772d1d1 100644 --- a/service/src/plugins/mod.rs +++ b/service/src/plugins/mod.rs @@ -3,16 +3,14 @@ pub mod config; pub(crate) mod external; -pub mod help; +// pub mod help; -pub use self::config::{PluginConfig, PluginPriority, PluginQuery}; +pub use self::config::{PluginConfig, PluginPriority}; pub use self::external::ExternalPlugin; -pub use self::help::HelpPlugin; use crate::{Indice, PluginHelp, Request}; use async_trait::async_trait; use flume::{Receiver, Sender}; -use regex::Regex; #[async_trait] pub trait Plugin @@ -81,44 +79,25 @@ pub struct PluginConnector { /// this plugin to spawn as a background service pub init: Box Sender>, - pub isolate_regex: Option, - - /// A compiled regular expression that a query must match - /// for the launcher service to justify spawning and sending - /// queries to this plugin - pub regex: Option, - /// The sender of the spawned background service that will be /// forwarded to the launncher service pub sender: Option>, } impl PluginConnector { - pub fn new( - config: PluginConfig, - regex: Option, - isolate_regex: Option, - init: Box Sender + Send>, - ) -> Self { + pub fn new(config: PluginConfig, init: Box Sender + Send>) -> Self { Self { config, init, - isolate_regex, - regex, sender: None, } } pub fn details(&self) -> PluginHelp { PluginHelp { - name: self.config.name.as_ref().to_owned(), - description: self.config.description.as_ref().to_owned(), - help: self - .config - .query - .help - .as_ref() - .map(|x| x.as_ref().to_owned()), + name: self.config.name.to_string(), + description: self.config.description.clone().unwrap_or_default(), + help: self.config.generic_query.clone(), } } From ba69d72402f31f4f9bc44b2e8751a7de0476148e Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Thu, 12 Dec 2024 00:31:58 +0100 Subject: [PATCH 3/9] update fde + fix plugin.desktop --- plugins/src/calc/plugin.desktop | 6 +++--- plugins/src/cosmic_toplevel/mod.rs | 8 ++++---- plugins/src/desktop_entries/mod.rs | 6 +++--- plugins/src/desktop_entries/utils.rs | 4 +--- service/src/plugins/config.rs | 11 ----------- 5 files changed, 11 insertions(+), 24 deletions(-) diff --git a/plugins/src/calc/plugin.desktop b/plugins/src/calc/plugin.desktop index 73d90dc..38bd5d1 100644 --- a/plugins/src/calc/plugin.desktop +++ b/plugins/src/calc/plugin.desktop @@ -4,6 +4,6 @@ Description=Syntax: = \nExample: = 10 J / (196x^2) = 4 kW/s Exec=calc Icon=x-office-spreadsheet Regex=^[=\\-0-9|(-0-9)].* -GenericQuery==\ -Priority = High -IsolateWith = "^(=).*" \ No newline at end of file +GenericQuery==\s +Priority=High +IsolateWith="^(=).*" \ No newline at end of file diff --git a/plugins/src/cosmic_toplevel/mod.rs b/plugins/src/cosmic_toplevel/mod.rs index 9414b90..65692a1 100644 --- a/plugins/src/cosmic_toplevel/mod.rs +++ b/plugins/src/cosmic_toplevel/mod.rs @@ -84,7 +84,7 @@ pub async fn main() { ) .map(|e| e.to_owned()) .unwrap_or( - DesktopEntry::from_appid(&info.app_id).to_owned(), + DesktopEntry::from_appid(info.app_id.clone()).to_owned(), ), info, })); @@ -99,7 +99,7 @@ pub async fn main() { &info.app_id, ) .map(|e| e.to_owned()) - .unwrap_or(DesktopEntry::from_appid(&info.app_id).to_owned()), + .unwrap_or(DesktopEntry::from_appid(info.app_id.clone()).to_owned()), info, })); } @@ -126,12 +126,12 @@ pub async fn main() { struct TopLevel { handle: ZcosmicToplevelHandleV1, info: ToplevelInfo, - entry: DesktopEntry<'static>, + entry: DesktopEntry, } struct App { locales: Vec, - desktop_entries: Vec>, + desktop_entries: Vec, ids_to_ignore: Vec, toplevels: Vec>, calloop_tx: calloop::channel::Sender, diff --git a/plugins/src/desktop_entries/mod.rs b/plugins/src/desktop_entries/mod.rs index 6575cc7..955c9cd 100644 --- a/plugins/src/desktop_entries/mod.rs +++ b/plugins/src/desktop_entries/mod.rs @@ -43,7 +43,7 @@ const EXCLUSIONS: &[&str] = &["GNOME Shell", "Initial Setup"]; struct App { current_desktop: Option>, is_desktop_cosmic: bool, - desktop_entries: Vec>, + desktop_entries: Vec, locales: Vec, tx: W, gpus: Option>, @@ -231,7 +231,7 @@ impl App { send(&mut self.tx, PluginResponse::Finished).await; } - async fn gnome_context(&self, entry: &DesktopEntry<'_>) -> Vec { + async fn gnome_context(&self, entry: &DesktopEntry) -> Vec { if self.gpus.is_some() { vec![ContextOption { id: 0, @@ -247,7 +247,7 @@ impl App { } } - async fn cosmic_context(&self, entry: &DesktopEntry<'_>) -> Vec { + async fn cosmic_context(&self, entry: &DesktopEntry) -> Vec { let mut options = Vec::new(); if let Some(gpus) = self.gpus.as_ref() { diff --git a/plugins/src/desktop_entries/utils.rs b/plugins/src/desktop_entries/utils.rs index 864aced..9a28162 100644 --- a/plugins/src/desktop_entries/utils.rs +++ b/plugins/src/desktop_entries/utils.rs @@ -4,8 +4,6 @@ use std::borrow::Cow; use freedesktop_desktop_entry::{DesktopEntry, PathSource}; -// todo: subscriptions with notify - pub fn path_string(source: &PathSource) -> Cow<'static, str> { match source { PathSource::Local | PathSource::LocalDesktop => "Local".into(), @@ -20,7 +18,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(de: &DesktopEntry, locales: &[String]) -> String { let path_source = PathSource::guess_from(&de.path); let desc_source = path_string(&path_source).to_string(); diff --git a/service/src/plugins/config.rs b/service/src/plugins/config.rs index d5e75b5..942226e 100644 --- a/service/src/plugins/config.rs +++ b/service/src/plugins/config.rs @@ -146,14 +146,3 @@ impl PluginConfig { Ok(config) } } - -#[test] -fn a() { - let p = PluginConfig::from_desktop_entry( - Path::new("source"), - Path::new("../plugins/src/calc/plugin.desktop"), - ) - .unwrap(); - - dbg!(&p); -} From fede441cb2bf744282c97a4a96484a9c07122248 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Thu, 12 Dec 2024 00:54:16 +0100 Subject: [PATCH 4/9] replace plugins --- plugins/src/calc/plugin.ron | 13 ------------- plugins/src/cosmic_toplevel/plugin.desktop | 8 ++++++++ plugins/src/cosmic_toplevel/plugin.ron | 8 -------- plugins/src/desktop_entries/plugin.desktop | 6 ++++++ plugins/src/desktop_entries/plugin.ron | 7 ------- plugins/src/files/plugin.desktop | 9 +++++++++ plugins/src/files/plugin.ron | 12 ------------ plugins/src/find/plugin.desktop | 8 ++++++++ plugins/src/find/plugin.ron | 11 ----------- plugins/src/pop_shell/plugin.desktop | 6 ++++++ plugins/src/pop_shell/plugin.ron | 7 ------- plugins/src/pulse/plugin.desktop | 5 +++++ plugins/src/pulse/plugin.ron | 6 ------ plugins/src/recent/plugin.desktop | 8 ++++++++ plugins/src/recent/plugin.ron | 11 ----------- plugins/src/scripts/plugin.desktop | 5 +++++ plugins/src/scripts/plugin.ron | 6 ------ plugins/src/terminal/plugin.desktop | 9 +++++++++ plugins/src/terminal/plugin.ron | 12 ------------ plugins/src/web/plugin.desktop | 8 ++++++++ plugins/src/web/plugin.ron | 8 -------- rust-toolchain | 1 - toolkit/examples/plugin.desktop | 9 +++++++++ toolkit/examples/plugin.ron | 12 ------------ 24 files changed, 81 insertions(+), 114 deletions(-) delete mode 100644 plugins/src/calc/plugin.ron create mode 100644 plugins/src/cosmic_toplevel/plugin.desktop delete mode 100644 plugins/src/cosmic_toplevel/plugin.ron create mode 100644 plugins/src/desktop_entries/plugin.desktop delete mode 100644 plugins/src/desktop_entries/plugin.ron create mode 100644 plugins/src/files/plugin.desktop delete mode 100644 plugins/src/files/plugin.ron create mode 100644 plugins/src/find/plugin.desktop delete mode 100644 plugins/src/find/plugin.ron create mode 100644 plugins/src/pop_shell/plugin.desktop delete mode 100644 plugins/src/pop_shell/plugin.ron create mode 100644 plugins/src/pulse/plugin.desktop delete mode 100644 plugins/src/pulse/plugin.ron create mode 100644 plugins/src/recent/plugin.desktop delete mode 100644 plugins/src/recent/plugin.ron create mode 100644 plugins/src/scripts/plugin.desktop delete mode 100644 plugins/src/scripts/plugin.ron create mode 100644 plugins/src/terminal/plugin.desktop delete mode 100644 plugins/src/terminal/plugin.ron create mode 100644 plugins/src/web/plugin.desktop delete mode 100644 plugins/src/web/plugin.ron delete mode 100644 rust-toolchain create mode 100644 toolkit/examples/plugin.desktop delete mode 100644 toolkit/examples/plugin.ron diff --git a/plugins/src/calc/plugin.ron b/plugins/src/calc/plugin.ron deleted file mode 100644 index 8eba8af..0000000 --- a/plugins/src/calc/plugin.ron +++ /dev/null @@ -1,13 +0,0 @@ -( - name: "Calculator with unit conversion (uses Qalculate! expressions)", - description: "Syntax: = \nExample: = 10 J / (196x^2) = 4 kW/s", - query: ( - regex: "^[=\\-0-9|(-0-9)].*", - help: "= ", - priority: High, - isolate_with: "^(=).*", - ), - bin: (path: "calc"), - icon: Name("x-office-spreadsheet"), - history: false, -) diff --git a/plugins/src/cosmic_toplevel/plugin.desktop b/plugins/src/cosmic_toplevel/plugin.desktop new file mode 100644 index 0000000..abf10cb --- /dev/null +++ b/plugins/src/cosmic_toplevel/plugin.desktop @@ -0,0 +1,8 @@ +[Plugin] +Name=COSMIC Windows +Description=Active windows controllable via Cosmic +Exec=cosmic-toplevel +Icon=focus-windows-symbolic +Priority=High +LongLived=true +ShowOnEmptyQuery=true \ No newline at end of file diff --git a/plugins/src/cosmic_toplevel/plugin.ron b/plugins/src/cosmic_toplevel/plugin.ron deleted file mode 100644 index 217fdc4..0000000 --- a/plugins/src/cosmic_toplevel/plugin.ron +++ /dev/null @@ -1,8 +0,0 @@ -( - name: "COSMIC Windows", - description: "Active windows controllable via Cosmic", - query: (persistent: true, priority: High), - bin: (path: "cosmic-toplevel"), - icon: Name("focus-windows-symbolic"), - long_lived: true, -) \ No newline at end of file diff --git a/plugins/src/desktop_entries/plugin.desktop b/plugins/src/desktop_entries/plugin.desktop new file mode 100644 index 0000000..c56701b --- /dev/null +++ b/plugins/src/desktop_entries/plugin.desktop @@ -0,0 +1,6 @@ +[Plugin] +Name=Desktop Entries +Description=Query applications by their .desktop entries +Exec=desktop-entries +Icon=new-window-symbolic +History=true \ No newline at end of file diff --git a/plugins/src/desktop_entries/plugin.ron b/plugins/src/desktop_entries/plugin.ron deleted file mode 100644 index 18de0bf..0000000 --- a/plugins/src/desktop_entries/plugin.ron +++ /dev/null @@ -1,7 +0,0 @@ -( - name: "Desktop Entries", - description: "Query applications by their .desktop entries", - bin: (path: "desktop-entries"), - icon: Name("new-window-symbolic"), - history: true, -) \ No newline at end of file diff --git a/plugins/src/files/plugin.desktop b/plugins/src/files/plugin.desktop new file mode 100644 index 0000000..00e63ab --- /dev/null +++ b/plugins/src/files/plugin.desktop @@ -0,0 +1,9 @@ +[Plugin] +Name=File navigation with tab autocomplete +Description=Syntax: { / | ~/ }\nExample: ~/Documents +Exec=files +Icon=system-file-manager +Regex=^(/|~).* +GenericQuery=~/ +Isolate=true +NoSort=true \ No newline at end of file diff --git a/plugins/src/files/plugin.ron b/plugins/src/files/plugin.ron deleted file mode 100644 index 30656a2..0000000 --- a/plugins/src/files/plugin.ron +++ /dev/null @@ -1,12 +0,0 @@ -( - name: "File navigation with tab autocomplete", - description: "Syntax: { / | ~/ }\nExample: ~/Documents", - query: ( - regex: "^(/|~).*", - help: "~/", - isolate: true, - no_sort: true, - ), - bin: (path: "files"), - icon: Name("system-file-manager"), -) diff --git a/plugins/src/find/plugin.desktop b/plugins/src/find/plugin.desktop new file mode 100644 index 0000000..b166837 --- /dev/null +++ b/plugins/src/find/plugin.desktop @@ -0,0 +1,8 @@ +[Plugin] +Name=File search +Description=Syntax: find \nExample: find my-document.odt +Exec=find +Icon=system-file-manager +Regex=^(find )+ +GenericQuery=find\s +Isolate=true \ No newline at end of file diff --git a/plugins/src/find/plugin.ron b/plugins/src/find/plugin.ron deleted file mode 100644 index 3791777..0000000 --- a/plugins/src/find/plugin.ron +++ /dev/null @@ -1,11 +0,0 @@ -( - name: "File search", - description: "Syntax: find \nExample: find my-document.odt", - query: ( - regex: "^(find )+", - help: "find ", - isolate: true, - ), - bin: (path: "find"), - icon: Name("system-file-manager"), -) diff --git a/plugins/src/pop_shell/plugin.desktop b/plugins/src/pop_shell/plugin.desktop new file mode 100644 index 0000000..1f5d2e9 --- /dev/null +++ b/plugins/src/pop_shell/plugin.desktop @@ -0,0 +1,6 @@ +[Plugin] +Name=Pop Shell Windows +Description=Active windows controllable via Pop Shell +Exec=pop-shell +Icon=focus-windows-symbolic +ShowOnEmptyQuery=true \ No newline at end of file diff --git a/plugins/src/pop_shell/plugin.ron b/plugins/src/pop_shell/plugin.ron deleted file mode 100644 index c10cf26..0000000 --- a/plugins/src/pop_shell/plugin.ron +++ /dev/null @@ -1,7 +0,0 @@ -( - name: "Pop Shell Windows", - description: "Active windows controllable via Pop Shell", - query: (persistent: true), - bin: (path: "pop-shell"), - icon: Name("focus-windows-symbolic"), -) \ No newline at end of file diff --git a/plugins/src/pulse/plugin.desktop b/plugins/src/pulse/plugin.desktop new file mode 100644 index 0000000..a5b7a58 --- /dev/null +++ b/plugins/src/pulse/plugin.desktop @@ -0,0 +1,5 @@ +[Plugin] +Name=PulseAudio Volume Control +Description=Control PulseAudio devices and volume +Exec=pulse +Icon=multimedia-volume-control \ No newline at end of file diff --git a/plugins/src/pulse/plugin.ron b/plugins/src/pulse/plugin.ron deleted file mode 100644 index 1b70c5c..0000000 --- a/plugins/src/pulse/plugin.ron +++ /dev/null @@ -1,6 +0,0 @@ -( - name: "PulseAudio Volume Control", - description: "Control PulseAudio devices and volume", - bin: (path: "pulse"), - icon: Name("multimedia-volume-control") -) \ No newline at end of file diff --git a/plugins/src/recent/plugin.desktop b/plugins/src/recent/plugin.desktop new file mode 100644 index 0000000..af828f4 --- /dev/null +++ b/plugins/src/recent/plugin.desktop @@ -0,0 +1,8 @@ +[Plugin] +Name=Recently-opened document search +Description=Syntax: recent \nExample: recent my-document.odt +Exec=recent +Regex=^(recent)\\s.* +GenericQuery=recent\s +Icon=system-file-manager +Isolate=true \ No newline at end of file diff --git a/plugins/src/recent/plugin.ron b/plugins/src/recent/plugin.ron deleted file mode 100644 index b0eb5af..0000000 --- a/plugins/src/recent/plugin.ron +++ /dev/null @@ -1,11 +0,0 @@ -( - name: "Recently-opened document search", - description: "Syntax: recent \nExample: recent my-document.odt", - query: ( - regex: "^(recent)\\s.*", - help: "recent ", - isolate: true - ), - bin: (path: "recent"), - icon: Name("system-file-manager") -) diff --git a/plugins/src/scripts/plugin.desktop b/plugins/src/scripts/plugin.desktop new file mode 100644 index 0000000..c160dac --- /dev/null +++ b/plugins/src/scripts/plugin.desktop @@ -0,0 +1,5 @@ +[Plugin] +Name=Scripts +Description=Shell scripts as launcher options +Exec=scripts +Icon=utilities-terminal \ No newline at end of file diff --git a/plugins/src/scripts/plugin.ron b/plugins/src/scripts/plugin.ron deleted file mode 100644 index ecaba34..0000000 --- a/plugins/src/scripts/plugin.ron +++ /dev/null @@ -1,6 +0,0 @@ -( - name: "Scripts", - description: "Shell scripts as launcher options", - bin: (path: "scripts"), - icon: Name("utilities-terminal"), -) \ No newline at end of file diff --git a/plugins/src/terminal/plugin.desktop b/plugins/src/terminal/plugin.desktop new file mode 100644 index 0000000..dc47d70 --- /dev/null +++ b/plugins/src/terminal/plugin.desktop @@ -0,0 +1,9 @@ +[Plugin] +Name=Terminal or background commands +Description=Syntax: { run | t: | : } \nExample: run sudo apt update +Exec=terminal +Icon=utilities-terminal +History=true +Isolate=true +GenericQuery=run\s +Regex=^(:|t:|run ).* \ No newline at end of file diff --git a/plugins/src/terminal/plugin.ron b/plugins/src/terminal/plugin.ron deleted file mode 100644 index 8d03699..0000000 --- a/plugins/src/terminal/plugin.ron +++ /dev/null @@ -1,12 +0,0 @@ -( - name: "Terminal or background commands", - description: "Syntax: { run | t: | : } \nExample: run sudo apt update", - query: ( - regex: "^(:|t:|run ).*", - help: "run ", - isolate: true, - ), - bin: (path: "terminal"), - icon: Name("utilities-terminal"), - history: true, -) diff --git a/plugins/src/web/plugin.desktop b/plugins/src/web/plugin.desktop new file mode 100644 index 0000000..fc74c4b --- /dev/null +++ b/plugins/src/web/plugin.desktop @@ -0,0 +1,8 @@ +[Plugin] +Name=Web search +Description=Syntax: { ddg | google | ... } \nExample: ddg how to install Pop!_OS +Exec=web +Icon=system-search +History=true +GenericQuery=ddg\s +Priority=High \ No newline at end of file diff --git a/plugins/src/web/plugin.ron b/plugins/src/web/plugin.ron deleted file mode 100644 index 12acc61..0000000 --- a/plugins/src/web/plugin.ron +++ /dev/null @@ -1,8 +0,0 @@ -( - name: "Web search", - description: "Syntax: { ddg | google | ... } \nExample: ddg how to install Pop!_OS", - query: (help: "ddg ", priority: High), - bin: (path: "web"), - icon: Name("system-search"), - history: true, -) diff --git a/rust-toolchain b/rust-toolchain deleted file mode 100644 index 2bf5ad0..0000000 --- a/rust-toolchain +++ /dev/null @@ -1 +0,0 @@ -stable diff --git a/toolkit/examples/plugin.desktop b/toolkit/examples/plugin.desktop new file mode 100644 index 0000000..dd06131 --- /dev/null +++ b/toolkit/examples/plugin.desktop @@ -0,0 +1,9 @@ +[Plugin] +Name=Find man pages +Description=Syntax: { whatis }\nExample: whatis git +Exec=man-pages-plugin +Icon=org.gnome.Documents-symbolic +NoSort=true +Isolate=true +GenericQuery=whatis\s +Regex=^(whatis ).+ \ No newline at end of file diff --git a/toolkit/examples/plugin.ron b/toolkit/examples/plugin.ron deleted file mode 100644 index 5a73a37..0000000 --- a/toolkit/examples/plugin.ron +++ /dev/null @@ -1,12 +0,0 @@ -( - name: "Find man pages", - description: "Syntax: { whatis }\nExample: whatis git", - query: ( - regex: "^(whatis ).+", - help: "whatis", - isolate: true, - no_sort: true, - ), - bin: (path: "man-pages-plugin"), - icon: Name("org.gnome.Documents-symbolic"), -) From 3f8740289c24e0e96ff21882e79e157e58b874e1 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Thu, 12 Dec 2024 01:21:43 +0100 Subject: [PATCH 5/9] restore help plugin --- plugins/src/cosmic_toplevel/mod.rs | 8 ++- service/src/lib.rs | 7 +++ service/src/plugins/config.rs | 59 +++++++++++--------- service/src/plugins/external/load.rs | 2 +- service/src/plugins/external/mod.rs | 7 +-- service/src/plugins/{help.rs => help/mod.rs} | 31 ++++------ service/src/plugins/help/plugin.desktop | 8 +++ service/src/plugins/mod.rs | 2 +- 8 files changed, 66 insertions(+), 58 deletions(-) rename service/src/plugins/{help.rs => help/mod.rs} (79%) create mode 100644 service/src/plugins/help/plugin.desktop diff --git a/plugins/src/cosmic_toplevel/mod.rs b/plugins/src/cosmic_toplevel/mod.rs index 65692a1..89007b1 100644 --- a/plugins/src/cosmic_toplevel/mod.rs +++ b/plugins/src/cosmic_toplevel/mod.rs @@ -84,7 +84,8 @@ pub async fn main() { ) .map(|e| e.to_owned()) .unwrap_or( - DesktopEntry::from_appid(info.app_id.clone()).to_owned(), + DesktopEntry::from_appid(info.app_id.clone()) + .to_owned(), ), info, })); @@ -99,7 +100,9 @@ pub async fn main() { &info.app_id, ) .map(|e| e.to_owned()) - .unwrap_or(DesktopEntry::from_appid(info.app_id.clone()).to_owned()), + .unwrap_or( + DesktopEntry::from_appid(info.app_id.clone()).to_owned(), + ), info, })); } @@ -133,6 +136,7 @@ struct App { locales: Vec, desktop_entries: Vec, ids_to_ignore: Vec, + #[allow(clippy::vec_box)] toplevels: Vec>, calloop_tx: calloop::channel::Sender, tx: W, diff --git a/service/src/lib.rs b/service/src/lib.rs index 2ed6d17..ee32f73 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -9,6 +9,7 @@ mod recent; pub use client::*; pub use plugins::config; pub use plugins::external::load; +use plugins::help::HelpPlugin; use crate::plugins::{ExternalPlugin, Plugin, PluginConfig, PluginConnector, PluginPriority}; use crate::priority::Priority; @@ -158,6 +159,12 @@ impl + Unpin> Service { }); } + self.register_plugin( + service_tx.clone(), + plugins::help::manifest(), + HelpPlugin::new, + ); + let f1 = request_handler(input, service_tx); let f2 = self.response_handler(service_rx); diff --git a/service/src/plugins/config.rs b/service/src/plugins/config.rs index 942226e..00f8ec3 100644 --- a/service/src/plugins/config.rs +++ b/service/src/plugins/config.rs @@ -46,44 +46,49 @@ impl Default for PluginPriority { } impl PluginConfig { - pub fn from_desktop_entry(source: &Path, config_path: &Path) -> anyhow::Result { - let locales = fde::get_languages_from_env(); - + pub fn from_path(source: &Path, config_path: &Path) -> anyhow::Result { let content = std::fs::read_to_string(config_path) .map_err(|e| anyhow!("error reading config at {}: {:?}", config_path.display(), e))?; - let desktop_entry = fde::DesktopEntry::from_str(config_path, &content, Some(&locales))?; + Self::from_str(source, config_path, &content) + } + + pub fn from_str(source: &Path, config_path: &Path, content: &str) -> anyhow::Result { + let locales = fde::get_languages_from_env(); + + let desktop_entry = fde::DesktopEntry::from_str(config_path, content, Some(&locales))?; let group = desktop_entry .groups .group("Plugin") .ok_or(anyhow!("no Plugin group"))?; - let mut config = PluginConfig::default(); - - config.name = group - .localized_entry("Name", &locales) - .ok_or(anyhow!("no Name field"))? - .to_string() - .into(); - - let exec = group - .localized_entry("Exec", &locales) - .ok_or(anyhow!("no Exec field"))?; - - let mut iter = exec.split(" "); - - let mut exec = PluginExec { - path: PathBuf::from(iter.next().unwrap()), - args: iter.map(|a| a.to_string()).collect(), + let mut config = PluginConfig { + name: group + .localized_entry("Name", &locales) + .ok_or(anyhow!("no Name field"))? + .to_string(), + exec: { + let exec = group + .localized_entry("Exec", &locales) + .ok_or(anyhow!("no Exec field"))?; + + let mut iter = exec.split(" "); + + let mut exec = PluginExec { + path: PathBuf::from(iter.next().unwrap()), + args: iter.map(|a| a.to_string()).collect(), + }; + + if !exec.path.is_absolute() { + exec.path = source.join(&exec.path); + }; + + exec + }, + ..Default::default() }; - if !exec.path.is_absolute() { - exec.path = source.join(&exec.path); - }; - - config.exec = exec.into(); - if let Some(description) = group.entry("Description") { config.description.replace(description.to_string()); } diff --git a/service/src/plugins/external/load.rs b/service/src/plugins/external/load.rs index 019717e..abb855a 100644 --- a/service/src/plugins/external/load.rs +++ b/service/src/plugins/external/load.rs @@ -15,7 +15,7 @@ pub fn from_paths() -> impl Stream { stream::iter(crate::plugin_paths()) .flat_map(|path| from_path(path.to_path_buf())) .map(|(source, config)| { - tokio::task::spawn_blocking(move || PluginConfig::from_desktop_entry(&source, &config)) + tokio::task::spawn_blocking(move || PluginConfig::from_path(&source, &config)) }) .buffered(num_cpus::get()) .filter_map(|x| async move { diff --git a/service/src/plugins/external/mod.rs b/service/src/plugins/external/mod.rs index 81da276..0f60c97 100644 --- a/service/src/plugins/external/mod.rs +++ b/service/src/plugins/external/mod.rs @@ -36,12 +36,7 @@ pub struct ExternalPlugin { } impl ExternalPlugin { - pub fn new( - id: usize, - name: String, - exec: PluginExec, - tx: Sender, - ) -> Self { + pub fn new(id: usize, name: String, exec: PluginExec, tx: Sender) -> Self { Self { id, name, diff --git a/service/src/plugins/help.rs b/service/src/plugins/help/mod.rs similarity index 79% rename from service/src/plugins/help.rs rename to service/src/plugins/help/mod.rs index 1dbf1bc..ffadac4 100644 --- a/service/src/plugins/help.rs +++ b/service/src/plugins/help/mod.rs @@ -5,27 +5,16 @@ use crate::*; use flume::Sender; use pop_launcher::*; use slab::Slab; -use std::borrow::Cow; - -pub const REGEX: Cow<'static, str> = Cow::Borrowed("^(\\?).*"); - -pub const CONFIG: PluginConfig = PluginConfig { - name: Cow::Borrowed("Help"), - description: Cow::Borrowed("Show available plugin prefixes"), - bin: None, - query: PluginQuery { - help: None, - isolate: true, - isolate_with: None, - no_sort: true, - persistent: false, - priority: PluginPriority::Default, - regex: None, - }, - icon: Some(IconSource::Name(Cow::Borrowed("system-help-symbolic"))), - history: false, - long_lived: false, -}; + +pub fn manifest() -> PluginConfig { + PluginConfig::from_str( + &PathBuf::default(), + &PathBuf::default(), + include_str!("plugin.desktop"), + ) + .unwrap() +} + pub struct HelpPlugin { pub id: usize, pub details: Slab, diff --git a/service/src/plugins/help/plugin.desktop b/service/src/plugins/help/plugin.desktop new file mode 100644 index 0000000..fe48538 --- /dev/null +++ b/service/src/plugins/help/plugin.desktop @@ -0,0 +1,8 @@ +[Plugin] +Name=Help +Description=Show available plugin prefixes +Exec= +Isolate=true +NoSort=true +Icon=system-help-symbolic +Regex=^(\\?).* \ No newline at end of file diff --git a/service/src/plugins/mod.rs b/service/src/plugins/mod.rs index 772d1d1..56ab032 100644 --- a/service/src/plugins/mod.rs +++ b/service/src/plugins/mod.rs @@ -3,7 +3,7 @@ pub mod config; pub(crate) mod external; -// pub mod help; +pub mod help; pub use self::config::{PluginConfig, PluginPriority}; pub use self::external::ExternalPlugin; From 6f4cb645731e59e29021aeff10b19a5495008cfa Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Thu, 12 Dec 2024 01:54:05 +0100 Subject: [PATCH 6/9] fix --- service/src/plugins/help/mod.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/service/src/plugins/help/mod.rs b/service/src/plugins/help/mod.rs index ffadac4..06e4442 100644 --- a/service/src/plugins/help/mod.rs +++ b/service/src/plugins/help/mod.rs @@ -1,6 +1,8 @@ // Copyright 2021 System76 // SPDX-License-Identifier: MPL-2.0 +use std::path::Path; + use crate::*; use flume::Sender; use pop_launcher::*; @@ -8,8 +10,9 @@ use slab::Slab; pub fn manifest() -> PluginConfig { PluginConfig::from_str( - &PathBuf::default(), - &PathBuf::default(), + &Path::new(""), + // dummy value + &Path::new("a.b.desktop"), include_str!("plugin.desktop"), ) .unwrap() From 00951161657ddbc5dce299d4e8c1867f39b39c92 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Thu, 12 Dec 2024 03:41:44 +0100 Subject: [PATCH 7/9] translation for poc --- plugins/src/find/plugin.desktop | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/src/find/plugin.desktop b/plugins/src/find/plugin.desktop index b166837..0288ab4 100644 --- a/plugins/src/find/plugin.desktop +++ b/plugins/src/find/plugin.desktop @@ -1,5 +1,6 @@ [Plugin] Name=File search +Name[fr]=Recherche de fichier Description=Syntax: find \nExample: find my-document.odt Exec=find Icon=system-file-manager From a0cd84b8f810562310565b014d0327d52d192690 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Thu, 12 Dec 2024 19:15:42 +0100 Subject: [PATCH 8/9] use github version of fde --- Cargo.lock | 1 + Cargo.toml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index a54a58b..0394d9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -815,6 +815,7 @@ dependencies = [ [[package]] name = "freedesktop-desktop-entry" version = "0.7.5" +source = "git+https://github.com/wiiznokes/freedesktop-desktop-entry?branch=update#3b60fdf36564af2d12fd4ec81b08601692b72086" dependencies = [ "cached", "dirs", diff --git a/Cargo.toml b/Cargo.toml index 8f9b9d3..d9b48d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,8 @@ features = ["io-util"] [patch.crates-io] -freedesktop-desktop-entry = { path = "../freedesktop-desktop-entry" } +# freedesktop-desktop-entry = { path = "../freedesktop-desktop-entry" } +freedesktop-desktop-entry = { git = "https://github.com/wiiznokes/freedesktop-desktop-entry", branch = "update" } [patch."https://github.com/pop-os/cosmic-protocols"] "cosmic-client-toolkit" = { git = "https://github.com/pop-os//cosmic-protocols" } From 18c811b146a663574127c894acf3de76f7789ad3 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Thu, 12 Dec 2024 19:28:24 +0100 Subject: [PATCH 9/9] translate Name to french --- plugins/src/calc/plugin.desktop | 1 + plugins/src/files/plugin.desktop | 1 + plugins/src/recent/plugin.desktop | 1 + plugins/src/terminal/plugin.desktop | 1 + plugins/src/web/plugin.desktop | 1 + 5 files changed, 5 insertions(+) diff --git a/plugins/src/calc/plugin.desktop b/plugins/src/calc/plugin.desktop index 38bd5d1..58a8bb1 100644 --- a/plugins/src/calc/plugin.desktop +++ b/plugins/src/calc/plugin.desktop @@ -1,5 +1,6 @@ [Plugin] Name=Calculator with unit conversion (uses Qalculate! expressions) +Name[fr]=Calculatrice avec conversion d'unités (utilise les Qalculate! expressions) Description=Syntax: = \nExample: = 10 J / (196x^2) = 4 kW/s Exec=calc Icon=x-office-spreadsheet diff --git a/plugins/src/files/plugin.desktop b/plugins/src/files/plugin.desktop index 00e63ab..adfada7 100644 --- a/plugins/src/files/plugin.desktop +++ b/plugins/src/files/plugin.desktop @@ -1,5 +1,6 @@ [Plugin] Name=File navigation with tab autocomplete +Name[fr]=Navigation des fichiers avec tab autocompletion Description=Syntax: { / | ~/ }\nExample: ~/Documents Exec=files Icon=system-file-manager diff --git a/plugins/src/recent/plugin.desktop b/plugins/src/recent/plugin.desktop index af828f4..cc59e35 100644 --- a/plugins/src/recent/plugin.desktop +++ b/plugins/src/recent/plugin.desktop @@ -1,5 +1,6 @@ [Plugin] Name=Recently-opened document search +Name[fr]=Rechercher documents récemment ouvert Description=Syntax: recent \nExample: recent my-document.odt Exec=recent Regex=^(recent)\\s.* diff --git a/plugins/src/terminal/plugin.desktop b/plugins/src/terminal/plugin.desktop index dc47d70..2c2437b 100644 --- a/plugins/src/terminal/plugin.desktop +++ b/plugins/src/terminal/plugin.desktop @@ -1,5 +1,6 @@ [Plugin] Name=Terminal or background commands +Name[fr]=Terminal ou commande en arrière plan Description=Syntax: { run | t: | : } \nExample: run sudo apt update Exec=terminal Icon=utilities-terminal diff --git a/plugins/src/web/plugin.desktop b/plugins/src/web/plugin.desktop index fc74c4b..c170b84 100644 --- a/plugins/src/web/plugin.desktop +++ b/plugins/src/web/plugin.desktop @@ -1,5 +1,6 @@ [Plugin] Name=Web search +Name[fr]=Recherche internet Description=Syntax: { ddg | google | ... } \nExample: ddg how to install Pop!_OS Exec=web Icon=system-search