# 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: ```csharp [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 `` 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: ```PowerShell $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.