6.6 KiB
6.6 KiB
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 ownprivileged.yamland executed viaosascript, 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 fromOpen()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.sockon Linux and/var/run/streamdeck-go/helper.sockon macOS./runis a Linux-specific tmpfs;/var/runis 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 viaosascript -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
/runvs/var/runsplit 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/byinstall.sh. RunAtLoad: true— starts at login.KeepAlive: true— restarts automatically if the process exits.- Log path substituted at install time by
install.shviased→~/Library/Logs/streamdeck-go.log. This path persists across reboots (unlike/tmpwhich 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 -sat startup setsIS_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/binis not in the current shell's PATH. - ✅ Service management — macOS: generates the launchd plist from the template (substituting binary + log paths via
sed), thenlaunchctl load. Linux:systemctl --user enable --now. - ✅ Log path — macOS:
~/Library/Logs/streamdeck-go.log(persistent). Linux: journald (unchanged). - ✅
readlink -fportability — macOS ships withoutreadlink -f(requires coreutils). Replaced withrealpath_portable()which triesrealpath, thenpython3 -c "os.path.realpath()", then a basic~-expansion fallback. - ✅
install -Dportability — Linuxinstall -Dauto-creates parent directories; macOSinstalldoes not support this flag. Replaced with explicitmkdir -p "$(dirname dst)"+install. - ✅ Fix dotfiles path bug — was
${DOTFILES}/streamdeck-go/.config/streamdeck-go; corrected to${DOTFILES}/.config/streamdeck-goto match the pattern shown in the README.
Makefile
- ✅ OS detection —
$(shell uname -s)setsOS;ifeq ($(OS),Darwin)/elseblocks set all platform-specific variables. - ✅ macOS
install-helper— no daemon, no group, no sudo. Just copiesconfig/privileged.example.yamlto~/.config/streamdeck-go/privileged.yaml. The user edits it to addpriv:commands. - ✅ Linux
install-helper— unchanged:dscl-free, usesgroupadd+usermod, installs helper binary + systemd system service. - ✅
udevtarget — 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) |