Développement d'applications de bureau Tauri dans Cursor : backend Rust, commandes et sécurité

Tauri est un framework pour créer des applications de bureau petites, rapides et sécurisées en combinant un backend Rust avec un frontend web de votre choix. Contrairement à Electron, Tauri utilise le WebView natif du système d'exploitation au lieu d'intégrer Chromium, ce qui réduit considérablement la taille des binaires et l'utilisation de la mémoire. Cursor, un éditeur IDE alimenté par IA, accélère le développement Tauri en générant l'échafaudage de projets, les commandes Rust, les liaisons TypeScript sécurisées par typage et en auditant votre configuration de sécurité. Ce guide couvre l'architecture Tauri, les commandes Rust, le système de plugins, le modèle de sécurité et l'intégration de bureau assistée par IA.
Prérequis
Avant de commencer, assurez-vous de disposer des éléments suivants :
- Cursor (version la plus récente recommandée) comme éditeur IDE
- Rust toolchain (installez via rustup) avec le compilateur stable
- Node.js 18 ou supérieur pour les outils frontend
- Dépendances système : sous Linux, installez
webkit2gtk,build-essentialet les packages associés ; sous macOS, installez les Xcode Command Line Tools ; sous Windows, installez les Visual Studio C++ Build Tools
Si vous n'avez jamais écrit de Rust, demandez à Cursor d'expliquer la propriété, l'emprunt et les durées de vie en utilisant des exemples de gestionnaires de commandes Tauri. Comprendre ces concepts tôt évite les erreurs de compilation qui peuvent sembler obscures aux développeurs JavaScript passant à Rust.
Comprendre l'architecture Tauri
Une application Tauri comporte trois couches :
- Frontend : Votre application web (React, Vue, Svelte ou HTML simple). Elle s'exécute dans le WebView natif du système d'exploitation.
- Core (Rust) : Le runtime Tauri, écrit en Rust, gère les fenêtres, le cycle de vie de l'application et les appels système natifs.
- Commandes : Fonctions Rust personnalisées que le frontend appelle via la couche IPC de Tauri. C'est ainsi que votre interface web interagit avec le système de fichiers, les bases de données et les API du système d'exploitation.
La différence clé avec Electron est que le core Rust a un accès système complet tandis que le frontend est sandboxé dans le WebView. La communication passe exclusivement par des commandes et événements explicitement déclarés.
Échafaudage de projet dans Cursor
Demandez à Cursor d'échafauder un projet Tauri + React + TypeScript :
Create a Tauri 2.0 project with React and TypeScript.
Set up the frontend with Vite, configure the Rust backend,
and include the tauri-plugin-fs and tauri-plugin-dialog plugins.
Cursor génère la structure de projet Tauri standard :
my-tauri-app/
src/ # Frontend source
App.tsx
main.tsx
src-tauri/ # Rust backend
src/
main.rs
lib.rs
Cargo.toml
tauri.conf.json
capabilities/
default.json
package.json
vite.config.ts
Tauri 2.0 a introduit des changements majeurs par rapport à Tauri 1.x, y compris un nouveau système de permissions, des fichiers de capacités et une API de plugins repensée. Si vous suivez d'anciens tutoriels, vérifiez que les appels API correspondent à la syntaxe Tauri 2.0. Demandez à Cursor de migrer tout code Tauri 1.x que vous rencontrez.
Backend Rust et commandes
Les commandes sont le pont entre votre backend Rust et votre frontend web. Vous définissez une fonction Rust, l'annotez avec #[tauri::command], et l'invoquez depuis JavaScript.
Définition des commandes
// src-tauri/src/lib.rs
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use tauri::{Manager, State};
// A shared application state managed by Tauri
pub struct AppState {
pub config_dir: PathBuf,
}
#[derive(Serialize, Deserialize)]
pub struct FileEntry {
pub name: String,
pub path: String,
pub is_dir: bool,
pub size: u64,
}
/// List files in a directory
#[tauri::command]
pub async fn list_files(dir: String) -> Result<Vec<FileEntry>, String> {
let entries = fs::read_dir(&dir).map_err(|e| e.to_string())?;
let mut files: Vec<FileEntry> = entries
.filter_map(|entry| entry.ok())
.filter_map(|entry| {
let metadata = entry.metadata().ok()?;
Ok(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(),
})
})
.collect();
files.sort_by(|a, b| b.is_dir.cmp(&a.is_dir).then(a.name.cmp(&b.name)));
Ok(files)
}
/// Save content to a file
#[tauri::command]
pub async fn save_file(path: String, content: String) -> Result<(), String> {
fs::write(&path, content).map_err(|e| e.to_string())
}
/// Read a file's content
#[tauri::command]
pub async fn read_file(path: String) -> Result<String, String> {
fs::read_to_string(&path).map_err(|e| e.to_string())
}
/// Get the app data directory
#[tauri::command]
pub fn get_data_dir(state: State<AppState>) -> String {
state.config_dir.to_string_lossy().to_string()
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.manage(AppState {
config_dir: tauri::api::path::app_config_dir(
&tauri::generate_context!(),
).unwrap_or_else(|_| PathBuf::from(".")),
})
.invoke_handler(tauri::generate_handler![
list_files,
save_file,
read_file,
get_data_dir,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Appel de commandes depuis le frontend
// src/lib/tauri-api.ts
import { invoke } from '@tauri-apps/api/core';
export interface FileEntry {
name: string;
path: string;
is_dir: boolean;
size: number;
}
export async function listFiles(dir: string): Promise<FileEntry[]> {
return invoke<FileEntry[]>('list_files', { dir });
}
export async function saveFile(path: string, content: string): Promise<void> {
await invoke('save_file', { path, content });
}
export async function readFile(path: string): Promise<string> {
return invoke<string>('read_file', { path });
}
export async function getDataDir(): Promise<string> {
return invoke<string>('get_data_dir');
}
Demandez à Cursor de générer des interfaces TypeScript qui reflètent vos structures Rust. Une invite comme « Créez des types TypeScript pour chaque structure dans mes gestionnaires de commandes Tauri et placez-les dans un fichier de types partagés » garantit que le frontend et le backend restent synchronisés, détectant les inadéquations de type à la compilation.
Le type Result de Rust correspond directement au rejet de Promise JavaScript. Si votre commande renvoie Result<T, String>, le frontend reçoit une promesse rejetée avec la chaîne d'erreur. Gérez toujours les erreurs avec .catch() ou try/catch dans le frontend pour éviter les rejets de promesses non gérés.
Le système de plugins
Tauri 2.0 utilise une architecture de plugins modulaire. Les fonctionnalités de base comme l'accès au système de fichiers, les boîtes de dialogue, les commandes shell et les requêtes HTTP sont fournies par des plugins officiels. Vous pouvez également créer des plugins personnalisés pour partager des fonctionnalités entre les applications.
Installation des plugins officiels
# File system access
npm install @tauri-apps/plugin-fs
cargo add tauri-plugin-fs --manifest-path src-tauri/Cargo.toml
# Native dialogs (open, save)
npm install @tauri-apps/plugin-dialog
cargo add tauri-plugin-dialog --manifest-path src-tauri/Cargo.toml
# Shell commands (open URLs, run scripts)
npm install @tauri-apps/plugin-shell
cargo add tauri-plugin-shell --manifest-path src-tauri/Cargo.toml
# HTTP client
npm install @tauri-apps/plugin-http
cargo add tauri-plugin-http --manifest-path src-tauri/Cargo.toml
Chaque plugin doit être enregistré dans le builder Tauri et se voir accorder des permissions via un fichier de capacités.
Utilisation du plugin de boîte de dialogue
// src/components/FileBrowser.tsx
import { open, save } from '@tauri-apps/plugin-dialog';
import { readFile, writeFile } from '@tauri-apps/plugin-fs';
import { useState } from 'react';
export function FileBrowser() {
const [content, setContent] = useState('');
const [filePath, setFilePath] = useState<string | null>(null);
async function handleOpen() {
const selected = await open({
multiple: false,
filters: [{ name: 'Markdown', extensions: ['md'] }],
});
if (typeof selected === 'string') {
setFilePath(selected);
const bytes = await readFile(selected);
setContent(new TextDecoder().decode(bytes));
}
}
async function handleSave() {
if (!filePath) return;
await writeFile(filePath, new TextEncoder().encode(content));
}
async function handleSaveAs() {
const target = await save({
filters: [{ name: 'Markdown', extensions: ['md'] }],
});
if (target) {
setFilePath(target);
await writeFile(target, new TextEncoder().encode(content));
}
}
return (
<div className="flex flex-col h-full">
<div className="flex gap-2 p-2">
<button onClick={handleOpen} className="rounded px-3 py-1 bg-blue-600 text-white">
Open
</button>
<button onClick={handleSave} className="rounded px-3 py-1 bg-green-600 text-white">
Save
</button>
<button onClick={handleSaveAs} className="rounded px-3 py-1 bg-gray-600 text-white">
Save As
</button>
</div>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
className="flex-1 p-4 font-mono text-sm"
placeholder="Select a file or start typing..."
/>
</div>
);
}
Demandez à Cursor d'échafauder un plugin Tauri personnalisé. Une invite comme « Créez un plugin Tauri qui expose une fonction Rust pour lire les variables d'environnement système et enregistrez-le dans mon application » démontre comment structurer le code de plugin, enregistrer les permissions et exposer l'API au frontend.
Le modèle de sécurité
Tauri 2.0 a introduit un système de permissions fine basé sur les capacités. Chaque commande, API de plugin et point d'accès aux ressources doit être explicitement autorisé dans un fichier de capacités.
Configuration des capacités
// src-tauri/capabilities/default.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"fs:allow-read-text-file",
"fs:allow-write-file",
"fs:allow-create-dir",
"dialog:allow-open",
"dialog:allow-save",
"shell:allow-open",
"http:default"
]
}
Délimitation de l'accès au système de fichiers
Vous pouvez restreindre les répertoires auxquels un plugin peut accéder en utilisant des règles de scope :
{
"identifier": "default",
"windows": ["main"],
"permissions": [
{
"identifier": "fs:allow-read-file",
"allow": [
{ "path": "$APPDATA/**" },
{ "path": "$DOCUMENT/**" }
],
"deny": [
{ "path": "$DOCUMENT/.ssh/**" }
]
}
]
}
N'utilisez jamais fs:default ou fs:allow-read-file sans scope. Ces permissions larges accordent l'accès à l'ensemble du système de fichiers depuis le WebView, ce qui vainc le but de l'architecture sandboxée de Tauri. Limitez toujours l'accès à des répertoires spécifiques comme $APPDATA ou des chemins sélectionnés par l'utilisateur.
Exécutez cette invite dans Cursor pour auditer votre configuration de sécurité Tauri : « Révisez mes fichiers de capacités et Cargo.toml pour les permissions trop larges. Signalez toute permission qui accorde l'accès au système de fichiers, au shell ou HTTP sans scope explicite. »
Événements et communication inter-processus
En plus des commandes, Tauri prend en charge un système d'événements pour la communication asynchrone du backend Rust vers le frontend.
Émission d'événements depuis Rust
use tauri::{Emitter, Window};
use std::time::Duration;
use tokio::time::interval;
/// Background task that emits progress updates
#[tauri::command]
pub async fn start_processing(window: Window) -> Result<(), String> {
let mut ticker = interval(Duration::from_millis(500));
for step in 0..=100 {
ticker.tick().await;
window
.emit("processing-progress", serde_json::json!({
"step": step,
"total": 100,
"message": format!("Processing item {} of 100", step),
}))
.map_err(|e| e.to_string())?;
if step == 50 {
window
.emit("processing-warning", "Halfway through, memory is high")
.map_err(|e| e.to_string())?;
}
}
window
.emit("processing-complete", "All items processed successfully")
.map_err(|e| e.to_string())?;
Ok(())
}
Écoute des événements dans le frontend
// src/components/ProcessingView.tsx
import { listen } from '@tauri-apps/api/event';
import { useEffect, useState } from 'react';
interface ProgressEvent {
step: number;
total: number;
message: string;
}
export function ProcessingView() {
const [progress, setProgress] = useState<ProgressEvent | null>(null);
const [warning, setWarning] = useState<string | null>(null);
const [done, setDone] = useState(false);
useEffect(() => {
const unlisteners: Array<() => void> = [];
const setup = async () => {
unlisteners.push(
await listen<ProgressEvent>('processing-progress', (event) => {
setProgress(event.payload);
})
);
unlisteners.push(
await listen<string>('processing-warning', (event) => {
setWarning(event.payload);
})
);
unlisteners.push(
await listen<string>('processing-complete', (event) => {
setDone(true);
})
);
};
setup();
return () => {
unlisteners.forEach((unlisten) => unlisten());
};
}, []);
return (
<div className="p-4">
{progress && (
<>
<div className="mb-2 text-sm text-gray-600">{progress.message}</div>
<div className="h-3 rounded-full bg-gray-200">
<div
className="h-3 rounded-full bg-blue-600 transition-all"
style={{ width: `${(progress.step / progress.total) * 100}%` }}
/>
</div>
</>
)}
{warning && (
<div className="mt-2 rounded bg-yellow-100 p-2 text-sm text-yellow-800">
{warning}
</div>
)}
{done && (
<div className="mt-4 rounded bg-green-100 p-2 text-sm text-green-800">
Processing complete!
</div>
)}
</div>
);
}
Nettoyez toujours les écouteurs d'événements dans la fonction de nettoyage useEffect de React. Les écouteurs d'événements Tauri persistent à travers les remontages de composants. Ne pas se désabonner cause des gestionnaires en double qui se déclenchent plusieurs fois et créent des fuites de mémoire.
Intégration de bureau assistée par IA
Cursor peut générer des fonctionnalités complètes d'intégration de bureau en décrivant le comportement souhaité. Voici des exemples pratiques.
Intégration de la barre système
Create a Tauri system tray icon with a context menu containing
"Show Window", "Settings", and "Quit". The tray icon should show
a notification badge when there are unread items. Write the Rust
code in src-tauri/src/tray.rs and register it in the builder.
Cursor génère :
// src-tauri/src/tray.rs
use tauri::{
menu::{Menu, MenuItem, PredefinedMenuItem},
tray::{TrayIconBuilder, TrayIconEvent, MouseButton, MouseButtonState},
Manager, Emitter,
};
pub fn setup_tray(app: &tauri::App) -> tauri::Result<()> {
let show = MenuItem::with_id(app, "show", "Show Window", true, None)?;
let settings = MenuItem::with_id(app, "settings", "Settings", true, None)?;
let separator = PredefinedMenuItem::separator(app)?;
let quit = MenuItem::with_id(app, "quit", "Quit", true, None)?;
let menu = Menu::with_items(app, &[&show, &settings, &separator, &quit])?;
TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.tooltip("My Tauri App")
.menu(&menu)
.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();
}
}
"settings" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.emit("navigate", "/settings");
let _ = window.show();
let _ = window.set_focus();
}
}
"quit" => {
app.exit(0);
}
_ => {}
}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
})
.build(app)?;
Ok(())
}
Les icônes de barre système se comportent différemment selon les plateformes. Sous macOS, cliquer sur l'icône de la barre devrait basculer la visibilité de la fenêtre. Sous Windows et Linux, le clic droit ouvre le menu contextuel. Demandez à Cursor de générer des gestionnaires spécifiques à la plateforme et testez sur toutes les plateformes cibles avant de publier.
Construction et distribution
Tauri construit des binaires natifs pour chaque plateforme sans dépendances de runtime externes.
Commandes de construction
# Build for the current platform
npm run tauri build
# Build with a specific bundle type
npm run tauri build -- --bundles dmg # macOS DMG
npm run tauri build -- --bundles nsis # Windows NSIS installer
npm run tauri build -- --bundles deb # Linux DEB
# Debug build with devtools
npm run tauri build -- --debug
Configuration
// src-tauri/tauri.conf.json
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "My Tauri App",
"version": "1.0.0",
"identifier": "com.example.my-tauri-app",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:1420",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build"
},
"app": {
"windows": [
{
"title": "My Tauri App",
"width": 1200,
"height": 800,
"minWidth": 800,
"minHeight": 600,
"resizable": true,
"center": true,
"decorations": true
}
],
"security": {
"csp": "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'",
"freezePrototype": true
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
Demandez à Cursor de générer un flux de travail CI GitHub Actions qui construit votre application Tauri pour macOS, Windows et Ubuntu. Une invite comme « Créez un flux de travail CI qui installe Rust, Node et les dépendances système pour Tauri, puis exécute tauri build sur trois plateformes et télécharge les bundles comme assets de release » vous donne un pipeline prêt pour la production.
Résumé
Tauri dans Cursor vous offre le meilleur des deux mondes : un core Rust petit et sécurisé avec un frontend web familier. En suivant les modèles de ce guide — commandes Rust typées, permissions de plugins délimitées, sécurité basée sur les capacités, communication par événements et intégration de bureau sensible à la plateforme — vous pouvez livrer des applications de bureau légères qui sont d'un ordre de grandeur plus petites que les équivalents Electron. Gardez vos fichiers de capacités strictement délimités, laissez Cursor générer et auditer vos commandes Rust, et testez toujours les fonctionnalités spécifiques à la plateforme sur chaque système d'exploitation cible.