- Treat "timeout" and "interrupted system call" from ReadWithTimeout as non-fatal — hidraw on Linux returns errors instead of (0, nil) for both, causing false device-dead detection and reconnect loops - Resolve icons_dir relative to the config file when the path is not absolute, so the service finds icons regardless of working directory - Installer now copies bundled icons to the config dir on first install
streamdeck-go
Important
This is a sloppy utility that's designed to just work. 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
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 |
Bindings for libhidapi — USB HID read/write |
github.com/fsnotify/fsnotify |
Config file watching for live reload |
golang.org/x/image |
Bi-linear image scaling |
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)
An interactive installer that handles everything, including optional dotfiles directory integration.
# 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
The installer walks you through the whole setup:
❯ Building streamdeck-go...
✓ Build complete
❯ Checking udev rule...
✓ udev rule already installed — skipping
Config location
─────────────────────────────────────────────
· streamdeck-go stores its config and icons in a single directory.
· You can keep that directory inside your dotfiles repo and symlink it
· into ~/.config — the same pattern used by Hyprland, Waybar, etc.
❯ Use a dotfiles directory? [Y/n]
❯ Path to dotfiles repo [~/dotfiles]:
· Will create:
· ~/dotfiles/.config/streamdeck-go/
· ~/dotfiles/.config/streamdeck-go/config.yaml
· ~/dotfiles/.config/streamdeck-go/icons/
· Will symlink:
· ~/.config/streamdeck-go
· └─▶ ~/dotfiles/.config/streamdeck-go
❯ Confirm? [Y/n]
The resulting structure inside your dotfiles repo mirrors everything else in .config:
~/dotfiles/
└── .config/
├── hypr/
├── waybar/
└── streamdeck-go/ ← lives here, symlinked to ~/.config/streamdeck-go
├── config.yaml
└── icons/
After install, edit and save config.yaml — the deck reloads live, no restart needed:
$EDITOR ~/.config/streamdeck-go/config.yaml
To remove:
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):
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:
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 installabove.
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:
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:
systemctl --user edit streamdeck-go
Add:
[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.
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:
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:
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):
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:# Add to ~/.config/fish/config.fish, ~/.bashrc, or session init: systemctl --user import-environment SSH_AUTH_SOCKOr hardcode the socket path in the service:
systemctl --user edit streamdeck-go[Service] Environment=SSH_AUTH_SOCK=%t/keyring/sshRun
echo $SSH_AUTH_SOCKin a terminal to find the correct path for your desktop environment.
Common terminal flags:
| Terminal | Flag to run a command |
|---|---|
| ghostty | ghostty -e <cmd> |
| alacritty | alacritty -e <cmd> |
| kitty | kitty <cmd> |
| foot | foot <cmd> |
| wezterm | wezterm start -- <cmd> |
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.
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):
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):
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.
yay -S streamdeck-go # coming soon