### Install Plugin Dependencies Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Install project dependencies within a sample directory. Run this before linking or building. ```bash npm install ``` -------------------------------- ### Build and Link Stream Deck Plugin Source: https://github.com/elgatosf/streamdeck-plugin-samples/blob/main/README.md Steps to build a Stream Deck plugin sample. This includes installing dependencies, linking the plugin to Stream Deck, and starting a watch process for development. ```bash # Install dependencies npm i # Install the sample in Stream Deck (*.sdPlugin should be # replaced with the name of the folder ending in .sdPlugin) streamdeck link *.sdPlugin # Build the sample and watch for changes npm run watch ``` -------------------------------- ### Install Dependencies with pnpm Source: https://github.com/elgatosf/streamdeck-plugin-samples/blob/main/image-resizer/README.md Installs project dependencies and fetches platform-specific native binaries for modules like sharp. ```sh pnpm install ``` -------------------------------- ### Install Stream Deck CLI Source: https://github.com/elgatosf/streamdeck-plugin-samples/blob/main/README.md Install the Stream Deck CLI tool globally using npm. This tool is recommended for building and managing Stream Deck plugins. ```bash npm install -g @elgato/cli ``` -------------------------------- ### Build the Plugin with pnpm Source: https://github.com/elgatosf/streamdeck-plugin-samples/blob/main/image-resizer/README.md Builds the Stream Deck plugin after dependencies are installed. ```sh pnpm build ``` -------------------------------- ### Configure Select Component with Data Source Source: https://github.com/elgatosf/streamdeck-plugin-samples/blob/main/data-sources/README.md Use the 'datasource' attribute on an sdpi-select component to dynamically populate its options. This example shows how to link the component to a 'getProducts' data source. ```html ``` -------------------------------- ### Set Key Image Dynamically with action.setImage() Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Use `setImage()` with a base64-encoded data URI to display any image on a key. This example fetches a remote image and renders it without file I/O. ```typescript import streamDeck from "@elgato/streamdeck"; import { KeyAction } from "@elgato/streamdeck"; async function setRemoteImage(action: KeyAction) { try { const response = await fetch("https://cataas.com/cat?width=144&height=144"); const buffer = await response.arrayBuffer(); const base64 = Buffer.from(buffer).toString("base64"); // data URI format: "data:;base64," await action.setImage(`data:image/png;base64,${base64}`); } catch (e) { streamDeck.logger.error("Failed to set image:", e); } } ``` -------------------------------- ### Image Resizing Action with Sharp Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt An example Stream Deck action that uses the `sharp` library to resize an image. It takes an `imagePath`, `width`, `height`, and `extension` from settings and saves the resized image. ```typescript // src/actions/resize-image.ts import streamDeck, { action, SingletonAction, KeyDownEvent } from "@elgato/streamdeck"; import path from "node:path"; import sharp from "sharp"; // resolved from com.elgato.image-resizer.sdPlugin/node_modules @action({ UUID: "com.elgato.image-resizer.resize" }) export class ResizeImage extends SingletonAction<{ imagePath?: string; width?: number; height?: number; extension?: "jpg"|"png"|"webp" }> { override async onKeyDown(ev: KeyDownEvent): Promise { const { imagePath, extension = "jpg", width = 1920, height = 1080 } = ev.payload.settings; if (!imagePath) { ev.action.showAlert(); return; } const { dir, name } = path.parse(imagePath); const out = path.join(dir, `${name} (${width}x${height}).${extension}`); await sharp(imagePath).resize(Number(width), Number(height)).toFormat(extension).toFile(out); ev.action.showOk(); // Output: file written to e.g. "/Users/me/photo (1920x1080).jpg" } } ``` -------------------------------- ### Property Inspector Settings UI with Range Slider Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt An HTML file for a Stream Deck Property Inspector using `sdpi-components`. This example renders a labelled slider for the `incrementBy` setting, which is automatically persisted. ```html ``` -------------------------------- ### Property Inspector Settings UI with Checkbox Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt An HTML file for a Stream Deck Property Inspector using `sdpi-components`. This example includes a labelled checkbox for the `autoUpdate` setting, which is a boolean that is persisted and available in `ev.payload.settings`. ```html ``` -------------------------------- ### Detect Short Press vs. Long Press with `onKeyDown` and `onKeyUp` Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Distinguish between a tap and a long press by starting a timer in `onKeyDown` and clearing it in `onKeyUp`. If the timer completes, it signifies a long press. ```typescript import { action, SingletonAction, KeyDownEvent, KeyUpEvent } from "@elgato/streamdeck"; @action({ UUID: "com.example.press-demo" }) export class PressDemo extends SingletonAction { private timers = new Map>(); override onKeyDown(ev: KeyDownEvent): void { const id = ev.action.id; this.timers.set(id, setTimeout(() => { this.timers.delete(id); ev.action.setTitle("LONG"); // long press (>1s) }, 1000)); } override onKeyUp(ev: KeyUpEvent): void { const timer = this.timers.get(ev.action.id); if (timer) { clearTimeout(timer); this.timers.delete(ev.action.id); ev.action.setTitle("TAP"); // short press } } } ``` -------------------------------- ### Build and Watch for Changes Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Build the plugin and watch for file changes. This triggers a plugin restart on save, enabling live development. ```bash npm run watch ``` -------------------------------- ### Build Plugin Once Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Build the plugin once. This command compiles the plugin's code. ```bash npm run build ``` -------------------------------- ### Build and Link Image Resizer Plugin Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Build the 'image-resizer' plugin using pnpm and link it to Stream Deck. This ensures native modules are handled correctly. ```bash pnpm build streamdeck link com.elgato.image-resizer.sdPlugin ``` -------------------------------- ### Plugin Entry Point and Connection Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Registers actions and establishes the WebSocket connection to the Stream Deck application. Enables diagnostic tracing of SDK messages. ```typescript // plugin.ts import streamDeck from "@elgato/streamdeck"; import { IncrementCounter } from "./actions/increment-counter"; // Enable trace-level logging for all SDK messages (remove in production) streamDeck.logger.setLevel("trace"); // Register each action class by instantiating it streamDeck.actions.registerAction(new IncrementCounter()); // Open the WebSocket connection to Stream Deck streamDeck.connect(); ``` -------------------------------- ### Persisting Action Settings with `setSettings()` and `getSettings()` Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Demonstrates how to save and retrieve persistent settings for an action. `setSettings()` is fire-and-forget, while `getSettings()` returns a Promise. ```APIDOC ## `action.setSettings()` / `action.getSettings()` — Persisting Action Settings Settings are serialized to JSON and stored by Stream Deck. They are restored on subsequent `onWillAppear` events. `getSettings()` returns a Promise; `setSettings()` is fire-and-forget. ```typescript import { action, SingletonAction, KeyDownEvent, KeyUpEvent } from "@elgato/streamdeck"; type CounterSettings = { count?: number; incrementBy?: number }; @action({ UUID: "com.elgato.counter.action" }) export class IncrementCounter extends SingletonAction { private pressState = new Map(); override async onKeyDown(ev: KeyDownEvent): Promise { // Long-press (1500ms) resets the counter const timeoutId = setTimeout(() => { this.pressState.delete(ev.action.id); ev.action.setTitle("0"); ev.action.setSettings({ ...ev.payload.settings, count: 0 }); // persist reset }, 1500); const controller = new AbortController(); controller.signal.addEventListener("abort", () => { this.pressState.delete(ev.action.id); clearTimeout(timeoutId); }, { once: true }); this.pressState.set(ev.action.id, controller); } override async onKeyUp(ev: KeyUpEvent): Promise { const controller = this.pressState.get(ev.action.id); if (!controller) return; // long-press already fired controller.abort(); // cancel the reset timer const settings = { ...ev.payload.settings }; settings.incrementBy ??= 1; settings.count = (settings.count ?? 0) + settings.incrementBy; await ev.action.setSettings(settings); // persist new count await ev.action.setTitle(`${settings.count}`); } } ``` ``` -------------------------------- ### Persist Action Settings with `setSettings()` and `getSettings()` Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Use `setSettings()` to save action configurations and `getSettings()` to retrieve them. Settings are JSON-serialized and restored on `onWillAppear`. `getSettings()` returns a Promise; `setSettings()` is fire-and-forget. ```typescript import { action, SingletonAction, KeyDownEvent, KeyUpEvent } from "@elgato/streamdeck"; type CounterSettings = { count?: number; incrementBy?: number }; @action({ UUID: "com.elgato.counter.action" }) export class IncrementCounter extends SingletonAction { private pressState = new Map(); override async onKeyDown(ev: KeyDownEvent): Promise { // Long-press (1500ms) resets the counter const timeoutId = setTimeout(() => { this.pressState.delete(ev.action.id); ev.action.setTitle("0"); ev.action.setSettings({ ...ev.payload.settings, count: 0 }); // persist reset }, 1500); const controller = new AbortController(); controller.signal.addEventListener("abort", () => { this.pressState.delete(ev.action.id); clearTimeout(timeoutId); }, { once: true }); this.pressState.set(ev.action.id, controller); } override async onKeyUp(ev: KeyUpEvent): Promise { const controller = this.pressState.get(ev.action.id); if (!controller) return; // long-press already fired controller.abort(); // cancel the reset timer const settings = { ...ev.payload.settings }; settings.incrementBy ??= 1; settings.count = (settings.count ?? 0) + settings.incrementBy; await ev.action.setSettings(settings); // persist new count await ev.action.setTitle(`${settings.count}`); } } ``` -------------------------------- ### Visual Feedback with `showOk()` and `showAlert()` Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Provides visual confirmation on the Stream Deck key. `showOk()` displays a green checkmark for success, and `showAlert()` displays a yellow warning symbol for errors. ```APIDOC ## `action.showOk()` / `action.showAlert()` — Visual Feedback Indicators `showOk()` briefly displays a green checkmark on the key to confirm a successful operation. `showAlert()` displays a yellow warning symbol to indicate an error. ```typescript import streamDeck, { action, SingletonAction, KeyDownEvent } from "@elgato/streamdeck"; import sharp from "sharp"; import path from "node:path"; type ResizeImageSettings = { imagePath?: string; width?: number | string; height?: number | string; extension?: "jpg" | "png" | "webp"; }; @action({ UUID: "com.elgato.image-resizer.resize" }) export class ResizeImage extends SingletonAction { override async onKeyDown(ev: KeyDownEvent): Promise { const { imagePath, extension = "jpg", width = 1920, height = 1080 } = ev.payload.settings; const w = typeof width === "string" ? parseInt(width, 10) : width; const h = typeof height === "string" ? parseInt(height, 10) : height; if (!imagePath || isNaN(w) || isNaN(h) || w <= 0 || h <= 0) { ev.action.showAlert(); // yellow "!" on the key streamDeck.logger.error("Invalid settings for image resize."); return; } try { const { dir, name } = path.parse(imagePath); const outputPath = path.join(dir, `${name} (${w}x${h}).${extension}`); await sharp(imagePath).resize(w, h).toFormat(extension).toFile(outputPath); ev.action.showOk(); // green "✓" on the key } catch (error) { ev.action.showAlert(); // yellow "!" on the key streamDeck.logger.error("Failed to resize image:", error); } } } ``` ``` -------------------------------- ### Provide Visual Feedback with `showOk()` and `showAlert()` Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Use `showOk()` to display a green checkmark for success and `showAlert()` for a yellow warning symbol on error. These methods provide immediate visual confirmation to the user. ```typescript import streamDeck, { action, SingletonAction, KeyDownEvent } from "@elgato/streamdeck"; import sharp from "sharp"; import path from "node:path"; type ResizeImageSettings = { imagePath?: string; width?: number | string; height?: number | string; extension?: "jpg" | "png" | "webp"; }; @action({ UUID: "com.elgato.image-resizer.resize" }) export class ResizeImage extends SingletonAction { override async onKeyDown(ev: KeyDownEvent): Promise { const { imagePath, extension = "jpg", width = 1920, height = 1080 } = ev.payload.settings; const w = typeof width === "string" ? parseInt(width, 10) : width; const h = typeof height === "string" ? parseInt(height, 10) : height; if (!imagePath || isNaN(w) || isNaN(h) || w <= 0 || h <= 0) { ev.action.showAlert(); // yellow "!" on the key streamDeck.logger.error("Invalid settings for image resize."); return; } try { const { dir, name } = path.parse(imagePath); const outputPath = path.join(dir, `${name} (${w}x${h}).${extension}`); await sharp(imagePath).resize(w, h).toFormat(extension).toFile(outputPath); ev.action.showOk(); // green "✓" on the key } catch (error) { ev.action.showAlert(); // yellow "!" on the key streamDeck.logger.error("Failed to resize image:", error); } } } ``` -------------------------------- ### pnpm Configuration in package.json Source: https://github.com/elgatosf/streamdeck-plugin-samples/blob/main/image-resizer/README.md Configures pnpm to manage dependencies and run post-install scripts for native modules within the plugin directory. ```json "packageManager": "pnpm@10.32.0", "postinstall": "pnpm --dir com.elgato.image-resizer.sdPlugin install --frozen-lockfile" ``` -------------------------------- ### Setting a Key Image Dynamically with `action.setImage()` Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Dynamically set a key's image by providing a base64-encoded data URI. This is useful for displaying remote images without file I/O. ```APIDOC ## `action.setImage()` — Setting a Key Image Dynamically `setImage()` accepts a base64-encoded data URI to display any image on a key. This is how the Cat Keys sample fetches a remote image and renders it without any file I/O. ```typescript import streamDeck from "@elgato/streamdeck"; import { KeyAction } from "@elgato/streamdeck"; async function setRemoteImage(action: KeyAction) { try { const response = await fetch("https://cataas.com/cat?width=144&height=144"); const buffer = await response.arrayBuffer(); const base64 = Buffer.from(buffer).toString("base64"); // data URI format: "data:;base64," await action.setImage(`data:image/png;base64,${base64}`); } catch (e) { streamDeck.logger.error("Failed to set image:", e); } } ``` ``` -------------------------------- ### Link Plugin to Stream Deck Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Link the plugin folder to Stream Deck for hot-reloading on rebuild. Replace 'com.elgato.counter.sdPlugin' with your plugin's UUID. ```bash streamdeck link com.elgato.counter.sdPlugin ``` -------------------------------- ### Open URL in Default Browser with streamDeck.system.openUrl() Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Use this to open a URL in the user's default browser. Ensure the URL is correctly formatted. ```typescript import streamDeck, { action, SingletonAction, KeyDownEvent } from "@elgato/streamdeck"; type Settings = { product?: string }; @action({ UUID: "com.elgato.product-viewer.open-product-page" }) export class OpenProductPage extends SingletonAction { override onKeyDown(ev: KeyDownEvent): void { const url = ev.payload.settings.product; if (url) { // Opens e.g. "https://www.elgato.com/uk/en/p/stream-deck-plus" in the browser streamDeck.system.openUrl(url); } } } ``` -------------------------------- ### Handle Lifecycle Events with onWillAppear and onWillDisappear Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Use `onWillAppear` to initialize state and `onWillDisappear` to clean up timers or subscriptions when an action appears or disappears from the Stream Deck layout. ```typescript import streamDeck, { action, SingletonAction, WillAppearEvent, WillDisappearEvent } from "@elgato/streamdeck"; const FIFTEEN_MINUTES = 15 * 60 * 1000; @action({ UUID: "com.elgato.cat-keys.random-cat" }) export class RandomCat extends SingletonAction<{ autoUpdate: boolean }> { private timer: NodeJS.Timeout | undefined; override onWillAppear(ev: WillAppearEvent<{ autoUpdate: boolean }>): void { if (!ev.action.isKey()) return; // Immediately fetch and display a cat image this.fetchAndSetCat(ev.action); // Start a polling timer if not already running if (!this.timer) { this.timer = setInterval(() => { for (const action of this.actions) { if (action.isKey()) { action.getSettings().then((settings) => { if (settings.autoUpdate) this.fetchAndSetCat(action); }); } } }, FIFTEEN_MINUTES); } } override async onWillDisappear(ev: WillDisappearEvent<{ autoUpdate: boolean }>): Promise { // Clean up timer when the last instance disappears if (this.actions.next().done) { clearInterval(this.timer); this.timer = undefined; } } private async fetchAndSetCat(action: any) { const response = await fetch("https://cataas.com/cat?width=144&height=144"); const buffer = await response.arrayBuffer(); const data = Buffer.from(buffer).toString("base64"); action.setImage(`data:image/png;base64,${data}`); } } ``` -------------------------------- ### Internationalization (i18n) with streamDeck.i18n.translate() Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Translates localization keys using locale JSON files placed at the root of the `.sdPlugin` folder. If no language is specified, the user's system language is used. ```typescript // src/actions/newString.ts import streamdeck, { action, SingletonAction, WillAppearEvent, KeyDownEvent, Language } from "@elgato/streamdeck"; type NewStringSettings = { language: Language | undefined; titleKey: string | undefined }; @action({ UUID: "com.elgato.hello-world.newstring" }) export class NewString extends SingletonAction { override async onWillAppear(ev: WillAppearEvent): Promise { if (!ev.action.isKey()) return; let settings = ev.payload.settings; if (!settings.titleKey) { settings = { ...settings, titleKey: "clickme" }; ev.action.setSettings(settings); } await this.displayTitle(settings, ev.action); } override async onKeyDown(ev: KeyDownEvent): Promise { const keys = ["flash", "selectprop", "on", "off", "toggle", "action", "dynamic", "clickme"]; const titleKey = keys[Math.floor(Math.random() * keys.length)]; const settings = { ...ev.payload.settings, titleKey }; ev.action.setSettings(settings); await this.displayTitle(settings, ev.action); } private async displayTitle(settings: NewStringSettings, action: any) { // translate("clickme", "de") → "Klick mich" (from de.json) // translate("thisLanguage", "de") → "Deutsch" const title = streamdeck.i18n.translate(settings.titleKey!, settings.language); const lang = streamdeck.i18n.translate("thisLanguage", settings.language); await action.setTitle(`${lang}\n\n${title}`); } } ``` ```json // com.elgato.hello-world.sdPlugin/en.json { "Localization": { "flash": "Flash", "clickme": "Click Me", "thisLanguage": "English" } } ``` -------------------------------- ### streamDeck.system.openUrl() Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Instructs Stream Deck to open a URL in the user's default browser. This is the recommended cross-platform method for launching web links from a plugin. ```APIDOC ## `streamDeck.system.openUrl()` — Opening URLs in the Default Browser `openUrl()` instructs Stream Deck to open a URL in the user's default browser. It is the correct cross-platform way to launch web links from a plugin. ```typescript import streamDeck, { action, SingletonAction, KeyDownEvent } from "@elgato/streamdeck"; type Settings = { product?: string }; @action({ UUID: "com.elgato.product-viewer.open-product-page" }) export class OpenProductPage extends SingletonAction { override onKeyDown(ev: KeyDownEvent): void { const url = ev.payload.settings.product; if (url) { // Opens e.g. "https://www.elgato.com/uk/en/p/stream-deck-plus" in the browser streamDeck.system.openUrl(url); } } } ``` ``` -------------------------------- ### pnpm Workspace Configuration for Plugin Source: https://github.com/elgatosf/streamdeck-plugin-samples/blob/main/image-resizer/README.md Sets up a minimal pnpm workspace within the plugin folder to manage its dependencies, including specific settings for native module handling. ```yaml packages: - . nodeLinker: hoisted packageImportMethod: copy supportedArchitectures: os: - win32 - darwin cpu: - x64 - arm64 onlyBuiltDependencies: - sharp ``` -------------------------------- ### Stream Deck Plugin Build Command Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt The command to build a Stream Deck plugin using the Elgato Stream Deck CLI. Requires Node.js 20+. ```bash ``` -------------------------------- ### Configure pnpm for Native Modules Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Configure `pnpm-workspace.yaml` to support specific architectures for native Node.js modules like `sharp`. Mark native packages as `external` in Rollup to avoid bundling their binary files. ```yaml // com.elgato.image-resizer.sdPlugin/pnpm-workspace.yaml (simplified) // nodeLinker: hoisted // packageImportMethod: copy // supportedArchitectures: // os: [win32, darwin] // cpu: [x64, arm64] // onlyBuiltDependencies: [sharp] ``` -------------------------------- ### Stream Deck Plugin Manifest Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt The `manifest.json` file declares plugin metadata, including UUID, SDK version, platform requirements, and action definitions. SDKVersion 2 targets the current Node.js SDK, while SDKVersion 3 targets a newer version. ```json { "Name": "Counter", "Version": "1.5.0.0", "Author": "Elgato", "UUID": "com.elgato.counter", "SDKVersion": 3, "CodePath": "bin/plugin.js", "Description": "A simple counter action for Stream Deck.", "Software": { "MinimumVersion": "6.9" }, "OS": [ { "Platform": "mac", "MinimumVersion": "12" }, { "Platform": "windows", "MinimumVersion": "10" } ], "Nodejs": { "Version": "20", "Debug": "enabled" }, "Actions": [ { "Name": "Count", "UUID": "com.elgato.counter.action", "Icon": "imgs/actions/counter/icon", "Tooltip": "Displays a count, which increments on press.", "PropertyInspectorPath": "ui/increment-counter.html", "Controllers": ["Keypad"], "UserTitleEnabled": false, "States": [ { "Image": "imgs/actions/counter/key", "TitleAlignment": "middle" } ] } ] } ``` -------------------------------- ### Lifecycle Events: onWillAppear and onWillDisappear Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt These events are triggered when an action appears or disappears from the Stream Deck layout. Use `onWillAppear` for initialization and `onWillDisappear` for cleanup. ```APIDOC ## `onWillAppear` / `onWillDisappear` — Lifecycle Events These events fire when an action appears or disappears from the visible Stream Deck layout (e.g., page or profile change). Use `onWillAppear` to initialize state and `onWillDisappear` to clean up timers or subscriptions. ```typescript import streamDeck, { action, SingletonAction, WillAppearEvent, WillDisappearEvent } from "@elgato/streamdeck"; const FIFTEEN_MINUTES = 15 * 60 * 1000; @action({ UUID: "com.elgato.cat-keys.random-cat" }) export class RandomCat extends SingletonAction<{ autoUpdate: boolean }> { private timer: NodeJS.Timeout | undefined; override onWillAppear(ev: WillAppearEvent<{ autoUpdate: boolean }>): void { if (!ev.action.isKey()) return; // Immediately fetch and display a cat image this.fetchAndSetCat(ev.action); // Start a polling timer if not already running if (!this.timer) { this.timer = setInterval(() => { for (const action of this.actions) { if (action.isKey()) { action.getSettings().then((settings) => { if (settings.autoUpdate) this.fetchAndSetCat(action); }); } } }, FIFTEEN_MINUTES); } } override async onWillDisappear(ev: WillDisappearEvent<{ autoUpdate: boolean }>): Promise { // Clean up timer when the last instance disappears if (this.actions.next().done) { clearInterval(this.timer); this.timer = undefined; } } private async fetchAndSetCat(action: any) { const response = await fetch("https://cataas.com/cat?width=144&height=144"); const buffer = await response.arrayBuffer(); const data = Buffer.from(buffer).toString("base64"); action.setImage(`data:image/png;base64,${data}`); } } ``` ``` -------------------------------- ### Declare Native Dependency in Plugin package.json Source: https://github.com/elgatosf/streamdeck-plugin-samples/blob/main/image-resizer/README.md Declares 'sharp' as a dependency within the plugin's specific package.json to ensure it's included in the final plugin bundle. ```json "dependencies": { "sharp": "^0.34.5" } ``` -------------------------------- ### Switch Encoder Touch Display Layouts with setFeedbackLayout() Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Use `setFeedbackLayout()` to swap the entire layout on an encoder's touch display. You can use built-in layout identifiers (e.g., `$A1`) or relative paths to custom JSON layout files within the `.sdPlugin` folder. Ensure the layout supports the feedback items you are setting. ```typescript import { action, SingletonAction, DialRotateEvent } from "@elgato/streamdeck"; @action({ UUID: "com.elgato.layouts.built-in-layout" }) export class BuiltInLayout extends SingletonAction { override onDialRotate(ev: DialRotateEvent<{ value: number }>): void { let { value = 0 } = ev.payload.settings; value = Math.max(0, Math.min(5, value + (ev.payload.ticks > 0 ? 1 : -1))); switch (value) { case 0: ev.action.setFeedbackLayout("$X1"); // minimal single-value layout ev.action.setFeedback({ title: "Layout $X1" }); break; case 1: ev.action.setFeedbackLayout("$A0"); // full-canvas layout ev.action.setFeedback({ title: "Layout $A0", "full-canvas": { background: "blue" } }); break; case 3: ev.action.setFeedbackLayout("$B1"); // title + value + indicator bar ev.action.setFeedback({ title: "Volume", value: "50", indicator: { value: "50" } }); break; case 5: ev.action.setFeedbackLayout("$C1"); // dual icon + dual indicator ev.action.setFeedback({ icon1: { value: "imgs/actions/layout/layout.svg" }, icon2: { value: "imgs/actions/layout/layout.svg" }, indicator1: { value: 25 }, indicator2: { value: 75 }, }); break; } ev.action.setSettings({ value }); } } ``` ```typescript // Switch to a custom JSON layout file bundled inside the .sdPlugin folder await ev.action.setFeedbackLayout("layouts/custom-layout-1.json"); await ev.action.setFeedback({ title: "Custom Layout 1" }); // Custom layout with image and multiple named slots await ev.action.setFeedbackLayout("layouts/custom-layout-3.json"); await ev.action.setFeedback({ battery: { value: 50 }, icon2: { value: "imgs/actions/custom-layout/battery.svg" }, icon: { value: "imgs/actions/custom-layout/controller.svg" }, percent: { value: "50%" }, }); ``` -------------------------------- ### Link Plugin to Stream Deck Source: https://github.com/elgatosf/streamdeck-plugin-samples/blob/main/image-resizer/README.md Links the built plugin to the Stream Deck application for testing and development. ```sh streamdeck link com.elgato.image-resizer.sdPlugin ``` -------------------------------- ### action.setFeedbackLayout() Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt `setFeedbackLayout()` swaps the entire layout on an encoder's touch display. It accepts built-in layout identifiers or relative paths to custom layout files. ```APIDOC ## `action.setFeedbackLayout()` ### Description Swaps the entire layout on an encoder's touch display. You can use built-in layout identifiers (e.g., `$A1`, `$B1`, `$C1`) or a relative path to a custom `*.json` layout file located within the `.sdPlugin` folder. ### Method `setFeedbackLayout(layout: string)` ### Parameters #### Path Parameters - **layout** (string) - Required - The identifier for a built-in layout (e.g., `$X1`, `$A0`, `$B1`, `$C1`) or a relative path to a custom JSON layout file (e.g., `layouts/custom-layout-1.json`). ### Request Example ```typescript // Switch to a built-in layout ev.action.setFeedbackLayout("$X1"); // Switch to a custom JSON layout file bundled inside the .sdPlugin folder await ev.action.setFeedbackLayout("layouts/custom-layout-1.json"); ``` ### Response This method does not return a value. ``` -------------------------------- ### Manage Multi-State Key Icons with setState Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Use `setState(index)` to switch the displayed icon for a key. This is useful for creating toggle-style buttons without needing to call `setImage`. Ensure your `manifest.json` declares the available states. ```typescript import { action, SingletonAction } from "@elgato/streamdeck"; // manifest.json declares two States: index 0 = "light off", index 1 = "light on" @action({ UUID: "com.elgato.lightsout.gamepiece" }) export class GamePiece extends SingletonAction<{ light: 0 | 1 }> { private states = new Map(); // Turn all tracked keys on or off setAll(deviceId: string, state: 0 | 1) { for (const action of this.actions) { if (!action.isKey() || action.device.id !== deviceId) continue; this.states.set(action.id, state); action.setState(state); // 0 = off icon, 1 = on icon } } // Flash all keys on/off N times with a 100ms interval async flashAll(deviceId: string, count: number) { while (count-- > 0) { await new Promise(r => setTimeout(r, 100)); this.setAll(deviceId, 1); // off await new Promise(r => setTimeout(r, 100)); this.setAll(deviceId, 0); // on } } } ``` -------------------------------- ### action.setFeedback() Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt `setFeedback()` updates the content of the currently active layout on an encoder's touch display. Keys in the feedback object correspond to named items defined in the active layout. ```APIDOC ## `action.setFeedback()` ### Description Updates the content of the currently active layout on an encoder's touch display. Keys in the feedback object correspond to named items (e.g., `title`, `value`, `indicator`) defined in the active layout. ### Method `setFeedback(feedback: object)` ### Parameters #### Request Body - **feedback** (object) - Required - An object where keys correspond to named items in the active layout and values are the content to display. ### Request Example ```typescript ev.action.setFeedback({ value: 50, indicator: { value: 50 } }); ev.action.setFeedback({ title: "Volume", value: "50", indicator: { value: "50" } }); ev.action.setFeedback({ battery: { value: 50 }, icon2: { value: "imgs/actions/custom-layout/battery.svg" }, icon: { value: "imgs/actions/custom-layout/controller.svg" }, percent: { value: "50%" }, }); ``` ### Response This method does not return a value. ``` -------------------------------- ### Detecting Short Press vs Long Press Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Implement logic to differentiate between a quick tap and a long press on a Stream Deck key by using timers within `onKeyDown` and `onKeyUp` events. ```APIDOC ## `onKeyDown` / `onKeyUp` — Short Press vs Long Press Detection Stream Deck fires `onKeyDown` on press and `onKeyUp` on release. To distinguish a short tap from a long press, start a timer in `onKeyDown` and cancel it in `onKeyUp`; if the timer fires first, it was a long press. ```typescript import { action, SingletonAction, KeyDownEvent, KeyUpEvent } from "@elgato/streamdeck"; @action({ UUID: "com.example.press-demo" }) export class PressDemo extends SingletonAction { private timers = new Map> (); override onKeyDown(ev: KeyDownEvent): void { const id = ev.action.id; this.timers.set(id, setTimeout(() => { this.timers.delete(id); ev.action.setTitle("LONG"); // long press (>1s) }, 1000)); } override onKeyUp(ev: KeyUpEvent): void { const timer = this.timers.get(ev.action.id); if (timer) { clearTimeout(timer); this.timers.delete(ev.action.id); ev.action.setTitle("TAP"); // short press } } } ``` ``` -------------------------------- ### streamDeck.ui.sendToPropertyInspector() / onSendToPlugin Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Enables messaging between the Property Inspector and the plugin. The Property Inspector sends messages to the plugin via `sendToPlugin`, and the plugin responds using `streamDeck.ui.sendToPropertyInspector()`. ```APIDOC ## `onSendToPlugin` / `streamDeck.ui.sendToPropertyInspector()` — Plugin ↔ Property Inspector Messaging When the Property Inspector needs dynamic data (e.g., to populate a dropdown), it sends a message to the plugin via `sendToPlugin`. The plugin responds using `streamDeck.ui.sendToPropertyInspector()` with a structured payload that `sdpi-components` data sources consume automatically. ```typescript // src/actions/open-product-page.ts import streamDeck, { action, SingletonAction, SendToPluginEvent } from "@elgato/streamdeck"; import type { JsonValue } from "@elgato/utils"; type Settings = { product?: string }; type DataSourcePayload = { event: string; items: { value: string; label: string }[] }; @action({ UUID: "com.elgato.product-viewer.open-product-page" }) export class OpenProductPage extends SingletonAction { override onSendToPlugin(ev: SendToPluginEvent): void { // sdpi-components sends { event: "getProducts" } when the select mounts if (ev.payload instanceof Object && "event" in ev.payload && ev.payload.event === "getProducts") { streamDeck.ui.sendToPropertyInspector({ event: "getProducts", items: [ { value: "https://www.elgato.com/uk/en/p/stream-deck-plus", label: "Stream Deck +" }, { value: "https://www.elgato.com/uk/en/p/stream-deck-mini", label: "Stream Deck Mini" }, { value: "https://www.elgato.com/uk/en/p/stream-deck-xl", label: "Stream Deck XL" }, ], } satisfies DataSourcePayload); } } } ``` ```html ``` ``` -------------------------------- ### Handle Data Source Requests in Plugin Source: https://github.com/elgatosf/streamdeck-plugin-samples/blob/main/data-sources/README.md Override the onSendToPlugin method in your Stream Deck action to listen for data source requests from the property inspector. Respond with the requested data using a specific payload structure. ```typescript /** * Listen for messages from the property inspector. * @param ev Event information. */ override onSendToPlugin(ev: SendToPluginEvent): Promise | void { // Check if the payload is requesting a data source, i.e. the structure is { event: string } if (ev.payload instanceof Object && "event" in ev.payload && ev.payload.event === "getProducts") { // Send the product ranges to the property inspector. streamDeck.ui.current?.sendToPropertyInspector({ event: "getProducts", items: this.#getStreamDeckProducts(), } satisfies DataSourcePayload); } } ``` -------------------------------- ### SingletonAction Base Class for Actions Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt The base class for action implementations, providing type safety for settings and access to all visible action instances. ```typescript import { action, SingletonAction, WillAppearEvent, KeyUpEvent } from "@elgato/streamdeck"; type MySettings = { count: number }; @action({ UUID: "com.example.my-plugin.my-action" }) export class MyAction extends SingletonAction { // Called when any instance of this action becomes visible override onWillAppear(ev: WillAppearEvent): void { const { count = 0 } = ev.payload.settings; ev.action.setTitle(`${count}`); } // Iterate all visible instances (e.g., for a global reset) resetAll() { for (const action of this.actions) { action.setSettings({ count: 0 }); action.setTitle("0"); } } } ``` -------------------------------- ### Implement Spatial Logic with Action Coordinates Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Use `action.coordinates` to access the grid position of a key. This is useful for implementing spatial logic, such as finding keys adjacent to a pressed key. Note that coordinates are unavailable in multi-actions. ```typescript import { action, Coordinates, KeyAction, KeyDownEvent, SingletonAction } from "@elgato/streamdeck"; @action({ UUID: "com.elgato.lightsout.gamepiece" }) export class GamePiece extends SingletonAction<{ light: 0 | 1 }> { private states = new Map(); // Yield only the actions that neighbour a given coordinate (cross pattern) *adjacentItems(deviceId: string, coords: Coordinates): IterableIterator> { const offsets = [[-1,0],[0,0],[1,0],[0,-1],[0,1]]; for (const a of this.actions) { if (a.device.id !== deviceId || !a.isKey() || !a.coordinates) continue; for (const [dc, dr] of offsets) { if (a.coordinates.column === coords.column + dc && a.coordinates.row === coords.row + dr) { yield a as KeyAction<{ light: 0 | 1 }>; } } } } override async onKeyDown(ev: KeyDownEvent<{ light: 0 | 1 }>): Promise { if (ev.payload.isInMultiAction) return; // coordinates unavailable in multi-actions // Toggle the pressed key and its 4 cardinal neighbours for (const a of this.adjacentItems(ev.action.device.id, ev.payload.coordinates)) { const newState = this.states.get(a.id) ? 0 : 1; this.states.set(a.id, newState); a.setState(newState); } } } ``` -------------------------------- ### Rollup Configuration for Native Modules Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Configure `rollup.config.mjs` to exclude native modules like `sharp` from the bundle, allowing them to be resolved at runtime. Use `commonjs` plugin to handle dynamic `require()` calls. ```javascript // rollup.config.mjs (key settings for native modules) export default { external: ["sharp"], // do NOT bundle; resolved at runtime from sdPlugin/node_modules plugins: [ commonjs({ ignoreDynamicRequires: true }), // allow sharp's dynamic require() calls nodeResolve(), typescript(), ], }; ``` -------------------------------- ### streamDeck.i18n.translate() Source: https://context7.com/elgatosf/streamdeck-plugin-samples/llms.txt Facilitates internationalization by looking up localization keys in the plugin's locale JSON files. It supports specifying a language or defaults to the user's system language. ```APIDOC ## `streamDeck.i18n.translate()` — Internationalization (i18n) `streamDeck.i18n.translate(key, language?)` looks up a localization key in the plugin's locale JSON files. Locale files are placed at the root of the `.sdPlugin` folder (e.g., `en.json`, `de.json`). If `language` is omitted, the user's system language is used. ```typescript // src/actions/newString.ts import streamdeck, { action, SingletonAction, WillAppearEvent, KeyDownEvent, Language } from "@elgato/streamdeck"; type NewStringSettings = { language: Language | undefined; titleKey: string | undefined }; @action({ UUID: "com.elgato.hello-world.newstring" }) export class NewString extends SingletonAction { override async onWillAppear(ev: WillAppearEvent): Promise { if (!ev.action.isKey()) return; let settings = ev.payload.settings; if (!settings.titleKey) { settings = { ...settings, titleKey: "clickme" }; ev.action.setSettings(settings); } await this.displayTitle(settings, ev.action); } override async onKeyDown(ev: KeyDownEvent): Promise { const keys = ["flash", "selectprop", "on", "off", "toggle", "action", "dynamic", "clickme"]; const titleKey = keys[Math.floor(Math.random() * keys.length)]; const settings = { ...ev.payload.settings, titleKey }; ev.action.setSettings(settings); await this.displayTitle(settings, ev.action); } private async displayTitle(settings: NewStringSettings, action: any) { // translate("clickme", "de") → "Klick mich" (from de.json) // translate("thisLanguage", "de") → "Deutsch" const title = streamdeck.i18n.translate(settings.titleKey!, settings.language); const lang = streamdeck.i18n.translate("thisLanguage", settings.language); await action.setTitle(`${lang}\n\n${title}`); } } ``` ```json // com.elgato.hello-world.sdPlugin/en.json { "Localization": { "flash": "Flash", "clickme": "Click Me", "thisLanguage": "English" } } ``` ```