# Tauri Syntax Commands

> >

- **Type:** Skill
- **Install:** `agentstack add skill-impertio-studio-tauri-2-claude-skill-package-tauri-syntax-commands`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Impertio-Studio](https://agentstack.voostack.com/s/impertio-studio)
- **Installs:** 0
- **Category:** [AI & ML](https://agentstack.voostack.com/c/ai-and-ml)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Impertio-Studio](https://github.com/Impertio-Studio)
- **Source:** https://github.com/Impertio-Studio/Tauri-2-Claude-Skill-Package/tree/main/skills/source/tauri-syntax/tauri-syntax-commands
- **Website:** https://github.com/OpenAEC-Foundation

## Install

```sh
agentstack add skill-impertio-studio-tauri-2-claude-skill-package-tauri-syntax-commands
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# tauri-syntax-commands

## Quick Reference

### Command Definition (Rust)

| Pattern | Syntax | Notes |
|---------|--------|-------|
| Sync command | `#[tauri::command] fn name() -> T` | Runs on main thread |
| Async command | `#[tauri::command] async fn name() -> T` | Spawned on tokio runtime |
| Forced async | `#[tauri::command(async)] fn name() -> T` | Sync function on tokio runtime |
| Renamed args | `#[tauri::command(rename_all = "snake_case")]` | JS passes snake_case keys |

### Argument Types (Rust ` | `T[]` | Array to Vec |
| `Option` | `T \| null \| undefined` | Nullable |
| `PathBuf` | `string` | Path string |
| Custom struct | object | Must `#[derive(Deserialize)]` |

### Return Types (Rust -> JS)

| Rust Return | JS Result | Notes |
|-------------|-----------|-------|
| `()` | `void` / `null` | No return value |
| `T` (Serialize) | `T` | Direct serialization |
| `Result` | `T` or throw `E` | E must impl `Serialize` |
| `Response` | `ArrayBuffer` | Binary data bypass |

### Special Injected Parameters (NOT passed from JS)

| Rust Parameter | Purpose | Example |
|----------------|---------|---------|
| `app: AppHandle` | Application handle | `app.emit("event", data)` |
| `window: WebviewWindow` | Calling window | `window.label()` |
| `state: State` | Managed state | `state.inner()` |
| `request: tauri::ipc::Request` | Raw IPC request | `request.headers()`, `request.body()` |

### Frontend invoke() API

| Feature | Syntax |
|---------|--------|
| Import | `import { invoke } from '@tauri-apps/api/core'` |
| Basic call | `await invoke('command_name')` |
| With args | `await invoke('cmd', { argName: value })` |
| Typed return | `await invoke('cmd')` |
| With headers | `await invoke('cmd', args, { headers: {...} })` |
| Binary data | `await invoke('cmd', new Uint8Array([...]))` |

### Critical Warnings

**NEVER** use snake_case keys in JS `invoke()` arguments -- Tauri auto-converts camelCase to snake_case. Passing `{ file_path: "x" }` will NOT match Rust parameter `file_path`.

**NEVER** forget to implement `Serialize` on error types -- `Result` requires E to implement `Serialize` to send errors to the frontend.

**NEVER** use `&str` parameters in async commands -- borrowed references cannot cross the async spawn boundary. Use `String` instead.

**NEVER** register commands with multiple `invoke_handler()` calls -- only the last one takes effect.

**NEVER** mark commands in `lib.rs` as `pub` -- glue code generation prevents it. Move to a separate module if `pub` is needed.

**ALWAYS** wrap invoke() calls in try/catch -- Rust `Result::Err` rejects the JS Promise.

**ALWAYS** use `Channel` for streaming data instead of multiple events -- channels are tied to the command invocation and type-safe.

**ALWAYS** derive `serde::Deserialize` on custom argument types and `serde::Serialize` on custom return types.

---

## Essential Patterns

### Pattern 1: Basic Command (Rust + TypeScript)

```rust
// src-tauri/src/commands.rs
#[tauri::command]
pub fn greet(name: String) -> String {
    format!("Hello, {}!", name)
}
```

```typescript
// src/main.ts
import { invoke } from '@tauri-apps/api/core';

const greeting = await invoke('greet', { name: 'World' });
console.log(greeting); // "Hello, World!"
```

### Pattern 2: Async Command with Error Handling

```rust
#[derive(Debug, thiserror::Error)]
enum Error {
    #[error(transparent)]
    Io(#[from] std::io::Error),
    #[error("not found: {0}")]
    NotFound(String),
}

// REQUIRED: Serialize impl for sending errors to frontend
impl serde::Serialize for Error {
    fn serialize(&self, serializer: S) -> Result
    where S: serde::ser::Serializer {
        serializer.serialize_str(self.to_string().as_ref())
    }
}

#[tauri::command]
async fn read_file(path: String) -> Result {
    let content = tokio::fs::read_to_string(&path).await?;
    Ok(content)
}
```

```typescript
try {
    const content = await invoke('read_file', { path: '/tmp/data.txt' });
    console.log(content);
} catch (error) {
    // error is the serialized string: "not found: ..." or "io error: ..."
    console.error('Failed:', error);
}
```

### Pattern 3: Structured Error Types

For richer error handling on the frontend, use tagged enum serialization.

```rust
#[derive(serde::Serialize)]
#[serde(tag = "kind", content = "message")]
#[serde(rename_all = "camelCase")]
enum AppError {
    Io(String),
    NotFound(String),
    Unauthorized(String),
}

#[tauri::command]
async fn load_data(id: u32) -> Result {
    if id == 0 {
        return Err(AppError::NotFound("Item not found".into()));
    }
    Ok(Data { /* ... */ })
}
```

```typescript
try {
    const data = await invoke('load_data', { id: 42 });
} catch (err: any) {
    // err = { kind: 'notFound', message: 'Item not found' }
    switch (err.kind) {
        case 'notFound':
            showNotFoundUI(err.message);
            break;
        case 'unauthorized':
            redirectToLogin();
            break;
        default:
            console.error('Unexpected error:', err);
    }
}
```

### Pattern 4: Special Injected Parameters

These parameters are injected by Tauri -- the frontend does NOT pass them.

```rust
use tauri::{AppHandle, State, WebviewWindow};

#[tauri::command]
async fn complex_command(
    app: AppHandle,                       // injected: app handle
    window: WebviewWindow,                // injected: calling window
    state: State,            // injected: managed state
    // Regular parameters (passed from JS):
    query: String,
    limit: Option,
) -> Result, String> {
    println!("Called from window: {}", window.label());
    let config = state.inner();
    // ... use app, window, state as needed
    Ok(vec![])
}
```

```typescript
// Frontend only passes the regular parameters -- injected params are omitted
const items = await invoke('complex_command', {
    query: 'search term',
    limit: 10,
});
```

### Pattern 5: Channel Streaming (Rust -> JS)

```rust
use tauri::ipc::Channel;

#[derive(Clone, serde::Serialize)]
struct DownloadProgress {
    bytes_read: u64,
    total: u64,
    percent: f64,
}

#[tauri::command]
async fn download_file(
    url: String,
    on_progress: Channel,
) -> Result {
    let response = reqwest::get(&url).await.map_err(|e| e.to_string())?;
    let total = response.content_length().unwrap_or(0);
    let mut bytes_read = 0u64;

    // Stream progress updates
    on_progress.send(DownloadProgress {
        bytes_read,
        total,
        percent: 0.0,
    }).unwrap();

    // ... read chunks, updating progress ...

    Ok("download complete".into())
}
```

```typescript
import { invoke, Channel } from '@tauri-apps/api/core';

interface DownloadProgress {
    bytesRead: number;
    total: number;
    percent: number;
}

const onProgress = new Channel();
onProgress.onmessage = (progress) => {
    updateProgressBar(progress.percent);
    console.log(`${progress.bytesRead}/${progress.total}`);
};

const result = await invoke('download_file', {
    url: 'https://example.com/file.zip',
    onProgress,  // Pass the channel as a regular argument
});
```

### Pattern 6: Binary Data with Request/Response

Bypass JSON serialization for large binary payloads.

```rust
use tauri::ipc::{Request, Response};

// Receiving binary data from JS
#[tauri::command]
fn upload(request: Request) -> Result {
    let tauri::ipc::InvokeBody::Raw(data) = request.body() else {
        return Err("Expected binary data".into());
    };
    let auth = request.headers().get("Authorization");
    println!("Received {} bytes", data.len());
    Ok(format!("Uploaded {} bytes", data.len()))
}

// Returning binary data to JS
#[tauri::command]
fn get_image(path: String) -> Response {
    let data = std::fs::read(path).unwrap();
    Response::new(data)
}
```

```typescript
// Sending binary data
const data = new Uint8Array([1, 2, 3, 4, 5]);
const result = await invoke('upload', data, {
    headers: { Authorization: 'Bearer token123' },
});

// Receiving binary data
const imageData = await invoke('get_image', { path: '/photo.jpg' });
```

### Pattern 7: Command Registration

```rust
// src-tauri/src/lib.rs
mod commands;

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            commands::greet,
            commands::read_file,
            commands::download_file,
            commands::upload,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

**Rule**: ALL commands MUST be listed in a single `generate_handler![]` macro call. Multiple `invoke_handler()` calls do NOT merge.

---

## Argument Name Mapping

Tauri automatically converts between JS camelCase and Rust snake_case.

| Rust Parameter | JS Argument Key |
|----------------|-----------------|
| `file_path: String` | `filePath` |
| `user_name: String` | `userName` |
| `item_count: u32` | `itemCount` |
| `is_active: bool` | `isActive` |

To override this behavior:

```rust
#[tauri::command(rename_all = "snake_case")]
fn my_command(file_path: String) { }
```

```typescript
// With rename_all = "snake_case", pass snake_case keys
await invoke('my_command', { file_path: '/tmp/file.txt' });
```

---

## Channel vs Events

| Feature | Channel | Events |
|---------|---------|--------|
| Direction | Rust -> JS (response stream) | Bidirectional |
| Lifetime | Tied to command invocation | App lifetime |
| Targeting | Specific caller | Broadcast or filtered |
| Type safety | Generic `Channel` | Serialized JSON |
| Use case | Progress, streaming data | App-wide notifications |

**ALWAYS** use `Channel` for progress updates and streaming data from a command. **NEVER** use events for command-specific streaming -- channels are scoped to the invocation.

---

## Runtime Generics (Plugin Authors)

When writing commands for plugins or libraries that need to be runtime-agnostic:

```rust
use tauri::{AppHandle, Runtime, WebviewWindow};

#[tauri::command]
async fn plugin_command(
    app: AppHandle,
    window: WebviewWindow,
    data: String,
) -> Result {
    Ok(format!("Processed: {}", data))
}
```

With the default `wry` feature, `R` resolves to `Wry`. This generic is only necessary for plugin/library code.

---

## Reference Links

- [references/methods.md](references/methods.md) -- Command macro attributes, invoke signature, Channel API
- [references/examples.md](references/examples.md) -- Sync commands, async commands, binary data, channels, errors
- [references/anti-patterns.md](references/anti-patterns.md) -- All command + invoke mistakes

### Official Sources

- https://v2.tauri.app/develop/calling-rust/
- https://docs.rs/tauri/latest/tauri/command/index.html
- https://docs.rs/tauri/latest/tauri/ipc/struct.Channel.html
- https://www.npmjs.com/package/@tauri-apps/api

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Impertio-Studio](https://github.com/Impertio-Studio)
- **Source:** [Impertio-Studio/Tauri-2-Claude-Skill-Package](https://github.com/Impertio-Studio/Tauri-2-Claude-Skill-Package)
- **License:** MIT
- **Homepage:** https://github.com/OpenAEC-Foundation

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-impertio-studio-tauri-2-claude-skill-package-tauri-syntax-commands
- Seller: https://agentstack.voostack.com/s/impertio-studio
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
