// Copyright 2019-2021 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT //! The Tauri plugin extension to expand Tauri functionality. use crate::{ utils::config::PluginConfig, AppHandle, Invoke, InvokeHandler, PageLoadPayload, RunEvent, Runtime, Window, }; use serde::de::DeserializeOwned; use serde_json::Value as JsonValue; use tauri_macros::default_runtime; use std::{collections::HashMap, fmt}; /// The result type of Tauri plugin module. pub type Result = std::result::Result>; /// The plugin interface. pub trait Plugin: Send { /// The plugin name. Used as key on the plugin config object. fn name(&self) -> &'static str; /// Initializes the plugin. #[allow(unused_variables)] fn initialize(&mut self, app: &AppHandle, config: JsonValue) -> Result<()> { Ok(()) } /// Add the provided JavaScript to a list of scripts that should be run after the global object has been created, /// but before the HTML document has been parsed and before any other script included by the HTML document is run. /// /// Since it runs on all top-level document and child frame page navigations, /// it's recommended to check the `window.location` to guard your script from running on unexpected origins. /// /// The script is wrapped into its own context with `(function () { /* your script here */ })();`, /// so global variables must be assigned to `window` instead of implicity declared. fn initialization_script(&self) -> Option { None } /// Callback invoked when the webview is created. #[allow(unused_variables)] fn created(&mut self, window: Window) {} /// Callback invoked when the webview performs a navigation to a page. #[allow(unused_variables)] fn on_page_load(&mut self, window: Window, payload: PageLoadPayload) {} /// Callback invoked when the event loop receives a new event. #[allow(unused_variables)] fn on_event(&mut self, app: &AppHandle, event: &RunEvent) {} /// Extend commands to [`crate::Builder::invoke_handler`]. #[allow(unused_variables)] fn extend_api(&mut self, invoke: Invoke) {} } type SetupHook = dyn FnOnce(&AppHandle) -> Result<()> + Send; type SetupWithConfigHook = dyn FnOnce(&AppHandle, T) -> Result<()> + Send; type OnWebviewReady = dyn FnMut(Window) + Send; type OnEvent = dyn FnMut(&AppHandle, &RunEvent) + Send; type OnPageLoad = dyn FnMut(Window, PageLoadPayload) + Send; type OnDrop = dyn FnOnce(AppHandle) + Send; /// Builds a [`TauriPlugin`]. /// /// This Builder offers a more concise way to construct Tauri plugins than implementing the Plugin trait directly. /// /// # Conventions /// /// When using the Builder Pattern it is encouraged to export a function called `init` that constructs and returns the plugin. /// While plugin authors can provide every possible way to construct a plugin, /// sticking to the `init` function convention helps users to quickly identify the correct function to call. /// /// ```rust /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; /// /// pub fn init() -> TauriPlugin { /// Builder::new("example") /// .build() /// } /// ``` /// /// When plugins expose more complex configuration options, it can be helpful to provide a Builder instead: /// /// ```rust /// use tauri::{plugin::{Builder as PluginBuilder, TauriPlugin}, Runtime}; /// /// pub struct Builder { /// option_a: String, /// option_b: String, /// option_c: bool /// } /// /// impl Default for Builder { /// fn default() -> Self { /// Self { /// option_a: "foo".to_string(), /// option_b: "bar".to_string(), /// option_c: false /// } /// } /// } /// /// impl Builder { /// pub fn new() -> Self { /// Default::default() /// } /// /// pub fn option_a(mut self, option_a: String) -> Self { /// self.option_a = option_a; /// self /// } /// /// pub fn option_b(mut self, option_b: String) -> Self { /// self.option_b = option_b; /// self /// } /// /// pub fn option_c(mut self, option_c: bool) -> Self { /// self.option_c = option_c; /// self /// } /// /// pub fn build(self) -> TauriPlugin { /// PluginBuilder::new("example") /// .setup(move |app_handle| { /// // use the options here to do stuff /// println!("a: {}, b: {}, c: {}", self.option_a, self.option_b, self.option_c); /// /// Ok(()) /// }) /// .build() /// } /// } /// ``` pub struct Builder { name: &'static str, invoke_handler: Box>, setup: Option>>, setup_with_config: Option>>, js_init_script: Option, on_page_load: Box>, on_webview_ready: Box>, on_event: Box>, on_drop: Option>>, } impl Builder { /// Creates a new Plugin builder. pub fn new(name: &'static str) -> Self { Self { name, setup: None, setup_with_config: None, js_init_script: None, invoke_handler: Box::new(|_| ()), on_page_load: Box::new(|_, _| ()), on_webview_ready: Box::new(|_| ()), on_event: Box::new(|_, _| ()), on_drop: None, } } /// Defines the JS message handler callback. /// It is recommended you use the [tauri::generate_handler] to generate the input to this method, as the input type is not considered stable yet. /// /// # Examples /// /// ```rust /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; /// /// #[tauri::command] /// async fn foobar(app: tauri::AppHandle, window: tauri::Window) -> Result<(), String> { /// println!("foobar"); /// /// Ok(()) /// } /// /// fn init() -> TauriPlugin { /// Builder::new("example") /// .invoke_handler(tauri::generate_handler![foobar]) /// .build() /// } /// /// ``` /// [tauri::generate_handler]: ../macro.generate_handler.html #[must_use] pub fn invoke_handler(mut self, invoke_handler: F) -> Self where F: Fn(Invoke) + Send + Sync + 'static, { self.invoke_handler = Box::new(invoke_handler); self } /// Sets the provided JavaScript to be run after the global object has been created, /// but before the HTML document has been parsed and before any other script included by the HTML document is run. /// /// Since it runs on all top-level document and child frame page navigations, /// it's recommended to check the `window.location` to guard your script from running on unexpected origins. /// /// The script is wrapped into its own context with `(function () { /* your script here */ })();`, /// so global variables must be assigned to `window` instead of implicity declared. /// /// Note that calling this function multiple times overrides previous values. /// /// # Examples /// /// ```rust /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; /// /// const INIT_SCRIPT: &str = r#" /// if (window.location.origin === 'https://tauri.app') { /// console.log("hello world from js init script"); /// /// window.__MY_CUSTOM_PROPERTY__ = { foo: 'bar' }; /// } /// "#; /// /// fn init() -> TauriPlugin { /// Builder::new("example") /// .js_init_script(INIT_SCRIPT.to_string()) /// .build() /// } /// ``` #[must_use] pub fn js_init_script(mut self, js_init_script: String) -> Self { self.js_init_script = Some(js_init_script); self } /// Define a closure that runs when the plugin is registered. /// /// This is a convenience function around [setup_with_config], without the need to specify a configuration object. /// /// The closure gets called before the [setup_with_config] closure. /// /// # Examples /// /// ```rust /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime, Manager}; /// use std::path::PathBuf; /// /// #[derive(Debug, Default)] /// struct PluginState { /// dir: Option /// } /// /// fn init() -> TauriPlugin { /// Builder::new("example") /// .setup(|app_handle| { /// app_handle.manage(PluginState::default()); /// /// Ok(()) /// }) /// .build() /// } /// ``` /// /// [setup_with_config]: struct.Builder.html#method.setup_with_config #[must_use] pub fn setup(mut self, setup: F) -> Self where F: FnOnce(&AppHandle) -> Result<()> + Send + 'static, { self.setup.replace(Box::new(setup)); self } /// Define a closure that runs when the plugin is registered, accepting a configuration object set on `tauri.conf.json > plugins > yourPluginName`. /// /// If your plugin is not pulling a configuration object from `tauri.conf.json`, use [setup]. /// /// The closure gets called after the [setup] closure. /// /// # Examples /// /// ```rust,no_run /// #[derive(serde::Deserialize)] /// struct Config { /// api_url: String, /// } /// /// fn init() -> tauri::plugin::TauriPlugin { /// tauri::plugin::Builder::::new("api") /// .setup_with_config(|_app_handle, config| { /// println!("config: {:?}", config.api_url); /// Ok(()) /// }) /// .build() /// } /// /// tauri::Builder::default().plugin(init()); /// ``` /// /// [setup]: struct.Builder.html#method.setup #[must_use] pub fn setup_with_config(mut self, setup_with_config: F) -> Self where F: FnOnce(&AppHandle, C) -> Result<()> + Send + 'static, { self.setup_with_config.replace(Box::new(setup_with_config)); self } /// Callback invoked when the webview performs a navigation to a page. /// /// # Examples /// /// ```rust /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; /// /// fn init() -> TauriPlugin { /// Builder::new("example") /// .on_page_load(|window, payload| { /// println!("Loaded URL {} in window {}", payload.url(), window.label()); /// }) /// .build() /// } /// ``` #[must_use] pub fn on_page_load(mut self, on_page_load: F) -> Self where F: FnMut(Window, PageLoadPayload) + Send + 'static, { self.on_page_load = Box::new(on_page_load); self } /// Callback invoked when the webview is created. /// /// # Examples /// /// ```rust /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; /// /// fn init() -> TauriPlugin { /// Builder::new("example") /// .on_webview_ready(|window| { /// println!("created window {}", window.label()); /// }) /// .build() /// } /// ``` #[must_use] pub fn on_webview_ready(mut self, on_webview_ready: F) -> Self where F: FnMut(Window) + Send + 'static, { self.on_webview_ready = Box::new(on_webview_ready); self } /// Callback invoked when the event loop receives a new event. /// /// # Examples /// /// ```rust /// use tauri::{plugin::{Builder, TauriPlugin}, RunEvent, Runtime}; /// /// fn init() -> TauriPlugin { /// Builder::new("example") /// .on_event(|app_handle, event| { /// match event { /// RunEvent::ExitRequested { api, .. } => { /// // Prevents the app from exiting. /// // This will cause the core thread to continue running in the background even without any open windows. /// api.prevent_exit(); /// } /// // Ignore all other cases. /// _ => {} /// } /// }) /// .build() /// } /// ``` #[must_use] pub fn on_event(mut self, on_event: F) -> Self where F: FnMut(&AppHandle, &RunEvent) + Send + 'static, { self.on_event = Box::new(on_event); self } /// Callback invoked when the plugin is dropped. /// /// # Examples /// /// ```rust /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; /// /// fn init() -> TauriPlugin { /// Builder::new("example") /// .on_drop(|app| { /// println!("plugin has been dropped and is no longer running"); /// // you can run cleanup logic here /// }) /// .build() /// } /// ``` #[must_use] pub fn on_drop(mut self, on_drop: F) -> Self where F: FnOnce(AppHandle) + Send + 'static, { self.on_drop.replace(Box::new(on_drop)); self } /// Builds the [TauriPlugin]. pub fn build(self) -> TauriPlugin { TauriPlugin { name: self.name, app: None, invoke_handler: self.invoke_handler, setup: self.setup, setup_with_config: self.setup_with_config, js_init_script: self.js_init_script, on_page_load: self.on_page_load, on_webview_ready: self.on_webview_ready, on_event: self.on_event, on_drop: self.on_drop, } } } /// Plugin struct that is returned by the [`Builder`]. Should only be constructed through the builder. pub struct TauriPlugin { name: &'static str, app: Option>, invoke_handler: Box>, setup: Option>>, setup_with_config: Option>>, js_init_script: Option, on_page_load: Box>, on_webview_ready: Box>, on_event: Box>, on_drop: Option>>, } impl Drop for TauriPlugin { fn drop(&mut self) { if let (Some(on_drop), Some(app)) = (self.on_drop.take(), self.app.take()) { on_drop(app); } } } impl Plugin for TauriPlugin { fn name(&self) -> &'static str { self.name } fn initialize(&mut self, app: &AppHandle, config: JsonValue) -> Result<()> { self.app.replace(app.clone()); if let Some(s) = self.setup.take() { (s)(app)?; } if let Some(s) = self.setup_with_config.take() { (s)(app, serde_json::from_value(config)?)?; } Ok(()) } fn initialization_script(&self) -> Option { self.js_init_script.clone() } fn created(&mut self, window: Window) { (self.on_webview_ready)(window) } fn on_page_load(&mut self, window: Window, payload: PageLoadPayload) { (self.on_page_load)(window, payload) } fn on_event(&mut self, app: &AppHandle, event: &RunEvent) { (self.on_event)(app, event) } fn extend_api(&mut self, invoke: Invoke) { (self.invoke_handler)(invoke) } } /// Plugin collection type. #[default_runtime(crate::Wry, wry)] pub(crate) struct PluginStore { store: HashMap<&'static str, Box>>, } impl fmt::Debug for PluginStore { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("PluginStore") .field("plugins", &self.store.keys()) .finish() } } impl Default for PluginStore { fn default() -> Self { Self { store: HashMap::new(), } } } impl PluginStore { /// Adds a plugin to the store. /// /// Returns `true` if a plugin with the same name is already in the store. pub fn register + 'static>(&mut self, plugin: P) -> bool { self.store.insert(plugin.name(), Box::new(plugin)).is_some() } /// Removes the plugin with the given name from the store. pub fn unregister(&mut self, plugin: &'static str) -> bool { self.store.remove(plugin).is_some() } /// Initializes all plugins in the store. pub(crate) fn initialize( &mut self, app: &AppHandle, config: &PluginConfig, ) -> crate::Result<()> { self.store.values_mut().try_for_each(|plugin| { plugin .initialize( app, config.0.get(plugin.name()).cloned().unwrap_or_default(), ) .map_err(|e| crate::Error::PluginInitialization(plugin.name().to_string(), e.to_string())) }) } /// Generates an initialization script from all plugins in the store. pub(crate) fn initialization_script(&self) -> String { self .store .values() .filter_map(|p| p.initialization_script()) .fold(String::new(), |acc, script| { format!("{}\n(function () {{ {} }})();", acc, script) }) } /// Runs the created hook for all plugins in the store. pub(crate) fn created(&mut self, window: Window) { self .store .values_mut() .for_each(|plugin| plugin.created(window.clone())) } /// Runs the on_page_load hook for all plugins in the store. pub(crate) fn on_page_load(&mut self, window: Window, payload: PageLoadPayload) { self .store .values_mut() .for_each(|plugin| plugin.on_page_load(window.clone(), payload.clone())) } /// Runs the on_event hook for all plugins in the store. pub(crate) fn on_event(&mut self, app: &AppHandle, event: &RunEvent) { self .store .values_mut() .for_each(|plugin| plugin.on_event(app, event)) } pub(crate) fn extend_api(&mut self, mut invoke: Invoke) { let command = invoke.message.command.replace("plugin:", ""); let mut tokens = command.split('|'); // safe to unwrap: split always has a least one item let target = tokens.next().unwrap(); if let Some(plugin) = self.store.get_mut(target) { invoke.message.command = tokens .next() .map(|c| c.to_string()) .unwrap_or_else(String::new); plugin.extend_api(invoke); } else { invoke .resolver .reject(format!("plugin {} not found", target)); } } }