Zum Hauptinhalt springen

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

Tauri Desktop-App-Entwicklung in Cursor

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-essential und verwandte Pakete; unter macOS installieren Sie Xcode Command Line Tools; unter Windows installieren Sie die Visual Studio C++ Build Tools
tipp

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
warnung

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');
}
tipp

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.

warnung

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>
);
}
tipp

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/**" }
]
}
]
}
warnung

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.

tipp

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>
);
}
tipp

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(())
}
warnung

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"
]
}
}
tipp

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.