A native macOS focus timer for LSAT prep. Every completed block is written to an Obsidian vault and to a Notion database, so study time tracks itself.
Built in Swift (AppKit + WKWebView). No frameworks, no npm, no local server, no network calls except the Notion API. The whole app is one Swift file, one HTML file, and an icon, and it weighs about 600 KB.
Grab the latest .dmg from Releases,
open it, and drag LSAT Timer to your Applications folder. Universal binary,
macOS 13 or later.
The build is signed ad hoc rather than notarized, so the first launch needs one extra step: right click the app and choose Open, then confirm. macOS remembers the choice. If it still refuses, clear the quarantine flag:
xattr -dr com.apple.quarantine "/Applications/LSAT Timer.app"Prefer to build it yourself? See Build below. It takes about ten seconds.
- Focus blocks with short and long breaks. Defaults to 35 / 5 / 15 with four blocks per cycle, because 35 minutes is a real LSAT section length.
- Tag each block with an activity: LR drill, RC drill, Full PT, Review, Flashcards, Reading, Other. Optional one line note.
- Chime plus a macOS notification when a block ends. Breaks can start themselves.
- Guided Notion setup: paste a token, pick a database, or have the app build one.
- Today, this week, and day streak, plus the day's blocks listed in the window.
- Dock badge counts down the minutes remaining. Keep on Top with Cmd+T.
- Strict monochrome dark UI, matching the Second Brain Companion design system.
Obsidian. One note per day at
1-raw/law-school/lsat/study-sessions/YYYY-MM-DD.md, rewritten in full each time
a block finishes:
---
date: 2026-08-14
type: lsat-study-log
total_minutes: 70
pomodoros: 2
source: LSAT Timer
---
# LSAT study — Friday, August 14, 2026
**70 min** across 2 blocks · LR drill 35m · Review 35m
Plan: [[10-week-study-plan]] · Misses: [[wrong-answer-book]]
| Start | End | Min | Activity | Notes |
| --- | --- | --- | --- | --- |
| 9:05 AM | 9:40 AM | 35 | LR drill | PT 155 section 2 |Notion. One row per block in a database with these properties: Session, Date, Minutes, Activity, Pomodoros, Started, Ended, Notes, Source. If the request fails or no token is set, the block goes to a local queue and retries on the next launch, or immediately when you click the Notion pill in the footer.
Local. ~/.local/share/lsat-timer/sessions.jsonl is the source of truth. The
day notes are derived from it, so deleting a note is safe.
Requires the Xcode command line tools. Nothing else: no package manager, no dependencies. The build produces a universal binary when the x86_64 toolchain is available, and falls back to arm64 alone when it is not.
./build.sh # builds into ~/Applications
./build.sh /ApplicationsOpen Connect Notion from the app menu, or click the Notion pill in the footer. The screen walks two steps:
- Create an integration. A button opens https://www.notion.so/my-integrations. Make one, name it LSAT Timer, and copy the internal integration secret. Paste it in. The app checks it against Notion live and tells you which workspace answered, so a bad paste is caught immediately rather than at the end of your first block.
- Give it a page. In Notion, open the page or database you want, use the
...menu, choose Connections, and add the integration. Then press Find databases and pick one. If you would rather not build the database yourself, press Find pages, pick a page, and Create Study Sessions builds it with the right columns already in place.
The token goes into your login keychain, not a file. Nothing is sent anywhere except
api.notion.com. Disconnect clears both the token and the database.
Obsidian needs no credentials at all. Point VAULT at your vault and blocks start
landing there immediately.
Everything is still readable and editable at
~/.config/second-brain/lsat-timer.env, which the app re-reads on every block:
NOTION_TOKEN=ntn_xxx # optional: the keychain takes priority
NOTION_DATABASE_ID=...
VAULT=/path/to/your/vault
OBSIDIAN_SUBPATH=1-raw/law-school/lsat/study-sessionsnative/main.swift the app: window, menus, bridge, file writes, Notion client
native/Info.plist bundle metadata
app/index.html the entire UI, one file, no assets
build.sh compile universal, assemble, ad hoc sign
package-dmg.sh wrap the built app in a .dmg and .zip
The UI talks to Swift over a WKWebView message handler. A small shim rewrites
fetch('/api/…') onto that handler, so the HTML stays plain and portable.
Strict monochrome, dark first. Near-black #0B0B0D behind translucent glass
panels, hairline white strokes, and system SF Pro throughout, so there are no
bundled font files and nothing to load at runtime.
There is no accent hue. Selection is expressed the one way the language allows: a white fill with black text. That single idiom covers the chosen activity, the primary button, and the settings switches. Breaks are marked by dashing the dial track rather than by changing color, the way a lighter tier is marked elsewhere in the system.
| Token | Value |
|---|---|
| Background | #0B0B0D |
| Glass, card | rgba(120,120,128,0.14) |
| Glass, control | rgba(120,120,128,0.18) |
| Strokes | 0.5px white at 8 to 16 percent |
| Text | white, then rgba(235,235,245,0.55), then 0.42 |
| Radii | card 20, field 14, capsules fully round |
| Type | SF Pro. Title 27/700, body 15, caption 11.5, eyebrow 11/600 caps at 0.8 tracking |
Numerals use tabular figures everywhere a value changes in place, so the clock and the totals do not jitter.
MIT
