# streamdeck-go > [!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. 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 - Animated GIF support — frames pre-encoded at startup, cycled at the GIF's native rate - Runs any shell command on key 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 - Automatic reconnect — survives USB unplug, KVM switches, and suspend/resume - Runs as a systemd user service, starts automatically with your desktop session - No Stream Deck app, no Node.js, no Electron **Planned:** text/label overlays on keys, multi-page layouts, AUR package — see [Roadmap](#roadmap) --- ## Architecture ``` streamdeck-go/ ├── cmd/ │ └── streamdeck/ │ └── main.go # Entry point, config watcher, event loop ├── 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 ├── config.example.yaml # Starter config (copied to ~/.config on install) ├── Makefile # build / install / uninstall ├── go.mod └── go.sum ``` ### How it works ``` ~/.config/streamdeck-go/config.yaml │ ├── fsnotify watcher ──── file saved? ──▶ cancel ctx → reload config → restart run() │ └──▶ run(ctx) │ ├── static icon ──▶ device.SetKeyImage() (scale → flip → JPEG → HID) │ ├── animated GIF ──▶ device.EncodeFrame() (pre-encode all frames once) │ └──▶ goroutine/key: loop frames, sleep per-frame delay │ (cancelled via ctx on reload) │ └── event loop ──▶ device.ReadButtons() (250 ms timeout, checks ctx) └──▶ key-down: exec.Command("sh", "-c", command) ``` HID output reports are mutex-guarded so concurrent animation goroutines never interleave partial image data across keys. --- ## Dependencies ### Go packages | Package | Purpose | |---|---| | [`github.com/sstallion/go-hid`](https://github.com/sstallion/go-hid) | Bindings for `libhidapi` — USB HID read/write | | [`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 | | Go stdlib `image/gif`, `image/jpeg`, `image/png` | Image decoding and JPEG encoding | ### System library `libhidapi` must be present at runtime: | Distro | Command | |---|---| | Arch / Manjaro | `sudo pacman -S hidapi` | | Debian / Ubuntu | `sudo apt install libhidapi-hidraw0` | | Fedora / RHEL | `sudo dnf install hidapi` | | openSUSE | `sudo zypper install libhidapi-hidraw0` | --- ## Installation ### Option A — `make install` (recommended) Builds the binary, installs the systemd user service, and writes a default config if one doesn't already exist. ```bash # Prerequisites sudo pacman -S go hidapi # Arch; adjust for your distro (see table above) git clone https://github.com/WoodardDigital/streamdeck-go cd streamdeck-go make install ``` `make install` does the following automatically: 1. Installs the udev rule (`/etc/udev/rules.d/99-streamdeck.rules`) so the device is accessible without root 2. Builds the binary and copies it to `~/.local/bin/streamdeck-go` 3. Creates `~/.config/streamdeck-go/` and `~/.config/streamdeck-go/icons/` 4. Writes a starter `~/.config/streamdeck-go/config.yaml` (only if one doesn't exist) 5. Installs and enables the systemd user service — starts now and on every login After install, edit your config: ```bash $EDITOR ~/.config/streamdeck-go/config.yaml ``` Changes are picked up automatically — no need to restart anything. To remove: ```bash make uninstall # stops the service and removes the binary; config is preserved ``` --- ### Option B — 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): ```bash echo 'KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", MODE="0666"' \ | sudo tee /etc/udev/rules.d/99-streamdeck.rules sudo udevadm control --reload sudo udevadm trigger ``` **2. Build and run:** ```bash 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/ # Or point at any config file: 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. --- ### Option C — AUR (Arch Linux) > AUR package coming soon. Until then, use `make install` above. --- ## Systemd service 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`. Useful commands: ```bash systemctl --user status streamdeck-go # check if running systemctl --user restart streamdeck-go # restart manually 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: ```bash systemctl --user edit streamdeck-go ``` Add: ```ini [Service] ExecStart= ExecStart=%h/.local/bin/streamdeck-go -config %h/.config/streamdeck-go/config.yaml ``` --- ## Configuration Config lives at `~/.config/streamdeck-go/config.yaml` (or wherever `-config` points). **Editing and saving the file reloads the deck live** — icons update, animations restart, no service restart required. ```yaml 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 device: vendor_id: 0x0fd9 product_id: 0x00ba # Keys are 0-indexed, left-to-right, top-to-bottom. # Stream Deck XL layout (8 columns × 4 rows): # # 0 1 2 3 4 5 6 7 # 8 9 10 11 12 13 14 15 # 16 17 18 19 20 21 22 23 # 24 25 26 27 28 29 30 31 keys: 0: icon: ghostty.png # PNG, JPEG, or GIF — relative to icons_dir command: ghostty 1: icon: firefox.png command: firefox 8: icon: loading.gif # animated — cycles at the GIF's native frame rate command: "" ``` ### Terminal & SSH commands Any shell command works — including launching terminals and SSH sessions. **Open a terminal on key press:** ```yaml keys: 0: icon: ghostty.png command: ghostty 1: icon: terminal.png command: alacritty # or kitty, wezterm, foot, etc. ``` **SSH — open an interactive session in a terminal:** ```yaml keys: 5: icon: homeserver.png command: "ghostty -e ssh user@homeserver" 6: icon: pi.png command: "alacritty -e ssh pi@raspberrypi.local" ``` 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** > > 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: > > ```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 > ``` > > ```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. **Common terminal flags:** | Terminal | Flag to run a command | |---|---| | ghostty | `ghostty -e ` | | alacritty | `alacritty -e ` | | kitty | `kitty ` | | foot | `foot ` | | wezterm | `wezterm start -- ` | --- ### Supported icon formats | Format | Notes | |---|---| | PNG | Recommended for static icons | | JPEG | Good for photos / complex images | | GIF | Animated — all frames pre-encoded at startup, cycled in a background goroutine | Icons are scaled to 96×96 px using bi-linear filtering. The XL renders images mirrored, so they are pre-flipped before sending — your icons will appear the right 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 | 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). --- ## Roadmap ### Text / label overlay on icons 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): ```yaml keys: 4: 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 ``` ### 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. Example config (proposed): ```yaml pages: default: 0: icon: apps.png command: "page:apps" # switch to the 'apps' page 1: icon: firefox.png command: firefox apps: 0: icon: back.png command: "page:default" 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 ```