Building a Desktop Widget with Tauri v2
CostDog is a desktop widget that sits on top of your terminal. Here's how we built it with Tauri v2.
Why Tauri?
The Architecture
āāāāāāāāāāāāāāāāāāāāāāā
ā Tauri WebView ā
ā (HTML/CSS/JS) ā
āāāāāāāāāāāāāāāāāāāāāāā¤
ā Rust Backend ā
ā (SQLite, Scanner) ā
āāāāāāāāāāāāāāāāāāāāāāā¤
ā System Window ā
ā (Always on top) ā
āāāāāāāāāāāāāāāāāāāāāāā
Key Design Decisions
1. Frameless Window
We used `decorations: false` to remove the native title bar, then built our own draggable bar with a close button.
2. Dynamic Resizing
The widget starts at 410Ć36px (just the bar) and expands to 410Ć520px when clicked. We use Tauri's `resize_window` command for this.
3. Auto-Refresh
A background thread in Rust scans session logs every 30 seconds and emits a `refresh-data` event to the frontend via Tauri's event system.
4. Local SQLite
All data is stored in `~/.costdog/costdog.sqlite` using rusqlite with WAL journal mode for concurrent reads.
Challenges
1. **macOS dragging** ā CSS `-webkit-app-region: drag` doesn't work reliably. We switched to Tauri's `data-tauri-drag-region` attribute.
2. **Gatekeeper** ā Unsigned macOS apps are blocked. Users need to run `xattr -cr` to unblock.
3. **Window focus** ā Always-on-top windows can steal focus. We had to handle click-through carefully.
Code Example
Here's how the Rust backend scans Claude Code sessions:
fn scan_claude_sessions(db: &Connection) -> Result<()> {
let home = dirs::home_dir().unwrap();
let claude_dir = home.join(".claude").join("projects");
for entry in WalkDir::new(&claude_dir) {
let entry = entry?;
if entry.path().extension().map_or(false, |e| e == "jsonl") {
parse_jsonl(entry.path(), db)?;
}
}
Ok(())
}
Lessons Learned
1. **Start simple.** We started with a single HTML file. No build step, no framework.
2. **Rust is worth it.** The learning curve is steep, but the performance and safety pay off.
3. **Desktop apps are different.** Window management, system tray, auto-update ā all need special handling.