Get the FREE Ultimate OpenClaw Setup Guide →

tauri

Scanned
npx machina-cli add skill johnlarkin1/claude-code-extensions/tauri --openclaw
Files (1)
SKILL.md
10.6 KB

Tauri Development

Overview

Tauri is a framework for building lightweight, secure desktop applications using web technologies for the frontend and Rust for the backend. This skill provides guidance for Tauri v2 development including:

  • Creating and registering commands (Rust-to-JS communication)
  • Event system for background operations
  • State management across commands
  • Plugin development with permissions
  • Best practices for security and performance

Quick Reference

Project Structure

my-app/
├── src/                    # Frontend (React/Vue/Svelte/etc.)
├── src-tauri/
│   ├── src/
│   │   ├── lib.rs          # App setup, command registration
│   │   ├── main.rs         # Binary entry point
│   │   └── commands/       # Command modules (recommended)
│   │       ├── mod.rs
│   │       └── feature.rs
│   ├── Cargo.toml
│   ├── tauri.conf.json     # App configuration
│   └── capabilities/       # Permission definitions
└── package.json

Essential Imports

Rust:

use tauri::{command, AppHandle, State, Emitter, Runtime};
use serde::{Deserialize, Serialize};

JavaScript/TypeScript:

import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';

Creating Commands

Basic Command

#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

Register in lib.rs:

pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Call from frontend:

const result = await invoke('greet', { name: 'World' });

Async Commands

Use async for I/O operations, database queries, or network requests:

#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
    reqwest::get(&url)
        .await
        .map_err(|e| e.to_string())?
        .text()
        .await
        .map_err(|e| e.to_string())
}

Important: Async commands cannot use borrowed types (&str). Convert to owned types:

// Won't compile:
async fn bad(name: &str) -> String { ... }

// Use this instead:
async fn good(name: String) -> String { ... }

Commands with AppHandle

Access application-wide functionality:

#[tauri::command]
async fn save_file(app: tauri::AppHandle, content: String) -> Result<(), String> {
    let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
    std::fs::write(app_dir.join("data.txt"), content).map_err(|e| e.to_string())
}

Commands with WebviewWindow

Access the calling window:

#[tauri::command]
fn get_window_label(window: tauri::WebviewWindow) -> String {
    window.label().to_string()
}

Error Handling

Simple String Errors

#[tauri::command]
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("cannot divide by zero".to_string())
    } else {
        Ok(a / b)
    }
}

Custom Error Types (Recommended)

use thiserror::Error;
use serde::Serialize;

