adding mac support mainly

This commit is contained in:
lwoodard
2026-04-13 08:11:19 -06:00
parent 9c681e482e
commit 0f5a136764
9 changed files with 657 additions and 190 deletions

98
mac-support.md Normal file
View File

@@ -0,0 +1,98 @@
# 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) |