9.9 KiB
9.9 KiB
Tauri v2 IPC Patterns Reference
Overview
Tauri v2 provides three IPC primitives:
- Commands: Request-response (most common)
- Events: Fire-and-forget notifications
- Channels: High-frequency streaming
Commands (invoke)
Basic Command
Rust:
#[tauri::command]
fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
// Register in builder
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
Frontend:
import { invoke } from '@tauri-apps/api/core';
const result = await invoke<string>('greet', { name: 'World' });
Command with Multiple Arguments
Rust:
#[tauri::command]
fn calculate(a: i32, b: i32, operation: String) -> i32 {
match operation.as_str() {
"add" => a + b,
"sub" => a - b,
"mul" => a * b,
"div" => a / b,
_ => 0,
}
}
Frontend:
const result = await invoke<number>('calculate', {
a: 10,
b: 5,
operation: 'add'
});
Async Command
Rust:
#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
// Use owned types (String, not &str) in async commands
let response = reqwest::get(&url)
.await
.map_err(|e| e.to_string())?;
response.text()
.await
.map_err(|e| e.to_string())
}
Frontend:
try {
const data = await invoke<string>('fetch_data', { url: 'https://api.example.com' });
} catch (error) {
console.error('Failed:', error);
}
Command with Result Error Handling
Rust:
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("File not found: {0}")]
NotFound(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Permission denied")]
PermissionDenied,
}
impl serde::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())
}
}
#[tauri::command]
fn read_config(path: String) -> Result<Config, AppError> {
if !std::path::Path::new(&path).exists() {
return Err(AppError::NotFound(path));
}
// ...
}
Frontend:
try {
const config = await invoke<Config>('read_config', { path: '/config.json' });
} catch (error) {
// error is the serialized error string
console.error('Config error:', error);
}
Command with State
Rust:
use std::sync::Mutex;
use tauri::State;
struct AppState {
counter: u32,
items: Vec<String>,
}
#[tauri::command]
fn get_count(state: State<'_, Mutex<AppState>>) -> u32 {
state.lock().unwrap().counter
}
#[tauri::command]
fn increment(state: State<'_, Mutex<AppState>>) -> u32 {
let mut s = state.lock().unwrap();
s.counter += 1;
s.counter
}
#[tauri::command]
fn add_item(item: String, state: State<'_, Mutex<AppState>>) {
state.lock().unwrap().items.push(item);
}
// In builder:
tauri::Builder::default()
.manage(Mutex::new(AppState { counter: 0, items: vec![] }))
.invoke_handler(tauri::generate_handler![get_count, increment, add_item])
Command with Window Access
Rust:
use tauri::{WebviewWindow, AppHandle};
#[tauri::command]
fn get_window_info(window: WebviewWindow) -> String {
format!("Window label: {}", window.label())
}
#[tauri::command]
fn create_window(app: AppHandle) -> Result<(), String> {
tauri::WebviewWindowBuilder::new(
&app,
"new-window",
tauri::WebviewUrl::App("index.html".into())
)
.title("New Window")
.build()
.map_err(|e| e.to_string())?;
Ok(())
}
Command with Raw Binary Data
Rust:
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)) // Avoids JSON serialization overhead
}
#[tauri::command]
fn upload_file(request: tauri::ipc::Request) -> Result<(), String> {
let tauri::ipc::InvokeBody::Raw(data) = request.body() else {
return Err("Expected raw body".into());
};
std::fs::write("upload.bin", data).map_err(|e| e.to_string())
}
Frontend:
// Reading binary
const data = await invoke<ArrayBuffer>('read_binary_file', { path: '/file.bin' });
// Uploading binary
const fileData = new Uint8Array([1, 2, 3, 4]);
await invoke('upload_file', fileData);
Events
Emit from Rust to Frontend
Rust:
use tauri::Emitter;
#[tauri::command]
fn start_background_task(app: tauri::AppHandle) {
std::thread::spawn(move || {
for i in 0..100 {
std::thread::sleep(std::time::Duration::from_millis(100));
app.emit("progress", i).unwrap();
}
app.emit("complete", "Task finished").unwrap();
});
}
// Emit to specific window
#[tauri::command]
fn notify_window(app: tauri::AppHandle, window_label: String, message: String) {
app.emit_to(&window_label, "notification", message).unwrap();
}
Frontend:
import { listen, once } from '@tauri-apps/api/event';
// Listen continuously
const unlisten = await listen<number>('progress', (event) => {
console.log(`Progress: ${event.payload}%`);
});
// Listen once
await once<string>('complete', (event) => {
console.log(event.payload);
});
// Clean up when done
unlisten();
Emit from Frontend to Rust
Frontend:
import { emit } from '@tauri-apps/api/event';
await emit('user-action', { action: 'click', target: 'button' });
Rust (in setup or command):
use tauri::Listener;
fn setup_listeners(app: &tauri::App) {
app.listen("user-action", |event| {
println!("User action: {:?}", event.payload());
});
}
Window-Specific Events
Rust:
use tauri::{Emitter, WebviewWindow};
#[tauri::command]
fn emit_to_window(window: WebviewWindow, message: String) {
window.emit("window-message", message).unwrap();
}
Channels (Streaming)
Basic Channel Pattern
Rust:
use tauri::ipc::Channel;
#[derive(Clone, serde::Serialize)]
struct ProgressUpdate {
current: u32,
total: u32,
message: String,
}
#[tauri::command]
async fn process_files(
files: Vec<String>,
on_progress: Channel<ProgressUpdate>
) -> Result<(), String> {
let total = files.len() as u32;
for (i, file) in files.iter().enumerate() {
// Process file...
on_progress.send(ProgressUpdate {
current: i as u32 + 1,
total,
message: format!("Processing {}", file),
}).unwrap();
}
Ok(())
}
Frontend:
import { invoke, Channel } from '@tauri-apps/api/core';
interface ProgressUpdate {
current: number;
total: number;
message: string;
}
const channel = new Channel<ProgressUpdate>();
channel.onmessage = (update) => {
const percent = (update.current / update.total) * 100;
console.log(`${percent}% - ${update.message}`);
};
await invoke('process_files', {
files: ['file1.txt', 'file2.txt'],
onProgress: channel
});
Tagged Union Events (Discriminated)
Rust:
use tauri::ipc::Channel;
#[derive(Clone, serde::Serialize)]
#[serde(tag = "event", content = "data")]
enum DownloadEvent {
Started { url: String, size: u64 },
Progress { downloaded: u64, total: u64 },
Complete { path: String },
Error { message: String },
}
#[tauri::command]
async fn download_file(
url: String,
on_event: Channel<DownloadEvent>
) -> Result<String, String> {
on_event.send(DownloadEvent::Started {
url: url.clone(),
size: 1000,
}).unwrap();
for i in 0..=100 {
on_event.send(DownloadEvent::Progress {
downloaded: i * 10,
total: 1000,
}).unwrap();
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
let path = "/downloads/file.zip".to_string();
on_event.send(DownloadEvent::Complete {
path: path.clone(),
}).unwrap();
Ok(path)
}
Frontend:
import { invoke, Channel } from '@tauri-apps/api/core';
type DownloadEvent =
| { event: 'Started'; data: { url: string; size: number } }
| { event: 'Progress'; data: { downloaded: number; total: number } }
| { event: 'Complete'; data: { path: string } }
| { event: 'Error'; data: { message: string } };
const channel = new Channel<DownloadEvent>();
channel.onmessage = (msg) => {
switch (msg.event) {
case 'Started':
console.log(`Starting download: ${msg.data.url} (${msg.data.size} bytes)`);
break;
case 'Progress':
const percent = (msg.data.downloaded / msg.data.total) * 100;
console.log(`Download: ${percent.toFixed(1)}%`);
break;
case 'Complete':
console.log(`Downloaded to: ${msg.data.path}`);
break;
case 'Error':
console.error(`Download failed: ${msg.data.message}`);
break;
}
};
const path = await invoke<string>('download_file', {
url: 'https://example.com/file.zip',
onEvent: channel
});
IPC Selection Guide
| Pattern | Use Case | Direction | Frequency |
|---|---|---|---|
| Commands | Request-response, data fetching | Frontend → Rust | One-time |
| Events | Notifications, state changes | Bidirectional | Low-medium |
| Channels | Progress updates, streaming data | Rust → Frontend | High |
When to Use Each
Commands (invoke)
- Fetching data from Rust
- Performing actions that return results
- CRUD operations
- Most common pattern
Events (emit/listen)
- Notifying UI of background changes
- Broadcasting to multiple windows
- Fire-and-forget notifications
- System events (window close, minimize)
Channels
- File download/upload progress
- Long-running operations with updates
- Streaming log output
- Real-time data feeds