Files
StreamDeck-Plugins/dynamic-deck
2026-09-09 00:02:16 -04:00
..
2026-09-09 00:02:16 -04:00
2026-09-09 00:02:16 -04:00
2026-09-09 00:02:16 -04:00
2026-09-09 00:02:16 -04:00
2026-09-09 00:02:16 -04:00
2026-09-09 00:02:16 -04:00
2026-09-09 00:02:16 -04:00
2026-09-09 00:02:16 -04:00
2026-09-09 00:02:16 -04:00
2026-09-09 00:02:16 -04:00
2026-09-09 00:02:16 -04:00

Dynamic Deck - Stream Deck Plugin Architecture & Development Context

Plugin ID: com.ryan-kegel.dynamic-deck
Author: Ryan Kegel Node.js Version: 24 SDK Version: 3 (Elgato Stream Deck Node.js SDK @elgato/streamdeck)


Overview

Dynamic Deck is a custom modular Stream Deck plugin built using TypeScript and Node.js. It integrates desktop application switching, smart home controls, API balance monitoring, and utility actions into a unified plugin structure.


Action Inventory

1. Fast App Launcher (com.ryan-kegel.dynamic-deck.launch-app)

Scans the host system for installed applications, populates a custom dropdown menu in the Property Inspector, automatically extracts application icons, updates button text, and either launches or focuses the selected application.

  • Property Inspector UI: Custom HTML/JS interface with styled select elements, dropdown options menu, and options checkboxes.
  • Key Features:
    • App Discovery: Scans Windows Start Menu shortcuts (.lnk files) via PowerShell scripts and extracts executable paths.
    • High-Resolution Icon Extraction: Uses Win32 P/Invoke (PrivateExtractIcons from user32.dll) via C# wrapped inside PowerShell to retrieve 256x256 PNG icons, falling back to standard ExtractAssociatedIcon.
    • Title Auto-Generation: Shortens application executable/shortcut names to a maximum of 2 words (e.g., "Visual Studio Code" -> "Visual Studio") and formats button labels automatically.
    • Process Focus / Bring-to-Front: Checks running system processes for existing window handles (MainWindowHandle). If found, it invokes Win32 ShowWindow (SW_RESTORE = 9) and SetForegroundWindow to bring active windows to the front instead of spawning duplicate app instances.
    • Native UI Title Sync: Eliminates duplicate input fields in the Property Inspector by leveraging Stream Deck's native built-in Title input and updating it via ev.action.setTitle().

2. DeepSeek Balance (com.ryan-kegel.dynamic-deck.deepseek-balance)

Monitors API key balance for DeepSeek services and opens the developer usage dashboard on key press.

  • Property Inspector UI: Modern sdpi-components form layout for secure API key configuration.

3. Ecobee Smart Home Controls

  • Temperature Control (com.ryan-kegel.dynamic-deck.ecobee): Adjusts thermostat target temperature and displays current indoor ambient temperature readings.
  • HVAC Mode (com.ryan-kegel.dynamic-deck.ecobee-mode): Toggles system operating modes (Heat, Cool, Auto, Off).

4. Counter (com.ryan-kegel.dynamic-deck.increment)

A basic numeric state incrementer action.


Key Architectural Decisions & Engineering Solutions

1. High-Resolution Icon Extraction Engine

Standard Win32 shell calls (ExtractAssociatedIcon) extract default 32x32 thumbnail icons, resulting in blurry, pixelated icons on Stream Deck keys.

Solution: In launch-app.ts, icon extraction was upgraded by embedding a custom C# helper class (HighResIconExtractor) compiled dynamically in PowerShell using Add-Type. It requests explicit 256x256 pixel icon groups directly from executable headers:

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern uint PrivateExtractIcons(
    string szFileName, int nIconIndex, int cxIcon, int cyIcon,
    IntPtr[] phicon, uint[] piconid, uint nIcons, uint flags);

The high-resolution bitmap is saved directly to an in-memory PNG stream and returned as base64 data to setImage().

2. Native Property Inspector Title Integration

Adding a custom <input type="text" id="titleInput"> field to launch-app.html resulted in two Title fields appearing in the settings UI because Stream Deck automatically injects its native Title field at the top of every Property Inspector.

Solution:

  1. The custom HTML row for Title in launch-app.html was removed.
  2. When an app is selected in the Property Inspector or picked via OpenFileDialog, the backend (launch-app.ts) calculates the shortened title name and invokes await ev.action.setTitle(title).
  3. Stream Deck automatically synchronizes this title to both the physical button face and the native Property Inspector Title input field.

3. Resolving Settings Race Conditions

Calling saveSettings() (setSettings) inside the Property Inspector WebSocket script (launch-app.js) concurrently with sending message payloads (sendToPlugin) created race conditions where stored setting objects were overwritten before backend logic could resolve.

Solution: Settings management was centralized into the backend action handler (launch-app.ts). The Property Inspector (launch-app.js) sends intent payloads (e.g., { event: "appSelected", app: fullAppObject }) to the backend plugin, which executes icon/title processing and calls await ev.action.setSettings(updatedSettings) in a single atomic transaction.

4. Windows Process Focus Logic

Standard child_process.execFile calls trigger new executable instances rather than bringing active windows to focus.

Solution: The plugin passes executable paths to an encoded PowerShell process focus routine before falling back to Start-Process:

$proc = Get-Process -Name $exeName -ErrorAction SilentlyContinue \vert{} Where-Object {$_.MainWindowHandle -ne 0 } | Select-Object -First 1;
if ($proc) {
    $type::ShowWindow($proc.MainWindowHandle, 9); # SW_RESTORE
    $type::SetForegroundWindow($proc.MainWindowHandle);
} else {
    Start-Process -FilePath $targetPath;
}

Data & Event Flow Sequence

┌─────────────────────────────────┐                 ┌─────────────────────────────────┐
│ Property Inspector (Client UI) │                 │ Plugin Backend (TypeScript)    │
└────────────────┬────────────────┘                 └────────────────┬────────────────┘
                 │                                                   │
                 │────── sendToPlugin: { event: "getApps" } ────────>│
                 │                                                   │─── Scans Start Menu
                 │                                                   │─── Extracts 256x256 Base64 Icons
                 │<───── sendToPropertyInspector: apps [] ───────────│
                 │                                                   │
                 │────── sendToPlugin: { event: "appSelected" } ────>│
                 │                                                   │─── Formats/Shortens Title
                 │                                                   │─── ev.action.setTitle(...)
                 │                                                   │─── ev.action.setImage(...)
                 │                                                   │─── ev.action.setSettings(...)
                 │<───── sendToPropertyInspector: { title } ─────────│

File Structure Map

  • manifest.json: Plugin metadata, minimum SDK/OS targets, action declarations, and entry points.
  • src/actions/launch-app.ts: Fast App Launcher backend logic (PowerShell app discovery, high-res P/Invoke icon extraction, window activation, settings persistence).
  • ui/launch-app.html: Property Inspector structure for App Launcher options and dropdown.
  • ui/launch-app.js: Property Inspector logic handling WebSocket initialization, dropdown UI rendering, and message dispatch.ui/deepseek-balance.html: Property Inspector form using for API key entry.