Tauri Commands
Complete guide to creating and using Tauri commands in DesQTA
Tauri Commands Overview
Tauri commands are Rust functions exposed to the frontend JavaScript/TypeScript code. They provide a secure bridge between the frontend and backend, allowing you to access native system capabilities.
Command Basics
Creating a Command
// src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
// Register in main()
.invoke_handler(tauri::generate_handler![greet])
Frontend Usage
import { invoke } from '@tauri-apps/api/core';
const greeting = await invoke<string>('greet', { name: 'World' });
console.log(greeting); // "Hello, World!"
Command Types
Simple Commands
#[tauri::command]
fn get_version() -> String {
"1.0.0".to_string()
}
#[tauri::command]
fn add_numbers(a: i32, b: i32) -> i32 {
a + b
}
Commands with AppHandle
use tauri::AppHandle;
#[tauri::command]
fn quit_app(app: AppHandle) {
app.exit(0);
}
#[tauri::command]
fn get_app_path(app: AppHandle) -> Result<String, String> {
app.path_resolver()
.app_data_dir()
.ok_or_else(|| "App data dir not found".to_string())
.and_then(|path| {
path.to_str()
.ok_or_else(|| "Invalid path".to_string())
.map(|s| s.to_string())
})
}
Commands with Window
use tauri::Window;
#[tauri::command]
fn show_window(window: Window) -> Result<(), String> {
window.show().map_err(|e| e.to_string())
}
#[tauri::command]
fn set_window_title(window: Window, title: String) -> Result<(), String> {
window.set_title(&title).map_err(|e| e.to_string())
}
Async Commands
#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
let response = reqwest::get(&url)
.await
.map_err(|e| e.to_string())?;
response.text()
.await
.map_err(|e| e.to_string())
}
Command Parameters
Basic Types
#[tauri::command]
fn process_data(
name: String,
age: i32,
active: bool,
score: f64,
) -> String {
format!("Name: {}, Age: {}, Active: {}, Score: {}", name, age, active, score)
}
Complex Types (Serde)
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct User {
id: i32,
name: String,
email: String,
}
#[tauri::command]
fn create_user(user: User) -> Result<User, String> {
// Process user
Ok(user)
}
Optional Parameters
#[tauri::command]
fn process_with_option(value: Option<String>) -> String {
match value {
Some(v) => format!("Value: {}", v),
None => "No value".to_string(),
}
}
Collections
#[tauri::command]
fn process_items(items: Vec<String>) -> usize {
items.len()
}
#[tauri::command]
fn process_map(data: HashMap<String, String>) -> String {
format!("Map has {} entries", data.len())
}
Error Handling
Result Type
#[tauri::command]
fn risky_operation() -> Result<String, String> {
match do_something() {
Ok(result) => Ok(result),
Err(e) => Err(format!("Operation failed: {}", e))
}
}
Using anyhow
use anyhow::{Result, Context};
#[tauri::command]
async fn fetch_with_context(url: String) -> Result<String, String> {
reqwest::get(&url)
.await
.context("Failed to fetch URL")?
.text()
.await
.context("Failed to read response")
.map_err(|e| e.to_string())
}
Real-World Examples from DesQTA
Session Management
// src-tauri/src/utils/session.rs
#[tauri::command]
pub fn check_session_exists() -> bool {
Session::exists()
}
#[tauri::command]
pub fn get_session() -> Result<Session, String> {
Session::load()
.map_err(|e| format!("Failed to load session: {}", e))
}
#[tauri::command]
pub fn save_session(session: Session) -> Result<(), String> {
session.save()
.map_err(|e| format!("Failed to save session: {}", e))
}
Settings Management
// src-tauri/src/utils/settings.rs
#[tauri::command]
pub fn get_settings() -> Result<Settings, String> {
Settings::load()
.map_err(|e| format!("Failed to load settings: {}", e))
}
#[tauri::command]
pub fn get_settings_subset(keys: Vec<String>) -> Result<HashMap<String, serde_json::Value>, String> {
let settings = Settings::load()
.map_err(|e| format!("Failed to load settings: {}", e))?;
let mut subset = HashMap::new();
for key in keys {
match key.as_str() {
"theme" => subset.insert("theme".to_string(), json!(settings.theme)),
"accent_color" => subset.insert("accent_color".to_string(), json!(settings.accent_color)),
// ... more keys
_ => {}
}
}
Ok(subset)
}
#[tauri::command]
pub fn save_settings(settings: Settings) -> Result<(), String> {
settings.save()
.map_err(|e| format!("Failed to save settings: {}", e))
}
Network Requests
// src-tauri/src/utils/netgrab.rs
#[tauri::command]
pub async fn fetch_api_data(
url: String,
method: RequestMethod,
headers: Option<HashMap<String, String>>,
body: Option<serde_json::Value>,
parameters: Option<HashMap<String, String>>,
is_image: bool,
return_url: bool,
) -> Result<String, String> {
let client = create_client();
let mut request = match method {
RequestMethod::GET => client.get(&url),
RequestMethod::POST => client.post(&url),
};
// Add headers
request = append_default_headers(request).await;
if let Some(custom_headers) = headers {
for (key, value) in custom_headers {
request = request.header(&key, &value);
}
}
// Add body
if let Some(body_data) = body {
request = request.json(&body_data);
}
// Add parameters
if let Some(params) = parameters {
request = request.query(¶ms);
}
// Execute request
let response = request.send().await
.map_err(|e| format!("Request failed: {}", e))?;
if is_image {
let bytes = response.bytes().await
.map_err(|e| format!("Failed to read image: {}", e))?;
let base64 = base64::encode(&bytes);
Ok(format!("data:image/png;base64,{}", base64))
} else {
response.text().await
.map_err(|e| format!("Failed to read response: {}", e))
}
}
File System Operations
// src-tauri/src/utils/notes_filesystem.rs
#[tauri::command]
pub fn save_note_file(note_id: String, content: String) -> Result<(), String> {
let notes_dir = get_notes_directory()
.map_err(|e| format!("Failed to get notes directory: {}", e))?;
let file_path = notes_dir.join(format!("{}.md", note_id));
fs::write(&file_path, content)
.map_err(|e| format!("Failed to write file: {}", e))?;
Ok(())
}
#[tauri::command]
pub fn load_note_file(note_id: String) -> Result<String, String> {
let notes_dir = get_notes_directory()
.map_err(|e| format!("Failed to get notes directory: {}", e))?;
let file_path = notes_dir.join(format!("{}.md", note_id));
fs::read_to_string(&file_path)
.map_err(|e| format!("Failed to read file: {}", e))
}
Command Registration
Registering Commands
// src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![
greet,
check_session_exists,
get_settings,
save_settings,
fetch_api_data,
// ... more commands
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Organizing Commands
// src-tauri/src/lib.rs
mod auth;
mod utils;
// Import command functions
use auth::*;
use utils::*;
// Register all commands
.invoke_handler(tauri::generate_handler![
// Auth commands
check_session_exists,
login,
logout,
// Settings commands
get_settings,
save_settings,
get_settings_subset,
// Data commands
fetch_api_data,
get_assessments,
// ... more
])
Frontend Integration
Type-Safe Invocation
import { invoke } from '@tauri-apps/api/core';
// Type the return value
const sessionExists = await invoke<boolean>('check_session_exists');
// Type parameters
interface Settings {
theme: string;
accent_color: string;
}
const settings = await invoke<Settings>('get_settings');
// Type both
const result = await invoke<Settings>('save_settings', { settings });
Error Handling
try {
const result = await invoke('risky_operation');
// Use result
} catch (error) {
console.error('Command failed:', error);
// Handle error
}
Command Wrapper
// src/lib/utils/tauriCommands.ts
import { invoke } from '@tauri-apps/api/core';
import { logger } from './logger';
export async function safeInvoke<T>(
command: string,
args?: Record<string, any>
): Promise<T | null> {
try {
return await invoke<T>(command, args);
} catch (error) {
logger.error('tauri', command, `Command failed: ${error}`, { error, args });
return null;
}
}
Advanced Patterns
Command with State
use std::sync::Mutex;
static COUNTER: Mutex<i32> = Mutex::new(0);
#[tauri::command]
fn increment_counter() -> i32 {
let mut count = COUNTER.lock().unwrap();
*count += 1;
*count
}
Command with Database
use rusqlite::Connection;
#[tauri::command]
fn query_database(query: String) -> Result<Vec<HashMap<String, String>>, String> {
let conn = Connection::open("data.db")
.map_err(|e| format!("Failed to open database: {}", e))?;
let mut stmt = conn.prepare(&query)
.map_err(|e| format!("Failed to prepare query: {}", e))?;
let rows = stmt.query_map([], |row| {
let mut map = HashMap::new();
// Map row to HashMap
Ok(map)
})
.map_err(|e| format!("Query failed: {}", e))?;
let mut results = Vec::new();
for row in rows {
results.push(row.map_err(|e| format!("Row error: {}", e))?);
}
Ok(results)
}
Command with Events
use tauri::{AppHandle, Emitter};
#[tauri::command]
fn trigger_event(app: AppHandle, data: String) -> Result<(), String> {
app.emit("custom-event", data)
.map_err(|e| format!("Failed to emit event: {}", e))
}
Best Practices
1. Error Messages
// ✅ Good - Descriptive errors
#[tauri::command]
fn load_data() -> Result<String, String> {
fs::read_to_string("data.txt")
.map_err(|e| format!("Failed to read data.txt: {}", e))
}
// ❌ Avoid - Generic errors
#[tauri::command]
fn load_data() -> Result<String, String> {
fs::read_to_string("data.txt")
.map_err(|_| "Error".to_string())
}
2. Logging
use crate::logger;
#[tauri::command]
fn important_operation() -> Result<String, String> {
logger::info("Starting important operation");
match do_work() {
Ok(result) => {
logger::info("Operation succeeded");
Ok(result)
}
Err(e) => {
logger::error(&format!("Operation failed: {}", e));
Err(e.to_string())
}
}
}
3. Type Safety
// ✅ Good - Use proper types
#[tauri::command]
fn process_user(user: User) -> Result<User, String> {
// ...
}
// ❌ Avoid - Using String for everything
#[tauri::command]
fn process_user(user_json: String) -> Result<String, String> {
// ...
}
4. Async When Needed
// ✅ Good - Async for I/O
#[tauri::command]
async fn fetch_data() -> Result<String, String> {
reqwest::get("https://api.example.com/data")
.await?
.text()
.await
.map_err(|e| e.to_string())
}
// ✅ Good - Sync for CPU-bound
#[tauri::command]
fn calculate_sum(numbers: Vec<i32>) -> i32 {
numbers.iter().sum()
}
Security Considerations
Input Validation
#[tauri::command]
fn process_path(path: String) -> Result<String, String> {
// Validate path
if path.contains("..") {
return Err("Invalid path".to_string());
}
// Process path
Ok(path)
}
Permission Checks
#[tauri::command]
fn sensitive_operation(app: AppHandle) -> Result<(), String> {
// Check if operation is allowed
if !is_operation_allowed(&app) {
return Err("Operation not allowed".to_string());
}
// Perform operation
Ok(())
}
Next Steps
- Backend Modules - Organizing Rust code
- Session Management - Session handling
- Settings System - Settings management
On this page