6.4 KiB
App facts
Things true of exactly one app. Everything in the other files applies to all four.
Margin, the writing studio
python/margin, package margin-app, bundle id studio.margin.app, version 0.1.17, FSL-1.1-MIT.
The oldest and most polished of the four, and the one that ships first on any new channel.
Library data is self-contained {book-id}.margin JSON files with images embedded as base64, plus
custom-dictionary.txt, in ~/Library/Application Support/studio.margin.app/library/. That
simplicity is what made the Drive backup feature small.
Google Drive backup is built and lives in src-tauri/src/gdrive.rs, with src/backup.ts,
src/store/useBackup.ts, BackupButton and the BackupSettings panel on the front. Loopback plus
PKCE through the system browser, scope drive.file and openid email. drive.file is
non-sensitive, so there is no Google verification review and no CASA audit, and the consent screen is
set to Production to avoid the unverified warning and the seven-day refresh token expiry that testing
mode imposes.
The Drive layout is a visible margin/ folder, one file per book, latest only, because Drive keeps
revisions itself. Backup triggers are manual, on app close, and every 15 minutes, all gated on a
dirty check so nothing uploads unless something changed. The cloud icon opens the panel rather than
backing up in one click, because the panel is the single home for back up, restore, disconnect and
account. Restore is offered as an ignorable link on the empty-library state, which doubles as
connect-on-a-new-machine.
The refresh token and sync state sit in plaintext in backup.json in the app data directory. That
was a deliberate step back from the keychain, for the reasons in code-style.md, and
it matches the app's existing local-data model given the limited scope. The newer apps seal theirs
with XChaCha20-Poly1305 instead, and Margin should be brought up to that.
Margin has no test script at all. It is the only one of the four without one.
appstore/ and target-mas/ are the Mac App Store build track.
Margin Calendar
python/margin-caledar, package margin-calendar, bundle id studio.margin.calendar, MIT. The
directory name really is misspelt.
Its docs/design.md, docs/conventions.md and docs/architecture.md are the model the rest of the
suite documents by. When writing docs for another app, follow those.
Prev and next move one day, never a week. Week view is a rolling seven days from the anchor by
default, with weekMode() in src/time.ts stored under margincal-week-mode and weekAnchor() as
the single source both spanFor and GridView use. Never reintroduce a bare startOfWeek call in
either. The snapping "calendar" mode exists in Settings, and only there do the arrows step by a week,
because a day step would be invisible.
The vertical axis has contraction hysteresis for the same reason the horizontal one slides by a day: positional memory is most of the speed of a keyboard-driven calendar.
Persisted localStorage keys are margincal-folds, margincal-bounds, margincal-view and
margincal-theme. See platform.md for how to read them out of the installed app.
This is the app with the Nix flake, and the one whose Linux story the others should copy.
Margin Docs
rust/margin-editor, package margin-docs, remote margin-docs, MIT. Three names for one thing,
and the directory is the odd one out.
No memory files and no CLAUDE.md exist for this app, so nothing was ever written down about it. It
is also the largest front end in the suite at 43,000 lines, and it is a near twin of Margin on the
Rust side: pdf.rs, fonts.rs, macspell.rs and writingtools.rs exist in both. That duplication
is the subject of ../typesetting.md.
Margin Mail
rust/margin-mail, bundle id in the same studio.margin.* family, FSL-1.1-MIT. Third in the suite
and the largest Rust codebase at 46,000 lines. No git remote yet.
The premise is a keyboard-first email client over Gmail where the user never feels Gmail. HEY and Superhuman are the feature vocabulary. Mailspring is the reliability bar.
Product decisions locked on 2026-09-03, not to be reopened unless the user does:
macOS first, then iOS, then minor work for Linux. List plus reading pane in the Superhuman shape, with HEY's Reply Later and Set Aside piles, and Feed and Paper Trail as views. The Inbox is one list in time order under Back, with unseen shown by weight alone: no dot, no band, no groups. Seen on open at once, a new reply makes a thread unseen again, the dock badge stays, and there are no counts in the list. The Screener is on by default with a first-run pass that screens in anyone who has emailed before. No AI in v1, though the design leaves room. Trackers are blocked and never sent, because privacy and ownership are core tenets. Scheduled sending is skipped; snooze and Bubble Up stay, evaluated lazily when a device opens or wakes. Multiple accounts with per-account views and an optional unified view. Backends after Gmail are generic IMAP and SMTP, then JMAP for Fastmail. Calendar invites RSVP inline and hand off to Margin Calendar, with no calendar sidebar. Compose is inline at the end of the thread, or a floating card for new mail. Full local mirror of mail, with attachments on demand.
App state must not depend on Gmail or any provider. In the user's words: "if I switch to protonmail tomorrow I don't want to lose data." Local data is the truth and cloud stores are backup only, behind one interface, with Google Drive for non-technical friends and Cloudflare R2 for the user. Key portable state on the RFC Message-ID and the sender address, never on provider ids.
Four Playwright specs fail on a clean tree and are not flakes. tests/shell.spec.ts ("New for you
above Previously seen"), tests/snooze.spec.ts ("Back above New for you") and tests/triage.spec.ts
("Mark all as seen is a link on the heading") all assert Inbox group heads that the app deliberately
no longer draws: GROUPS in src/ipc.ts has no new or seen label and ListColumn renders
GroupHead with no action. The fourth, tests/kit.spec.ts ("every text field tells the webview not
to fill it in"), is a static scan that flags an input in Kit.tsx and one in Settings.tsx. Match on
the test name, not the line number. If exactly these fail, say they are pre-existing and move on.
Fixing them means rewriting the assertions to the current design, which is its own piece of work.