use crate::{
app::UriSchemeResponder,
ipc::{Invoke, InvokeHandler, ScopeObject, ScopeValue},
manager::webview::UriSchemeProtocol,
utils::config::PluginConfig,
webview::PageLoadPayload,
AppHandle, Error, RunEvent, Runtime, UriSchemeContext, Webview, Window,
};
use serde::{
de::{Deserialize, DeserializeOwned, Deserializer, Error as DeError},
Serialize, Serializer,
};
use serde_json::Value as JsonValue;
use tauri_macros::default_runtime;
use thiserror::Error;
use url::Url;
use std::{
borrow::Cow,
collections::HashMap,
fmt::{self, Debug},
sync::Arc,
};
#[cfg(mobile)]
pub mod mobile;
pub trait Plugin<R: Runtime>: Send {
fn name(&self) -> &'static str;
#[allow(unused_variables)]
fn initialize(
&mut self,
app: &AppHandle<R>,
config: JsonValue,
) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
fn initialization_script(&self) -> Option<String> {
None
}
#[allow(unused_variables)]
fn window_created(&mut self, window: Window<R>) {}
#[allow(unused_variables)]
fn webview_created(&mut self, webview: Webview<R>) {}
#[allow(unused_variables)]
fn on_navigation(&mut self, webview: &Webview<R>, url: &Url) -> bool {
true
}
#[allow(unused_variables)]
fn on_page_load(&mut self, webview: &Webview<R>, payload: &PageLoadPayload<'_>) {}
#[allow(unused_variables)]
fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {}
#[allow(unused_variables)]
fn extend_api(&mut self, invoke: Invoke<R>) -> bool {
false
}
}
type SetupHook<R, C> =
dyn FnOnce(&AppHandle<R>, PluginApi<R, C>) -> Result<(), Box<dyn std::error::Error>> + Send;
type OnWindowReady<R> = dyn FnMut(Window<R>) + Send;
type OnWebviewReady<R> = dyn FnMut(Webview<R>) + Send;
type OnEvent<R> = dyn FnMut(&AppHandle<R>, &RunEvent) + Send;
type OnNavigation<R> = dyn Fn(&Webview<R>, &Url) -> bool + Send;
type OnPageLoad<R> = dyn FnMut(&Webview<R>, &PageLoadPayload<'_>) + Send;
type OnDrop<R> = dyn FnOnce(AppHandle<R>) + Send;
#[derive(Debug)]
#[allow(dead_code)]
pub struct PluginHandle<R: Runtime> {
name: &'static str,
handle: AppHandle<R>,
}
impl<R: Runtime> Clone for PluginHandle<R> {
fn clone(&self) -> Self {
Self {
name: self.name,
handle: self.handle.clone(),
}
}
}
impl<R: Runtime> PluginHandle<R> {
pub fn app(&self) -> &AppHandle<R> {
&self.handle
}
}
#[derive(Clone)]
#[allow(dead_code)]
pub struct PluginApi<R: Runtime, C: DeserializeOwned> {
handle: AppHandle<R>,
name: &'static str,
raw_config: Arc<JsonValue>,
config: C,
}
impl<R: Runtime, C: DeserializeOwned> PluginApi<R, C> {
pub fn config(&self) -> &C {
&self.config
}
pub fn app(&self) -> &AppHandle<R> {
&self.handle
}
pub fn scope<T: ScopeObject>(&self) -> crate::Result<ScopeValue<T>> {
self
.handle
.manager
.runtime_authority
.lock()
.unwrap()
.scope_manager
.get_global_scope_typed(&self.handle, self.name)
}
}
#[derive(Debug, Clone, Hash, PartialEq, Error)]
#[non_exhaustive]
pub enum BuilderError {
#[error("plugin uses reserved name: {0}")]
ReservedName(String),
}
const RESERVED_PLUGIN_NAMES: &[&str] = &["core", "tauri"];
pub struct Builder<R: Runtime, C: DeserializeOwned = ()> {
name: &'static str,
invoke_handler: Box<InvokeHandler<R>>,
setup: Option<Box<SetupHook<R, C>>>,
js_init_script: Option<String>,
on_navigation: Box<OnNavigation<R>>,
on_page_load: Box<OnPageLoad<R>>,
on_window_ready: Box<OnWindowReady<R>>,
on_webview_ready: Box<OnWebviewReady<R>>,
on_event: Box<OnEvent<R>>,
on_drop: Option<Box<OnDrop<R>>>,
uri_scheme_protocols: HashMap<String, Arc<UriSchemeProtocol<R>>>,
}
impl<R: Runtime, C: DeserializeOwned> Builder<R, C> {
pub fn new(name: &'static str) -> Self {
Self {
name,
setup: None,
js_init_script: None,
invoke_handler: Box::new(|_| false),
on_navigation: Box::new(|_, _| true),
on_page_load: Box::new(|_, _| ()),
on_window_ready: Box::new(|_| ()),
on_webview_ready: Box::new(|_| ()),
on_event: Box::new(|_, _| ()),
on_drop: None,
uri_scheme_protocols: Default::default(),
}
}
#[must_use]
pub fn invoke_handler<F>(mut self, invoke_handler: F) -> Self
where
F: Fn(Invoke<R>) -> bool + Send + Sync + 'static,
{
self.invoke_handler = Box::new(invoke_handler);
self
}
#[must_use]
pub fn js_init_script(mut self, js_init_script: String) -> Self {
self.js_init_script = Some(js_init_script);
self
}
#[must_use]
pub fn setup<F>(mut self, setup: F) -> Self
where
F: FnOnce(&AppHandle<R>, PluginApi<R, C>) -> Result<(), Box<dyn std::error::Error>>
+ Send
+ 'static,
{
self.setup.replace(Box::new(setup));
self
}
#[must_use]
pub fn on_navigation<F>(mut self, on_navigation: F) -> Self
where
F: Fn(&Webview<R>, &Url) -> bool + Send + 'static,
{
self.on_navigation = Box::new(on_navigation);
self
}
#[must_use]
pub fn on_page_load<F>(mut self, on_page_load: F) -> Self
where
F: FnMut(&Webview<R>, &PageLoadPayload<'_>) + Send + 'static,
{
self.on_page_load = Box::new(on_page_load);
self
}
#[must_use]
pub fn on_window_ready<F>(mut self, on_window_ready: F) -> Self
where
F: FnMut(Window<R>) + Send + 'static,
{
self.on_window_ready = Box::new(on_window_ready);
self
}
#[must_use]
pub fn on_webview_ready<F>(mut self, on_webview_ready: F) -> Self
where
F: FnMut(Webview<R>) + Send + 'static,
{
self.on_webview_ready = Box::new(on_webview_ready);
self
}
#[must_use]
pub fn on_event<F>(mut self, on_event: F) -> Self
where
F: FnMut(&AppHandle<R>, &RunEvent) + Send + 'static,
{
self.on_event = Box::new(on_event);
self
}
#[must_use]
pub fn on_drop<F>(mut self, on_drop: F) -> Self
where
F: FnOnce(AppHandle<R>) + Send + 'static,
{
self.on_drop.replace(Box::new(on_drop));
self
}
#[must_use]
pub fn register_uri_scheme_protocol<
N: Into<String>,
T: Into<Cow<'static, [u8]>>,
H: Fn(UriSchemeContext<'_, R>, http::Request<Vec<u8>>) -> http::Response<T>
+ Send
+ Sync
+ 'static,
>(
mut self,
uri_scheme: N,
protocol: H,
) -> Self {
self.uri_scheme_protocols.insert(
uri_scheme.into(),
Arc::new(UriSchemeProtocol {
protocol: Box::new(move |ctx, request, responder| {
responder.respond(protocol(ctx, request))
}),
}),
);
self
}
#[must_use]
pub fn register_asynchronous_uri_scheme_protocol<
N: Into<String>,
H: Fn(UriSchemeContext<'_, R>, http::Request<Vec<u8>>, UriSchemeResponder) + Send + Sync + 'static,
>(
mut self,
uri_scheme: N,
protocol: H,
) -> Self {
self.uri_scheme_protocols.insert(
uri_scheme.into(),
Arc::new(UriSchemeProtocol {
protocol: Box::new(protocol),
}),
);
self
}
pub fn try_build(self) -> Result<TauriPlugin<R, C>, BuilderError> {
if let Some(&reserved) = RESERVED_PLUGIN_NAMES.iter().find(|&r| r == &self.name) {
return Err(BuilderError::ReservedName(reserved.into()));
}
Ok(TauriPlugin {
name: self.name,
app: None,
invoke_handler: self.invoke_handler,
setup: self.setup,
js_init_script: self.js_init_script,
on_navigation: self.on_navigation,
on_page_load: self.on_page_load,
on_window_ready: self.on_window_ready,
on_webview_ready: self.on_webview_ready,
on_event: self.on_event,
on_drop: self.on_drop,
uri_scheme_protocols: self.uri_scheme_protocols,
})
}
pub fn build(self) -> TauriPlugin<R, C> {
self.try_build().expect("valid plugin")
}
}
pub struct TauriPlugin<R: Runtime, C: DeserializeOwned = ()> {
name: &'static str,
app: Option<AppHandle<R>>,
invoke_handler: Box<InvokeHandler<R>>,
setup: Option<Box<SetupHook<R, C>>>,
js_init_script: Option<String>,
on_navigation: Box<OnNavigation<R>>,
on_page_load: Box<OnPageLoad<R>>,
on_window_ready: Box<OnWindowReady<R>>,
on_webview_ready: Box<OnWebviewReady<R>>,
on_event: Box<OnEvent<R>>,
on_drop: Option<Box<OnDrop<R>>>,
uri_scheme_protocols: HashMap<String, Arc<UriSchemeProtocol<R>>>,
}
impl<R: Runtime, C: DeserializeOwned> Drop for TauriPlugin<R, C> {
fn drop(&mut self) {
if let (Some(on_drop), Some(app)) = (self.on_drop.take(), self.app.take()) {
on_drop(app);
}
}
}
impl<R: Runtime, C: DeserializeOwned> Plugin<R> for TauriPlugin<R, C> {
fn name(&self) -> &'static str {
self.name
}
fn initialize(
&mut self,
app: &AppHandle<R>,
config: JsonValue,
) -> Result<(), Box<dyn std::error::Error>> {
self.app.replace(app.clone());
if let Some(s) = self.setup.take() {
(s)(
app,
PluginApi {
name: self.name,
handle: app.clone(),
raw_config: Arc::new(config.clone()),
config: serde_json::from_value(config).map_err(|err| {
format!(
"Error deserializing 'plugins.{}' within your Tauri configuration: {err}",
self.name
)
})?,
},
)?;
}
for (uri_scheme, protocol) in &self.uri_scheme_protocols {
app
.manager
.webview
.register_uri_scheme_protocol(uri_scheme, protocol.clone())
}
Ok(())
}
fn initialization_script(&self) -> Option<String> {
self.js_init_script.clone()
}
fn window_created(&mut self, window: Window<R>) {
(self.on_window_ready)(window)
}
fn webview_created(&mut self, webview: Webview<R>) {
(self.on_webview_ready)(webview)
}
fn on_navigation(&mut self, webview: &Webview<R>, url: &Url) -> bool {
(self.on_navigation)(webview, url)
}
fn on_page_load(&mut self, webview: &Webview<R>, payload: &PageLoadPayload<'_>) {
(self.on_page_load)(webview, payload)
}
fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {
(self.on_event)(app, event)
}
fn extend_api(&mut self, invoke: Invoke<R>) -> bool {
(self.invoke_handler)(invoke)
}
}
#[default_runtime(crate::Wry, wry)]
pub(crate) struct PluginStore<R: Runtime> {
store: Vec<Box<dyn Plugin<R>>>,
}
impl<R: Runtime> fmt::Debug for PluginStore<R> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let plugins: Vec<&str> = self.store.iter().map(|plugins| plugins.name()).collect();
f.debug_struct("PluginStore")
.field("plugins", &plugins)
.finish()
}
}
impl<R: Runtime> Default for PluginStore<R> {
fn default() -> Self {
Self { store: Vec::new() }
}
}
impl<R: Runtime> PluginStore<R> {
pub fn register(&mut self, plugin: Box<dyn Plugin<R>>) -> bool {
let len = self.store.len();
self.store.retain(|p| p.name() != plugin.name());
let result = len != self.store.len();
self.store.push(plugin);
result
}
pub fn unregister(&mut self, plugin: &'static str) -> bool {
let len = self.store.len();
self.store.retain(|p| p.name() != plugin);
len != self.store.len()
}
pub(crate) fn initialize(
&self,
plugin: &mut Box<dyn Plugin<R>>,
app: &AppHandle<R>,
config: &PluginConfig,
) -> crate::Result<()> {
initialize(plugin, app, config)
}
pub(crate) fn initialize_all(
&mut self,
app: &AppHandle<R>,
config: &PluginConfig,
) -> crate::Result<()> {
self
.store
.iter_mut()
.try_for_each(|plugin| initialize(plugin, app, config))
}
pub(crate) fn initialization_script(&self) -> Vec<String> {
self
.store
.iter()
.filter_map(|p| p.initialization_script())
.map(|script| format!("(function () {{ {script} }})();"))
.collect()
}
pub(crate) fn window_created(&mut self, window: Window<R>) {
self.store.iter_mut().for_each(|plugin| {
#[cfg(feature = "tracing")]
let _span = tracing::trace_span!("plugin::hooks::created", name = plugin.name()).entered();
plugin.window_created(window.clone())
})
}
pub(crate) fn webview_created(&mut self, webview: Webview<R>) {
self
.store
.iter_mut()
.for_each(|plugin| plugin.webview_created(webview.clone()))
}
pub(crate) fn on_navigation(&mut self, webview: &Webview<R>, url: &Url) -> bool {
for plugin in self.store.iter_mut() {
#[cfg(feature = "tracing")]
let _span =
tracing::trace_span!("plugin::hooks::on_navigation", name = plugin.name()).entered();
if !plugin.on_navigation(webview, url) {
return false;
}
}
true
}
pub(crate) fn on_page_load(&mut self, webview: &Webview<R>, payload: &PageLoadPayload<'_>) {
self.store.iter_mut().for_each(|plugin| {
#[cfg(feature = "tracing")]
let _span =
tracing::trace_span!("plugin::hooks::on_page_load", name = plugin.name()).entered();
plugin.on_page_load(webview, payload)
})
}
pub(crate) fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {
self
.store
.iter_mut()
.for_each(|plugin| plugin.on_event(app, event))
}
pub(crate) fn extend_api(&mut self, plugin: &str, invoke: Invoke<R>) -> bool {
for p in self.store.iter_mut() {
if p.name() == plugin {
#[cfg(feature = "tracing")]
let _span = tracing::trace_span!("plugin::hooks::ipc", name = plugin).entered();
return p.extend_api(invoke);
}
}
invoke.resolver.reject(format!("plugin {plugin} not found"));
true
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(name = "plugin::hooks::initialize", skip(plugin, app), fields(name = plugin.name())))]
fn initialize<R: Runtime>(
plugin: &mut Box<dyn Plugin<R>>,
app: &AppHandle<R>,
config: &PluginConfig,
) -> crate::Result<()> {
plugin
.initialize(
app,
config.0.get(plugin.name()).cloned().unwrap_or_default(),
)
.map_err(|e| Error::PluginInitialization(plugin.name().to_string(), e.to_string()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
pub enum PermissionState {
Granted,
Denied,
#[default]
Prompt,
PromptWithRationale,
}
impl std::fmt::Display for PermissionState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Granted => write!(f, "granted"),
Self::Denied => write!(f, "denied"),
Self::Prompt => write!(f, "prompt"),
Self::PromptWithRationale => write!(f, "prompt-with-rationale"),
}
}
}
impl Serialize for PermissionState {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
impl<'de> Deserialize<'de> for PermissionState {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = <String as Deserialize>::deserialize(deserializer)?;
match s.to_lowercase().as_str() {
"granted" => Ok(Self::Granted),
"denied" => Ok(Self::Denied),
"prompt" => Ok(Self::Prompt),
"prompt-with-rationale" => Ok(Self::PromptWithRationale),
_ => Err(DeError::custom(format!("unknown permission state '{s}'"))),
}
}
}