Skip to main content
tauri-app-dev Expert guidance for building cross-platform desktop applications with Tauri 2.0 and Rust. Use when developing Tauri apps including commands and IPC, file system operations, window management, state management, system tray, menus, plugin development, security configuration (capabilities/permissions), bundling/distribution, and auto-updates. Covers patterns for editor applications requiring file dialogs, native menus, and frontend-backend communication.
Jump to install Skills Marketplace Discover and explore AI skills built by the community.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Copy promptShow prompt details A direct command skips the review prompt. Inspect the source before running it.
npx skills add https://github.com/xiaolai/vmark --skill tauri-app-devThe command stays on one line. Scroll horizontally to inspect it before copying.
Prefer a local copy? Download the files currently available to SkillsMP.
Download Zip Downloading... More from this repository Build or update MCP server/client integrations for VMark. Use when configuring MCP servers, adding MCP tools, or updating MCP-related docs and settings.
E2E testing expert for Tauri applications using Tauri MCP server. Use when testing running Tauri apps - session management, webview interaction, IPC verification, screenshot capture, and debugging. ALWAYS use tauri_* tools, NEVER Chrome DevTools MCP for Tauri apps.
Translate VMark documentation and app strings to all 9 supported locales with subagent-driven audit, proofreading, and cultural polish. Use when adding or updating website pages, React locale JSON (src/locales/en/*.json), or Rust locale YAML (src-tauri/locales/en.yml) that need multi-language support.
Related occupations SOC
Based on SOC occupation classification
name tauri-app-dev description Expert guidance for building cross-platform desktop applications with Tauri 2.0 and Rust. Use when developing Tauri apps including commands and IPC, file system operations, window management, state management, system tray, menus, plugin development, security configuration (capabilities/permissions), bundling/distribution, and auto-updates. Covers patterns for editor applications requiring file dialogs, native menus, and frontend-backend communication.
Tauri 2.0 App Development
Tauri is a framework for building small, fast, secure desktop apps using web frontends and Rust backends.
Architecture Overview
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Frontend (Webview) โ
โ HTML/CSS/JS โข React/Vue/Svelte โ
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโ
โ IPC (invoke/events)
โโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Tauri Core (Rust) โ
โ Commands โข State โข Plugins โข Events โ
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TAO (windows) + WRY (webview)
โโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Operating System โ
โ macOS โข Windows โข Linux โข Mobile โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Project Structure
my-app/
โโโ src/ # Frontend source
โโโ src-tauri/
โ โโโ Cargo.toml # Rust dependencies
โ โโโ tauri.conf.json # Tauri configuration
โ โโโ capabilities/ # Security permissions (v2)
โ โ โโโ default.json
โ โโโ src/
โ โ โโโ main.rs # Desktop entry point
โ โ โโโ lib.rs # Main app logic + mobile entry
โ โโโ icons/
โโโ package.json
Commands (Frontend โ Rust)
Define commands in Rust with #[tauri::command]:
#[tauri::command]
fn greet (name: ) {
( , name)
}
(path: ) < , > {
std::fs:: (&path). (|e| e. ())
}
() {
tauri::Builder:: ()
. (tauri::generate_handler![greet, read_file])
. (tauri::generate_context!())
. ( );
}
String
->
String
format!
"Hello, {}!"
#[tauri::command]
async
fn
read_file
String
->
Result
String
String
read_to_string
map_err
to_string
pub
fn
run
default
invoke_handler
run
expect
"error running app"
Call from frontend (direct):
import { invoke } from '@tauri-apps/api/core' ;
const greeting = await invoke<string >('greet' , { name : 'World' });
const content = await invoke<string >('read_file' , { path : '/tmp/test.txt' });
Project convention: Wrap invoke() with TanStack Query for caching and state management:
import { useQuery, useMutation } from '@tanstack/react-query' ;
import { invoke } from '@tauri-apps/api/core' ;
const { data : content } = useQuery ({
queryKey : ['file' , path],
queryFn : () => invoke<string >('read_file' , { path }),
});
const { mutate : saveFile } = useMutation ({
mutationFn : (content : string ) => invoke ('write_file' , { path, content }),
});
Arguments must implement serde::Deserialize
Return types must implement serde::Serialize
Use Result<T, E> for fallible operations
Async commands run on thread pool (non-blocking)
Snake_case in Rust โ camelCase in JS arguments
State Management Share state across commands:
use std::sync::Mutex;
use tauri::State;
struct AppState {
counter: Mutex<i32 >,
db: Mutex<Option <Database>>,
}
#[tauri::command]
fn increment (state: State<'_ , AppState>) -> i32 {
let mut counter = state.counter.lock ().unwrap ();
*counter += 1 ;
*counter
}
pub fn run () {
tauri::Builder::default ()
.manage (AppState {
counter: Mutex::new (0 ),
db: Mutex::new (None ),
})
.invoke_handler (tauri::generate_handler![increment])
.run (tauri::generate_context!())
.expect ("error running app" );
}
Access via AppHandle (for background threads):
use tauri::Manager;
#[tauri::command]
async fn background_task (app: tauri::AppHandle) {
let state = app.state::<AppState>();
}
Events (Rust โ Frontend) use tauri::Emitter;
#[tauri::command]
fn start_process (app: tauri::AppHandle) {
std::thread::spawn (move || {
for i in 0 ..100 {
app.emit ("progress" , i).unwrap ();
std::thread::sleep (std::time::Duration::from_millis (50 ));
}
app.emit ("complete" , "Done!" ).unwrap ();
});
}
import { listen } from '@tauri-apps/api/event' ;
const unlisten = await listen<number >('progress' , (event ) => {
console .log (`Progress: ${event.payload} %` );
});
unlisten ();
Essential Plugins Install plugins: cargo add <plugin> in src-tauri, pnpm add <package> in frontend.
Plugin Cargo Crate NPM Package Purpose File System tauri-plugin-fs@tauri-apps/plugin-fsRead/write files Dialog tauri-plugin-dialog@tauri-apps/plugin-dialogOpen/save dialogs Clipboard tauri-plugin-clipboard-manager@tauri-apps/plugin-clipboard-managerCopy/paste Shell tauri-plugin-shell@tauri-apps/plugin-shellRun external commands Store tauri-plugin-store@tauri-apps/plugin-storeKey-value persistence Updater tauri-plugin-updater@tauri-apps/plugin-updaterAuto-updates
pub fn run () {
tauri::Builder::default ()
.plugin (tauri_plugin_fs::init ())
.plugin (tauri_plugin_dialog::init ())
.plugin (tauri_plugin_clipboard_manager::init ())
.run (tauri::generate_context!())
.expect ("error running app" );
}
Security: Capabilities & Permissions Tauri 2.0 uses capabilities (in src-tauri/capabilities/) to control what APIs each window can access.
src-tauri/capabilities/default.json:
{
"$schema" : "../gen/schemas/desktop-schema.json" ,
"identifier" : "main-capability" ,
"windows" : [ "main" ] ,
"permissions" : [
"core:default" ,
"fs:default" ,
"fs:allow-read-text-file" ,
"dialog:default" ,
{
"identifier" : "fs:scope" ,
"allow" : [ { "path" : "$APPDATA/**" } , { "path" : "$DOCUMENT/**" } ]
}
]
}
Scope variables: $APPDATA, $APPCONFIG, $DOCUMENT, $DOWNLOAD, $HOME, $TEMP, etc.
File Operations (Editor Pattern) import { open, save } from '@tauri-apps/plugin-dialog' ;
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs' ;
const path = await open ({
filters : [{ name : 'Markdown' , extensions : ['md' ] }],
multiple : false ,
});
if (path) {
const content = await readTextFile (path);
await writeTextFile (path, modifiedContent);
}
const savePath = await save ({
filters : [{ name : 'Markdown' , extensions : ['md' ] }],
defaultPath : 'untitled.md' ,
});
if (savePath) {
await writeTextFile (savePath, content);
}
Window Management Create windows at runtime:
use tauri::{WebviewUrl, WebviewWindowBuilder};
#[tauri::command]
async fn open_settings (app: tauri::AppHandle) -> Result <(), String > {
WebviewWindowBuilder::new (&app, "settings" , WebviewUrl::App ("settings.html" .into ()))
.title ("Settings" )
.inner_size (600.0 , 400.0 )
.build ()
.map_err (|e| e.to_string ())?;
Ok (())
}
Configure in tauri.conf.json:
{
"app" : {
"windows" : [
{
"label" : "main" ,
"title" : "My App" ,
"width" : 1200 ,
"height" : 800 ,
"decorations" : true ,
"resizable" : true
}
]
}
}
Custom Titlebar Set decorations: false in config, then:
<div data-tauri-drag-region class ="titlebar" >
<span > My App</span >
<button id ="minimize" > โ</button >
<button id ="maximize" > โก</button >
<button id ="close" > ร</button >
</div >
import { getCurrentWindow } from '@tauri-apps/api/window' ;
const appWindow = getCurrentWindow ();
document .getElementById ('minimize' )?.addEventListener ('click' , () => appWindow.minimize ());
document .getElementById ('maximize' )?.addEventListener ('click' , () => appWindow.toggleMaximize ());
document .getElementById ('close' )?.addEventListener ('click' , () => appWindow.close ());
Building & Distribution
pnpm tauri dev
pnpm tauri build
pnpm tauri build --target universal-apple-darwin
pnpm tauri build --bundles deb,appimage
pnpm tauri build --bundles nsis
macOS: target/release/bundle/macos/*.app, *.dmg
Windows: target/release/bundle/nsis/*-setup.exe, msi/*.msi
Linux: target/release/bundle/deb/*.deb, appimage/*.AppImage
Quick Reference
Test-Driven Development (TDD) CRITICAL: Always follow TDD - write tests BEFORE implementation.
TDD Workflow 1. RED โ Write failing test first
2. GREEN โ Write minimal code to pass
3. REFACTOR โ Clean up, keep tests green
Testing Stack Layer Tool Purpose Rust Unit cargo testTest commands, business logic React Unit Vitest Test components, hooks, stores Integration Vitest + MSW Test frontend with mocked IPC E2E Tauri MCP Test running app (NOT Chrome DevTools)
E2E Testing with Tauri MCP IMPORTANT: Always use tauri_* MCP tools for testing the running app. Do NOT use chrome-devtools MCP - it's for browser pages only.
tauri_driver_session ({ action : 'start' , port : 9223 })
tauri_webview_screenshot ()
tauri_webview_find_element ({ selector : '.editor-content' })
tauri_webview_interact ({ action : 'click' , selector : '#save-button' })
tauri_webview_keyboard ({ action : 'type' , selector : 'input' , text : 'hello' })
tauri_webview_wait_for ({ type : 'selector' , value : '.success-toast' })
tauri_ipc_monitor ({ action : 'start' })
tauri_ipc_get_captured ({ filter : 'save_file' })
tauri_ipc_execute_command ({ command : 'get_app_state' })
tauri_read_logs ({ source : 'console' , lines : 50 })
Rust Unit Tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_greet () {
let result = greet ("World" .to_string ());
assert_eq! (result, "Hello, World!" );
}
#[test]
fn test_parse_markdown () {
let input = "# Hello" ;
let result = parse_markdown (input);
assert! (result.is_ok ());
assert_eq! (result.unwrap ().title, "Hello" );
}
#[tokio::test]
async fn test_async_command () {
let result = read_file ("/tmp/test.txt" .to_string ()).await ;
}
}
Run: cd src-tauri && cargo test
React Component Tests (Vitest)
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Editor } from './Editor'
vi.mock ('@tauri-apps/api/core' , () => ({
invoke : vi.fn ()
}))
describe ('Editor' , () => {
it ('should render editor content' , () => {
render (<Editor initialValue ="# Hello" /> )
expect (screen.getByText ('Hello' )).toBeInTheDocument ()
})
it ('should call save on Ctrl+S' , async () => {
const { invoke } = await import ('@tauri-apps/api/core' )
render (<Editor initialValue ="test" /> )
await userEvent.keyboard ('{Control>}s{/Control}' )
expect (invoke).toHaveBeenCalledWith ('save_file' , expect.any (Object ))
})
})
Zustand Store Tests
import { describe, it, expect, beforeEach } from 'vitest'
import { useEditorStore } from './editorStore'
describe ('editorStore' , () => {
beforeEach (() => {
useEditorStore.setState ({
content : '' ,
isDirty : false ,
filePath : null
})
})
it ('should update content and mark dirty' , () => {
const { setContent } = useEditorStore.getState ()
setContent ('new content' )
const state = useEditorStore.getState ()
expect (state.content ).toBe ('new content' )
expect (state.isDirty ).toBe (true )
})
it ('should clear dirty flag after save' , () => {
useEditorStore.setState ({ isDirty : true })
const { markSaved } = useEditorStore.getState ()
markSaved ()
expect (useEditorStore.getState ().isDirty ).toBe (false )
})
})
Integration Tests with Mocked IPC
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient , QueryClientProvider } from '@tanstack/react-query'
import { useFileOperations } from './useFileOperations'
vi.mock ('@tauri-apps/api/core' , () => ({
invoke : vi.fn ()
}))
vi.mock ('@tauri-apps/plugin-dialog' , () => ({
open : vi.fn (),
save : vi.fn ()
}))
describe ('useFileOperations' , () => {
let queryClient : QueryClient
beforeEach (() => {
queryClient = new QueryClient ({
defaultOptions : { queries : { retry : false } }
})
vi.clearAllMocks ()
})
it ('should open file and load content' , async () => {
const { invoke } = await import ('@tauri-apps/api/core' )
const { open } = await import ('@tauri-apps/plugin-dialog' )
vi.mocked (open).mockResolvedValue ('/path/to/file.md' )
vi.mocked (invoke).mockResolvedValue ('# File Content' )
const { result } = renderHook (() => useFileOperations (), {
wrapper : ({ children } ) => (
<QueryClientProvider client ={queryClient} >
{children}
</QueryClientProvider >
)
})
await result.current .openFile ()
await waitFor (() => {
expect (invoke).toHaveBeenCalledWith ('read_file' , { path : '/path/to/file.md' })
})
})
})
TDD Example: Adding a New Feature
describe ('useWordCount' , () => {
it ('should count words in content' , () => {
const { result } = renderHook (() => useWordCount ('hello world' ))
expect (result.current .words ).toBe (2 )
})
it ('should handle empty content' , () => {
const { result } = renderHook (() => useWordCount ('' ))
expect (result.current .words ).toBe (0 )
})
it ('should count characters' , () => {
const { result } = renderHook (() => useWordCount ('hello' ))
expect (result.current .characters ).toBe (5 )
})
})
export function useWordCount (content : string ) {
return {
words : content.trim () ? content.trim ().split (/\s+/ ).length : 0 ,
characters : content.length
}
}
export function useWordCount (content : string ): WordCountResult {
return useMemo (() => ({
words : content.trim () ? content.trim ().split (/\s+/ ).length : 0 ,
characters : content.length ,
charactersNoSpaces : content.replace (/\s/g , '' ).length
}), [content])
}
Running Tests
pnpm test
pnpm test :watch
pnpm test :coverage
cd src-tauri && cargo test
pnpm check:all
Debugging Tips
DevTools: Right-click โ Inspect, or Cmd+Option+I (macOS) / Ctrl+Shift+I (Windows/Linux)
Rust logs: Use log crate + tauri-plugin-log or println! (visible in terminal)
Check capabilities: "Not allowed" errors mean missing permissions in capabilities
IPC errors: Ensure argument names match (snake_case Rust โ camelCase JS)
E2E debugging: Use tauri_read_logs({ source: 'console' }) to see webview console
Related Skills
tauri-v2-integration โ VMark-specific Tauri IPC patterns (invoke/emit bridges, menu accelerators)
tauri-mcp-testing โ E2E testing of the running Tauri app via MCP tools
rust-tauri-backend โ VMark Rust backend (commands, menu items, filesystem)