install
source · Clone the upstream repo
git clone https://github.com/MacPhobos/research-mind
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/MacPhobos/research-mind "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/toolchains-rust-frameworks-tauri" ~/.claude/skills/macphobos-research-mind-toolchains-rust-frameworks-tauri && rm -rf "$T"
manifest:
.claude/skills/toolchains-rust-frameworks-tauri/skill.mdsource content
Tauri Advanced Event System
Event Fundamentals
Backend → Frontend Events
Basic event emission:
use tauri::Window; #[tauri::command] async fn start_download( url: String, window: Window, ) -> Result<(), String> { window.emit("download-started", url) .map_err(|e| e.to_string())?; // Perform download... window.emit("download-complete", "Success") .map_err(|e| e.to_string()) }
Frontend listener:
import { listen, UnlistenFn } from '@tauri-apps/api/event'; const unlisten = await listen<string>('download-started', (event) => { console.log('Download started:', event.payload); }); // Clean up when done unlisten();
Structured Event Payloads
Typed Events with Serde
Backend:
use serde::Serialize; #[derive(Serialize, Clone)] struct ProgressEvent { current: usize, total: usize, percentage: f64, message: String, speed_mbps: Option<f64>, } #[tauri::command] async fn download_file( url: String, window: Window, ) -> Result<(), String> { let total_size = get_file_size(&url).await?; for chunk in 0..total_size { // Download chunk... let progress = ProgressEvent { current: chunk, total: total_size, percentage: (chunk as f64 / total_size as f64) * 100.0, message: format!("Downloading... {}/{}", chunk, total_size), speed_mbps: Some(calculate_speed()), }; window.emit("download-progress", progress) .map_err(|e| e.to_string())?; } Ok(()) }
Frontend:
interface ProgressEvent { current: number; total: number; percentage: number; message: string; speed_mbps?: number; } const unlisten = await listen<ProgressEvent>('download-progress', (event) => { const { current, total, percentage, message, speed_mbps } = event.payload; updateProgressBar(percentage); updateStatus(message); if (speed_mbps) { updateSpeed(speed_mbps); } });
Complex Event Payloads
#[derive(Serialize, Clone)] #[serde(tag = "type", content = "data")] enum AppEvent { UserLoggedIn { user_id: String, username: String }, UserLoggedOut { user_id: String }, DataSynced { items_count: usize, timestamp: String }, ErrorOccurred { code: String, message: String, recoverable: bool }, } #[tauri::command] async fn perform_login( username: String, password: String, window: Window, ) -> Result<String, String> { let user = authenticate(&username, &password).await?; // Emit structured event window.emit("app-event", AppEvent::UserLoggedIn { user_id: user.id.clone(), username: user.username.clone(), }).map_err(|e| e.to_string())?; Ok(user.id) }
Frontend:
type AppEvent = | { type: 'UserLoggedIn'; data: { user_id: string; username: string } } | { type: 'UserLoggedOut'; data: { user_id: string } } | { type: 'DataSynced'; data: { items_count: number; timestamp: string } } | { type: 'ErrorOccurred'; data: { code: string; message: string; recoverable: boolean } }; listen<AppEvent>('app-event', (event) => { const appEvent = event.payload; switch (appEvent.type) { case 'UserLoggedIn': handleLogin(appEvent.data.user_id, appEvent.data.username); break; case 'UserLoggedOut': handleLogout(appEvent.data.user_id); break; case 'DataSynced': showSyncSuccess(appEvent.data.items_count); break; case 'ErrorOccurred': handleError(appEvent.data); break; } });
Streaming Data Patterns
Real-Time Data Stream
#[tauri::command] async fn stream_sensor_data( sensor_id: String, window: Window, ) -> Result<(), String> { let mut interval = tokio::time::interval(Duration::from_millis(100)); for _ in 0..100 { interval.tick().await; let reading = read_sensor(&sensor_id).await?; window.emit("sensor-reading", reading) .map_err(|e| e.to_string())?; } window.emit("sensor-stream-ended", sensor_id) .map_err(|e| e.to_string()) }
Frontend with React:
import { useEffect, useState } from 'react'; import { listen } from '@tauri-apps/api/event'; interface SensorReading { value: number; timestamp: number; unit: string; } function SensorMonitor() { const [readings, setReadings] = useState<SensorReading[]>([]); useEffect(() => { let unlisten: UnlistenFn | undefined; listen<SensorReading>('sensor-reading', (event) => { setReadings(prev => [...prev.slice(-99), event.payload]); }).then(fn => unlisten = fn); return () => unlisten?.(); }, []); return ( <div> {readings.map((r, i) => ( <div key={i}>{r.value} {r.unit}</div> ))} </div> ); }
Buffered Streaming
#[tauri::command] async fn stream_logs( log_file: String, window: Window, ) -> Result<(), String> { use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::fs::File; let file = File::open(log_file).await .map_err(|e| e.to_string())?; let reader = BufReader::new(file); let mut lines = reader.lines(); let mut buffer = Vec::new(); while let Some(line) = lines.next_line().await .map_err(|e| e.to_string())? { buffer.push(line); // Send in batches of 10 lines if buffer.len() >= 10 { window.emit("log-batch", buffer.clone()) .map_err(|e| e.to_string())?; buffer.clear(); } } // Send remaining lines if !buffer.is_empty() { window.emit("log-batch", buffer) .map_err(|e| e.to_string())?; } Ok(()) }
Multi-Window Communication
Broadcasting to All Windows
use tauri::{AppHandle, Manager}; #[tauri::command] async fn broadcast_message( message: String, app: AppHandle, ) -> Result<(), String> { // Emit to ALL windows app.emit_all("broadcast", message) .map_err(|e| e.to_string()) }
Targeted Window Messaging
#[tauri::command] async fn send_to_window( target_window: String, message: String, app: AppHandle, ) -> Result<(), String> { // Get specific window if let Some(window) = app.get_window(&target_window) { window.emit("private-message", message) .map_err(|e| e.to_string())?; Ok(()) } else { Err(format!("Window '{}' not found", target_window)) } }
Window-to-Window via Backend
Window A (sender):
import { invoke } from '@tauri-apps/api/core'; async function sendToSettings(data: any) { await invoke('relay_to_settings', { data }); }
Backend relay:
#[tauri::command] async fn relay_to_settings( data: serde_json::Value, app: AppHandle, ) -> Result<(), String> { if let Some(settings_window) = app.get_window("settings") { settings_window.emit("data-update", data) .map_err(|e| e.to_string())?; } Ok(()) }
Window B (receiver - settings):
import { listen } from '@tauri-apps/api/event'; useEffect(() => { let unlisten: UnlistenFn | undefined; listen('data-update', (event) => { console.log('Received from main window:', event.payload); updateSettings(event.payload); }).then(fn => unlisten = fn); return () => unlisten?.(); }, []);
Frontend → Backend Events
Custom Frontend Events
import { emit } from '@tauri-apps/api/event'; // Frontend emits event await emit('user-action', { action: 'button-click', button_id: 'save-button', timestamp: Date.now() });
Backend listener:
use tauri::{Manager, Listener}; fn main() { tauri::Builder::default() .setup(|app| { let app_handle = app.handle(); // Listen for frontend events app_handle.listen_global("user-action", move |event| { if let Some(payload) = event.payload() { println!("User action: {}", payload); // Process event... } }); Ok(()) }) .run(tauri::generate_context!()) .expect("error while running tauri application"); }
Advanced Listener Management
React Hook for Events
import { useEffect, useState } from 'react'; import { listen, UnlistenFn } from '@tauri-apps/api/event'; function useEvent<T>(eventName: string): T | null { const [payload, setPayload] = useState<T | null>(null); useEffect(() => { let unlisten: UnlistenFn | undefined; listen<T>(eventName, (event) => { setPayload(event.payload); }).then(fn => unlisten = fn); return () => unlisten?.(); }, [eventName]); return payload; } // Usage function ProgressDisplay() { const progress = useEvent<ProgressEvent>('download-progress'); if (!progress) return null; return ( <div> Progress: {progress.percentage.toFixed(2)}% </div> ); }
Event Queue Pattern
import { listen } from '@tauri-apps/api/event'; class EventQueue<T> { private queue: T[] = []; private unlisten?: UnlistenFn; async start(eventName: string) { this.unlisten = await listen<T>(eventName, (event) => { this.queue.push(event.payload); }); } dequeue(): T | undefined { return this.queue.shift(); } clear() { this.queue = []; } stop() { this.unlisten?.(); } get length() { return this.queue.length; } } // Usage const progressQueue = new EventQueue<ProgressEvent>(); await progressQueue.start('download-progress'); // Process queue periodically setInterval(() => { while (progressQueue.length > 0) { const event = progressQueue.dequeue(); processProgress(event); } }, 100);
One-Time Events
import { once } from '@tauri-apps/api/event'; // Listen for event only once await once<string>('initialization-complete', (event) => { console.log('App initialized:', event.payload); startApp(); });
Error Handling in Events
Safe Event Emission
async fn emit_safe(window: &Window, event: &str, payload: impl Serialize) -> Result<(), String> { window.emit(event, payload) .map_err(|e| { eprintln!("Failed to emit event '{}': {}", event, e); e.to_string() }) } #[tauri::command] async fn process_with_events( window: Window, ) -> Result<(), String> { emit_safe(&window, "processing-started", "Starting...") .await?; // Process... emit_safe(&window, "processing-complete", "Done!") .await?; Ok(()) }
Performance Considerations
Throttling Events
use std::time::{Duration, Instant}; #[tauri::command] async fn high_frequency_updates( window: Window, ) -> Result<(), String> { let mut last_emit = Instant::now(); let throttle_duration = Duration::from_millis(100); for i in 0..10000 { let value = compute_value(i); // Only emit every 100ms if last_emit.elapsed() >= throttle_duration { window.emit("update", value) .map_err(|e| e.to_string())?; last_emit = Instant::now(); } } Ok(()) }
Batching Events
#[tauri::command] async fn batch_updates( window: Window, ) -> Result<(), String> { let mut batch = Vec::new(); for item in process_items() { batch.push(item); // Emit in batches of 50 if batch.len() >= 50 { window.emit("batch-update", batch.clone()) .map_err(|e| e.to_string())?; batch.clear(); } } // Emit remaining items if !batch.is_empty() { window.emit("batch-update", batch) .map_err(|e| e.to_string())?; } Ok(()) }
Best Practices
- Always clean up listeners - Use
to prevent memory leaksunlisten() - Type event payloads - Define interfaces for type safety
- Use structured events - Tagged unions for multiple event types
- Throttle high-frequency events - Prevent overwhelming frontend
- Batch when possible - Reduce serialization overhead
- Handle errors gracefully - Log failed emissions, don't crash
- Use once() for one-time events - Initialization, completion signals
- Namespace event names - Use prefixes like "download:", "user:", "system:"
Common Pitfalls
❌ Forgetting to unlisten:
// WRONG - memory leak function Component() { listen('my-event', handler); // Never cleaned up! } // CORRECT function Component() { useEffect(() => { let unlisten: UnlistenFn | undefined; listen('my-event', handler).then(fn => unlisten = fn); return () => unlisten?.(); }, []); }
❌ Not handling serialization errors:
// WRONG - struct can't serialize #[derive(Clone)] // Missing Serialize! struct Event { } window.emit("event", Event {}); // Runtime error! // CORRECT #[derive(Serialize, Clone)] struct Event { }
❌ Emitting too frequently:
// WRONG - 10000 events in quick succession for i in 0..10000 { window.emit("update", i); // Overwhelming! } // CORRECT - throttle or batch
Summary
- Events are async - Backend → Frontend communication
- Always type payloads - Use serde::Serialize + TypeScript interfaces
- Clean up listeners - Call
in cleanupunlisten() - Throttle/batch - High-frequency events need rate limiting
- Use structured payloads - Tagged unions for multiple event types
- Window targeting -
for specific,emit()
for broadcastemit_all() - Frontend events - Use
from frontend, listen in backend setupemit()