diff --git a/Makefile b/Makefile index b2eec14..d6c9dee 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,29 @@ BINARY := streamdeck-go HELPER := streamdeck-helper PREFIX ?= $(HOME)/.local -BIN_DIR := $(PREFIX)/bin -SYS_BIN := /usr/local/bin CONFIG_DIR := $(HOME)/.config/streamdeck-go -SYSTEMD_USER := $(HOME)/.config/systemd/user -SYSTEMD_SYS := /etc/systemd/system -ETC_DIR := /etc/streamdeck-go -UDEV_RULE := /etc/udev/rules.d/99-streamdeck.rules GROUP := streamdeck -.PHONY: build build-helper install install-helper uninstall udev +# ── OS detection ────────────────────────────────────────────────────────────── +OS := $(shell uname -s) + +ifeq ($(OS),Darwin) + BIN_DIR := $(HOME)/go/bin + ETC_DIR := /usr/local/etc/streamdeck-go + LAUNCHAGENTS := $(HOME)/Library/LaunchAgents + LAUNCHDAEMONS := /Library/LaunchDaemons + AGENT_PLIST := $(LAUNCHAGENTS)/com.woodarddigital.streamdeck-go.plist + DAEMON_PLIST := $(LAUNCHDAEMONS)/com.woodarddigital.streamdeck-go-helper.plist +else + BIN_DIR := $(PREFIX)/bin + SYS_BIN := /usr/local/bin + ETC_DIR := /etc/streamdeck-go + SYSTEMD_USER := $(HOME)/.config/systemd/user + SYSTEMD_SYS := /etc/systemd/system + UDEV_RULE := /etc/udev/rules.d/99-streamdeck.rules +endif + +.PHONY: build build-helper install install-helper uninstall uninstall-helper udev # ── Build ───────────────────────────────────────────────────────────────────── @@ -22,25 +35,37 @@ build-helper: # ── Install ─────────────────────────────────────────────────────────────────── -# Interactive install — prompts for dotfiles directory, creates symlink, -# installs binary + udev rule + systemd user service. +# Interactive install — prompts for dotfiles directory, installs binary + service. install: @bash install.sh # Install the privileged helper (requires sudo). -# Creates a 'streamdeck' group, adds the current user to it, installs the -# helper binary as root, and enables it as a system service. +ifeq ($(OS),Darwin) +# On macOS there is no root daemon — privileged commands run via osascript (admin +# auth dialog). install-helper just drops the whitelist into the user config dir. +install-helper: + mkdir -p $(CONFIG_DIR) + @if [ ! -f $(CONFIG_DIR)/privileged.yaml ]; then \ + install -m 644 config/privileged.example.yaml $(CONFIG_DIR)/privileged.yaml; \ + echo "Whitelist written to $(CONFIG_DIR)/privileged.yaml"; \ + else \ + echo "Whitelist already exists — not overwriting"; \ + fi + @echo "" + @echo "Edit $(CONFIG_DIR)/privileged.yaml to add priv: commands." + @echo "No sudo or daemon required — macOS will prompt for admin auth on use." +else install-helper: build-helper - # Create group and add current user. + # Create group and add current user (Linux). @if ! getent group $(GROUP) > /dev/null; then \ sudo groupadd $(GROUP); \ echo "Created group '$(GROUP)'"; \ fi sudo usermod -aG $(GROUP) $(USER) - # Install helper binary (owned root, not world-executable by others). + # Install helper binary (owned root, not world-executable). sudo install -Dm750 $(HELPER) $(SYS_BIN)/$(HELPER) sudo chown root:$(GROUP) $(SYS_BIN)/$(HELPER) - # Install whitelist config (root-owned, not world-writable). + # Install whitelist config. sudo mkdir -p $(ETC_DIR) @if [ ! -f $(ETC_DIR)/privileged.yaml ]; then \ sudo install -Dm640 config/privileged.example.yaml $(ETC_DIR)/privileged.yaml; \ @@ -57,8 +82,13 @@ install-helper: build-helper @echo "Helper installed. Edit $(ETC_DIR)/privileged.yaml (as root) to add commands." @echo "NOTE: log out and back in for group membership to take effect," @echo " or run: newgrp $(GROUP)" +endif +# udev: Linux-only device permission rule. udev: +ifeq ($(OS),Darwin) + @echo "udev is Linux-only — not needed on macOS (IOKit grants HID access directly)." +else @if [ ! -f $(UDEV_RULE) ]; then \ echo 'KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", MODE="0666"' \ | sudo tee $(UDEV_RULE); \ @@ -68,9 +98,21 @@ udev: else \ echo "udev rule already exists."; \ fi +endif # ── Uninstall ───────────────────────────────────────────────────────────────── +ifeq ($(OS),Darwin) +uninstall: + launchctl unload $(AGENT_PLIST) 2>/dev/null || true + rm -f $(AGENT_PLIST) + rm -f $(BIN_DIR)/$(BINARY) + @echo "Uninstalled. Config at $(CONFIG_DIR) preserved." + +uninstall-helper: + @echo "No helper daemon on macOS — nothing to uninstall." + @echo "Whitelist at $(CONFIG_DIR)/privileged.yaml preserved." +else uninstall: systemctl --user disable --now streamdeck-go.service || true rm -f $(BIN_DIR)/$(BINARY) @@ -84,3 +126,4 @@ uninstall-helper: sudo rm -f $(SYSTEMD_SYS)/streamdeck-go-helper.service sudo systemctl daemon-reload @echo "Helper uninstalled. Whitelist at $(ETC_DIR)/privileged.yaml preserved." +endif diff --git a/README.md b/README.md index e0198d4..5825959 100644 --- a/README.md +++ b/README.md @@ -3,23 +3,22 @@ > [!IMPORTANT] > This is a sloppy utility that's designed to just work. [406.fail](https://406.fail) - -A lightweight, dotfile-style controller for the **Elgato Stream Deck XL** on Linux. +A lightweight, dotfile-style controller for the **Elgato Stream Deck XL** on Linux and macOS. No Elgato software required — communicates directly with the device over USB HID. --- ## Features -- Configure keys with a single YAML file (Designed for dotfiles compatability) -- PNG and JPEG icons, automatically scaled to key size +- Configure keys with a single YAML file (designed for dotfiles compatibility) +- PNG, JPEG, and SVG icons, automatically scaled to key size - Animated GIF support — frames pre-encoded at startup, cycled at the GIF's native rate - Runs any shell command on key press - **Status/toggle keys** — poll any shell command on an interval, swap icons based on output; icon updates on press - **Live config reload** — save your config and the deck updates instantly, no restart needed -- Privileged command helper — run whitelisted root commands via a Unix socket; supports polkit auth dialogs +- **Privileged commands** — run whitelisted root/admin commands; Linux uses a root helper daemon, macOS uses the native admin auth dialog - Automatic reconnect — survives USB unplug, KVM switches, and suspend/resume -- Runs as a systemd user service, starts automatically with your desktop session +- Runs as a systemd user service (Linux) or launchd agent (macOS), starts automatically at login - No Stream Deck app, no Node.js, no Electron **Planned:** text/label overlays on keys, multi-page layouts, AUR package — see [Roadmap](#roadmap) @@ -31,15 +30,19 @@ No Elgato software required — communicates directly with the device over USB H ``` streamdeck-go/ ├── cmd/ -│ └── streamdeck/ -│ └── main.go # Entry point, config watcher, event loop +│ ├── streamdeck/ +│ │ └── main.go # Entry point, config watcher, event loop +│ └── streamdeck-helper/ +│ └── main.go # Privileged helper daemon (Linux only) ├── internal/ │ ├── config/ │ │ └── config.go # YAML parsing with XDG-aware defaults │ └── device/ │ └── streamdeck.go # USB HID communication, image encoding, button reads ├── systemd/ -│ └── streamdeck-go.service # Systemd user service unit +│ └── streamdeck-go.service # systemd user service (Linux) +├── launchd/ +│ └── com.woodarddigital.streamdeck-go.plist # launchd agent (macOS) ├── config.example.yaml # Starter config (copied to ~/.config on install) ├── Makefile # build / install / uninstall ├── go.mod @@ -84,14 +87,16 @@ interleave partial image data across keys. | [`github.com/fsnotify/fsnotify`](https://github.com/fsnotify/fsnotify) | Config file watching for live reload | | [`golang.org/x/image`](https://pkg.go.dev/golang.org/x/image) | Bi-linear image scaling | | [`gopkg.in/yaml.v3`](https://pkg.go.dev/gopkg.in/yaml.v3) | YAML config parsing | +| [`github.com/srwiley/oksvg`](https://github.com/srwiley/oksvg) | SVG rasterisation | | Go stdlib `image/gif`, `image/jpeg`, `image/png` | Image decoding and JPEG encoding | ### System library `libhidapi` must be present at runtime: -| Distro | Command | +| Platform | Command | |---|---| +| macOS | `brew install hidapi` | | Arch / Manjaro | `sudo pacman -S hidapi` | | Debian / Ubuntu | `sudo apt install libhidapi-hidraw0` | | Fedora / RHEL | `sudo dnf install hidapi` | @@ -101,14 +106,39 @@ interleave partial image data across keys. ## Installation -### Option A — `make install` (recommended) +### macOS + +```bash +# Prerequisites +brew install go hidapi + +git clone https://github.com/WoodardDigital/streamdeck-go +cd streamdeck-go +make install +``` + +The binary installs to `~/go/bin/streamdeck-go`. Make sure `~/go/bin` is in your `PATH`: + +```bash +# Add to ~/.zshrc or ~/.bash_profile if not already present: +export PATH="$HOME/go/bin:$PATH" +``` + +A launchd agent is installed to `~/Library/LaunchAgents/` and started automatically. +No sudo required for the main binary. + +For privileged commands (sleep, reboot, etc.) see [Privileged commands — macOS](#privileged-commands--macos). + +--- + +### Linux — `make install` (recommended) An interactive installer that handles everything, including optional dotfiles directory integration. ```bash -# Prerequisites -sudo pacman -S go hidapi # Arch; adjust for your distro (see table above) +# Prerequisites — Arch example; adjust for your distro (see table above) +sudo pacman -S go hidapi git clone https://github.com/WoodardDigital/streamdeck-go cd streamdeck-go @@ -158,25 +188,11 @@ The resulting structure inside your dotfiles repo mirrors everything else in `.c └── icons/ ``` -After install, edit and save `config.yaml` — the deck reloads live, no restart needed: - -```bash -$EDITOR ~/.config/streamdeck-go/config.yaml -``` - -To remove: - -```bash -make uninstall # stops the service and removes the binary; config is preserved -``` - --- -### Option B — manual / dev setup +### Linux — manual / dev setup -Use this if you want to run directly from the repo (e.g. while developing). - -**1. udev rule** (one-time, needed once regardless of install method): +**1. udev rule** (one-time): ```bash echo 'KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", MODE="0666"' \ @@ -191,7 +207,6 @@ sudo udevadm trigger git clone https://github.com/WoodardDigital/streamdeck-go cd streamdeck-go -# Run with the repo's config.yaml (created from the example if absent): cp config.example.yaml config.yaml go run ./cmd/streamdeck/ @@ -199,24 +214,41 @@ go run ./cmd/streamdeck/ go run ./cmd/streamdeck/ -config ~/.config/streamdeck-go/config.yaml ``` -When no `-config` flag is given, the binary always checks -`~/.config/streamdeck-go/config.yaml` first (respecting `$XDG_CONFIG_HOME`). -The repo's `config.yaml` is gitignored — it's your local scratchpad. +When no `-config` flag is given the binary checks `~/.config/streamdeck-go/config.yaml` +first (respecting `$XDG_CONFIG_HOME`). The repo's `config.yaml` is gitignored. --- -### Option C — AUR (Arch Linux) +### AUR (Arch Linux) > AUR package coming soon. Until then, use `make install` above. --- -## Systemd service +## Service management -The service file lives at `systemd/streamdeck-go.service` in the repo and is -installed to `~/.config/systemd/user/streamdeck-go.service` by `make install`. +### macOS (launchd) -Useful commands: +```bash +# Status +launchctl list | grep streamdeck + +# Stop / start +launchctl unload ~/Library/LaunchAgents/com.woodarddigital.streamdeck-go.plist +launchctl load ~/Library/LaunchAgents/com.woodarddigital.streamdeck-go.plist + +# Logs (persistent across reboots) +tail -f ~/Library/Logs/streamdeck-go.log +``` + +The agent starts automatically at login and restarts if the process exits. +Sleep/wake is handled by the reconnect loop in the binary — no extra config needed. + +To use a custom config path, edit the installed plist at +`~/Library/LaunchAgents/com.woodarddigital.streamdeck-go.plist` and add +`-config /path/to/config.yaml` to the `ProgramArguments` array, then reload. + +### Linux (systemd) ```bash systemctl --user status streamdeck-go # check if running @@ -225,7 +257,7 @@ systemctl --user stop streamdeck-go # stop journalctl --user -u streamdeck-go -f # follow logs ``` -To use a custom config path with the service, edit the unit after install: +To use a custom config path: ```bash systemctl --user edit streamdeck-go @@ -252,7 +284,8 @@ icons_dir: ~/.config/streamdeck-go/icons # default; can be any path brightness: 70 # 0–100 # USB IDs — defaults match Stream Deck XL v2 -# Run: lsusb | grep Elgato +# macOS: system_profiler SPUSBDataType | grep -A5 "Stream Deck" +# Linux: lsusb | grep Elgato device: vendor_id: 0x0fd9 product_id: 0x00ba @@ -267,11 +300,11 @@ device: keys: 0: - icon: ghostty.png # PNG, JPEG, or GIF — relative to icons_dir + icon: ghostty.png # PNG, JPEG, GIF, or SVG — relative to icons_dir command: ghostty 1: icon: firefox.png - command: firefox + command: open -a Firefox # macOS; use "firefox" on Linux 8: icon: loading.gif # animated — cycles at the GIF's native frame rate command: "" @@ -300,7 +333,31 @@ keys: | Yes | stdout contains the string | stdout does not contain it | | No (omitted) | command exits 0 | command exits non-zero | -**More examples:** +**macOS examples:** + +```yaml +# Mute microphone (macOS — requires SwitchAudioSource or similar) +3: + command: "osascript -e 'set volume input muted not (input muted of (get volume settings))'" + icon_true: mic-muted.png + icon_false: mic-active.png + poll: + command: "osascript -e 'input muted of (get volume settings)'" + interval: 2s + match: "true" + +# VPN toggle (macOS) +4: + command: "osascript -e 'tell application \"Tunnelblick\" to connect \"My VPN\"'" + icon_true: vpn-on.png + icon_false: vpn-off.png + poll: + command: "scutil --nc status \"My VPN\"" + interval: 5s + match: "Connected" +``` + +**Linux examples:** ```yaml # VPN status (exit-code match — no match string needed) @@ -338,68 +395,50 @@ keys: Any shell command works — including launching terminals and SSH sessions. -**Open a terminal on key press:** +**Open a terminal:** ```yaml keys: 0: icon: ghostty.png - command: ghostty + command: ghostty # Linux 1: icon: terminal.png - command: alacritty # or kitty, wezterm, foot, etc. + command: open -a Terminal # macOS — or: open -a iTerm ``` -**SSH — open an interactive session in a terminal:** +**SSH:** ```yaml keys: 5: icon: homeserver.png - command: "ghostty -e ssh user@homeserver" + command: "ghostty -e ssh user@homeserver" # Linux 6: - icon: pi.png - command: "alacritty -e ssh pi@raspberrypi.local" + icon: homeserver.png + command: "open -a Terminal ssh://user@homeserver" # macOS ``` -This is the recommended pattern for SSH — the terminal handles the TTY, -resize events, and any passphrase prompt. - -**SSH — run a remote command silently (no terminal):** - -```yaml -keys: - 7: - icon: deploy.png - command: "ssh user@host 'cd /app && git pull && systemctl restart app'" -``` - -Works as long as SSH key auth is set up and the key has no passphrase (or the agent is available — see note below). - -> **SSH agent & the systemd service** +> **SSH agent & the service** > -> The service starts before your desktop session fully initialises, so -> `SSH_AUTH_SOCK` (used for passphrase-protected keys) may not be in its -> environment. Fix by importing it from your session startup: +> The service starts before your shell environment is fully loaded, so +> `SSH_AUTH_SOCK` may not be set. Fix: > +> **Linux:** > ```bash -> # Add to ~/.config/fish/config.fish, ~/.bashrc, or session init: > systemctl --user import-environment SSH_AUTH_SOCK > ``` > -> Or hardcode the socket path in the service: -> -> ```bash -> systemctl --user edit streamdeck-go +> **macOS** — add to `~/Library/LaunchAgents/com.woodarddigital.streamdeck-go.plist` +> inside the `` block: +> ```xml +> EnvironmentVariables +> +> SSH_AUTH_SOCK +> /private/tmp/com.apple.launchd.XXXXX/Listeners +> > ``` -> -> ```ini -> [Service] -> Environment=SSH_AUTH_SOCK=%t/keyring/ssh -> ``` -> -> Run `echo $SSH_AUTH_SOCK` in a terminal to find the correct path for your -> desktop environment. +> Run `echo $SSH_AUTH_SOCK` in a terminal to find the correct path. **Common terminal flags:** @@ -408,8 +447,62 @@ Works as long as SSH key auth is set up and the key has no passphrase (or the ag | ghostty | `ghostty -e ` | | alacritty | `alacritty -e ` | | kitty | `kitty ` | -| foot | `foot ` | | wezterm | `wezterm start -- ` | +| Terminal.app | `open -a Terminal