Skip to content

Latest commit

 

History

History
303 lines (219 loc) · 10.6 KB

index.mdx

File metadata and controls

303 lines (219 loc) · 10.6 KB
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 */}

Naming Convention

{/* 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}.

Initialize Plugin Project

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.

Mobile Plugin Development

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.

Plugin Configuration

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()
}

Lifecycle Events

Plugins can hook into several lifecycle events:

There are additional lifecycle events for mobile plugins.

setup

  • 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(())
  })

on_navigation

  • 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"
  })

on_webview_ready

  • 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");
    });
  })

on_event

  • 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();
      }
      _ => {}
    }
  })

on_drop

  • 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...
  })

Exposing Rust APIs

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(())
  })

Adding Commands

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.

Managing State

A plugin can manage state in the same way a Tauri application does. Read the State Management guide for more information.