Claude-skill-inception tauri-file-watching-events
install
source · Clone the upstream repo
git clone https://github.com/strataga/claude-skill-inception
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/strataga/claude-skill-inception "$T" && mkdir -p ~/.claude/skills && cp -r "$T/tauri-file-watching-events" ~/.claude/skills/strataga-claude-skill-inception-tauri-file-watching-events && rm -rf "$T"
manifest:
tauri-file-watching-events/SKILL.mdsource content
Tauri File-Watching for CLI-to-GUI Event Streaming
Problem
When building a Tauri desktop app that needs to display real-time updates from a CLI tool or external process, you need a way to bridge the output from the CLI (which writes to files or stdout) to the GUI frontend.
Context / Trigger Conditions
- Building a Tauri app with a React/TypeScript frontend
- Have a separate CLI tool that writes events/logs to a JSONL file
- Need real-time updates in the GUI when the CLI produces new output
- Want to show agent/process status, activity logs, or progress in the GUI
Solution
1. Define Event Types in Tauri Backend (Rust)
use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::fs::File; use std::io::{BufRead, BufReader, Seek, SeekFrom}; use std::path::PathBuf; use std::time::Duration; use tauri::Emitter; use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MyEvent { pub timestamp: DateTime<Utc>, pub event_id: Uuid, pub session_id: Uuid, pub event_type: String, pub data: EventData, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EventData { pub id: String, pub name: String, pub status: String, // ... other fields }
2. Implement File Watcher Command
#[tauri::command] async fn start_event_watcher(app: tauri::AppHandle) -> Result<(), String> { let events_path = get_events_file_path(); // e.g., ~/.myapp/events.jsonl tokio::spawn(async move { let mut last_position: u64 = 0; let mut last_session_id: Option<Uuid> = None; loop { if let Ok(mut file) = File::open(&events_path) { if let Ok(metadata) = file.metadata() { let file_size = metadata.len(); // Handle file truncation (new session) if file_size < last_position { last_position = 0; last_session_id = None; } // Seek to last read position if file.seek(SeekFrom::Start(last_position)).is_ok() { let reader = BufReader::new(&file); for line in reader.lines() { if let Ok(line) = line { if let Ok(event) = serde_json::from_str::<MyEvent>(&line) { // Track session and emit to frontend if last_session_id.is_none() || last_session_id == Some(event.session_id) { last_session_id = Some(event.session_id); let _ = app.emit("my-event", &event); } else { // New session detected last_session_id = Some(event.session_id); let _ = app.emit("session-change", event.session_id.to_string()); let _ = app.emit("my-event", &event); } } last_position += line.len() as u64 + 1; // +1 for newline } } } } } // Poll interval tokio::time::sleep(Duration::from_millis(200)).await; } }); Ok(()) } // Register in invoke_handler .invoke_handler(tauri::generate_handler![start_event_watcher, /* ... */])
3. Frontend Event Listener (React/TypeScript)
import { invoke } from '@tauri-apps/api/core'; import { listen, type UnlistenFn } from '@tauri-apps/api/event'; interface BackendEvent { timestamp: string; event_id: string; session_id: string; event_type: string; data: { id: string; name: string; status: string; }; } // In your React component/context useEffect(() => { let unlistenEvent: UnlistenFn | null = null; let unlistenSession: UnlistenFn | null = null; async function initialize() { // Start the backend watcher await invoke('start_event_watcher'); // Listen for events unlistenEvent = await listen<BackendEvent>('my-event', (event) => { processEvent(event.payload); }); // Listen for session changes (clear state) unlistenSession = await listen<string>('session-change', () => { clearState(); }); } initialize(); return () => { if (unlistenEvent) unlistenEvent(); if (unlistenSession) unlistenSession(); }; }, []);
4. Type Mapping Between Backend and Frontend
// Map backend status strings to frontend enum function mapStatus(backendStatus: string): FrontendStatus { const statusMap: Record<string, FrontendStatus> = { 'spawned': 'idle', 'running': 'working', 'completed': 'completed', 'failed': 'error', }; return statusMap[backendStatus.toLowerCase()] || 'idle'; }
Verification
- Start the CLI tool that writes to the JSONL file
- Open the Tauri GUI
- Verify events appear in real-time in the GUI
- Start a new CLI session and verify old state is cleared
Example Use Cases
- Agent monitoring dashboard (showing AI agent status)
- Build tool GUI (showing compilation progress)
- Log viewer (tailing log files)
- Process manager (showing running processes)
Notes
- Poll interval: 200ms is a good balance between responsiveness and CPU usage
- Position tracking: Essential to avoid re-reading entire file on each poll
- Session handling: Clear frontend state when session_id changes
- File truncation: Reset position when file size decreases (new session started)
- Error handling: Gracefully handle missing files (CLI not running yet)
Key Dependencies
Rust/Tauri:
withtauri
traitEmitter
andserde
for JSON parsingserde_json
for timestamps (optional)chrono
for unique IDs (optional)uuid
TypeScript:
for@tauri-apps/api/coreinvoke
for@tauri-apps/api/eventlisten