Files
streamdeck-go/mac-support.md
2026-04-13 08:11:19 -06:00

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 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 matchingReadButtons() 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 pathhelperSocketPath() 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 macOSrunPrivileged() 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 detectionuname -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)