What it is
A 1,170-line native macOS app that reads the MacBook lid-angle sensor 20 times a second and plays FreeSlapMac's own unused creaking.mp3. FreeSlapMac ships the toggle, the preference and the sound — but no sensor code. Then the sensitivity setting turned out to be arithmetic: 1 degree in one 50 ms sample is 20 deg/s, which beat every threshold, so all three levels fired on a twitch.
Version history
Straight from the project’s own card data — what each version added.
launchd + Python helper: read the sensor, played the clip, no window, no icon.
Swift rewrite as a menu-bar accessory: worked, but double-click opened nothing visible.
Regular app with a 480x309 window, Dock icon and reopen handling.
Detector rewritten: span-averaged speed, 3 degree travel gate, 2-tick sustain; thresholds rescaled to 35 / 22 / 14 deg/s. 10/10 + 8/8 tests.
Files on the site
Full analysis
Reflection — Lid Creak 2.2.0
Four fixes, four different failures, one feature. The interesting part is that each one was only visible once the previous one was gone, and that I got to the last one by being told the number.
1. The switch was a decoration
FreeSlapMac 1.1.0 advertises "Lid Creaking Sound", stores _creakingEnabled, and ships creaking.mp3 in a 267-clip pack. Reading strings out of the binary was what settled it: there is no HID usage page 0x20 access and no reference to that asset anywhere in the code. The toggle writes a preference nothing reads.
The lesson is about evidence, not code. The UI said the feature existed; the binary said it did not. A screenshot would have agreed with the user ("it doesn't work"), a symbol dump explained why. When a feature "doesn't work", the first question is whether it is there.
2. "It works" was not the ask
The first build was a launchd agent running a Python script. It genuinely worked — it read the sensor and played the clip. The reply was: "is it an app?" and then "make it an app".
That is a fair distinction I had collapsed. Something that runs invisibly, with no icon, no window and no Force Quit entry, is a daemon, and a user cannot consent to, inspect or stop a daemon the way they can an app. "Working" and "being an app" are separate requirements, and only one of them was in my head.
3. The app opened to nothing
The Swift rewrite was a menu-bar accessory: LSUIElement = true, setActivationPolicy(.accessory). Double-clicking it when it was already running re-activated a process with no window — so the honest report was "the app doesnt open", while pgrep showed it had been running for days.
I had verified the wrong thing: my checks were pgrep, codesign, launchctl — all true, all irrelevant to "what does the user see when they double-click". The fix was structural (.regular, a real window, applicationShouldHandleReopen), and the verification had to change with it: render the window's own content view, and check isVisible / occlusionState / isOnActiveSpace.
Also worth recording: screencapture fails on this Mac ("could not create image from display") because screen recording is not granted, so a --snapshot mode that draws the window into a PNG from inside the app was the only way to see my own UI. Necessity, but a good one — it runs headless in a build script.
4. The sensitivity setting was arithmetically impossible
"the sensitivity didnt matter i did low and i angle changed 1 degree still creaked" — with the number included, which is what made it findable in minutes instead of hours.
v2.1.0 measured speed = |Δangle| / 0.05 s
the sensor reports whole degrees → 1° / 0.05 s = 20 °/s
Low was 15 °/s
So the least sensitive setting was already more sensitive than the smallest movement the hardware can express. Every level fired. The thresholds were a distraction: three different numbers all above 20 behave identically on a 1° input, which is exactly what "the sensitivity didnt matter" describes.
The right response was not to raise the numbers. It was to change the quantity: measure speed across a span wide enough that quantisation stops dominating, gate on total travel, and require the condition to hold for more than one tick. The numbers then had something to scale against.
What I should have done at the start: write down the sensor's resolution, then check that the smallest possible movement sits below the least sensitive threshold. That one line of arithmetic would have caught it before a single build. It is now the first row of both the simulate suite and the boundary tests, named "the old bug".
What the fix bought, beyond this app
The decision logic is now a pure Detector fed by (time, angle) samples, with the sensor as just one caller. That turned an untestable hardware behaviour into three cheap modes — --simulate, --trigger-test, --record — and it is the reason the answer to "did you actually fix it" is a table of eight timelines instead of "try moving your laptop".
Honest about what is still unproven
- Everything is verified on one machine, one macOS version, one lid. The 40° jump threshold and the
- The eight timelines are synthetic by construction: they prove the decision, not the acoustics.
- The sound belongs to FreeSlapMac. That is a licensing boundary, and it also means the download is
arm64only, ad-hoc signed. Both are fixable (universal build; a Developer ID), neither was- The 1.4 s cooldown is taste. It is the one number on the page that is not derived from anything.
0.5 s stall discard are reasoned, not measured against a corpus of real traces.
Nobody has listened to a thousand creaks and graded them.
inert on a Mac without it — the app says so rather than failing quietly, but it is a real limit of publishing a companion rather than a standalone.
worth doing for a personal utility.
The pattern worth keeping
Three of these four failures were the same mistake in different clothes: I verified the mechanism and called it the outcome. The daemon ran → "it works". The process existed → "it opens". The slider moved → "sensitivity works". Each time the user was reporting the outcome, and I was reporting the mechanism. The detector rewrite is the version of this where I finally built the outcome into the test: eight timelines that would have failed before the fix, and one row named after the bug report that started it.
Build spec
The project’s own build notes, rendered. Open the raw .md ↗
Show the spec
Build spec — Lid Creak 2.2.0 (native macOS app)
The ask, in the user's words
"can you stop the creaking sound" → then, after the launchd helper was explained:
"is it an app?" → "make it an app" → "the app doesnt open" → "the sensitivity didnt matter
i did low and i angle changed 1 degree still creaked"
Four separate failures behind one feature, each one only visible after the previous was fixed:
- FreeSlapMac 1.1.0 has a Lid Creaking Sound toggle with no sensor code behind it.
- The first fix was a launchd + Python helper — it worked, but it was not an app, and
- The app that replaced it was a menu-bar accessory (
LSUIElement+.accessory), so a - With a window finally visible, the sensitivity setting was exposed as fake — every level
"make it an app" meant a thing with a window.
double-click on an already-running copy did nothing visible: "the app doesnt open".
fired on a one-degree movement.
What had to be true at the end
- A real
.appin/Applicationsthat opens a window on double-click, with a Dock icon, a - It plays a creak (the complaint was never "make it silent" — the silent fork is a
- It uses only public APIs: no kernel extension, no admin password, no private framework.
- Low / Normal / High must measurably differ: a 1° movement must not creak at the least
- The sound is FreeSlapMac's and is not redistributed.
- Ship it: a downloadable zip, a build log page, and a card on the portfolio.
menu, ⌘Q, and applicationShouldHandleReopen so a second double-click reopens it.
separate, earlier build).
sensitive setting, and the difference must be provable without moving a real lid.
Hardware facts established first
0x20 (sensors) / usage 0x8A (lid angle) |uint16 little-endian, degrees |The third row is the whole sensitivity bug. A sensor that only reports integers cannot express speed inside one sample; it can only report that the number changed.
The arithmetic that had to be fixed
v2.1.0: speed = |angle[n] − angle[n−1]| / 0.05 s
1° / 0.05 s = 20 °/s
thresholds: Low 15 · Normal 8 · High 5 °/s → every level fires
The detector that replaced it
Three gates, none of which trusts a single sample:
kMinTravel, kWindow, kMinWindow = 0.18 |kSpeedWindow, kSpeedMin = 0.10 |kConfirmTicks |Retrigger cooldown kRetrigger = 1.4 s. Discard a ΔT > 0.5 s (sleep/stall) and any single-step jump over 40° (lid closing / machine moving) instead of dividing by it.
Rescaled thresholds: Low 35 · Normal 22 · High 14 °/s.
Testability, which is the actual fix
The decision logic had to be a pure object:
final class Detector {
func feed(time: TimeInterval, angle: Double) -> Decision // .creak / .quiet
}
LidSensor only feeds it. That buys three modes:
--simulate— eight scripted lid timelines (the regression suite for the bug).--trigger-test— ten boundary assertions, run bybuild.shbefore it will finish.--record N— log a real sensor trace so a real creak can be replayed offline.
Audio path
- Find
creaking.mp3inFreeSlapMac.app/FreeSlapMac Silent.app/SlapMac.app/ - Render the first 1.8 s to
~/Library/Application Support/LidCreak/creak.caf, normalised - Playback volume rides speed:
0.55 + speed/120, clamped to 1.0. --selftestexits 2 and says so when the output device is muted or at volume 0 — the
slapmac.app.
to −1 dBFS, gain measured from the clip's own peak (≈ ×8; source peaks ≈ 0.11 / −19 dBFS). Idempotent: re-running --selftest does not re-render.
failure that made an earlier build look broken.
Build
arch -arm64 swiftc -O -swift-version 5 -target arm64-apple-macos13.0 LidCreak.swift \
-o "Lid Creak.app/Contents/MacOS/LidCreak" \
-framework AppKit -framework AVFoundation -framework IOKit -framework QuartzCore
codesign --force --deep --sign - "Lid Creak.app"
arch -arm64 is required because the agent shell runs under Rosetta (sysctl -n sysctl.proc_translated is non-zero); without it the compiler resolves an x86_64 swift-driver and dies on llbuild.framework.
Packaging
dittofor the bundle (preserves the signature),ditto -c -k --sequesterRsrc --keepParentSHA256SUMS.txtcovering the binary and every source file.- Verified by extracting the zip back out:
codesign --verify --deep --strictvalid,
for the zip.
lipo -archs = arm64, shasum -c 8/8 OK, --simulate PASS from the extracted copy.
Acceptance
--simulate 8 timelines | 8/8 PASS (1° blip = 6.7 °/s, 0 fires) |--trigger-test | 10/10 PASS |.regular app, 480×309 window, Dock icon |