The first version of Skopia had one job: fire a gentle prompt every twenty seconds so the user blinks. It worked, it was small, and it was very nearly unusable.
Not because the reminders were bad. Because of the demo. Sharing a screen, mid-sentence, in front of a customer, a small eye appeared in the middle of the display. Everybody saw it. That is the moment an app is uninstalled, and no amount of correctness about blink rates argues with it.
Version 1.2.0 is almost entirely about not doing that. Here is what the problem turned out to be.
The real failure mode
Reminder apps are usually evaluated on whether they remind you. In practice they are abandoned on the opposite axis. Nobody deletes a break app for missing a break. They delete it for firing during:
- a video call, where an overlay lands on a shared screen
- a screen recording, where it is baked into the file and the take has to be redone
- a film, where it is simply rude
- a full-screen editor or game, where it breaks the exact state the user went full screen to protect
The asymmetry is severe. A missed reminder costs a few seconds of dry eye. A badly timed one costs the whole product. So the design question stopped being "when should we remind?" and became "when must we absolutely not?"
Five conditions, in priority order
Skopia checks five things, in a fixed order, and the first active one wins for display purposes:
| Condition | Default |
|---|---|
| On a callWho holds the microphone (CoreAudio) | On |
| Screen recordingA known recorder's power assertion, or the built-in recorder | On |
| Video playbackDisplay-sleep power assertions | Off — see below |
| Deep focus appsA list you choose | On |
| Full screenWindow geometry matches a screen | On |
The ordering is not arbitrary. When several conditions are active at once — you are on a call, in full screen, in an app you marked as focus — the menu bar shows one of them, and it should be the one that explains the situation best. "On a call" is a better explanation than "full screen", even when both are true.
The detection itself is covered in detail in the post on why Skopia has no camera. Short version: every one of the five reads public, permission-free system metadata. No camera, no microphone permission, no screen recording permission, no accessibility access.
Six rules that came out of building it
1. Freeze, do not skip
The first implementation suppressed the reminder and let the clock run. That is wrong, and it is wrong in a way that only shows up after an hour-long meeting: you come off the call and are immediately hit with a reminder, then a 20-20-20 window, then a palming break, because three counters all expired while you were talking.
Now everything freezes. The blink countdown and both rest counters stop and resume from the value they held. A break you could not take is not a break you owe. Skipping and freezing look identical for thirty seconds and completely different for sixty minutes.
2. Withdraw what is already on screen
Suppressing the next prompt is not enough. If a rest overlay is already visible when a call starts, it has to disappear — and it does, along with a reset of that exercise's counter. The condition that starts one second after the overlay appears is exactly the case a naive implementation gets wrong.
3. Debounce asymmetrically
Entering a state and leaving it deserve different confidence thresholds.
Video playback and screen recording only clear after three consecutive negative readings — about six seconds. Without that, the pause between two clips, or the moment a player buffers, resumes reminders for four seconds and then suspends them again. The user sees a flicker and concludes the feature is broken.
Video playback also enters slowly — four consecutive readings, about eight seconds. A video call holds a display-sleep assertion too, so without that delay the menu bar would flash VIDEO for the first seconds of every call before the microphone signal caught up and corrected it to ON CALL.
Full screen and deep focus apps switch immediately, because their signals are stable. Calls get their own debounce further down, in the microphone state machine: two consecutive positive readings to enter, four to leave, so a notification sound is not a meeting.
4. When the signal is unreliable, default to off
Video playback detection works by reading which processes hold a display-sleep power assertion. Video players hold one. So do Amphetamine and caffeinate — permanently, by design.
Enabled by default, that combination pauses Skopia forever for every user of those tools. They would not see a pause condition working as intended; they would see an app that silently stopped doing its job, and they would be right to uninstall it. So the condition ships off, and the user turns it on knowing what it keys off.
The general rule: a heuristic you cannot fully trust should be opt-in, not a surprise.
5. Always say why
An app that goes quiet without explanation is indistinguishable from an app that crashed. When Skopia suspends, the menu bar says which condition is active, and the popover names the application behind it — "on call · zoom.us", not just a stopped countdown.
This turned out to matter more than expected, and not only for trust. It is the debugging surface. When someone reports "it stopped reminding me", the menu bar has usually already answered the question, and the answer is often a stale power assertion.
6. Manual beats automatic
Pausing by hand clears the entire active condition set. The user's explicit decision is not something a heuristic gets to argue with. On resume, the next tick recomputes everything from scratch. Automatic behaviour should never be able to override an instruction the person actually gave.
Making it testable
Detection code that talks to CoreAudio, IOKit and the window server is the kind of code that never gets tested, because setting up the world is harder than writing the logic. The fix was to separate the two everywhere.
- The call state machine is a pure value type. It has no timer, no CoreAudio, no main thread — it takes a sequence of readings and reports state changes. The debounce logic is fully testable without a microphone.
- The full-screen rules take a window list as an argument. The function that decides "is this window full screen on this display" is pure, so it can be tested against synthetic window dictionaries with no full-screen app and no second monitor.
- The same shape applies to screen recording detection: the rule is a pure function over a window list and a set of process IDs.
Only the thin layer that fetches the real snapshot touches the system. Everything with an opinion in it is testable, and the pause and reminder logic now has unit tests covering exactly these cases.
The cost
Four of the five conditions are evaluated in a single poll every two seconds, sharing one snapshot rather than each running its own query. Full screen and deep focus apps therefore suspend within about three seconds, counting the one-second timer tick.
Calls are slower, deliberately. The microphone runs on its own three-second poll, and the state machine wants two consecutive positive readings before it will call something a meeting — so entering a call takes up to about ten seconds, and leaving one about twelve. Video playback needs four consecutive readings, roughly eight seconds. The built-in recorder needs two, roughly four.
None of that is a delay anyone notices, and the alternative — polling faster and trusting single readings — would spend battery to buy false positives.
What it added up to
The reminder logic in Skopia is a few dozen lines. Knowing when not to run it is several hundred, plus the tests. That ratio felt wrong while building it and looks correct now.
A tool that lives in your menu bar all day is judged on its worst moment, not its average one. Getting the worst moment right is most of the work.
Everything described here shipped in Skopia 1.2.0.
Skopia pauses on calls, screen recordings, full screen and apps you mark as deep focus — and tells you which one it is. Quiet Mac menu bar app, $4.99 one-time.
Get Skopia — $4.99 →