pomotroid

GitHub

:tomato: Simple and visually-pleasing Pomodoro timer

RAW Rules

CLAUDE.md

# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

Pomotroid is a Pomodoro timer desktop app built with **Tauri 2 + Rust** (backend) and **SvelteKit + Svelte 5** (frontend). The app has been fully rewritten from Electron+Vue to this stack.

## Commands

```bash
# Development (runs Vite dev server + Tauri)
npm run tauri dev

# Production build
npm run tauri build

# Frontend-only Vite dev (no Tauri IPC available)
npm run dev

# Format all frontend source files
npm run format

# Type-check Svelte + TypeScript
npm run check

# Regenerate i18n types after editing inlang message files
npm run paraglide:compile
```

There are no automated tests in this codebase.

## Architecture

### Two windows

- **`main`** (`src/routes/+page.svelte`) β€” the timer window
- **`settings`** (`src/routes/settings/+page.svelte`) β€” opened via `new WebviewWindow('settings', ...)` from `Titlebar.svelte`
- **`stats`** (`src/routes/stats/+page.svelte`) β€” statistics window

### IPC layer

All frontend↔backend communication goes through `src/lib/ipc/index.ts`. This module exports typed wrappers around `invoke()` and `listen()`. Never call `invoke()` directly in components β€” always go through this module.

Rust commands live in `src-tauri/src/commands.rs`. All commands return `Result<T, String>`.

**Tauri events emitted by Rust:**

- `timer:tick` β€” `{ elapsed_secs, total_secs }` β€” fires every second while running
- `timer:paused` β€” `{ elapsed_secs }`
- `timer:resumed` β€” `{ elapsed_secs }`
- `timer:round-change` β€” full `TimerSnapshot`
- `timer:reset` β€” full `TimerSnapshot` (used to sync frontend after settings changes)
- `settings:changed` β€” full `Settings` object
- `themes:changed` β€” `Theme[]`
- `sessions:cleared`

### TypeScript ↔ Rust type contract

`src/lib/types.ts` mirrors Rust structs (snake_case field names). When modifying `Settings` in `src-tauri/src/settings/mod.rs` or `TimerSnapshot` in `src-tauri/src/timer/mod.rs`, update `types.ts` to match.

**Important conversions** β€” Rust converts on load/save, frontend always sees the converted form:

- Time: stored in DB as **minutes**, `Settings` struct holds **seconds**
- Volume: stored in DB as **0–100**, `Settings` struct holds **0.0–1.0**

### Settings storage

SQLite key/value table. DB column names differ from Rust struct field names (e.g., DB key `work_rounds` β†’ struct field `long_break_interval`). Mappings are in `src-tauri/src/settings/mod.rs`. New settings need a DB key mapping, a default value in `settings/defaults.rs`, and a corresponding `settings_set` key handler.

Schema migrations are in `src-tauri/src/db/migrations.rs`. Add a new `MIGRATION_N` constant and increment the schema version check in `run()`.

### Timer engine

The timer runs in a background thread. `TimerController` (`src-tauri/src/timer/mod.rs`) is the public API registered as Tauri state. `SequenceState` (`src-tauri/src/timer/sequence.rs`) tracks round type and count β€” it has its own `work_rounds_total` field that must be kept in sync via `apply_settings()` when settings change.

### Theme system

Themes are JSON files in `static/themes/` (built-in) and `{app_data_dir}/themes/` (custom, user-created). Each theme is a JSON object with `name` and `colors` (CSS custom property map with `--` prefix keys). The Rust `themes` module watches the custom directory for changes and emits `themes:changed`. `applyTheme()` in `src/lib/stores/theme.ts` sets CSS custom properties on `:root`.

### Capabilities / permissions

`src-tauri/capabilities/default.json` β€” all allowed Tauri APIs for both windows. Any new IPC command or API plugin used from the frontend must be allowlisted here.

### Localization

Uses `@inlang/paraglide-js`. Message files are in `project.inlang/`. After editing messages, run `npm run paraglide:compile` to regenerate `src/paraglide/`. Import message functions from `$lib/locale.svelte.ts`.

### UI conventions

- Window is decoration-free; custom titlebar in `Titlebar.svelte`
- Compact mode: `isCompact = w < 300 || h < 300` β€” hides footer/label/play-pause, shows only dial
- `uiScale` applied via CSS `zoom` on `.timer` div (not `transform: scale`)
- Slider track alignment: use `calc(frac * (100% - 14px) + 7px)` to match native thumb position

### Rust modules

| Module          | Purpose                                                                              |
| --------------- | ------------------------------------------------------------------------------------ |
| `audio`         | rodio-based audio playback, custom sound file management                             |
| `db`            | SQLite connection, migrations, queries                                               |
| `notifications` | OS notification wrapper                                                              |
| `settings`      | Settings struct, DB load/save, defaults                                              |
| `shortcuts`     | Global keyboard shortcut registration via tauri-plugin-global-shortcut               |
| `themes`        | Theme loading, custom theme watching (notify crate), tray icon rendering (tiny-skia) |
| `timer`         | Timer engine (background thread), sequence state, controller                         |
| `tray`          | System tray icon, tray menu, timer display in tray                                   |
| `websocket`     | Optional WebSocket server (axum) for external timer control                          |