Tauri Desktop-App-Entwicklung in Cursor: Rust-Backend, Commands und Sicherheit

Tauri ist ein Framework zum Erstellen kleiner, schneller und sicherer Desktop-Anwendungen durch die Kombination eines Rust-Backends mit einem Web-Frontend Ihrer Wahl. Im Gegensatz zu Electron verwendet Tauri die native Webview des Betriebssystems anstelle eines gebündelten Chromium, was zu deutlich kleineren Binärdateien und geringerem Speicherverbrauch führt. Cursor, ein KI-gestützter IDE-Editor, beschleunigt die Tauri-Entwicklung durch Projekt-Scaffolding, Generierung von Rust-Commands, Schreiben von typsicheren TypeScript-Bindings und Prüfung Ihrer Sicherheitskonfiguration. Dieser Leitfaden behandelt die Tauri-Architektur, Rust-Commands, das Plugin-System, das Sicherheitsmodell und KI-gestützte Desktop-Integration.
Voraussetzungen
Stellen Sie vor dem Start sicher, dass Sie Folgendes haben:
- Cursor (aktuellste Version empfohlen) als IDE-Editor
- Rust-Toolchain (Installation über rustup) mit dem Stable-Compiler
- Node.js 18 oder höher für das Frontend-Tooling
- Systemabhängigkeiten: Installieren Sie unter Linux
webkit2gtk,build-essentialund verwandte Pakete; unter macOS installieren Sie Xcode Command Line Tools; unter Windows installieren Sie die Visual Studio C++ Build Tools
Wenn Sie noch nie Rust geschrieben haben, bitten Sie Cursor, Ownership, Borrowing und Lifetimes anhand von Beispielen aus Tauri-Command-Handlern zu erklären. Das frühe Verständnis dieser Konzepte verhindert Compiler-Fehler, die für JavaScript-Entwickler, die zu Rust wechseln, kryptisch wirken können.
Die Tauri-Architektur verstehen
Eine Tauri-Anwendung hat drei Schichten:
- Frontend: Ihre Web-Anwendung (React, Vue, Svelte oder reines HTML). Diese läuft in der betriebssystemnativen Webview.
- Core (Rust): Die Tauri-Laufzeit, geschrieben in Rust, verwaltet Fenster, den Anwendungslebenszyklus und native Systemaufrufe.
- Commands: Benutzerdefinierte Rust-Funktionen, die das Frontend über Tauris IPC-Schicht aufruft. So interagiert Ihre Web-UI mit dem Dateisystem, Datenbanken und OS-APIs.
Der wesentliche Unterschied zu Electron besteht darin, dass der Rust-Core vollen Systemzugriff hat, während das Frontend in der Webview sandboxed ist. Die Kommunikation erfolgt ausschließlich über explizit deklarierte Commands und Events.
Projekt-Scaffolding in Cursor
Bitten Sie Cursor, ein Tauri + React + TypeScript-Projekt zu scaffolden:
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 generiert die Standard-Tauri-Projektstruktur:
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 brachte bedeutende Änderungen gegenüber Tauri 1.x mit sich, einschließlich eines neuen Berechtigungssystems, Capabilities-Dateien und einer neu gestalteten Plugin-API. Wenn Sie älteren Tutorials folgen, überprüfen Sie, ob die API-Aufrufe zur Tauri 2.0-Syntax passen. Bitten Sie Cursor, jeglichen Tauri 1.x-Code zu migrieren, den Sie finden.
Rust-Backend und Commands
Commands sind die Brücke zwischen Ihrem Rust-Backend und Ihrem Web-Frontend. Sie definieren eine Rust-Funktion, annotieren sie mit #[tauri::command] und rufen sie aus JavaScript auf.
Commands definieren
// 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");
}
Commands vom Frontend aufrufen
// 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');
}
Bitten Sie Cursor, TypeScript-Interfaces zu generieren, die Ihre Rust-Structs spiegeln. Ein Prompt wie „Erstelle TypeScript-Typen für jedes Struct in meinen Tauri-Command-Handlern und lege sie in einer gemeinsamen Typdatei ab" stellt sicher, dass Frontend und Backend synchron bleiben und Typmissverhältnisse zur Kompilierzeit erkannt werden.
Rusts Result-Typ wird direkt zu JavaScript-Promise-Ablehnung (Rejection) zugeordnet. Wenn Ihr Command Result<T, String> zurückgibt, empfängt das Frontend eine abgelehnte Promise mit dem Fehler-String. Behandeln Sie Fehler immer mit .catch() oder try/catch im Frontend, um unbehandelte Promise-Rejections zu vermeiden.
Das Plugin-System
Tauri 2.0 verwendet eine modulare Plugin-Architektur. Kernfunktionalität wie Dateisystemzugriff, Dialoge, Shell-Befehle und HTTP-Anfragen werden durch offizielle Plugins bereitgestellt. Sie können auch benutzerdefinierte Plugins erstellen, um Funktionalität über mehrere Apps hinweg zu teilen.
Offizielle Plugins installieren
# 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
Jedes Plugin muss im Tauri-Builder registriert und über eine Capabilities-Datei mit Berechtigungen versehen werden.
Verwendung des Dialog-Plugins
// 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>
);
}
Bitten Sie Cursor, ein benutzerdefiniertes Tauri-Plugin zu scaffolden. Ein Prompt wie „Erstelle ein Tauri-Plugin, das eine Rust-Funktion zum Lesen von System-Umgebungsvariablen verfügbar macht und in meiner App registriert" zeigt, wie man Plugin-Code strukturiert, Berechtigungen registriert und die API dem Frontend zugänglich macht.
Das Sicherheitsmodell
Tauri 2.0 führte ein feingranulares Berechtigungssystem basierend auf Capabilities ein. Jeder Command, jede Plugin-API und jeder Ressourcenzugriffspunkt muss explizit in einer Capabilities-Datei erlaubt werden.
Capabilities-Konfiguration
// 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"
]
}
Dateisystemzugriff einschränken
Sie können einschränken, auf welche Verzeichnisse ein Plugin zugreifen kann, indem Sie Scope-Regeln verwenden:
{
"identifier": "default",
"windows": ["main"],
"permissions": [
{
"identifier": "fs:allow-read-file",
"allow": [
{ "path": "$APPDATA/**" },
{ "path": "$DOCUMENT/**" }
],
"deny": [
{ "path": "$DOCUMENT/.ssh/**" }
]
}
]
}
Verwenden Sie niemals fs:default oder fs:allow-read-file ohne Scope. Diese breiten Berechtigungen gewähren Zugriff auf das gesamte Dateisystem aus der Webview, was den Zweck von Tauris sandboxed-Architektur zunichte macht. Schränken Sie den Zugriff immer auf spezifische Verzeichnisse wie $APPDATA oder vom Benutzer ausgewählte Pfade ein.
Führen Sie diesen Prompt in Cursor aus, um Ihre Tauri-Sicherheitskonfiguration zu prüfen: „Überprüfe meine Capabilities-Dateien und Cargo.toml auf zu breite Berechtigungen. Markiere jede Berechtigung, die Dateisystem-, Shell- oder HTTP-Zugriff ohne expliziten Scope gewährt."
Events und Inter-Prozess-Kommunikation
Zusätzlich zu Commands unterstützt Tauri ein Event-System für asynchrone Kommunikation vom Rust-Backend zum Frontend.
Events aus Rust emittieren
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(())
}
Events im Frontend abhören
// 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>
);
}
Bereinigen Sie Event-Listener immer in der Cleanup-Funktion Ihres React-useEffect. Tauri-Event-Listener bleiben über Komponenten-Remounts hinweg bestehen. Das Nicht-Abbestellen verursacht doppelte Handler, die mehrfach feuern und Speicherlecks erzeugen.
KI-gestützte Desktop-Integration
Cursor kann vollständige Desktop-Integrationsfunktionen generieren, indem Sie das gewünschte Verhalten beschreiben. Hier sind praktische Beispiele.
System-Tray-Integration
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 generiert:
// 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(())
}
System-Tray-Icons verhalten sich plattformübergreifend unterschiedlich. Unter macOS sollte ein Klick auf das Tray-Icon die Fenster-Sichtbarkeit umschalten. Unter Windows und Linux öffnet ein Rechtsklick das Kontextmenü. Bitten Sie Cursor, plattformspezifische Handler zu generieren und testen Sie auf allen Zielplattformen vor dem Release.
Bauen und Verteilen
Tauri erstellt native Binärdateien für jede Plattform ohne externe Laufzeitabhängigkeiten.
Build-Befehle
# 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
Konfiguration
// 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"
]
}
}
Bitten Sie Cursor, einen GitHub Actions CI-Workflow zu generieren, der Ihre Tauri-App für macOS, Windows und Ubuntu baut. Ein Prompt wie „Erstelle einen CI-Workflow, der Rust, Node und Systemabhängigkeiten für Tauri installiert, dann tauri build auf drei Plattformen ausführt und die Bundles als Release-Assets hochlädt" gibt Ihnen eine produktionsbereite Pipeline.
Zusammenfassung
Tauri in Cursor gibt Ihnen das Beste aus beiden Welten: einen kleinen, sicheren Rust-Core mit einem vertrauten Web-Frontend. Mit den Mustern dieses Leitfadens — typisierte Rust-Commands, eingeschränkte Plugin-Berechtigungen, Capability-basierte Sicherheit, Event-gesteuerte Kommunikation und plattformbewusste Desktop-Integration — können Sie schlanke Desktop-Anwendungen ausliefern, die um eine Größenordnung kleiner sind als Electron-Äquivalente. Halten Sie Ihre Capabilities-Dateien streng eingeschränkt, lassen Sie Cursor Ihre Rust-Commands generieren und prüfen, und testen Sie plattformspezifische Funktionen immer auf jedem Ziel-Betriebssystem.