Skip to content

Commit 8293392

Browse files
oknozormmstick
authored andcommitted
feat: add 'plugin_trait' feature to pop-launcher-toolkit
1 parent edaae9d commit 8293392

File tree

4 files changed

+255
-6
lines changed

4 files changed

+255
-6
lines changed

Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

toolkit/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,9 @@ description = "A wrapper around pop-launcher, pop-launcher-service and pop-launc
99
[dependencies]
1010
pop-launcher-plugins = { path = "../plugins"}
1111
pop-launcher-service = { path = "../service"}
12-
pop-launcher = { path = "../" }
12+
pop-launcher = { path = "../" }
13+
async-trait = "0.1.53"
14+
tracing = "0.1.32"
15+
tracing-subscriber = { version = "0.3.9", default-features = false, features = ["std", "fmt", "env-filter"] }
16+
dirs = "4.0.0"
17+
futures = "0.3.21"

toolkit/src/lib.rs

Lines changed: 116 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,121 @@
11
// Copyright 2021 System76 <[email protected]>
22
// SPDX-License-Identifier: MPL-2.0
33

4+
//! # pop-launcher-toolkit
5+
//!
6+
//! A toolkit to write pop-launcher client and plugin.
7+
//!
8+
//! ## Crates
9+
//! - **[`launcher`]:** re-export the pop-launcher crate, containing all the IPC message struct and
10+
//! some utility functions to locate plugins.
11+
//! - **[`service`]:** re-export the pop-launcher-service crate, containing deserializable plugin config struct.
12+
//! This is useful if your client needs to read user defined plugins configs.
13+
//! - **[`plugins`]:** re-export pop-launcher-plugins which defines all the default pop-launcher plugins.
14+
//! Useful if your client needs to read default plugin configs
15+
//!
16+
//! ## Writing a plugin
17+
//!
18+
//! Add the following to your Cargo.toml :
19+
//!
20+
//! ```toml
21+
//! [dependencies]
22+
//! tokio = { version = "1.18.2", features = ["rt"] }
23+
//! pop-launcher-toolkit = { git = "https://github.com/pop-os/launcher" }
24+
//! ```
25+
//!
26+
//! And implement the [`PluginExt`] trait:
27+
//!
28+
//! [`PluginExt`]: plugin_trait::PluginExt
29+
//!
30+
//! ```rust
31+
//! use pop_launcher_toolkit::launcher::{Indice, PluginResponse, PluginSearchResult};
32+
//! use pop_launcher_toolkit::plugin_trait::{async_trait, PluginExt};
33+
//! use pop_launcher_toolkit::plugins;
34+
//!
35+
//! // The plugin struct, here it holds the search result
36+
//! pub struct MyPlugin {
37+
//! data: Vec<String>
38+
//! }
39+
//!
40+
//! #[async_trait]
41+
//! impl PluginExt for MyPlugin {
42+
//!
43+
//! // Define the name of you plugin, this will be used
44+
//! // to generate a logfile in $XDG_STATE_HOME at runtime.
45+
//! fn name(&self) -> &str {
46+
//! "my_awesome_plugin"
47+
//! }
48+
//!
49+
//! // Respond to `pop-launcher` 'search' query
50+
//! async fn search(&mut self, query: &str) {
51+
//! // `pop-launcher` dispatches request to plugins according to the regex defined in
52+
//! // the `plugin.ron` config file, here we get rid of the prefix
53+
//! // before processing the request.
54+
//! let query = query.strip_prefix("plug ").unwrap();
55+
//!
56+
//! // Iterate through our internal search results with their indices.
57+
//! let search_results = self.data.iter()
58+
//! .enumerate()
59+
//! .filter(|(idx, data)| data.contains(query));
60+
//!
61+
//! // Send our search results to `pop-launcher` using their indices as id.
62+
//! for (idx, search_result) in search_results {
63+
//! self.respond_with(PluginResponse::Append(PluginSearchResult {
64+
//! id: idx as u32,
65+
//! name: search_result.clone(),
66+
//! description: "".to_string(),
67+
//! keywords: None,
68+
//! icon: None,
69+
//! exec: None,
70+
//! window: None,
71+
//! })).await;
72+
//! }
73+
//!
74+
//! // tell `pop-launcher` we are done with this request
75+
//! self.respond_with(PluginResponse::Finished).await;
76+
//! }
77+
//!
78+
//! // Respond to `pop-launcher` 'activate' query
79+
//! async fn activate(&mut self, id: Indice) {
80+
//! // Get the selected entry
81+
//! let entry = self.data.get(id as usize).unwrap();
82+
//! // Here we use xdg_open to run the entry but this could be anything
83+
//! plugins::xdg_open(entry);
84+
//! // Tell pop launcher we are done
85+
//! self.respond_with(PluginResponse::Finished);
86+
//! }
87+
//!
88+
//! // Respond to `pop-launcher` 'close' request.
89+
//! async fn quit(&mut self, id: Indice) {
90+
//! self.respond_with(PluginResponse::Close).await;
91+
//! }
92+
//! }
93+
//!
94+
//! #[tokio::main(flavor = "current_thread")]
95+
//! pub async fn main() {
96+
//!
97+
//! // Here we declare our plugin with dummy values, and never mutate them.
98+
//! // In a real plugin we would probably use some kind of mutable shared reference to
99+
//! // update our search results.
100+
//! let mut plugin = MyPlugin {
101+
//! data: vec!["https://crates.io".to_string(), "https://en.wikipedia.org".to_string()],
102+
//! };
103+
//!
104+
//! /// If you need to debug your plugin or display error messages use `tcracing` macros.
105+
//! tracing::info!("Starting my_awsome_plugin");
106+
//!
107+
//! // Call the plugin entry point function to start
108+
//! // talking with pop_launcherc
109+
//! plugin.run().await;
110+
//! }
111+
//! ```
112+
113+
pub use pop_launcher as launcher;
114+
pub use pop_launcher_plugins as plugins;
4115
pub use pop_launcher_service::{
5-
self as service,
6-
load::from_path as load_plugin_from_path,
7-
load::from_paths as load_plugins_from_paths
116+
self as service, load::from_path as load_plugin_from_path,
117+
load::from_paths as load_plugins_from_paths,
8118
};
9-
pub use pop_launcher_plugins as plugins;
10-
pub use pop_launcher as launcher;
119+
120+
/// A helper trait to quickly create `pop-launcher` plugins
121+
pub mod plugin_trait;

