Skip to main content

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

Defining Commands

Step 1: Implement Rust Command

src-tauri/src/space_fs/read_write/text.rs
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

src-tauri/src/lib.rs
.invoke_handler(tauri::generate_handler![
    space_read_text,
    space_write_text,
    space_list_dir,
    // ... other commands
])

Step 3: Add TypeScript Types

src/lib/tauri.ts
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 caching

Command Categories

Space Lifecycle

space_create
{ path: string } → SpaceInfo
Creates 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 } → SpaceInfo
Opens an existing space
const info = await invoke('space_open', {
  path: '/Users/me/my-space'
});
space_get_current
void → string | null
Returns current space path or null
const path = await invoke('space_get_current');
// Returns: '/Users/me/my-space' or null
space_close
void → void
Closes 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 } → TextFileDoc
Reads 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 } → TextFileWriteResult
Writes 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 } → void
Creates a directory
await invoke('space_create_dir', {
  path: 'notes/new-folder'
});
space_rename_path
{ from_path: string, to_path: string } → void
Renames/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 } → void
Deletes a file or directory
await invoke('space_delete_path', {
  path: 'notes/old-folder',
  recursive: true
});

Search & Index

index_rebuild
void → IndexRebuildResult
Rebuilds the SQLite full-text search index
const result = await invoke('index_rebuild');
// Returns: { indexed: 1234 }
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 }]
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 } → AiChatStartResult
Starts 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 } → void
Toggles 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:
src-tauri/src/lib.rs
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:
src/lib/tauri.ts
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:
Bad
for (const path of paths) {
  const doc = await invoke('space_read_text', { path });
  // Process doc...
}
Use batch command:
Good
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();

Next Steps

Indexing

Learn about SQLite indexing

Storage

Content-addressed file storage