# macOS Support — Change Catalog

All changes made on the `Mac` branch to achieve a unified Linux/macOS codebase.
No Windows support planned.

---

## Status Legend
- ✅ Done
- ⬜ Pending

---

## Design philosophy

The goal is a single binary that works on both platforms with no build tags. All
platform differences are resolved at runtime via `runtime.GOOS`. The user experience
should feel native on each OS — launchd on macOS, systemd on Linux, etc.

The biggest architectural difference is privileged commands:
- **Linux** uses a root helper daemon + Unix socket. The whitelist is root-owned so the
  user cannot modify it. The main process never runs as root.
- **macOS** skips the daemon entirely. `priv:` commands are looked up in the user's own
  `privileged.yaml` and executed via `osascript`, which shows the standard macOS admin
  auth dialog. No root process, no group management, no socket chown.

---

## Code Changes

### `internal/device/streamdeck.go`
- ✅ **Platform-aware open error hint** — Linux: `sudo chmod a+rw /dev/hidraw*`; macOS: `brew install hidapi` + Input Monitoring note. The hint is embedded in the error returned from `Open()` so it surfaces wherever the error is logged.
- ✅ **Broaden HID read error matching** — `ReadButtons()` catches `"timeout"`, `"timed out"`, and `"interrupted"` as non-fatal. Linux hidraw returns `"timeout"`; macOS IOHIDManager may return `"timed out"`. Both should result in a retry rather than counting toward the 3-error device-death threshold.

### `cmd/streamdeck/main.go`
- ✅ **Runtime helper socket path** — `helperSocketPath()` returns `/run/streamdeck-go/helper.sock` on Linux and `/var/run/streamdeck-go/helper.sock` on macOS. `/run` is a Linux-specific tmpfs; `/var/run` is the macOS equivalent.
- ✅ **No root daemon on macOS** — `runPrivileged()` dispatches to:
  - `runPrivilegedDarwin()` on macOS: reads `~/.config/streamdeck-go/privileged.yaml`, looks up the command, runs it via `osascript -e 'do shell script "..." with administrator privileges'`. The OS shows its standard admin auth dialog. No daemon, no sudo, no group.
  - `runPrivilegedHelper()` on Linux: existing Unix socket approach, unchanged.
- ✅ **Remove dead code** — unused `blank()` function removed.

### `cmd/streamdeck-helper/main.go`
- ✅ **Runtime socket path** — same `/run` vs `/var/run` split as above. The helper binary is Linux-only in practice but the path function is correct on both platforms for consistency.
- ✅ **Runtime whitelist path** — Linux: `/etc/streamdeck-go/privileged.yaml`; unchanged (helper not used on macOS).

---

## New Files

### `launchd/com.woodarddigital.streamdeck-go.plist`
- ✅ macOS LaunchAgent for the user daemon. Installed to `~/Library/LaunchAgents/` by `install.sh`.
- `RunAtLoad: true` — starts at login.
- `KeepAlive: true` — restarts automatically if the process exits.
- Log path substituted at install time by `install.sh` via `sed` → `~/Library/Logs/streamdeck-go.log`. This path persists across reboots (unlike `/tmp` which is cleared on reboot).
- Binary path also substituted at install time.

### `launchd/com.woodarddigital.streamdeck-go-helper.plist`
- ✅ Kept for reference but **not used on macOS** — no root daemon needed.
- On macOS, privileged commands are handled inline via osascript (see above).

---

## Updated Files

### `install.sh`
- ✅ **OS detection** — `uname -s` at startup sets `IS_MAC=true/false`. All platform-specific blocks branch on this.
- ✅ **Dependency detection** — macOS: checks for hidapi dylib or pkg-config, installs via `brew`. Linux: existing pkg-config / ldconfig paths.
- ✅ **Skip udev on macOS** — macOS HID devices are accessible via IOKit without any device rules. The step is replaced with an informational message about Input Monitoring.
- ✅ **Binary install path** — macOS: `~/go/bin/` (no sudo required). Linux: `~/.local/bin/`. A PATH warning is shown on macOS if `~/go/bin` is not in the current shell's PATH.
- ✅ **Service management** — macOS: generates the launchd plist from the template (substituting binary + log paths via `sed`), then `launchctl load`. Linux: `systemctl --user enable --now`.
- ✅ **Log path** — macOS: `~/Library/Logs/streamdeck-go.log` (persistent). Linux: journald (unchanged).
- ✅ **`readlink -f` portability** — macOS ships without `readlink -f` (requires coreutils). Replaced with `realpath_portable()` which tries `realpath`, then `python3 -c "os.path.realpath()"`, then a basic `~`-expansion fallback.
- ✅ **`install -D` portability** — Linux `install -D` auto-creates parent directories; macOS `install` does not support this flag. Replaced with explicit `mkdir -p "$(dirname dst)"` + `install`.
- ✅ **Fix dotfiles path bug** — was `${DOTFILES}/streamdeck-go/.config/streamdeck-go`; corrected to `${DOTFILES}/.config/streamdeck-go` to match the pattern shown in the README.

### `Makefile`
- ✅ **OS detection** — `$(shell uname -s)` sets `OS`; `ifeq ($(OS),Darwin)` / `else` blocks set all platform-specific variables.
- ✅ **macOS `install-helper`** — no daemon, no group, no sudo. Just copies `config/privileged.example.yaml` to `~/.config/streamdeck-go/privileged.yaml`. The user edits it to add `priv:` commands.
- ✅ **Linux `install-helper`** — unchanged: `dscl`-free, uses `groupadd` + `usermod`, installs helper binary + systemd system service.
- ✅ **`udev` target** — guarded: prints a skip message on macOS, runs the udev rule install on Linux.
- ✅ **`uninstall` / `uninstall-helper`** — macOS: `launchctl unload` + file removal, no daemon to stop for helper. Linux: `systemctl disable` + file removal.

---

## Platform comparison

| Topic | Linux | macOS |
|---|---|---|
| HID access | udev rule grants `MODE="0666"` on hidraw | IOKit — no rules needed; accessible to user by default |
| Input Monitoring | N/A | May prompt on first run; Stream Deck is not keyboard/mouse so usually auto-granted |
| Service manager | systemd (user + system units) | launchd (LaunchAgent for user; no LaunchDaemon needed) |
| Socket dir | `/run/streamdeck-go/` | `/var/run/streamdeck-go/` |
| Privileged command mechanism | Root helper daemon + Unix socket | `osascript` admin auth dialog (inline, no daemon) |
| Whitelist location | `/etc/streamdeck-go/privileged.yaml` (root-owned 640) | `~/.config/streamdeck-go/privileged.yaml` (user-owned 644) |
| Binary location | `~/.local/bin/` | `~/go/bin/` |
| Log location | `journalctl --user -u streamdeck-go` | `~/Library/Logs/streamdeck-go.log` |
| Config path | `~/.config/streamdeck-go/` (XDG) | `~/.config/streamdeck-go/` (XDG — works fine on macOS for CLI tools) |
| Sleep/wake | Handled by reconnect loop | Handled by reconnect loop (same code) |
