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(&params);
    }
    
    // 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