tauri
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
- Validate all input from frontend before processing
- Use capabilities to explicitly allow commands in
tauri.conf.json - Never trust paths - validate and sanitize file paths
- Avoid shell commands when possible; use Rust APIs
- Define granular permissions for plugin commands
- Use the isolation pattern for applications with untrusted content
Resources
References
references/commands-reference.md- Detailed command patterns and examplesreferences/plugin-development.md- Complete plugin development guide
Official Documentation
More from johnlarkin1/claude-code-extensions
textual
Build terminal user interface (TUI) applications with the Textual framework. Use when creating new Textual apps, adding screens/widgets, styling with TCSS, handling events and reactivity, testing TUI apps, or any task involving "textual", "TUI", or terminal-based Python applications.
146manim
Create mathematical animations and visualizations using Manim (ManimCE - Community Edition). Use this skill when users want to build Manim visualizations, create math animations, animate equations, graphs, geometric proofs, 3D objects, or any programmatic video animation. Triggers on requests mentioning "manim", "mathematical animation", "animate equation", "visualize algorithm", "create animation of", "3D visualization", or building explanatory math videos.
13excalidraw
Generate Excalidraw diagrams (.excalidraw JSON files) for whiteboarding, flowcharts, architecture diagrams, sequence diagrams, mind maps, wireframes, and org charts. Use when user requests diagrams, visual documentation, system architecture visualization, process flows, or any hand-drawn style diagram. Triggers on requests mentioning Excalidraw, diagram creation, flowcharts, architecture diagrams, sequence diagrams, wireframes, or visual documentation.
11ics-generator
Generate ICS calendar files (.ics) from natural language descriptions. Use when user wants to create calendar events, meetings, appointments, reminders, recurring events, or schedule items. Triggers on requests mentioning "calendar event", "ICS file", ".ics", "meeting invite", "appointment", "recurring event", "schedule", "RRULE", "reminder", "RSVP", "calendar invite", "block my calendar", or "add to calendar".
9graphviz
Generate GraphViz DOT files (.dot) for directed/undirected graphs, hierarchical layouts, network diagrams, dependency graphs, state machines, and complex graph visualizations. Use when precise node positioning is needed, when rendering to PNG/SVG/PDF is required, when complex graph algorithms (clustering, ranking) are needed, or when dealing with large graphs (100+ nodes). Triggers on requests mentioning GraphViz, DOT language, network diagrams, dependency graphs, or when sophisticated graph layout is required.
6mermaid
Generate Mermaid diagrams (.mmd, .mermaid files, or markdown code blocks) for flowcharts, sequence diagrams, class diagrams, ER diagrams, state diagrams, Gantt charts, pie charts, mindmaps, timelines, and git graphs. Use when user requests diagrams for documentation, markdown files, README visualizations, or any text-based diagram format that renders in GitHub/GitLab. Triggers on requests mentioning Mermaid, markdown diagrams, documentation diagrams, or when output needs to be embedded in markdown.
5