13 KiB
Pommedoro
A Pomodoro timer that respects how your brain actually works.
The Problem with Traditional Pomodoro Timers
Every Pomodoro app does the same thing: a timer counts down, an alarm fires, and a dialog box demands your attention. You dismiss it. You always dismiss it. The break never happens.
This isn't a willpower failure. It's a design failure.
When you're deep in flow — writing, coding, designing — your mind is in a groove. A sudden alarm is a context switch, and human beings are wired to reject context switches. The instinctive response to an interruption during focus is to kill the interruption, not to obey it. So you click "dismiss" and keep working, and the entire purpose of the break is defeated.
Traditional timers treat breaks as binary events: you're working, then you're not. But that's not how attention works. Attention is a continuum. You can't jump from full focus to full rest without a bridge. Transitions, not hard cuts, are the only way successful breaks will actually occur.
How Pommedoro Works
Pommedoro is built around the idea that your timer should ease you toward a break rather than ambush you with one.
Phase 1: Escalating Awareness
During the last five minutes of a work session, Pommedoro begins painting soft teal gradients along the edges of all your screens. These are gentle — barely noticeable at first. They fade in and out slowly, like breathing.
As time goes on, the blinks become more frequent and more intense:
- 5:00 – 3:00 remaining: A brief flash every 60 seconds. Subtle. A whisper.
- 3:00 – 1:00 remaining: Every 30 seconds. You start to notice.
- 1:00 – 0:30 remaining: Every 10 seconds. The edges are becoming familiar.
- 0:30 – 0:10 remaining: Every 5 seconds. Your peripheral vision has accepted what's coming.
- 0:10 – 0:05 remaining: Every 2 seconds. A steady pulse.
- Last 5 seconds: The edges hold solid. A countdown pill appears at the bottom of the screen.
The intensity of the gradient also escalates over this period — from a faint 15% opacity to a vivid 75%. Your subconscious has been processing this for five full minutes before the break screen ever appears.
Phase 2: The Break Screen
When the timer reaches zero, the screen transitions to a full teal overlay. This isn't an alert dialog you reflexively dismiss — it is your screen now. It carries a simple suggestion:
"Stretch your neck and shoulders" "Step outside for a minute of fresh air" "Unclench your jaw and relax your shoulders"
These are real, physical actions. Not platitudes. Not "take a break!" — that's meaningless. Pommedoro tells you what to do with your break, and if a suggestion doesn't resonate, you can dismiss it and it won't come back.
A 5-minute break countdown runs in the background. You can mark the suggestion as completed ("Success!"), skip it ("Next Time"), or permanently dismiss suggestions that don't suit you ("Don't Suggest").
Phase 3: Reflection
After completing a suggestion, Pommedoro asks one question:
"How did that impact your readiness for the day?"
Three options: Feeling Great, About the Same, Not Really. No journaling. No friction. Just a moment of honest self-assessment before you return to work.
Phase 4: Ready When You Are
The final screen says "Ready when you are" and offers Pause or Continue. There's no urgency. The break timer is still visible but the message is clear: you decide when to go back. The next 25-minute cycle starts when you press Continue.
Why This Works
The escalating gradient approach works because it operates on the same channel as your focus: your visual field. Rather than competing with your attention via an auditory alarm or a modal dialog, Pommedoro gradually joins your visual environment. By the time the break arrives, your brain has already been transitioning for five minutes. The break screen isn't an interruption — it's the natural conclusion of a process you've been subconsciously participating in.
This is the difference between someone tapping you on the shoulder while you're reading versus slowly turning up the lights in the room. One triggers a startle response and gets dismissed. The other lets your eyes adjust naturally.
The actionable suggestions matter too. "Take a break" is an instruction with no substance. "Do some pushups" or "Drink some water" gives your body and mind a concrete thing to switch to. The context switch succeeds because there's a real context to switch into.
Features
- 25-minute work sessions with 5-minute escalating visual transitions
- Edge gradient warnings that increase in frequency and intensity
- Full-screen break overlay with actionable wellness suggestions
- 5-minute break timer with self-assessment reflection
- Menu bar timer with pause/resume support
- Debug mode for testing (1-minute cycles)
- Launch at Login via macOS LaunchAgent
- Native macOS app — no Electron, no web views, just AppKit
- Self-update — checks the repo over HTTPS (no login); offers an update only when the server has a newer release (SHA + timestamp)
Changelog
See CHANGELOG.md for release history and recent changes.
Install
1. Download
Go to colinknapp.com/stories/pommedoro.html or download the DMG directly:
2. Install
In Finder, open your Downloads folder and double-click Pommedoro.dmg. When the disk image opens, drag Pommedoro into the Applications folder (or use the shortcut arrow if the window shows one). Then eject the disk image (right-click the "Pommedoro" volume in the Finder sidebar and choose Eject, or drag it to the Trash).
3. If macOS says "Pommedoro can't be opened" or "move to Trash"
Don't trash it. This happens because the app is distributed outside the Mac App Store.
- Open System Settings → Privacy & Security.
- Scroll down to the message about "Pommedoro was blocked…" (or "from an unidentified developer").
- Click Open Anyway and confirm.
4. Open the app
Open Applications (or use Spotlight: Cmd+Space, type "Pommedoro") and double-click Pommedoro. It runs as a menu bar app — look for the timer icon in your menu bar.
Build from Source
Requires macOS 13+ and Swift 5.9+.
| Command | What it does |
|---|---|
make bundle |
Build the .app (default target) |
make install |
Build and install to /Applications; stamps as dev build (auto-update disabled) |
make run |
Build and launch the app from .build/ (does not install) |
make dmg |
Build the .app and create .build/Pommedoro.dmg |
make release |
Build DMG and copy it + SHA256 into releases/ for publishing |
make clean |
Remove build artifacts and the app bundle |
make icons |
Regenerate Pommedoro.icns from Resources/boot-logo.svg |
make install-agent |
Install LaunchAgent for Launch at Login (after make install) |
For day-to-day development and releasing to users, see Development & Release below.
Development & Release
This section describes how to work on Pommedoro locally, how releases and auto-update work, and how to publish a new version.
Local development
-
Quick run (no install)
make run— builds and opens the app from.build/Pommedoro.app. Good for quick tests. -
Install and run from /Applications
make install— builds, installs to/Applications/Pommedoro.app, and stamps the app as a dev build by writingdevinto the update state file. The app will not prompt you to “update” to the release version; the menu item “Check for Updates” will show “Auto-update is disabled for local dev builds.” -
After code changes
Runmake installagain (ormake run). There is no need to touch the update state for normal dev; the dev stamp persists until you install from a DMG or run a release flow. -
Launch at Login (optional)
make install-agent— installs a LaunchAgent that starts Pommedoro at login. Run aftermake install. Usemake uninstall-agentto remove it.
How releases and auto-update work
-
Release artifacts
A release consists of:releases/Pommedoro.dmg— the disk image users install from.releases/Pommedoro.dmg.sha256— the SHA256 hash of that DMG (one line, 64 hex chars).
-
Where the app checks for updates
The app fetches over HTTPS with no login:- SHA:
https://git.nixc.us/colin/pommedoro/raw/branch/main/releases/Pommedoro.dmg.sha256 - DMG:
https://git.nixc.us/colin/pommedoro/raw/branch/main/releases/Pommedoro.dmg
It uses the response Last-Modified header as the release “timestamp” so it only offers an update when the server has a newer release, not just a different one.
- SHA:
-
Local state (what version am I?)
The app stores its idea of the current release in:- File:
~/Library/Application Support/Pommedoro/current.sha256 - Format: two lines:
SHA256(64 hex chars) and a Unix timestamp (or0). - Set when:
- Installing from the DMG (via
Install.command) — writes the DMG’s SHA and, if it matches the remote, the remote’s Last-Modified timestamp. - After a successful in-app update — the swap script writes the new SHA and timestamp.
- After
make install— writesdevso the app never nags for updates.
- Installing from the DMG (via
- File:
-
When “Update available” is shown
Only if both are true:- The release SHA on the server is different from the one in the state file.
- The server’s Last-Modified is newer than the stored timestamp (so we don’t offer “updates” to an older release).
If the SHA in the state file matches the server, the app always reports “Up to Date,” regardless of timestamp.
Full release workflow (recommended)
Use this to build, test, and publish a release so the installed app and the server stay in sync and “Check for Updates” behaves correctly.
-
Run the full test and push the release
./build-test.sh --pushThis will:
- Stop any running Pommedoro, clean the build, and run
make release. - Create the DMG and
releases/Pommedoro.dmg.sha256. - Simulate a user install (quarantine, mount DMG, run
Install.command), then verify the app runs and the local state file is stamped with the release SHA. - Push:
git add releases/Pommedoro.dmg releases/Pommedoro.dmg.sha256, commit, and push. - Prove: Fetch the remote SHA and confirm it equals the first line of the local state file. If not, the script exits with failure.
- Stop any running Pommedoro, clean the build, and run
-
Result
- The app launched by
Install.commandis the same build as the one now on the server. - “Check for Updates” in the menu should show Up to Date.
- The app launched by
Without --push:
./build-test.sh runs the same build and install checks but does not commit or push. Use this to verify the DMG and install flow before you push.
When to run what
| Goal | Command |
|---|---|
| Code and run locally without installing | make run |
| Install to /Applications and develop (no update prompts) | make install |
| Build a DMG only (e.g. to test the image) | make release (or make dmg for .build/ only) |
| Test full install from DMG and state stamp | ./build-test.sh |
| Test, push release, and prove update state | ./build-test.sh --push |
Troubleshooting
-
“Update available” even though I just pushed / same version
The app compares the first line of~/Library/Application Support/Pommedoro/current.sha256to the SHA from the server. If you installed from a DMG and then pushed that same build, run./build-test.sh --pushso the script pushes and then verifies remote SHA = local state. If the script’s proof passes but the app still nags, the app may be reading a different path (e.g. different user); the script and app both use the same state file path. -
I’m a dev; I don’t want update prompts
Usemake install. It writesdevinto the state file so the app never offers to update. Installing from a DMG (or updating in-app) overwrites that with a real SHA + timestamp. -
State file location
~/Library/Application Support/Pommedoro/current.sha256. Two lines: SHA, then timestamp (or0). For a dev build, the first line is the literal stringdev.
Requirements
- macOS 13+
- Swift 5.9+
- Apple Silicon or Intel Mac
Author
Colin Knapp — colinknapp.com
License
This work is licensed under Creative Commons Attribution 4.0 International (CC BY 4.0).
You are free to share and adapt this work for any purpose, including commercially, as long as you give appropriate credit.