Build log · Software & apps

Lid Creak 2.2.0 — the lid-angle app FreeSlapMac never wrote

Software & appsdone4 versionsmacOS appSwift · 1,170 linesHID 0x20 / 0x8A10/10 testsarm64

✅ reflection written · every fact below is generated from this project’s own spec and files

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.

V1

launchd + Python helper: read the sensor, played the clip, no window, no icon.

V2

Swift rewrite as a menu-bar accessory: worked, but double-click opened nothing visible.

V2.1

Regular app with a 480x309 window, Dock icon and reopen handling.

V2.2

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
  • 0.5 s stall discard are reasoned, not measured against a corpus of real traces.

  • The eight timelines are synthetic by construction: they prove the decision, not the acoustics.
  • Nobody has listened to a thousand creaks and graded them.

  • The sound belongs to FreeSlapMac. That is a licensing boundary, and it also means the download is
  • 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.

  • arm64 only, ad-hoc signed. Both are fixable (universal build; a Developer ID), neither was
  • worth doing for a personal utility.

  • The 1.4 s cooldown is taste. It is the one number on the page that is not derived from anything.

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:

  1. FreeSlapMac 1.1.0 has a Lid Creaking Sound toggle with no sensor code behind it.
  2. The first fix was a launchd + Python helper — it worked, but it was not an app, and
  3. "make it an app" meant a thing with a window.

  4. The app that replaced it was a menu-bar accessory (LSUIElement + .accessory), so a
  5. double-click on an already-running copy did nothing visible: "the app doesnt open".

  6. With a window finally visible, the sensitivity setting was exposed as fake — every level
  7. fired on a one-degree movement.

What had to be true at the end

  • A real .app in /Applications that opens a window on double-click, with a Dock icon, a
  • menu, ⌘Q, and applicationShouldHandleReopen so a second double-click reopens it.

  • It plays a creak (the complaint was never "make it silent" — the silent fork is a
  • separate, earlier build).

  • 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
  • sensitive setting, and the difference must be provable without moving a real lid.

  • The sound is FreeSlapMac's and is not redistributed.
  • Ship it: a downloadable zip, a build log page, and a card on the portfolio.

Hardware facts established first

| fact | value |
|---|---|
| device | HID usage page 0x20 (sensors) / usage 0x8A (lid angle) |
| read | feature report 1, uint16 little-endian, degrees |
| resolution | 1° — whole degrees |
| persistence | the device disappears when the lid closes; re-enumerate |

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:

| gate | rule | constant |
|---|---|---|
| travel | ≥ 3° across a 0.35 s window | kMinTravel, kWindow, kMinWindow = 0.18 |
| speed | measured across a ≥ 0.15 s span | kSpeedWindow, kSpeedMin = 0.10 |
| sustain | 2 consecutive qualifying ticks | 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 by build.sh before it will finish.
  • --record N — log a real sensor trace so a real creak can be replayed offline.

Audio path

  1. Find creaking.mp3 in FreeSlapMac.app / FreeSlapMac Silent.app / SlapMac.app /
  2. slapmac.app.

  3. Render the first 1.8 s to ~/Library/Application Support/LidCreak/creak.caf, normalised
  4. 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.

  5. Playback volume rides speed: 0.55 + speed/120, clamped to 1.0.
  6. --selftest exits 2 and says so when the output device is muted or at volume 0 — the
  7. 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

  • ditto for the bundle (preserves the signature), ditto -c -k --sequesterRsrc --keepParent
  • for the zip.

  • SHA256SUMS.txt covering the binary and every source file.
  • Verified by extracting the zip back out: codesign --verify --deep --strict valid,
  • lipo -archs = arm64, shasum -c 8/8 OK, --simulate PASS from the extracted copy.

Acceptance

| check | result |
|---|---|
| --simulate 8 timelines | 8/8 PASS (1° blip = 6.7 °/s, 0 fires) |
| --trigger-test | 10/10 PASS |
| zip extracted and run | signature valid, 8/8 checksums, PASS |
| window opens on launch | yes — .regular app, 480×309 window, Dock icon |
| audio | plays FreeSlapMac's clip, volume rides speed |
| admin | none |