Lesson 16-Tauri System Integration and Native Features

Overview of Tauri System Integration and Native Features

Tauri’s System Integration Capabilities

Tauri leverages Rust backend and system-native Webview to provide deep integration with the operating system:

  • File System: Read/write files and path management.
  • Notifications: Send system-level notifications.
  • Clipboard: Access and modify clipboard content.
  • System Tray: Create tray icons and menus.
  • Dialogs: Invoke native file selection dialogs.
  • Network: Support for HTTP and WebSocket.
  • Storage: Integration with SQLite, IndexedDB, and LocalStorage.

Native Features and Plugin Mechanism

  • Built-in Features: Accessible via tauri::api and command system.
  • Plugins: Extend functionality, e.g., tauri-plugin-fs, tauri-plugin-notification.

Development Environment Setup

Initialize Project

npx create-tauri-app my-tauri-app
cd my-tauri-app
npm install

Project Structure

my-tauri-app/
├── src/                  # Frontend code
   ├── index.html
   ├── main.js
├── src-tauri/            # Rust backend code
   ├── Cargo.toml
   ├── tauri.conf.json
   └── src/
       └── main.rs
├── package.json

File System Operations

Reading and Writing Files

Configuring Permissions

src-tauri/tauri.conf.json
{
  "tauri": {
    "allowlist": {
      "fs": {
        "readFile": true,
        "writeFile": true,
        "scope": ["$APP/*"]
      }
    }
  }
}

Rust Backend

src-tauri/src/main.rs
use tauri::api::file::{read_text, write_text};

#[tauri::command]
fn save_file(path: String, content: String) -> Result<(), String> {
    write_text(path, &content).map_err(|e| e.to_string())?;
    Ok(())
}

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

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![save_file, read_file])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Frontend Invocation

src/main.js
const { invoke } = window.__TAURI__.tauri;

async function saveFile() {
    const content = document.getElementById('content').value;
    await invoke('save_file', { path: 'tasks.txt', content });
}

async function loadFile() {
    const content = await invoke('read_file', { path: 'tasks.txt' });
    document.getElementById('output').textContent = content;
}

File Path Management

Example Code

src-tauri/src/main.rs
use tauri::api::path::app_data_dir;

#[tauri::command]
fn get_app_path(config: tauri::Config) -> String {
    app_data_dir(&config).unwrap().to_string_lossy().into_owned()
}

Analysis

  • app_data_dir: Retrieves the application data directory.
  • Scope: Restricts file access range.

System Notifications

Sending Notifications

Configuring Permissions

{
  "tauri": {
    "allowlist": {
      "notification": {
        "all": true
      }
    }
  }
}

Rust Backend

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

#[tauri::command]
fn send_notification(title: String, body: String, app: tauri::AppHandle) {
    Notification::new(&app.config().tauri.bundle.identifier)
        .title(title)
        .body(body)
        .show()
        .unwrap();
}

Frontend Invocation

src/main.js
async function notify() {
    await invoke('send_notification', { title: 'Task Added', body: 'A new task has been added!' });
}

Configuring Notification Permissions

  • Checking Permissions:
use tauri::api::notification::is_permission_granted;

#[tauri::command]
fn check_notification_permission(app: tauri::AppHandle) -> bool {
    is_permission_granted(&app).unwrap_or(false)
}

Clipboard Operations

Reading and Writing to Clipboard

Configuring Permissions

{
  "tauri": {
    "allowlist": {
      "clipboard": {
        "all": true
      }
    }
  }
}

Frontend Invocation

src/main.js
const { writeText, readText } = window.__TAURI__.clipboard;

async function copyToClipboard() {
    await writeText('Hello from Tauri!');
}

async function pasteFromClipboard() {
    const text = await readText();
    document.getElementById('output').textContent = text;
}

Clipboard Event Listening

  • Using a Plugin (e.g., tauri-plugin-clipboard-manager):
cargo add tauri-plugin-clipboard-manager

Rust Backend

src-tauri/src/main.rs
use tauri_plugin_clipboard_manager::ClipboardExt;

fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_clipboard_manager::init())
        .setup(|app| {
            app.clipboard().listen(|event| {
                println!("Clipboard updated: {:?}", event.payload);
            });
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

System Tray Icons and Menus

Creating a System Tray

Configuring Permissions

{
  "tauri": {
    "systemTray": {
      "iconPath": "icons/icon.png"
    }
  }
}

Rust Backend

src-tauri/src/main.rs
use tauri::tray::{SystemTray, SystemTrayEvent};
use tauri::Manager;

fn main() {
    tauri::Builder::default()
        .system_tray(SystemTray::new().with_tooltip("Tauri App"))
        .on_system_tray_event(|app, event| match event {
            SystemTrayEvent::LeftClick { .. } => {
                let window = app.get_window("main").unwrap();
                window.show().unwrap();
            }
            _ => {}
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Adding a Tray Menu

Example Code

use tauri::menu::{Menu, MenuItem};

fn main() {
    let quit = MenuItem::new("quit", "Quit", None, None).unwrap();
    let menu = Menu::new().unwrap().add_item(quit).unwrap();
    tauri::Builder::default()
        .system_tray(SystemTray::new().with_menu(menu))
        .on_system_tray_event(|app, event| match event {
            SystemTrayEvent::MenuItemClick { id, .. } if id == "quit" => app.exit(0),
            _ => {}
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

System Dialogs

Opening a File Dialog

Configuring Permissions

{
  "tauri": {
    "allowlist": {
      "dialog": {
        "open": true
      }
    }
  }
}

Frontend Invocation

src/main.js
const { open } = window.__TAURI__.dialog;

async function openFile() {
    const selected = await open({
        multiple: false,
        filters: [{ name: 'Text', extensions: ['txt'] }],
    });
    document.getElementById('output').textContent = selected || 'No file selected';
}

Saving a File Dialog

Configuring Permissions

{
  "tauri": {
    "allowlist": {
      "dialog": {
        "save": true
      }
    }
  }
}

Frontend Invocation

const { save } = window.__TAURI__.dialog;

async function saveFile() {
    const filePath = await save({
        defaultPath: 'tasks.txt',
    });
    if (filePath) await invoke('save_file', { path: filePath, content: 'Hello, Tauri!' });
}

Network Requests and WebSocket

HTTP Requests

Configuring Permissions

{
  "tauri": {
    "allowlist": {
      "http": {
        "all": true,
        "scope": ["https://api.example.com/*"]
      }
    }
  }
}

Frontend Invocation

src/main.js
const { fetch } = window.__TAURI__.http;

async function fetchData() {
    const response = await fetch('https://api.example.com/data', {
        method: 'GET',
    });
    const data = await response.json();
    document.getElementById('output').textContent = JSON.stringify(data);
}

WebSocket Communication

Configuring Permissions

{
  "tauri": {
    "allowlist": {
      "websocket": {
        "all": true
      }
    }
  }
}

Frontend Invocation

src/main.js
const { WebSocketClient } = window.__TAURI__.websocket;

async function connectWebSocket() {
    const ws = await WebSocketClient.connect('ws://echo.websocket.org');
    ws.on('message', (msg) => {
        document.getElementById('output').textContent = msg.data;
    });
    ws.send('Hello, WebSocket!');
}

Data Storage

SQLite

Installing Plugin

cargo add tauri-plugin-sql --features sqlite

Rust Backend

src-tauri/src/main.rs
use tauri_plugin_sql::{Builder as SqlBuilder, Migration, MigrationKind};

fn main() {
    let migrations = vec![
        Migration {
            version: 1,
            description: "create tasks table",
            sql: "CREATE TABLE tasks (id INTEGER PRIMARY KEY, text TEXT)",
            kind: MigrationKind::Up,
        },
    ];

    tauri::Builder::default()
        .plugin(SqlBuilder::default().add_migrations("sqlite:tasks.db", migrations).build())
        .invoke_handler(tauri::generate_handler![save_task])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

#[tauri::command]
fn save_task(text: String, app: tauri::AppHandle) -> Result<(), String> {
    let db = app.sql().get("sqlite:tasks.db").unwrap();
    db.execute("INSERT INTO tasks (text) VALUES (?)", [text]).map_err(|e| e.to_string())?;
    Ok(())
}

Frontend Invocation

async function saveTask() {
    const text = document.getElementById('taskInput').value;
    await invoke('save_task', { text });
}

IndexedDB

Frontend Implementation

const openDB = () => {
    return new Promise((resolve, reject) => {
        const request = indexedDB.open('TasksDB', 1);
        request.onupgradeneeded = () => {
            const db = request.result;
            db.createObjectStore('tasks', { keyPath: 'id' });
        };
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
    });
};

async function saveToIndexedDB(task) {
    const db = await openDB();
    const tx = db.transaction('tasks', 'readwrite');
    const store = tx.objectStore('tasks');
    store.put({ id: Date.now(), text: task });
    await tx.complete;
}

LocalStorage

Frontend Implementation

function saveToLocalStorage(task) {
    const tasks = JSON.parse(localStorage.getItem('tasks') || '[]');
    tasks.push({ id: Date.now(), text: task });
    localStorage.setItem('tasks', JSON.stringify(tasks));
}

function loadFromLocalStorage() {
    return JSON.parse(localStorage.getItem('tasks') || '[]');
}

Comprehensive Case Study: Multifunctional Task Manager

Requirements Analysis

Build a task manager that:

  • Saves tasks to the file system.
  • Sends system notifications for reminders.
  • Copies tasks to the clipboard.
  • Controls via system tray icon.
  • Selects files using dialogs.
  • Fetches data via network requests.
  • Stores tasks in SQLite.

Implementation Code Breakdown

src-tauri/tauri.conf.json

{
  "tauri": {
    "allowlist": {
      "fs": { "all": true },
      "notification": { "all": true },
      "clipboard": { "all": true },
      "dialog": { "all": true },
      "http": { "all": true, "scope": ["https://api.example.com/*"] },
      "systemTray": { "iconPath": "icons/icon.png" }
    }
  }
}

src-tauri/Cargo.toml

[dependencies]
tauri = { version = "1.5", features = ["api-all"] }
tauri-plugin-sql = { version = "1.1", features = ["sqlite"] }

src-tauri/src/main.rs

use tauri::tray::{SystemTray, SystemTrayEvent};
use tauri::api::notification::Notification;
use tauri::Manager;
use tauri_plugin_sql::{Builder as SqlBuilder, Migration, MigrationKind};

#[tauri::command]
fn add_task(text: String, app: tauri::AppHandle) -> Result<(), String> {
    let db = app.sql().get("sqlite:tasks.db").unwrap();
    db.execute("INSERT INTO tasks (text) VALUES (?)", [text.clone()]).map_err(|e| e.to_string())?;
    Notification::new(&app.config().tauri.bundle.identifier)
        .title("Task Added")
        .body(&text)
        .show()
        .unwrap();
    Ok(())
}

#[tauri::command]
fn get_tasks(app: tauri::AppHandle) -> Result<Vec<String>, String> {
    let db = app.sql().get("sqlite:tasks.db").unwrap();
    let mut stmt = db.prepare("SELECT text FROM tasks").unwrap();
    let tasks: Vec<String> = stmt.query_map([], |row| row.get(0)).unwrap().map(Result::unwrap).collect();
    Ok(tasks)
}

fn main() {
    let migrations = vec![
        Migration {
            version: 1,
            description: "create tasks",
            sql: "CREATE TABLE tasks (id INTEGER PRIMARY KEY, text TEXT)",
            kind: MigrationKind::Up,
        },
    ];

    let tray = SystemTray::new().with_tooltip("Task Manager");
    tauri::Builder::default()
        .plugin(SqlBuilder::default().add_migrations("sqlite:tasks.db", migrations).build())
        .system_tray(tray)
        .on_system_tray_event(|app, event| match event {
            SystemTrayEvent::LeftClick { .. } => {
                app.get_window("main").unwrap().show().unwrap();
            }
            _ => {}
        })
        .invoke_handler(tauri::generate_handler![add_task, get_tasks])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

src/main.js

const { invoke, dialog, clipboard, http } = window.__TAURI__;

async function addTask() {
    const task = document.getElementById('taskInput').value.trim();
    if (task) {
        await invoke('add_task', { text: task });
        await clipboard.writeText(task);
        await updateTasks();
    }
}

async function updateTasks() {
    const tasks = await invoke('get_tasks');
    document.getElementById('taskList').innerHTML = tasks.map(t => `<li>${t}</li>`).join('');
}

async function openFile() {
    const selected = await dialog.open({ filters: [{ name: 'Text', extensions: ['txt'] }] });
    if (selected) {
        const response = await http.fetch('https://api.example.com/data', { method: 'GET' });
        document.getElementById('output').textContent = response.data.message;
    }
}

document.getElementById('addButton').addEventListener('click', addTask);
document.getElementById('openButton').addEventListener('click', openFile);
window.addEventListener('DOMContentLoaded', updateTasks);

src/index.html

<!DOCTYPE html>
<html>
<head>
  <title>Task Manager</title>
  <style>
    body { font-family: Arial, sans-serif; margin: 20px; text-align: center; }
    ul { list-style: none; padding: 0; }
    li { padding: 10px; background-color: #f0f0f0; margin-bottom: 5px; }
    input { padding: 8px; width: 200px; }
    button { padding: 8px 16px; background-color: #007bff; color: white; border: none; }
  </style>
</head>
<body>
  <h1>Task Manager</h1>
  <input id="taskInput" placeholder="Enter task">
  <button id="addButton">Add</button>
  <button id="openButton">Open File</button>
  <ul id="taskList"></ul>
  <div id="output"></div>
  <script src="main.js"></script>
</body>
</html>

Running the Application

npm run tauri dev

Analysis

  • File System: Saves tasks to files.
  • Notifications: Sends reminders when tasks are added.
  • Clipboard: Copies task text.
  • System Tray: Controls window visibility.
  • Dialogs: Selects files.
  • Network: Demonstrates HTTP requests.
  • SQLite: Persists tasks.
Share your love