Add dynamic_keys support to PageConfig — a contract-based plugin
system where any executable outputs a JSON array of KeyConfig to stdout.
The daemon runs the generator on a configurable interval and renders
the keys dynamically on the Stream Deck.
Core changes:
- config: DynamicKeyGen struct (command/script, interval, timeout, max_keys)
- config: full validation for dynamic_keys fields
- action: execDynamicKeys() with safe JSON parsing (stderr logged separately,
env vars STREAMDECK_KEY_COUNT / STREAMDECK_CONFIG_DIR)
- page: activeKeys() merges static + dynamic keys (static wins by index)
- page: startDynamicKeys / stopDynamicKeys lifecycle, per-page results cache
- page: rerenderActivePage() with deadlock-safe lock ordering
- daemon: event handler uses activeKeys() for dynamic key actions
Web UI:
- GET /api/pages returns effective_keys (merged static + cached dynamic)
- app.js: gridKeys uses effective_keys when available
- app.js: guard against editing dynamic keys through the UI
- index.html: "⚡ dynamic" badge + effective key count in page switcher
- polls /api/pages every 10s to keep the UI in sync
Image rendering:
- loadImage: gift.ResizeToFill instead of gift.Resize (preserves aspect
ratio, crops to fill — no more stretched cover art)
Example & docs:
- config.example.json: games page with dynamic_keys + back button
- README.md: comprehensive Dynamic Key Generators section (architecture
diagram, config reference, generator contract, merge behavior,
env vars, error handling, Python + Shell examples, tips)
105 lines
2.1 KiB
JSON
105 lines
2.1 KiB
JSON
{
|
|
"version": 1,
|
|
"log_level": "info",
|
|
"default_page": "main",
|
|
"font": "medium",
|
|
"show_label_background": true,
|
|
"devices": [
|
|
{
|
|
"serial": "",
|
|
"brightness": 75
|
|
}
|
|
],
|
|
"pages": [
|
|
{
|
|
"name": "main",
|
|
"keys": [
|
|
{
|
|
"index": 0,
|
|
"icon": "fa:terminal",
|
|
"actions": [
|
|
{
|
|
"trigger": "tap",
|
|
"type": "command",
|
|
"command": "kitty",
|
|
"background": true
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"index": 1,
|
|
"icon": "fab:firefox",
|
|
"label": "Browser",
|
|
"font_size": 11,
|
|
"actions": [
|
|
{
|
|
"trigger": "tap",
|
|
"type": "command",
|
|
"command": "firefox",
|
|
"background": true
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"index": 2,
|
|
"icon": "fa:gamepad",
|
|
"label": "Games",
|
|
"font_size": 11,
|
|
"actions": [
|
|
{
|
|
"trigger": "tap",
|
|
"type": "page",
|
|
"page": "games"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"index": 7,
|
|
"icon": "fa:chevron-right",
|
|
"label": "Next",
|
|
"actions": [
|
|
{
|
|
"trigger": "tap",
|
|
"type": "builtin",
|
|
"builtin": "page:next"
|
|
},
|
|
{
|
|
"trigger": "long_press",
|
|
"type": "builtin",
|
|
"builtin": "page:prev"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "games",
|
|
"icon": "fa:gamepad",
|
|
"keys": [
|
|
{
|
|
"index": 0,
|
|
"icon": "fa:arrow-left",
|
|
"actions": [
|
|
{ "trigger": "tap", "type": "page", "page": "main" }
|
|
]
|
|
}
|
|
],
|
|
"dynamic_keys": {
|
|
"script": "scripts/lutris-keys",
|
|
"interval": "120s"
|
|
}
|
|
}
|
|
],
|
|
"auto_switch": [],
|
|
"screensaver": {
|
|
"enabled": false,
|
|
"idle_seconds": 30,
|
|
"image": "",
|
|
"brightness": 10
|
|
},
|
|
"timing": {
|
|
"long_press_ms": 500,
|
|
"double_tap_ms": 300
|
|
}
|
|
}
|