adding mac support mainly
This commit is contained in:
287
README.md
287
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 `<dict>` block:
|
||||
> ```xml
|
||||
> <key>EnvironmentVariables</key>
|
||||
> <dict>
|
||||
> <key>SSH_AUTH_SOCK</key>
|
||||
> <string>/private/tmp/com.apple.launchd.XXXXX/Listeners</string>
|
||||
> </dict>
|
||||
> ```
|
||||
>
|
||||
> ```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 <cmd>` |
|
||||
| alacritty | `alacritty -e <cmd>` |
|
||||
| kitty | `kitty <cmd>` |
|
||||
| foot | `foot <cmd>` |
|
||||
| wezterm | `wezterm start -- <cmd>` |
|
||||
| Terminal.app | `open -a Terminal <script>` |
|
||||
| iTerm2 | `open -a iTerm <script>` |
|
||||
|
||||
---
|
||||
|
||||
### Privileged commands
|
||||
|
||||
Use the `priv:` prefix to run commands that require elevated privileges.
|
||||
|
||||
```yaml
|
||||
keys:
|
||||
30:
|
||||
icon: suspend.png
|
||||
command: "priv:suspend"
|
||||
```
|
||||
|
||||
#### Privileged commands — macOS
|
||||
|
||||
No root daemon required. The main process reads
|
||||
`~/.config/streamdeck-go/privileged.yaml` and runs the command via `osascript`,
|
||||
which shows the standard macOS admin authentication dialog.
|
||||
|
||||
Setup:
|
||||
|
||||
```bash
|
||||
make install-helper # copies privileged.yaml to ~/.config/streamdeck-go/
|
||||
```
|
||||
|
||||
Edit `~/.config/streamdeck-go/privileged.yaml`:
|
||||
|
||||
```yaml
|
||||
commands:
|
||||
suspend: "pmset sleepnow"
|
||||
reboot: "shutdown -r now"
|
||||
poweroff: "shutdown -h now"
|
||||
```
|
||||
|
||||
#### Privileged commands — Linux
|
||||
|
||||
A root helper daemon (`streamdeck-go-helper`) validates commands against a
|
||||
root-owned whitelist at `/etc/streamdeck-go/privileged.yaml` before running them.
|
||||
The main daemon communicates with it over a Unix socket.
|
||||
|
||||
```bash
|
||||
make install-helper # creates streamdeck group, installs helper + system service
|
||||
```
|
||||
|
||||
Edit `/etc/streamdeck-go/privileged.yaml` (as root):
|
||||
|
||||
```yaml
|
||||
commands:
|
||||
suspend: "systemctl suspend"
|
||||
reboot: "systemctl reboot"
|
||||
poweroff: "systemctl poweroff"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -430,13 +523,16 @@ way round.
|
||||
|
||||
## Supported Devices
|
||||
|
||||
| Model | Product ID | Keys | Status |
|
||||
|---|---|---|---|
|
||||
| Stream Deck XL v2 | `0x00ba` | 32 | tested |
|
||||
| Stream Deck XL v1 | `0x006c` | 32 | untested |
|
||||
| Stream Deck MK.2 | `0x006d` | 15 | untested |
|
||||
| Model | Product ID | Keys | Linux | macOS |
|
||||
|---|---|---|---|---|
|
||||
| Stream Deck XL v2 | `0x00ba` | 32 | tested | tested |
|
||||
| Stream Deck XL v1 | `0x006c` | 32 | untested | untested |
|
||||
| Stream Deck MK.2 | `0x006d` | 15 | untested | untested |
|
||||
|
||||
To find your device's product ID:
|
||||
- **macOS:** `system_profiler SPUSBDataType | grep -A5 "Stream Deck"`
|
||||
- **Linux:** `lsusb | grep Elgato`
|
||||
|
||||
Run `lsusb | grep Elgato` to find your device's product ID.
|
||||
To add a model, edit the `models` map in [internal/device/streamdeck.go](internal/device/streamdeck.go).
|
||||
|
||||
---
|
||||
@@ -447,8 +543,6 @@ To add a model, edit the `models` map in [internal/device/streamdeck.go](interna
|
||||
|
||||
Render dynamic text directly onto a key image at runtime — useful for showing
|
||||
live state like volume level, a clock, a counter, or the current git branch.
|
||||
Icons would be composited with a text layer before being sent to the device, so
|
||||
no pre-made image is needed for every possible value.
|
||||
|
||||
Example config (proposed):
|
||||
|
||||
@@ -458,14 +552,13 @@ keys:
|
||||
icon: volume.png
|
||||
label: "$(pactl get-sink-volume @DEFAULT_SINK@ | awk '{print $5}')"
|
||||
label_position: bottom # top | center | bottom
|
||||
refresh: 5s # re-evaluate and redraw every 5 seconds
|
||||
refresh: 5s
|
||||
```
|
||||
|
||||
### Multi-page layouts
|
||||
|
||||
Support more than 32 actions by organising keys into named pages. A designated
|
||||
key (or key combination) switches between pages. The deck reloads instantly with
|
||||
the new page's icons when switching.
|
||||
key switches between pages.
|
||||
|
||||
Example config (proposed):
|
||||
|
||||
@@ -474,7 +567,7 @@ pages:
|
||||
default:
|
||||
0:
|
||||
icon: apps.png
|
||||
command: "page:apps" # switch to the 'apps' page
|
||||
command: "page:apps"
|
||||
1:
|
||||
icon: firefox.png
|
||||
command: firefox
|
||||
@@ -486,16 +579,10 @@ pages:
|
||||
1:
|
||||
icon: ghostty.png
|
||||
command: ghostty
|
||||
2:
|
||||
icon: obsidian.png
|
||||
command: obsidian
|
||||
```
|
||||
|
||||
### AUR package
|
||||
|
||||
A `PKGBUILD` for Arch Linux so the full install (binary, services, udev rule,
|
||||
config skeleton) is handled by `yay` or `paru` like any other package.
|
||||
|
||||
```bash
|
||||
yay -S streamdeck-go # coming soon
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user