### 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"
}
}
```
```