245 lines
13 KiB
Markdown
245 lines
13 KiB
Markdown
# Pommedoro
|
||
|
||
**A Pomodoro timer that respects how your brain actually works.**
|
||
|
||
[Download the latest DMG](https://git.nixc.us/colin/pommedoro/raw/branch/main/releases/Pommedoro.dmg)
|
||
|
||
---
|
||
|
||
## 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](CHANGELOG.md) for release history and recent changes.
|
||
|
||
|
||
## Install
|
||
|
||
### 1. Download
|
||
|
||
Go to [colinknapp.com/stories/pommedoro.html](https://colinknapp.com/stories/pommedoro.html) or download the DMG directly:
|
||
|
||
**[Pommedoro.dmg](https://git.nixc.us/colin/pommedoro/raw/branch/main/releases/Pommedoro.dmg)**
|
||
|
||
### 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.
|
||
|
||
1. Open **System Settings** → **Privacy & Security**.
|
||
2. Scroll down to the message about "Pommedoro was blocked…" (or "from an unidentified developer").
|
||
3. 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 writing `dev` into 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**
|
||
Run `make install` again (or `make 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 after `make install`. Use `make uninstall-agent` to 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.
|
||
|
||
- **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 (or `0`).
|
||
- **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` — writes `dev` so the app never nags for updates.
|
||
|
||
- **When “Update available” is shown**
|
||
Only if **both** are true:
|
||
1. The release SHA on the server is **different** from the one in the state file.
|
||
2. 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.
|
||
|
||
1. **Run the full test and push the release**
|
||
```bash
|
||
./build-test.sh --push
|
||
```
|
||
This 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.
|
||
|
||
2. **Result**
|
||
- The app launched by `Install.command` is the same build as the one now on the server.
|
||
- “Check for Updates” in the menu should show **Up to Date**.
|
||
|
||
3. **Redeploy local (optional)**
|
||
To reinstall from the release DMG after pushing (e.g. you were on a dev build), run:
|
||
```bash
|
||
./redeploy-local.sh
|
||
```
|
||
This stops Pommedoro, mounts `releases/Pommedoro.dmg`, runs `Install.command`, then unmounts. Your local install then matches the release you pushed.
|
||
|
||
**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` |
|
||
| Reinstall from release DMG so local matches server | `./redeploy-local.sh` |
|
||
|
||
### Troubleshooting
|
||
|
||
- **“Update available” even though I just pushed / same version**
|
||
The app compares the **first line** of `~/Library/Application Support/Pommedoro/current.sha256` to the SHA from the server. If you installed from a DMG and then pushed that same build, run `./build-test.sh --push` so 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**
|
||
Use `make install`. It writes `dev` into 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 (or `0`). For a dev build, the first line is the literal string `dev`.
|
||
|
||
---
|
||
|
||
## Requirements
|
||
|
||
- macOS 13+
|
||
- Swift 5.9+
|
||
- Apple Silicon or Intel Mac
|
||
|
||
## Author
|
||
|
||
**Colin Knapp** — [colinknapp.com](https://colinknapp.com)
|
||
|
||
## License
|
||
|
||
This work is licensed under [Creative Commons Attribution 4.0 International (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/).
|
||
|
||
You are free to share and adapt this work for any purpose, including commercially, as long as you give appropriate credit.
|