Changelog: QuitProtect 2.0.9, 2.1.0 and 2.2.0

A hand ticking items off a checklist.

Photo by Jakub Żerdzicki on Unsplash

QuitProtect stops you quitting an app by accident. You press commandQ, nothing happens, and you get on with your life. Press it twice, or hold it, and the app quits as normal. It is about ninety lines of interesting code wrapped in a menu-bar icon, and it has been quietly doing its job since March.

On Thursday somebody I have never met opened three pull requests against it.

That has not happened before. Jorvik apps are open source because I think they should be, not because I expect anyone to turn up. Mostly nobody does. So the first thing I want to say is that RSS1102 did real work here, unpaid, on someone else’s project, and did it carefully. Two of those three are shipping today.

The third one taught me more than either.

First, an embarrassment

The pull requests arrived on Thursday. I read them on Sunday.

Not because I was ignoring them — because I never knew they existed. I have a menu-bar app, Lookout, whose entire job is to tell me when something on GitHub wants my attention. It stayed silent for three days while three pull requests sat there.

The reason is a single word in a search query, and I will write about that separately, because it is really Lookout’s story rather than QuitProtect’s. The short version: GitHub’s search treats issues and pull requests as different things, and I had only ever asked it about issues. Lookout was working perfectly. It just wasn’t answering a question I had failed to ask.

Fixed now. But it is a useful reminder that a tool which tells you when something needs attention is only ever as good as your definition of something.

QuitProtect 2.1.0: 简体中文

The first pull request localises the app into Simplified Chinese. Set your Mac to Chinese and the whole interface — menu, settings, About panel — is now in Chinese. Everyone else sees exactly what they saw before; not one English string changed.

I cannot read Chinese. I can, however, check the things that are checkable, and I did: the two string tables have identical keys, no orphans, no duplicates, no mismatched format specifiers. The typography is what convinced me it was careful work rather than a machine translation — full-width punctuation, the correct Simplified Chinese quotation marks rather than the Traditional ones, the 破折号 rendered properly as a double em dash, and a space between every run of Latin characters and the Han characters beside it. Apple’s own macOS terminology throughout, too, rather than invented equivalents.

Those are the details you only get right if you care.

To be clear about what is not covered: Traditional Chinese falls back to English, as does every other language. The README and the product page now say so plainly, because the original wording implied a wider net than actually exists.

The bit that made it bigger than one app

Here is where it got interesting.

All my apps share a small toolkit — the About panel, the settings shell, the menu builder, the menu-bar icon. It is not a library; it is a folder of Swift files copied verbatim into every app, and a change in one is propagated by hand to the rest. Crude, but I know exactly what is in every app, which is worth a lot.

The localisation touched six of those shared files. That created a problem I very nearly missed.

The helper that looks up translated strings lived outside the toolkit, in QuitProtect. So those six shared files now referenced something they did not contain. Copy them into any other app and they would not compile.

That much is loud, and easily caught. The dangerous direction is the other one. The next time I propagated an unrelated toolkit change into QuitProtect, it would have quietly reverted every translated menu and settings label back to hardcoded English. It would have compiled without a single warning. The Chinese strings file would still have been sitting in the app bundle, looking perfectly correct, doing nothing.

I would have found out months later, from a user, if at all.

The fix was to move the helper into the toolkit and make it public — and once you do that, something rather nice falls out. The whole toolkit becomes localisable, which means every Jorvik app can now be translated the same way, not just this one. RSS1102 turned that round in about twenty minutes.

Before adopting it across the estate I wanted to be certain it could not break the twelve apps that have no translations at all. The mechanism relies on every lookup carrying its English text as a fallback, so an app with no translation files should get the English back untouched. I did not want to assume that, so I ran it: a program with no translation files at all, asking for three strings. Back came Quit Mode, Fallback Works and Blocked 7 times, exactly as written.

That is the entire safety property, and it took two minutes to prove rather than believe.

QuitProtect 2.2.0: an optional nudge

The second pull request adds a small overlay. Switch on Show quit guidance in Settings and the first commandQ puts a little panel on screen telling you what to do next — press again within the interval, or keep holding.

It is off by default, and I want to explain why I insisted on that, because the first revision had it on.

