# freeslapmac-lidcreak — build spec

**Goal.** FreeSlapMac 1.1.0 ships a *Lid Creaking Sound* switch that does nothing. Make it work:
a lid movement produces a creak, a slow drift does not, and the whole thing is installable by
someone else as a small package — without modifying the app bundle.

## Constraints

- **No bundle surgery.** The app is signed; patching it breaks updates and cannot be shared as a
  diff. Everything lives outside `FreeSlapMac.app`.
- **No root.** The sensor must be read as the logged-in user; no `sudo`, no privileged helper,
  no TCC prompt.
- **Reuse the app's own asset.** `Contents/Resources/Sounds/creaking.mp3` already ships in the
  bundle and is referenced zero times. Play *that*, not a downloaded sound.
- **The in-app toggle must actually control it.** If the user turns *Lid Creaking Sound* off, the
  daemon stays silent.
- **Survive reboot and app updates.** LaunchAgent with `RunAtLoad` + `KeepAlive`.
- **No false triggers while typing.** A laptop on a desk picks up everything; the trigger has to
  be a speed threshold, not an angle delta.

## Facts established before writing code

| item | value | how |
|---|---|---|
| sensor | HID usage page `0x20`, usage `0x8A`, feature report 1 | enumerated SPU HID devices, read every feature report |
| payload | 3 bytes, `01 68 00` → `0x0068` = 104° | byte 0 is the report ID, bytes 1–2 uint16 LE |
| privileges | none | `IOHIDDeviceOpen` + `IOHIDDeviceGetReport` as a normal user |
| app toggle pref | `com.devesh.freeslapmac _creakingEnabled` | `defaults read` |
| asset | `Sounds/creaking.mp3`, 150,816 B, 5.136 s | `afinfo` |
| free app has | label, description, preference | `strings` |
| free app lacks | `creaking.mp3` reference (0), `LidAngle` (0) | `strings \| grep -c` |
| paid app has | `LidAngleSensor`, `CreakAudioEngine`, `lidCreakEnabled` | `strings` |

## Algorithm

```
every 50 ms (20 Hz):
    angle = read_feature_report(dev, 1)        # None if the device is gone
    if angle is None: re-enumerate, continue
    dt = now - prev_t
    if dt > 0.5: prev = angle; continue        # sleep/stall: pair is meaningless
    speed = abs(angle - prev) / dt
    if speed >= 8 and now - last_creak >= 1.4 and gate_open():
        creak(volume = clamp(0.35 + speed/60, 0.35, 0.9))
```

`gate_open()` = FreeSlapMac process exists **and** `_creakingEnabled` is true (missing key counts
as true, so a renamed pref fails open rather than silently muting the feature).

Playback: `afplay -t 1.8 -v <volume> <bundle>/Contents/Resources/Sounds/creaking.mp3`, fired
detached so a 20 Hz loop never blocks on audio.

## Files

| file | role |
|---|---|
| `lidcreak.py` | the daemon; `--self-test`, `--trigger`, `--volume`, `--watch` |
| `install.sh` | copies the helper, writes and bootstraps the LaunchAgent, flips the pref, self-tests |
| `uninstall.sh` | boots out the agent, removes helper and logs, leaves the app alone |
| `README.md` | what broke, install/verify/remove, tuning, limitations |
| `LICENSE` | MIT |

## Verification (must all be real, not asserted)

1. `--self-test` prints the live angle, app-running state, toggle state, plays the asset.
2. Synthetic sweep: injecting one fast angle change fires exactly one creak with the expected volume.
3. 25 s of idle: zero creaks.
4. Real hand tilt: log shows `move … speed=…  trigger=YES` followed by `creak vol=…`.
5. Install/uninstall cycle run twice against a sandbox `$HOME` and a test launchd label, exit 0,
   live agent untouched.
6. Reboot test: `launchctl list | grep lidcreak` after login.

## Known limits

- A closed lid sleeps the Mac before the hinge finishes, so the creak fires *during* the movement.
- No sensor on Intel Macs or desktops → installer must refuse instead of installing a silent agent.
- The app's toggle remains dead UI; this is a companion, not a patch.
