Skip to content

Commit 2336540

Browse files
statikclaude
andauthored
Add detailed build plan for Cuesheet Tauri app (#1)
* Add detailed build plan for Cuesheet Tauri app Plan covers the port of vbs plt cuesheet generation (kindlyops/vbs#887) to a hexagonal Rust core with Typst linked as a library, a Tufte-styled Tauri 2 GUI using the Fantastic Mr Fox palette, license attribution via cargo-about, and a tag-driven release pipeline with gated code signing and the Tauri updater. https://claude.ai/code/session_01Sn1jjXDRydhdL4AGnkWNGQ * Add GitHub Pages site (three.js red panda diorama) to plan https://claude.ai/code/session_01Sn1jjXDRydhdL4AGnkWNGQ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f821a76 commit 2336540

1 file changed

Lines changed: 282 additions & 0 deletions

File tree

docs/PLAN.md

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
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

Comments
 (0)