Lesson 15-Tauri Basic Programming

Frontend and Rust Interaction

Frontend Calling Rust Functions

Defining Rust Commands

// 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 Invocation Example

// src/App.vue
<script setup>
import { invoke } from '@tauri-apps/api/tauri'

async function sayHello() {
  const response = await invoke('greet', { name: 'World' })
  console.log(response) // "Hello, World!"
}
</script>

<template>
  <button @click="sayHello">Greet</button>
</template>

Rust Calling Frontend Methods

Defining Frontend Callbacks

// src-tauri/src/main.rs
#[tauri::command]
fn trigger_frontend_callback(window: tauri::Window) {
    window.emit('rust-event', "Data from Rust").unwrap();
}

Frontend Event Listening

// src/App.vue
<script setup>
import { listen } from '@tauri-apps/api/event'

onMounted(() => {
  listen('rust-event', (event) => {
    console.log(event.payload) // "Data from Rust"
  })
})
</script>

Data Serialization

Using Serde for Complex Data Transfer

// src-tauri/src/main.rs
use serde::{Serialize, Deserialize}

#[derive(Serialize, Deserialize)]
struct User {
    id: u32,
    name: String,
    email: String,
}

#[tauri::command]
fn get_user() -> User {
    User {
        id: 1,
        name: "Alice".into(),
        email: "alice@example.com".into(),
    }
}

Frontend Receiving Complex Data

// src/App.vue
<script setup>
import { invoke } from '@tauri-apps/api/tauri'

interface User {
  id: number
  name: string
  email: string
}

async function fetchUser() {
  const user: User = await invoke('get_user')
  console.log(user)
}
</script>

Error Handling

Rust-Side Error Definition

// src-tauri/src/main.rs
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("File not found")]
    FileNotFound,
    #[error("Permission denied")]
    PermissionDenied,
}

#[tauri::command]
fn read_file(path: String) -> Result<String, String> {
    std::fs::read_to_string(path)
        .map_err(|e| match e.kind() {
            std::io::ErrorKind::NotFound => AppError::FileNotFound.to_string(),
            std::io::ErrorKind::PermissionDenied => AppError::PermissionDenied.to_string(),
            _ => e.to_string(),
        })
}

Frontend Error Handling

// src/App.vue
<script setup>
import { invoke } from '@tauri-apps/api/tauri'

async function readFile() {
  try {
    const content = await invoke('read_file', { path: '/etc/secret' })
    console.log(content)
  } catch (error) {
    if (error.includes('File not found')) {
      alert('File not found!')
    } else if (error.includes('Permission denied')) {
      alert('Permission denied!')
    } else {
      alert('Unknown error: ' + error)
    }
  }
}
</script>

Asynchronous Communication

Rust-Side Asynchronous Commands

// src-tauri/src/main.rs
use tokio::fs;

#[tauri::command]
async fn async_read_file(path: String) -> Result<String, String> {
    fs::read_to_string(path)
        .await
        .map_err(|e| e.to_string())
}

Frontend Calling Asynchronous Commands

// src/App.vue
<script setup>
import { invoke } from '@tauri-apps/api/tauri'

async function loadFile() {
  const content = await invoke('async_read_file', { 
    path: '/tmp/data.txt' 
  })
  console.log(content)
}
</script>

System API Integration

File System Operations

File Read/Write Example

// src-tauri/src/main.rs
use std::fs;

#[tauri::command]
fn write_file(path: String, content: String) -> Result<(), String> {
    fs::write(path, content).map_err(|e| e.to_string())
}

#[tauri::command]
fn read_file(path: String) -> Result<String, String> {
    fs::read_to_string(path).map_err(|e| e.to_string())
}

Frontend File Operations

// src/App.vue
<script setup>
import { invoke } from '@tauri-apps/api/tauri'

async function saveNote() {
  await invoke('write_file', {
    path: '/tmp/note.txt',
    content: 'Hello Tauri!'
  })
}

async function loadNote() {
  const content = await invoke('read_file', { path: '/tmp/note.txt' })
  console.log(content)
}
</script>

System Dialogs

File Dialog Example

// src-tauri/src/main.rs
use tauri::api::dialog;

#[tauri::command]
fn open_file_dialog() -> Result<String, String> {
    dialog::FileDialogBuilder::new()
        .add_filter("Text Files", &["txt"])
        .pick_file()
        .map(|path| path.to_string_lossy().into_owned())
        .map_err(|e| e.to_string())
}

Frontend Calling Dialogs

// src/App.vue
<script setup>
import { invoke } from '@tauri-apps/api/tauri'

async function openFileDialog() {
  const path = await invoke('open_file_dialog')
  console.log('Selected file:', path)
}
</script>

Clipboard Operations

Clipboard Read/Write

// src-tauri/src/main.rs
use tauri::api::clipboard;

#[tauri::command]
fn get_clipboard() -> Result<String, String> {
    clipboard::read_text().map_err(|e| e.to_string())
}

#[tauri::command]
fn set_clipboard(text: String) -> Result<(), String> {
    clipboard::write_text(text).map_err(|e| e.to_string())
}

Frontend Clipboard Operations

