
Desktop Applications
- 99 installs
- 63 repo stars
- Updated July 18, 2026
- bobmatnyc/claude-mpm-skills
desktop-applications is a Claude Code skill for building cross-platform Rust desktop apps with Tauri or native GUI frameworks like egui, iced, and slint.
About
desktop-applications is a skill for building cross-platform desktop apps in Rust. It covers Tauri for web UI plus a Rust backend and native GUI frameworks like egui, iced, and slint for pure Rust interfaces. A developer uses it to build Electron alternatives, system utilities, or high-performance tools for Windows, macOS, and Linux. It covers framework choice, IPC and state management, platform integration, testing, and distribution.
- Builds cross-platform desktop apps with Tauri (web UI + Rust backend)
- Covers native GUI alternatives egui, iced, and slint for pure Rust interfaces
- Bundle sizes under 5MB with type-safe IPC and Tokio async patterns
Desktop Applications by the numbers
- 99 all-time installs (skills.sh)
- Ranked #65 of 121 Rust skills by installs in the Skillselion catalog
- Data as of Aug 1, 2026 (Skillselion catalog sync)
desktop-applications capabilities & compatibility
- Capabilities
- desktop app · frontend · cross platform build
- Use cases
- frontend · devops
- Platforms
- macOS · Windows · Linux
What desktop-applications says it does
Build cross-platform desktop applications with Rust using Tauri framework and native GUI alternatives
With bundle sizes under 5MB and memory usage 1/10th of Electron, Tauri apps deliver desktop-class performance.
**TAURI FOR WEB UI + RUST BACKEND | NATIVE GUI FOR PURE RUST | NEVER MIX BUSINESS LOGIC IN FRONTEND**
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill desktop-applicationsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 99 |
|---|---|
| repo stars | ★ 63 |
| Last updated | July 18, 2026 |
| Repository | bobmatnyc/claude-mpm-skills ↗ |
What it does
Build a cross-platform Rust desktop app with Tauri or a native GUI framework and distribute it.
Who is it for?
developers building cross-platform desktop apps as Electron alternatives with Rust
Skip if: simple web apps, mobile-first apps, pure CLI tools, or teams lacking Rust experience
When should I use this skill?
building desktop applications that need native performance, small bundle sizes, system integration, or memory safety
By the numbers
- bundle sizes under 5MB
- memory usage 1/10th of Electron
- 3 native GUI frameworks: egui, iced, slint
Files
Rust Desktop Applications
Overview
Rust has emerged as a premier language for building desktop applications that combine native performance with memory safety. The ecosystem offers two main approaches: Tauri for hybrid web UI + Rust backend apps (think Electron but 10x smaller and faster), and native GUI frameworks like egui, iced, and slint for pure Rust interfaces.
Tauri has revolutionized desktop development by enabling developers to use web technologies (React, Vue, Svelte) for the frontend while leveraging Rust's performance and safety for system-level operations. With bundle sizes under 5MB and memory usage 1/10th of Electron, Tauri apps deliver desktop-class performance. Native frameworks shine for specialized use cases: egui for immediate-mode tools and game editors, iced for Elm-style reactive apps, slint for embedded and declarative UIs.
This skill covers the complete Rust desktop development lifecycle from framework selection through architecture, state management, platform integration, and deployment. You'll build production-ready applications with proper IPC patterns, async runtime integration, native system access, and cross-platform distribution.
When to Use This Skill
Activate when building desktop applications that need native performance, small bundle sizes, system integration, or memory safety guarantees. Specifically use when:
- Building Electron alternatives with web UI + Rust backend (Tauri)
- Creating high-performance developer tools or productivity apps
- Developing system utilities requiring native OS integration
- Building cross-platform apps for Windows, macOS, and Linux
- Need <10MB bundle sizes vs 100MB+ Electron apps
- Implementing real-time applications (audio/video processing, games)
- Creating embedded GUI applications (kiosks, IoT devices)
Don't Use When
- Simple web apps - Use Next.js, Vite, or web frameworks
- Mobile-first applications - Use Flutter, React Native, or Kotlin Multiplatform
- Purely CLI tools - Use clap/structopt for command-line apps
- Browser extensions - Use WebExtensions API
- Quick prototypes - Native development has setup overhead
- Team lacks Rust experience - Steep learning curve for system programming
The Iron Law
TAURI FOR WEB UI + RUST BACKEND | NATIVE GUI FOR PURE RUST | NEVER MIX BUSINESS LOGIC IN FRONTEND
Duplicating logic between frontend and backend, or bypassing IPC for direct access, violates architecture.
Core Principles
1. Framework Alignment: Tauri for web-skilled teams, native GUI for Rust-first projects 2. Clear Separation: Frontend handles UI, Rust backend handles business logic and system access 3. Type-Safe IPC: Commands and events strongly typed with serde serialization 4. Async Runtime: Tokio for backend concurrency, prevent blocking main thread 5. Security First: Validate all IPC inputs, minimize exposed commands, CSP policies 6. Platform Abstraction: Write once, handle platform differences gracefully
Quick Start
1. Choose Your Framework
- Tauri: Have web skills (React/Vue/Svelte)? Want rapid UI development? →
cargo install tauri-cli - Native GUI: Pure Rust project? Immediate mode or reactive patterns? → Choose egui/iced/slint
2. Initialize Project
# Tauri
cargo create-tauri-app my-app
# Select: npm, React/Vue/Svelte, TypeScript
# Native (egui example)
cargo new my-app
cargo add eframe egui3. Setup Architecture
- Tauri: Define commands in
src-tauri/src/main.rs, handle IPC - Native: Implement app state, event loop, and UI update logic
- Structure:
src/(backend),ui/orsrc-ui/(frontend if Tauri)
4. Implement Core Features
- Define Tauri commands with
#[tauri::command] - Setup state management (Arc<Mutex<T>> or channels)
- Integrate Tokio for async operations
- Add error handling with
Result<T, E>
5. Add Platform Integration
- File system access (dialogs, read/write)
- System tray, notifications, auto-updates
- Deep linking, custom URL schemes
- OS-specific features (Windows registry, macOS sandboxing)
6. Build and Distribute
# Development
cargo tauri dev # or cargo run
# Production build
cargo tauri build # Creates installers for current platform
# Cross-platform: Use GitHub Actions with matrix buildsFramework Decision Tree
Need desktop app?
├─ Have web frontend skills (React/Vue/Svelte)?
│ └─ YES → Use Tauri
│ ├─ Need <5MB bundles? ✓
│ ├─ System integration? ✓
│ ├─ Cross-platform? ✓
│ └─ Rapid UI development? ✓
│
└─ Pure Rust, no web frontend?
├─ Game editor or immediate mode tools? → egui
├─ Elm-style reactive architecture? → iced
├─ Declarative UI, embedded devices? → slint
└─ Data-first reactive? → druidTauri when: Web UI expertise, need modern frontend frameworks, rapid iteration Native when: Maximum performance, no web dependencies, specialized UI patterns
Navigation
Detailed guides available:
- [Tauri Framework](references/tauri-framework.md): Complete Tauri architecture, project setup, IPC communication patterns, native API access, configuration, security model, and build process with real-world examples
- [Native GUI Frameworks](references/native-gui-frameworks.md): Deep dive into egui, iced, druid, and slint - architecture patterns, when to use each, comparison matrix, and production code examples
- [Architecture Patterns](references/architecture-patterns.md): Desktop-specific patterns including MVC/MVVM, command pattern, event-driven architecture, plugin systems, resource management, and error handling strategies
- [State Management](references/state-management.md): State management strategies, async runtime integration with Tokio, message passing, reactive patterns, persistence (configs/databases), and multi-window state sharing
- [Platform Integration](references/platform-integration.md): File system access, system tray, notifications, auto-updates, deep linking, OS-specific features (Windows/macOS/Linux), permissions, and security
- [Testing & Deployment](references/testing-deployment.md): Integration testing, UI testing approaches, cross-compilation, platform-specific builds, distribution (installers/bundles/stores), signing, notarization, and CI/CD pipelines
Key Patterns
Correct Tauri Pattern:
✅ Commands in Rust backend
✅ Type-safe IPC with serde
✅ Async operations with Tokio
✅ State management with Arc<Mutex<T>>
✅ Error propagation with Result<T, E>
✅ Frontend calls backend via invoke()Correct Native GUI Pattern:
✅ Immediate mode (egui) or retained mode (iced)
✅ State updates trigger redraws
✅ Event handling in Rust
✅ Platform-agnostic rendering
✅ Resource cleanup on dropIncorrect Patterns:
❌ Business logic in frontend JavaScript
❌ Exposing unsafe commands without validation
❌ Blocking operations on main thread
❌ Direct filesystem access from frontend
❌ Missing error handling on IPC
❌ Hardcoded platform-specific pathsRed Flags - STOP
- Blocking the main thread - Use Tokio spawn for long operations
- Exposing sensitive commands - Validate, rate limit, minimize surface area
- Missing CSP in Tauri - Configure Content Security Policy
- No input validation - Always validate IPC command arguments
- Direct frontend file access - Use Tauri file system APIs
- Ignoring platform differences - Test on all target platforms
- Large bundle sizes - Profile and optimize dependencies
- No auto-update strategy - Users won't manually update
Integration with Other Skills
- vite-local-dev: Integrate Vite with Tauri for hot module replacement and fast frontend builds
- async-testing: Test async Tokio code in Tauri commands and background tasks
- performance-profiling: Profile Rust backend with Criterion, flamegraphs for optimization
- test-driven-development: Write tests for commands, state management, and business logic
- verification-before-completion: Test cross-platform builds before shipping
- systematic-debugging: Debug Tauri IPC issues, inspect console logs, use Rust debugger
Real-World Impact
Performance Metrics:
- Bundle size: 3-5MB (Tauri) vs 100-200MB (Electron)
- Memory usage: 50-100MB (Tauri) vs 500MB-1GB (Electron)
- Startup time: <1s (Tauri) vs 3-5s (Electron)
- Build time: 1-2 min (Tauri) vs 5-10 min (Electron)
Production Examples:
- Warp Terminal: High-performance terminal built with Rust (egui/custom)
- Lapce: Fast code editor using Druid (later custom framework)
- Zed: Collaborative code editor with native Rust UI
- Notion-like apps: Using Tauri for desktop versions
- System utilities: File managers, task managers, monitoring tools
The Bottom Line
Rust desktop development offers unmatched performance with memory safety.
Choose Tauri for web UI + Rust backend with tiny bundles. Choose native GUI for pure Rust with specialized patterns. Architect with clear frontend/backend separation. Use type-safe IPC. Integrate Tokio for async. Handle platform differences. Test cross-platform early.
This is the Rust desktop way.
{
"name": "desktop-applications",
"version": "1.0.0",
"category": "toolchain",
"toolchain": "rust",
"framework": null,
"tags": [
"performance",
"async",
"api",
"testing",
"debugging"
],
"entry_point_tokens": 52,
"full_tokens": 32953,
"author": "bobmatnyc",
"license": "MIT",
"requires": [],
"updated": "2025-11-21",
"source_path": "rust/desktop-applications/SKILL.md",
"source": "https://github.com/bobmatnyc/claude-mpm",
"created": "2025-11-21",
"modified": "2025-11-21",
"maintainer": "Claude MPM Team",
"attribution_required": true,
"repository": "https://github.com/bobmatnyc/claude-mpm-skills"
}
Architecture Patterns
Desktop-specific architectural patterns for building maintainable, scalable Rust applications with clear separation of concerns.
Core Architectural Patterns
MVC (Model-View-Controller)
Traditional pattern adapted for desktop applications.
// Model - Application state and business logic
mod model {
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct User {
pub id: u64,
pub name: String,
pub email: String,
}
#[derive(Clone, Debug)]
pub struct UserModel {
users: Vec<User>,
}
impl UserModel {
pub fn new() -> Self {
Self { users: Vec::new() }
}
pub fn add_user(&mut self, user: User) {
self.users.push(user);
}
pub fn remove_user(&mut self, id: u64) {
self.users.retain(|u| u.id != id);
}
pub fn get_users(&self) -> &[User] {
&self.users
}
pub fn find_user(&self, id: u64) -> Option<&User> {
self.users.iter().find(|u| u.id == id)
}
}
}
// Controller - Handles user input and updates model
mod controller {
use super::model::{User, UserModel};
pub struct UserController {
model: UserModel,
}
impl UserController {
pub fn new(model: UserModel) -> Self {
Self { model }
}
pub fn create_user(&mut self, name: String, email: String) -> Result<(), String> {
// Validation
if name.is_empty() {
return Err("Name cannot be empty".to_string());
}
let id = self.model.get_users().len() as u64 + 1;
let user = User { id, name, email };
self.model.add_user(user);
Ok(())
}
pub fn delete_user(&mut self, id: u64) -> Result<(), String> {
if self.model.find_user(id).is_none() {
return Err("User not found".to_string());
}
self.model.remove_user(id);
Ok(())
}
pub fn get_model(&self) -> &UserModel {
&self.model
}
}
}
// View - UI rendering (Tauri example)
#[tauri::command]
fn get_users(controller: tauri::State<UserController>) -> Vec<User> {
controller.get_model().get_users().to_vec()
}
#[tauri::command]
fn create_user(
controller: tauri::State<UserController>,
name: String,
email: String,
) -> Result<(), String> {
controller.inner().lock().unwrap().create_user(name, email)
}MVVM (Model-View-ViewModel)
Better for reactive UIs with two-way data binding.
use std::sync::{Arc, Mutex};
use tokio::sync::broadcast;
// Model - Business data
#[derive(Clone, Debug)]
pub struct TodoItem {
pub id: u64,
pub title: String,
pub completed: bool,
}
pub struct TodoModel {
items: Vec<TodoItem>,
next_id: u64,
}
impl TodoModel {
pub fn new() -> Self {
Self {
items: Vec::new(),
next_id: 1,
}
}
pub fn add_item(&mut self, title: String) -> TodoItem {
let item = TodoItem {
id: self.next_id,
title,
completed: false,
};
self.next_id += 1;
self.items.push(item.clone());
item
}
pub fn toggle_item(&mut self, id: u64) -> Option<bool> {
self.items
.iter_mut()
.find(|item| item.id == id)
.map(|item| {
item.completed = !item.completed;
item.completed
})
}
pub fn get_items(&self) -> &[TodoItem] {
&self.items
}
}
// ViewModel - Presentation logic and state
pub struct TodoViewModel {
model: Arc<Mutex<TodoModel>>,
change_notifier: broadcast::Sender<ViewModelEvent>,
}
#[derive(Clone, Debug)]
pub enum ViewModelEvent {
ItemAdded(TodoItem),
ItemToggled(u64, bool),
ItemsChanged,
}
impl TodoViewModel {
pub fn new() -> Self {
let (tx, _rx) = broadcast::channel(100);
Self {
model: Arc::new(Mutex::new(TodoModel::new())),
change_notifier: tx,
}
}
pub fn add_todo(&self, title: String) -> Result<(), String> {
if title.trim().is_empty() {
return Err("Title cannot be empty".to_string());
}
let mut model = self.model.lock().unwrap();
let item = model.add_item(title);
drop(model);
let _ = self.change_notifier.send(ViewModelEvent::ItemAdded(item));
Ok(())
}
pub fn toggle_todo(&self, id: u64) -> Result<(), String> {
let mut model = self.model.lock().unwrap();
let completed = model
.toggle_item(id)
.ok_or("Item not found".to_string())?;
drop(model);
let _ = self
.change_notifier
.send(ViewModelEvent::ItemToggled(id, completed));
Ok(())
}
pub fn get_todos(&self) -> Vec<TodoItem> {
self.model.lock().unwrap().get_items().to_vec()
}
pub fn subscribe(&self) -> broadcast::Receiver<ViewModelEvent> {
self.change_notifier.subscribe()
}
}
// View - Tauri commands
#[tauri::command]
async fn add_todo(viewmodel: tauri::State<'_, TodoViewModel>, title: String) -> Result<(), String> {
viewmodel.add_todo(title)
}
#[tauri::command]
async fn toggle_todo(viewmodel: tauri::State<'_, TodoViewModel>, id: u64) -> Result<(), String> {
viewmodel.toggle_todo(id)
}
#[tauri::command]
async fn get_todos(viewmodel: tauri::State<'_, TodoViewModel>) -> Vec<TodoItem> {
viewmodel.get_todos()
}
// Setup with change notifications
fn main() {
let viewmodel = TodoViewModel::new();
let mut rx = viewmodel.subscribe();
// Background task to push updates to frontend
tokio::spawn(async move {
while let Ok(event) = rx.recv().await {
// Emit event to frontend
println!("ViewModel changed: {:?}", event);
}
});
tauri::Builder::default()
.manage(viewmodel)
.invoke_handler(tauri::generate_handler![add_todo, toggle_todo, get_todos])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Command Pattern
Encapsulate actions as objects for undo/redo functionality.
use std::fmt;
// Command trait
pub trait Command: fmt::Debug {
fn execute(&mut self, app: &mut Application) -> Result<(), String>;
fn undo(&mut self, app: &mut Application) -> Result<(), String>;
fn description(&self) -> String;
}
// Application state
pub struct Application {
pub text: String,
pub cursor: usize,
}
// Concrete commands
#[derive(Debug)]
pub struct InsertTextCommand {
text: String,
position: usize,
}
impl Command for InsertTextCommand {
fn execute(&mut self, app: &mut Application) -> Result<(), String> {
app.text.insert_str(self.position, &self.text);
app.cursor = self.position + self.text.len();
Ok(())
}
fn undo(&mut self, app: &mut Application) -> Result<(), String> {
let start = self.position;
let end = self.position + self.text.len();
app.text.drain(start..end);
app.cursor = self.position;
Ok(())
}
fn description(&self) -> String {
format!("Insert '{}'", self.text)
}
}
#[derive(Debug)]
pub struct DeleteTextCommand {
deleted_text: String,
position: usize,
length: usize,
}
impl Command for DeleteTextCommand {
fn execute(&mut self, app: &mut Application) -> Result<(), String> {
let start = self.position;
let end = self.position + self.length;
self.deleted_text = app.text.drain(start..end).collect();
app.cursor = self.position;
Ok(())
}
fn undo(&mut self, app: &mut Application) -> Result<(), String> {
app.text.insert_str(self.position, &self.deleted_text);
app.cursor = self.position + self.deleted_text.len();
Ok(())
}
fn description(&self) -> String {
format!("Delete {} characters", self.length)
}
}
// Command manager with undo/redo
pub struct CommandManager {
history: Vec<Box<dyn Command>>,
current: usize,
}
impl CommandManager {
pub fn new() -> Self {
Self {
history: Vec::new(),
current: 0,
}
}
pub fn execute(&mut self, mut command: Box<dyn Command>, app: &mut Application) -> Result<(), String> {
command.execute(app)?;
// Clear redo history
self.history.truncate(self.current);
self.history.push(command);
self.current += 1;
Ok(())
}
pub fn undo(&mut self, app: &mut Application) -> Result<(), String> {
if self.current == 0 {
return Err("Nothing to undo".to_string());
}
self.current -= 1;
self.history[self.current].undo(app)?;
Ok(())
}
pub fn redo(&mut self, app: &mut Application) -> Result<(), String> {
if self.current >= self.history.len() {
return Err("Nothing to redo".to_string());
}
self.history[self.current].execute(app)?;
self.current += 1;
Ok(())
}
pub fn can_undo(&self) -> bool {
self.current > 0
}
pub fn can_redo(&self) -> bool {
self.current < self.history.len()
}
pub fn get_history(&self) -> Vec<String> {
self.history
.iter()
.take(self.current)
.map(|cmd| cmd.description())
.collect()
}
}
// Tauri integration
use std::sync::Mutex;
struct AppState {
app: Mutex<Application>,
commands: Mutex<CommandManager>,
}
#[tauri::command]
fn insert_text(state: tauri::State<AppState>, text: String, position: usize) -> Result<(), String> {
let mut app = state.app.lock().unwrap();
let mut commands = state.commands.lock().unwrap();
let command = Box::new(InsertTextCommand { text, position });
commands.execute(command, &mut app)
}
#[tauri::command]
fn undo(state: tauri::State<AppState>) -> Result<(), String> {
let mut app = state.app.lock().unwrap();
let mut commands = state.commands.lock().unwrap();
commands.undo(&mut app)
}
#[tauri::command]
fn redo(state: tauri::State<AppState>) -> Result<(), String> {
let mut app = state.app.lock().unwrap();
let mut commands = state.commands.lock().unwrap();
commands.redo(&mut app)
}Event-Driven Architecture
Event Bus Pattern
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc;
// Event types
#[derive(Clone, Debug)]
pub enum AppEvent {
UserLoggedIn { user_id: u64, username: String },
FileOpened { path: String },
DataChanged { entity: String, id: u64 },
ErrorOccurred { message: String },
}
// Event handler trait
pub trait EventHandler: Send + Sync {
fn handle(&self, event: &AppEvent);
}
// Event bus
pub struct EventBus {
handlers: Arc<Mutex<HashMap<String, Vec<Arc<dyn EventHandler>>>>>,
sender: mpsc::UnboundedSender<AppEvent>,
}
impl EventBus {
pub fn new() -> Self {
let handlers = Arc::new(Mutex::new(HashMap::new()));
let handlers_clone = handlers.clone();
let (sender, mut receiver) = mpsc::unbounded_channel();
// Background task to dispatch events
tokio::spawn(async move {
while let Some(event) = receiver.recv().await {
let event_type = format!("{:?}", event).split('{').next().unwrap().trim().to_string();
let handlers = handlers_clone.lock().unwrap();
if let Some(handlers_list) = handlers.get(&event_type) {
for handler in handlers_list {
handler.handle(&event);
}
}
}
});
Self { handlers, sender }
}
pub fn subscribe(&self, event_type: &str, handler: Arc<dyn EventHandler>) {
let mut handlers = self.handlers.lock().unwrap();
handlers
.entry(event_type.to_string())
.or_insert_with(Vec::new)
.push(handler);
}
pub fn publish(&self, event: AppEvent) {
let _ = self.sender.send(event);
}
}
// Example handlers
struct LoggingHandler;
impl EventHandler for LoggingHandler {
fn handle(&self, event: &AppEvent) {
println!("[LOG] Event: {:?}", event);
}
}
struct AnalyticsHandler;
impl EventHandler for AnalyticsHandler {
fn handle(&self, event: &AppEvent) {
// Send to analytics service
println!("[ANALYTICS] Tracking: {:?}", event);
}
}
// Usage
fn setup_event_bus() -> EventBus {
let event_bus = EventBus::new();
event_bus.subscribe("UserLoggedIn", Arc::new(LoggingHandler));
event_bus.subscribe("UserLoggedIn", Arc::new(AnalyticsHandler));
event_bus
}
#[tauri::command]
fn login_user(
event_bus: tauri::State<EventBus>,
user_id: u64,
username: String,
) -> Result<(), String> {
// Perform login logic...
event_bus.publish(AppEvent::UserLoggedIn { user_id, username });
Ok(())
}Plugin System
Dynamic Plugin Architecture
use std::collections::HashMap;
use std::sync::Arc;
// Plugin trait
pub trait Plugin: Send + Sync {
fn name(&self) -> &str;
fn version(&self) -> &str;
fn initialize(&mut self, context: &PluginContext) -> Result<(), String>;
fn shutdown(&mut self) -> Result<(), String>;
fn execute(&self, command: &str, args: Vec<String>) -> Result<String, String>;
}
// Plugin context (shared resources)
pub struct PluginContext {
pub app_name: String,
pub config_dir: String,
}
// Plugin manager
pub struct PluginManager {
plugins: HashMap<String, Box<dyn Plugin>>,
context: Arc<PluginContext>,
}
impl PluginManager {
pub fn new(context: PluginContext) -> Self {
Self {
plugins: HashMap::new(),
context: Arc::new(context),
}
}
pub fn register(&mut self, mut plugin: Box<dyn Plugin>) -> Result<(), String> {
let name = plugin.name().to_string();
plugin.initialize(&self.context)?;
self.plugins.insert(name.clone(), plugin);
println!("Plugin registered: {}", name);
Ok(())
}
pub fn execute(
&self,
plugin_name: &str,
command: &str,
args: Vec<String>,
) -> Result<String, String> {
self.plugins
.get(plugin_name)
.ok_or_else(|| format!("Plugin '{}' not found", plugin_name))?
.execute(command, args)
}
pub fn list_plugins(&self) -> Vec<(String, String)> {
self.plugins
.values()
.map(|p| (p.name().to_string(), p.version().to_string()))
.collect()
}
pub fn shutdown_all(&mut self) -> Result<(), String> {
for (name, plugin) in self.plugins.iter_mut() {
plugin.shutdown().map_err(|e| {
format!("Failed to shutdown plugin '{}': {}", name, e)
})?;
}
Ok(())
}
}
// Example plugin
struct MarkdownPlugin {
enabled: bool,
}
impl Plugin for MarkdownPlugin {
fn name(&self) -> &str {
"markdown"
}
fn version(&self) -> &str {
"1.0.0"
}
fn initialize(&mut self, _context: &PluginContext) -> Result<(), String> {
self.enabled = true;
println!("Markdown plugin initialized");
Ok(())
}
fn shutdown(&mut self) -> Result<(), String> {
self.enabled = false;
println!("Markdown plugin shutdown");
Ok(())
}
fn execute(&self, command: &str, args: Vec<String>) -> Result<String, String> {
if !self.enabled {
return Err("Plugin not enabled".to_string());
}
match command {
"render" => {
if args.is_empty() {
return Err("No markdown text provided".to_string());
}
// Simplified markdown rendering
Ok(format!("<html>{}</html>", args[0]))
}
_ => Err(format!("Unknown command: {}", command)),
}
}
}
// Tauri integration
#[tauri::command]
fn execute_plugin(
manager: tauri::State<PluginManager>,
plugin: String,
command: String,
args: Vec<String>,
) -> Result<String, String> {
manager.execute(&plugin, &command, args)
}
#[tauri::command]
fn list_plugins(manager: tauri::State<PluginManager>) -> Vec<(String, String)> {
manager.list_plugins()
}Resource Management
Resource Pool Pattern
use std::sync::{Arc, Mutex};
use std::collections::VecDeque;
pub struct ResourcePool<T> {
resources: Arc<Mutex<VecDeque<T>>>,
factory: Arc<dyn Fn() -> T + Send + Sync>,
max_size: usize,
}
impl<T: Send + 'static> ResourcePool<T> {
pub fn new<F>(factory: F, max_size: usize) -> Self
where
F: Fn() -> T + Send + Sync + 'static,
{
Self {
resources: Arc::new(Mutex::new(VecDeque::new())),
factory: Arc::new(factory),
max_size,
}
}
pub fn acquire(&self) -> PooledResource<T> {
let resource = {
let mut pool = self.resources.lock().unwrap();
pool.pop_front().unwrap_or_else(|| (self.factory)())
};
PooledResource {
resource: Some(resource),
pool: self.resources.clone(),
max_size: self.max_size,
}
}
pub fn size(&self) -> usize {
self.resources.lock().unwrap().len()
}
}
pub struct PooledResource<T> {
resource: Option<T>,
pool: Arc<Mutex<VecDeque<T>>>,
max_size: usize,
}
impl<T> PooledResource<T> {
pub fn get(&self) -> &T {
self.resource.as_ref().unwrap()
}
pub fn get_mut(&mut self) -> &mut T {
self.resource.as_mut().unwrap()
}
}
impl<T> Drop for PooledResource<T> {
fn drop(&mut self) {
if let Some(resource) = self.resource.take() {
let mut pool = self.pool.lock().unwrap();
if pool.len() < self.max_size {
pool.push_back(resource);
}
}
}
}
// Example: Database connection pool
use sqlx::{SqlitePool, SqliteConnection};
pub struct DatabasePool {
pool: ResourcePool<SqliteConnection>,
}
impl DatabasePool {
pub async fn new(database_url: &str, max_size: usize) -> Self {
let url = database_url.to_string();
Self {
pool: ResourcePool::new(
move || {
// This would need to be async in real implementation
unimplemented!("Use sqlx::SqlitePool instead")
},
max_size,
),
}
}
}Error Handling Strategies
Application-Level Error Types
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Not found: {0}")]
NotFound(String),
#[error("Validation error: {0}")]
Validation(String),
#[error("Permission denied: {0}")]
PermissionDenied(String),
#[error("Internal error: {0}")]
Internal(String),
}
// Convert to Tauri-compatible error
impl From<AppError> for String {
fn from(error: AppError) -> Self {
error.to_string()
}
}
// Result type alias
pub type AppResult<T> = Result<T, AppError>;
// Usage in commands
#[tauri::command]
async fn save_data(data: String) -> Result<(), String> {
perform_save(&data)
.await
.map_err(|e: AppError| e.to_string())
}
async fn perform_save(data: &str) -> AppResult<()> {
// Validation
if data.is_empty() {
return Err(AppError::Validation("Data cannot be empty".to_string()));
}
// IO operation
std::fs::write("data.txt", data)?;
Ok(())
}Error Recovery Pattern
use std::time::Duration;
use tokio::time::sleep;
pub struct RetryPolicy {
max_attempts: u32,
delay: Duration,
exponential_backoff: bool,
}
impl RetryPolicy {
pub fn new(max_attempts: u32, delay: Duration) -> Self {
Self {
max_attempts,
delay,
exponential_backoff: false,
}
}
pub fn with_exponential_backoff(mut self) -> Self {
self.exponential_backoff = true;
self
}
pub async fn execute<F, T, E>(&self, mut operation: F) -> Result<T, E>
where
F: FnMut() -> Result<T, E>,
E: std::fmt::Display,
{
let mut attempt = 0;
let mut delay = self.delay;
loop {
attempt += 1;
match operation() {
Ok(result) => return Ok(result),
Err(error) => {
if attempt >= self.max_attempts {
println!("Operation failed after {} attempts", attempt);
return Err(error);
}
println!(
"Attempt {} failed: {}. Retrying in {:?}...",
attempt, error, delay
);
sleep(delay).await;
if self.exponential_backoff {
delay *= 2;
}
}
}
}
}
}
// Usage
async fn fetch_with_retry(url: &str) -> Result<String, String> {
let policy = RetryPolicy::new(3, Duration::from_secs(1))
.with_exponential_backoff();
policy
.execute(|| {
// Attempt operation
Ok("data".to_string())
})
.await
}These patterns provide a solid foundation for building maintainable desktop applications. Choose and combine based on application complexity and requirements.
Native GUI Frameworks
Comprehensive guide to pure Rust GUI frameworks: egui, iced, slint, and druid. When you need maximum performance, no web dependencies, or specialized UI patterns.
Framework Overview
Comparison Matrix
| Framework | Paradigm | Rendering | Best For | Maturity | Bundle Size |
|---|---|---|---|---|---|
| egui | Immediate Mode | CPU (optional GPU) | Tools, editors, games | Mature | ~3MB |
| iced | Elm Architecture | GPU (wgpu) | Cross-platform apps | Growing | ~5MB |
| slint | Declarative | GPU/CPU | Embedded, desktop | Mature | ~4MB |
| druid | Data-first | GPU (piet) | Reactive apps | Maintenance | ~4MB |
When to Use Each
egui (Immediate Mode)
- ✅ Game editors, debug tools, developer tools
- ✅ Rapid prototyping, quick iterations
- ✅ Dynamic UIs that change frequently
- ✅ Integration with game engines (Bevy, macroquad)
- ❌ Complex state management
- ❌ Strict design systems
iced (Elm Architecture)
- ✅ Cross-platform consistency
- ✅ Type-safe state management
- ✅ Predictable updates
- ✅ Custom widgets
- ❌ Steep learning curve
- ❌ Limited ecosystem
slint (Declarative UI)
- ✅ Embedded systems, IoT devices
- ✅ Designer-developer collaboration
- ✅ Declarative markup language
- ✅ Touch-first interfaces
- ❌ Larger binary size
- ❌ Proprietary markup
druid (Data-first)
- ✅ Data-driven applications
- ✅ Lens-based state management
- ✅ Widget composition
- ❌ Maintenance mode (archived)
- ❌ Limited documentation
egui - Immediate Mode GUI
Architecture
Immediate mode means UI is rebuilt every frame based on current state. No separate UI tree or state synchronization.
Core Concepts:
// Every frame:
1. Read input events
2. Run application logic
3. Build UI from scratch
4. Render outputSetup
# Cargo.toml
[dependencies]
eframe = "0.27" # egui + native windowing
egui = "0.27"
# Optional: Additional widgets
egui_extras = "0.27"
egui_plot = "0.27"Basic Application
use eframe::egui;
fn main() -> Result<(), eframe::Error> {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([800.0, 600.0]),
..Default::default()
};
eframe::run_native(
"My egui App",
options,
Box::new(|_cc| Box::new(MyApp::default())),
)
}
struct MyApp {
name: String,
age: u32,
}
impl Default for MyApp {
fn default() -> Self {
Self {
name: "Arthur".to_owned(),
age: 42,
}
}
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("My egui Application");
ui.horizontal(|ui| {
ui.label("Name:");
ui.text_edit_singleline(&mut self.name);
});
ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));
if ui.button("Click me!").clicked() {
println!("Hello, {}! You are {} years old.", self.name, self.age);
}
});
}
}Layout Patterns
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Top panel
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| {
if ui.button("Open").clicked() {
// Handle open
}
if ui.button("Save").clicked() {
// Handle save
}
});
});
});
// Side panel
egui::SidePanel::left("side_panel").show(ctx, |ui| {
ui.heading("Settings");
ui.separator();
// Settings content
});
// Bottom panel
egui::TopBottomPanel::bottom("bottom_panel").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Status: Ready");
});
});
// Central panel (fills remaining space)
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Main Content");
// Main application content
});
}
}Widgets
// Text input
ui.text_edit_singleline(&mut self.text);
ui.text_edit_multiline(&mut self.multiline_text);
// Buttons
if ui.button("Click me").clicked() { }
if ui.small_button("Small").clicked() { }
ui.add_enabled(false, egui::Button::new("Disabled"));
// Checkboxes and radio
ui.checkbox(&mut self.checked, "Check me");
ui.radio_value(&mut self.choice, Choice::A, "Option A");
ui.radio_value(&mut self.choice, Choice::B, "Option B");
// Sliders and drag values
ui.add(egui::Slider::new(&mut self.value, 0.0..=100.0));
ui.add(egui::DragValue::new(&mut self.value).speed(0.1));
// Combo box (dropdown)
egui::ComboBox::from_label("Select item")
.selected_text(format!("{:?}", self.selected))
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.selected, Item::A, "Item A");
ui.selectable_value(&mut self.selected, Item::B, "Item B");
});
// Color picker
ui.color_edit_button_rgb(&mut self.color);
// Images
ui.image(egui::include_image!("icon.png"));
// Plotting
use egui_plot::{Line, Plot, PlotPoints};
let sin: PlotPoints = (0..1000)
.map(|i| {
let x = i as f64 * 0.01;
[x, x.sin()]
})
.collect();
Plot::new("my_plot").show(ui, |plot_ui| {
plot_ui.line(Line::new(sin));
});Custom Widgets
fn custom_widget(ui: &mut egui::Ui, value: &mut f32) -> egui::Response {
let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
if ui.is_rect_visible(rect) {
let visuals = ui.style().interact(&response);
let rect = rect.expand(visuals.expansion);
let radius = 0.5 * rect.height();
ui.painter()
.rect(rect, radius, visuals.bg_fill, visuals.bg_stroke);
// Draw custom content
let text = format!("{:.1}", value);
ui.painter().text(
rect.center(),
egui::Align2::CENTER_CENTER,
text,
egui::FontId::default(),
visuals.text_color(),
);
}
response
}State Management with egui
use std::sync::{Arc, Mutex};
struct SharedState {
data: Arc<Mutex<AppData>>,
}
struct AppData {
counter: i32,
items: Vec<String>,
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let mut data = self.state.data.lock().unwrap();
egui::CentralPanel::default().show(ctx, |ui| {
ui.label(format!("Counter: {}", data.counter));
if ui.button("Increment").clicked() {
data.counter += 1;
}
for item in &data.items {
ui.label(item);
}
});
}
}iced - Elm Architecture
Architecture
Iced follows The Elm Architecture: Model (state) + Update (state changes) + View (UI).
┌─────────────┐
│ View │ ─── Displays state
└──────┬──────┘
│ User interaction
▼
┌─────────────┐
│ Message │ ─── Event/action
└──────┬──────┘
│
▼
┌─────────────┐
│ Update │ ─── Modifies state
└──────┬──────┘
│
▼
┌─────────────┐
│ Model │ ─── Application state
└─────────────┘Setup
[dependencies]
iced = "0.12"Basic Application
use iced::{
widget::{button, column, text, text_input},
Element, Sandbox, Settings,
};
pub fn main() -> iced::Result {
Counter::run(Settings::default())
}
struct Counter {
value: i32,
input: String,
}
#[derive(Debug, Clone)]
enum Message {
Increment,
Decrement,
Reset,
InputChanged(String),
}
impl Sandbox for Counter {
type Message = Message;
fn new() -> Self {
Self {
value: 0,
input: String::new(),
}
}
fn title(&self) -> String {
String::from("Counter - Iced")
}
fn update(&mut self, message: Message) {
match message {
Message::Increment => {
self.value += 1;
}
Message::Decrement => {
self.value -= 1;
}
Message::Reset => {
self.value = 0;
}
Message::InputChanged(value) => {
self.input = value;
}
}
}
fn view(&self) -> Element<Message> {
column![
button("Increment").on_press(Message::Increment),
text(self.value).size(50),
button("Decrement").on_press(Message::Decrement),
button("Reset").on_press(Message::Reset),
text_input("Type something...", &self.input)
.on_input(Message::InputChanged),
]
.padding(20)
.into()
}
}Advanced Application with Commands
use iced::{Application, Command, Element, Settings, Theme};
use iced::widget::{button, column, text};
pub fn main() -> iced::Result {
MyApp::run(Settings::default())
}
struct MyApp {
state: AppState,
data: Option<String>,
}
enum AppState {
Idle,
Loading,
Loaded,
Error(String),
}
#[derive(Debug, Clone)]
enum Message {
LoadData,
DataLoaded(Result<String, String>),
}
impl Application for MyApp {
type Executor = iced::executor::Default;
type Message = Message;
type Theme = Theme;
type Flags = ();
fn new(_flags: ()) -> (Self, Command<Message>) {
(
Self {
state: AppState::Idle,
data: None,
},
Command::none(),
)
}
fn title(&self) -> String {
String::from("Async App - Iced")
}
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::LoadData => {
self.state = AppState::Loading;
Command::perform(fetch_data(), Message::DataLoaded)
}
Message::DataLoaded(Ok(data)) => {
self.state = AppState::Loaded;
self.data = Some(data);
Command::none()
}
Message::DataLoaded(Err(error)) => {
self.state = AppState::Error(error);
Command::none()
}
}
}
fn view(&self) -> Element<Message> {
let content = match &self.state {
AppState::Idle => {
column![button("Load Data").on_press(Message::LoadData)]
}
AppState::Loading => {
column![text("Loading...")]
}
AppState::Loaded => {
column![
text(self.data.as_ref().unwrap()),
button("Reload").on_press(Message::LoadData),
]
}
AppState::Error(error) => {
column![
text(format!("Error: {}", error)),
button("Retry").on_press(Message::LoadData),
]
}
};
content.padding(20).into()
}
fn theme(&self) -> Theme {
Theme::Dark
}
}
async fn fetch_data() -> Result<String, String> {
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
Ok("Data loaded successfully!".to_string())
}Custom Widgets in iced
use iced::widget::canvas::{self, Cache, Canvas, Cursor, Frame, Geometry, Path};
use iced::{Color, Element, Length, Point, Rectangle, Size, Theme};
struct CircleWidget {
cache: Cache,
}
impl CircleWidget {
fn new() -> Self {
Self {
cache: Cache::default(),
}
}
}
impl<Message> canvas::Program<Message> for CircleWidget {
type State = ();
fn draw(
&self,
_state: &(),
renderer: &iced::Renderer,
_theme: &Theme,
bounds: Rectangle,
_cursor: Cursor,
) -> Vec<Geometry> {
let geometry = self.cache.draw(renderer, bounds.size(), |frame| {
let center = frame.center();
let radius = frame.width().min(frame.height()) / 4.0;
let circle = Path::circle(center, radius);
frame.fill(&circle, Color::from_rgb(0.0, 0.5, 1.0));
});
vec![geometry]
}
}
// Usage in view
fn view(&self) -> Element<Message> {
Canvas::new(CircleWidget::new())
.width(Length::Fill)
.height(Length::Fill)
.into()
}Styling in iced
use iced::widget::button::{self, Button};
use iced::widget::container::{self, Container};
use iced::{Background, Border, Color, Theme};
struct CustomButtonStyle;
impl button::StyleSheet for CustomButtonStyle {
type Style = Theme;
fn active(&self, _style: &Self::Style) -> button::Appearance {
button::Appearance {
background: Some(Background::Color(Color::from_rgb(0.2, 0.6, 1.0))),
text_color: Color::WHITE,
border: Border {
radius: 5.0.into(),
..Default::default()
},
..Default::default()
}
}
fn hovered(&self, style: &Self::Style) -> button::Appearance {
let active = self.active(style);
button::Appearance {
background: Some(Background::Color(Color::from_rgb(0.3, 0.7, 1.0))),
..active
}
}
}
// Usage
button("Custom Styled Button").style(CustomButtonStyle)slint - Declarative UI
Architecture
Slint uses a declarative markup language (.slint files) compiled to Rust code.
Setup
[dependencies]
slint = "1.5"
[build-dependencies]
slint-build = "1.5"build.rs:
fn main() {
slint_build::compile("ui/app.slint").unwrap();
}Basic Application
ui/app.slint:
import { Button, VerticalBox, HorizontalBox, LineEdit } from "std-widgets.slint";
export component App inherits Window {
in-out property<int> counter: 0;
in-out property<string> name: "World";
VerticalBox {
Text {
text: "Counter: \{counter}";
font-size: 24px;
}
HorizontalBox {
Button {
text: "Increment";
clicked => {
counter += 1;
}
}
Button {
text: "Decrement";
clicked => {
counter -= 1;
}
}
}
LineEdit {
placeholder-text: "Enter name";
text <=> name;
}
Text {
text: "Hello, \{name}!";
}
}
}main.rs:
slint::include_modules!();
fn main() {
let app = App::new().unwrap();
// Access properties
app.set_counter(10);
println!("Counter: {}", app.get_counter());
// Run application
app.run().unwrap();
}Advanced slint Features
Callbacks:
export component App inherits Window {
callback button-clicked(string);
Button {
text: "Click me";
clicked => {
button-clicked("Button was clicked!");
}
}
}slint::include_modules!();
fn main() {
let app = App::new().unwrap();
app.on_button_clicked(|msg| {
println!("Callback: {}", msg);
});
app.run().unwrap();
}Custom Structs:
export struct Person {
name: string,
age: int,
}
export component App inherits Window {
in-out property<Person> user: { name: "Alice", age: 30 };
Text {
text: "\{user.name} is \{user.age} years old";
}
}use slint::Model;
slint::include_modules!();
fn main() {
let app = App::new().unwrap();
let person = Person {
name: "Bob".into(),
age: 25,
};
app.set_user(person);
app.run().unwrap();
}Lists and Models:
export component App inherits Window {
in-out property<[string]> items: ["Item 1", "Item 2", "Item 3"];
VerticalBox {
for item in items: Text {
text: item;
}
}
}use slint::{Model, ModelRc, VecModel};
slint::include_modules!();
fn main() {
let app = App::new().unwrap();
let model = Rc::new(VecModel::from(vec![
"Dynamic Item 1".into(),
"Dynamic Item 2".into(),
]));
app.set_items(ModelRc::from(model.clone()));
app.run().unwrap();
}druid (Archived - Reference Only)
Druid is in maintenance mode but offers valuable patterns for data-driven UIs.
Core Concepts
Lens Pattern:
use druid::widget::{Button, Flex, Label, TextBox};
use druid::{AppLauncher, Data, Lens, Widget, WindowDesc};
#[derive(Clone, Data, Lens)]
struct AppState {
name: String,
count: u32,
}
fn build_ui() -> impl Widget<AppState> {
Flex::column()
.with_child(Label::new(|data: &AppState, _env: &_| {
format!("Hello, {}!", data.name)
}))
.with_child(TextBox::new().lens(AppState::name))
.with_child(Label::new(|data: &AppState, _env: &_| {
format!("Count: {}", data.count)
}))
.with_child(Button::new("Increment").on_click(|_ctx, data: &mut AppState, _env| {
data.count += 1;
}))
}
fn main() {
let main_window = WindowDesc::new(build_ui())
.title("Druid App")
.window_size((400.0, 300.0));
let initial_state = AppState {
name: "World".to_string(),
count: 0,
};
AppLauncher::with_window(main_window)
.launch(initial_state)
.expect("Failed to launch application");
}Framework Selection Guide
Decision Tree
Choose GUI framework based on:
Project Type:
├─ Game editor, debug tools → egui
├─ Cross-platform app with complex state → iced
├─ Embedded device, touch interface → slint
└─ Data-driven (consider alternatives) → druid/iced
Development Speed:
├─ Rapid prototyping → egui
├─ Type-safe architecture → iced
└─ Designer collaboration → slint
Performance Needs:
├─ 60+ FPS immediate updates → egui
├─ GPU-accelerated rendering → iced/slint
└─ Minimal CPU usage → slint
Team Experience:
├─ Immediate mode GUI → egui
├─ Elm/functional programming → iced
└─ QML/declarative UI → slintMigration Paths
From web to native:
- Tauri → egui: Extract backend, rebuild UI
- React → iced: Messages ≈ Actions, State ≈ Model
Between native frameworks:
- egui → iced: Refactor to Elm architecture
- iced → slint: Extract logic, rebuild in .slint
Production Tips
Performance
egui:
- Use
ui.ctx().request_repaint()sparingly - Cache expensive computations
- Profile with
puffinprofiler
iced:
- Minimize
Commandusage - Use
subscriptionfor continuous updates - Batch state updates
slint:
- Use
propertybindings efficiently - Optimize model updates
- Profile with built-in tools
Distribution
All frameworks support:
- Static binaries (3-10MB)
- Cross-compilation
- Native installers (MSI, DMG, DEB)
Testing
egui:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_app_logic() {
let mut app = MyApp::default();
// Test state changes
assert_eq!(app.counter, 0);
}
}iced:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_update() {
let mut app = Counter::new();
app.update(Message::Increment);
assert_eq!(app.value, 1);
}
}This guide covers production-ready patterns for all major Rust GUI frameworks. Choose based on project needs, team skills, and performance requirements.
Platform Integration
Comprehensive guide to integrating with native platform features across Windows, macOS, and Linux in Rust desktop applications.
File System Access
File Dialogs
use tauri::api::dialog::{FileDialogBuilder, MessageDialogBuilder, MessageDialogKind};
#[tauri::command]
async fn open_file_dialog() -> Result<Option<String>, String> {
let path = FileDialogBuilder::new()
.add_filter("Text Files", &["txt", "md"])
.add_filter("All Files", &["*"])
.set_title("Select a file")
.pick_file();
Ok(path.map(|p| p.to_string_lossy().to_string()))
}
#[tauri::command]
async fn open_folder_dialog() -> Result<Option<String>, String> {
let path = FileDialogBuilder::new()
.set_title("Select a folder")
.pick_folder();
Ok(path.map(|p| p.to_string_lossy().to_string()))
}
#[tauri::command]
async fn save_file_dialog() -> Result<Option<String>, String> {
let path = FileDialogBuilder::new()
.add_filter("JSON Files", &["json"])
.set_file_name("untitled.json")
.save_file();
Ok(path.map(|p| p.to_string_lossy().to_string()))
}
#[tauri::command]
async fn show_message(title: String, message: String) -> Result<(), String> {
MessageDialogBuilder::new(title, message)
.kind(MessageDialogKind::Info)
.show();
Ok(())
}
#[tauri::command]
async fn confirm_dialog(title: String, message: String) -> Result<bool, String> {
let confirmed = MessageDialogBuilder::new(title, message)
.kind(MessageDialogKind::Warning)
.buttons(tauri::api::dialog::MessageDialogButtons::OkCancel)
.show();
Ok(confirmed)
}Safe File System Operations
use std::path::{Path, PathBuf};
use std::fs;
// Validate file paths to prevent directory traversal
fn validate_path(path: &str, base_dir: &Path) -> Result<PathBuf, String> {
let path = Path::new(path);
// Canonicalize to resolve .. and symlinks
let canonical = path
.canonicalize()
.map_err(|_| "Invalid path".to_string())?;
// Ensure path is within base directory
if !canonical.starts_with(base_dir) {
return Err("Path outside allowed directory".to_string());
}
Ok(canonical)
}
#[tauri::command]
async fn read_file_safe(app: tauri::AppHandle, relative_path: String) -> Result<String, String> {
let app_dir = app
.path()
.app_data_dir()
.map_err(|e| e.to_string())?;
let file_path = validate_path(&relative_path, &app_dir)?;
fs::read_to_string(file_path).map_err(|e| e.to_string())
}
#[tauri::command]
async fn write_file_safe(
app: tauri::AppHandle,
relative_path: String,
content: String,
) -> Result<(), String> {
let app_dir = app
.path()
.app_data_dir()
.map_err(|e| e.to_string())?;
// Ensure directory exists
fs::create_dir_all(&app_dir).map_err(|e| e.to_string())?;
let file_path = app_dir.join(&relative_path);
// Security check
let canonical = file_path
.canonicalize()
.or_else(|_| {
// File doesn't exist yet, validate parent
file_path
.parent()
.ok_or("Invalid path")?
.canonicalize()
.map(|p| p.join(file_path.file_name().unwrap()))
})
.map_err(|_| "Invalid path".to_string())?;
if !canonical.starts_with(&app_dir) {
return Err("Path outside allowed directory".to_string());
}
fs::write(canonical, content).map_err(|e| e.to_string())
}File Watching
use notify::{Watcher, RecursiveMode, Event};
use std::sync::mpsc::channel;
use std::time::Duration;
struct FileWatcher {
watcher: notify::RecommendedWatcher,
}
impl FileWatcher {
fn new(app_handle: tauri::AppHandle) -> Result<Self, String> {
let (tx, rx) = channel();
let mut watcher = notify::recommended_watcher(tx)
.map_err(|e| e.to_string())?;
// Spawn task to handle events
tokio::spawn(async move {
while let Ok(event) = rx.recv() {
if let Ok(Event { kind, paths, .. }) = event {
let _ = app_handle.emit("file-changed", FileChangeEvent {
kind: format!("{:?}", kind),
paths: paths.iter().map(|p| p.to_string_lossy().to_string()).collect(),
});
}
}
});
Ok(Self { watcher })
}
fn watch(&mut self, path: &str) -> Result<(), String> {
self.watcher
.watch(Path::new(path), RecursiveMode::Recursive)
.map_err(|e| e.to_string())
}
fn unwatch(&mut self, path: &str) -> Result<(), String> {
self.watcher
.unwatch(Path::new(path))
.map_err(|e| e.to_string())
}
}
#[derive(Clone, serde::Serialize)]
struct FileChangeEvent {
kind: String,
paths: Vec<String>,
}
#[tauri::command]
fn watch_directory(
watcher: tauri::State<FileWatcher>,
path: String,
) -> Result<(), String> {
watcher.inner().lock().unwrap().watch(&path)
}System Tray Integration
Cross-Platform System Tray
use tauri::{
menu::{Menu, MenuItem, Submenu},
tray::{TrayIconBuilder, TrayIconEvent},
Manager, Runtime,
};
fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> Result<(), Box<dyn std::error::Error>> {
// Create menu items
let show_item = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?;
let hide_item = MenuItem::with_id(app, "hide", "Hide Window", true, None::<&str>)?;
let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
// Create submenu
let settings_menu = Submenu::with_items(
app,
"Settings",
true,
&[
&MenuItem::with_id(app, "preferences", "Preferences", true, None::<&str>)?,
&MenuItem::with_id(app, "about", "About", true, None::<&str>)?,
],
)?;
// Create menu
let menu = Menu::with_items(app, &[&show_item, &hide_item, &settings_menu, &quit_item])?;
// Build tray icon
let _tray = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.tooltip("My Application")
.on_menu_event(|app, event| match event.id.as_ref() {
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
"hide" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
}
}
"quit" => {
app.exit(0);
}
"preferences" => {
// Open preferences window
println!("Open preferences");
}
"about" => {
// Show about dialog
println!("Show about dialog");
}
_ => {}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click { .. } = event {
// Handle tray icon click
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
if window.is_visible().unwrap_or(false) {
let _ = window.hide();
} else {
let _ = window.show();
let _ = window.set_focus();
}
}
}
})
.build(app)?;
Ok(())
}
fn main() {
tauri::Builder::default()
.setup(|app| {
create_tray(app.handle())?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Dynamic Tray Menu Updates
use std::sync::Mutex;
struct TrayState {
is_recording: Mutex<bool>,
}
#[tauri::command]
fn toggle_recording(
app: tauri::AppHandle,
state: tauri::State<TrayState>,
) -> Result<(), String> {
let mut is_recording = state.is_recording.lock().unwrap();
*is_recording = !*is_recording;
// Update tray menu
let tray = app.tray_by_id("main").ok_or("Tray not found")?;
let menu_item = tray
.get_item("toggle_recording")
.ok_or("Menu item not found")?;
menu_item
.set_text(if *is_recording {
"Stop Recording"
} else {
"Start Recording"
})
.map_err(|e| e.to_string())?;
Ok(())
}Native Notifications
Cross-Platform Notifications
use tauri::Notification;
#[tauri::command]
fn send_notification(
app: tauri::AppHandle,
title: String,
body: String,
) -> Result<(), String> {
Notification::new(&app.config().identifier)
.title(title)
.body(body)
.icon("icon")
.show()
.map_err(|e| e.to_string())
}
#[tauri::command]
fn send_notification_with_action(
app: tauri::AppHandle,
title: String,
body: String,
) -> Result<(), String> {
// Note: Actions are platform-dependent
#[cfg(target_os = "macos")]
{
Notification::new(&app.config().identifier)
.title(title)
.body(body)
.sound("default")
.show()
.map_err(|e| e.to_string())
}
#[cfg(not(target_os = "macos"))]
{
Notification::new(&app.config().identifier)
.title(title)
.body(body)
.show()
.map_err(|e| e.to_string())
}
}Notification with User Interaction
use tauri::{Emitter, Manager};
#[tauri::command]
async fn send_interactive_notification(
app: tauri::AppHandle,
title: String,
body: String,
) -> Result<(), String> {
// Send notification
Notification::new(&app.config().identifier)
.title(&title)
.body(&body)
.show()
.map_err(|e| e.to_string())?;
// Listen for notification clicks (platform-dependent)
// This is a simplified example; real implementation needs platform-specific code
Ok(())
}Auto-Updates
Tauri Updater Integration
use tauri_plugin_updater::UpdaterExt;
#[tauri::command]
async fn check_for_updates(app: tauri::AppHandle) -> Result<Option<String>, String> {
let update = app
.updater()
.check()
.await
.map_err(|e| e.to_string())?;
if let Some(update) = update {
Ok(Some(format!(
"Update available: {} (current: {})",
update.version,
update.current_version
)))
} else {
Ok(None)
}
}
#[tauri::command]
async fn install_update(app: tauri::AppHandle) -> Result<(), String> {
let update = app
.updater()
.check()
.await
.map_err(|e| e.to_string())?;
if let Some(update) = update {
// Download and install
update
.download_and_install(
|chunk_length, content_length| {
println!(
"Downloaded {} of {:?}",
chunk_length,
content_length
);
},
|| {
println!("Download finished");
},
)
.await
.map_err(|e| e.to_string())?;
// Restart app
app.restart();
}
Ok(())
}
// Setup auto-update check
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build())
.setup(|app| {
let handle = app.handle().clone();
// Check for updates on startup
tauri::async_runtime::spawn(async move {
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
if let Ok(Some(update)) = handle.updater().check().await {
println!("Update available: {}", update.version);
// Emit event to frontend
let _ = handle.emit("update-available", &update.version);
}
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Deep Linking / Custom URL Schemes
Register URL Scheme
tauri.conf.json:
{
"bundle": {
"macOS": {
"associatedDomains": ["myapp://"],
"category": "public.app-category.developer-tools"
},
"windows": {
"webviewInstallMode": {
"type": "downloadBootstrapper"
},
"protocols": [
{
"name": "myapp",
"schemes": ["myapp"]
}
]
}
}
}Handle Deep Links
use tauri::{Emitter, Manager};
fn main() {
tauri::Builder::default()
.setup(|app| {
// Register URL handler
app.listen_any("deep-link://", |event| {
println!("Received deep link: {:?}", event.payload());
});
Ok(())
})
.plugin(tauri_plugin_deep_link::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
#[tauri::command]
fn handle_url(app: tauri::AppHandle, url: String) -> Result<(), String> {
println!("Handling URL: {}", url);
// Parse URL and navigate
if url.starts_with("myapp://open/") {
let file = url.strip_prefix("myapp://open/").unwrap();
app.emit("open-file", file).map_err(|e| e.to_string())?;
}
Ok(())
}Platform-Specific Features
Windows
#[cfg(target_os = "windows")]
mod windows {
use winapi::um::winuser::{MessageBoxW, MB_OK};
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
pub fn show_native_message_box(title: &str, message: &str) {
let title_wide: Vec<u16> = OsStr::new(title)
.encode_wide()
.chain(std::iter::once(0))
.collect();
let message_wide: Vec<u16> = OsStr::new(message)
.encode_wide()
.chain(std::iter::once(0))
.collect();
unsafe {
MessageBoxW(
std::ptr::null_mut(),
message_wide.as_ptr(),
title_wide.as_ptr(),
MB_OK,
);
}
}
// Windows Registry access
use winreg::enums::*;
use winreg::RegKey;
pub fn read_registry_value(key_path: &str, value_name: &str) -> Option<String> {
let hklm = RegKey::predef(HKEY_CURRENT_USER);
let key = hklm.open_subkey(key_path).ok()?;
key.get_value(value_name).ok()
}
pub fn write_registry_value(
key_path: &str,
value_name: &str,
value: &str,
) -> Result<(), std::io::Error> {
let hklm = RegKey::predef(HKEY_CURRENT_USER);
let (key, _) = hklm.create_subkey(key_path)?;
key.set_value(value_name, &value)?;
Ok(())
}
}
#[tauri::command]
#[cfg(target_os = "windows")]
fn windows_specific_feature() -> Result<String, String> {
windows::show_native_message_box("Title", "Message");
let value = windows::read_registry_value(
"Software\\MyApp",
"Setting1",
)
.unwrap_or_default();
Ok(value)
}macOS
#[cfg(target_os = "macos")]
mod macos {
use cocoa::base::nil;
use cocoa::foundation::NSString;
use objc::{class, msg_send, sel, sel_impl};
pub fn set_dock_badge(label: &str) {
unsafe {
let app = cocoa::appkit::NSApp();
let dock_tile: cocoa::base::id = msg_send![app, dockTile];
let badge_label = NSString::alloc(nil).init_str(label);
let _: () = msg_send![dock_tile, setBadgeLabel: badge_label];
}
}
pub fn clear_dock_badge() {
unsafe {
let app = cocoa::appkit::NSApp();
let dock_tile: cocoa::base::id = msg_send![app, dockTile];
let _: () = msg_send![dock_tile, setBadgeLabel: nil];
}
}
// Access macOS services
use std::process::Command;
pub fn trigger_notification_center(title: &str, message: &str) {
let script = format!(
r#"display notification "{}" with title "{}""#,
message, title
);
Command::new("osascript")
.arg("-e")
.arg(script)
.output()
.ok();
}
}
#[tauri::command]
#[cfg(target_os = "macos")]
fn macos_specific_feature(badge: String) -> Result<(), String> {
macos::set_dock_badge(&badge);
Ok(())
}
#[tauri::command]
#[cfg(target_os = "macos")]
fn clear_badge() -> Result<(), String> {
macos::clear_dock_badge();
Ok(())
}Linux
#[cfg(target_os = "linux")]
mod linux {
use std::process::Command;
pub fn send_desktop_notification(title: &str, message: &str) -> Result<(), String> {
Command::new("notify-send")
.arg(title)
.arg(message)
.output()
.map_err(|e| e.to_string())?;
Ok(())
}
// D-Bus integration
use dbus::blocking::Connection;
use std::time::Duration;
pub fn get_desktop_environment() -> Result<String, Box<dyn std::error::Error>> {
let conn = Connection::new_session()?;
let proxy = conn.with_proxy(
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
Duration::from_millis(5000),
);
// Query desktop environment
// This is a simplified example
Ok("Unknown".to_string())
}
}
#[tauri::command]
#[cfg(target_os = "linux")]
fn linux_specific_feature(title: String, message: String) -> Result<(), String> {
linux::send_desktop_notification(&title, &message)
}Permissions and Security
Scope Configuration
use tauri::Manager;
fn main() {
tauri::Builder::default()
.setup(|app| {
// Configure file system scope
let scope = app.fs_scope();
// Allow access to specific directories
let app_data_dir = app.path().app_data_dir()?;
scope.allow_directory(&app_data_dir, true)?;
let documents_dir = app.path().document_dir()?;
scope.allow_directory(&documents_dir, false)?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Runtime Permission Checks
use tauri::Manager;
#[tauri::command]
fn read_file_with_permission(
app: tauri::AppHandle,
path: String,
) -> Result<String, String> {
let scope = app.fs_scope();
// Check if path is allowed
if !scope.is_allowed(&path) {
return Err("Access denied: path not in scope".to_string());
}
std::fs::read_to_string(&path).map_err(|e| e.to_string())
}
#[tauri::command]
fn request_file_access(
app: tauri::AppHandle,
path: String,
) -> Result<(), String> {
let scope = app.fs_scope();
// Request access (user must approve via dialog)
scope
.allow_file(&path)
.map_err(|e| e.to_string())?;
Ok(())
}These platform integration patterns enable full access to native OS features while maintaining cross-platform compatibility and security.
State Management
Comprehensive guide to managing application state in Rust desktop applications, from simple local state to complex async operations and multi-window synchronization.
State Management Strategies
Local State (Single Component)
Simplest form - state lives within a single component or module.
// egui example
struct MyApp {
counter: i32,
text: String,
selected: Option<usize>,
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.label(format!("Counter: {}", self.counter));
if ui.button("Increment").clicked() {
self.counter += 1;
}
ui.text_edit_singleline(&mut self.text);
});
}
}Tauri example:
use std::sync::Mutex;
struct AppState {
counter: Mutex<i32>,
}
#[tauri::command]
fn increment(state: tauri::State<AppState>) -> i32 {
let mut counter = state.counter.lock().unwrap();
*counter += 1;
*counter
}
#[tauri::command]
fn get_counter(state: tauri::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_counter])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Shared State with Arc<Mutex<T>>
Thread-safe shared state for multi-threaded applications.
use std::sync::{Arc, Mutex};
#[derive(Clone)]
struct SharedState {
data: Arc<Mutex<AppData>>,
}
struct AppData {
users: Vec<User>,
settings: Settings,
}
impl SharedState {
fn new() -> Self {
Self {
data: Arc::new(Mutex::new(AppData {
users: Vec::new(),
settings: Settings::default(),
})),
}
}
fn add_user(&self, user: User) {
let mut data = self.data.lock().unwrap();
data.users.push(user);
}
fn get_users(&self) -> Vec<User> {
let data = self.data.lock().unwrap();
data.users.clone()
}
}
// Tauri commands
#[tauri::command]
fn add_user(state: tauri::State<SharedState>, name: String, email: String) {
let user = User {
id: generate_id(),
name,
email,
};
state.add_user(user);
}
#[tauri::command]
fn get_users(state: tauri::State<SharedState>) -> Vec<User> {
state.get_users()
}RwLock for Read-Heavy Workloads
Better performance when reads outnumber writes.
use std::sync::{Arc, RwLock};
struct AppState {
cache: Arc<RwLock<HashMap<String, String>>>,
}
impl AppState {
fn new() -> Self {
Self {
cache: Arc::new(RwLock::new(HashMap::new())),
}
}
// Multiple readers can access simultaneously
fn get(&self, key: &str) -> Option<String> {
let cache = self.cache.read().unwrap();
cache.get(key).cloned()
}
// Exclusive write access
fn set(&self, key: String, value: String) {
let mut cache = self.cache.write().unwrap();
cache.insert(key, value);
}
// Bulk read operation
fn get_all(&self) -> HashMap<String, String> {
let cache = self.cache.read().unwrap();
cache.clone()
}
}
#[tauri::command]
fn cache_get(state: tauri::State<AppState>, key: String) -> Option<String> {
state.get(&key)
}
#[tauri::command]
fn cache_set(state: tauri::State<AppState>, key: String, value: String) {
state.set(key, value);
}Async Runtime Integration
Tokio Integration with Tauri
use tokio::sync::RwLock as TokioRwLock;
use std::sync::Arc;
struct AsyncState {
data: Arc<TokioRwLock<AppData>>,
}
#[derive(Clone)]
struct AppData {
items: Vec<Item>,
loading: bool,
}
impl AsyncState {
fn new() -> Self {
Self {
data: Arc::new(TokioRwLock::new(AppData {
items: Vec::new(),
loading: false,
})),
}
}
async fn fetch_items(&self) -> Result<Vec<Item>, String> {
// Set loading state
{
let mut data = self.data.write().await;
data.loading = true;
}
// Perform async operation
let items = fetch_from_api().await.map_err(|e| e.to_string())?;
// Update state
{
let mut data = self.data.write().await;
data.items = items.clone();
data.loading = false;
}
Ok(items)
}
async fn get_items(&self) -> Vec<Item> {
let data = self.data.read().await;
data.items.clone()
}
async fn is_loading(&self) -> bool {
let data = self.data.read().await;
data.loading
}
}
#[tauri::command]
async fn fetch_items(state: tauri::State<'_, AsyncState>) -> Result<Vec<Item>, String> {
state.fetch_items().await
}
#[tauri::command]
async fn get_items(state: tauri::State<'_, AsyncState>) -> Vec<Item> {
state.get_items().await
}
async fn fetch_from_api() -> Result<Vec<Item>, Box<dyn std::error::Error>> {
use reqwest;
let response = reqwest::get("https://api.example.com/items")
.await?
.json::<Vec<Item>>()
.await?;
Ok(response)
}Background Tasks and Channels
use tokio::sync::mpsc;
use tokio::time::{interval, Duration};
struct BackgroundWorker {
tx: mpsc::UnboundedSender<WorkerMessage>,
}
enum WorkerMessage {
ProcessData(String),
Stop,
}
impl BackgroundWorker {
fn new(app_handle: tauri::AppHandle) -> Self {
let (tx, mut rx) = mpsc::unbounded_channel();
tokio::spawn(async move {
let mut ticker = interval(Duration::from_secs(1));
loop {
tokio::select! {
_ = ticker.tick() => {
// Periodic task
let _ = app_handle.emit("tick", "Periodic update");
}
Some(msg) = rx.recv() => {
match msg {
WorkerMessage::ProcessData(data) => {
// Process data
println!("Processing: {}", data);
let _ = app_handle.emit("data-processed", data);
}
WorkerMessage::Stop => {
println!("Stopping worker");
break;
}
}
}
}
}
});
Self { tx }
}
fn send(&self, msg: WorkerMessage) {
let _ = self.tx.send(msg);
}
}
#[tauri::command]
fn process_data(worker: tauri::State<BackgroundWorker>, data: String) {
worker.send(WorkerMessage::ProcessData(data));
}
fn main() {
tauri::Builder::default()
.setup(|app| {
let worker = BackgroundWorker::new(app.handle());
app.manage(worker);
Ok(())
})
.invoke_handler(tauri::generate_handler![process_data])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Message Passing Patterns
Command-Query Separation
use tokio::sync::mpsc;
// Commands (modify state)
enum Command {
AddUser { name: String, email: String },
RemoveUser { id: u64 },
UpdateSettings { key: String, value: String },
}
// Queries (read state)
enum Query {
GetUser { id: u64, response: oneshot::Sender<Option<User>> },
GetAllUsers { response: oneshot::Sender<Vec<User>> },
GetSettings { response: oneshot::Sender<Settings> },
}
struct StateManager {
command_tx: mpsc::UnboundedSender<Command>,
query_tx: mpsc::UnboundedSender<Query>,
}
impl StateManager {
fn new() -> Self {
let (command_tx, mut command_rx) = mpsc::unbounded_channel();
let (query_tx, mut query_rx) = mpsc::unbounded_channel();
// State lives in this task
tokio::spawn(async move {
let mut state = AppState::new();
loop {
tokio::select! {
Some(cmd) = command_rx.recv() => {
match cmd {
Command::AddUser { name, email } => {
state.add_user(User { id: generate_id(), name, email });
}
Command::RemoveUser { id } => {
state.remove_user(id);
}
Command::UpdateSettings { key, value } => {
state.update_setting(key, value);
}
}
}
Some(query) = query_rx.recv() => {
match query {
Query::GetUser { id, response } => {
let _ = response.send(state.get_user(id));
}
Query::GetAllUsers { response } => {
let _ = response.send(state.get_all_users());
}
Query::GetSettings { response } => {
let _ = response.send(state.get_settings());
}
}
}
}
}
});
Self { command_tx, query_tx }
}
fn send_command(&self, cmd: Command) {
let _ = self.command_tx.send(cmd);
}
async fn query_user(&self, id: u64) -> Option<User> {
let (tx, rx) = oneshot::channel();
let _ = self.query_tx.send(Query::GetUser { id, response: tx });
rx.await.unwrap()
}
async fn query_all_users(&self) -> Vec<User> {
let (tx, rx) = oneshot::channel();
let _ = self.query_tx.send(Query::GetAllUsers { response: tx });
rx.await.unwrap()
}
}
// Tauri commands
#[tauri::command]
fn add_user(manager: tauri::State<StateManager>, name: String, email: String) {
manager.send_command(Command::AddUser { name, email });
}
#[tauri::command]
async fn get_user(manager: tauri::State<'_, StateManager>, id: u64) -> Option<User> {
manager.query_user(id).await
}Actor Pattern
use tokio::sync::mpsc;
trait Actor {
type Message;
fn handle(&mut self, msg: Self::Message);
}
struct ActorHandle<M> {
tx: mpsc::UnboundedSender<M>,
}
impl<M: Send + 'static> ActorHandle<M> {
fn new<A>(mut actor: A) -> Self
where
A: Actor<Message = M> + Send + 'static,
{
let (tx, mut rx) = mpsc::unbounded_channel();
tokio::spawn(async move {
while let Some(msg) = rx.recv().await {
actor.handle(msg);
}
});
Self { tx }
}
fn send(&self, msg: M) {
let _ = self.tx.send(msg);
}
}
// Example actor
struct UserActor {
users: HashMap<u64, User>,
}
enum UserMessage {
Add(User),
Remove(u64),
Get { id: u64, response: oneshot::Sender<Option<User>> },
}
impl Actor for UserActor {
type Message = UserMessage;
fn handle(&mut self, msg: Self::Message) {
match msg {
UserMessage::Add(user) => {
self.users.insert(user.id, user);
}
UserMessage::Remove(id) => {
self.users.remove(&id);
}
UserMessage::Get { id, response } => {
let user = self.users.get(&id).cloned();
let _ = response.send(user);
}
}
}
}
// Usage
fn setup_actors() -> ActorHandle<UserMessage> {
let actor = UserActor {
users: HashMap::new(),
};
ActorHandle::new(actor)
}Reactive State Patterns
Observable State with Signals
use std::sync::{Arc, Mutex};
use std::collections::HashMap;
type Listener<T> = Box<dyn Fn(&T) + Send + Sync>;
struct Signal<T: Clone> {
value: Arc<Mutex<T>>,
listeners: Arc<Mutex<Vec<Listener<T>>>>,
}
impl<T: Clone + Send + Sync + 'static> Signal<T> {
fn new(initial: T) -> Self {
Self {
value: Arc::new(Mutex::new(initial)),
listeners: Arc::new(Mutex::new(Vec::new())),
}
}
fn get(&self) -> T {
self.value.lock().unwrap().clone()
}
fn set(&self, new_value: T) {
{
let mut value = self.value.lock().unwrap();
*value = new_value.clone();
}
// Notify listeners
let listeners = self.listeners.lock().unwrap();
for listener in listeners.iter() {
listener(&new_value);
}
}
fn update<F>(&self, f: F)
where
F: FnOnce(&mut T),
{
let new_value = {
let mut value = self.value.lock().unwrap();
f(&mut value);
value.clone()
};
// Notify listeners
let listeners = self.listeners.lock().unwrap();
for listener in listeners.iter() {
listener(&new_value);
}
}
fn subscribe<F>(&self, listener: F)
where
F: Fn(&T) + Send + Sync + 'static,
{
let mut listeners = self.listeners.lock().unwrap();
listeners.push(Box::new(listener));
}
}
// Example usage
struct AppState {
counter: Signal<i32>,
username: Signal<String>,
}
impl AppState {
fn new() -> Self {
Self {
counter: Signal::new(0),
username: Signal::new(String::from("Guest")),
}
}
}
fn setup_state(app_handle: tauri::AppHandle) -> AppState {
let state = AppState::new();
// Subscribe to changes
let handle = app_handle.clone();
state.counter.subscribe(move |value| {
let _ = handle.emit("counter-changed", value);
});
let handle = app_handle.clone();
state.username.subscribe(move |value| {
let _ = handle.emit("username-changed", value);
});
state
}
#[tauri::command]
fn increment_counter(state: tauri::State<AppState>) {
state.counter.update(|c| *c += 1);
}
#[tauri::command]
fn set_username(state: tauri::State<AppState>, name: String) {
state.username.set(name);
}
#[tauri::command]
fn get_counter(state: tauri::State<AppState>) -> i32 {
state.counter.get()
}Computed Values
struct Computed<T, F>
where
T: Clone,
F: Fn() -> T,
{
compute: F,
cached: Arc<Mutex<Option<T>>>,
}
impl<T: Clone, F: Fn() -> T> Computed<T, F> {
fn new(compute: F) -> Self {
Self {
compute,
cached: Arc::new(Mutex::new(None)),
}
}
fn get(&self) -> T {
let mut cached = self.cached.lock().unwrap();
if let Some(value) = cached.as_ref() {
value.clone()
} else {
let value = (self.compute)();
*cached = Some(value.clone());
value
}
}
fn invalidate(&self) {
let mut cached = self.cached.lock().unwrap();
*cached = None;
}
}
// Example
struct TodoState {
todos: Signal<Vec<Todo>>,
completed_count: Computed<usize, Box<dyn Fn() -> usize + Send + Sync>>,
}
impl TodoState {
fn new() -> Self {
let todos = Signal::new(Vec::new());
let todos_clone = todos.clone();
let completed_count = Computed::new(Box::new(move || {
todos_clone
.get()
.iter()
.filter(|t| t.completed)
.count()
}));
Self {
todos,
completed_count,
}
}
fn add_todo(&self, todo: Todo) {
self.todos.update(|todos| todos.push(todo));
self.completed_count.invalidate();
}
fn toggle_todo(&self, id: u64) {
self.todos.update(|todos| {
if let Some(todo) = todos.iter_mut().find(|t| t.id == id) {
todo.completed = !todo.completed;
}
});
self.completed_count.invalidate();
}
fn get_completed_count(&self) -> usize {
self.completed_count.get()
}
}Persistence
File-Based Persistence
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Serialize, Deserialize, Clone)]
struct AppSettings {
theme: String,
language: String,
window_size: (u32, u32),
}
struct PersistedState {
settings: Signal<AppSettings>,
config_path: PathBuf,
}
impl PersistedState {
fn new(config_path: PathBuf) -> Self {
let settings = Self::load_settings(&config_path)
.unwrap_or_else(|_| AppSettings::default());
let state = Self {
settings: Signal::new(settings),
config_path,
};
// Auto-save on changes
let config_path = state.config_path.clone();
state.settings.subscribe(move |settings| {
let _ = Self::save_settings(&config_path, settings);
});
state
}
fn load_settings(path: &PathBuf) -> Result<AppSettings, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(path)?;
let settings = serde_json::from_str(&content)?;
Ok(settings)
}
fn save_settings(path: &PathBuf, settings: &AppSettings) -> Result<(), Box<dyn std::error::Error>> {
let content = serde_json::to_string_pretty(settings)?;
std::fs::write(path, content)?;
Ok(())
}
fn update_settings<F>(&self, f: F)
where
F: FnOnce(&mut AppSettings),
{
self.settings.update(f);
}
}
#[tauri::command]
fn update_theme(state: tauri::State<PersistedState>, theme: String) {
state.update_settings(|s| s.theme = theme);
}
#[tauri::command]
fn get_settings(state: tauri::State<PersistedState>) -> AppSettings {
state.settings.get()
}Database Integration with sqlx
use sqlx::{SqlitePool, FromRow};
#[derive(FromRow, Serialize, Clone)]
struct Note {
id: i64,
title: String,
content: String,
created_at: String,
}
struct DatabaseState {
pool: SqlitePool,
}
impl DatabaseState {
async fn new(database_url: &str) -> Result<Self, sqlx::Error> {
let pool = SqlitePool::connect(database_url).await?;
// Run migrations
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
"#,
)
.execute(&pool)
.await?;
Ok(Self { pool })
}
async fn create_note(&self, title: String, content: String) -> Result<Note, sqlx::Error> {
let note = sqlx::query_as::<_, Note>(
"INSERT INTO notes (title, content) VALUES (?, ?) RETURNING *",
)
.bind(title)
.bind(content)
.fetch_one(&self.pool)
.await?;
Ok(note)
}
async fn get_all_notes(&self) -> Result<Vec<Note>, sqlx::Error> {
sqlx::query_as::<_, Note>("SELECT * FROM notes ORDER BY created_at DESC")
.fetch_all(&self.pool)
.await
}
async fn update_note(&self, id: i64, title: String, content: String) -> Result<(), sqlx::Error> {
sqlx::query("UPDATE notes SET title = ?, content = ? WHERE id = ?")
.bind(title)
.bind(content)
.bind(id)
.execute(&self.pool)
.await?;
Ok(())
}
async fn delete_note(&self, id: i64) -> Result<(), sqlx::Error> {
sqlx::query("DELETE FROM notes WHERE id = ?")
.bind(id)
.execute(&self.pool)
.await?;
Ok(())
}
}
#[tauri::command]
async fn create_note(
state: tauri::State<'_, DatabaseState>,
title: String,
content: String,
) -> Result<Note, String> {
state
.create_note(title, content)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn get_all_notes(state: tauri::State<'_, DatabaseState>) -> Result<Vec<Note>, String> {
state.get_all_notes().await.map_err(|e| e.to_string())
}Multi-Window State Sharing
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone)]
struct SharedAppState {
data: Arc<RwLock<GlobalData>>,
}
struct GlobalData {
current_user: Option<User>,
notifications: Vec<Notification>,
}
impl SharedAppState {
fn new() -> Self {
Self {
data: Arc::new(RwLock::new(GlobalData {
current_user: None,
notifications: Vec::new(),
})),
}
}
async fn set_user(&self, user: User) {
let mut data = self.data.write().await;
data.current_user = Some(user);
}
async fn add_notification(&self, notification: Notification) {
let mut data = self.data.write().await;
data.notifications.push(notification);
}
async fn get_user(&self) -> Option<User> {
let data = self.data.read().await;
data.current_user.clone()
}
}
// Broadcast state changes to all windows
use tauri::{Emitter, Manager};
#[tauri::command]
async fn login_user(
app: tauri::AppHandle,
state: tauri::State<'_, SharedAppState>,
username: String,
) -> Result<(), String> {
let user = User {
id: 1,
name: username,
email: "user@example.com".to_string(),
};
state.set_user(user.clone()).await;
// Notify all windows
app.emit("user-logged-in", &user).map_err(|e| e.to_string())?;
Ok(())
}
// Open new window with shared state
#[tauri::command]
fn open_settings_window(app: tauri::AppHandle) -> Result<(), String> {
tauri::WebviewWindowBuilder::new(
&app,
"settings",
tauri::WebviewUrl::App("settings.html".into()),
)
.title("Settings")
.build()
.map_err(|e| e.to_string())?;
Ok(())
}These state management patterns provide flexibility for applications of all sizes - from simple local state to complex distributed state with persistence and multi-window synchronization.
Tauri Framework
Complete guide to building desktop applications with Tauri 2.x - the modern alternative to Electron with web UI + Rust backend.
Architecture Overview
What is Tauri?
Tauri is a framework for building desktop applications using web technologies for the frontend (HTML, CSS, JavaScript) and Rust for the backend. Unlike Electron which bundles Chromium and Node.js (~100MB+), Tauri uses the OS's native webview (WebKit on macOS, WebView2 on Windows, WebKitGTK on Linux) resulting in 3-5MB bundles.
Core Architecture:
┌─────────────────────────────────────────┐
│ Frontend (Web) │
│ React/Vue/Svelte/Vanilla │
│ ├─ UI Rendering │
│ ├─ User Interactions │
│ └─ invoke() calls to backend │
└──────────────┬──────────────────────────┘
│ IPC (JSON)
┌──────────────▼──────────────────────────┐
│ Tauri Core (Rust) │
│ ├─ Command Handlers │
│ ├─ Event System │
│ ├─ State Management │
│ └─ Plugin System │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ Native OS APIs │
│ ├─ File System │
│ ├─ Shell/Process │
│ ├─ HTTP Client │
│ ├─ System Tray │
│ └─ Notifications │
└─────────────────────────────────────────┘Project Structure
my-tauri-app/
├─ src-tauri/ # Rust backend
│ ├─ src/
│ │ ├─ main.rs # Entry point, command registration
│ │ ├─ commands/ # Command modules
│ │ ├─ state/ # Application state
│ │ └─ lib.rs # Optional library code
│ ├─ Cargo.toml # Rust dependencies
│ ├─ tauri.conf.json # Tauri configuration
│ ├─ icons/ # App icons
│ └─ capabilities/ # Security capabilities (v2)
├─ src/ # Frontend source
│ ├─ App.tsx # Main React/Vue component
│ ├─ components/
│ ├─ styles/
│ └─ main.tsx # Frontend entry
├─ package.json # Node dependencies
└─ vite.config.ts # Vite configurationSetup and Installation
Prerequisites
# Rust toolchain (rustup.rs)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Node.js (for frontend tooling)
# Install via nvm, fnm, or nodejs.org
# Platform-specific requirements:
# Windows: WebView2, Visual Studio Build Tools
# macOS: Xcode Command Line Tools
# Linux: webkit2gtk, build-essentialCreate New Tauri Project
# Install Tauri CLI
cargo install tauri-cli --version "^2.0.0"
# Create project with wizard
cargo create-tauri-app
# Or with specific frontend:
npm create tauri-app@latest
# Select: Package manager (npm/yarn/pnpm)
# Frontend framework (React/Vue/Svelte/Vanilla)
# TypeScript (recommended: Yes)Development Workflow
# Start development server (hot reload)
cargo tauri dev
# Opens app window + watches for changes
# Frontend: Vite HMR
# Backend: Cargo watch (rebuild on .rs changes)
# Build for production
cargo tauri build
# Creates optimized bundle in src-tauri/target/release/bundle/
# Run frontend only (testing UI)
npm run devIPC Communication
Commands (Frontend → Backend)
Commands are Rust functions exposed to the frontend via #[tauri::command].
Basic Command:
// src-tauri/src/main.rs
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Frontend Usage:
// src/App.tsx
import { invoke } from '@tauri-apps/api/core';
async function handleGreet() {
const message = await invoke<string>('greet', { name: 'World' });
console.log(message); // "Hello, World!"
}Advanced Commands with State
use tauri::State;
use std::sync::Mutex;
struct AppState {
counter: Mutex<i32>,
}
#[tauri::command]
fn increment_counter(state: State<AppState>) -> i32 {
let mut counter = state.counter.lock().unwrap();
*counter += 1;
*counter
}
#[tauri::command]
fn get_counter(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_counter,
get_counter
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Frontend:
import { invoke } from '@tauri-apps/api/core';
const count = await invoke<number>('increment_counter');
const current = await invoke<number>('get_counter');Async Commands with Tokio
use tokio::time::{sleep, Duration};
#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
let client = reqwest::Client::new();
let response = client
.get(&url)
.send()
.await
.map_err(|e| e.to_string())?;
response.text().await.map_err(|e| e.to_string())
}
#[tauri::command]
async fn long_running_task() -> Result<String, String> {
sleep(Duration::from_secs(5)).await;
Ok("Task completed".to_string())
}Error Handling
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
struct ApiError {
message: String,
code: u32,
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
#[tauri::command]
fn risky_operation(value: i32) -> Result<String, ApiError> {
if value < 0 {
return Err(ApiError {
message: "Value must be positive".to_string(),
code: 400,
});
}
Ok(format!("Success: {}", value))
}Frontend Error Handling:
try {
const result = await invoke<string>('risky_operation', { value: -1 });
} catch (error) {
console.error('Command failed:', error);
// error is serialized ApiError
}Events (Backend → Frontend)
Events enable pushing data from backend to frontend.
Backend Emit:
use tauri::{Emitter, Manager};
#[tauri::command]
async fn start_monitoring(app: tauri::AppHandle) {
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
app.emit("status-update", "Running").unwrap();
}
});
}Frontend Listen:
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen<string>('status-update', (event) => {
console.log('Status:', event.payload);
});
// Later: cleanup
unlisten();Native API Access
File System
use tauri::api::dialog::blocking::FileDialogBuilder;
use std::fs;
#[tauri::command]
fn open_file_dialog() -> Option<String> {
FileDialogBuilder::new().pick_file()
.map(|path| path.to_string_lossy().to_string())
}
#[tauri::command]
fn read_file_content(path: String) -> Result<String, String> {
fs::read_to_string(path).map_err(|e| e.to_string())
}
#[tauri::command]
fn write_file_content(path: String, content: String) -> Result<(), String> {
fs::write(path, content).map_err(|e| e.to_string())
}Frontend:
import { invoke } from '@tauri-apps/api/core';
async function openFile() {
const path = await invoke<string | null>('open_file_dialog');
if (path) {
const content = await invoke<string>('read_file_content', { path });
console.log(content);
}
}System Tray
use tauri::{
menu::{Menu, MenuItem},
tray::TrayIconBuilder,
Manager,
};
fn main() {
tauri::Builder::default()
.setup(|app| {
let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&quit])?;
let _tray = TrayIconBuilder::new()
.menu(&menu)
.on_menu_event(|app, event| match event.id.as_ref() {
"quit" => {
app.exit(0);
}
_ => {}
})
.build(app)?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Notifications
use tauri::Notification;
#[tauri::command]
fn send_notification(app: tauri::AppHandle, message: String) -> Result<(), String> {
Notification::new(&app.config().identifier)
.title("My App")
.body(message)
.show()
.map_err(|e| e.to_string())
}Shell/Process Execution
use tauri::api::process::{Command, CommandEvent};
#[tauri::command]
async fn run_command(program: String, args: Vec<String>) -> Result<String, String> {
let (mut rx, _child) = Command::new(program)
.args(args)
.spawn()
.map_err(|e| e.to_string())?;
let mut output = String::new();
while let Some(event) = rx.recv().await {
match event {
CommandEvent::Stdout(line) => output.push_str(&line),
CommandEvent::Stderr(line) => output.push_str(&line),
CommandEvent::Terminated(_) => break,
_ => {}
}
}
Ok(output)
}Configuration
tauri.conf.json
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"productName": "My App",
"version": "1.0.0",
"identifier": "com.mycompany.myapp",
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"devUrl": "http://localhost:5173",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "My App",
"width": 1200,
"height": 800,
"resizable": true,
"fullscreen": false,
"minWidth": 800,
"minHeight": 600
}
],
"security": {
"csp": "default-src 'self'; img-src 'self' https: data:;"
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/icon.icns",
"icons/icon.ico"
],
"macOS": {
"minimumSystemVersion": "10.13"
},
"windows": {
"webviewInstallMode": {
"type": "downloadBootstrapper"
}
}
}
}Security Configuration
Content Security Policy (CSP):
{
"app": {
"security": {
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; connect-src 'self' https://api.myapp.com"
}
}
}Capabilities (Tauri v2):
// src-tauri/capabilities/default.json
{
"identifier": "default",
"description": "Default capabilities",
"windows": ["main"],
"permissions": [
"core:default",
"fs:allow-read-text-file",
"fs:allow-write-text-file",
"dialog:allow-open",
"dialog:allow-save",
"shell:allow-execute"
]
}Advanced Patterns
Window Management
use tauri::{Manager, WebviewUrl, WebviewWindowBuilder};
#[tauri::command]
async fn open_new_window(app: tauri::AppHandle) -> Result<(), String> {
WebviewWindowBuilder::new(
&app,
"new-window",
WebviewUrl::App("index.html".into())
)
.title("New Window")
.inner_size(800.0, 600.0)
.build()
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
fn close_window(window: tauri::Window) -> Result<(), String> {
window.close().map_err(|e| e.to_string())
}Custom Protocol
use tauri::{http::ResponseBuilder, Manager};
fn main() {
tauri::Builder::default()
.setup(|app| {
app.handle().plugin(
tauri_plugin_localhost::Builder::new()
.build(),
)?;
Ok(())
})
.register_uri_scheme_protocol("myapp", |_app, request| {
// Handle custom myapp:// protocol
ResponseBuilder::new()
.status(200)
.body(b"Custom protocol response".to_vec())
.map_err(Into::into)
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Plugin Development
use tauri::{plugin::Plugin, Runtime};
pub struct MyPlugin<R: Runtime> {
_marker: std::marker::PhantomData<R>,
}
impl<R: Runtime> Plugin<R> for MyPlugin<R> {
fn name(&self) -> &'static str {
"my-plugin"
}
fn initialize(&mut self, app: &tauri::AppHandle<R>, _config: serde_json::Value) -> tauri::plugin::Result<()> {
// Initialize plugin
Ok(())
}
}
// Usage in main.rs
fn main() {
tauri::Builder::default()
.plugin(MyPlugin { _marker: std::marker::PhantomData })
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Performance Optimization
Bundle Size Reduction
Cargo.toml optimizations:
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1 # Better optimization
panic = "abort" # Remove panic unwinding code
strip = true # Strip symbolsLazy Loading
// Frontend: Code splitting
const HeavyComponent = lazy(() => import('./HeavyComponent'));
// Backend: Lazy state initialization
use once_cell::sync::Lazy;
static EXPENSIVE_RESOURCE: Lazy<ExpensiveType> = Lazy::new(|| {
// Initialize only when first accessed
ExpensiveType::new()
});Debouncing IPC Calls
import { debounce } from 'lodash';
const debouncedSearch = debounce(async (query: string) => {
const results = await invoke('search', { query });
setResults(results);
}, 300);Build and Distribution
Build Commands
# Development build
cargo tauri dev
# Production build (current platform)
cargo tauri build
# Build with debug info
cargo tauri build --debug
# Specific bundle type
cargo tauri build --bundles deb,appimage # Linux
cargo tauri build --bundles dmg,app # macOS
cargo tauri build --bundles msi,nsis # WindowsCode Signing
macOS:
# Sign app
codesign --deep --force --verify --verbose \
--sign "Developer ID Application: Your Name" \
target/release/bundle/macos/MyApp.app
# Notarize
xcrun notarytool submit target/release/bundle/dmg/MyApp.dmg \
--apple-id "your@email.com" \
--password "app-specific-password" \
--team-id "TEAMID"Windows:
# Sign with signtool.exe
signtool sign /tr http://timestamp.digicert.com /td sha256 `
/fd sha256 /a "target\release\MyApp.exe"Auto-Updates
// Install tauri-plugin-updater
use tauri_plugin_updater::UpdaterExt;
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_updater::init())
.setup(|app| {
let handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
let update = handle.updater().check().await;
// Handle update
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Production Examples
File Manager Command
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Serialize, Deserialize)]
struct FileEntry {
name: String,
path: String,
is_dir: bool,
size: u64,
}
#[tauri::command]
fn list_directory(path: String) -> Result<Vec<FileEntry>, String> {
let dir_path = PathBuf::from(path);
if !dir_path.exists() {
return Err("Directory does not exist".to_string());
}
let mut entries = Vec::new();
for entry in fs::read_dir(dir_path).map_err(|e| e.to_string())? {
let entry = entry.map_err(|e| e.to_string())?;
let metadata = entry.metadata().map_err(|e| e.to_string())?;
entries.push(FileEntry {
name: entry.file_name().to_string_lossy().to_string(),
path: entry.path().to_string_lossy().to_string(),
is_dir: metadata.is_dir(),
size: metadata.len(),
});
}
Ok(entries)
}Database Integration
use sqlx::{SqlitePool, FromRow};
use tauri::State;
#[derive(FromRow, Serialize)]
struct User {
id: i64,
name: String,
email: String,
}
struct DbState {
pool: SqlitePool,
}
#[tauri::command]
async fn get_users(state: State<'_, DbState>) -> Result<Vec<User>, String> {
sqlx::query_as::<_, User>("SELECT id, name, email FROM users")
.fetch_all(&state.pool)
.await
.map_err(|e| e.to_string())
}
#[tokio::main]
async fn main() {
let pool = SqlitePool::connect("sqlite://app.db")
.await
.expect("Failed to connect to database");
tauri::Builder::default()
.manage(DbState { pool })
.invoke_handler(tauri::generate_handler![get_users])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Debugging
# Enable Rust backtraces
RUST_BACKTRACE=1 cargo tauri dev
# Open DevTools
# macOS/Linux: Cmd/Ctrl + Shift + I
# Or programmatically:#[cfg(debug_assertions)]
window.open_devtools();Console logging from Rust:
println!("Debug: {:?}", value); // Appears in terminalFrontend console:
console.log('Frontend log'); // Appears in DevToolsThis comprehensive guide covers Tauri fundamentals through advanced patterns. Combine with architecture-patterns.md for structure, state-management.md for complex state, and platform-integration.md for OS-specific features.