Files
margin/simplify/guidelines/app-facts.md
T
2026-10-03 22:48:49 +05:30

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.