toolkit/src/plugin_trait.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright 2021 System76 <[email protected]>
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
use futures::StreamExt;
5+
use pop_launcher::{async_stdin, async_stdout, json_input_stream, Indice, PluginResponse, Request};
6+
7+
pub use async_trait::async_trait;
8+
use pop_launcher_plugins as plugins;
9+
10+
/// Re-export of the tracing crate, use this to add custom logs to your plugin
11+
pub use tracing;
12+
13+
/// A helper trait to create `pop-launcher` plugins.
14+
#[async_trait]
15+
pub trait PluginExt
16+
where
17+
Self: Sized + Send,
18+
{
19+
/// The name of our plugin, currently this is used internally to create the plugin log file at
20+
/// `$XDG_STATE_HOME/pop-launcher/{name}.log`
21+
fn name(&self) -> &str;
22+
23+
/// Handle a [`Request::Search`] issued by `pop-launcher`.
24+
/// To send search result back use [`PluginResponse::Append`].
25+
/// Once finished [`PluginResponse::Finished`] is expected to notify the search result are ready to be displayed.
26+
async fn search(&mut self, query: &str);
27+
28+
/// Define how the plugin should handle [`Request::Activate`] request.
29+
/// Typically run the requested entry (for instance using [`super::plugins::xdg_open`])
30+
/// and close the client with a [`PluginResponse::Close`]
31+
async fn activate(&mut self, id: Indice);
32+
33+
/// Define how the plugin should handle [`Request::ActivateContext`] request.
34+
/// Typically run the requested entry with the provided context (for instance using [`super::plugins::xdg_open`])
35+
/// and close the client with a [`PluginResponse::Close`]
36+
async fn activate_context(&mut self, _id: Indice, _context: Indice) {}
37+
38+
/// Handle an autocompletion request from the client
39+
async fn complete(&mut self, _id: Indice) {}
40+
41+
/// `pop-launcher` request the context for the given [`SearchResult`] id.
42+
/// to send the requested context use [`PluginResponse::Context`]
43+
///
44+
/// [`SearchResult`]: pop_launcher::SearchResult
45+
async fn context(&mut self, _id: Indice) {}
46+
47+
/// This is automatically called after `pop-launcher` requests the plugin to exit.
48+
/// Use this only if your plugin does not need to perform specific clean ups.
49+
fn exit(&mut self) {}
50+
51+
/// Whenever a new search query is issued, `pop-launcher` will send a [`Request::Interrupt`]
52+
/// so we can stop any ongoing computation before handling the next query.
53+
/// This is especially useful for plugins that rely on external services
54+
/// to get their search results (a HTTP endpoint for instance)
55+
async fn interrupt(&mut self) {}
56+
57+
/// The launcher is asking us to quit a specific item.
58+
async fn quit(&mut self, _id: Indice) {}
59+
60+
/// A helper function to send [`PluginResponse`] back to `pop-launcher`
61+
async fn respond_with(&self, response: PluginResponse) {
62+
plugins::send(&mut async_stdout(), response).await
63+
}
64+
65+
/// Run the plugin
66+
async fn run(&mut self) {
67+
self.init_logging();
68+
let mut receiver = json_input_stream(async_stdin());
69+
while let Some(request) = receiver.next().await {
70+
tracing::event!(
71+
tracing::Level::DEBUG,
72+
"{}: received {:?}",
73+
self.name(),
74+
request
75+
);
76+
77+
match request {
78+
Ok(request) => match request {
79+
Request::Search(query) => self.search(&query).await,
80+
Request::Interrupt => self.interrupt().await,
81+
Request::Activate(id) => self.activate(id).await,
82+
Request::ActivateContext { id, context } => {
83+
self.activate_context(id, context).await
84+
}
85+
Request::Complete(id) => self.complete(id).await,
86+
Request::Context(id) => self.context(id).await,
87+
Request::Quit(id) => self.quit(id).await,
88+
Request::Exit => {
89+
self.exit();
90+
break;
91+
}
92+
},
93+
Err(why) => tracing::error!("Malformed json request: {why}"),
94+
}
95+
}
96+
97+
tracing::event!(tracing::Level::DEBUG, "{}: exiting plugin", self.name());
98+
}
99+
100+
fn init_logging(&self) {
101+
let logdir = match dirs::state_dir() {
102+
Some(dir) => dir.join("pop-launcher/"),
103+
None => dirs::home_dir()
104+
.expect("home directory required")
105+
.join(".cache/pop-launcher"),
106+
};
107+
108+
let _ = std::fs::create_dir_all(&logdir);
109+
110+
let logfile = std::fs::OpenOptions::new()
111+
.create(true)
112+
.truncate(true)
113+
.write(true)
114+
.open(
115+
logdir
116+
.join([self.name(), ".log"].concat().as_str())
117+
.as_path(),
118+
);
119+
120+
if let Ok(file) = logfile {
121+
use tracing_subscriber::{fmt, EnvFilter};
122+
fmt()
123+
.with_env_filter(EnvFilter::from_default_env())
124+
.with_writer(file)
125+
.init();
126+
}
127+
}
128+
}

0 commit comments

Comments
 (0)