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:
@@ -3,10 +3,12 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -99,6 +101,52 @@ func execDisplayCapture(d *config.DisplayCfg, timeout time.Duration) (string, er
|
||||
return output, err
|
||||
}
|
||||
|
||||
func execDynamicKeys(dg *config.DynamicKeyGen, timeout time.Duration, keyCount int) ([]config.KeyConfig, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if dg.Command != "" {
|
||||
cmd = exec.CommandContext(ctx, "sh", "-c", dg.Command)
|
||||
} else {
|
||||
script := dg.Script
|
||||
if !filepath.IsAbs(script) {
|
||||
script = filepath.Join(configDir(), script)
|
||||
}
|
||||
cmd = exec.CommandContext(ctx, script)
|
||||
}
|
||||
|
||||
// Set environment variables for the script
|
||||
cmd.Env = append(os.Environ(),
|
||||
fmt.Sprintf("STREAMDECK_KEY_COUNT=%d", keyCount),
|
||||
fmt.Sprintf("STREAMDECK_CONFIG_DIR=%s", configDir()),
|
||||
)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
|
||||
// Log stderr separately (don't merge into stdout for JSON parsing)
|
||||
if stderr.Len() > 0 {
|
||||
slog.Debug("dynamic_keys script stderr", "output", stderr.String())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("execute dynamic_keys: %w", err)
|
||||
}
|
||||
|
||||
// Parse JSON from stdout
|
||||
var keys []config.KeyConfig
|
||||
output := stdout.String()
|
||||
if err := json.Unmarshal([]byte(output), &keys); err != nil {
|
||||
return nil, fmt.Errorf("parse dynamic_keys JSON: %w", err)
|
||||
}
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func execBuiltin(a *config.Action, deck *Deck, pm *PageManager) error {
|
||||
parts := strings.SplitN(a.Builtin, ":", 2)
|
||||
if len(parts) < 1 {
|
||||
|
||||
Reference in New Issue
Block a user