definitive-guide.md: - Add missing AsyncReadExt import in streaming example - Fix v1/v2 WindowUrl table entry (v1 used WindowUrl not WebviewUrl) - Rename 3-Step Pattern to 4-Step Pattern for plugins agent-prompts.md: - Add new Section 15: Prompt for debugging build/compile errors error-resolution-guide.md: - Improve A3 lib naming convention explanation - Clarify G3 multiple definition root cause - Add actionable WebView2 install commands to H1 - Add O9-O11 mobile build errors (Android SDK, macOS, CocoaPods) cheatsheet.md: - Add deep linking subsection with plugin example - Add gen/schemas/ to file hierarchy - Fix tokio::spawn -> tauri::async_runtime::spawn in WS example - Bump tokio-tungstenite 0.21 -> 0.24 ws_server_client_inject_guide.md: - Add WebSocket.OPEN guard to sendToAllWindows() tauri2-links.md: - Expand from 6 to 16 official documentation links
354 lines
10 KiB
Markdown
354 lines
10 KiB
Markdown
# 🚀 Tauri 2.0 Definitive Cheat Sheet
|
|
|
|
**Tauri 2.0** features a complete architecture overhaul centered around mobile support (iOS/Android), modularized core plugins, and a strict **Access Control List (ACL)** security model. This cheat sheet covers core implementation patterns with up-to-date syntax.
|
|
|
|
---
|
|
|
|
## 🛠️ Project Setup & Structure
|
|
|
|
Tauri 2.0 splits frontend configurations and introduces modular Rust codebases via `lib.rs`.
|
|
|
|
### CLI Management
|
|
```bash
|
|
# Initialize a new app
|
|
npm create tauri-app@latest
|
|
|
|
# Run development mode (Hot-reloads UI + Rust)
|
|
npm run tauri dev
|
|
|
|
# Build production artifacts
|
|
npm run tauri build
|
|
```
|
|
|
|
### File Hierarchy
|
|
```text
|
|
my-app/
|
|
├── src/ # Frontend UI (React, Vue, Svelte, etc.)
|
|
├── src-tauri/ # Backend environment
|
|
│ ├── capabilities/ # NEW: Security capability JSON/TOML files
|
|
│ │ └── default.json # Maps app windows to permissions
|
|
│ ├── gen/
|
|
│ │ └── schemas/ # Generated JSON schemas (referenced by $schema in capabilities)
|
|
│ ├── src/
|
|
│ │ ├── main.rs # Minimal platform entry-point
|
|
│ │ └── lib.rs # Core Application setup & commands
|
|
│ ├── Cargo.toml # Rust dependencies
|
|
│ └── tauri.conf.json # Desktop/Mobile global window configurations
|
|
```
|
|
|
|
---
|
|
|
|
## 🔒 Security: The Tauri 2.0 Permission System
|
|
|
|
Tauri 2.0 enforces explicit privileges. Frontend access to system commands or plugins requires configuring a **Permission** inside a **Capability** map.
|
|
|
|
### 1. Identify Plugin Permissions
|
|
Official plugins expose granular identifier strings:
|
|
* **Core Permissions**: `core:path:allow-home`, `core:event:allow-listen`, `core:event:allow-emit`.
|
|
* **Plugin Permissions**: `<plugin-name>:allow-<command>` (e.g., `http:allow-request`, `fs:allow-read`).
|
|
|
|
### 2. Define Capabilities (`src-tauri/capabilities/default.json`)
|
|
Capabilities attach permissions to specific application windows.
|
|
|
|
```json
|
|
{
|
|
"\$schema": "../gen/schemas/capability.json",
|
|
"identifier": "main-window-capability",
|
|
"description": "Allowed permissions for the primary UI",
|
|
"windows": ["main"],
|
|
"permissions": [
|
|
"core:path:allow-home",
|
|
"core:event:allow-listen",
|
|
"core:event:allow-emit",
|
|
"fs:allow-read",
|
|
"websocket:allow-connect",
|
|
"websocket:allow-send"
|
|
]
|
|
}
|
|
```
|
|
|
|
### 3. Fine-Grained Scopes
|
|
Scopes restrict what a plugin command can touch (e.g., locking file-system access down to a explicit directory). Define these inside your capability entries:
|
|
|
|
```json
|
|
{
|
|
"identifier": "secure-fs-scope",
|
|
"windows": ["main"],
|
|
"permissions": [
|
|
{
|
|
"identifier": "fs:allow-write",
|
|
"allow": [{ "path": "\$APPDATA/logs/*" }]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📡 WebSockets in Tauri 2.0
|
|
|
|
Tauri 2.0 approaches WebSockets from two distinct perspectives depending on requirements: utilizing the client plugin to query a external endpoint, or instantiating a native high-performance Rust WebSocket Server.
|
|
|
|
### Option A: The Built-in WebSocket Client Plugin
|
|
Use this to safely initiate connections to external services from the frontend via a high-performance Rust proxy client.
|
|
|
|
#### 1. Setup Backend Setup (`src-tauri/src/lib.rs`)
|
|
```rust
|
|
// Ensure 'tauri-plugin-websocket' is added to Cargo.toml
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
pub fn run() {
|
|
tauri::Builder::default()
|
|
.plugin(tauri_plugin_websocket::init()) // Initialize client plugin
|
|
.run(tauri::generate_context!())
|
|
.expect("error while running tauri application");
|
|
}
|
|
```
|
|
|
|
#### 2. Frontend Execution (`JavaScript / TypeScript`)
|
|
Ensure `websocket:allow-connect` and `websocket:allow-send` are added to your active capability configuration.
|
|
```typescript
|
|
import WebSocket from '@tauri-apps/plugin-websocket';
|
|
|
|
// Connect to a server
|
|
const ws = await WebSocket.connect('ws://127.0.0.1:8080');
|
|
|
|
// Handle incoming messages
|
|
ws.addListener((msg) => {
|
|
console.log('Received payload:', msg.data);
|
|
});
|
|
|
|
// Push data out
|
|
await ws.send({ type: 'Text', data: 'Hello World!' });
|
|
|
|
// Disconnect gracefully
|
|
await ws.disconnect();
|
|
```
|
|
|
|
### Option B: Built-in Custom Rust WebSocket Server
|
|
If you want your Tauri application to **act as a WebSocket Server** (e.g., exposing a local port so mobile apps or local interfaces can stream data directly to your backend), spin up a Tokio-backed server thread inside the Tauri `setup` stage.
|
|
|
|
#### 1. Dependencies (`src-tauri/Cargo.toml`)
|
|
```toml
|
|
[dependencies]
|
|
tauri = { version = "2.0", features = [] }
|
|
tokio = { version = "1", features = ["full"] }
|
|
tokio-tungstenite = "0.24" # Industry standard high-performance WS crate
|
|
futures-util = "0.3"
|
|
```
|
|
|
|
#### 2. Server Implementation (`src-tauri/src/lib.rs`)
|
|
```rust
|
|
use tauri::Manager;
|
|
use tokio::net::TcpListener;
|
|
use futures-util::{StreamExt, SinkExt};
|
|
use tokio_tungstenite::accept_async;
|
|
|
|
async fn start_ws_server(port: &str) {
|
|
let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
|
|
.await
|
|
.expect("Failed to bind port");
|
|
|
|
while let Ok((stream, _)) = listener.accept().await {
|
|
tauri::async_runtime::spawn(async move {
|
|
if let Ok(mut ws_stream) = accept_async(stream).await {
|
|
println!("New connection established to Tauri WS Server!");
|
|
while let Some(Ok(msg)) = ws_stream.next().await {
|
|
if msg.is_text() || msg.is_binary() {
|
|
// Eco back received payloads
|
|
let _ = ws_stream.send(msg).await;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
pub fn run() {
|
|
tauri::Builder::default()
|
|
.setup(|app| {
|
|
// Spin up native server safely using Tauri's async runtime (NOT tokio::spawn directly)
|
|
tauri::async_runtime::spawn(start_ws_server("8080"));
|
|
Ok(())
|
|
})
|
|
.run(tauri::generate_context!())
|
|
.expect("error while running tauri application");
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## ⚡ Inter-Process Communication (IPC) & State
|
|
|
|
### Commands (Invoking Rust from Frontend)
|
|
Pass data directly via JSON-RPC structures across the isolation barrier.
|
|
|
|
```rust
|
|
// src-tauri/src/lib.rs
|
|
use std::sync::Mutex;
|
|
use tauri::State;
|
|
|
|
pub struct AppState {
|
|
pub counter: Mutex<u32>,
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn increment_counter(state: State<'_, AppState>, value: u32) -> u32 {
|
|
let mut counter = state.counter.lock().unwrap();
|
|
*counter += value;
|
|
*counter // Return to frontend
|
|
}
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
pub fn run() {
|
|
tauri::Builder::default()
|
|
.manage(AppState { counter: Mutex::new(0) }) // Inject State
|
|
.invoke_handler(tauri::generate_handler![increment_counter])
|
|
.run(tauri::generate_context!())
|
|
.expect("failed to run app");
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// Frontend Invoke Pattern
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
|
|
const result = await invoke<number>('increment_counter', { value: 5 });
|
|
console.log(result); // Output: 5
|
|
```
|
|
|
|
### Events (Asynchronous Global Communication)
|
|
Ideal for un-prompted data streaming from Rust to UI components.
|
|
|
|
```rust
|
|
// Backend Emit
|
|
use tauri::{Emitter, AppHandle};
|
|
|
|
fn stream_system_status(app_handle: &AppHandle, load: f32) {
|
|
// Emits a global event to all active window contexts
|
|
app_handle.emit("cpu-status", load).unwrap();
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// Frontend Listen
|
|
import { listen } from '@tauri-apps/api/event';
|
|
|
|
// Make sure 'core:event:allow-listen' is in capabilities
|
|
const unlisten = await listen<number>('cpu-status', (event) => {
|
|
console.log(`CPU utilization: ${event.payload}%`);
|
|
});
|
|
|
|
// Execute unlisten() later to clean up memory hooks
|
|
```
|
|
|
|
---
|
|
|
|
## 📇 Deep Links & Windowing
|
|
|
|
### Multiple Window Spawning
|
|
|
|
```rust
|
|
// Rust implementation
|
|
use tauri::WebviewUrl;
|
|
|
|
fn create_extra_window(app: &tauri::AppHandle) {
|
|
let _window = tauri::WebviewWindowBuilder::new(
|
|
app,
|
|
"secondary_panel",
|
|
WebviewUrl::App("index.html/#/settings".into())
|
|
)
|
|
.title("Settings Panel")
|
|
.inner_size(600.0, 400.0)
|
|
.build()
|
|
.unwrap();
|
|
}
|
|
```
|
|
|
|
### Single Instance Lock
|
|
Ensures only one instance runs, focusing the existing window when a second launch is attempted.
|
|
|
|
```rust
|
|
// src-tauri/Cargo.toml: tauri-plugin-single-instance = "2"
|
|
// src-tauri/capabilities/default.json: add "single-instance:default" to permissions
|
|
|
|
// src-tauri/src/lib.rs
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
pub fn run() {
|
|
tauri::Builder::default()
|
|
.plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
|
|
println!("Attempted alternative launch instances: {:?}, working dir: {:?}", args, cwd);
|
|
// Focus primary window on secondary call triggers
|
|
if let Some(window) = app.get_webview_window("main") {
|
|
let _ = window.set_focus();
|
|
}
|
|
}))
|
|
.run(tauri::generate_context!())
|
|
.expect("failed execution");
|
|
}
|
|
```
|
|
|
|
### Deep Linking with `tauri-plugin-deep-link`
|
|
Registers a custom URL scheme (e.g., `my-app://open?token=xyz`) so the OS routes external links into your running app.
|
|
|
|
#### 1. Setup
|
|
```toml
|
|
# src-tauri/Cargo.toml
|
|
[dependencies]
|
|
tauri-plugin-deep-link = "2"
|
|
```
|
|
```json
|
|
// src-tauri/capabilities/default.json — add permission
|
|
{
|
|
"permissions": ["deep-link:default"]
|
|
}
|
|
```
|
|
```json
|
|
// src-tauri/tauri.conf.json — register the scheme
|
|
{
|
|
"app": {
|
|
"deepLink": {
|
|
"desktop": {
|
|
"schemes": ["my-app"]
|
|
},
|
|
"mobile": {
|
|
"scheme": "myapp"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 2. Rust Handler
|
|
```rust
|
|
// src-tauri/src/lib.rs
|
|
use tauri_plugin_deep_link::DeepLinkExt;
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
pub fn run() {
|
|
tauri::Builder::default()
|
|
.plugin(tauri_plugin_deep_link::init())
|
|
.setup(|app| {
|
|
// macOS / Linux / Windows desktop handler
|
|
#[cfg(desktop)]
|
|
app.deep_link().on_open_url(|url| {
|
|
println!("Deep link opened: {}", url);
|
|
// Parse URL and route to the appropriate handler
|
|
});
|
|
Ok(())
|
|
})
|
|
.run(tauri::generate_context!())
|
|
.expect("failed execution");
|
|
}
|
|
```
|
|
|
|
#### 3. Frontend Listener (optional)
|
|
```typescript
|
|
import { listen } from '@tauri-apps/api/event';
|
|
|
|
// The deep-link plugin emits 'deep-link://new-url' events
|
|
await listen('deep-link://new-url', (event) => {
|
|
console.log('Received deep link:', event.payload);
|
|
});
|
|
```
|