124 lines
7.8 KiB
Markdown
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.
|