diff --git a/tauri2-docs/ws_server_client_inject_guide.md b/tauri2-docs/ws_server_client_inject_guide.md index e69de29..79e91f2 100644 --- a/tauri2-docs/ws_server_client_inject_guide.md +++ b/tauri2-docs/ws_server_client_inject_guide.md @@ -0,0 +1,554 @@ +# WebSocket Server, Client & Script Injection Guide + +> Based on the working `moxie-app` reference implementation in this repository. +> This guide documents the complete pattern for running a native WebSocket server inside Tauri 2.0, connecting frontend clients, and injecting scripts into external webview windows. + +--- + +## Overview + +This guide covers three interrelated patterns used in the reference `moxie-app`: + +1. **Native WebSocket Server** — A Tokio-backed WebSocket server running inside the Tauri process +2. **Frontend WebSocket Client** — JavaScript clients connecting to the local server for bidirectional communication +3. **Script Injection** — Injecting JavaScript into dynamically created webview windows (including external URLs) to bridge them into the WebSocket network + +### Architecture Diagram + +```text +┌─────────────────────────────────────────────────────────┐ +│ Tauri Process │ +│ │ +│ ┌──────────────┐ ┌──────────────────────────────┐ │ +│ │ lib.rs │ │ Tokio WebSocket Server │ │ +│ │ │────▶│ (ws://127.0.0.1:8080) │ │ +│ │ .setup() │ │ │ │ +│ │ spawns │ │ broadcast channel (16 msg) │ │ +│ │ server │ │ │ │ +│ └──────────────┘ └───────────┬───────────────────┘ │ +│ │ │ +│ ┌─────────────────────────┼─────────────────┐ │ +│ │ │ │ │ +│ ┌──────▼──────┐ ┌───────────────▼──────────┐ │ │ +│ │ Main Window │ │ Remote Window │ │ │ +│ │ (index.html) │ │ (external URL) │ │ │ +│ │ │ │ + injected script │ │ │ +│ │ WebSocket │ │ + WebSocket client │ │ │ +│ │ Client (JS) │ │ Client (injected JS) │ │ │ +│ └──────┬──────┘ └───────────────┬──────────┘ │ │ +└─────────┼─────────────────────────┼────────────────┘ │ + │ │ │ + │ WebSocket (local) │ │ + └─────────────────────────┘ │ +``` + +--- + +## Part 1: Native WebSocket Server (Rust) + +### Why Run a WebSocket Server Inside Tauri? + +Standard Tauri IPC (`invoke`, events, channels) only works between Tauri's own windows and its Rust backend. When you need to: + +- Communicate with **external applications** running on the same machine +- Bridge **external URLs** loaded in webview windows back to the Tauri backend +- Enable **cross-window communication** at scale (beyond Tauri's event system) +- Create a **local API endpoint** for mobile apps or other tools to connect to + +...a native WebSocket server running on localhost is the solution. + +### Dependencies (`Cargo.toml`) + +```toml +[dependencies] +tauri = { version = "2", features = [] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["net", "rt", "sync", "macros"] } +tokio-tungstenite = "0.24" +futures-util = "0.3" +``` + +**Key dependencies explained:** +- `tokio` — The async runtime Tauri uses internally. The `net` feature provides `TcpListener`, `sync` provides `broadcast` channels, `rt` provides the runtime, and `macros` provides `tokio::select!`. +- `tokio-tungstenite` — Industry-standard WebSocket implementation for Tokio. Provides `accept_async` for accepting WS connections and the `Message` type for frame construction. +- `futures-util` — Provides `StreamExt` (for `.next()` on streams) and `SinkExt` (for `.send()` on sinks). + +### Server Implementation (`src-tauri/src/server.rs`) + +```rust +use tokio::net::TcpListener; +use tokio_tungstenite::accept_async; +use futures_util::stream::StreamExt; +use futures_util::sink::SinkExt; +use tokio::sync::broadcast; + +pub async fn start_websocket_server() { + // Bind to localhost only (security: not exposed to network) + let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap(); + println!("WebSocket Server listening on ws://127.0.0.1:8080"); + + // Broadcast channel: fans out messages to ALL connected clients + // Queue depth of 16 means if a slow client hasn't read 16 messages, + // the oldest undelivered message is dropped (lagging policy) + let (tx, _rx) = broadcast::channel::(16); + + // Accept connections in a loop + while let Ok((stream, _addr)) = listener.accept().await { + // Clone the broadcast transmitter for this connection's task + let tx = tx.clone(); + // Create a dedicated receiver for this connection + let mut rx = tx.subscribe(); + + // Spawn a separate async task per client connection + tauri::async_runtime::spawn(async move { + // Perform the WebSocket handshake + if let Ok(ws_stream) = accept_async(stream).await { + println!("New WebSocket client connected"); + let (mut ws_sender, mut ws_receiver) = ws_stream.split(); + + // Main message loop using tokio::select! + loop { + tokio::select! { + // Branch A: Message FROM this specific client + incoming = ws_receiver.next() => { + match incoming { + Some(Ok(msg)) if msg.is_text() => { + let text = msg.to_text().unwrap().to_string(); + println!("Received from client: {}", text); + + // Broadcast to ALL other connected clients + let _ = tx.send(text); + } + Some(Ok(msg)) if msg.is_binary() => { + // Handle binary data if needed + let _ = tx.send(format!("[binary:{}bytes]", msg.len())); + } + _ => { + // None = client disconnected, Err = error + println!("Client disconnected"); + break; + } + } + } + // Branch B: Broadcast message FROM another client + broadcast_msg = rx.recv() => { + if let Ok(payload) = broadcast_msg { + // Construct a WebSocket text frame and send it + let frame = tokio_tungstenite::tungstenite::Message::Text( + payload.into() + ); + if let Err(e) = ws_sender.send(frame).await { + eprintln!("Failed to send to client: {}", e); + break; + } + } + } + } + } + } else { + eprintln!("WebSocket handshake failed"); + } + }); + } +} +``` + +### How the Broadcast Pattern Works + +```text +Client A sends "hello" ─────▶ Rust Server ─────▶ broadcast("hello") ─────▶ Client B receives "hello" + ─────▶ Client C receives "hello" + ─────▶ Client D receives "hello" + +Client B sends "world" ─────▶ Rust Server ─────▶ broadcast("world") ─────▶ Client A receives "world" + ─────▶ Client C receives "world" + ─────▶ Client D receives "world" +``` + +Each client: +1. Gets its own `rx = tx.subscribe()` receiver +2. The main loop uses `tokio::select!` to wait on BOTH incoming messages AND broadcast messages simultaneously +3. When a client sends a message, it goes through `tx.send()` which broadcasts to all OTHER clients (the sender's own receiver won't get its own message because `subscribe()` only receives messages sent after subscription) + +### Spawning the Server from `lib.rs` + +```rust +// src-tauri/src/lib.rs +mod server; + +use tauri::Manager; + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) + .setup(|_app| { + // Spawn the WebSocket server as a background task + // This runs on Tauri's managed Tokio runtime + tauri::async_runtime::spawn(server::start_websocket_server()); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![greet]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +**Key details:** +- `tauri::async_runtime::spawn()` is used instead of `tokio::spawn()` to ensure the task runs on Tauri's managed async runtime +- The server task runs for the entire lifetime of the application +- The `.setup()` closure returns immediately — the server runs in the background + +--- + +## Part 2: Frontend WebSocket Client + +### Main Window Connection (`src/main.js`) + +The main application window connects to the WebSocket server using the standard browser `WebSocket` API: + +```javascript +const { invoke } = window.__TAURI__.core; + +// Connect to the local WebSocket server +const mainSocket = new WebSocket('ws://127.0.0.1:8080'); + +mainSocket.onopen = () => { + console.log('Main Control Window connected to the broadcast hub!'); +}; + +mainSocket.onmessage = (event) => { + const data = JSON.parse(event.data); + console.log('Received broadcast:', data); + + // Handle specific message types + if (data.localsession) { + document.querySelector('#session-display').textContent = + `Session: ${data.localsession}`; + } +}; + +mainSocket.onerror = (err) => { + console.error('WebSocket connection error:', err); +}; + +// Sending data to other connected windows +function sendToAllWindows(payload) { + mainSocket.send(JSON.stringify(payload)); +} + +// Example: button click sends data +document.querySelector('#broadcast-btn').addEventListener('click', () => { + sendToAllWindows({ + sender: 'main-window', + type: 'notification', + content: 'Hello from the main window!' + }); +}); +``` + +### Message Protocol + +While you can send any format over WebSocket, using JSON with a consistent structure is recommended: + +```javascript +// Standard message envelope +{ + "sender": "main-window", // Origin identifier + "type": "action-type", // Message category + "content": "payload data", // Message body + "timestamp": 1234567890 // Optional: for ordering +} +``` + +### Connection Lifecycle Management + +For production apps, handle reconnection and cleanup: + +```javascript +class TauriSocket { + constructor(url) { + this.url = url; + this.ws = null; + this.reconnectDelay = 1000; + this.maxReconnectDelay = 30000; + this.listeners = new Map(); + } + + connect() { + this.ws = new WebSocket(this.url); + + this.ws.onopen = () => { + console.log('Connected to local WS server'); + this.reconnectDelay = 1000; // Reset on successful connect + this.emit('connected'); + }; + + this.ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + this.emit('message', data); + } catch (e) { + console.error('Failed to parse WS message:', e); + } + }; + + this.ws.onclose = () => { + console.log('Disconnected, reconnecting...'); + this.emit('disconnected'); + setTimeout(() => this.connect(), this.reconnectDelay); + this.reconnectDelay = Math.min( + this.reconnectDelay * 1.5, + this.maxReconnectDelay + ); + }; + + this.ws.onerror = (err) => { + console.error('WebSocket error:', err); + }; + } + + send(data) { + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(data)); + } + } + + on(event, callback) { + this.listeners.set(event, callback); + } + + emit(event, data) { + this.listeners.get(event)?.(data); + } + + disconnect() { + this.ws?.close(); + } +} + +// Usage +const socket = new TauriSocket('ws://127.0.0.1:8080'); +socket.connect(); +socket.on('message', (data) => console.log('Got:', data)); +``` + +--- + +## Part 3: Script Injection into Webview Windows + +### The Use Case + +When your Tauri app opens windows with **external URLs** (e.g., a web-based admin panel, a third-party dashboard, or any remote page), those pages have no knowledge of Tauri or your local WebSocket server. Script injection allows you to: + +1. **Bridge the external page** into your local communication network +2. **Extract data** from the page (localStorage, DOM, cookies) and relay it to Tauri +3. **Modify the page** (UI changes, injected controls, overlays) +4. **Share sessions** between windows (e.g., pass auth tokens) + +### Injection via `initialization_script` + +The `WebviewWindowBuilder::initialization_script()` method injects JavaScript that runs when the webview's page loads. Combined with `include_str!()`, you can load scripts from external files at compile time: + +```rust +// src-tauri/src/lib.rs +use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder}; + +#[tauri::command] +async fn open_remote_session(app_handle: AppHandle, url: String) -> Result { + let target_url = url.parse() + .map(WebviewUrl::External) + .map_err(|_| format!("Invalid URL: {}", url))?; + + let window_label = "remote-session"; + + // Check if window already exists — focus it instead of duplicating + if let Some(existing) = app_handle.get_webview_window(window_label) { + let _ = existing.set_focus(); + return Ok(format!("Focused existing window for {}", url)); + } + + // Load the injection script at compile time + let script_to_inject = include_str!("../extWebview.js"); + + let app_clone = app_handle.clone(); + let url_clone = target_url.clone(); + + // Window creation MUST happen on the main thread + app_handle.run_on_main_thread(move || { + let _window = WebviewWindowBuilder::new(&app_clone, window_label, url_clone) + .title("Remote Session") + .inner_size(1024.0, 768.0) + .resizable(true) + .focused(true) + .initialization_script(script_to_inject) // ← Inject JS here + .build(); + }).map_err(|e| format!("Failed to dispatch to main thread: {}", e))?; + + Ok(format!("Dispatched window spawn for: {}", url)) +} +``` + +### The Injected Script (`src-tauri/extWebview.js`) + +```javascript +// extWebview.js — This script runs automatically when the webview loads +window.addEventListener('DOMContentLoaded', () => { + console.log('[TAURI] Initializing injected socket link...'); + + const ws = new WebSocket('ws://127.0.0.1:8080'); + + ws.onopen = () => { + console.log('[TAURI] Injected socket connected to backend!'); + + // Announce this window's identity to the Tauri backend + const payload = { + sender: 'remote-webview', + type: 'identify', + url: window.location.href, + title: document.title + }; + ws.send(JSON.stringify(payload)); + }; + + ws.onmessage = (event) => { + const data = JSON.parse(event.data); + console.log('[TAURI] RECEIVED FROM BACKEND:', data); + + // Respond with session/token data if requested + if (data.type === 'request-session') { + ws.send(JSON.stringify({ + sender: 'remote-webview', + type: 'session-response', + localsession: window.localStorage.getItem('auth-token') || 'no-session' + })); + } + + // Handle commands from the backend + if (data.type === 'execute-action') { + // Perform actions on the remote page as instructed by Tauri + console.log('[TAURI] Executing action:', data.action); + } + }; + + ws.onerror = (err) => { + console.error('[TAURI] Injected socket error:', err); + }; + + // Expose the socket on window for the page's own code to use + window.localAppSocket = ws; + + // Periodically send status updates + setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ + sender: 'remote-webview', + type: 'heartbeat', + url: window.location.href + })); + } + }, 30000); +}); +``` + +### CSP Considerations + +Content Security Policy (CSP) can block script injection on external pages. If you need injection on external URLs: + +```json +// tauri.conf.json — disable CSP (use with caution!) +{ + "app": { + "security": { + "csp": null + } + } +} +``` + +For better security, craft a specific CSP that allows your injection patterns while blocking other sources: + +```json +{ + "app": { + "security": { + "csp": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src ws://127.0.0.1:* http://127.0.0.1:* https:;" + } + } +} +``` + +--- + +## Complete Working Example Flow + +### Step-by-step data flow through the entire system: + +```text +1. App starts → lib.rs setup() → spawns WebSocket server on ws://127.0.0.1:8080 + +2. Main window (index.html) loads → main.js creates WebSocket client + → connects to ws://127.0.0.1:8080 + → onopen fires → ready to communicate + +3. User enters URL and clicks button → main.js calls invoke('greet', { name: url }) + +4. Rust greet() command: + → parses URL as WebviewUrl::External + → checks if window already exists (focus if so) + → loads extWebview.js via include_str!() + → dispatches window creation to main thread + → creates new WebviewWindow with external URL + injected script + +5. Remote window loads external page → extWebview.js runs on DOMContentLoaded + → creates WebSocket client + → connects to ws://127.0.0.1:8080 + → sends identification message + +6. Server broadcasts identification to all clients + → Main window receives it → displays session info + → Other remote windows receive it (if any) + +7. Main window user clicks "Send" → main.js sends JSON via WebSocket + → Server broadcasts to all clients + → All remote windows receive and log the message +``` + +--- + +## Choosing Between Approaches + +| Requirement | Recommended Approach | +|-------------|---------------------| +| Connect to external WebSocket server | `tauri-plugin-websocket` (client plugin) | +| App IS the WebSocket server | Native Tokio server (this guide) | +| Tauri-to-frontend communication | Tauri events (`emit`/`listen`) | +| High-throughput streaming data | Tauri channels (`Channel`) | +| Cross-window messaging (internal) | Tauri events or native WS server | +| External page needs to talk to Tauri | Native WS server + script injection | +| Multiple external pages sharing state | Native WS server + script injection | + +--- + +## Troubleshooting + +### "Address already in use" (port 8080) +- Another process is using port 8080 +- Solution: Change the port in both `server.rs` and all frontend connection code +- Or kill the process using port 8080: `lsof -i :8080` / `netstat -ano | findstr :8080` + +### WebSocket connection fails from injected scripts +- CSP may be blocking the connection → set `"csp": null` in config +- The injected script runs before the page's own scripts → ensure DOM is ready via `DOMContentLoaded` +- External HTTPS pages may refuse unencrypted WebSocket connections → page must allow mixed content + +### Messages not broadcasting to other windows +- Each client needs its own `rx = tx.subscribe()` receiver +- The `tx.send()` call broadcasts to all subscribers EXCEPT the sender's own subscription +- Ensure `tokio::select!` has both branches (incoming AND broadcast) + +### Window creation fails from async command +- Window creation MUST happen on the main thread +- Use `app_handle.run_on_main_thread(move || { ... })` for async contexts +- Not doing this will cause a panic or silent failure diff --git a/tauri2-guide/agent-prompts.md b/tauri2-guide/agent-prompts.md new file mode 100644 index 0000000..43641a2 --- /dev/null +++ b/tauri2-guide/agent-prompts.md @@ -0,0 +1,509 @@ +# Tauri 2.0 Agent Prompts — Definitive Collection + +> **Purpose**: Copy-paste ready prompts for instructing AI agents to build Tauri 2.0 applications. +> **Constraint**: These prompts are designed for sandbox environments where code compilation is NOT available. Agents must verify correctness using logic, API references, and pattern matching. + +--- + +## Table of Contents + +1. [Universal System Prompt (Required Prefix)](#1-universal-system-prompt-required-prefix) +2. [Prompt: Scaffold a New Tauri 2.0 Project](#2-prompt-scaffold-a-new-tauri-20-project) +3. [Prompt: Add a Tauri Plugin](#3-prompt-add-a-tauri-plugin) +4. [Prompt: Create a Custom Command (IPC)](#4-prompt-create-a-custom-command-ipc) +5. [Prompt: Add State Management](#5-prompt-add-state-management) +6. [Prompt: Implement Event-Based Communication](#6-prompt-implement-event-based-communication) +7. [Prompt: Create Multi-Window App](#7-prompt-create-multi-window-app) +8. [Prompt: Build a WebSocket Server in Rust](#8-prompt-build-a-websocket-server-in-rust) +9. [Prompt: Implement Script Injection](#9-prompt-implement-script-injection) +10. [Prompt: Set Up Capabilities & Permissions](#10-prompt-set-up-capabilities--permissions) +11. [Prompt: Migrate a v1 App to v2](#11-prompt-migrate-a-v1-app-to-v2) +12. [Prompt: Full App Build (End-to-End)](#12-prompt-full-app-build-end-to-end) +13. [Verification Checklist for Agents](#13-verification-checklist-for-agents) +14. [Quick Reference: v1 vs v2 API Map](#14-quick-reference-v1-vs-v2-api-map) + +--- + +## 1. Universal System Prompt (Required Prefix) + +> **Always include this as the system context before any task-specific prompt.** + +``` +You are a Tauri 2.0 expert developer. You build desktop and mobile applications using the Tauri 2.0 framework (Rust backend + web frontend). + +CRITICAL ENVIRONMENT CONSTRAINTS: +- You CANNOT compile or run code in this sandbox. Do NOT attempt compilation. +- Verify correctness using logic, API pattern matching, and reference knowledge only. +- Cross-reference every code snippet against known Tauri 2.0 API patterns before outputting. +- If you are uncertain about an API or pattern, say so explicitly rather than guessing. + +MANDATORY TAURI 2.0 RULES — NEVER VIOLATE: +1. This is Tauri 2.0 (NOT v1). The APIs are fundamentally different. +2. Use `tauri::WebviewWindow` (NOT `tauri::Window`). +3. Use `tauri::WebviewWindowBuilder` (NOT `tauri::WindowBuilder`). +4. Use `tauri::WebviewUrl` (NOT `tauri::WindowUrl`). +5. Use `Manager::get_webview_window()` (NOT `Manager::get_window()`). +6. Use `tauri::Emitter` trait for `emit()` and `emit_to()` (NOT the old window-only emit). +7. Use `tauri::Listener` trait for `listen()` and `once()` on the Rust side. +8. JS import `invoke` from `@tauri-apps/api/core` (NOT `@tauri-apps/api/tauri`). +9. JS import window utilities from `@tauri-apps/api/webviewWindow` (NOT `@tauri-apps/api/window`). +10. JS plugins import from `@tauri-apps/plugin-` (NOT `@tauri-apps/api/`). +11. Config uses `app` section (NOT `tauri` section), `build.frontendDist` (NOT `build.distDir`). +12. Security uses Capabilities + Permissions (NOT `allowlist`). +13. Every plugin requires: (a) Rust crate in Cargo.toml, (b) `.plugin()` registration in Builder, (c) npm package, (d) permissions in capabilities. +14. Use the `lib.rs` + `main.rs` pattern. Core logic goes in `lib.rs`. `main.rs` only calls `lib::run()`. +15. `#[cfg_attr(mobile, tauri::mobile_entry_point)]` attribute is REQUIRED on the `pub fn run()` function. +16. `invoke_handler` can only be called ONCE — all commands go in a single `tauri::generate_handler![]`. +17. Async commands cannot use borrowed references (`&str`, `&Path`) directly — use owned types or wrap return in `Result`. +18. Window creation from async contexts MUST use `app_handle.run_on_main_thread()`. +19. Always clean up event listeners (call `unlisten()`) on the frontend. +20. Use `tauri = { version = "2" }` and `tauri-build = { version = "2" }` (NOT version 1). + +FILE STRUCTURE: +- Frontend source: `src/` +- Rust backend: `src-tauri/` +- Rust entry-point: `src-tauri/src/main.rs` (minimal) +- Core app logic: `src-tauri/src/lib.rs` +- Extra modules: `src-tauri/src/.rs` +- Capabilities: `src-tauri/capabilities/default.json` +- Config: `src-tauri/tauri.conf.json` +- Rust deps: `src-tauri/Cargo.toml` +- Build script: `src-tauri/build.rs` + +OFFICIAL DOCUMENTATION: +- Main docs: https://v2.tauri.app +- Rust API: https://docs.rs/tauri/latest/tauri/ +- Migration guide: https://v2.tauri.app/start/migrate/from-tauri-1/ +- Capabilities: https://v2.tauri.app/security/capabilities/ +- Permissions: https://v2.tauri.app/security/permissions/ +- Configuration: https://v2.tauri.app/develop/configuration-files/ +- Calling Rust: https://v2.tauri.app/develop/calling-rust/ +- Calling Frontend: https://v2.tauri.app/develop/calling-frontend/ +- Plugin index: https://v2.tauri.app/plugin/ +``` + +--- + +## 2. Prompt: Scaffold a New Tauri 2.0 Project + +``` +Create a new Tauri 2.0 project from scratch with vanilla JavaScript (no frontend framework). + +Requirements: +- App name: [APP_NAME] +- Window title: [WINDOW_TITLE] +- Default window size: [WIDTH]x[HEIGHT] +- withGlobalTauri enabled (for window.__TAURI__ access without npm package) + +Generate ALL of the following files with complete, correct Tauri 2.0 code: + +1. `package.json` — with @tauri-apps/cli v2 devDependency and scripts (dev, build, tauri) +2. `src/index.html` — basic HTML5 boilerplate +3. `src/main.js` — demonstrate invoking a Rust command +4. `src/styles.css` — basic styling +5. `src-tauri/tauri.conf.json` — correct v2 structure (app section, build section, bundle section) +6. `src-tauri/Cargo.toml` — with tauri v2, tauri-build v2, serde, serde_json. Include [lib] with crate-type ["staticlib", "cdylib", "rlib"] +7. `src-tauri/build.rs` — standard tauri_build::build() +8. `src-tauri/capabilities/default.json` — with core:default permission +9. `src-tauri/src/main.rs` — minimal entry-point calling lib::run() +10. `src-tauri/src/lib.rs` — with Builder pattern, mobile_entry_point attribute, a sample "greet" command, and the command registered in invoke_handler + +Verify every line follows Tauri 2.0 conventions. Cross-check config structure against the v2 schema. +``` + +--- + +## 3. Prompt: Add a Tauri Plugin + +``` +Add the [PLUGIN_NAME] plugin to an existing Tauri 2.0 project. + +Plugin to add: [tauri-plugin-STORE/turi-plugin-FS/etc.] +JS package name: [@tauri-apps/plugin-store/etc.] + +Instructions: +1. Add the Rust crate to `src-tauri/Cargo.toml` as `tauri-plugin-[name] = "2"` +2. Register the plugin in `src-tauri/src/lib.rs` using `.plugin(tauri_plugin_[name]::init())` or the Builder pattern if the plugin requires configuration +3. Add the npm package: `@tauri-apps/plugin-[name]` +4. Update `src-tauri/capabilities/default.json` to add the appropriate permission (usually `[name]:default` or specific allow permissions) +5. Provide a frontend usage example showing the correct v2 import from `@tauri-apps/plugin-[name]` + +Show the exact changes needed for each file. Do NOT use v1 import paths or API patterns. +``` + +--- + +## 4. Prompt: Create a Custom Command (IPC) + +``` +Create a Tauri 2.0 command called "[COMMAND_NAME]" that [DESCRIBE WHAT IT DOES]. + +Requirements: +- Define the command using `#[tauri::command]` in a separate module file `src-tauri/src/commands.rs` +- The command must be `pub` (required for separate modules) +- Include proper error handling with `Result` or a custom error type +- Register the command in `src-tauri/src/lib.rs` using `mod commands;` and including it in `generate_handler![commands::COMMAND_NAME]` +- Provide the frontend JavaScript code to invoke the command using `invoke()` from `@tauri-apps/api/core` or `window.__TAURI__.core` +- If the command is async, ensure all arguments are owned types (not borrowed references) + +Show complete, correct code for both the Rust side and the JS side. +``` + +--- + +## 5. Prompt: Add State Management + +``` +Add shared state management to an existing Tauri 2.0 application. + +Requirements: +1. Define a state struct in `src-tauri/src/lib.rs` using `std::sync::Mutex` for thread safety +2. Manage the state in the Builder using `.manage(MyState { ... })` +3. Create at least two commands that read and write the managed state using `tauri::State<'_, MyState>` +4. Show the frontend code for invoking these commands + +If persistent storage is also needed: +- Add `tauri-plugin-store = "2"` to Cargo.toml +- Register the plugin +- Add `store:default` to capabilities +- Show frontend usage with `@tauri-apps/plugin-store` +``` + +--- + +## 6. Prompt: Implement Event-Based Communication + +``` +Implement bidirectional event communication in a Tauri 2.0 application. + +Requirements: + +Rust side: +- Use `use tauri::{Emitter, AppHandle}` to emit events from Rust +- Demonstrate both global emit (`app.emit()`) and targeted emit (`app.emit_to()`) +- If listening for events on the Rust side, use `use tauri::Listener` and `app.listen()` or `app.once()` + +Frontend side: +- Import from `@tauri-apps/api/event` (NOT `@tauri-apps/api/tauri`) +- Show `listen()`, `once()`, `emit()`, and `emitTo()` usage +- Demonstrate proper cleanup with `unlisten()` +- Show targeted listening using `getCurrentWebviewWindow().listen()` + +Show complete code for: +1. A Rust command that emits a progress event +2. A Rust command that emits a completion event to a specific window +3. Frontend listeners that handle these events +4. Frontend emitting an event back to Rust + +Make sure `core:event:default` (or specific `core:event:allow-*` permissions) is included in capabilities. +``` + +--- + +## 7. Prompt: Create Multi-Window App + +``` +Create a Tauri 2.0 application with multiple windows. + +Requirements: +1. Main window defined in `tauri.conf.json` with label "main" +2. A command `open_secondary` that creates a new window dynamically using `WebviewWindowBuilder` +3. The secondary window should load a local HTML file (`WebviewUrl::App("secondary.html".into())`) +4. If creating the window from an async context, use `app_handle.run_on_main_thread()` +5. Check for existing windows with the same label using `app_handle.get_webview_window(label)` and focus it if it exists +6. Show frontend code for creating windows using `new WebviewWindow(label, options)` from `@tauri-apps/api/webviewWindow` + +CRITICAL: Use `tauri::WebviewWindowBuilder` and `tauri::WebviewUrl` (NOT the v1 `WindowBuilder`/`WindowUrl`). +CRITICAL: Use `app_handle.get_webview_window()` (NOT `app_handle.get_window()`). +``` + +--- + +## 8. Prompt: Build a WebSocket Server in Rust + +``` +Build a native WebSocket server that runs inside a Tauri 2.0 application using the Tokio async runtime. + +Requirements: + +Dependencies (Cargo.toml): +- `tokio = { version = "1", features = ["net", "rt", "sync", "macros"] }` +- `tokio-tungstenite = "0.24"` +- `futures-util = "0.3"` + +Server implementation (`src-tauri/src/server.rs`): +- Bind a TcpListener to `127.0.0.1:[PORT]` +- Use `tokio::sync::broadcast` channel for message fan-out to all connected clients +- Accept WebSocket connections using `tokio_tungstenite::accept_async` +- Use `tokio::select!` to handle both incoming messages from clients AND broadcast messages from other clients +- Handle disconnections gracefully +- Spawn the server in `src-tauri/src/lib.rs` using `tauri::async_runtime::spawn(server::start_websocket_server())` inside the `.setup()` closure + +Frontend client: +- Connect using standard `new WebSocket('ws://127.0.0.1:[PORT]')` +- Show send and receive handlers + +Key pattern: Each client connection gets its own broadcast receiver via `tx.subscribe()`, enabling fan-out to all connected clients. +``` + +--- + +## 9. Prompt: Implement Script Injection + +``` +Implement JavaScript injection into a Tauri 2.0 WebviewWindow. + +Requirements: +1. Create a JavaScript file (e.g., `src-tauri/inject.js`) that will be injected into webview windows +2. The injected script should: + - Connect to a local WebSocket server (ws://127.0.0.1:[PORT]) + - Send data from the webview page (e.g., localStorage contents) back to the server + - Receive data from the server and process it + - Expose the socket connection on `window.localAppSocket` for the page's own code to use +3. Use `include_str!("../inject.js")` to load the script at compile time +4. Inject it using `WebviewWindowBuilder::initialization_script(script_content)` +5. Show a complete working example of creating a window with an external URL and injecting the script + +IMPORTANT: Set `"csp": null` in the app security config if injecting scripts into external URLs, otherwise CSP may block the injection. + +Show both the Rust window creation code with injection and the JavaScript injection script. +``` + +--- + +## 10. Prompt: Set Up Capabilities & Permissions + +``` +Set up the Tauri 2.0 Capabilities and Permissions system for an application. + +The application needs access to: +- [LIST REQUIRED FEATURES: file system, dialogs, HTTP requests, clipboard, notifications, etc.] + +Requirements: +1. Create `src-tauri/capabilities/default.json` with proper v2 structure +2. Include the `$schema` pointing to `../gen/schemas/desktop-schema.json` +3. Set the `identifier`, `description`, `windows` (list of window labels), and `platforms` fields +4. Add the correct permission identifiers for each required feature: + - File system: `fs:default` or specific `fs:allow-*` + - Dialogs: `dialog:default` or specific `dialog:allow-*` + - HTTP: `http:default` or specific `http:allow-*` + - Events: `core:event:default` + - Window management: `core:window:default` +5. If fine-grained scopes are needed (e.g., restricting file access to specific directories), show scoped permissions with `allow` and `deny` arrays using path variables like `$APPDATA`, `$HOME`, `$RESOURCE` + +CRITICAL: Do NOT use the v1 `allowlist` in `tauri.conf.json`. In v2, all permissions are defined via capability files. +``` + +--- + +## 11. Prompt: Migrate a v1 App to v2 + +``` +Migrate the following Tauri v1 code to Tauri 2.0. + +[PASTE v1 CODE HERE] + +Migration checklist — verify ALL of these changes: + +Config (tauri.conf.json): +- [ ] `tauri` section renamed to `app` +- [ ] `build.distDir` renamed to `build.frontendDist` +- [ ] `build.devPath` renamed to `build.devUrl` +- [ ] `build.withGlobalTauri` moved to `app.withGlobalTauri` +- [ ] `tauri.allowlist` REMOVED → replaced by capabilities +- [ ] `tauri.bundle` promoted to top-level `bundle` +- [ ] `bundle.identifier` promoted to top-level `identifier` +- [ ] `tauri.updater` moved to `plugins.updater` + +Project structure: +- [ ] `main.rs` logic moved to `lib.rs` with `pub fn run()` +- [ ] New minimal `main.rs` calling `lib::run()` +- [ ] `Cargo.toml` has `[lib]` section with `crate-type = ["staticlib", "cdylib", "rlib"]` +- [ ] `#[cfg_attr(mobile, tauri::mobile_entry_point)]` added to `run()` + +Rust code: +- [ ] All `tauri::api::*` imports REMOVED (replaced by plugins) +- [ ] `tauri::Window` → `tauri::WebviewWindow` +- [ ] `tauri::WindowBuilder` → `tauri::WebviewWindowBuilder` +- [ ] `tauri::WindowUrl` → `tauri::WebviewUrl` +- [ ] `get_window()` → `get_webview_window()` +- [ ] `tauri::Emitter` trait imported for `emit()` / `emit_to()` +- [ ] `tauri::Listener` trait imported for `listen()` / `once()` +- [ ] All plugins registered with `.plugin()` in Builder +- [ ] `tauri` and `tauri-build` version set to "2" + +JavaScript code: +- [ ] `@tauri-apps/api/tauri` → `@tauri-apps/api/core` +- [ ] `@tauri-apps/api/window` → `@tauri-apps/api/webviewWindow` +- [ ] All plugin imports → `@tauri-apps/plugin-` +- [ ] `@tauri-apps/cli` version set to "^2" + +Capabilities: +- [ ] Created `src-tauri/capabilities/default.json` +- [ ] All necessary permissions listed +- [ ] `core:default` included at minimum + +Output the complete migrated code for every file that changed. +``` + +--- + +## 12. Prompt: Full App Build (End-to-End) + +``` +Build a complete Tauri 2.0 application with the following requirements: + +App Name: [APP_NAME] +Purpose: [DESCRIBE APP PURPOSE] + +Features needed: +- [FEATURE 1: e.g., File system read/write] +- [FEATURE 2: e.g., HTTP API client] +- [FEATURE 3: e.g., Persistent settings storage] +- [FEATURE 4: e.g., System tray icon] +- [FEATURE 5: e.g., Multi-window support] +- [FEATURE 6: e.g., Custom Rust commands] + +Frontend: [Vanilla JS / React / Vue / Svelte / etc.] + +Generate ALL files for a complete, working Tauri 2.0 application: +1. `package.json` +2. `src/index.html` (or appropriate entry point for the chosen framework) +3. Frontend source files (JavaScript/TypeScript/CSS) +4. `src-tauri/tauri.conf.json` — v2 structure with all necessary config +5. `src-tauri/Cargo.toml` — all dependencies including required plugins +6. `src-tauri/build.rs` +7. `src-tauri/capabilities/default.json` — all required permissions +8. `src-tauri/src/main.rs` — minimal entry-point +9. `src-tauri/src/lib.rs` — Builder with all plugins, commands, setup +10. Additional Rust module files as needed +11. Any injected scripts if required + +For every file: +- Use ONLY valid Tauri 2.0 code +- Use the correct v2 import paths +- Use v2 type names (WebviewWindow, WebviewUrl, etc.) +- Include proper error handling +- Add appropriate comments explaining the v2 patterns used + +Do NOT skip any file. Generate complete, production-ready code. +``` + +--- + +## 13. Verification Checklist for Agents + +> Agents should run through this checklist mentally for EVERY code snippet they generate. + +### Config Verification +- [ ] `tauri.conf.json` uses `app` section (not `tauri`) +- [ ] `build.frontendDist` (not `build.distDir`) +- [ ] `build.devUrl` (not `build.devPath`) +- [ ] `withGlobalTauri` is under `app` (not `build`) +- [ ] `identifier` is at top level (not under `bundle`) +- [ ] `bundle` is at top level (not under `tauri`) +- [ ] No `allowlist` present anywhere + +### Rust Code Verification +- [ ] `tauri` version is `"2"` (not `"1"`) +- [ ] `tauri-build` version is `"2"` (not `"1"`) +- [ ] `Cargo.toml` has `[lib]` with `crate-type = ["staticlib", "cdylib", "rlib"]` +- [ ] `main.rs` only contains `#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]` and `fn main() { lib_name::run() }` +- [ ] `lib.rs` has `#[cfg_attr(mobile, tauri::mobile_entry_point)]` on `pub fn run()` +- [ ] No `tauri::api::*` imports anywhere +- [ ] `WebviewWindow` used (not `Window`) +- [ ] `WebviewWindowBuilder` used (not `WindowBuilder`) +- [ ] `WebviewUrl` used (not `WindowUrl`) +- [ ] `get_webview_window()` used (not `get_window()`) +- [ ] `Emitter` trait imported when using `emit()` or `emit_to()` +- [ ] `Listener` trait imported when using `listen()` or `once()` +- [ ] `invoke_handler` called only ONCE with ALL commands +- [ ] Async commands use owned types or Result wrapping (not bare `&str`) +- [ ] Window creation in async contexts uses `run_on_main_thread()` +- [ ] All plugins registered with `.plugin()` before `.run()` + +### Frontend Code Verification +- [ ] `invoke` imported from `@tauri-apps/api/core` (not `@tauri-apps/api/tauri`) +- [ ] `WebviewWindow` imported from `@tauri-apps/api/webviewWindow` (not `@tauri-apps/api/window`) +- [ ] Event functions from `@tauri-apps/api/event` (not core or tauri) +- [ ] Plugin imports from `@tauri-apps/plugin-` (not `@tauri-apps/api/`) +- [ ] `@tauri-apps/cli` is version `^2` +- [ ] Event listeners cleaned up with `unlisten()` +- [ ] Argument names in `invoke()` calls match Rust command parameter names (camelCase by default) + +### Capabilities Verification +- [ ] Capability files exist in `src-tauri/capabilities/` +- [ ] `$schema` points to correct schema path +- [ ] All used plugins have corresponding permissions +- [ ] Window labels in capabilities match actual window labels +- [ ] No `allowlist` in config files + +### Plugin Verification +- [ ] Rust crate: `tauri-plugin- = "2"` in Cargo.toml +- [ ] Registration: `.plugin(tauri_plugin_::init())` in Builder +- [ ] JS package: `@tauri-apps/plugin-` in dependencies +- [ ] Permissions: Listed in capabilities file + +--- + +## 14. Quick Reference: v1 vs v2 API Map + +> Keep this table handy when migrating or reviewing code. + +### Rust Type Renames +| v1 | v2 | +|----|----| +| `tauri::Window` | `tauri::WebviewWindow` | +| `tauri::WindowBuilder` | `tauri::WebviewWindowBuilder` | +| `tauri::WindowUrl` | `tauri::WebviewUrl` | +| `Manager::get_window()` | `Manager::get_webview_window()` | +| `tauri::WindowEvent` | `tauri::WebviewWindowEvent` | +| `tauri::Manager::fs_scope()` | `tauri_plugin_fs::FsExt` | + +### Rust API Removals (replaced by plugins) +| v1 | v2 Plugin | +|----|-----------| +| `tauri::api::dialog` | `tauri-plugin-dialog` | +| `tauri::api::fs` | `std::fs` / `tauri-plugin-fs` | +| `tauri::api::http` | `tauri-plugin-http` | +| `tauri::api::path` | `tauri::Manager::path()` | +| `tauri::api::process::Command` | `tauri-plugin-shell` | +| `tauri::api::clipboard` | `tauri-plugin-clipboard-manager` | +| `tauri::api::notification` | `tauri-plugin-notification` | +| `tauri::api::global_shortcut` | `tauri-plugin-global-shortcut` | +| `tauri::api::updater` | `tauri-plugin-updater` | +| `tauri::api::cli` | `tauri-plugin-cli` | +| `tauri::api::os` | `tauri-plugin-os` | +| `tauri::api::version` | `semver` crate | + +### JavaScript Import Path Changes +| v1 | v2 | +|----|----| +| `@tauri-apps/api/tauri` | `@tauri-apps/api/core` | +| `@tauri-apps/api/window` | `@tauri-apps/api/webviewWindow` | +| `@tauri-apps/api/dialog` | `@tauri-apps/plugin-dialog` | +| `@tauri-apps/api/fs` | `@tauri-apps/plugin-fs` | +| `@tauri-apps/api/http` | `@tauri-apps/plugin-http` | +| `@tauri-apps/api/notification` | `@tauri-apps/plugin-notification` | +| `@tauri-apps/api/clipboard` | `@tauri-apps/plugin-clipboard-manager` | +| `@tauri-apps/api/shell` | `@tauri-apps/plugin-shell` | +| `@tauri-apps/api/global-shortcut` | `@tauri-apps/plugin-global-shortcut` | +| `@tauri-apps/api/updater` | `@tauri-apps/plugin-updater` | +| `@tauri-apps/api/os` | `@tauri-apps/plugin-os` | +| `@tauri-apps/api/process` | `@tauri-apps/plugin-process` | +| `@tauri-apps/api/cli` | `@tauri-apps/plugin-cli` | + +### Config Path Changes +| v1 | v2 | +|----|----| +| `build.distDir` | `build.frontendDist` | +| `build.devPath` | `build.devUrl` | +| `build.withGlobalTauri` | `app.withGlobalTauri` | +| `tauri.*` | `app.*` | +| `tauri.allowlist.*` | Capabilities system | +| `tauri.windows.fileDropEnabled` | `app.windows.dragDropEnabled` | +| `tauri.bundle` | `bundle` (top-level) | +| `bundle.identifier` | `identifier` (top-level) | +| `tauri.updater` | `plugins.updater` | +| `tauri.systemTray` | `app.trayIcon` | +| `tauri.cli` | `plugins.cli` | diff --git a/tauri2-guide/definitive-guide.md b/tauri2-guide/definitive-guide.md new file mode 100644 index 0000000..ae08823 --- /dev/null +++ b/tauri2-guide/definitive-guide.md @@ -0,0 +1,1816 @@ +# Tauri 2.0 Definitive Developer Guide + +> **Version**: 2.0 | **Last Updated**: 2026-05-31 +> **Official Docs**: https://v2.tauri.app +> **Reference App**: `moxie-app/` (included in this repository) + +--- + +## Table of Contents + +1. [Introduction & Architecture Overview](#1-introduction--architecture-overview) +2. [Project Structure & Scaffolding](#2-project-structure--scaffolding) +3. [Configuration System (`tauri.conf.json`)](#3-configuration-system-tauriconfjson) +4. [Capabilities & Permissions (Security Model)](#4-capabilities--permissions-security-model) +5. [Rust Backend: `lib.rs` & Commands](#5-rust-backend-librs--commands) +6. [Frontend Integration Patterns](#6-frontend-integration-patterns) +7. [IPC: Commands, Events & Channels](#7-ipc-commands-events--channels) +8. [Window Management](#8-window-management) +9. [Plugin Ecosystem](#9-plugin-ecosystem) +10. [Native WebSocket Server (Rust-side)](#10-native-websocket-server-rust-side) +11. [Script Injection into Webview Windows](#11-script-injection-into-webview-windows) +12. [State Management](#12-state-management) +13. [v1 to v2 Migration Reference](#13-v1-to-v2-migration-reference) +14. [Common Anti-Patterns to Avoid](#14-common-anti-patterns-to-avoid) +15. [Quick Reference Cheat Sheet](#15-quick-reference-cheat-sheet) + +--- + +## 1. Introduction & Architecture Overview + +Tauri 2.0 is a framework for building small, fast binaries for all major desktop and mobile platforms. It uses a **hybrid architecture** where a Rust backend handles system-level operations while a web frontend (HTML/CSS/JS, or any framework like React/Vue/Svelte) renders the UI inside a native OS webview. + +### Core Architecture Principles + +- **Rust Core (`src-tauri/`)**: Handles system access, file operations, network calls, window management, and any heavy computation. All system interactions MUST go through Rust to maintain security boundaries. +- **Web Frontend (`src/`)**: Renders the UI using standard web technologies. Communicates with Rust exclusively through Tauri's IPC mechanisms (`invoke`, events, channels). +- **Security Boundary**: The frontend runs in a sandboxed webview. It cannot directly access the filesystem, network, or OS APIs — it must request these through Tauri commands and plugins, which are governed by the **Capabilities & Permissions** system. +- **Mobile Support (NEW in v2)**: Tauri 2.0 adds first-class iOS and Android support. This requires a specific project structure (`lib.rs` + `main.rs` pattern) and platform-specific capability configurations. + +### What Changed from v1 to v2 + +Tauri 2.0 is a **complete architectural overhaul**, not an incremental update. The most significant changes are: + +| Area | v1 | v2 | +|------|----|----| +| Security | `allowlist` in config | **Capabilities + Permissions** (ACL-based) | +| APIs | Built-in `tauri::api` module | **Everything is a plugin** | +| Window type | `Window` / `WindowBuilder` | **`WebviewWindow` / `WebviewWindowBuilder`** | +| Config structure | Nested `tauri >` key | Flattened: `app`, `bundle`, `build` | +| Event system | Per-window scoped | **`emit()` is global; `emit_to()` for targeting** | +| JS imports | `@tauri-apps/api/tauri` | **`@tauri-apps/api/core`** | +| Mobile support | Community plugins | **First-class iOS/Android support** | +| Project structure | `main.rs` only | **`lib.rs` + `main.rs` pattern required** | + +--- + +## 2. Project Structure & Scaffolding + +### Standard Tauri 2.0 File Hierarchy + +```text +my-app/ +├── src/ # Frontend UI source +│ ├── index.html # Main HTML entry +│ ├── main.js # Frontend JavaScript +│ └── styles.css # Styles +├── src-tauri/ # Rust backend environment +│ ├── capabilities/ # Security capability definitions +│ │ └── default.json # Maps windows → permissions +│ ├── icons/ # App icons for all platforms +│ ├── src/ +│ │ ├── main.rs # Minimal desktop entry-point +│ │ ├── lib.rs # Core application setup & commands +│ │ └── .rs # Additional Rust modules +│ ├── build.rs # Tauri build script +│ ├── Cargo.toml # Rust dependencies +│ ├── Cargo.lock # Lockfile (COMMIT THIS) +│ └── tauri.conf.json # Main Tauri configuration +├── package.json # Node.js dependencies & scripts +└── package-lock.json +``` + +### Creating a New Project + +```bash +# Using the interactive CLI scaffolder +npm create tauri-app@latest + +# Or with specific template +npm create tauri-app@latest -- --template vanilla-ts + +# Cargo alternative +cargo create-tauri-app +``` + +Supported templates: Vanilla, Vue, Svelte, React, Solid, Angular, Preact, Yew, Leptos, Sycamore. + +### Adding Tauri to an Existing Frontend + +```bash +npm install -D @tauri-apps/cli@latest +npx tauri init +``` + +The `init` command will prompt for: app name, window title, web assets location, dev server URL, and frontend build/dev commands. + +### The `lib.rs` + `main.rs` Pattern (Required in v2) + +Tauri 2.0 requires your core application logic to live in `lib.rs`, with a minimal `main.rs` that calls into it. This is **mandatory** for mobile support because mobile platforms require a shared library entry-point rather than a standard `main()` function. + +**`src-tauri/src/main.rs`** — Desktop-only entry-point: +```rust +// Prevents additional console window on Windows in release mode +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + my_app_lib::run() +} +``` + +**`src-tauri/src/lib.rs`** — Core application: +```rust +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + // plugins, commands, setup, etc. + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +**`src-tauri/Cargo.toml`** — Must declare the library: +```toml +[lib] +name = "my_app_lib" +crate-type = ["staticlib", "cdylib", "rlib"] +``` + +The `_lib` suffix in the crate name prevents naming conflicts with the binary on Windows. The three crate types cover: static linking (mobile), dynamic linking (mobile), and Rust library usage (desktop/tests). + +### Development Commands + +```bash +# Run in development mode (hot-reloads UI + auto-recompiles Rust) +npm run tauri dev +# or: pnpm tauri dev, bun tauri dev, cargo tauri dev + +# Build production artifacts +npm run tauri build +``` + +--- + +## 3. Configuration System (`tauri.conf.json`) + +### Main Configuration File + +The primary configuration file is `src-tauri/tauri.conf.json`. Tauri 2.0 supports three formats: + +| Format | Feature Flag Required | +|--------|-----------------------| +| **JSON** (default) | None | +| **JSON5** | `config-json5` on both `tauri` and `tauri-build` | +| **TOML** | `config-toml` on both `tauri` and `tauri-build` | + +### Complete v2 Configuration Structure + +```json +{ + "$schema": "https://tauri.app", + "productName": "my-app", + "version": "1.0.0", + "identifier": "com.mycompany.myapp", + "build": { + "frontendDist": "../dist", + "devUrl": "http://localhost:1420", + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "npm run build" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "label": "main", + "title": "My App", + "width": 800, + "height": 600, + "resizable": true, + "fullscreen": false, + "center": true, + "dragDropEnabled": true, + "useHttpsScheme": false + } + ], + "security": { + "csp": "default-src 'self'; style-src 'self' 'unsafe-inline'", + "assetProtocol": { + "scope": ["$APPDATA/**", "$RESOURCE/**"] + } + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "licenseFile": "LICENSE", + "copyright": "", + "category": "Utility" + } +} +``` + +### Key Configuration Changes from v1 + +| v1 Path | v2 Path | Notes | +|---------|---------|-------| +| `package.productName` | Top-level `productName` | Moved out of `package` | +| `package.version` | Top-level `version` | Moved out of `package` | +| `package` | *(removed)* | Fields redistributed | +| `build.distDir` | `build.frontendDist` | Renamed | +| `build.devPath` | `build.devUrl` | Renamed | +| `build.withGlobalTauri` | `app.withGlobalTauri` | Moved to `app` section | +| `tauri.*` | `app.*` | Top-level key renamed | +| `tauri.allowlist` | *(removed)* | Replaced by capabilities | +| `tauri.windows.fileDropEnabled` | `app.windows.dragDropEnabled` | Renamed | +| `tauri.bundle` | Top-level `bundle` | Promoted to top-level | +| `tauri.updater` | `plugins.updater` | Moved to plugins | +| `tauri.systemTray` | `app.trayIcon` | Renamed | +| `tauri.cli` | `plugins.cli` | Moved to plugins | +| `bundle.identifier` | Top-level `identifier` | Promoted to top-level | + +### Platform-Specific Configuration + +Create platform override files that **merge** with the main config using JSON Merge Patch (RFC 7396). Arrays are replaced entirely (not element-by-element merged). + +| Platform | File Pattern | +|----------|-------------| +| Linux | `tauri.linux.conf.json` or `Tauri.linux.toml` | +| Windows | `tauri.windows.conf.json` or `Tauri.windows.toml` | +| macOS | `tauri.macos.conf.json` or `Tauri.macos.toml` | +| Android | `tauri.android.conf.json` or `Tauri.android.toml` | +| iOS | `tauri.ios.conf.json` or `Tauri.ios.toml` | + +### Cargo.toml Dependencies + +```toml +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +``` + +**Important**: Keep `tauri`, `tauri-build`, and `@tauri-apps/cli` on the same minor version. Always commit `Cargo.lock` for reproducible builds. + +### package.json Scripts + +```json +{ + "scripts": { + "tauri": "tauri", + "dev": "tauri dev", + "build": "tauri build" + }, + "devDependencies": { + "@tauri-apps/cli": "^2" + } +} +``` + +The `"tauri"` script is only required when using `npm` (not needed for yarn/pnpm/bun). + +--- + +## 4. Capabilities & Permissions (Security Model) + +Tauri 2.0 introduces a strict **Access Control List (ACL)** security model that replaces the v1 `allowlist`. This is the most important new concept to understand. + +### Core Concepts + +- **Capabilities** define **which permissions** are granted to **which windows or webviews**. +- **Permissions** describe explicit privileges for specific commands (e.g., `fs:allow-read`, `http:allow-request`). +- **Scopes** further restrict what a permission can access (e.g., only certain file paths). +- Multiple capabilities can apply to the same window — permissions **merge**. +- Security boundaries are based on **window labels** (not titles). + +### Capability File Location + +``` +src-tauri/capabilities/.json (or .toml) +``` + +All files in this directory are **automatically loaded** by default. + +### Basic Capability File + +```json +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "main-window-capability", + "description": "Permissions for the primary application window", + "platforms": ["linux", "macOS", "windows"], + "windows": ["main"], + "permissions": [ + "core:default" + ] +} +``` + +**Field reference:** +- `$schema` — Points to generated schema for IDE autocompletion. Use `desktop-schema.json` for desktop, `mobile-schema.json` for mobile. +- `identifier` — Unique capability name (ASCII lowercase, max 116 chars). +- `description` — Human-readable purpose. +- `windows` — Array of window labels this capability applies to. Use `["*"]` for all windows. +- `permissions` — Array of permission identifiers. +- `platforms` — Optional. Restricts to specific platforms. Defaults to all platforms if omitted. + +### Permission Identifier Naming Convention + +| Pattern | Meaning | Example | +|---------|---------|---------| +| `:default` | Default permission set for a plugin | `fs:default` | +| `:allow-` | Allow a specific command | `fs:allow-read` | +| `:deny-` | Deny a specific command | `fs:deny-write` | +| `core::` | Core Tauri module permission | `core:event:allow-emit` | + +### Common Permission Identifiers + +**Core permissions:** +- `core:default` — Basic runtime permissions +- `core:path:default` — Path resolution +- `core:event:default` — Event system (includes listen/emit) +- `core:event:allow-listen` — Allow listening to events +- `core:event:allow-emit` — Allow emitting events +- `core:window:default` — Window management +- `core:window:allow-set-title` — Allow changing window title + +**Plugin permissions:** +- `fs:allow-read`, `fs:allow-write`, `fs:allow-exists`, `fs:allow-mkdir`, `fs:allow-remove` +- `http:allow-request` — HTTP client requests +- `dialog:allow-open`, `dialog:allow-save`, `dialog:allow-message` +- `shell:allow-open`, `shell:allow-execute` +- `notification:allow-notify`, `notification:allow-is-permission-granted` +- `clipboard-manager:allow-read-text`, `clipboard-manager:allow-write-text` +- `websocket:allow-connect`, `websocket:allow-send` +- `store:default` — Persistent key-value store + +### Fine-Grained Scopes + +Scopes restrict what a permission can actually access: + +```json +{ + "identifier": "secure-fs-capability", + "windows": ["main"], + "permissions": [ + { + "identifier": "fs:allow-read", + "allow": [{ "path": "$APPDATA/logs/*" }, { "path": "$HOME/Documents/*.txt" }] + }, + { + "identifier": "fs:allow-write", + "allow": [{ "path": "$APPDATA/logs/*.log" }] + }, + { + "identifier": "fs:deny-write", + "deny": [{ "path": "$APPDATA/config/*" }] + } + ] +} +``` + +Available scope variables: `$APPDATA`, `$HOME`, `$APPCONFIG`, `$APPCACHE`, `$APPLOG`, `$RESOURCE`, `$EXE`. + +### Restricting Custom Commands + +By default, all `#[tauri::command]` functions are accessible from all windows. To restrict a custom command to specific windows only, use `build.rs`: + +```rust +fn main() { + tauri_build::try_build( + tauri_build::Attributes::new() + .app_manifest( + tauri_build::AppManifest::new() + .commands(&["my_restricted_command", "admin_only_command"]) + ), + ) + .unwrap(); +} +``` + +Then grant access in the capability: +```json +{ + "identifier": "admin-capability", + "windows": ["admin-panel"], + "permissions": [ + { "identifier": "allow-my-restricted-command" }, + { "identifier": "allow-admin-only-command" } + ] +} +``` + +### Referencing Capabilities Explicitly + +By default, all capability files in `src-tauri/capabilities/` are auto-discovered. To control which ones are used explicitly: + +```json +{ + "app": { + "security": { + "capabilities": ["my-capability", "admin-capability"] + } + } +} +``` + +### Inline Capabilities + +You can define capabilities directly in `tauri.conf.json` instead of separate files: + +```json +{ + "app": { + "security": { + "capabilities": [ + { + "identifier": "inline-cap", + "windows": ["*"], + "permissions": ["core:default"] + } + ] + } + } +} +``` + +--- + +## 5. Rust Backend: `lib.rs` & Commands + +### Application Builder Pattern + +All Tauri 2.0 applications follow the `tauri::Builder` pattern: + +```rust +use tauri::Manager; + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) // Register plugins + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_fs::init()) + .setup(|app| { // Setup hook (runs once at startup) + // Initialize resources, spawn servers, etc. + Ok(()) + }) + .manage(AppState { /* ... */ }) // Inject managed state + .invoke_handler(tauri::generate_handler![ // Register commands + my_command, + another_command, + module::command_in_module + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +**Critical rules:** +- `invoke_handler` can only be called **once**. All commands must be in a single `generate_handler![]` call. +- Commands defined in `lib.rs` **cannot** be marked `pub`. +- Commands defined in separate modules **must** be marked `pub`. +- The `#[cfg_attr(mobile, tauri::mobile_entry_point)]` attribute is required for the `run()` function. + +### Defining Commands + +Commands are the primary IPC mechanism. They are Rust functions annotated with `#[tauri::command]` and callable from the frontend via `invoke()`. + +#### Basic Command + +```rust +#[tauri::command] +fn greet(name: String) -> String { + format!("Hello, {}! Welcome from Rust.", name) +} +``` + +#### Command with Error Handling + +```rust +#[tauri::command] +fn login(user: String, password: String) -> Result { + if user == "admin" && password == "secret" { + Ok("authenticated".to_string()) + } else { + Err("invalid credentials".to_string()) + } +} +``` + +#### Async Command + +```rust +#[tauri::command] +async fn fetch_data(url: String) -> Result { + let response = reqwest::get(&url) + .await + .map_err(|e| e.to_string())?; + response.text().await.map_err(|e| e.to_string()) +} +``` + +**IMPORTANT limitation**: Async commands cannot accept borrowed references (`&str`, `&Path`) directly. Convert to owned types (`String`, `PathBuf`) or wrap the return in `Result`: + +```rust +// Option A: Use owned types +#[tauri::command] +async fn process(value: String) -> String { /* ... */ } + +// Option B: Wrap return in Result +#[tauri::command] +async fn process(value: &str) -> Result { /* ... */ } +``` + +#### Custom Error Types (Recommended) + +```rust +use thiserror::Error; + +#[derive(Debug, Error)] +enum AppError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("Network error: {0}")] + Network(#[from] reqwest::Error), +} + +// Implement Serialize so the error can cross the IPC boundary +impl serde::Serialize for AppError { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + +#[tauri::command] +fn read_config() -> Result { + let content = std::fs::read_to_string("config.toml")?; + Ok(content) +} +``` + +#### Structured Error with Kind/Tag + +```rust +#[derive(Debug, Error)] +enum AppError { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error("{0}")] + Custom(String), +} + +#[derive(serde::Serialize)] +#[serde(tag = "kind", content = "message")] +#[serde(rename_all = "camelCase")] +enum ErrorKind { + Io(String), + Custom(String), +} + +impl serde::Serialize for AppError { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + let kind = match self { + Self::Io(e) => ErrorKind::Io(e.to_string()), + Self::Custom(msg) => ErrorKind::Custom(msg.clone()), + }; + kind.serialize(serializer) + } +} +``` + +Frontend receives: `{ kind: 'io', message: '...' }` — making it easy to handle errors by type. + +#### Accessing Special Objects in Commands + +**AppHandle** — Access to the application instance: +```rust +#[tauri::command] +fn do_something(app_handle: tauri::AppHandle) { + let app_dir = app_handle.path().app_dir(); + // Spawn windows, emit events, access state... +} +``` + +**WebviewWindow** — The window that invoked the command: +```rust +#[tauri::command] +fn get_window_info(webview_window: tauri::WebviewWindow) -> String { + webview_window.label().to_string() +} +``` + +**Managed State** — Shared application state: +```rust +use tauri::State; +use std::sync::Mutex; + +struct DbConnection(Mutex); + +#[tauri::command] +fn query_database(state: State<'_, DbConnection>) -> String { + let conn = state.0.lock().unwrap(); + conn.clone() +} +``` + +**Raw Request** — Access headers and raw body: +```rust +#[tauri::command] +fn upload(request: tauri::ipc::Request) -> Result<(), String> { + let tauri::ipc::InvokeBody::Raw(data) = request.body() else { + return Err("Expected raw body".to_string()); + }; + let auth = request.headers().get("Authorization") + .ok_or("Missing auth header")?; + Ok(()) +} +``` + +#### Commands in Separate Modules + +```rust +// src-tauri/src/commands.rs +#[tauri::command] +pub fn create_user(name: String) -> String { + format!("Created user: {}", name) +} + +#[tauri::command] +pub fn delete_user(id: u32) -> bool { + // ... + true +} +``` + +```rust +// src-tauri/src/lib.rs +mod commands; + +pub fn run() { + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![ + commands::create_user, + commands::delete_user + ]) + // ... +} +``` + +Note: The `commands::` prefix is Rust path resolution only — the frontend still calls `invoke('create_user', ...)` without any prefix. + +### Returning Large Data (ArrayBuffers) + +For large binary data like file contents, use `tauri::ipc::Response`: + +```rust +use tauri::ipc::Response; + +#[tauri::command] +fn read_file() -> Response { + let data = std::fs::read("/path/to/file").unwrap(); + Response::new(data) +} +``` + +### Streaming Data (Channels) + +For streaming large amounts of data to the frontend: + +```rust +use tauri::ipc::Channel; + +#[tauri::command] +async fn stream_file(path: std::path::PathBuf, on_chunk: Channel>) { + let mut file = tokio::fs::File::open(path).await.unwrap(); + let mut buf = vec![0u8; 4096]; + loop { + let n = file.read(&mut buf).await.unwrap(); + if n == 0 { break; } + on_chunk.send(&buf[..n]).unwrap(); + } +} +``` + +--- + +## 6. Frontend Integration Patterns + +### Using `@tauri-apps/api` npm Package (Recommended) + +```bash +npm install @tauri-apps/api +``` + +```javascript +// Imports in v2 — NOTE the path changes from v1 +import { invoke } from '@tauri-apps/api/core'; +import { listen, emit, emitTo, once } from '@tauri-apps/api/event'; +import { getCurrentWebviewWindow, WebviewWindow } from '@tauri-apps/api/webviewWindow'; +import { Channel } from '@tauri-apps/api/core'; +``` + +### Using Global `window.__TAURI__` (No npm package needed) + +Requires `app.withGlobalTauri: true` in `tauri.conf.json`: + +```javascript +// No import needed — accessed via the global object +const { invoke } = window.__TAURI__.core; +const { listen, emit } = window.__TAURI__.event; +``` + +This is the pattern used in the reference `moxie-app` and is ideal for vanilla JS projects without a bundler. + +### Import Path Changes from v1 + +| v1 Import | v2 Import | +|-----------|-----------| +| `@tauri-apps/api/tauri` | `@tauri-apps/api/core` | +| `@tauri-apps/api/window` | `@tauri-apps/api/webviewWindow` | +| `@tauri-apps/api/cli` | `@tauri-apps/plugin-cli` | +| `@tauri-apps/api/clipboard` | `@tauri-apps/plugin-clipboard-manager` | +| `@tauri-apps/api/dialog` | `@tauri-apps/plugin-dialog` | +| `@tauri-apps/api/fs` | `@tauri-apps/plugin-fs` | +| `@tauri-apps/api/global-shortcut` | `@tauri-apps/plugin-global-shortcut` | +| `@tauri-apps/api/http` | `@tauri-apps/plugin-http` | +| `@tauri-apps/api/notification` | `@tauri-apps/plugin-notification` | +| `@tauri-apps/api/shell` | `@tauri-apps/plugin-shell` | +| `@tauri-apps/api/updater` | `@tauri-apps/plugin-updater` | +| `@tauri-apps/api/os` | `@tauri-apps/plugin-os` | +| `@tauri-apps/api/process` | `@tauri-apps/plugin-process` | + +**Rule of thumb in v2**: If it's not `core`, `event`, or `webviewWindow`, it's a plugin. + +### Plugin JS Package Naming + +All v2 plugin JS packages follow: `@tauri-apps/plugin-` + +```bash +npm install @tauri-apps/plugin-store +npm install @tauri-apps/plugin-fs +npm install @tauri-apps/plugin-http +``` + +--- + +## 7. IPC: Commands, Events & Channels + +### Commands (Frontend → Rust) + +The primary request-response mechanism: + +```javascript +// Frontend (v2 import) +import { invoke } from '@tauri-apps/api/core'; + +// Simple invocation +const result = await invoke('greet', { name: 'World' }); +console.log(result); // "Hello, World! Welcome from Rust." + +// With error handling +try { + const token = await invoke('login', { user: 'admin', password: 'secret' }); + console.log('Authenticated:', token); +} catch (error) { + console.error('Login failed:', error); +} +``` + +**Argument naming convention**: Arguments are passed as a JSON object with **camelCase** keys by default. Use `#[tauri::command(rename_all = "snake_case")]` to accept snake_case from the frontend. + +### Events (Bidirectional, Multi-Consumer) + +Events are fire-and-forget messages. Use them for notifications, streaming status updates, or any data that multiple components might need. + +#### Emitting Events from Rust + +```rust +use tauri::{AppHandle, Emitter}; + +// Global event (all listeners receive it) +app_handle.emit("download-progress", 42)?; + +// Target a specific window +app_handle.emit_to("settings-panel", "config-changed", new_config)?; + +// Filter to specific windows +use tauri::EventTarget; +app_handle.emit_filter("notification", payload, |target| { + matches!(target, EventTarget::WebviewWindow { label } if label == "main") +})?; +``` + +#### Listening for Events on the Frontend + +```javascript +import { listen, once, emit, emitTo } from '@tauri-apps/api/event'; +import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; + +// Global listen (receives ALL events with this name) +const unlisten = await listen('download-progress', (event) => { + console.log(`Progress: ${event.payload}%`); +}); + +// Targeted listen (only events emitted to THIS window) +const appWebview = getCurrentWebviewWindow(); +const unlisten2 = await appWebview.listen('config-changed', (event) => { + console.log('New config:', event.payload); +}); + +// Listen once, then auto-cleanup +await once('initialization-complete', (event) => { + console.log('App ready!', event.payload); +}); + +// Emit from frontend to Rust +await emit('user-action', { type: 'click', target: 'button' }); + +// Emit to a specific window from frontend +const target = new WebviewWindow('settings-panel'); +await target.emit('settings-request', { key: 'theme' }); +``` + +**Always call `unlisten()`** when the component unmounts or the listener is no longer needed to prevent memory leaks. + +#### Listening for Events on the Rust Side + +```rust +use tauri::{Listener, Manager}; + +pub fn run() { + tauri::Builder::default() + .setup(|app| { + // Listen globally + app.listen("user-action", |event| { + println!("Received: {}", event.payload()); + }); + + // Listen on a specific window + let main = app.get_webview_window("main").unwrap(); + main.listen("config-changed", |event| { + println!("Config updated: {}", event.data); + }); + + // Listen once + app.once("ready", |event| { + println!("App is ready!"); + }); + + Ok(()) + }) + // ... +} +``` + +### Channels (High-Throughput Streaming) + +Channels are the recommended mechanism for streaming large amounts of ordered data from Rust to the frontend. They are faster and more memory-efficient than events for high-throughput scenarios. + +```rust +use tauri::ipc::Channel; +use serde::Serialize; + +#[derive(Clone, Serialize)] +#[serde(tag = "event", content = "data")] +enum StreamEvent { + Started { total: usize }, + Progress { current: usize, chunk: Vec }, + Finished { total: usize }, +} + +#[tauri::command] +fn download_file(url: String, on_event: Channel) { + on_event.send(StreamEvent::Started { total: 1000 }).unwrap(); + // ... streaming logic ... + on_event.send(StreamEvent::Finished { total: 1000 }).unwrap(); +} +``` + +```javascript +import { invoke, Channel } from '@tauri-apps/api/core'; + +const onEvent = new Channel(); +onEvent.onmessage = (event) => { + switch (event.event) { + case 'started': console.log(`Starting: ${event.data.total} bytes`); break; + case 'progress': /* handle chunk */ break; + case 'finished': console.log('Done!'); break; + } +}; + +await invoke('download_file', { url: 'https://...', onEvent }); +``` + +### Evaluating JavaScript from Rust + +```rust +use tauri::Manager; + +// In a command or setup hook +let webview = app.get_webview_window("main").unwrap(); +webview.eval("document.getElementById('status').textContent = 'Loaded!'")?; +``` + +For complex data passing, use the `serialize-to-javascript` crate. + +--- + +## 8. Window Management + +### Creating Windows Dynamically + +```rust +use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder}; + +#[tauri::command] +fn open_settings(app_handle: AppHandle) -> Result<(), String> { + // Focus existing window if it exists + if let Some(window) = app_handle.get_webview_window("settings") { + window.set_focus().map_err(|e| e.to_string())?; + return Ok(()); + } + + // Create new window + let _window = WebviewWindowBuilder::new( + &app_handle, + "settings", + WebviewUrl::App("settings.html".into()) + ) + .title("Settings") + .inner_size(600.0, 400.0) + .resizable(true) + .center(true) + .build() + .map_err(|e| e.to_string())?; + + Ok(()) +} +``` + +### Creating Windows with External URLs + +```rust +use tauri::{AppHandle, WebviewUrl, WebviewWindowBuilder}; + +#[tauri::command] +async fn open_remote(app_handle: AppHandle, url: String) -> Result { + let target_url = url.parse() + .map(WebviewUrl::External) + .map_err(|_| "Invalid URL".to_string())?; + + app_handle.run_on_main_thread(move || { + WebviewWindowBuilder::new(&app_handle, "remote-window", target_url) + .title("Remote Session") + .inner_size(1024.0, 768.0) + .build() + }).map_err(|e| e.to_string())?; + + Ok("Window spawned".to_string()) +} +``` + +**Key pattern**: Always use `run_on_main_thread` when creating windows from async contexts or non-main-thread code. This is critical — window creation MUST happen on the main thread. + +### Window Builder Options + +```rust +WebviewWindowBuilder::new(&app, "label", WebviewUrl::App("path".into())) + .title("Window Title") + .inner_size(800.0, 600.0) // Width, Height + .min_inner_size(400.0, 300.0) // Minimum size + .max_inner_size(1920.0, 1080.0) // Maximum size + .resizable(true) + .fullscreen(false) + .center(true) + .decorations(true) // Title bar + .always_on_top(false) + .visible(true) // Start visible + .focused(true) + .initialization_script("...") // Inject JS on load + .build()?; +``` + +### Managing Windows from Frontend + +```javascript +import { getCurrentWebviewWindow, WebviewWindow } from '@tauri-apps/api/webviewWindow'; + +// Get current window reference +const appWindow = getCurrentWebviewWindow(); + +// Window operations +await appWindow.setTitle("New Title"); +await appWindow.center(); +await appWindow.minimize(); +await appWindow.maximize(); +await appWindow.unmaximize(); +await appWindow.close(); +await appWindow.setFullscreen(true); +await appWindow.setResizable(false); +await appWindow.setSize({ width: 1024, height: 768 }); +await appWindow.setPosition({ x: 100, y: 100 }); + +// Create new window from frontend +const newWindow = new WebviewWindow('secondary', { + url: 'index.html#/settings', + title: 'Settings', + width: 600, + height: 400, +}); +await newWindow.once('tauri://created', () => { + console.log('Window created!'); +}); +await newWindow.once('tauri://error', (e) => { + console.error('Window creation error:', e); +}); +``` + +### Window v1 to v2 API Renames + +| v1 | v2 | +|----|----| +| `tauri::Window` | `tauri::WebviewWindow` | +| `tauri::WindowBuilder` | `tauri::WebviewWindowBuilder` | +| `tauri::WindowUrl` | `tauri::WebviewUrl` | +| `app.get_window("main")` | `app.get_webview_window("main")` | +| `WebviewUrl::App(path)` | `WebviewUrl::App(path.into())` | +| `WebviewUrl::External(url)` | `WebviewUrl::External(url)` | + +--- + +## 9. Plugin Ecosystem + +### Complete List of Official Tauri 2.0 Plugins (33 plugins) + +| Plugin | Crate | JS Package | Platforms | +|--------|-------|-----------|-----------| +| Autostart | `tauri-plugin-autostart` | `@tauri-apps/plugin-autostart` | Win, Lin, Mac | +| Barcode Scanner | `tauri-plugin-barcode-scanner` | `@tauri-apps/plugin-barcode-scanner` | Android, iOS | +| Biometric | `tauri-plugin-biometric` | `@tauri-apps/plugin-biometric` | Android, iOS | +| Clipboard Manager | `tauri-plugin-clipboard-manager` | `@tauri-apps/plugin-clipboard-manager` | Win, Lin, Mac, Android, iOS | +| CLI | `tauri-plugin-cli` | `@tauri-apps/plugin-cli` | Win, Lin, Mac, Android, iOS | +| Deep Linking | `tauri-plugin-deep-link` | `@tauri-apps/plugin-deep-link` | Win, Mac, Android, iOS | +| Dialog | `tauri-plugin-dialog` | `@tauri-apps/plugin-dialog` | Win, Lin, Mac, Android | +| File System | `tauri-plugin-fs` | `@tauri-apps/plugin-fs` | Win, Lin, Mac, Android, iOS | +| Geolocation | `tauri-plugin-geolocation` | `@tauri-apps/plugin-geolocation` | Android, iOS | +| Global Shortcut | `tauri-plugin-global-shortcut` | `@tauri-apps/plugin-global-shortcut` | Win, Lin, Mac | +| Haptics | `tauri-plugin-haptics` | `@tauri-apps/plugin-haptics` | Android, iOS | +| HTTP Client | `tauri-plugin-http` | `@tauri-apps/plugin-http` | Win, Lin, Mac, Android, iOS | +| Localhost | `tauri-plugin-localhost` | `@tauri-apps/plugin-localhost` | Win, Lin, Mac | +| Logging | `tauri-plugin-log` | `@tauri-apps/plugin-log` | Win, Lin, Mac, Android, iOS | +| NFC | `tauri-plugin-nfc` | `@tauri-apps/plugin-nfc` | Android, iOS | +| Notification | `tauri-plugin-notification` | `@tauri-apps/plugin-notification` | Win, Lin, Mac, Android | +| Opener | `tauri-plugin-opener` | `@tauri-apps/plugin-opener` | Win, Lin, Mac, Android, iOS | +| OS Info | `tauri-plugin-os` | `@tauri-apps/plugin-os` | Win, Lin, Mac, Android, iOS | +| Persisted Scope | `tauri-plugin-persisted-scope` | `@tauri-apps/plugin-persisted-scope` | Win, Lin, Mac, Android, iOS | +| Positioner | `tauri-plugin-positioner` | `@tauri-apps/plugin-positioner` | Win, Lin, Mac | +| Process | `tauri-plugin-process` | `@tauri-apps/plugin-process` | Win, Lin, Mac, Android, iOS | +| Shell | `tauri-plugin-shell` | `@tauri-apps/plugin-shell` | Win, Lin, Mac, Android | +| Single Instance | `tauri-plugin-single-instance` | `@tauri-apps/plugin-single-instance` | Win, Lin, Mac | +| SQL | `tauri-plugin-sql` | `@tauri-apps/plugin-sql` | Win, Lin, Mac, Android, iOS | +| Store | `tauri-plugin-store` | `@tauri-apps/plugin-store` | Win, Lin, Mac, Android, iOS | +| Stronghold | `tauri-plugin-stronghold` | `@tauri-apps/plugin-stronghold` | Win, Lin, Mac, Android, iOS | +| System Tray | `tauri-plugin-system-tray` | `@tauri-apps/plugin-system-tray` | Win, Lin, Mac | +| Updater | `tauri-plugin-updater` | `@tauri-apps/plugin-updater` | Win, Lin, Mac | +| Upload | `tauri-plugin-upload` | `@tauri-apps/plugin-upload` | Win, Lin, Mac, Android, iOS | +| Websocket | `tauri-plugin-websocket` | `@tauri-apps/plugin-websocket` | Win, Lin, Mac, Android, iOS | +| Window State | `tauri-plugin-window-state` | `@tauri-apps/plugin-window-state` | Win, Lin, Mac, Android, iOS | +| Window Customization | `tauri-plugin-window-customization` | `@tauri-apps/plugin-window-customization` | Win, Lin, Mac | + +### Adding a Plugin (3-Step Pattern) + +Every plugin follows the same 3-step setup: + +**Step 1: Add Rust dependency** +```toml +# src-tauri/Cargo.toml +[dependencies] +tauri-plugin-store = "2" +``` + +**Step 2: Register in Builder** +```rust +// src-tauri/src/lib.rs +tauri::Builder::default() + .plugin(tauri_plugin_store::Builder::default().build()) +``` + +**Step 3: Add JS dependency** +```bash +npm install @tauri-apps/plugin-store +``` + +**Step 4 (optional but required): Add permissions** +```json +// src-tauri/capabilities/default.json +{ + "permissions": ["store:default"] +} +``` + +### Plugin Registration Variations + +Different plugins have different initialization patterns: + +```rust +// Simple init (most plugins) +.plugin(tauri_plugin_opener::init()) +.plugin(tauri_plugin_dialog::init()) +.plugin(tauri_plugin_fs::init()) +.plugin(tauri_plugin_notification::init()) +.plugin(tauri_plugin_clipboard_manager::init()) + +// Builder pattern (plugins with configuration) +.plugin(tauri_plugin_store::Builder::default().build()) +.plugin(tauri_plugin_log::Builder::default().build()) + +// With closures/config +.plugin(tauri_plugin_single_instance::init(|app, args, cwd| { + if let Some(window) = app.get_webview_window("main") { + let _ = window.set_focus(); + } +})) + +.plugin(tauri_plugin_shell::init()) +``` + +### Key Plugin Examples + +#### File System Plugin (`tauri-plugin-fs`) + +Rust side uses `std::fs`. JS side uses the plugin: + +```javascript +import { readFile, writeFile, mkdir, exists, remove, readDir } from '@tauri-apps/plugin-fs'; + +// Read a text file +const content = await readFile('config.json'); + +// Write a file +await writeFile('output.txt', 'Hello World!'); + +// Create directory +await mkdir('data/logs', { recursive: true }); + +// Check existence +const hasFile = await exists('config.json'); + +// Remove file +await remove('temp.txt'); +``` + +#### HTTP Client Plugin (`tauri-plugin-http`) + +```javascript +import { fetch } from '@tauri-apps/plugin-http'; + +const response = await fetch('https://api.example.com/data', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ key: 'value' }), +}); +const data = await response.json(); +``` + +#### Store Plugin (Persistent Key-Value) + +```javascript +import { Store } from '@tauri-apps/plugin-store'; + +const store = await Store.load('settings.json'); +await store.set('theme', 'dark'); +await store.set('volume', 75); +const theme = await store.get('theme'); // 'dark' +await store.save(); +``` + +#### Dialog Plugin + +```javascript +import { open, save, message, ask, confirm } from '@tauri-apps/plugin-dialog'; + +// File picker +const file = await open({ multiple: false, filters: [{ name: 'JSON', extensions: ['json'] }] }); + +// Save dialog +const path = await save({ filters: [{ name: 'Text', extensions: ['txt'] }] }); + +// Alert +await message('Operation completed!'); + +// Confirmation +const yes = await confirm('Are you sure you want to delete this file?'); +``` + +--- + +## 10. Native WebSocket Server (Rust-side) + +This section documents the WebSocket server pattern used in the reference `moxie-app`. This pattern is useful when your Tauri app needs to **act as a server** — accepting connections from local webviews, external applications, or other processes. + +### Architecture Overview + +The WebSocket server runs as a Tokio async task spawned during the Tauri `setup` phase. It uses `tokio-tungstenite` for the WebSocket implementation and `tokio::sync::broadcast` for message fan-out to all connected clients. + +### Dependencies (`Cargo.toml`) + +```toml +[dependencies] +tokio = { version = "1", features = ["net", "rt", "sync", "macros"] } +tokio-tungstenite = "0.24" +futures-util = "0.3" +``` + +### Server Implementation + +```rust +// src-tauri/src/server.rs +use tokio::net::TcpListener; +use tokio_tungstenite::accept_async; +use futures_util::stream::StreamExt; +use futures_util::sink::SinkExt; +use tokio::sync::broadcast; + +pub async fn start_websocket_server() { + let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap(); + println!("WebSocket Server listening on ws://127.0.0.1:8080"); + + // Broadcast channel: 16 message queue depth + let (tx, _rx) = broadcast::channel::(16); + + while let Ok((stream, _addr)) = listener.accept().await { + let tx = tx.clone(); + let mut rx = tx.subscribe(); + + // Spawn a task per client connection + tauri::async_runtime::spawn(async move { + if let Ok(ws_stream) = accept_async(stream).await { + let (mut ws_sender, mut ws_receiver) = ws_stream.split(); + + loop { + tokio::select! { + // Branch A: Receive message FROM this client + incoming = ws_receiver.next() => { + match incoming { + Some(Ok(msg)) if msg.is_text() => { + let text = msg.to_text().unwrap().to_string(); + println!("Received: {}", text); + // Broadcast to ALL other connected clients + let _ = tx.send(text); + } + _ => break, // Disconnect + } + } + // Branch B: Receive broadcast FROM other clients + broadcast = rx.recv() => { + if let Ok(payload) = broadcast { + let frame = tokio_tungstenite::tungstenite::Message::Text( + payload.into() + ); + if let Err(e) = ws_sender.send(frame).await { + eprintln!("Send error: {}", e); + break; + } + } + } + } + } + } + }); + } +} +``` + +### Spawning the Server from `lib.rs` + +```rust +// src-tauri/src/lib.rs +mod server; + +pub fn run() { + tauri::Builder::default() + .setup(|_app| { + tauri::async_runtime::spawn(server::start_websocket_server()); + Ok(()) + }) + // ... +} +``` + +**Key pattern**: Use `tauri::async_runtime::spawn()` to launch the server. This ensures the server runs on Tauri's managed async runtime rather than blocking the setup hook. + +### Frontend Client Connection + +```javascript +// Connect to the local WebSocket server +const ws = new WebSocket('ws://127.0.0.1:8080'); + +ws.onopen = () => { + console.log('Connected to local WS server'); + ws.send(JSON.stringify({ type: 'hello', from: 'main-window' })); +}; + +ws.onmessage = (event) => { + const data = JSON.parse(event.data); + console.log('Received from server:', data); +}; + +ws.onerror = (err) => { + console.error('WebSocket error:', err); +}; +``` + +### When to Use This Pattern vs. the WebSocket Plugin + +| Scenario | Approach | +|----------|----------| +| App connects to an **external** WebSocket server | Use `tauri-plugin-websocket` (client plugin) | +| App **is** the WebSocket server | Use the native Tokio pattern above | +| Cross-window communication within the app | Use Tauri events/channels, or this pattern for high throughput | + +--- + +## 11. Script Injection into Webview Windows + +This section documents the script injection pattern used in the reference `moxie-app` for injecting JavaScript into dynamically created webview windows (including those loading external URLs). + +### The Pattern + +Use `WebviewWindowBuilder::initialization_script()` to inject JavaScript that runs when the webview's page loads: + +```rust +// src-tauri/src/lib.rs + +// Load script from an external file (recommended for larger scripts) +let script_to_inject = include_str!("../extWebview.js"); + +WebviewWindowBuilder::new(&app_clone, window_label, url_clone) + .title("Remote Session") + .inner_size(1024.0, 768.0) + .initialization_script(script_to_inject) + .build(); +``` + +### The Injected Script + +```javascript +// extWebview.js +// This script runs automatically when the webview loads +window.addEventListener('DOMContentLoaded', () => { + console.log('Initializing injected socket link...'); + + const ws = new WebSocket('ws://127.0.0.1:8080'); + + ws.onopen = () => { + console.log('Injected socket connected!'); + const payload = { + command: 'remote_action', + message: 'Hello from: ' + window.location.href + }; + ws.send(JSON.stringify(payload)); + }; + + ws.onmessage = (event) => { + console.log('RECEIVED FROM TAURI BACKEND:', JSON.parse(event.data)); + // Respond with session data + ws.send(JSON.stringify({ + localsession: window.localStorage.getItem('token') || 'no-session' + })); + }; + + ws.onerror = (err) => { + console.error('Injected socket error:', err); + }; + + // Expose socket globally for the page's own code to use + window.localAppSocket = ws; +}); +``` + +### Use Cases + +- **Bridge external pages**: Allow pages loaded from external URLs to communicate with the Tauri backend via a local WebSocket connection. +- **Data extraction**: Inject scripts that read data from external pages (localStorage, DOM elements, cookies) and relay it back to the Tauri backend. +- **UI modification**: Modify the appearance or behavior of external pages loaded in a webview. +- **Session sharing**: Extract authentication tokens or session data from one webview and relay it to another. + +### Important Notes + +- The injected script runs in the webview's JavaScript context, so it has access to that page's `window`, `document`, and `localStorage`. +- For security reasons, injected scripts on external URLs depend on CSP (Content Security Policy). Set `"csp": null` in `tauri.conf.json` to disable CSP restrictions (use with caution). +- Use `include_str!()` to load scripts from files at compile time, keeping your Rust code clean. + +--- + +## 12. State Management + +### Managed State (Tauri's Built-in) + +Use `tauri::State` for shared state across commands: + +```rust +use std::sync::Mutex; +use tauri::State; + +struct AppState { + pub user: Mutex>, + pub settings: Mutex, +} + +#[tauri::command] +fn get_user(state: State<'_, AppState>) -> Option { + state.user.lock().unwrap().clone() +} + +#[tauri::command] +fn set_user(state: State<'_, AppState>, name: String) { + *state.user.lock().unwrap() = Some(name); +} + +pub fn run() { + tauri::Builder::default() + .manage(AppState { + user: Mutex::new(None), + settings: Mutex::new(Settings::default()), + }) + .invoke_handler(tauri::generate_handler![get_user, set_user]) + // ... +} +``` + +**Thread-safe types**: Use `Mutex` for simple cases, `RwLock` for read-heavy workloads, or `tokio::sync::Mutex` for async contexts. + +### Using the Store Plugin (Persistent KV Storage) + +For data that persists across app restarts: + +```rust +// Cargo.toml +tauri-plugin-store = "2" +``` + +```javascript +// Frontend +import { Store } from '@tauri-apps/plugin-store'; + +const store = await Store.load('app-state.json'); +await store.set('user-preferences', { theme: 'dark', language: 'en' }); +await store.save(); + +// Read on next launch +const prefs = await store.get('user-preferences'); +``` + +### Recommended Patterns + +| Data Type | Approach | +|-----------|----------| +| Transient UI state (form inputs, selections) | Frontend state management (React state, Vue refs, etc.) | +| Shared app state across commands | `tauri::State` with `Mutex`/`RwLock` | +| Persistent user preferences | `tauri-plugin-store` | +| Structured persistent data (records, models) | `tauri-plugin-sql` with SQLite | +| Encrypted sensitive data | `tauri-plugin-stronghold` | + +--- + +## 13. v1 to v2 Migration Reference + +### Automated Migration + +```bash +npm install @tauri-apps/cli@latest +npm run tauri migrate +``` + +This automates config migration and generates capability files from the v1 allowlist. **However**, it is NOT a complete replacement for understanding the changes — manual review is still required. + +### Rust API Migration Map + +| v1 Pattern | v2 Replacement | +|------------|----------------| +| `tauri::api::dialog::*` | `tauri-plugin-dialog` | +| `tauri::api::fs::*` | `std::fs` (Rust) or `tauri-plugin-fs` (JS) | +| `tauri::api::http::*` | `tauri-plugin-http` | +| `tauri::api::path::*`, `tauri::PathResolver` | `tauri::Manager::path()` | +| `tauri::api::process::Command` | `tauri-plugin-shell` | +| `tauri::api::shell::*` | `tauri-plugin-shell` | +| `tauri::api::clipboard::*` | `tauri-plugin-clipboard-manager` | +| `tauri::api::notification::*` | `tauri-plugin-notification` | +| `tauri::api::global_shortcut::*` | `tauri-plugin-global-shortcut` | +| `tauri::api::updater::*` | `tauri-plugin-updater` | +| `tauri::api::cli::*` | `tauri-plugin-cli` | +| `tauri::api::os::*` | `tauri-plugin-os` | +| `tauri::api::version` | `semver` crate | +| `tauri::Window` | `tauri::WebviewWindow` | +| `tauri::WindowBuilder` | `tauri::WebviewWindowBuilder` | +| `tauri::WindowUrl` | `tauri::WebviewUrl` | +| `Manager::get_window()` | `Manager::get_webview_window()` | +| `App::window()` | `App::get_webview_window()` | +| `tauri::Menu` | `tauri::menu::MenuBuilder` | +| `tauri::MenuItem` | `tauri::menu::PredefinedMenuItem` | +| `tauri::CustomMenuItem` | `tauri::menu::MenuItemBuilder` | +| `tauri::Submenu` | `tauri::menu::SubmenuBuilder` | +| `tauri::SystemTray` | `tauri::tray::TrayIconBuilder` | +| `App::clipboard_manager()` | `tauri_plugin_clipboard_manager::ClipboardExt` | +| `App::global_shortcut_manager()` | `tauri_plugin_global_shortcut::GlobalShortcutExt` | +| `Manager::fs_scope()` | `tauri_plugin_fs::FsExt` | +| `emit()` | `emit()` (now broadcasts to ALL listeners) | +| `emit_to()` | NEW in v2 for targeted emission | +| `listen_global()` | Renamed to `listen_any()` | + +### JavaScript API Migration Map + +| v1 Import | v2 Import | +|-----------|-----------| +| `@tauri-apps/api/tauri` | `@tauri-apps/api/core` | +| `@tauri-apps/api/window` | `@tauri-apps/api/webviewWindow` | +| `@tauri-apps/api/dialog` | `@tauri-apps/plugin-dialog` | +| `@tauri-apps/api/fs` | `@tauri-apps/plugin-fs` | +| `@tauri-apps/api/http` | `@tauri-apps/plugin-http` | +| `@tauri-apps/api/notification` | `@tauri-apps/plugin-notification` | +| `@tauri-apps/api/clipboard` | `@tauri-apps/plugin-clipboard-manager` | +| `@tauri-apps/api/shell` | `@tauri-apps/plugin-shell` | +| `@tauri-apps/api/global-shortcut` | `@tauri-apps/plugin-global-shortcut` | +| `@tauri-apps/api/updater` | `@tauri-apps/plugin-updater` | +| `@tauri-apps/api/os` | `@tauri-apps/plugin-os` | +| `@tauri-apps/api/process` | `@tauri-apps/plugin-process` | +| `@tauri-apps/api/cli` | `@tauri-apps/plugin-cli` | +| `tauri-plugin-*` JS packages | `@tauri-apps/plugin-*` | + +### Environment Variable Migration + +| v1 | v2 | +|----|----| +| `TAURI_PRIVATE_KEY` | `TAURI_SIGNING_PRIVATE_KEY` | +| `TAURI_KEY_PASSWORD` | `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | +| `TAURI_SKIP_DEVSERVER_CHECK` | `TAURI_CLI_NO_DEV_SERVER_WAIT` | +| `TAURI_DEV_SERVER_PORT` | `TAURI_CLI_PORT` | +| `TAURI_TRAY` | `TAURI_LINUX_AYATANA_APPINDICATOR` | +| `TAURI_APPLE_DEVELOPMENT_TEAM` | `APPLE_DEVELOPMENT_TEAM` | +| `TAURI_PLATFORM` | `TAURI_ENV_PLATFORM` | +| `TAURI_ARCH` | `TAURI_ENV_ARCH` | +| `TAURI_DEBUG` | `TAURI_ENV_DEBUG` | + +### Windows Origin URL Change + +**v1**: `https://tauri.localhost` +**v2**: `http://tauri.localhost` + +This means IndexedDB, LocalStorage, and Cookies from v1 apps will **not carry over** to v2. To keep the HTTPS scheme: + +```json +{ + "app": { + "windows": [{ "useHttpsScheme": true }] + } +} +``` + +--- + +## 14. Common Anti-Patterns to Avoid + +### 1. Using v1 APIs or Import Paths + +```rust +// WRONG - v1 pattern (tauri::api was REMOVED) +use tauri::api::dialog; +use tauri::api::fs; + +// CORRECT - v2 pattern +// Use std::fs in Rust, or tauri-plugin-fs on the frontend +``` + +```javascript +// WRONG - v1 import path +import { invoke } from '@tauri-apps/api/tauri'; +import { open } from '@tauri-apps/api/dialog'; + +// CORRECT - v2 import path +import { invoke } from '@tauri-apps/api/core'; +import { open } from '@tauri-apps/plugin-dialog'; +``` + +### 2. Using `tauri.conf.json` v1 Structure + +```json +// WRONG - v1 structure +{ + "tauri": { + "allowlist": { ... }, + "windows": [...], + "bundle": { ... } + }, + "build": { "distDir": "../dist", "withGlobalTauri": true } +} + +// CORRECT - v2 structure +{ + "app": { + "security": { "capabilities": [...] }, + "windows": [...], + "withGlobalTauri": true + }, + "build": { "frontendDist": "../dist" }, + "bundle": { ... } +} +``` + +### 3. Using Old Window Types + +```rust +// WRONG - v1 types +let window = tauri::WindowBuilder::new(&app, "main", tauri::WindowUrl::App("index.html".into())); +app.get_window("main"); + +// CORRECT - v2 types +let window = tauri::WebviewWindowBuilder::new(&app, "main", tauri::WebviewUrl::App("index.html".into())); +app.get_webview_window("main"); +``` + +### 4. Calling `invoke_handler` Multiple Times + +```rust +// WRONG - only the last one takes effect +.invoke_handler(tauri::generate_handler![command_a]) +.invoke_handler(tauri::generate_handler![command_b]) + +// CORRECT - all commands in a single call +.invoke_handler(tauri::generate_handler![command_a, command_b]) +``` + +### 5. Using Borrowed Types in Async Commands + +```rust +// WRONG - &str cannot cross async boundary +#[tauri::command] +async fn process(data: &str) -> String { ... } + +// CORRECT - use owned types +#[tauri::command] +async fn process(data: String) -> String { ... } + +// ALTERNATIVELY - wrap return in Result +#[tauri::command] +async fn process(data: &str) -> Result { ... } +``` + +### 6. Forgetting to Add Permissions for Plugins + +Every plugin requires permissions to be granted in a capability file. Without them, the plugin's frontend API calls will silently fail or throw permission errors. + +```json +// WRONG - no permissions for the fs plugin +{ + "permissions": ["core:default"] +} + +// CORRECT - grant fs plugin permissions +{ + "permissions": [ + "core:default", + "fs:default" + ] +} +``` + +### 7. Creating Windows Outside the Main Thread + +```rust +// WRONG - window creation from async context without dispatch +#[tauri::command] +async fn open_window(app_handle: AppHandle) { + tauri::WebviewWindowBuilder::new(&app_handle, "new", WebviewUrl::App("page.html".into())) + .build(); // This will panic or fail! +} + +// CORRECT - dispatch to main thread +#[tauri::command] +async fn open_window(app_handle: AppHandle) -> Result<(), String> { + app_handle.run_on_main_thread(move || { + let _ = tauri::WebviewWindowBuilder::new( + &app_handle, + "new", + tauri::WebviewUrl::App("page.html".into()), + ) + .build(); + }).map_err(|e| e.to_string())?; + Ok(()) +} +``` + +### 8. Not Cleaning Up Event Listeners + +```javascript +// WRONG - listener leaks memory +const component = () => { + useEffect(() => { + listen('my-event', handler); // Never cleaned up! + }, []); +}; + +// CORRECT - clean up on unmount +const component = () => { + useEffect(() => { + const unlisten = listen('my-event', handler); + return () => { unlisten.then(fn => fn()); }; + }, []); +}; +``` + +--- + +## 15. Quick Reference Cheat Sheet + +### Tauri Builder Setup + +```rust +tauri::Builder::default() + .plugin(plugin::init()) // Register plugins + .setup(|app| Ok(())) // Startup logic + .manage(State {}) // Inject state + .invoke_handler(tauri::generate_handler![ // Register commands + cmd1, cmd2, module::cmd3 + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +``` + +### Command Definition + +```rust +#[tauri::command] +async fn command_name(arg: String) -> Result { ... } +``` + +### Frontend Invoke + +```javascript +import { invoke } from '@tauri-apps/api/core'; +const result = await invoke('command_name', { arg: 'value' }); +``` + +### Event System + +```rust +// Rust +use tauri::{AppHandle, Emitter}; +app_handle.emit("event-name", payload)?; +app_handle.emit_to("window-label", "event-name", payload)?; +``` + +```javascript +// Frontend +import { listen, emit, emitTo } from '@tauri-apps/api/event'; +const unlisten = await listen('event-name', (event) => { /* event.payload */ }); +await emit('event-name', data); +await emitTo('window-label', 'event-name', data); +``` + +### Window Management + +```rust +use tauri::{WebviewWindowBuilder, WebviewUrl, Manager}; +WebviewWindowBuilder::new(&app, "label", WebviewUrl::App("page.html".into())) + .title("Title").inner_size(800.0, 600.0).build()?; +app.get_webview_window("label")?.set_focus()?; +``` + +### Key v2 Naming Rules + +- Window type: `WebviewWindow` (not `Window`) +- Window builder: `WebviewWindowBuilder` (not `WindowBuilder`) +- URL type: `WebviewUrl` (not `WindowUrl`) +- Get window: `get_webview_window()` (not `get_window()`) +- Core import: `@tauri-apps/api/core` (not `@tauri-apps/api/tauri`) +- Plugin imports: `@tauri-apps/plugin-` (not `@tauri-apps/api/`) +- Config section: `app` (not `tauri`) +- Security model: Capabilities + Permissions (not `allowlist`) + +### Official Documentation Links + +- Main Docs: https://v2.tauri.app +- Migration Guide: https://v2.tauri.app/start/migrate/from-tauri-1/ +- Capabilities: https://v2.tauri.app/security/capabilities/ +- Permissions: https://v2.tauri.app/security/permissions/ +- Configuration: https://v2.tauri.app/develop/configuration-files/ +- Calling Rust: https://v2.tauri.app/develop/calling-rust/ +- Calling Frontend: https://v2.tauri.app/develop/calling-frontend/ +- Plugin Index: https://v2.tauri.app/plugin/ +- Rust Docs: https://docs.rs/tauri/latest/tauri/