feat: add 'plugin_trait' feature to pop-launcher-toolkit
This commit is contained in:
parent
edaae9d004
commit
8293392969
4 changed files with 256 additions and 7 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
|
@ -1146,9 +1146,14 @@ dependencies = [
|
||||||
name = "pop-launcher-toolkit"
|
name = "pop-launcher-toolkit"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"dirs 4.0.0",
|
||||||
|
"futures",
|
||||||
"pop-launcher",
|
"pop-launcher",
|
||||||
"pop-launcher-plugins",
|
"pop-launcher-plugins",
|
||||||
"pop-launcher-service",
|
"pop-launcher-service",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,8 @@ description = "A wrapper around pop-launcher, pop-launcher-service and pop-launc
|
||||||
pop-launcher-plugins = { path = "../plugins"}
|
pop-launcher-plugins = { path = "../plugins"}
|
||||||
pop-launcher-service = { path = "../service"}
|
pop-launcher-service = { path = "../service"}
|
||||||
pop-launcher = { path = "../" }
|
pop-launcher = { path = "../" }
|
||||||
|
async-trait = "0.1.53"
|
||||||
|
tracing = "0.1.32"
|
||||||
|
tracing-subscriber = { version = "0.3.9", default-features = false, features = ["std", "fmt", "env-filter"] }
|
||||||
|
dirs = "4.0.0"
|
||||||
|
futures = "0.3.21"
|
||||||
|
|
@ -1,10 +1,121 @@
|
||||||
// Copyright 2021 System76 <info@system76.com>
|
// Copyright 2021 System76 <info@system76.com>
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
pub use pop_launcher_service::{
|
//! # pop-launcher-toolkit
|
||||||
self as service,
|
//!
|
||||||
load::from_path as load_plugin_from_path,
|
//! A toolkit to write pop-launcher client and plugin.
|
||||||
load::from_paths as load_plugins_from_paths
|
//!
|
||||||
};
|
//! ## Crates
|
||||||
pub use pop_launcher_plugins as plugins;
|
//! - **[`launcher`]:** re-export the pop-launcher crate, containing all the IPC message struct and
|
||||||
|
//! some utility functions to locate plugins.
|
||||||
|
//! - **[`service`]:** re-export the pop-launcher-service crate, containing deserializable plugin config struct.
|
||||||
|
//! This is useful if your client needs to read user defined plugins configs.
|
||||||
|
//! - **[`plugins`]:** re-export pop-launcher-plugins which defines all the default pop-launcher plugins.
|
||||||
|
//! Useful if your client needs to read default plugin configs
|
||||||
|
//!
|
||||||
|
//! ## Writing a plugin
|
||||||
|
//!
|
||||||
|
//! Add the following to your Cargo.toml :
|
||||||
|
//!
|
||||||
|
//! ```toml
|
||||||
|
//! [dependencies]
|
||||||
|
//! tokio = { version = "1.18.2", features = ["rt"] }
|
||||||
|
//! pop-launcher-toolkit = { git = "https://github.com/pop-os/launcher" }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! And implement the [`PluginExt`] trait:
|
||||||
|
//!
|
||||||
|
//! [`PluginExt`]: plugin_trait::PluginExt
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use pop_launcher_toolkit::launcher::{Indice, PluginResponse, PluginSearchResult};
|
||||||
|
//! use pop_launcher_toolkit::plugin_trait::{async_trait, PluginExt};
|
||||||
|
//! use pop_launcher_toolkit::plugins;
|
||||||
|
//!
|
||||||
|
//! // The plugin struct, here it holds the search result
|
||||||
|
//! pub struct MyPlugin {
|
||||||
|
//! data: Vec<String>
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! #[async_trait]
|
||||||
|
//! impl PluginExt for MyPlugin {
|
||||||
|
//!
|
||||||
|
//! // Define the name of you plugin, this will be used
|
||||||
|
//! // to generate a logfile in $XDG_STATE_HOME at runtime.
|
||||||
|
//! fn name(&self) -> &str {
|
||||||
|
//! "my_awesome_plugin"
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! // Respond to `pop-launcher` 'search' query
|
||||||
|
//! async fn search(&mut self, query: &str) {
|
||||||
|
//! // `pop-launcher` dispatches request to plugins according to the regex defined in
|
||||||
|
//! // the `plugin.ron` config file, here we get rid of the prefix
|
||||||
|
//! // before processing the request.
|
||||||
|
//! let query = query.strip_prefix("plug ").unwrap();
|
||||||
|
//!
|
||||||
|
//! // Iterate through our internal search results with their indices.
|
||||||
|
//! let search_results = self.data.iter()
|
||||||
|
//! .enumerate()
|
||||||
|
//! .filter(|(idx, data)| data.contains(query));
|
||||||
|
//!
|
||||||
|
//! // Send our search results to `pop-launcher` using their indices as id.
|
||||||
|
//! for (idx, search_result) in search_results {
|
||||||
|
//! self.respond_with(PluginResponse::Append(PluginSearchResult {
|
||||||
|
//! id: idx as u32,
|
||||||
|
//! name: search_result.clone(),
|
||||||
|
//! description: "".to_string(),
|
||||||
|
//! keywords: None,
|
||||||
|
//! icon: None,
|
||||||
|
//! exec: None,
|
||||||
|
//! window: None,
|
||||||
|
//! })).await;
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! // tell `pop-launcher` we are done with this request
|
||||||
|
//! self.respond_with(PluginResponse::Finished).await;
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! // Respond to `pop-launcher` 'activate' query
|
||||||
|
//! async fn activate(&mut self, id: Indice) {
|
||||||
|
//! // Get the selected entry
|
||||||
|
//! let entry = self.data.get(id as usize).unwrap();
|
||||||
|
//! // Here we use xdg_open to run the entry but this could be anything
|
||||||
|
//! plugins::xdg_open(entry);
|
||||||
|
//! // Tell pop launcher we are done
|
||||||
|
//! self.respond_with(PluginResponse::Finished);
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! // Respond to `pop-launcher` 'close' request.
|
||||||
|
//! async fn quit(&mut self, id: Indice) {
|
||||||
|
//! self.respond_with(PluginResponse::Close).await;
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! #[tokio::main(flavor = "current_thread")]
|
||||||
|
//! pub async fn main() {
|
||||||
|
//!
|
||||||
|
//! // Here we declare our plugin with dummy values, and never mutate them.
|
||||||
|
//! // In a real plugin we would probably use some kind of mutable shared reference to
|
||||||
|
//! // update our search results.
|
||||||
|
//! let mut plugin = MyPlugin {
|
||||||
|
//! data: vec!["https://crates.io".to_string(), "https://en.wikipedia.org".to_string()],
|
||||||
|
//! };
|
||||||
|
//!
|
||||||
|
//! /// If you need to debug your plugin or display error messages use `tcracing` macros.
|
||||||
|
//! tracing::info!("Starting my_awsome_plugin");
|
||||||
|
//!
|
||||||
|
//! // Call the plugin entry point function to start
|
||||||
|
//! // talking with pop_launcherc
|
||||||
|
//! plugin.run().await;
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
pub use pop_launcher as launcher;
|
pub use pop_launcher as launcher;
|
||||||
|
pub use pop_launcher_plugins as plugins;
|
||||||
|
pub use pop_launcher_service::{
|
||||||
|
self as service, load::from_path as load_plugin_from_path,
|
||||||
|
load::from_paths as load_plugins_from_paths,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A helper trait to quickly create `pop-launcher` plugins
|
||||||
|
pub mod plugin_trait;
|
||||||
|
|
|
||||||
128
toolkit/src/plugin_trait.rs
Normal file
128
toolkit/src/plugin_trait.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
// Copyright 2021 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use futures::StreamExt;
|
||||||
|
use pop_launcher::{async_stdin, async_stdout, json_input_stream, Indice, PluginResponse, Request};
|
||||||
|
|
||||||
|
pub use async_trait::async_trait;
|
||||||
|
use pop_launcher_plugins as plugins;
|
||||||
|
|
||||||
|
/// Re-export of the tracing crate, use this to add custom logs to your plugin
|
||||||
|
pub use tracing;
|
||||||
|
|
||||||
|
/// A helper trait to create `pop-launcher` plugins.
|
||||||
|
#[async_trait]
|
||||||
|
pub trait PluginExt
|
||||||
|
where
|
||||||
|
Self: Sized + Send,
|
||||||
|
{
|
||||||
|
/// The name of our plugin, currently this is used internally to create the plugin log file at
|
||||||
|
/// `$XDG_STATE_HOME/pop-launcher/{name}.log`
|
||||||
|
fn name(&self) -> &str;
|
||||||
|
|
||||||
|
/// Handle a [`Request::Search`] issued by `pop-launcher`.
|
||||||
|
/// To send search result back use [`PluginResponse::Append`].
|
||||||
|
/// Once finished [`PluginResponse::Finished`] is expected to notify the search result are ready to be displayed.
|
||||||
|
async fn search(&mut self, query: &str);
|
||||||
|
|
||||||
|
/// Define how the plugin should handle [`Request::Activate`] request.
|
||||||
|
/// Typically run the requested entry (for instance using [`super::plugins::xdg_open`])
|
||||||
|
/// and close the client with a [`PluginResponse::Close`]
|
||||||
|
async fn activate(&mut self, id: Indice);
|
||||||
|
|
||||||
|
/// Define how the plugin should handle [`Request::ActivateContext`] request.
|
||||||
|
/// Typically run the requested entry with the provided context (for instance using [`super::plugins::xdg_open`])
|
||||||
|
/// and close the client with a [`PluginResponse::Close`]
|
||||||
|
async fn activate_context(&mut self, _id: Indice, _context: Indice) {}
|
||||||
|
|
||||||
|
/// Handle an autocompletion request from the client
|
||||||
|
async fn complete(&mut self, _id: Indice) {}
|
||||||
|
|
||||||
|
/// `pop-launcher` request the context for the given [`SearchResult`] id.
|
||||||
|
/// to send the requested context use [`PluginResponse::Context`]
|
||||||
|
///
|
||||||
|
/// [`SearchResult`]: pop_launcher::SearchResult
|
||||||
|
async fn context(&mut self, _id: Indice) {}
|
||||||
|
|
||||||
|
/// This is automatically called after `pop-launcher` requests the plugin to exit.
|
||||||
|
/// Use this only if your plugin does not need to perform specific clean ups.
|
||||||
|
fn exit(&mut self) {}
|
||||||
|
|
||||||
|
/// Whenever a new search query is issued, `pop-launcher` will send a [`Request::Interrupt`]
|
||||||
|
/// so we can stop any ongoing computation before handling the next query.
|
||||||
|
/// This is especially useful for plugins that rely on external services
|
||||||
|
/// to get their search results (a HTTP endpoint for instance)
|
||||||
|
async fn interrupt(&mut self) {}
|
||||||
|
|
||||||
|
/// The launcher is asking us to quit a specific item.
|
||||||
|
async fn quit(&mut self, _id: Indice) {}
|
||||||
|
|
||||||
|
/// A helper function to send [`PluginResponse`] back to `pop-launcher`
|
||||||
|
async fn respond_with(&self, response: PluginResponse) {
|
||||||
|
plugins::send(&mut async_stdout(), response).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the plugin
|
||||||
|
async fn run(&mut self) {
|
||||||
|
self.init_logging();
|
||||||
|
let mut receiver = json_input_stream(async_stdin());
|
||||||
|
while let Some(request) = receiver.next().await {
|
||||||
|
tracing::event!(
|
||||||
|
tracing::Level::DEBUG,
|
||||||
|
"{}: received {:?}",
|
||||||
|
self.name(),
|
||||||
|
request
|
||||||
|
);
|
||||||
|
|
||||||
|
match request {
|
||||||
|
Ok(request) => match request {
|
||||||
|
Request::Search(query) => self.search(&query).await,
|
||||||
|
Request::Interrupt => self.interrupt().await,
|
||||||
|
Request::Activate(id) => self.activate(id).await,
|
||||||
|
Request::ActivateContext { id, context } => {
|
||||||
|
self.activate_context(id, context).await
|
||||||
|
}
|
||||||
|
Request::Complete(id) => self.complete(id).await,
|
||||||
|
Request::Context(id) => self.context(id).await,
|
||||||
|
Request::Quit(id) => self.quit(id).await,
|
||||||
|
Request::Exit => {
|
||||||
|
self.exit();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(why) => tracing::error!("Malformed json request: {why}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::event!(tracing::Level::DEBUG, "{}: exiting plugin", self.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_logging(&self) {
|
||||||
|
let logdir = match dirs::state_dir() {
|
||||||
|
Some(dir) => dir.join("pop-launcher/"),
|
||||||
|
None => dirs::home_dir()
|
||||||
|
.expect("home directory required")
|
||||||
|
.join(".cache/pop-launcher"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = std::fs::create_dir_all(&logdir);
|
||||||
|
|
||||||
|
let logfile = std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.write(true)
|
||||||
|
.open(
|
||||||
|
logdir
|
||||||
|
.join([self.name(), ".log"].concat().as_str())
|
||||||
|
.as_path(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Ok(file) = logfile {
|
||||||
|
use tracing_subscriber::{fmt, EnvFilter};
|
||||||
|
fmt()
|
||||||
|
.with_env_filter(EnvFilter::from_default_env())
|
||||||
|
.with_writer(file)
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue