# 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) |