QuitProtect’s entire premise is that the first commandQ is absorbed invisibly. That is the product. Shipping a version that suddenly starts drawing on screen for every user who happens to have auto-updates on is a change to what they bought into, made on their behalf, without asking. Worse in hold-to-quit mode, where the overlay fires the instant you press the key — so a momentary accidental commandQ, precisely the event this app exists to swallow silently, would have painted something on screen.

The window handling underneath is genuinely good work: the panel never takes focus, cannot become the active window, is transparent to the mouse, and floats above full-screen apps. Whatever you were working in stays active. That is harder to get right than it sounds and it was right first time.

My other objection was subtler, and the response to it is the best thing in the whole exchange.

In the first version the overlay stayed up for a fixed period after it appeared — the double-press interval plus 0.4 seconds, or the hold duration plus 0.3. Which meant it routinely outlived the thing it was describing. Double-press in Safari with a short interval: Safari quits after about a fifth of a second, and the overlay carries on telling you to press commandQ again for the best part of a second, attached to an application that no longer exists. Tap and release in hold mode and you get over a second of instruction to keep holding a key you have let go of.

I have a rule about numbers like +0.4 and +0.3. If you cannot say where a constant came from, it is not a tuning value, it is an undiagnosed problem wearing a disguise. And these were a textbook case: they existed only because nothing ever told the overlay that the gesture had finished. They were padding around a missing signal.

The obvious response would have been to name them and move on. Instead the revision added the missing signal — the engine now reports when a quit gesture begins and when it resolves, across every path: the accepted second press, the key release, the timeout, a mode change, shutdown. Eleven places in all. And then it deleted both constants, because with a real signal there is nothing left to guess.

What survived is a single minimum visible duration of 0.6 seconds, kept for a reason that can actually be stated: below that a fade-in and fade-out is most of the animation and the thing is physically unreadable. That is derived from human legibility rather than from taste, and it is written down next to the number.

Working out why a constant exists before naming it is the harder half of that job, and much rarer than it should be.

QuitProtect 2.0.9: the one I got wrong

The third pull request changed when the app asks for Accessibility permission. I read it closely, went to Apple’s headers, and found that it made no difference at all — the system call it replaced already only prompted when permission was actually missing. The change was reasonable, well-intentioned, and a no-op.

But reading it sent me line by line through the permission and event-tap code, which I had not looked at properly in months. And there I found three real bugs, all mine.

The worst one is the reason this got its own release.

QuitProtect needs Accessibility permission to see your keystrokes. If you revoke that permission while the app is running — untick it in System Settings — macOS immediately kills the mechanism the app uses to watch the keyboard. It does not tell the app. The app’s code simply stops being called, and it has no way of knowing the difference between “nobody has pressed anything” and “I have been switched off”.

So QuitProtect carried on with a filled-in menu-bar icon and a ticked Protection Active menu item, over a mechanism that was dead. It was telling you it was guarding your unsaved work when it was doing nothing of the kind.

For an app whose entire job is absorbing one keystroke, that is the single failure it must never have. Everything else here is a nicety.

It is fixed twice over, deliberately. The app now checks whether its keyboard hook is alive rather than remembering that it once was — so the icon and menu cannot go stale, whatever else happens. And it also listens for the permission being withdrawn, so it reacts immediately and puts protection back on its own if you grant it again. Belt and braces, because the second mechanism relies on an undocumented system notification and I would rather not bet a safety feature on it.

The other two were smaller. If the keyboard hook failed to start even with permission granted, the app went inert with no protection and no route back short of a restart; it now keeps trying. And toggling protection off and on while permission was missing left a stray timer running each time.

I also deleted four pieces of state that were written but never read. State that can drift from reality while nobody is looking is worse than no state at all, and this file had four of them.

Credit

RSS1102 is acknowledged in the README and on the product page, for both contributions. The translation is the visible one. The toolkit change is the one that will still be paying off in a year, across apps he has never opened.

Three pull requests, two merged, one closed with thanks — and the closed one is the reason a real bug got fixed. That is a good week’s work from someone who owed me nothing.

Shipping now, via the download page or Homebrew, whichever you prefer.