|
| 1 | +# Cuesheet — Build Plan |
| 2 | + |
| 3 | +A small cross-platform GUI app (Tauri 2) that ports the cuesheet functionality from |
| 4 | +[`vbs plt build`](https://github.com/kindlyops/vbs/pull/887): open or drag-and-drop a |
| 5 | +purple playlist file, generate a PDF cuesheet with Typst, and save it via the OS-native |
| 6 | +save dialog. |
| 7 | + |
| 8 | +## Decisions (from interview) |
| 9 | + |
| 10 | +| Topic | Decision | |
| 11 | +|---|---| |
| 12 | +| Core logic language | Port parsing + cuesheet generation from Go to Rust (no sidecar) | |
| 13 | +| Typst | Linked as a Rust library crate (in-process compile, no bundled CLI binary) | |
| 14 | +| Scope | Fully offline — cuesheet built only from data inside the playlist ZIP | |
| 15 | +| App name | **Cuesheet**, bundle id `com.kindlyops.cuesheet` | |
| 16 | +| PDF output | Verbatim port of the vbs Typst template (Helvetica/Arial, teal `#235a68` accents) | |
| 17 | +| UX flow | Drop/browse → generate → native save dialog opens immediately | |
| 18 | +| App design | Tufte-style window design with Fantastic Mr Fox color palette | |
| 19 | +| Auto-update | Tauri built-in updater against GitHub Releases (`latest.json`) | |
| 20 | +| Code signing | Not yet — pipeline scaffolds Apple notarization + Windows Authenticode, activated automatically when secrets are added | |
| 21 | +| Targets | macOS (universal: aarch64 + x86_64) and Windows (x64) installers | |
| 22 | + |
| 23 | +## 1. Goals and non-goals |
| 24 | + |
| 25 | +**Goals** |
| 26 | + |
| 27 | +- One-screen app: a large affordance to browse for a playlist, plus whole-window drag-and-drop. |
| 28 | +- Parse the purple playlist export (ZIP + manifest.json + SQLite, schema version 14) entirely offline. |
| 29 | +- Emit a `cuesheet.typ` byte-identical (modulo timestamps) to vbs PR #887 and compile it to PDF in-process with the typst crates. |
| 30 | +- Open the OS-native save dialog prefilled with `<playlist-name>-cuesheet.pdf`. |
| 31 | +- Hexagonal architecture: the entire domain is a pure Rust crate testable headlessly in CI. |
| 32 | +- Fully automated tag-driven release pipeline producing `.dmg` (macOS) and NSIS `.exe` + `.msi` (Windows), plus updater artifacts. |
| 33 | +- Complete third-party license attribution (typst is Apache-2.0; full notice bundle generated at build time). |
| 34 | + |
| 35 | +**Non-goals** |
| 36 | + |
| 37 | +- No media download, ffmpeg cutting, or media-API resolution (that's the rest of `plt build`). |
| 38 | +- No playlist editing; read-only input. |
| 39 | +- No Linux packaging in the release pipeline (CI tests still run on Linux). |
| 40 | + |
| 41 | +## 2. Architecture (hexagonal) |
| 42 | + |
| 43 | +The domain core is a pure crate with no Tauri, filesystem-dialog, or GUI dependency. |
| 44 | +Everything I/O-shaped is a port; adapters live at the edges. |
| 45 | + |
| 46 | +``` |
| 47 | + ┌──────────────────────────────────────────┐ |
| 48 | + driving adapters │ cuesheet-core │ driven adapters |
| 49 | + │ │ |
| 50 | + Tauri command ───▶ │ ports (traits): │ |
| 51 | + (generate_cuesheet) │ PlaylistSource ──────────────────────│──▶ ZipPlaylistSource (zip + rusqlite) |
| 52 | + │ PdfCompiler ──────────────────────│──▶ TypstCompiler (typst crate + embedded fonts) |
| 53 | + CLI test harness ─▶ │ Clock ──────────────────────│──▶ SystemClock / FixedClock (tests) |
| 54 | + (xtask/golden tests)│ │ |
| 55 | + │ domain: Playlist, Cue, CueMarker, │ |
| 56 | + │ Ticks, EndAction, TypstTemplate │ |
| 57 | + └──────────────────────────────────────────┘ |
| 58 | +``` |
| 59 | + |
| 60 | +- **Driving side:** the Tauri command handler is a thin adapter — it receives a path (from |
| 61 | + dialog or drop event), calls `core::generate(path) -> Result<GeneratedCuesheet>`, and hands |
| 62 | + bytes to the save-dialog adapter. A tiny dev CLI (`cargo run -p cuesheet-cli -- <file>`) |
| 63 | + drives the same core for golden tests and local debugging without a GUI. |
| 64 | +- **Driven side:** `PlaylistSource` abstracts reading the ZIP/SQLite; `PdfCompiler` abstracts |
| 65 | + Typst so template tests can snapshot the `.typ` source without compiling, and compile tests |
| 66 | + can run against the real typst crate. |
| 67 | +- **Frontend:** purely presentational. All state transitions (idle → generating → saved/error) |
| 68 | + come from backend events; the UI holds no business logic. |
| 69 | + |
| 70 | +### Workspace layout |
| 71 | + |
| 72 | +``` |
| 73 | +cuesheet/ |
| 74 | +├── Cargo.toml # workspace |
| 75 | +├── crates/ |
| 76 | +│ ├── cuesheet-core/ # pure domain: parse, model, typst template emit |
| 77 | +│ │ ├── src/ |
| 78 | +│ │ │ ├── model.rs # Playlist, Cue, CueMarker, Ticks, EndAction |
| 79 | +│ │ │ ├── parse/ # manifest.json + SQLite extraction (port impl kept separate) |
| 80 | +│ │ │ ├── template.rs # verbatim port of the vbs cuesheet.typ emitter |
| 81 | +│ │ │ └── ports.rs # PlaylistSource, PdfCompiler, Clock traits |
| 82 | +│ │ └── tests/ # unit + golden-file tests, fixture builder |
| 83 | +│ ├── cuesheet-typst/ # PdfCompiler adapter: typst crate, font embedding, World impl |
| 84 | +│ └── cuesheet-cli/ # headless driving adapter for CI/golden tests |
| 85 | +├── src-tauri/ # Tauri app: commands, dialog/drop adapters, updater config |
| 86 | +├── src/ # frontend (Svelte 5 + TypeScript + Vite) |
| 87 | +├── docs/PLAN.md |
| 88 | +├── .github/workflows/{ci.yml, release.yml} |
| 89 | +└── about.toml # cargo-about license bundle config |
| 90 | +``` |
| 91 | + |
| 92 | +## 3. Core domain port (from vbs PR #887) |
| 93 | + |
| 94 | +### Input format |
| 95 | + |
| 96 | +Purple playlist export = ZIP archive containing: |
| 97 | + |
| 98 | +- `manifest.json` — requires `userDataBackup.databaseName` (string) and |
| 99 | + `userDataBackup.schemaVersion` (must be **14**; reject others with a clear message). |
| 100 | +- SQLite database (filename from the manifest) with tables `Tag`, `TagMap`, `PlaylistItem`, |
| 101 | + `PlaylistItemLocationMap`, `Location`, `IndependentMedia`, `PlaylistItemMarker`. |
| 102 | +- Embedded media files (thumbnails / independent images) referenced by `FilePath`. |
| 103 | + |
| 104 | +### Extracted model |
| 105 | + |
| 106 | +- **Playlist:** name (`Tag` where `Type = 2`), language. |
| 107 | +- **Items** in playback order: `Position`, `PlaylistItemId`, `Label`, `StartTrimTicks`, |
| 108 | + `EndTrimTicks`, `EndAction`, `ThumbnailFilePath` (nullable). |
| 109 | +- **Locations:** `MajorMultimediaType`, `BaseDurationTicks`, `KeySymbol`/`Track`/ |
| 110 | + `BookNumber`/`ChapterNumber`/`DocumentID` (nullable), `MepsLanguage`, `Type`. |
| 111 | +- **Independent media (images):** `DurationTicks`, `OriginalFilename`, `FilePath`, |
| 112 | + `MimeType`, `Hash`. |
| 113 | +- **Markers:** `Label`, `StartTimeTicks`, `DurationTicks`, `EndTransitionDurationTicks`. |
| 114 | + |
| 115 | +`Ticks` newtype: 1 tick = 100 ns (10 000 000 ticks/second), with display helpers for |
| 116 | +`mm:ss` / `h:mm:ss` formatting matching the vbs output exactly. `EndAction` enum maps the |
| 117 | +integer codes to the vbs labels (continue / stop / freeze). |
| 118 | + |
| 119 | +Offline durations come from `BaseDurationTicks` (published media) or `DurationTicks` |
| 120 | +(images), adjusted by trim ticks; thumbnails are the images embedded in the ZIP, extracted |
| 121 | +to a session temp dir for Typst to reference. |
| 122 | + |
| 123 | +### Typst template (verbatim) |
| 124 | + |
| 125 | +`template.rs` reproduces the vbs `cuesheet.typ` generator exactly: US Letter, 1.5 cm |
| 126 | +margins, footer rule + page numbers, Helvetica Neue/Arial 10 pt stack, header band with |
| 127 | +playlist name + metadata line (language, cue count, total duration), and the five-column |
| 128 | +table (cue number in `rgb("#235a68")`, 2 cm thumbnail, label + filename, duration with |
| 129 | +sparkline, end-action label). Fields that only exist after media processing (clip paths, |
| 130 | +cut resolution) render the same way vbs renders them when typst-only output is produced — |
| 131 | +verified against golden fixtures generated from the vbs CLI. |
| 132 | + |
| 133 | +### Typst as a library |
| 134 | + |
| 135 | +`cuesheet-typst` implements typst's `World` trait over an in-memory file map: the generated |
| 136 | +`.typ` source plus extracted thumbnail bytes, no real filesystem root needed. Fonts: since |
| 137 | +the template asks for Helvetica Neue/Arial, we embed **Liberation Sans** (metric-compatible |
| 138 | +with Arial, SIL OFL 1.1) so output is deterministic on every platform and in CI, while also |
| 139 | +exposing system fonts so macOS users get genuine Helvetica Neue when present. Typst crate |
| 140 | +version is pinned; its Apache-2.0 notice and the font OFL text ship in the license bundle. |
| 141 | + |
| 142 | +## 4. GUI design |
| 143 | + |
| 144 | +**Stack:** Tauri 2, Svelte 5 + TypeScript + Vite. Plugins: `dialog` (open/save), |
| 145 | +`updater`, `process`. Drag-and-drop via Tauri's native window drop events (works for files |
| 146 | +dragged from Finder/Explorer, unlike HTML5 DnD). |
| 147 | + |
| 148 | +**Layout (Tufte-style):** generous whitespace, a single centered column, ET Book–style |
| 149 | +serif stack (`et-book, Palatino, Georgia, serif` — ET Book is free, included with notice), |
| 150 | +hairline rules, sidenote-styled hints ("or drop a playlist anywhere on this window"), |
| 151 | +no chrome beyond the essentials. |
| 152 | + |
| 153 | +**Fantastic Mr Fox palette (design tokens):** |
| 154 | + |
| 155 | +| Token | Hex | Use | |
| 156 | +|---|---|---| |
| 157 | +| `--cream` | `#F7EED7` | window background | |
| 158 | +| `--fox` | `#C95B0C` | primary button, drop-zone highlight | |
| 159 | +| `--mustard` | `#E3A72F` | hover/active accents, progress | |
| 160 | +| `--russet` | `#7A4419` | rules, secondary text | |
| 161 | +| `--ink` | `#33271C` | body text | |
| 162 | +| `--apple-cider` | `#EFD9A7` | success state wash | |
| 163 | + |
| 164 | +**States:** |
| 165 | + |
| 166 | +1. **Idle** — large fox-orange "Choose a playlist…" button; whole window is a drop target; |
| 167 | + dropping anywhere highlights the window border in `--fox`. |
| 168 | +2. **Generating** — button swaps to an indeterminate progress treatment (parsing → |
| 169 | + compiling), still on-palette; sub-second for typical playlists but visible feedback regardless. |
| 170 | +3. **Save** — native save dialog opens automatically, default name |
| 171 | + `<playlist-name>-cuesheet.pdf`, remembering the last-used directory. |
| 172 | +4. **Done** — quiet confirmation line with a "Reveal in Finder/Explorer" link and |
| 173 | + "Generate another" returning to idle. |
| 174 | +5. **Error** — plain-language message (not a stack trace) for the known failure classes: |
| 175 | + not a ZIP, missing/invalid manifest, unsupported schema version, missing tables, |
| 176 | + typst compile failure; with a "details" disclosure for the underlying error. |
| 177 | + |
| 178 | +**Polish items:** custom app icon (fox-orange motif, full macOS/Windows icon set via |
| 179 | +`tauri icon`), window min-size and centered default, native menu with About/Check for |
| 180 | +Updates/Quit, About window showing version + bundled third-party license text, hidpi |
| 181 | +assets, reduced-motion-respecting transitions. |
| 182 | + |
| 183 | +## 5. Licensing and attribution |
| 184 | + |
| 185 | +- `cargo-about` generates `THIRD_PARTY_LICENSES.html` at build time from the full Rust |
| 186 | + dependency graph (typst Apache-2.0 + its tree); frontend deps covered via |
| 187 | + `license-checker` output merged into the same bundle. |
| 188 | +- Font licenses (Liberation Sans OFL 1.1, ET Book license) included verbatim. |
| 189 | +- The bundle is embedded in the app (About window) **and** shipped inside the installers. |
| 190 | +- Repo `LICENSE` (Apache-2.0, already present) referenced in the About window. |
| 191 | + |
| 192 | +## 6. CI and release pipeline |
| 193 | + |
| 194 | +### `ci.yml` (every push/PR) |
| 195 | + |
| 196 | +- Linux runner: `cargo fmt --check`, `cargo clippy -D warnings`, `cargo test --workspace` |
| 197 | + (core parsing, golden `.typ` snapshots, PDF smoke-compile via the typst crate — all headless). |
| 198 | +- `svelte-check` + frontend unit tests (Vitest) + `eslint`/`prettier` check. |
| 199 | +- A cross-platform build sanity job (macOS + Windows runners) running `tauri build |
| 200 | + --no-bundle` to catch platform-specific compile breaks before release day. |
| 201 | + |
| 202 | +### `release.yml` (on tag `v*`) |
| 203 | + |
| 204 | +1. Matrix: `macos-latest` (universal-apple-darwin) and `windows-latest` (x64), using |
| 205 | + `tauri-apps/tauri-action` to build `.dmg`/`.app` and NSIS `.exe` + `.msi`. |
| 206 | +2. **Signing, gated on secret presence** (steps are written now, no-op until secrets exist): |
| 207 | + - macOS: `APPLE_CERTIFICATE`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_ID`, |
| 208 | + `APPLE_PASSWORD`, `APPLE_TEAM_ID` → Developer ID signing + notarytool stapling. |
| 209 | + - Windows: `WINDOWS_CERTIFICATE`, `WINDOWS_CERTIFICATE_PASSWORD` → Authenticode. |
| 210 | + - Without secrets the job still produces working unsigned installers and the release |
| 211 | + notes state they're unsigned. |
| 212 | +3. **Updater:** `TAURI_SIGNING_PRIVATE_KEY` (+ password) signs update bundles; |
| 213 | + `tauri-action` assembles `latest.json` pointing at the GitHub Release assets. The |
| 214 | + updater keypair must be generated once (`tauri signer generate`) and added as secrets |
| 215 | + before the first release — documented in `docs/RELEASING.md`. The updater endpoint in |
| 216 | + `tauri.conf.json` targets `https://github.com/kindlyops/cuesheet/releases/latest/download/latest.json`. |
| 217 | +4. Release is created as a **draft** with generated notes + license bundle attached; |
| 218 | + publishing the draft is the single manual gate. |
| 219 | +5. Version bumping via one source of truth (`tauri.conf.json` + workspace version kept in |
| 220 | + sync by an `xtask bump` helper); tags drive everything else. |
| 221 | + |
| 222 | +## 7. Testing strategy |
| 223 | + |
| 224 | +| Layer | What | Where it runs | |
| 225 | +|---|---|---| |
| 226 | +| Domain unit tests | ticks math, end-action mapping, trim/duration calculation, name extraction | every CI run, Linux | |
| 227 | +| Parser tests | fixture builder that constructs in-memory playlist ZIPs (manifest + SQLite + images) covering happy path and every malformed-input class | CI, Linux | |
| 228 | +| Golden tests | `.typ` output snapshot-compared against fixtures captured from the vbs CLI to guarantee the "verbatim" requirement | CI, Linux | |
| 229 | +| PDF smoke tests | compile golden `.typ` with the typst crate, assert page count + non-empty output (PDF bytes aren't byte-stable, so structure-level asserts) | CI, Linux | |
| 230 | +| Adapter tests | Tauri command layer tested against mock ports | CI, Linux | |
| 231 | +| Frontend | Vitest component tests for the state machine (idle/generating/done/error) with mocked `invoke` | CI, Linux | |
| 232 | +| Build sanity | `tauri build --no-bundle` on macOS + Windows runners | CI, mac/win | |
| 233 | + |
| 234 | +The hexagonal split is what makes every functional test headless: nothing in |
| 235 | +`cuesheet-core`/`cuesheet-typst` knows Tauri exists. |
| 236 | + |
| 237 | +## 8. GitHub Pages site (three.js) |
| 238 | + |
| 239 | +A single-page marketing site at `https://kindlyops.github.io/cuesheet/`, quirky and |
| 240 | +handmade-feeling, sharing the app's design language. |
| 241 | + |
| 242 | +- **Centerpiece:** a paper-craft, stop-motion-styled autumn diorama — a low-poly **red |
| 243 | + panda** among paper trees and falling leaves, in the Mr Fox palette — rendered with |
| 244 | + three.js. The scene parallax-rotates subtly with scroll and mouse movement; animation |
| 245 | + uses a gentle stop-motion cadence (stepped keyframes at ~12 fps feel) and respects |
| 246 | + `prefers-reduced-motion` with a static hero render fallback. |
| 247 | +- **Content below the fold:** what-it-does blurb in the same Tufte typography, app |
| 248 | + screenshots, and macOS/Windows download buttons that resolve the latest GitHub Release |
| 249 | + assets at load time via the public Releases API (no rebuild needed per release). |
| 250 | +- **Implementation:** `site/` directory in this repo — Vite + TypeScript + three.js, models |
| 251 | + authored as glTF (low-poly, flat paper-texture materials, hand-built or via Blender |
| 252 | + export committed to the repo). No framework; the page is mostly static HTML/CSS. |
| 253 | +- **Deploy:** `pages.yml` workflow builds `site/` and deploys via |
| 254 | + `actions/deploy-pages` on every push to `main` touching `site/`. |
| 255 | +- **Budget:** scene kept under ~1 MB of assets, lazy-initialized after first paint, graceful |
| 256 | + no-WebGL fallback to the static render. |
| 257 | + |
| 258 | +## 9. Milestones |
| 259 | + |
| 260 | +1. **M1 — Core port:** workspace scaffold; model + ZIP/SQLite parser; fixture builder; |
| 261 | + unit + parser tests green in CI. |
| 262 | +2. **M2 — Typst output:** verbatim template emitter; golden fixtures from vbs; in-process |
| 263 | + PDF compile with embedded fonts; `cuesheet-cli` for headless generation. |
| 264 | +3. **M3 — App shell:** Tauri 2 + Svelte scaffold; browse + drag-and-drop; generate → |
| 265 | + native save dialog; error states; Mr Fox/Tufte design system; icon. |
| 266 | +4. **M4 — Polish & licensing:** About window, menus, license bundle (cargo-about), |
| 267 | + reveal-in-folder, last-directory memory. |
| 268 | +5. **M5 — Pipeline:** ci.yml, release.yml with gated signing + updater, `docs/RELEASING.md`, |
| 269 | + first tagged draft release (unsigned) validating end-to-end installers on both OSes. |
| 270 | +6. **M6 — Pages site:** red-panda diorama scene, landing page content, download buttons |
| 271 | + wired to latest release, `pages.yml` deploy workflow. |
| 272 | + |
| 273 | +## 10. Risks / notes |
| 274 | + |
| 275 | +- **Golden parity:** the vbs template emits some fields (clip filenames, resolution) that |
| 276 | + come from the media pipeline; the offline app reproduces vbs's behavior for the |
| 277 | + pre-processing case. Fixtures captured from the actual CLI keep this honest. |
| 278 | +- **Schema drift:** only schema version 14 is accepted, same as vbs; newer exports fail |
| 279 | + with an explicit "unsupported schema version" message rather than garbage output. |
| 280 | +- **Updater before signing:** Tauri's updater signature is independent of OS code signing, |
| 281 | + so auto-update works even while installers are unsigned; macOS Gatekeeper will still |
| 282 | + warn on first install until notarization secrets are added. |
0 commit comments