-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path.cursorrules
More file actions
487 lines (374 loc) · 36.7 KB
/
Copy path.cursorrules
File metadata and controls
487 lines (374 loc) · 36.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
# Agent & LLM Instructions — Principles of Excellent Software (Tauri v2 + Svelte 5 desktop suite)
> **Purpose.** This file is the single, canonical "manual of good software" for this workspace.
> It encodes every pattern that is already **standard, consolidated and battle-tested** across the
> apps so that an agentic LLM can **replicate a new desktop application end-to-end, fast and consistently**:
> the banded shell + sidebar, dark/light theming, 15-language i18n, the toast/dialog/shortcut systems
> (including the searchable, rebindable shortcuts tab), the custom select/dialog components,
> the Whisper integration (including the real build fix), and the **fully automated GitHub Actions
> release pipeline**: SemVer bump → multi-platform build → GitHub Release → AUR publish → backmerge.
>
> **This exact file is copied identically** into every project (`reaper`, `vesta`, `textmerger`,
> `effywhisper`, and the workspace root). Project-specific values (palette, Vite port, CSS framework)
> live in the clearly-labelled per-project sub-sections — read the one that matches the repo you are in.
>
> **How to use it.** Treat every rule below as a hard requirement unless the user explicitly overrides it.
> When in doubt, mirror the most mature reference implementation: **`vesta`** is the gold standard for
> the CI release automation, Whisper, i18n tooling and the shortcuts/select components; `reaper` for the
> banded layout; `textmerger` for the simpler single-crate structure. Prefer *reusing* an existing
> consolidated solution over inventing a new one.
---
## 0. Golden Rules (read first)
1. **Never hardcode colors.** Every color comes from a semantic CSS variable (`--bg`, `--surface`, `--accent`, …). See §2.
2. **No raw browser defaults.** No native `<select>`, `<input type=checkbox>`, default scrollbars, or default focus outlines in the app UI. See §5.
3. **One source of truth per concern.** Version → `PKGBUILD` `pkgver`. Heavy logic → a shared crate in `lib/`. Translations → `en.json` (master) + per-language overrides.
4. **Don't duplicate heavy logic across apps.** Extract it into a decoupled, callback-driven Rust crate (§12, §13).
5. **Every change is logged.** Use Conventional Commits (§14): the commit subject is the user-facing release note, the commit body carries the technical detail. No manual changelog files. **Keep bodies short and human** — see §14, no AI-flavored rationale dumps.
6. **Validate before declaring done.** `cargo check` (not full build) for Rust; run the i18n audit; a release is performed by the **Release GitHub Action** (§15) — never by hand-rolled local scripts.
7. **Return whole files when you modify code/scripts.** Never hand back fragments of a script the user has to splice in.
8. **Accessibility and RTL are not optional.** Keyboard handlers on every custom control; logical CSS properties everywhere (§5.12).
---
## 1. Project Map, Tech Stack & Standards
### 1.1 Per-project matrix
| Project | Structure | CSS framework | Vite/Tauri dev port | Whisper | Notes |
|---|---|---|---|---|---|
| **reaper** | Cargo **workspace** (`apps/<app>-gui/src-tauri`, `core/*`, `lib/*`) | **Vanilla CSS only** | **5173** | yes (`lib/srt-transcribe`) | yt-dlp/ffmpeg music archiver. Reference for the banded layout. |
| **textmerger** | **Single crate** (`textmerger/src-tauri`) | Vanilla CSS (some legacy Tailwind — do **not** add new Tailwind) | **5174** | no | Reference for the simple single-crate setup. |
| **vesta** | Cargo **workspace** (`apps/srt-gui/src-tauri`, `core/*`, `lib/*`, `cli/*`) | **Tailwind CSS v4** (`@import "tailwindcss";`) | **5175** | yes (`lib/srt-transcribe`) | Subtitles/Whisper/flashcards. Reference for Whisper + i18n tooling. |
| **effywhisper** | SvelteKit + `src-tauri` (less stable) | Vanilla CSS | (own) | benchmarks | Treat as experimental; stabilize toward this manual. |
### 1.2 Backend & framework
* **Backend**: Rust + **Tauri v2** — system access, low memory, strict capability/security model.
* **Frontend**: **Svelte 5 + TypeScript + Vite**.
* **Always prefer Svelte 5 runes** (`$state`, `$derived`, `$props()`, `$effect`) and snippets (`{#snippet}`) for new components.
* When touching legacy components, keep their Svelte 4 syntax (`export let`, `$:`) consistent, or refactor the whole component cleanly to runes — never mix the two styles in one file.
### 1.3 Styling rules by project
* **reaper / textmerger**: Vanilla CSS only — scoped `<style>` blocks in components or global classes in `src/app.css`. Do not introduce new Tailwind utilities.
* **vesta**: Tailwind v4 utility classes are allowed/encouraged for layout & spacing, **but they must consume the custom theme variables**. Premium/custom patterns (glassmorphism, segmented controls, …) are written as semantic classes in `app.css` or scoped `<style>`.
---
## 2. Design Tokens & CSS Variable Architecture
All styling is driven by semantic CSS variables defined in each app's `app.css`, so the scheme can change at runtime by flipping `:root[data-theme]` (§6) — never by editing component logic. Do not hardcode hex values in components.
**Canonical variable set** (every app defines all of these, dark by default + a `[data-theme='light']` block):
`--bg`, `--surface`, `--surface-2`, `--surface-3`, `--text`, `--muted`, `--border`,
`--accent`, `--accent-strong`, `--accent-text`, `--warn`, `--danger`, `--done`, `--shadow`
(+ `--glass-bg`, `--glass-border` where glassmorphism is used).
Semantic meaning: `--bg` base app background · `--surface` sidebar/panels · `--surface-2` cards/inputs · `--surface-3` hover/active · `--text` high-contrast text · `--muted` secondary text · `--border` thin borders · `--accent`/`--accent-strong` primary action + hover · `--accent-text` text on accent · `--warn`/`--danger`/`--done` feedback states · `--shadow` premium elevation.
### Reaper — Vanilla (dark / light)
```css
:root {
--bg:#111315; --surface:#181b1f; --surface-2:#20242a; --surface-3:#292f36;
--text:#f2f4f3; --muted:#a8b1ad; --border:#343b43;
--accent:#2a9d8f; --accent-strong:#43c2ad; --accent-text:#071916;
--warn:#e9a23b; --danger:#e15d64; --done:#78c679;
--shadow:0 18px 50px rgba(0,0,0,.22);
}
:root[data-theme='light'] {
--bg:#f5f4ef; --surface:#ffffff; --surface-2:#ece9e1; --surface-3:#ded9cf;
--text:#20221f; --muted:#626b66; --border:#d2cdc2;
--accent:#0f7d72; --accent-strong:#0c695f; --accent-text:#ffffff;
--warn:#9a640d; --danger:#b53d46; --done:#3f7f46;
--shadow:0 18px 38px rgba(55,48,38,.12);
}
```
### Vesta — Tailwind v4 (dark / light)
```css
:root {
--bg:#1a1a2e; --surface:#16213e; --surface-2:#0f172a; --surface-3:#1e293b;
--glass-bg:rgba(255,255,255,.05); --glass-border:rgba(255,255,255,.1);
--text:#ffffff; --muted:#94a3b8; --border:rgba(255,255,255,.1);
--accent:#667eea; --accent-strong:#764ba2; --accent-text:#ffffff;
--warn:#fbbf24; --danger:#ff416c; --done:#38ef7d;
--shadow:0 18px 50px rgba(0,0,0,.4);
}
:root[data-theme='light'] {
--bg:#f8fafc; --surface:#ffffff; --surface-2:#f1f5f9; --surface-3:#e2e8f0;
--glass-bg:rgba(0,0,0,.03); --glass-border:rgba(0,0,0,.08);
--text:#0f172a; --muted:#64748b; --border:rgba(0,0,0,.08);
--accent:#4f46e5; --accent-strong:#4338ca; --accent-text:#ffffff;
--warn:#d97706; --danger:#dc2626; --done:#16a34a;
--shadow:0 18px 38px rgba(15,23,42,.08);
}
```
### Textmerger — Vanilla (dark / light)
```css
:root {
--bg:#1e1e1e; --surface:#252526; --surface-2:#2d2d2d; --surface-3:#3c3c3c;
--text:#e5e7eb; --muted:#9ca3af; --border:#333333;
--accent:#007acc; --accent-strong:#0098ff; --accent-text:#ffffff;
--warn:#e9a23b; --danger:#e15d64; --done:#78c679;
--shadow:0 18px 50px rgba(0,0,0,.22);
}
:root[data-theme="light"] {
--bg:#ffffff; --surface:#f3f4f6; --surface-2:#e5e7eb; --surface-3:#d1d5db;
--text:#111827; --muted:#6b7280; --border:#e5e7eb;
--accent:#0066b3; --accent-strong:#005599; --accent-text:#ffffff;
--warn:#9a640d; --danger:#b53d46; --done:#3f7f46;
--shadow:0 18px 38px rgba(55,48,38,.12);
}
```
### Shared theme transition (global, in every app)
```css
body, button, input, div, span, nav {
transition: background-color .2s ease, color .2s ease, border-color .2s ease, box-shadow .2s ease;
}
```
---
## 3. Repository & Directory Architecture (replicable skeleton)
A **workspace** app (reaper/vesta) — the default for anything non-trivial or with shared crates:
```
<project>/
├─ Cargo.toml # [workspace] + [workspace.package].version + [workspace.dependencies]
├─ Cargo.lock # committed; kept in sync via `cargo update --workspace` at release
├─ .cargo/config.toml # mold linker + sparse registry (§17)
├─ apps/<name>-gui/
│ ├─ package.json # version mirrors PKGBUILD pkgver
│ ├─ src/
│ │ ├─ app.css # design tokens (§2) + global classes
│ │ ├─ App.svelte # the banded shell (§4)
│ │ ├─ main.ts
│ │ ├─ assets/avatar.png # cached <10KB attribution avatar (§11)
│ │ └─ lib/
│ │ ├─ i18n/{index.ts, locales/*.json} # 15 languages (§7)
│ │ ├─ stores/{theme.ts, snackbar.ts, shortcuts.ts, settings.ts, …}
│ │ ├─ utils/dialogGuard.ts # guardedOpen/guardedSave (§10)
│ │ └─ components/ … # Modal, KeybindRecorder, ConfirmDialog, …
│ ├─ scripts/check_missing_translations.py # i18n audit used by the release (§7, §15)
│ └─ src-tauri/{Cargo.toml, tauri.conf.json, capabilities/default.json, src/…, build.rs}
├─ core/* lib/* # pure-logic crates + shared heavy crates (e.g. srt-transcribe §12)
├─ vendor/whisper-rs-sys-0.15.0/ # patched FFI crate, pinned via [patch.crates-io] (§12)
├─ build-publish-scripts/ (vesta: build-scripts/) # ONLY the files CI needs (§14–§16):
│ PKGBUILD .SRCINFO LICENSE update_project_info.sh check_version_consistency.sh
│ extract-release-notes.sh ci-push-aur.sh (+ flatpak manifest where used)
│ # No manual build-aur/push-aur/git-release scripts: the Actions replace them.
├─ docs/{README.md, ARCHITECTURE.md, modules/}
└─ .github/workflows/
release.yml # SemVer bump + tag + backmerge (workflow_dispatch, §15)
build.yml # multi-platform build + GitHub Release (on tag v*, §15)
aur-publish.yml # AUR publish (workflow_run after build, §16)
```
A **single-crate** app (textmerger): same `src/` frontend layout, but `src-tauri` sits directly under the project and there is no `core/`/`lib/` workspace. Everything in this manual still applies except the multi-crate version checks.
---
## 4. Application Shell & "Band" Layout (MANDATORY structure)
Every app uses the same two-axis skeleton: a **collapsible sidebar column** + a **workspace split into horizontal bands**. This is what gives the suite its identity. Reaper's `app.css` is the canonical reference.
### 4.1 Outer shell — sidebar | workspace (CSS grid)
```css
.app-shell {
display: grid;
grid-template-columns: 280px minmax(0, 1fr); /* sidebar | workspace */
height: 100vh;
transition: grid-template-columns 180ms ease;
}
.app-shell.collapsed { grid-template-columns: 76px minmax(0, 1fr); } /* icon-rail mode */
```
```svelte
<main class="app-shell" class:collapsed={sidebarCollapsed}>
<aside class="sidebar"> … </aside>
<section class="workspace"> … </section>
</main>
```
### 4.2 Sidebar anatomy (top → bottom)
1. **Collapse toggle** (`.sidebar-collapse`) that flips `sidebarCollapsed`.
2. **Brand / logo** row (hidden text in collapsed mode).
3. **Primary nav** (`.main-nav`) — one button per workspace tab, `.active` on the current one, `.nav-icon` + `.nav-label` (label hidden when collapsed).
4. **Secondary/settings nav** (`.settings-nav`) where applicable.
5. **`.sidebar-spacer`** (flex spacer pushing the footer down).
6. **Attribution dock** pinned at the bottom-left (§11) — hidden in collapsed mode.
Collapsing rule: in `.app-shell.collapsed` hide `.nav-label`, brand text and `.attribution-dock`, and center the icons.
### 4.3 Workspace bands — top / content / bottom (CSS grid rows)
The workspace owns the scroll and is split into **three bands**: an optional top command band (`auto`), the **scrollable content** (`minmax(0,1fr)`), and a **fixed bottom action band** (`auto`).
```css
.workspace { min-width: 0; min-height: 0; overflow: hidden; } /* contain children, no double scroll */
.tab-layout { /* per-tab container */
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
gap: 14px;
height: 100%;
padding: 18px 18px 0 18px;
}
.footer-band { /* the bottom "band with the buttons" */
display: flex; align-items: center; justify-content: space-between;
height: 76px; flex-shrink: 0;
padding: 0 18px; margin: 0 -18px; /* bleed to the panel edges */
background: var(--surface-2);
border-top: 1px solid var(--border);
}
```
* The **bottom band is 76px tall**, uses `--surface-2` with a top border, and holds the page's primary actions (Start/Download/Save/Reset…). Settings tabs use the same band centered (`.settings-footer-band`).
* The middle band is the only scroll container: `min-height:0; overflow:auto`. Never let the whole window scroll.
* Switch tabs by toggling visibility (`class:hidden={activeTab!==…}`) or `{#if}` — keep one `.footer-band` per tab so actions stay contextual.
### 4.4 Containment checklist (prevents the classic "two scrollbars / squashed grid" bugs)
* Every grid/flex parent that contains a scroll area sets `min-width:0` and/or `min-height:0`.
* Panels use `height:100%; overflow:auto`; the shell itself never scrolls.
* Use `minmax(0, 1fr)` (not `1fr`) for flexible track sizing.
---
## 5. UI/UX Visual Harmony Rules (MANDATORY)
Premium desktop suite — every element must look intentional and consistent.
**5.1 Fields** — wrap in `.field` (label + control + helper). Control: `background:var(--surface-2); border:1px solid var(--border); border-radius:8–12px; min-height:40px; padding:0 12px; color:var(--text)`. Focus: `border-color:var(--accent); box-shadow:0 0 0 2px color-mix(in srgb, var(--accent) 24%, transparent)`. Labels: `color:var(--muted); font-size:.88rem; inline-flex; gap:6px`.
**5.2 Buttons** — `.btn-primary`/`.icon-text` (text) and `.icon-btn`/`.icon-only` (40×40 square). Primary: `background:var(--accent); color:var(--accent-text); border-color:var(--accent)`, hover `var(--accent-strong)`. Always transition bg/border/color/shadow at `.15s ease`.
**5.3 Segmented controls** — `.segmented` for exclusive choices (format/mode/auth). Active: accent bg + accent-text. Inactive: `--surface-2`/`--muted`, hover `--surface-3`/`--text`.
**5.4 Toggle cards / checkboxes** — never native `<input type=checkbox>`. Use `role="button" tabindex="0"` cards with `.active` → `border-color:var(--accent); background:color-mix(in srgb, var(--accent) 15%, var(--surface-2))`. Always add keydown: `Enter`/`Space` → `preventDefault()` + toggle.
**5.5 Dropdowns / selects** — never raw `<select>` for styled selects. Overlay: `position:absolute; z-index:50; top:calc(100% + 4px); background:var(--surface-2); border:1px solid var(--border); border-radius:8px; box-shadow:var(--shadow)`. Flip upward near the bottom edge. Close on `Escape`, backdrop click, or selection. The canonical component is **`SearchableSelect.svelte`** (vesta): trigger button styled as a §5.1 field, overlay with an inline filter input for long lists, full keyboard support (`ArrowUp/Down` to move, `Enter` to pick, `Escape` to close), `aria-expanded`/`role="listbox"`, and options rendered from `{value, label, icon?}` items. Every select in every app goes through this one component — same look, same behaviour.
**5.6 Range sliders** — pass progress as a custom prop `style="--percent:{(value-min)/(max-min)*100}%"` and use it to fill the track; `accent-color:var(--accent)`.
**5.7 Scrollbars** — invisible: `scrollbar-width:none` + `::-webkit-scrollbar{display:none}`; or thin: `scrollbar-width:thin; scrollbar-color:var(--border) transparent`. Never the default system scrollbar.
**5.8 Modals** — backdrop `position:fixed; inset:0; z-index:20; background:rgba(0,0,0,.65); backdrop-filter:blur(8px)` with `fade`. Card `border-radius:12px; background:var(--surface-2); border:1px solid var(--border); box-shadow:var(--shadow)` with `scale` (`duration:150, start:0.96`). **The card must `stopPropagation`** so backdrop-dismiss doesn't fire on inner clicks. Close on `Escape`, confirm on `Enter`. For native file dialogs use the guard in §10.
**5.9 Skeletons** — no spinners; CSS shimmer:
```css
.skeleton{background:linear-gradient(90deg,var(--surface-2) 25%,var(--surface-3) 50%,var(--surface-2) 75%);background-size:200% 100%;animation:shimmer 1.5s infinite linear;border-radius:4px;pointer-events:none;user-select:none}
@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
```
**5.10 Empty states** — never blank. `.empty-state` wrapper with SVG icon, header, description, and a clear call-to-action button.
**5.11 Accessibility & focus** — clear `:focus-visible` (`2px` accent ring) on `button,a,[role=button],input`. Trap focus inside modals and return it to the trigger on close.
**5.12 Logical CSS properties (RTL)** — always use logical props so Arabic (`ar`) flips natively: `margin/padding/border-inline-start|end`, `inset-inline-start|end` instead of left/right.
---
## 6. Theming Engine (`stores/theme.ts`)
Dark by default, persisted, applied via a single attribute on the root element. Canonical implementation:
* Type `Theme = 'dark' | 'light'`; storage key `"<app>_theme"` (e.g. `reaper_theme`).
* `applyTheme(t)` sets `document.documentElement.dataset.theme = t` (drives the `[data-theme='light']` block in §2).
* Store exposes `setTheme(t)` and `toggle()`, both persisting to `localStorage` and re-applying.
* Initialize from `localStorage` (fallback `'dark'`) and apply **before** the first paint.
* Guard `typeof localStorage/document === 'undefined'` for SSR/Tauri safety.
```ts
function applyTheme(t: Theme){ if (typeof document!=='undefined') document.documentElement.dataset.theme = t; }
```
---
## 7. i18n & Localization (15 languages, zero runtime deps)
No external i18n package — a lightweight reactive derived store keeps the bundle small.
* **Languages (15)**: `en, it, es, fr, de, pt, ru, zh, ja, ko, ar (RTL), pl, nl, tr, hi`.
* **Master + override merge** — each locale is merged over English so a missing key always falls back to English (never shows a raw key):
```ts
const translations = { en, it:{...en,...it}, ar:{...en,...ar}, /* …all 15… */ };
```
* **Store API** — a `derived` store exposing `$t(key, params?)` (a.k.a. `$locale`) that supports `{{paramName}}` placeholder replacement and flat or nested keys.
* **Language selection** — persisted under `"<app>_language"`; auto-detect from `navigator.language`/`navigator.languages`, falling back to `en` if unsupported. Maintain an `availableUILanguages` list with `code/name/nativeName/flag`.
* **RTL** — when the active language is `ar`, set `document.documentElement.dir = 'rtl'` (else `'ltr'`); layout already uses logical properties (§5.12).
* **Audit gate** — `apps/<app>-gui/scripts/check_missing_translations.py` validates locale sync. It is a **release blocker** (§15): run with `--fail-on-issues --block-reasons missing_locale,missing_key,empty_value,placeholder_mismatch`. Helper tooling (`merge_translations.py`, `split_translations.py`, `translate_align.py`) lives alongside it where present (vesta).
---
## 8. Keyboard Shortcuts Engine (`stores/shortcuts.ts` + `KeybindRecorder.svelte` + a Shortcuts tab)
* **Input protection** — in the global `handleKeydown` window listener, bail out if the target is `INPUT`, `TEXTAREA`, or `isContentEditable`.
* **Normalized combo** — parse modifiers in fixed order `Ctrl, Alt, Shift, Meta`, then the uppercased key; `SPACE` denotes spacebar: `const combo = keys.join('+').toUpperCase()`.
* **Interactive recording** — `KeybindRecorder.svelte` captures modifiers + key live and updates the `shortcuts` store.
* **Conflict prevention** — before saving a binding, check it against existing ones in the store and warn (toast) on duplicates.
* **Persistence** — user overrides live in `localStorage` (`"<app>-shortcut-overrides"`), merged over the defaults at load; a "reset all" action clears the key.
### 8.1 Shortcuts tab (MANDATORY in every app — vesta's `ShortcutsTab.svelte` is the reference)
A dedicated settings tab listing every shortcut, grouped by area, where the user can **rebind and search**:
* **Dual-mode search box** with a segmented `TEXT | KEYS` toggle:
* **TEXT mode** — plain substring search over the (localized) action names.
* **KEYS mode** — "keyboard search": the box becomes a live key-capture field
(`window.addEventListener('keydown', …, true)` while listening); the pressed
combo is normalized exactly like §8 combos and filters the list to the
shortcuts containing those keys. `Escape` stops listening; a clear button
resets. Always remove the global listener on stop/destroy.
* **Per-row rebind** via `KeybindRecorder` (click → "Press keys…" → capture → conflict check → save override).
* **Reset** per-row and reset-all (with `ConfirmDialog`).
* Rows show the combo as keycap chips (`formatKey(combo)` → one `<kbd>` per key), never raw strings.
---
## 9. Snackbars & Toasts (`stores/snackbar.ts` + `Snackbar.svelte`)
* **Standard duration: 2500 ms** for all common toasts (balance of visibility vs. immediacy).
* Store API: `snackbar.show(message, variant, duration?)` and `snackbar.dismiss(id)`.
* Variants → colors: `success`→`--done`, `info`→`--accent`, `warning`→`--warn`, `error`→`--danger`.
* Entry micro-animation re-triggered per toast via a `{#key}` block (`slide-up`/`fade-in-up`) + a linear shrinking progress bar matching the remaining duration.
---
## 10. Dialogs & Dialog Collision Guard (`utils/dialogGuard.ts`)
Tauri's `@tauri-apps/plugin-dialog` `open`/`save` **crash or stutter if opened twice concurrently**. Never call them directly. Always use the guarded wrappers:
```ts
import { open, save } from "@tauri-apps/plugin-dialog";
let dialogInFlight = false;
async function withDialogLock<T>(action:()=>Promise<T>):Promise<T|null>{
if (dialogInFlight) return null;
dialogInFlight = true;
try { return await action(); } finally { dialogInFlight = false; }
}
export const guardedOpen = (o?:Parameters<typeof open>[0]) => withDialogLock(()=>open(o));
export const guardedSave = (o?:Parameters<typeof save>[0]) => withDialogLock(()=>save(o));
```
Callers must handle the `null` return (a dialog was already open).
---
## 11. Bottom-Left Attribution Dock (every client app)
A standardized, premium footer in the bottom-left of the sidebar (or the settings footer if there's no sidebar):
* **Avatar** — round, **cached locally** at `src/assets/avatar.png` (64×64, <10KB; never fetched at runtime), linking to `https://pierspad.com`.
* **Version** — SemVer string linking to the GitHub **releases** page (ideally the exact tag `…/releases/tag/v{version}`).
* **License** — links to the repo `LICENSE`.
* **GitHub** — a two-row "GitHub / Repo" link with the standard GitHub SVG icon → repository.
* Use standardized identifiers: `repoUrl`, `releasesUrl`, `licenseUrl`, `authorUrl`, `authorIconUrl`, `formattedAppVersion`.
* Version utilities live in `utils/version.ts` (`normalizeVersion`, `compareVersions`, optional `checkGitHubRelease` for an update check against `/releases/latest`).
* Keep every `<div>` and Svelte block (`{#if}`, `{#each}`) closed in its correct lexical scope to avoid compiler/parse bugs.
---
## 12. Whisper Integration (the consolidated, fixed approach)
Audio transcription is shared by reaper & vesta through **one crate, `lib/srt-transcribe`** (identical in both). Never re-implement Whisper per app — depend on this crate. Its API is Tauri-agnostic and built around progress callbacks + `CancellationToken`.
### 12.1 The build fix — THIS is why Whisper "sometimes broke" (MANDATORY)
`whisper-rs` 0.16 pulls `whisper-rs-sys`, whose upstream `build.rs` **fails to link the split GGML static libs**, producing undefined-reference linker errors on recent whisper.cpp. The consolidated fix:
* **Vendor** the crate at `vendor/whisper-rs-sys-0.15.0/` with a patched `build.rs`, and pin it in the workspace `Cargo.toml`:
```toml
[patch.crates-io]
whisper-rs-sys = { path = "vendor/whisper-rs-sys-0.15.0" }
```
* The patch explicitly emits the static link directives for **all** GGML libraries:
```rust
println!("cargo:rustc-link-lib=static=whisper");
println!("cargo:rustc-link-lib=static=ggml");
println!("cargo:rustc-link-lib=static=ggml-base"); // <- missing upstream
println!("cargo:rustc-link-lib=static=ggml-cpu"); // <- missing upstream
```
* `cmake` builds with `BUILD_SHARED_LIBS=OFF` (static), `GGML_OPENMP=OFF` unless the `openmp` feature is on, Metal off on Linux. Default build is **CPU-only** (no CUDA/Vulkan/Metal feature) for portable, dependency-free binaries.
* If you bump `whisper-rs`, re-verify these link lines still exist in the vendored `build.rs` before trusting a green build.
### 12.2 Crate API surface (`srt-transcribe`)
* `audio.rs` — `convert_to_wav(ffmpeg_path, input, output, cancel)`: ffmpeg → **16 kHz mono `pcm_s16le` WAV**. **Drains stderr on a separate async task** to avoid the pipe-buffer deadlock, and supports cancellation via `tokio::select!` + `child.kill()`. `read_wav_to_f32` normalizes int/float samples and averages stereo → mono.
* `model.rs` — model registry (`tiny/base/small/medium/large`), cache dir `~/.cache/whisper`, filenames `ggml-<id>.bin` (`large`→`ggml-large-v3.bin`). `download_model` streams from Hugging Face (`ggerganov/whisper.cpp`) to a **`.partial` file, then atomically renames** on success; progress callback is **throttled to ~150 ms**; honors cancellation (removes the partial). `list_models`/`uninstall_model`/`validate_model_id` round it out.
* `transcribe.rs` — `transcribe_full`: greedy (`best_of:1`) or beam search 5 via `beam_size`, optional Silero VAD via `vad_model_path` (standalone `WhisperVadContext` → speech spans → per-span transcription with timestamp offsets; NOT `whisper_full_params.vad`, which is a silent no-op through `whisper_full_with_state`), `language` `auto`→`None`, threads default to the **physical** cores (capped at 8 — SMT hurts GGML), all whisper printing disabled, timestamps are **centiseconds → ×10 for ms**.
### 12.3 Frontend orchestration
Expose Tauri commands that call the crate, stream progress via events, and keep a cancellation token in app state. Model picker + download progress live in the relevant tab; ffmpeg path is resolved from settings/PATH.
---
## 13. Shared Crate / Library Architecture (heavy/optional features)
For any heavy, optional, or reusable feature (Whisper, yt-dlp downloading, translation engines, SRT parsing…):
* **Extract into a cohesive standalone crate** under `lib/` (or `core/` for pure logic), depended on by relative path.
* **Decoupled, portable APIs** — generic callbacks (progress %, stage) and optional `CancellationToken`, never Tauri state/handlers inside the crate.
* **Encapsulate heavy deps** (`whisper-rs`, `reqwest/stream`, `hound`, `id3`, …) so the GUI workspace stays light and the feature is trivially optional.
* Keep all internal crates on **one shared version** (the workspace version) — enforced at release (§14).
---
## 14. Versioning & Release-Notes Governance
* **Single source of truth: `PKGBUILD` `pkgver`.** Everything else is derived from it. The bump itself is performed **by the Release Action** (§15) — humans and agents never hand-edit version numbers across files.
* **Propagation** — `update_project_info.sh` writes that version into `apps/<app>-gui/package.json`, `src-tauri/tauri.conf.json`, `src-tauri/Cargo.toml`, the workspace `[workspace.package].version`, every `core/*` & `lib/*` `Cargo.toml`, the internal path-dependency `version="…"` pins, and syncs `Cargo.lock`.
* **Consistency gate** — `check_version_consistency.sh` (vesta: `check_internal_crate_versions.sh`) fails the release if any of the above (incl. `Cargo.lock`'s entry for the app crate) disagrees with `pkgver`.
* **Release notes** — generated by **semantic-release** (`@semantic-release/release-notes-generator`) from Conventional Commits: the GitHub Release body and `CHANGELOG.md` are derived automatically. No manual notes files; write commit subjects for end users and put technical detail in commit bodies.
* **Commit body style** — most commits need **no body at all**; the subject line is usually enough. When a body genuinely earns its place (a non-obvious root cause, a real tradeoff), keep it to 1-3 short lines in plain human language. Never write bodies as an enumerated rationale/changelog dump (e.g. "Also fixes X. Also adds Y. This ensures Z.") — that reads as AI-generated filler and nobody reads it. If you can't say it briefly, say it in the PR description instead, not the commit.
---
## 15. Release Pipeline — three GitHub Actions, zero manual steps
The whole release is CI-driven. **Do not create local release scripts** (`git-release.sh`, `push-aur.sh`, `build-aur.sh` are retired patterns): a `feat:`/`fix:`/`perf:` push to `main` releases itself — the only human action *needed* is writing Conventional Commit messages. Manual override still exists for edge cases: *Actions → "Release (semver bump)" → Run workflow*.
### 15.1 `release.yml` — "Release (semver bump)" (`push: main` + `workflow_dispatch`)
**Trigger** — every push to `main` runs a `detect` job first (Conventional Commits scan since the last tag): `feat:` → minor, `fix:`/`perf:` → patch, a `!` after the type or a `BREAKING CHANGE:` footer → major. If nothing in range matches (only `chore`/`docs`/`style`/`refactor`/`test`/`ci`/`build`), the job sets `skip=true` and the rest of the pipeline is skipped — no wasted Rust/Python toolchain setup, no empty release. The `detect` job also short-circuits on its own `chore: release v<version>` commit so it can never trigger itself; push/backmerge commits made with `GITHUB_TOKEN` don't fire `on: push` anyway (GitHub anti-recursion rule), this is just belt-and-suspenders. `workflow_dispatch` inputs: `bump` = `auto|patch|minor|major` (default `auto`, i.e. same Conventional-Commits detection as a push) **or** an explicit `version`, which always wins.
Steps, in order, all fail-fast:
1. `actions/checkout` with `fetch-depth: 0` (tags are needed for auto-notes) on the branch it was dispatched from (or `main` for an automatic push).
2. **Compute the new version** from `PKGBUILD` `pkgver` (+ the detected/chosen SemVer bump, or the explicit input); abort if the tag `v<version>` already exists. Write it back to `PKGBUILD`.
3. **Propagate** — `update_project_info.sh`; then the **consistency gate**; then (where present) the **i18n audit** with `--fail-on-issues --block-reasons missing_locale,missing_key,empty_value,placeholder_mismatch`.
4. **Release notes** — re-align the rolling header to `## Release Notes v<version>`; if the file is stale (unchanged since the last tag) or the section is empty, generate bullets from `git log <last-tag>..HEAD --no-merges` (excluding `chore: release` commits). Print the preview in the job log.
5. **Commit `chore: release v<version>` + tag + `git push --atomic origin <branch> v<version>`** (bot identity).
6. **Dispatch the build explicitly**: `gh workflow run "Build and Release" --ref v<version>`. This is mandatory — tags pushed with `GITHUB_TOKEN` do **not** fire `on: push` workflows (GitHub anti-recursion rule).
7. **Backmerge job** — after a release from `main`, merge into `dev` (and vice versa) and push; skip silently if the sibling branch doesn't exist; emit a warning and fail the job on conflicts (a human resolves).
Required workflow permissions: `contents: write`, `actions: write`. Concurrency group `release`, no cancel-in-progress — so a burst of pushes to `main` queues releases one at a time instead of racing on the shared `PKGBUILD` `pkgver`.
**Writing commits that should ship** — since `main` auto-releases, prefix commit subjects with `feat:`/`fix:`/`perf:` (Conventional Commits) whenever the change should bump the version; use `chore:`/`docs:`/`refactor:`/`test:`/`style:`/`ci:`/`build:` for anything that shouldn't trigger a release on its own (it'll ship bundled with the next `feat`/`fix`). Mark breaking changes with `!` after the type (`feat!:`) or a `BREAKING CHANGE:` footer.
### 15.2 `build.yml` — "Build and Release" (`on: push: tags: v*` + `workflow_dispatch`)
Builds a matrix: Linux `ubuntu-22.04` → `.deb`/`.rpm` (+AppImage where wanted) with sccache + rust-cache; Windows → `nsis`/`msi`; an **Arch job** that repackages the produced `.deb` with `makepkg` inside an `archlinux:base-devel` container; (vesta) a **Flatpak job** from the same `.deb`. Every job re-reads the version from `PKGBUILD` and re-runs the consistency gate. The final `release` job downloads all artifacts, extracts the tag's notes section with `extract-release-notes.sh`, and publishes the GitHub Release (`ncipollo/release-action`, `allowUpdates: true`).
---
## 16. AUR Publishing — `aur-publish.yml` + `ci-push-aur.sh` (fully automatic)
* **Trigger**: `workflow_run` on "Build and Release" `completed` with `branches: ['v*']` — for tag-triggered runs `head_branch` **is the tag name**, so the filter matches releases and excludes manual dispatches on `main`. `release: published` does **not** work here: releases created with `GITHUB_TOKEN` never propagate the event (anti-loop). Keep `workflow_dispatch` as manual retry.
* **Checkout the default branch, not the tag** — the CI scripts receive fixes on the default branch; checking out the tag would run the (possibly buggy) script version frozen at release time.
* **Wait for the asset**: poll `HEAD` on the released `.deb` URL (reconstructed from the PKGBUILD's `url` + `pkgver` + `_pkgname`) for up to ~3 minutes before publishing.
* **SSH**: secrets `AUR_SSH_PRIVATE_KEY` + `AUR_KNOWN_HOSTS`, written to a throwaway dir, `StrictHostKeyChecking yes`, host `aur.archlinux.org`, user `aur`.
* **Publish** inside `archlinux:base-devel` (as an unprivileged `builder` user) via `ci-push-aur.sh`: re-run the consistency gate → `git clone ssh://aur@aur.archlinux.org/<pkgname>.git` → `updpkgsums` (in a writable work dir) → `makepkg --printsrcinfo > .SRCINFO` → copy `PKGBUILD` + `.SRCINFO` → commit `Update to v<version>` **only if something changed** → push.
* **First release of a new app**: the AUR auto-creates the package repo on first push — cloning a not-yet-existing `<app>-bin.git` yields an empty repo (warning, not error), so the same workflow bootstraps the AUR package with **no manual step besides registering the SSH key on the AUR account**.
PKGBUILD conventions: `pkgname=<app>-bin`, `_pkgname=<app>`, `provides`/`conflicts=(<app>)`, `options=('!debug')`, `source=("…::https://github.com/pierspad/<app>/releases/download/v${pkgver}/<Asset>_${pkgver}_amd64.deb")`, real runtime `depends` (e.g. `webkit2gtk-4.1 gtk3 libsoup3 yt-dlp ffmpeg …`). Keep the `sha256sums` placeholder of 64 zeros in the repo; `updpkgsums` fills it at publish time.
---
## 17. Development Speed & Compilation (MANDATORY)
**17.1 mold linker** — on Linux the Rust toolchain links with `mold`. Keep `.cargo/config.toml` target configs for `x86_64`/`aarch64` Linux; don't disable them except for non-Linux release hosts.
**17.2 Profiles** — `[profile.dev] opt-level=0, debug=0 (or 1 if strictly needed), strip="debuginfo"`; `[profile.dev.package."*"] opt-level=1` so deps stay fast without slowing recompiles. Release: `codegen-units=1, lto="thin", opt-level=3, panic="abort", strip=true`.
**17.3 Unique Vite ports** (run all apps concurrently): reaper **5173**, textmerger **5174**, vesta **5175** (Tauri `devUrl` + Vite `server.port` must match).
**17.4 `cargo check`** — use it (2–3× faster than `build`) to validate correctness; `cargo watch -c -x check` in the background during active coding.
**17.5 Sparse registry & pruning** — `[registries.crates-io] protocol = "sparse"` in `.cargo/config.toml`; periodically `cargo machete`/`cargo-shear` to drop unused deps.
---
## 18. Definition of Done (quality gates before you stop)
* No hardcoded colors; all custom controls keyboard-operable; logical CSS properties used.
* Selects via `SearchableSelect`, dialogs via the shared Modal/ConfirmDialog patterns (§5) — never one-off variants.
* New strings added to `en.json` **and to all 15 locales**; i18n audit passes.
* New shortcuts registered in the store **and** visible/rebindable in the Shortcuts tab (§8.1).
* Rust compiles under `cargo check`; tests touched by the change pass.
* Conventional Commit written (user-facing subject, technical body) — it becomes the release note (§14).
* For a release: run the **Release Action** (§15) — never tag or push versions by hand.
* Whole modified files returned to the user; no fragments.