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

124 lines
7.8 KiB
Markdown

# 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 `<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:
```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 <sdpi-components> for API key entry.