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:
+23
-1
@@ -52,6 +52,7 @@ document.addEventListener('alpine:init', () => {
|
||||
await this.loadConfig()
|
||||
await this.loadDecks()
|
||||
setInterval(() => this.pollDisplayOutputs(), 3000)
|
||||
setInterval(() => this.loadEffectiveKeys(), 10000)
|
||||
this.connectSSE()
|
||||
},
|
||||
|
||||
@@ -101,6 +102,7 @@ document.addEventListener('alpine:init', () => {
|
||||
const res = await fetch('/api/config')
|
||||
this.config = await res.json()
|
||||
this.pages = this.config.pages
|
||||
await this.loadEffectiveKeys()
|
||||
if (this.pages.length > 0) {
|
||||
const saved = localStorage.getItem('sd_active_page')
|
||||
if (saved && this.pages.find(p => p.name === saved)) {
|
||||
@@ -115,6 +117,19 @@ document.addEventListener('alpine:init', () => {
|
||||
}
|
||||
},
|
||||
|
||||
async loadEffectiveKeys() {
|
||||
try {
|
||||
const res = await fetch('/api/pages')
|
||||
const pagesData = await res.json()
|
||||
for (const pd of pagesData) {
|
||||
const page = this.pages.find(p => p.name === pd.name)
|
||||
if (page && pd.effective_keys && pd.effective_keys.length > 0) {
|
||||
page.effective_keys = pd.effective_keys
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
},
|
||||
|
||||
syncSettings() {
|
||||
this.settingsForm = {
|
||||
brightness: this.getDeviceConfig(this.activeDeckSerial)?.brightness ?? 75,
|
||||
@@ -151,10 +166,13 @@ document.addEventListener('alpine:init', () => {
|
||||
get gridKeys() {
|
||||
const page = this.currentPage
|
||||
if (!page) return []
|
||||
const sourceKeys = (page.effective_keys && page.effective_keys.length > 0)
|
||||
? page.effective_keys
|
||||
: (page.keys || [])
|
||||
const keys = []
|
||||
const n = this.activeDeck.num_keys
|
||||
for (let i = 0; i < n; i++) {
|
||||
const kc = page.keys.find(k => k.index === i)
|
||||
const kc = sourceKeys.find(k => k.index === i)
|
||||
const dout = this.displayOutputs[i]
|
||||
keys.push({
|
||||
index: i,
|
||||
@@ -166,6 +184,7 @@ document.addEventListener('alpine:init', () => {
|
||||
displayBg: dout?.background || '',
|
||||
displayText: dout?.text || '',
|
||||
previewUrl: this.keyPreviewUrl(kc ? kc : {}, dout),
|
||||
isDynamic: !!(page.dynamic_keys && page.effective_keys?.find(k => k.index === i)),
|
||||
})
|
||||
}
|
||||
return keys
|
||||
@@ -306,6 +325,9 @@ document.addEventListener('alpine:init', () => {
|
||||
const page = this.currentPage
|
||||
if (!page) return
|
||||
const kc = page.keys.find(k => k.index === idx)
|
||||
if (!kc && page.dynamic_keys && page.effective_keys?.find(k => k.index === idx)) {
|
||||
return
|
||||
}
|
||||
this.editing = idx
|
||||
this.showAdvanced = false
|
||||
|
||||
|
||||
Reference in New Issue
Block a user