Get Glyph
Warning This documentation is still a work in progress. Some details may be out of date depending on the version of Glyph you are using, but it is being actively reviewed and improved.
Documentation AI Assistant Development Licensing

Documentation

Tauri Commands

IPC communication between frontend and backend

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 TextFileDoc

Defining 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 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 }
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 } → 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:

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();

Next Steps