Agent skill
tauri-rust-patterns
Tauri application development patterns in Rust. Command handlers, state management, IPC, plugin architecture, window management, and platform-specific workarounds. Use when building Tauri apps, implementing commands, managing state, or handling cross-platform issues. Trigger phrases include "tauri command", "tauri state", "window management", "IPC", "WSLg", or "transparent window".
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/tauri-rust-patterns
SKILL.md
Tauri Rust Patterns
Overview
Tauri enables building desktop applications with web frontends and Rust backends. This skill covers canonical patterns for Tauri development, focusing on command handlers, state management, IPC communication, and TMNL-specific configurations like transparent windows and WSLg workarounds.
Key capabilities:
- Type-safe IPC between frontend and backend
- Shared state management with
State<T> - Custom window configurations (transparency, decorations)
- Plugin architecture for modular functionality
- Cross-platform compatibility (Linux, Windows, macOS)
Canonical Sources
Tauri Documentation:
- Official docs: https://tauri.app/
- Rust API: https://docs.rs/tauri
- Command guide: https://tauri.app/v1/guides/features/command
- State management: https://tauri.app/v1/guides/features/state
TMNL Codebase:
CLAUDE.md— Tauri configuration section (lines 457-563)nix/modules/tauri.nix— Build environment configuration- Package.json — Tauri scripts and dependencies
- Example: Transparent frameless window setup
Related Skills:
rust-effect-patterns— Error handling in commandsmcp-server-development— Similar IPC patterns
Tauri Architecture
Application Flow
┌────────────────────────────────────────────┐
│ Frontend (Web) │
│ ┌──────────────────────────────────────┐ │
│ │ React/Vue/Svelte Components │ │
│ │ • UI rendering │ │
│ │ • User interactions │ │
│ └────────────┬─────────────────────────┘ │
└───────────────┼────────────────────────────┘
│ @tauri-apps/api
│ invoke("command", args)
│
┌───────────────▼────────────────────────────┐
│ Tauri Core (Rust) │
│ ┌──────────────────────────────────────┐ │
│ │ Command Handlers │ │
│ │ #[tauri::command] │ │
│ │ async fn greet(name: String) {...} │ │
│ └──────────────────────────────────────┘ │
│ ┌──────────────────────────────────────┐ │
│ │ State Management │ │
│ │ State<AppState> │ │
│ │ Mutex<T>, RwLock<T> │ │
│ └──────────────────────────────────────┘ │
│ ┌──────────────────────────────────────┐ │
│ │ Window Management │ │
│ │ WindowBuilder, WebviewWindow │ │
│ └──────────────────────────────────────┘ │
└───────────────────────────────────────────┘
Pattern 1: Command Handlers
Basic Command
use tauri::command;
#[command]
fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
// In main.rs
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Frontend (TypeScript):
import { invoke } from "@tauri-apps/api/core";
const greeting = await invoke<string>("greet", { name: "Alice" });
console.log(greeting); // "Hello, Alice!"
Pattern: Commands are functions annotated with #[command], registered via generate_handler!.
Async Command with Result
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Error, Debug, Serialize)]
pub enum CommandError {
#[error("File not found: {0}")]
FileNotFound(String),
#[error("IO error: {0}")]
IoError(String),
}
// Required for Tauri IPC
impl From<std::io::Error> for CommandError {
fn from(err: std::io::Error) -> Self {
CommandError::IoError(err.to_string())
}
}
#[derive(Serialize, Deserialize)]
pub struct FileContent {
path: String,
content: String,
size: usize,
}
#[command]
async fn read_file(path: String) -> Result<FileContent, CommandError> {
// Validate path
if !std::path::Path::new(&path).exists() {
return Err(CommandError::FileNotFound(path));
}
// Read file asynchronously
let content = tokio::fs::read_to_string(&path).await?;
let size = content.len();
Ok(FileContent { path, content, size })
}
Frontend:
try {
const file = await invoke<FileContent>("read_file", {
path: "/path/to/file.txt",
});
console.log(`Read ${file.size} bytes from ${file.path}`);
} catch (error) {
console.error("Error:", error); // CommandError message
}
Pattern: Use Result<T, E> with Serialize error types, async fn for I/O operations.
Command with Multiple Arguments
#[command]
async fn search_files(
directory: String,
pattern: String,
case_sensitive: bool,
) -> Result<Vec<String>, CommandError> {
let mut results = Vec::new();
let regex = if case_sensitive {
regex::Regex::new(&pattern)?
} else {
regex::RegexBuilder::new(&pattern)
.case_insensitive(true)
.build()?
};
for entry in walkdir::WalkDir::new(&directory) {
let entry = entry?;
if let Some(name) = entry.file_name().to_str() {
if regex.is_match(name) {
results.push(entry.path().display().to_string());
}
}
}
Ok(results)
}
Frontend:
const matches = await invoke<string[]>("search_files", {
directory: "/home/user/projects",
pattern: ".*\\.rs$",
caseSensitive: true,
});
Pattern: Arguments map to function parameters by name (camelCase ↔ snake_case).
Pattern 2: State Management
Basic State with Mutex
use std::sync::Mutex;
use tauri::State;
struct AppState {
counter: Mutex<i32>,
}
#[command]
fn increment(state: State<AppState>) -> i32 {
let mut counter = state.counter.lock().unwrap();
*counter += 1;
*counter
}
#[command]
fn get_count(state: State<AppState>) -> i32 {
*state.counter.lock().unwrap()
}
fn main() {
tauri::Builder::default()
.manage(AppState {
counter: Mutex::new(0),
})
.invoke_handler(tauri::generate_handler![increment, get_count])
.run(tauri::generate_context!())
.expect("error");
}
Frontend:
await invoke("increment"); // 1
await invoke("increment"); // 2
const count = await invoke<number>("get_count"); // 2
Pattern: Use .manage(T) to register state, State<T> to access in commands.
Shared State with RwLock
use std::collections::HashMap;
use std::sync::RwLock;
struct CacheState {
data: RwLock<HashMap<String, String>>,
}
#[command]
fn cache_get(key: String, state: State<CacheState>) -> Option<String> {
let data = state.data.read().unwrap();
data.get(&key).cloned()
}
#[command]
fn cache_set(key: String, value: String, state: State<CacheState>) {
let mut data = state.data.write().unwrap();
data.insert(key, value);
}
#[command]
fn cache_clear(state: State<CacheState>) {
let mut data = state.data.write().unwrap();
data.clear();
}
Pattern: RwLock for read-heavy workloads (multiple readers, exclusive writer).
Complex State with Arc
use std::sync::Arc;
use tokio::sync::RwLock;
struct DatabaseConnection {
pool: sqlx::Pool<sqlx::Sqlite>,
}
struct AppState {
db: Arc<RwLock<DatabaseConnection>>,
}
#[command]
async fn query_users(state: State<'_, AppState>) -> Result<Vec<User>, CommandError> {
let db = state.db.read().await;
let users = sqlx::query_as::<_, User>("SELECT * FROM users")
.fetch_all(&db.pool)
.await?;
Ok(users)
}
#[tokio::main]
async fn main() {
let pool = sqlx::SqlitePool::connect("sqlite:db.sqlite").await.unwrap();
tauri::Builder::default()
.manage(AppState {
db: Arc::new(RwLock::new(DatabaseConnection { pool })),
})
.invoke_handler(tauri::generate_handler![query_users])
.run(tauri::generate_context!())
.expect("error");
}
Pattern: Use Arc<RwLock<T>> for async state, tokio::sync::RwLock for async locks.
Pattern 3: Window Management
TMNL Transparent Window Configuration
tauri.conf.json:
{
"tauri": {
"windows": [
{
"title": "TMNL",
"width": 1280,
"height": 800,
"decorations": false,
"transparent": true,
"macOSPrivateApi": true
}
]
}
}
Capabilities (default.json):
{
"permissions": [
"core:window:default",
"core:window:allow-start-dragging",
"core:window:allow-minimize",
"core:window:allow-maximize",
"core:window:allow-close",
"core:window:allow-set-decorations",
"core:window:allow-set-always-on-top"
]
}
Pattern: decorations: false + transparent: true = frameless transparent window.
Custom Window Controls (Frontend)
import { getCurrentWindow } from "@tauri-apps/api/window";
const appWindow = getCurrentWindow();
// Window operations
async function minimizeWindow() {
await appWindow.minimize();
}
async function toggleMaximize() {
await appWindow.toggleMaximize();
}
async function closeWindow() {
await appWindow.close();
}
// Drag region (in React component)
function TitleBar() {
return (
<div data-tauri-drag-region className="titlebar">
<span>TMNL</span>
<div className="controls">
<button onClick={minimizeWindow}>−</button>
<button onClick={toggleMaximize}>□</button>
<button onClick={closeWindow}>×</button>
</div>
</div>
);
}
CSS:
.titlebar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 32px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px;
background: rgba(0, 0, 0, 0.8);
z-index: 9999;
}
Pattern: Use data-tauri-drag-region attribute for draggable areas.
Programmatic Window Creation (Rust)
use tauri::{Manager, WindowBuilder, WindowUrl};
#[command]
fn create_settings_window(app: tauri::AppHandle) -> Result<(), String> {
let settings_window = WindowBuilder::new(
&app,
"settings",
WindowUrl::App("settings.html".into()),
)
.title("Settings")
.inner_size(600.0, 400.0)
.resizable(false)
.decorations(true)
.build()
.map_err(|e| e.to_string())?;
Ok(())
}
Pattern: Use WindowBuilder for runtime window creation.
Pattern 4: WSLg Rendering Workaround (TMNL-Specific)
Problem
WSLg renders Tauri windows with blank/tiny HTML content due to WebKitGTK compositing bugs.
Solution (Automatic Detection)
scripts/tauri-dev.sh:
#!/bin/bash
# Detect WSLg environment
if [ -n "$WSL_DISTRO_NAME" ]; then
echo "[tauri-dev] WSLg detected, applying WebKit workaround"
export WEBKIT_DISABLE_COMPOSITING_MODE=1
fi
# Run Tauri dev
bun run tauri dev
Nix mission-control script:
tauri-dev = {
description = "Run Tauri in development mode";
exec = ''
cd $FLAKE_ROOT/packages/tmnl
if [ -n "$WSL_DISTRO_NAME" ]; then
echo "[tauri-dev] WSLg detected, applying WebKit workaround"
export WEBKIT_DISABLE_COMPOSITING_MODE=1
fi
bun run tauri:dev
'';
};
Pattern: Check $WSL_DISTRO_NAME, set WEBKIT_DISABLE_COMPOSITING_MODE=1 if WSLg.
Platform-Specific Code (Rust)
#[cfg(target_os = "linux")]
fn apply_linux_fixes() {
// WSLg detection
if std::env::var("WSL_DISTRO_NAME").is_ok() {
std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
}
}
#[cfg(target_os = "windows")]
fn apply_windows_fixes() {
// Windows-specific logic
}
#[cfg(target_os = "macos")]
fn apply_macos_fixes() {
// macOS-specific logic
}
fn main() {
#[cfg(target_os = "linux")]
apply_linux_fixes();
#[cfg(target_os = "windows")]
apply_windows_fixes();
#[cfg(target_os = "macos")]
apply_macos_fixes();
tauri::Builder::default()
// ...
.run(tauri::generate_context!())
.expect("error");
}
Pattern: Use #[cfg(target_os = "...")] for platform-specific code.
Pattern 5: Events (Backend → Frontend)
Emitting Events from Rust
use tauri::{Emitter, Manager};
#[command]
async fn start_download(
url: String,
app: tauri::AppHandle,
) -> Result<(), CommandError> {
// Emit progress events
app.emit("download-progress", 0)?;
for i in 1..=100 {
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
app.emit("download-progress", i)?;
}
app.emit("download-complete", url)?;
Ok(())
}
Frontend (TypeScript):
import { listen } from "@tauri-apps/api/event";
// Listen for events
const unlisten = await listen<number>("download-progress", (event) => {
console.log(`Progress: ${event.payload}%`);
});
await listen<string>("download-complete", (event) => {
console.log(`Downloaded: ${event.payload}`);
unlisten(); // Stop listening
});
// Trigger download
await invoke("start_download", { url: "https://example.com/file.zip" });
Pattern: Use app.emit(event, payload) in Rust, listen(event, callback) in frontend.
Window-Specific Events
#[command]
fn notify_window(
window: tauri::Window,
message: String,
) -> Result<(), String> {
window.emit("notification", message)
.map_err(|e| e.to_string())
}
Frontend:
import { getCurrentWindow } from "@tauri-apps/api/window";
const window = getCurrentWindow();
await window.listen<string>("notification", (event) => {
alert(event.payload);
});
Pattern 6: Plugin Architecture
Custom Plugin Structure
use tauri::{plugin::Plugin, Runtime, AppHandle, Invoke};
pub struct DatabasePlugin<R: Runtime> {
invoke_handler: Box<dyn Fn(Invoke<R>) + Send + Sync>,
}
impl<R: Runtime> DatabasePlugin<R> {
pub fn new() -> Self {
Self {
invoke_handler: Box::new(|_| {}),
}
}
}
impl<R: Runtime> Plugin<R> for DatabasePlugin<R> {
fn name(&self) -> &'static str {
"database"
}
fn initialize(&mut self, app: &AppHandle<R>, _config: serde_json::Value) -> tauri::plugin::Result<()> {
// Initialize plugin
app.manage(DatabaseState::new());
Ok(())
}
fn extend_api(&mut self, invoke: Invoke<R>) {
(self.invoke_handler)(invoke);
}
}
// In main.rs
fn main() {
tauri::Builder::default()
.plugin(DatabasePlugin::new())
.run(tauri::generate_context!())
.expect("error");
}
Pattern: Implement Plugin<R> trait for modular functionality.
Pattern 7: Configuration and Build
TMNL Nix Environment
nix/modules/tauri.nix:
{ inputs, lib, ... }:
{
perSystem = { config, pkgs, system, lib, ... }: {
devShells.tmnl-tauri = pkgs.mkShell {
name = "tmnl-tauri";
inputsFrom = [ config.devShells.tmnl-core ];
nativeBuildInputs = with pkgs; [
# GTK/WebKit dependencies (Linux)
gtk3
webkitgtk_4_1
cairo
pango
harfbuzz
glib
atk
librsvg
libsoup_3
# Cross-compilation (Windows)
pkgsCross.mingwW64.stdenv.cc
# Rust toolchain
rustup
];
shellHook = ''
export PKG_CONFIG_PATH="${pkgs.gtk3}/lib/pkgconfig:${pkgs.webkitgtk_4_1}/lib/pkgconfig:$PKG_CONFIG_PATH"
export LD_LIBRARY_PATH="${pkgs.gtk3}/lib:${pkgs.webkitgtk_4_1}/lib:$LD_LIBRARY_PATH"
export RUST_SRC_PATH="${pkgs.rustPlatform.rustLibSrc}"
'';
};
};
}
Pattern: Include GTK3, WebKitGTK, and cross-compilation toolchains in Nix shell.
Cargo.toml Dependencies
[dependencies]
tauri = { version = "2.0", features = ["wry"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
thiserror = "2"
Cross-Compilation (Windows on Linux)
# Install Windows target
rustup target add x86_64-pc-windows-gnu
# Build for Windows
cargo build --target x86_64-pc-windows-gnu
# Or via Tauri CLI
bun run tauri build --target x86_64-pc-windows-gnu
Nix configuration:
export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS="-C link-args=-Wl,--allow-multiple-definition"
Testing Tauri Commands
Unit Testing Commands
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_greet() {
let result = greet("Alice".to_string());
assert_eq!(result, "Hello, Alice!");
}
#[tokio::test]
async fn test_read_file() {
let result = read_file("test.txt".to_string()).await;
assert!(result.is_ok());
}
#[test]
fn test_error_handling() {
let result = read_file("nonexistent.txt".to_string());
assert!(matches!(result, Err(CommandError::FileNotFound(_))));
}
}
Integration Testing with WebDriver
// Use tauri-driver for end-to-end tests
// Requires tauri-driver binary
#[cfg(test)]
mod integration {
use tauri_driver::Driver;
#[test]
fn test_window_title() {
let driver = Driver::new().unwrap();
let title = driver.title().unwrap();
assert_eq!(title, "TMNL");
}
}
Anti-Patterns
1. Blocking I/O in Commands
WRONG:
#[command]
fn read_file_blocking(path: String) -> String {
std::fs::read_to_string(path).unwrap() // Blocks async runtime!
}
RIGHT:
#[command]
async fn read_file_async(path: String) -> Result<String, CommandError> {
tokio::fs::read_to_string(path)
.await
.map_err(CommandError::from)
}
2. Unwrap in Command Handlers
WRONG:
#[command]
fn get_config(state: State<AppState>) -> Config {
state.config.lock().unwrap() // Panics if poisoned!
}
RIGHT:
#[command]
fn get_config(state: State<AppState>) -> Result<Config, CommandError> {
state.config.lock()
.map_err(|e| CommandError::LockPoisoned(e.to_string()))
.map(|c| c.clone())
}
3. Non-Serializable Error Types
WRONG:
#[command]
fn parse_json(input: String) -> Result<Value, serde_json::Error> {
// serde_json::Error doesn't implement Serialize!
serde_json::from_str(&input)
}
RIGHT:
#[derive(Serialize)]
struct ParseError(String);
impl From<serde_json::Error> for ParseError {
fn from(err: serde_json::Error) -> Self {
ParseError(err.to_string())
}
}
#[command]
fn parse_json(input: String) -> Result<Value, ParseError> {
serde_json::from_str(&input).map_err(ParseError::from)
}
4. Ignoring Platform Differences
WRONG:
#[command]
fn open_terminal() {
std::process::Command::new("gnome-terminal").spawn().unwrap();
// Fails on Windows/macOS!
}
RIGHT:
#[command]
fn open_terminal() -> Result<(), String> {
#[cfg(target_os = "linux")]
std::process::Command::new("gnome-terminal")
.spawn()
.map_err(|e| e.to_string())?;
#[cfg(target_os = "windows")]
std::process::Command::new("cmd")
.args(["/c", "start", "cmd"])
.spawn()
.map_err(|e| e.to_string())?;
#[cfg(target_os = "macos")]
std::process::Command::new("open")
.args(["-a", "Terminal"])
.spawn()
.map_err(|e| e.to_string())?;
Ok(())
}
Quick Reference
Command Syntax
// Sync command
#[command]
fn sync_fn(arg: String) -> String { ... }
// Async command
#[command]
async fn async_fn(arg: String) -> Result<String, Error> { ... }
// With state
#[command]
fn with_state(state: State<AppState>) -> i32 { ... }
// With window
#[command]
fn with_window(window: tauri::Window) { ... }
// With app handle
#[command]
fn with_app(app: tauri::AppHandle) { ... }
Frontend Invocation
import { invoke } from "@tauri-apps/api/core";
// Basic
const result = await invoke<string>("command_name", { arg: "value" });
// With error handling
try {
const result = await invoke<string>("command_name", { arg: "value" });
} catch (error) {
console.error("Command failed:", error);
}
State Management
// Register state
.manage(AppState { ... })
// Access in command
fn cmd(state: State<AppState>) { ... }
// Mutex for sync
Mutex<T>
// RwLock for read-heavy
RwLock<T>
// Arc<RwLock<T>> for async
Arc<tokio::sync::RwLock<T>>
TMNL Integration Checklist
- Commands use
async fnfor I/O operations - Error types implement
Serialize(usethiserror) - State uses
Mutex/RwLockappropriately - WSLg workaround applied (check
$WSL_DISTRO_NAME) - Transparent window configured in
tauri.conf.json - Custom drag region implemented with
data-tauri-drag-region - Window controls use
@tauri-apps/api/window - Platform-specific code uses
#[cfg(target_os = "...")] - Nix shell includes GTK/WebKit dependencies
- Cross-compilation target configured if needed
Further Reading
- Tauri Docs: https://tauri.app/
- Rust API: https://docs.rs/tauri
- TMNL Tauri Config:
CLAUDE.mdlines 457-563 - Nix Tauri Module:
nix/modules/tauri.nix - Error Handling: See
rust-effect-patternsskill
Didn't find tool you were looking for?