Native macOS spaced-repetition study app, open source (MIT). Two jobs: retain knowledge over time (FSRS v5 scheduling) and sustain focus while studying (integrated focus mode). Local-first: your data lives on your disk, no accounts, no networking.
⬇ Download the latest release —
grab Engram.zip, unzip, drag Engram.app to Applications.
First launch: the app is not notarized, so macOS will warn you. Right-click → Open → Open (only needed once). Requires macOS 14+.
Prefer building from source? See Build.
To rename the app:
Sources/Presentation/EngramApp.swift(AppInfo.name) andproject.yml— nowhere else.
- FSRS v5 scheduling — ported from py-fsrs 4.1.2 and validated against generated reference vectors; per-deck target retention.
- Notes, not just cards — typed multi-field notes with templates (one "Basic" type today, cloze/reversed slot in later), markdown rendering, free-form tags, live preview.
- Decks with subdecks (
Math::Algebra), per-deck daily limits and retention, roll-up due counts in the sidebar. - Review sessions by deck, tag, or everything: front → reveal → four
ratings with their predicted intervals.
Spacereveals,1–4grade. - Quick Add (
⌘⇧N) — small window to file a note without navigating. - Note browser — text + tag search, edit and delete in place.
- Focus mode — Pomodoro or deep work wrapped around a study queue, goals (minutes or cards), menu bar timer, optional Do Not Disturb hook, break/goal notifications.
- Stats — reviews per day, cards by state, real retention, due forecast, focus minutes per day.
- Daily reminder — one configurable local notification, opt-out respected.
- Local-first — a versioned SwiftData store in Application Support; no account, no telemetry, no network.
brew install xcodegen
xcodegen generate
open Engram.xcodeproj # run the Engram schemeThe core (Domain + Application) is a plain Swift package — no Xcode needed:
swift testThe FSRS engine is validated against reference vectors generated from
py-fsrs 4.1.2 (FSRS v5) with 1e-4 tolerance. To regenerate them:
python3 -m venv venv && venv/bin/pip install "fsrs==4.*"
venv/bin/python Scripts/generate_fsrs_vectors.py > Tests/DomainTests/FSRSTestVectors.swiftFour layers, dependency rule points inward. Domain imports nothing but Foundation.
Sources/
├── Domain/ # pure Swift: entities, FSRS engine, repository protocols
├── Application/ # use cases: review session, decks, stats, focus
├── Infrastructure/ # SwiftData repos, notifications, system focus (M2+)
└── Presentation/ # SwiftUI app (Xcode target; the rest is the SPM package)
Extensibility seams (implemented as abstractions from day one — the MVP ships one variant of each, the code depends only on the abstraction):
- NoteType — a card is not front+back; notes have N typed fields, templates generate cards (
NoteType.makeCards). - ContentType / ContentRenderer — markdown now, LaTeX/code/images plug in as renderers.
- StudySession — SRS review is one study mode; cram/quiz implement the same protocol.
- Decks + tags + CardQuery — subdecks, free tags; smart decks = saved queries.
- Quick Add — single write path in
DeckService.addNote. - Per-deck FSRS config —
DeckConfigon every deck. - DistractionBlocker — MVP triggers macOS DND; system-wide blocking is a future implementation.
Focus mode (Pomodoro or deep work, optional goal, optional deck/tag to study inside the block) lives in the sidebar's Focus entry, with a live timer in the menu bar. Everything below is optional — the session works without any of it.
macOS exposes no public API to toggle a Focus filter, so Engram drives two Shortcuts you create yourself (Shortcuts.app → new shortcut → Set Focus):
| Shortcut name | What it should do |
|---|---|
Engram Focus On |
Turn Do Not Disturb (or your own Focus) on |
Engram Focus Off |
Turn it off |
Name them exactly like that. Engram runs them via /usr/bin/shortcuts when a
focus block starts and ends. Only the focus engine calls this — the UI never
toggles it directly (seam 7, DistractionBlocker).
It degrades gracefully at every step: shortcuts missing, shortcuts CLI
unavailable, permission denied or a shortcut that hangs (it is killed after a
few seconds) are all swallowed silently. You lose the Do Not Disturb switch,
never the session.
Same for the rest: notifications (block ends, break reminders, goal reached) are
skipped if permission is denied, and ambient sound is disabled in the UI until
ambience-rain / ambience-whiteNoise / ambience-cafe loops are bundled in
the app target.
EngramMCP is a local MCP server over the same store, so Claude Desktop /
Claude Code can manage your cards — "add these 10 fraction cards to
Math::Fractions" just works. Tools: list_decks, create_deck, create_note,
search_notes, get_stats.
swift build -c release # builds .build/release/EngramMCPClaude Desktop — add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{ "mcpServers": { "engram": { "command": "/path/to/Engram/.build/release/EngramMCP" } } }Claude Code: claude mcp add engram -- /path/to/Engram/.build/release/EngramMCP
Local only, on purpose. The store is personal data; a remote server would
need hosting plus real OAuth to be safe, so there is none. The seam is ready if
that ever changes (same tool table, second transport — see the TODO in
Sources/MCP/EngramMCPServer.swift). ENGRAM_STORE_DIRECTORY overrides the
store location for experiments. Notes created over MCP appear in the app when
you reselect the deck.
- M0 — scaffold: XcodeGen project, SPM core, app opens a window
- M1 — FSRS v5 engine, ported from py-fsrs 4.1.2 (9 replay scenarios, 1e-4)
- M2 — SwiftData persistence, versioned schema, cascade rules
- M3 — decks/notes/tags UI, Quick Add, browser
- M4 — review session UI with keyboard shortcuts
- M5 — focus mode (Pomodoro/deep work, DND hook, menu bar timer, goals)
- M6 — stats (Swift Charts) + daily reminder
- M7 — polish, error surface, app icon, accessibility pass
The MVP is complete. 34 tests across engine, services and persistence.
Every gap is marked TODO(owner): at the exact seam it plugs into. Current inventory:
Content & study
- LaTeX renderer (KaTeX in WKWebView) at
ContentRenderer;.code/.imagecontent types. - New note types (reversed, cloze, typed answer) — re-sync generated cards on edit.
- Smart decks: saved
CardQueryfilters as sidebar entries. - Daily limits should subtract cards already studied today.
- Per-deck optimized FSRS weights (needs the review log history — already recorded).
Focus
- Bundle 2–3 royalty-free ambience loops (
ambience-rain/-whiteNoise/-cafe). - System-wide app/website blocking via Family Controls / Network Extension (second
DistractionBlockerimplementation; needs Apple entitlements).
App
- System-wide global hotkey for Quick Add.
- Refresh the daily notification body with the real due count on app close.
- Tag token field with completion.
- App Sandbox + entitlements, real bundle id prefix, screenshots for this README.
- Never read
card.front— there is none. Go throughnote.fieldsvia theNoteType(frontFields/backFields). - Never render raw strings — go through
FieldContentView/ContentRenderer. - Never assume study == SRS — go through
StudySession. - The FSRS engine must keep passing the reference vectors; if you touch
FSRS.swift, regenerate nothing — fix the code. ~/Proyectos/AgenticNotchis a good local reference for menu-bar app patterns, but it is GPL-3: learn from it, do not copy code into this MIT repo.
MIT — see LICENSE.