// src/App.vue
<script setup>
import { invoke } from '@tauri-apps/api/tauri'

async function copyToClipboard() {
  await invoke('set_clipboard', { text: 'Hello from Tauri!' })
}

async function pasteFromClipboard() {
  const text = await invoke('get_clipboard')
  console.log('Clipboard content:', text)
}
</script>

System Notifications

Sending Notifications

// src-tauri/src/main.rs
use tauri::api::notification;

#[tauri::command]
fn send_notification(title: String, body: String) -> Result<(), String> {
    notification::Notification::new()
        .title(&title)
        .body(&body)
        .show()
        .map_err(|e| e.to_string())
}

Frontend Calling Notifications

// src/App.vue
<script setup>
import { invoke } from '@tauri-apps/api/tauri'

async function showNotification() {
  await invoke('send_notification', {
    title: 'Tauri Notification',
    body: 'This is a system notification'
  })
}
</script>

Native Menus and System Tray

Creating Menus

// src-tauri/src/main.rs
use tauri::Menu;
use tauri::MenuEntry;

fn main() {
    let context = tauri::generate_context!();

    tauri::Builder::default()
        .menu(
            Menu::new()
                .add_item(MenuEntry::new("File").with_submenu(vec![
                    MenuEntry::new("Open").with_action(|| {
                        println!("Open file");
                    }),
                    MenuEntry::new("Exit").with_action(|| {
                        std::process::exit(0);
                    }),
                ]))
        )
        .run(context)
        .expect("error while running tauri application");
}

Creating System Tray

// src-tauri/src/main.rs
use tauri::Tray;
use tauri::TrayEvent;

fn main() {
    let context = tauri::generate_context!();

    tauri::Builder::default()
        .system_tray(
            Tray::new().with_menu(
                Menu::new().add_item(MenuEntry::new("Show").with_action(|| {
                    println!("Show window");
                })).add_item(MenuEntry::new("Exit").with_action(|| {
                    std::process::exit(0);
                }))
            )
        )
        .on_system_tray_event(|app, event| match event {
            TrayEvent::DoubleClick { .. } => {
                let window = app.get_window("main").unwrap();
                window.show().unwrap();
            }
            _ => {}
        })
        .run(context)
        .expect("error while running tauri application");
}

Configuration and Packaging

Tauri Configuration File

tauri.conf.json Example

{
  "build": {
    "beforeBuildCommand": "npm run build",
    "beforeDevCommand": "npm run dev",
    "devPath": "http://localhost:3000",
    "distDir": "../dist"
  },
  "package": {
    "productName": "MyTauriApp",
    "version": "0.1.0"
  },
  "tauri": {
    "allowlist": {
      "all": false,
      "fs": {
        "scope": ["$APPDATA/*", "$HOME/*"]
      },
      "dialog": {
        "all": true
      }
    },
    "security": {
      "csp": null
    },
    "bundle": {
      "identifier": "com.mycompany.myapp",
      "icon": ["icons/32x32.png", "icons/128x128.png", "icons/256x256.png"]
    }
  }
}

Application Icon Configuration

Preparing Icons

  1. Prepare PNG icons in multiple sizes (16×16, 32×32, 64×64, 128×128, 256×256).
  2. Create an icons directory and place the icon files.

Configuring Icon Paths

// tauri.conf.json
{
  "bundle": {
    "icon": [
      "icons/16x16.png",
      "icons/32x32.png",
      "icons/64x64.png",
      "icons/128x128.png",
      "icons/256x256.png"
    ]
  }
}

Packaging and Distribution

Running in Development Mode

npm run tauri dev

Building for Production

npm run tauri build

Package Output Directories

  • Windows: src-tauri/target/release/bundle/msi
  • macOS: src-tauri/target/release/bundle/dmg
  • Linux: src-tauri/target/release/bundle/appimage

Automatic Update Mechanism

Configuring Automatic Updates

// src-tauri/src/main.rs
fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let window = app.get_window("main").unwrap();

            // Check for updates
            tauri::async_runtime::spawn(async move {
                match tauri::updater::check_update(&app.config()).await {
                    Ok(update) if update.is_update_available() => {
                        window.emit("update-available", update).unwrap();
                    }
                    _ => {}
                }
            });

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Frontend Handling Updates

// src/App.vue
<script setup>
import { listen } from '@tauri-apps/api/event'

onMounted(() => {
  listen('update-available', (event) => {
    if (confirm('New version found. Update now?')) {
      // Trigger update download and installation
    }
  })
})
</script>

Multi-Platform Packaging and Testing

Cross-Platform Packaging Commands

# Windows
npm run tauri build -- --target x86_64-pc-windows-msvc

# macOS
npm run tauri build -- --target x86_64-apple-darwin

# Linux
npm run tauri build -- --target x86_64-unknown-linux-gnu

Testing Strategies

Unit Testing: Rust code unit tests

cargo test

Integration Testing: Frontend-Rust interaction tests

npm run test

End-to-End Testing: Full application workflow tests

npm run tauri test

Cross-Platform Validation:

  • Conduct UI adaptation tests on target platforms.
  • Verify file system permissions.
  • Test system API compatibility.
Share your love