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:
@@ -43,10 +43,19 @@ type PageConfig struct {
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Keys []KeyConfig `json:"keys"`
|
||||
DynamicKeys *DynamicKeyGen `json:"dynamic_keys,omitempty"`
|
||||
Background string `json:"background,omitempty"`
|
||||
Screensaver *ScreensaverCfg `json:"screensaver,omitempty"`
|
||||
}
|
||||
|
||||
type DynamicKeyGen struct {
|
||||
Command string `json:"command,omitempty"`
|
||||
Script string `json:"script,omitempty"`
|
||||
Interval string `json:"interval,omitempty"`
|
||||
Timeout string `json:"timeout,omitempty"`
|
||||
MaxKeys int `json:"max_keys,omitempty"`
|
||||
}
|
||||
|
||||
type KeyConfig struct {
|
||||
Index int `json:"index"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
@@ -293,6 +302,35 @@ func (c *Config) Validate() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.DynamicKeys != nil {
|
||||
if p.DynamicKeys.Command == "" && p.DynamicKeys.Script == "" {
|
||||
return fmt.Errorf("page %s: dynamic_keys requires command or script", p.Name)
|
||||
}
|
||||
if p.DynamicKeys.Command != "" && p.DynamicKeys.Script != "" {
|
||||
return fmt.Errorf("page %s: dynamic_keys command and script are mutually exclusive", p.Name)
|
||||
}
|
||||
if p.DynamicKeys.Interval != "" {
|
||||
interval, err := time.ParseDuration(p.DynamicKeys.Interval)
|
||||
if err != nil {
|
||||
return fmt.Errorf("page %s: invalid dynamic_keys interval %q: %w", p.Name, p.DynamicKeys.Interval, err)
|
||||
}
|
||||
if interval < time.Second {
|
||||
return fmt.Errorf("page %s: dynamic_keys interval must be at least 1s", p.Name)
|
||||
}
|
||||
if interval > 24*time.Hour {
|
||||
return fmt.Errorf("page %s: dynamic_keys interval must not exceed 24h", p.Name)
|
||||
}
|
||||
}
|
||||
if p.DynamicKeys.Timeout != "" {
|
||||
if _, err := time.ParseDuration(p.DynamicKeys.Timeout); err != nil {
|
||||
return fmt.Errorf("page %s: invalid dynamic_keys timeout %q: %w", p.Name, p.DynamicKeys.Timeout, err)
|
||||
}
|
||||
}
|
||||
if p.DynamicKeys.MaxKeys < 0 {
|
||||
return fmt.Errorf("page %s: dynamic_keys max_keys must be >= 0", p.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range c.Pages {
|
||||
|
||||
Reference in New Issue
Block a user