title | i18nReady |
---|---|
Plugin Development |
true |
{/* TODO: Add a CLI section */}
import CommandTabs from '@components/CommandTabs.astro';
{/* TODO: Link to windowing system, commands for sending messages, and event system */}
:::tip[Plugin Development]
This guide is for developing Tauri plugins. If you're looking for a list of the currently available plugins and how to use them then visit the Features and Recipes list.
:::
Plugins are able to hook into the Tauri lifecycle, expose Rust code that relies on the web view APIs, handle commands with Rust, Kotlin or Swift code, and much more.
Tauri offers a windowing system with web view functionality, a way to send messages between the Rust process and the web view, and an event system along with several tools to enhance the development experience. By design, the Tauri core does not contain features not needed by everyone. Instead it offers a mechanism to add external functionalities into a Tauri application called plugins.
A Tauri plugin is composed of a Cargo crate and an optional NPM package that provides API bindings for its commands and events. Additionally, a plugin project can include an Android library project and a Swift package for iOS. You can learn more about developing plugins for Android and iOS in the Mobile Plugin Development guide.
{/* TODO: tauri-apps/tauri#7749 */}
{/* TODO: Add link to allowlist */}
Tauri plugins have a prefix (tauri-plugin-
prefix for the Rust crate name and @tauri-apps/plugin-
for the NPM package) followed by the plugin name. The plugin name is specified on the plugin configuration under tauri.conf.json > plugin
and on the allowlist configuration.
By default Tauri prefixes your plugin crate with tauri-plugin-
. This helps your plugin to be discovered by the Tauri community, but is not not a requirement. When initializing a new plugin project, you must provide its name. The generated crate name will be tauri-plugin-{plugin-name}
and the JavaScript NPM package name will be tauri-plugin-{plugin-name}-api
(although we recommend using an NPM scope if possible). The Tauri naming convention for NPM packages is @scope-name/plugin-{plugin-name}
.
To bootstrap a new plugin project, run plugin init
. If you do not need the NPM package, use the --no-api
CLI flag.
This will initialize the plugin and the resulting code will look like this:
. plugin-name/
├── src/ - Rust code
│ ├── commands.rs - defines the commands the webview can use
| ├── desktop.rs - desktop implementation
│ ├── lib.rs - re-exports appropriate implementation, setup state...
│ └── mobile.rs - mobile implementation
├── android - Android library
├── ios - Swift package
├── webview-src - source code of the JavaScript API bindings
├── webview-dist - Transpiled assets from webview-src
├── Cargo.toml - Cargo crate metadata
└── package.json - NPM package metadata
{/* TODO: tauri-apps/tauri#7749 */}
If you have an existing plugin and would like to add Android or iOS capabilities to it, you can use plugin android add
and plugin ios add
to bootstrap the mobile library projects and guide you through the changes needed.
Plugins can run native mobile code written in Kotlin (or Java) and Swift. The default plugin template includes an Android library project using Kotlin and a Swift package. It includes an example mobile command showing how to trigger its execution from Rust code.
Read more about developing plugins for mobile in the Mobile Plugin Development guide.
In the Tauri application where the plugin is used, the plugin configuration is specified on tauri.conf.json
where plugin-name
is the name of the plugin:
{
"build": { ... },
"tauri": { ... },
"plugins": {
"plugin-name": {
"timeout": 30
}
}
}
The plugin's configuration is set on the Builder
and is parsed at runtime. Here is an example of the Config
struct being used to specify the plugin configuration:
// lib.rs
use tauri::plugin::{Builder, Runtime, TauriPlugin};
use serde::Deserialize;
// Define the plugin config
#[derive(Deserialize)]
struct Config {
timeout: usize,
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
// Make the plugin config optional
// by using `Builder::<R, Option<Config>>` instead
Builder::<R, Config>::new("<plugin-name>")
.setup(|app, api| {
let timeout = api.config.timeout;
Ok(())
})
.build()
}
Plugins can hook into several lifecycle events:
- setup: Plugin is being initialized
- on_navigation: Web view is attempting to perform navigation
- on_webview_ready: New window is being created
- on_event: Event loop events
- on_drop: Plugin is being deconstructed
There are additional lifecycle events for mobile plugins.
- When: Plugin is being initialized
- Why: Register mobile plugins, manage state, run background tasks
use tauri::{Manager, plugin::Builder};
use std::{collections::HashMap, sync::Mutex, time::Duration};
struct DummyStore(Mutex<HashMap<String, String>>);
Builder::new("<plugin-name>")
.setup(|app, api| {
app.manage(DummyStore(Default::default()));
let app_ = app.clone();
std::thread::spawn(move || {
loop {
app_.emit("tick", ());
std::thread::sleep(Duration::from_secs(1));
}
});
Ok(())
})
- When: Web view is attempting to perform navigation
- Why: Validate the navigation or track URL changes
Returning false
cancels the navigation.
use tauri::plugin::Builder;
Builder::new("<plugin-name>")
.on_navigation(|window, url| {
println!("window {} is navigating to {}", window.label(), url);
// Cancels the navigation if forbidden
url.scheme() != "forbidden"
})
- When: New window has been created
- Why: Execute an initialization script for every window
use tauri::plugin::Builder;
Builder::new("<plugin-name>")
.on_webview_ready(|window| {
window.listen("content-loaded", |event| {
println!("webview content has been loaded");
});
})
- When: Event loop events
- Why: Handle core events such as window events, menu events and application exit requested
With this lifecycle hook you can be notified of any event loop events.
use std::{collections::HashMap, fs::write, sync::Mutex};
use tauri::{plugin::Builder, Manager, RunEvent};
struct DummyStore(Mutex<HashMap<String, String>>);
Builder::new("<plugin-name>")
.setup(|app, _api| {
app.manage(DummyStore(Default::default()));
Ok(())
})
.on_event(|app, event| {
match event {
RunEvent::ExitRequested { api, .. } => {
// user requested a window to be closed and there's no windows left
// we can prevent the app from exiting:
api.prevent_exit();
}
RunEvent::Exit => {
// app is going to exit, you can cleanup here
let store = app.state::<DummyStore>();
write(
app.path().app_local_data_dir().unwrap().join("store.json"),
serde_json::to_string(&*store.0.lock().unwrap()).unwrap(),
)
.unwrap();
}
_ => {}
}
})
- When: Plugin is being deconstructed
- Why: Execute code when the plugin has been destroyed
See Drop
for more information.
use tauri::plugin::Builder;
Builder::new("<plugin-name>")
.on_drop(|app| {
// plugin has been destroyed...
})
The plugin APIs defined in the project's desktop.rs
and mobile.rs
are exported to the user as a struct with the same name as the plugin (in pascal case). When the plugin is setup, an instance of this struct is created and managed as a state so that users can retrieve it at any point in time with a Manager
instance (such as AppHandle
, App
, or Window
) through the extension trait defined in the plugin.
For example, the global-shortcut plugin
defines a GlobalShortcut
struct that can be read by using the global_shortcut
method of the GlobalShortcutExt
trait:
use tauri_plugin_global_shortcut::GlobalShortcutExt;
tauri::Builder::default()
.plugin(tauri_plugin_global_shortcut::init())
.setup(|app| {
app.global_shortcut().register(...);
Ok(())
})
Commands are defined in the commands.rs
file. They are regular Tauri applications commands. They can access the AppHandle and Window instances directly, access state, and take input the same way as application commands. Read the Commands guide for more details on Tauri commands.
This command shows how to get access to the AppHandle
and Window
instance via dependency injection, and takes two input parameters (on_progress
and url
):
use tauri::{command, ipc::Channel, AppHandle, Runtime, Window};
#[command]
async fn upload<R: Runtime>(app: AppHandle<R>, window: Window<R>, on_progress: Channel, url: String) {
// implement command logic here
on_progress.send(100).unwrap();
}
To expose the command to the webview, you must hook into the invoke_handler()
call in lib.rs
:
// lib.rs
Builder::new("<plugin-name>")
.invoke_handler(tauri::generate_handler![commands::upload])
Define a binding function in webview-src/index.ts
so that plugin users can easily call the command in JavaScript:
// webview-src/index.ts
import { invoke, Channel } from '@tauri-apps/api/tauri'
export async function upload(url: string, onProgressHandler: (progress: number) => void): Promise<void> {
const onProgress = new Channel<number>()
onProgress.onmessage = onProgressHandler
await invoke('plugin:<plugin-name>|upload', { url, onProgress })
}
Be sure to build the TypeScript code prior to testing it.
A plugin can manage state in the same way a Tauri application does. Read the State Management guide for more information.