# streamdeck-lets-go > A lightweight daemon for controlling Elgato Stream Deck devices with a built-in web UI. --- ## Features - **Web-based editor** — Alpine.js SPA for managing pages, keys, icons, and actions - **Multi-action keys** — assign different actions to **tap**, **long press**, **double tap**, and **hold** on the same key - **On-device display** — render icons, text labels, and periodic command output directly on Stream Deck keys; output can include custom background and text colors - **Auto page switching** — automatically change pages based on the focused window (Hyprland, Sway, Niri, GNOME, KDE, X11) - **Screensaver** — dim or blank the deck after a configurable idle timeout - **Gesture timing** — configurable long press (default 500ms) and double tap (default 300ms) thresholds - **Hot-reload** — config changes via the web UI are applied live; manual edits to `config.json` are detected and reloaded automatically - **No cloud, no Electron** — single Go binary with an embedded web frontend --- ## Quick Start ### Prerequisites - Linux with **CGO** enabled (required by `bearsh/hid` / libusb) - Stream Deck connected via USB ### Install from source ```bash go build -o streamdeck-lets-go . ``` ### Run ```bash # Full daemon (deck hardware + web UI) ./streamdeck-lets-go daemon # Web UI only (config editor without deck) ./streamdeck-lets-go serve -addr :9090 # List connected devices ./streamdeck-lets-go discover ``` Open `http://localhost:9090` in your browser. --- ## Configuration The config file is stored at `~/.config/streamdeck-lets-go/config.json`. You can edit it manually or through the web UI. ### Multi-action keys Each key can have multiple actions with different triggers: ```json { "index": 7, "icon": "fa:chevron-right", "label": "Next", "actions": [ { "trigger": "tap", "type": "builtin", "builtin": "page:next" }, { "trigger": "long_press", "type": "builtin", "builtin": "page:prev" } ] } ``` Supported triggers: | Trigger | Behavior | |---|---| | `tap` | Quick press and release | | `long_press` | Held past the threshold (default 500ms) | | `double_tap` | Two taps within the threshold (default 300ms) | | `hold_start` | Fires when held past the threshold | | `hold_end` | Fires on release after a hold | ### Key action types | Type | Field | Description | |---|---|---| | `command` | `command` | Run a shell command | | `script` | `script` | Run an executable script | | `builtin` | `builtin` | Built-in media/volume/brightness controls | | `page` | `page` | Switch to another page | | `keyboard` | `keys` | Send a keyboard shortcut (e.g. `ctrl+t`, `super+return`) | ### Keyboard actions Sends a key combination to the currently focused window. | Platform | Tool required | |---|---| | Wayland | `wtype` | | X11 | `xdotool` | The daemon warns on startup if the required tool is missing. ### Built-in actions | Value | Action | |---|---| | `volume_up` / `volume_down` / `volume_mute` | PipeWire volume (via `wpctl`) | | `brightness_up` / `brightness_down` | Display brightness (via `brightnessctl`) | | `media_play_pause` / `media_next` / `media_prev` / `media_stop` | MPRIS media (via `playerctl`) | | `page:next` / `page:prev` | Switch to next/previous page | | `deck:brightness-up` / `deck:brightness-down` | Cycle deck brightness | ### Periodic display Any key can display the output of a command or script, updated on an interval. Use this for weather, system stats, monitoring, etc. The command output can include **background color** and **text color** using one of these formats: **JSON format:** ```json {"text": "Server OK", "background": "#22c55e", "text_color": "#ffffff"} ``` **First-line-as-color format:** ``` #ff0000 Server down! ``` If no color is specified, the key uses its configured background or the default black. ### Dynamic Key Generators Generate an entire page of keys dynamically by running any executable (shell script, Python, Go binary, etc.). Useful for game launchers, music collections, Docker containers, monitoring dashboards — anything where the set of keys is not known at config time. ``` ┌──────────────────────────────────────────────────┐ │ Generator script (any language) │ │ → queries Lutris DB, Steam API, etc. │ │ → outputs JSON array of KeyConfig to stdout │ │ → receives STREAMDECK_KEY_COUNT env var │ └──────────────┬───────────────────────────────────┘ │ stdout: [{"index":0,"icon":"...",...}] ▼ ┌──────────────────────────────────────────────────┐ │ streamdeck-lets-go daemon │ │ → parses JSON │ │ → merges with static keys (static wins) │ │ → renders keys on the deck │ │ → re-runs on interval (default 60s) │ └──────────────────────────────────────────────────┘ ``` #### Config Add `dynamic_keys` to any page: ```json { "name": "Games", "icon": "fa:gamepad", "keys": [ { "index": 0, "icon": "fa:arrow-left", "actions": [ { "trigger": "tap", "type": "page", "page": "main" } ] } ], "dynamic_keys": { "command": "/home/user/bin/game-list", "interval": "120s", "timeout": "15s", "max_keys": 14 } } ``` | Field | Type | Default | Description | |---|---|---|---| | `command` | string | — | Shell command to execute (mutually exclusive with `script`) | | `script` | string | — | Path to an executable script; relative paths resolve against `~/.config/streamdeck-lets-go/` | | `interval` | string | `"60s"` | How often to re-run the generator (minimum 1s, maximum 24h) | | `timeout` | string | `"15s"` | Maximum execution time before the generator is killed | | `max_keys` | int | `deck.NumKeys()` | Maximum number of keys to accept (e.g. `13` to reserve 2 for navigation) | #### Generator contract The script **must** print a JSON array of key configs to stdout and exit with code 0. Each element supports the full `KeyConfig` schema: ```json [ { "index": 0, "icon": "/home/user/.local/share/lutris/coverart/cyberpunk-2077.jpg", "label": "Cyberpunk 2077", "font_size": 12, "icon_scale": 1.0, "background": "#222222", "actions": [ { "trigger": "tap", "type": "command", "command": "lutris lutris:rungame/cyberpunk-2077", "background": true } ] } ] ``` Every field from a static key is available: | Field | Type | Description | |---|---|---| | `index` | int | Key position on the deck (0-based) | | `icon` | string | Icon source: `fa:terminal`, `emoji:fire`, `@firefox`, `/absolute/path.png`, relative path | | `label` | string | Text shown below the icon | | `font_size` | number | Font size in points (default 18, recommended 11–14 for labels) | | `icon_scale` | number | Icon scale factor (0.0–1.0, default 0.55) | | `background` | string | Hex background color `"#222222"` | | `actions` | array | Same action schema as static keys — supports all triggers (`tap`, `long_press`, `double_tap`) and all action types (`command`, `builtin`, `script`, `page`, `keyboard`) | | `display` | object | Periodic display block (same as static key `display`) — command/script runs on interval and text is overlaid on the key | #### Merge behavior Static `keys` (defined directly on the page) and dynamic keys are **merged** at runtime: | Priority | Source | |---|---| | 1 (highest) | Static keys — their indices are reserved | | 2 | Dynamic keys — fill indices not occupied by static keys | This lets you keep navigation buttons fixed while the generator fills the rest of the deck: ``` Static: [← back] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] index 0 └────────────────────── dynamic (indices 1–14) ──────────────────────→ ``` #### Environment variables The generator receives these environment variables: | Variable | Example | Description | |---|---|---| | `STREAMDECK_KEY_COUNT` | `15` | Maximum number of keys to output (derived from deck model or `max_keys` config) | | `STREAMDECK_CONFIG_DIR` | `/home/user/.config/streamdeck-lets-go` | Path to the config directory — useful for finding companion files | #### Error handling | Situation | Behavior | |---|---| | Script fails (non-zero exit, timeout) | Warning logged; previous keys preserved | | Script returns empty array `[]` | Only static keys rendered | | Script returns invalid JSON | Warning logged; previous keys preserved | | Script outputs debug to stderr | Logged at DEBUG level; stdout JSON parsed cleanly | | More keys than `max_keys` | Excess keys silently dropped | | Generator updates while page is active | Keys re-rendered in place (no flicker) | #### Examples **Python — Lutris game launcher** Place this at `~/.config/streamdeck-lets-go/scripts/lutris-keys`, make executable (`chmod +x`). ```python #!/usr/bin/env python3 import json, os, sqlite3, pathlib db = pathlib.Path.home() / '.local/share/lutris/pga.db' cover = pathlib.Path.home() / '.local/share/lutris/coverart' max_keys = int(os.environ.get('STREAMDECK_KEY_COUNT', 15)) if not db.exists(): print('[]') exit(0) conn = sqlite3.connect(db) games = conn.execute( "SELECT slug, name FROM games WHERE installed = 1 ORDER BY name" ).fetchall() conn.close() keys = [] for idx, (slug, name) in enumerate(games): if idx >= max_keys: break icon = next(cover.glob(f'{slug}.*'), None) key = { "index": idx, "icon_scale": 1.0, "actions": [{ "trigger": "tap", "type": "command", "command": f"lutris lutris:rungame/{slug}", "background": True }] } if icon: key["icon"] = str(icon) keys.append(key) print(json.dumps(keys)) ``` Reference it in config: ```json { "name": "Games", "dynamic_keys": { "script": "scripts/lutris-keys", "interval": "120s" } } ``` **Shell — Simple test generator** ```bash #!/bin/sh for i in $(seq 0 $((STREAMDECK_KEY_COUNT - 1))); do [ "$i" -gt 0 ] && echo "," printf '{"index":%d,"icon":"fa:hashtag","label":"Item %d"}' "$i" "$i" done ``` Run with any language you like — the only requirement is a valid JSON array on stdout. #### Tips - Use `max_keys` to reserve space for static navigation buttons - Enable `show_label_background: true` in the global config if labels on full-bleed icons are hard to read - Test your generator standalone before wiring it up: `STREAMDECK_KEY_COUNT=15 ./my-generator` - For large data sources, pass pagination params through the command string: `"command": "scripts/albums --page 0"` - Dynamic keys support `display` blocks — nest monitoring data inside generated keys ### Gesture timing Configured in the web UI (Settings → Gesture Timing) or in `config.json`: ```json "timing": { "long_press_ms": 500, "double_tap_ms": 300 } ``` ### Default font Set globally in Settings → Device → Default Font (`"medium"` or `"regular"`), or override per-key in the key editor. ### Auto-switch rules Automatically change pages based on the focused window: ```json { "wm_class": "firefox", "page": "browser" } ``` Supported compositors: Hyprland, Sway, Niri, GNOME, KDE, X11. ### Screensaver After a configurable idle period the deck dims or shows a custom image. --- ## Building from source ```bash git clone git@git.totmin.ru:en2zmax/streamdeck-lets-go.git cd streamdeck-lets-go go build -o streamdeck-lets-go . ``` Dependencies: | Package | Purpose | |---|---| | `libusb-1.0` | HID communication via CGO | | `librsvg` | SVG icon rendering (optional) | | `fontconfig` | System font detection | --- ## Supported devices | Model | PID | Keys | |---|---|---| | Stream Deck Original | `0x006d` | 15 | | Stream Deck Mini | `0x0060` | 6 | | Stream Deck XL | `0x006c` | 32 | | Stream Deck MK.2 | `0x0080` | 15 | | Stream Deck Original V2 | `0x0063` | 15 | --- ## Credits This project builds on the work of the following open-source libraries: - **[dh1tw/streamdeck](https://github.com/dh1tw/streamdeck)** (MIT) — Go protocol driver for Elgato Stream Deck devices - **[bearsh/hid](https://github.com/bearsh/hid)** (MIT) — Go USB HID bindings via CGO / libusb - **[disintegration/gift](https://github.com/disintegration/gift)** (MIT) — Go Image Filtering Toolkit --- ## License MIT