#[derive(Debug, Error)]
pub enum AppError {
    #[error("database error: {0}")]
    Database(#[from] rusqlite::Error),
    #[error("file not found: {0}")]
    NotFound(String),
    #[error("permission denied")]
    PermissionDenied,
}

impl Serialize for AppError {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::ser::Serializer,
    {
        serializer.serialize_str(self.to_string().as_ref())
    }
}

type Result<T> = std::result::Result<T, AppError>;

#[tauri::command]
fn load_config() -> Result<Config> {
    // Errors auto-convert via From trait
}

Frontend error handling:

try {
    const result = await invoke('load_config');
} catch (error) {
    console.error('Command failed:', error);
}

State Management

Share data across commands using managed state:

use std::sync::Mutex;

struct AppState {
    counter: Mutex<i32>,
    config: Config,
}

#[tauri::command]
fn increment(state: tauri::State<AppState>) -> i32 {
    let mut counter = state.counter.lock().unwrap();
    *counter += 1;
    *counter
}

#[tauri::command]
fn get_config(state: tauri::State<AppState>) -> Config {
    state.config.clone()
}

pub fn run() {
    tauri::Builder::default()
        .manage(AppState {
            counter: Mutex::new(0),
            config: Config::default(),
        })
        .invoke_handler(tauri::generate_handler![increment, get_config])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Async commands with state require owned access:

#[tauri::command]
async fn async_with_state(state: tauri::State<'_, AppState>) -> Result<String, String> {
    // Clone what you need before async operations
    let config = state.config.clone();
    // Now safe to await
    Ok(format!("{:?}", config))
}

Event System

Emit events from Rust to notify frontend of background operations:

Emitting Events

use tauri::Emitter;

#[tauri::command]
async fn long_running_task(app: AppHandle) -> Result<(), String> {
    app.emit("task-started", ()).map_err(|e| e.to_string())?;

    for i in 0..100 {
        // Do work...
        app.emit("task-progress", i).map_err(|e| e.to_string())?;
    }

    app.emit("task-complete", "done").map_err(|e| e.to_string())?;
    Ok(())
}

Listening in Frontend

import { listen } from '@tauri-apps/api/event';

const unlisten = await listen('task-progress', (event) => {
    console.log('Progress:', event.payload);
});

// Clean up when done
unlisten();

Typed Event Payloads

#[derive(Clone, Serialize)]
struct ProgressPayload {
    step: usize,
    total: usize,
    message: String,
}

app.emit("progress", ProgressPayload {
    step: 50,
    total: 100,
    message: "Processing...".to_string(),
})?;

Organizing Commands

For larger applications, organize commands in modules:

src-tauri/src/commands/mod.rs:

pub mod files;
pub mod database;
pub mod auth;

src-tauri/src/commands/files.rs:

use tauri::command;

#[command]
pub fn read_file(path: String) -> Result<String, String> {
    std::fs::read_to_string(&path).map_err(|e| e.to_string())
}

#[command]
pub fn write_file(path: String, content: String) -> Result<(), String> {
    std::fs::write(&path, content).map_err(|e| e.to_string())
}

src-tauri/src/lib.rs:

mod commands;

pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            commands::files::read_file,
            commands::files::write_file,
            commands::database::query,
            commands::auth::login,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Performance Optimization

Large Data Returns

Bypass JSON serialization for binary data:

use tauri::ipc::Response;

#[tauri::command]
fn read_binary_file(path: String) -> Result<Response, String> {
    let data = std::fs::read(&path).map_err(|e| e.to_string())?;
    Ok(Response::new(data))
}

Streaming with Channels

For real-time data streaming:

use tauri::ipc::Channel;

#[tauri::command]
fn stream_logs(channel: Channel<String>) {
    std::thread::spawn(move || {
        loop {
            let log_line = get_next_log();
            if channel.send(log_line).is_err() {
                break; // Frontend closed channel
            }
        }
    });
}

Plugin Development

Plugin Structure

Initialize with: npx @tauri-apps/cli plugin new my-plugin

tauri-plugin-my-plugin/
├── src/
│   ├── lib.rs          # Plugin entry point
│   ├── commands.rs     # Plugin commands
│   ├── error.rs        # Error types
│   └── models.rs       # Data structures
├── permissions/        # Permission definitions
├── guest-js/           # TypeScript bindings
└── Cargo.toml

Basic Plugin

src/lib.rs:

use tauri::{
    plugin::{Builder, TauriPlugin},
    Manager, Runtime,
};

mod commands;
mod error;

pub fn init<R: Runtime>() -> TauriPlugin<R> {
    Builder::new("my-plugin")
        .invoke_handler(tauri::generate_handler![
            commands::do_something
        ])
        .setup(|app, api| {
            // Initialize plugin state
            Ok(())
        })
        .build()
}

src/commands.rs:

use tauri::{command, AppHandle, Runtime};

#[command]
pub async fn do_something<R: Runtime>(app: AppHandle<R>) -> Result<String, String> {
    Ok("done".to_string())
}

Using Plugins

pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_my_plugin::init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Plugin Permissions

Define in permissions/default.toml:

[default]
description = "Default permissions for my-plugin"
permissions = ["allow-do-something"]

[[permission]]
identifier = "allow-do-something"
description = "Allows the do_something command"
commands.allow = ["do_something"]

See references/plugin-development.md for comprehensive plugin guidance.

Security Best Practices

  1. Validate all input from frontend before processing
  2. Use capabilities to explicitly allow commands in tauri.conf.json
  3. Never trust paths - validate and sanitize file paths
  4. Avoid shell commands when possible; use Rust APIs
  5. Define granular permissions for plugin commands
  6. Use the isolation pattern for applications with untrusted content

Resources

References

  • references/commands-reference.md - Detailed command patterns and examples
  • references/plugin-development.md - Complete plugin development guide

Official Documentation

Source

git clone https://github.com/johnlarkin1/claude-code-extensions/blob/main/skills/tauri/SKILL.mdView on GitHub

Overview

Tauri enables lightweight, secure desktop apps by pairing web frontends with Rust backends. This skill covers Tauri v2 development tasks such as creating and registering commands, implementing IPC, managing shared state, and building plugins with proper security and performance considerations. It also guides integration of Rust with JavaScript/TypeScript frontends and common patterns like WebviewWindow.

How This Skill Works

You define commands with #[tauri::command], register them in lib.rs using tauri::Builder and tauri::generate_handler!, and call them from the frontend via invoke(). The framework provides an event system and State for cross-command data, while plugins and capabilities handle extended functionality and permissions.

When to Use It

  • When creating a new Tauri app or extending an existing one with Rust backend logic
  • When adding commands and IPC between Rust and the web frontend using invoke()
  • When developing or integrating Tauri plugins with explicit permissions
  • When needing shared application state accessed across commands and windows
  • When integrating Rust code with JavaScript/TypeScript frontends (WebView/WebviewWindow)

Quick Start

  1. Step 1: Define a #[tauri::command] in Rust and register it in run() with tauri::Builder::default().invoke_handler(tauri::generate_handler![greet]).
  2. Step 2: Call the command from the frontend using await invoke('greet', { name: 'World' }).
  3. Step 3: Build and run the app, then expand with State, WebviewWindow access, or plugins as needed.

Best Practices

  • Annotate core functionality with #[tauri::command] and register in lib.rs via tauri::Builder and tauri::generate_handler![...].
  • Prefer async for I/O-bound commands and use owned types (String) instead of borrowed &str to avoid lifetime issues.
  • Use AppHandle for app-wide operations like reading app_data_dir() and file I/O.
  • Leverage WebviewWindow in commands when you need window-specific data or control.
  • Design plugins with explicit capabilities and permissions to minimize privileges and improve security.

Example Use Cases

  • Greeting command: #[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}!", name) }
  • Async data fetch: #[tauri::command] async fn fetch_data(url: String) -> Result<String, String> { ... }
  • App-wide save: #[tauri::command] async fn save_file(app: tauri::AppHandle, content: String) -> Result<(), String> { ... }
  • Window access: #[tauri::command] fn get_window_label(window: tauri::WebviewWindow) -> String { window.label().to_string() }
  • Error handling: #[tauri::command] fn divide(a: f64, b: f64) -> Result<f64, String> { if b == 0.0 { Err("cannot divide by zero".to_string()) } else { Ok(a / b) } }

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers