Overview
Tauri commands are the IPC (inter-process communication) layer between the React frontend and Rust backend. All commands are:
- Typed on both sides (Rust + TypeScript)
- Async by default
- Serialized via JSON (serde)
Command Flow
sequenceDiagram
participant FE as Frontend (React)
participant IPC as Tauri IPC
participant CMD as Rust Command
participant FS as Filesystem
FE->>IPC: invoke('space_read_text', { path })
IPC->>CMD: Deserialize args
CMD->>FS: Read file
FS->>CMD: File contents
CMD->>IPC: Serialize result
IPC->>FE: Return TextFileDocDefining Commands
Step 1: Implement Rust Command
use tauri::State;
use crate::space::SpaceState;
#[derive(serde::Serialize)]
pub struct TextFileDoc {
pub rel_path: String,
pub text: String,
pub etag: String,
pub mtime_ms: u64,
}
#[tauri::command]
pub fn space_read_text(
path: String,
state: State<SpaceState>,
) -> Result<TextFileDoc, String> {
let current = state.current.lock().unwrap();
let space = current.as_ref().ok_or("No space open")?;
let abs_path = paths::join_under(&space.root, &path)
.map_err(|e| format!("Invalid path: {}", e))?;
let text = fs::read_to_string(&abs_path)
.map_err(|e| format!("Failed to read: {}", e))?;
let metadata = fs::metadata(&abs_path)?;
let mtime_ms = metadata.modified()?
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let etag = format!("{}-{}", mtime_ms, metadata.len());
Ok(TextFileDoc {
rel_path: path,
text,
etag,
mtime_ms,
})
}Step 2: Register in lib.rs
.invoke_handler(tauri::generate_handler![
space_read_text,
space_write_text,
space_list_dir,
// ... other commands
])Step 3: Add TypeScript Types
export interface TextFileDoc {
rel_path: string;
text: string;
etag: string;
mtime_ms: number;
}
interface TauriCommands {
space_read_text: CommandDef<{ path: string }, TextFileDoc>;
}Step 4: Invoke from Frontend
import { invoke } from '@/lib/tauri';
const doc = await invoke('space_read_text', {
path: 'notes/example.md'
});
console.log(doc.text); // File contents
console.log(doc.etag); // ETag for cachingCommand Categories
Space Lifecycle
space_create { path: string } → SpaceInfoCreates a new space at the given path
const info = await invoke('space_create', {
path: '/Users/me/my-space'
});
// Returns: { root: '/Users/me/my-space', schema_version: 1 }space_open { path: string } → SpaceInfoOpens an existing space
const info = await invoke('space_open', {
path: '/Users/me/my-space'
});space_get_current void → string | nullReturns current space path or null
const path = await invoke('space_get_current');
// Returns: '/Users/me/my-space' or nullspace_close void → voidCloses the current space
await invoke('space_close');File System Operations
space_list_dir { dir?: string | null } → FsEntry[]Lists files and directories
const entries = await invoke('space_list_dir', {
dir: 'notes/projects' // Optional, defaults to root
});
// Returns: [{ name: 'file.md', rel_path: 'notes/projects/file.md', kind: 'file', is_markdown: true }]space_read_text { path: string } → TextFileDocReads a text file with metadata
const doc = await invoke('space_read_text', {
path: 'notes/example.md'
});
// Returns: { rel_path, text, etag, mtime_ms }space_write_text { path: string, text: string, base_mtime_ms?: number } → TextFileWriteResultWrites a text file atomically
const result = await invoke('space_write_text', {
path: 'notes/example.md',
text: '# Hello World',
base_mtime_ms: doc.mtime_ms // Optional: detect conflicts
});
// Returns: { etag: '...', mtime_ms: 1234567890 }space_create_dir { path: string } → voidCreates a directory
await invoke('space_create_dir', {
path: 'notes/new-folder'
});space_rename_path { from_path: string, to_path: string } → voidRenames/moves a file or directory
await invoke('space_rename_path', {
from_path: 'notes/old.md',
to_path: 'notes/new.md'
});space_delete_path { path: string, recursive?: boolean } → voidDeletes a file or directory
await invoke('space_delete_path', {
path: 'notes/old-folder',
recursive: true
});Search & Index
index_rebuild void → IndexRebuildResultRebuilds the SQLite full-text search index
const result = await invoke('index_rebuild');
// Returns: { indexed: 1234 }search { query: string } → SearchResult[]Full-text search across all notes
const results = await invoke('search', {
query: 'machine learning'
});
// Returns: [{ id: 'notes/ml.md', title: 'ML Notes', snippet: '...', score: 0.95 }]search_advanced { request: SearchAdvancedRequest } → SearchResult[]Advanced search with filters
const results = await invoke('search_advanced', {
request: {
query: 'AI',
tags: ['research', 'paper'],
title_only: false,
limit: 50
}
});tags_list { limit?: number } → TagCount[]Lists all tags with usage counts
const tags = await invoke('tags_list', { limit: 100 });
// Returns: [{ tag: 'research', count: 42 }, { tag: 'project', count: 18 }]backlinks { note_id: string } → BacklinkItem[]Finds notes linking to a given note
const links = await invoke('backlinks', {
note_id: 'notes/example.md'
});
// Returns: [{ id: 'notes/other.md', title: 'Other Note', updated: '2024-03-15T10:30:00Z' }]AI Commands
ai_chat_start { request: AiChatStartRequest } → AiChatStartResultStarts an AI chat conversation
const result = await invoke('ai_chat_start', {
request: {
profile_id: 'openai-gpt4',
messages: [
{ role: 'user', content: 'Explain quantum computing' }
],
mode: 'chat',
context: '# Research Notes\n...',
audit: true
}
});
// Returns: { job_id: 'abc123' }ai_profiles_list void → AiProfile[]Lists configured AI provider profiles
const profiles = await invoke('ai_profiles_list');
// Returns: [{ id: 'openai', name: 'OpenAI GPT-4', provider: 'openai', model: 'gpt-4', ... }]ai_models_list { profile_id: string } → AiModel[]Lists available models for a provider
const models = await invoke('ai_models_list', {
profile_id: 'openai'
});
// Returns: [{ id: 'gpt-4', name: 'GPT-4', context_length: 8192, ... }]Tasks
tasks_query { bucket: TaskBucket, today: string, limit?: number, folders?: string[] } → TaskItem[]Queries tasks by bucket (inbox, today, upcoming)
const tasks = await invoke('tasks_query', {
bucket: 'today',
today: '2024-03-15',
limit: 100,
folders: ['notes/projects']
});
// Returns: [{ task_id: '...', note_title: 'Project X', raw_text: '- [ ] Task', ... }]task_set_checked { task_id: string, checked: boolean } → voidToggles task completion
await invoke('task_set_checked', {
task_id: 'task-abc123',
checked: true
});Error Handling
Rust Side
Always return Result<T, String>:
#[tauri::command]
pub fn risky_operation(path: String) -> Result<String, String> {
if path.is_empty() {
return Err("Path cannot be empty".to_string());
}
let contents = fs::read_to_string(&path)
.map_err(|e| format!("Failed to read {}: {}", path, e))?;
Ok(contents)
}Frontend Side
Use try/catch with TauriInvokeError:
import { invoke, TauriInvokeError } from '@/lib/tauri';
try {
const result = await invoke('risky_operation', { path: '' });
} catch (err) {
if (err instanceof TauriInvokeError) {
console.error('Command failed:', err.message);
console.error('Raw error:', err.raw);
}
}State Management
Tauri State
Global state accessible to all commands:
use tauri::Manager;
pub fn run() {
tauri::Builder::default()
.manage(SpaceState {
current: Mutex::new(None),
})
.invoke_handler(tauri::generate_handler![...])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Access in commands:
#[tauri::command]
fn my_command(state: State<SpaceState>) -> Result<String, String> {
let current = state.current.lock().unwrap();
let space = current.as_ref().ok_or("No space open")?;
Ok(space.root.display().to_string())
}Type Safety
Enforcing Type Consistency
The TauriCommands interface ensures TypeScript types match Rust:
type CommandDef<Args, Result> = { args: Args; result: Result };
interface TauriCommands {
space_read_text: CommandDef<{ path: string }, TextFileDoc>;
// ^──────────────^───────────^
// Args match Rust Result matches Rust
}The invoke() helper enforces these types:
export async function invoke<K extends keyof TauriCommands>(
command: K,
...args: ArgsTuple<K>
): Promise<TauriCommands[K]['result']> {
// Implementation
}TypeScript will error if you:
- Pass wrong argument types
- Forget required arguments
- Expect wrong return type
Performance Tips
Batch Operations
Instead of N individual calls:
for (const path of paths) {
const doc = await invoke('space_read_text', { path });
// Process doc...
}Use batch command:
const docs = await invoke('space_read_texts_batch', { paths });
// Process all docs...Streaming Large Data
For large results, use events instead of return values:
use tauri::Emitter;
#[tauri::command]
pub fn large_operation(app: tauri::AppHandle) -> Result<(), String> {
for chunk in get_large_data() {
app.emit("data-chunk", &chunk)?;
}
Ok(())
}import { listen } from '@tauri-apps/api/event';
const unlisten = await listen<string>('data-chunk', (event) => {
console.log('Received chunk:', event.payload);
});
await invoke('large_operation');
unlisten();