Dynamic key generators: plugin system for auto-generated pages

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)
This commit is contained in:
Maksim Totmin
2026-06-21 21:05:32 +07:00
parent ac175fb144
commit 8df8b65163
9 changed files with 581 additions and 43 deletions
+17 -8
View File
@@ -339,18 +339,27 @@ func (s *WebServer) handleGetPages(w http.ResponseWriter, r *http.Request) {
defer s.mu.RUnlock()
type pageInfo struct {
Name string `json:"name"`
Keys []config.KeyConfig `json:"keys"`
Icon string `json:"icon,omitempty"`
Name string `json:"name"`
Keys []config.KeyConfig `json:"keys"`
Icon string `json:"icon,omitempty"`
DynamicKeys *config.DynamicKeyGen `json:"dynamic_keys,omitempty"`
Background string `json:"background,omitempty"`
EffectiveKeys []config.KeyConfig `json:"effective_keys,omitempty"`
}
pages := make([]pageInfo, 0, len(s.cfg.Pages))
for _, p := range s.cfg.Pages {
pages = append(pages, pageInfo{
Name: p.Name,
Keys: p.Keys,
Icon: p.Icon,
})
pi := pageInfo{
Name: p.Name,
Keys: p.Keys,
Icon: p.Icon,
DynamicKeys: p.DynamicKeys,
Background: p.Background,
}
if p.DynamicKeys != nil && s.pm != nil {
pi.EffectiveKeys = s.pm.GetEffectiveKeys(p.Name)
}
pages = append(pages, pi)
}
w.Header().Set("Content-Type", "application/json")