{"id":32151617,"url":"https://github.com/emorydunn/streamdeckplugin","last_synced_at":"2026-02-21T15:02:24.568Z","repository":{"id":46046207,"uuid":"389469538","full_name":"emorydunn/StreamDeckPlugin","owner":"emorydunn","description":"A library for creating Stream Deck plugins in Swift. ","archived":false,"fork":false,"pushed_at":"2026-01-16T21:36:46.000Z","size":726,"stargazers_count":77,"open_issues_count":10,"forks_count":10,"subscribers_count":7,"default_branch":"main","last_synced_at":"2026-01-19T15:25:27.159Z","etag":null,"topics":["elgato","elgato-stream-deck","macos","streamdeck","streamdeck-sdk","streamdecksdk","swift"],"latest_commit_sha":null,"homepage":"https://emorydunn.github.io/StreamDeckPlugin/","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/emorydunn.png","metadata":{"files":{"readme":"README.md","changelog":"Changelog.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["emorydunn"],"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":null}},"created_at":"2021-07-26T00:42:38.000Z","updated_at":"2026-01-16T21:37:13.000Z","dependencies_parsed_at":"2024-04-11T17:57:47.815Z","dependency_job_id":"94eaa949-a002-45c8-bf83-752d6b483622","html_url":"https://github.com/emorydunn/StreamDeckPlugin","commit_stats":{"total_commits":170,"total_committers":3,"mean_commits":"56.666666666666664","dds":0.02352941176470591,"last_synced_commit":"90f940adafe6fdc0db09ee8d6836ba8d7342eea8"},"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/emorydunn/StreamDeckPlugin","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emorydunn%2FStreamDeckPlugin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emorydunn%2FStreamDeckPlugin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emorydunn%2FStreamDeckPlugin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emorydunn%2FStreamDeckPlugin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/emorydunn","download_url":"https://codeload.github.com/emorydunn/StreamDeckPlugin/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emorydunn%2FStreamDeckPlugin/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29684075,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-21T14:31:22.911Z","status":"ssl_error","status_checked_at":"2026-02-21T14:31:22.570Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["elgato","elgato-stream-deck","macos","streamdeck","streamdeck-sdk","streamdecksdk","swift"],"created_at":"2025-10-21T10:48:17.632Z","updated_at":"2026-02-21T15:02:24.562Z","avatar_url":"https://github.com/emorydunn.png","language":"Swift","readme":"# StreamDeck\n\n![Swift](https://github.com/emorydunn/StreamDeckPlugin/workflows/Swift/badge.svg) ![Documentation badge](https://emorydunn.github.io/StreamDeckPlugin/badge.svg)\n\nA library for creating Stream Deck plugins in Swift.\n\n## Usage\n\nYour plugin should conform to `Plugin`, which handles event routing to your actions and lets you interact with the Stream Deck application.\n\n```swift\n@main\nclass CounterPlugin: Plugin {\n\n    static var name: String = \"Counter\"\n\n    static var description: String = \"Count things. On your Stream Deck!\"\n\n    static var author: String = \"Emory Dunn\"\n\n    static var icon: String = \"Icons/pluginIcon\"\n\n    static var version: String = \"0.4\"\n\n    @ActionBuilder\n    static var actions: [any Action.Type] {\n      IncrementAction.self\n      DecrementAction.self\n      RotaryAction.self\n    }\n\n    static var layouts: [Layout] {\n      Layout(id: \"counter\") {\n        // The title of the layout\n        Text(title: \"Current Count\")\n          .textAlignment(.center)\n          .frame(width: 180, height: 24)\n          .position(x: (200 - 180) / 2, y: 10)\n\n        // A large counter label\n        Text(key: \"count-text\", value: \"0\")\n          .textAlignment(.center)\n          .font(size: 16, weight: 600)\n          .frame(width: 180, height: 24)\n          .position(x: (200 - 180) / 2, y: 30)\n\n        // A bar that shows the current count\n        Bar(key: \"count-bar\", value: 0, range: -50..\u003c50)\n          .frame(width: 180, height: 20)\n          .position(x: (200 - 180) / 2, y: 60)\n          .barBackground(.black)\n          .barStyle(.doubleTrapezoid)\n          .barBorder(\"#943E93\")\n      }\n    }\n\n    required init() { }\n\n}\n```\n\nBy using the `@main` attribute your plugin will be automatically initialized.\n\n## Declaring a Plugin\n\nA plugin both defines the code used to interact with the Stream Deck and the manifest for the plugin. When declaring your plugin there are a number of static properties which are defined to tell the Stream Deck application about what your plugin is and what it can do. Not all properties are required, for instance your plugin doesn't need to add a custom category. Optional properties have default overloads to help reduce boilerplate.\n\nMany of the properties are shown to users, such as the name and description. Others are used internally by the Stream Deck application. The most important property is `actions` which is where you define the actions your plugin provides.\n\n```swift\n\n// Define actions with a builder\n@ActionBuilder\nstatic var actions: [any Action.Type] {\n  IncrementAction.self\n  DecrementAction.self\n  RotaryAction.self\n}\n\n// Or define actions in an array\nstatic var actions: [any Action.Type] = [\n    IncrementAction.self,\n    DecrementAction.self\n]\n```\n\nActions are provided as a type because the plugin will initialize a new instance per visible key.\n\n### The Environment and Global Settings\n\nThere are two ways to share a global state amongst actions:\n\n1. The Environment\n2. Global Settings\n\nIn use they're very similar, only differing by a couple of protocols. The important difference is that environmental values aren't persisted whereas global settings are stored and will be consistent across launches of the Stream Deck app.\n\nThere are two ways to declare environmental values and global settings.\n\nStart with a struct that conforms to `EnvironmentKey` or `GlobalSettingKey`. This defines the default value and how the value will be accessed:\n\n```swift\nstruct Count: EnvironmentKey {\n    static let defaultValue: Int = 0\n}\n```\n\nNext add an extension to either `EnvironmentValues` or `GlobalSettings`:\n\n```swift\nextension EnvironmentValues {\n    var count: Int {\n        get { self[Count.self] }\n        set { self[Count.self] = newValue }\n    }\n}\n```\n\nTo use the value in your actions use the corresponding property wrapper:\n\n```swift\n@Environment(\\.count) var count // For an environment key\n@GlobalSetting(\\.count) var count // For a global settings key\n```\n\nThe value can be read and updated from inside an action callback.\n\n#### Macros\n\nAdditionally, instead of manually declaring the keys, you can use the `@Entry` macro. The macro handles generating both the struct and variable for the key path. By default the name is auto-generated based on the property name, but a custom key can be provided.\n\n```swift\nextension EnvironmentValues {\n    @Entry var count = 42\n}\n\nextension GlobalSettings {\n    @Entry(\"CountDracula\") var theCount = 42\n}\n```\n\n## Creating Actions\n\nEach action in your plugin is defined as a separate struct conforming to `Action`. There are several helper protocols available for specific action types.\n\n| Protocol             | Description                                  |\n| -------------------- | -------------------------------------------- |\n| `KeyAction`          | A key action which has multiple states       |\n| `StatelessKeyAction` | A key action which has a single state        |\n| `EncoderAction`      | A rotary encoder action on the Stream Deck + |\n\nUsing one of the above protocols simply provides default values on top of `Action`, and you can provide your own values as needed. For instance `KeyAction` sets the `controllers` property to `[.keypad]` by default, and `EncoderAction` sets it to `[.encoder]`. To create an action that provides both key and encoder actions set `controllers` to `[.keypad, .encoder]` no matter which convenience protocol you're using.\n\nFor all action there are several common static properties which need to be defined.\n\n```swift\nstruct IncrementAction: KeyAction {\n\n    typealias Settings = NoSettings\n\n    static var name: String = \"Increment\"\n\n    static var uuid: String = \"counter.increment\"\n\n    static var icon: String = \"Icons/actionIcon\"\n\n    static var states: [PluginActionState]? = [\n        PluginActionState(image: \"Icons/actionDefaultImage\", titleAlignment: .middle)\n    ]\n\n    var context: String\n\n    var coordinates: StreamDeck.Coordinates?\n\n    @GlobalSetting(\\.count) var count\n\n    required init(context: String, coordinates: Coordinates?) {\n        self.context = context\n        self.coordinates = coordinates\n    }\n}\n```\n\n### Action Settings\n\nIf your action uses a [property inspector][pi] for configuration you can use a `Codable` struct as the `Settings`. The current settings will be sent in the payload in events.\n\n```swift\nstruct ChooseAction: KeyAction {\n    enum Settings: String, Codable {\n        case optionOne\n        case optionTwo\n        case optionThree\n    }\n}\n```\n\n#### Working with SPDI Components\n\nIf you define your Property Inspectors using [SPDI Components][SPDI] your plugin can both send dynamic properties natively.\n\nFor example if you have an `spdi-select`:\n\n```HTML\n\u003csdpi-item label=\"Recipe\"\u003e\n  \u003csdpi-select\n    id=\"recipeSelect\"\n    setting=\"processRecipe\"\n    placeholder=\"Choose a process recipe\"\n    datasource=\"getRecipes\"\n    loading=\"Loading process recipes...\"\n  \u003e\n  \u003c/sdpi-select\u003e\n\u003c/sdpi-item\u003e\n\n```\n\nYour plugin can easily provide values for it using `DataSourcePayload`. The `event` sent by the plugin matches the `datasource` in the component and the initializer provides a convenient map function.\n\n```swift\nlet recipeNames = listProcessRecipes() // Returns an array of strings\n\nlet response = DataSourcePayload(event: \"getRecipes\", items: recipeNames) { recipeName in\n  DatasourceItem(label: recipeName)\n}\n\nsendToPropertyInspector(response)\n```\n\n### Events\n\nYour action can both [receive events][er] from the app and [send events][es] to the app. Most of the events will be from user interaction, key presses and dial rotation, but also from system events such as the action appearing on a Stream Deck or the property inspector appearing.\n\nTo receive events simply implement the corresponding method in your action, for instance to be notified when a key is released use `keyUp`. If your action displays settings to the user, use `willAppear` to update the title to reflect the current value.\n\n```swift\nstruct IncrementAction: KeyAction {\n\n    func willAppear(device: String, payload: AppearEvent\u003cNoSettings\u003e) {\n        setTitle(to: \"\\(count)\", target: nil, state: nil)\n    }\n\n    func didReceiveGlobalSettings() {\n        log.log(\"Global settings changed, updating title with \\(self.count)\")\n        setTitle(to: \"\\(count)\", target: nil, state: nil)\n    }\n\n    func keyUp(device: String, payload: KeyEvent\u003cSettings\u003e) {\n        count += 1\n    }\n\n    func longKeyPress(device: String, payload: KeyEvent\u003cNoSettings\u003e) {\n      count = 0\n      showOk()\n      log.log(\"Resetting count to \\(self.count)\")\n    }\n}\n```\n\nIn the above example, `setTitle` is an event that an action can send. In this case it sets the title of the action. It's called in two places: when the action appears to set the initial title and when the global settings are changed so it can keep the visible counter in sync.\n\n## Stream Deck Plus Layouts\n\nDesigning [custom layouts][plus_layout] for the Stream Deck Plus is accomplished with using a result builder. Each `Layout` is built from components, such as `Text`, `Image`, etc. The layout is defined in the plugin manifest. For instance, to build a custom bar layout from the example `counter` plugin:\n\n```swift\n\nextension LayoutName {\n  static let counter: LayoutName = \"counter\"\n}\n\nLayout(id: .counter) {\n  // The title of the layout\n  Text(title: \"Current Count\")\n    .textAlignment(.center)\n    .frame(width: 180, height: 24)\n    .position(x: (200 - 180) / 2, y: 10)\n\n  // A large counter label\n  Text(key: \"countText\", value: \"0\")\n    .textAlignment(.center)\n    .font(size: 16, weight: 600)\n    .frame(width: 180, height: 24)\n    .position(x: (200 - 180) / 2, y: 30)\n\n  // A bar that shows the current count\n  Bar(key: \"countBar\", value: 0, range: -50...50)\n    .frame(width: 180, height: 20)\n    .position(x: (200 - 180) / 2, y: 60)\n    .barBackground(.black)\n    .barStyle(.doubleTrapezoid)\n    .barBorder(\"#943E93\")\n}\n\nstruct CounterSettings: LayoutSettings {\n  var countText: TextLayoutSettings\n  var countBar: BarLayoutSettings\n\n  init(count: Int, bgColor: Color) {\n    self.countText = TextLayoutSettings(value: count.formatted())\n    self.countBar = BarLayoutSettings(value: Double(count), bar_fill_c: bgColor)\n  }\n}\n```\n\nThe layout is saved into the Layouts folder in the same directory as the manifest. In order to use the layout on a rotary action set the layout property of the encoder to the folder and id of the layout, e.g. `Layouts/counter.json`.\n\nThe layout can be updated with a struct which conforms to `LayoutSettings`, like `CounterSettings` above.\n\n```swift\nlet feedback = CounterSettings(count: count, bgColor: .red)\n\nsetFeedback(feedback)\n```\n\nAny editable property can be updated this way. Please refer to the [documentation](https://docs.elgato.com/sdk/plugins/layouts-sd+#items) for more details.\n\n## Exporting Your Plugin\n\nYour plugin executable ships with an automatic way to generate the plugin's `manifest.json` file in a type-safe manor. Use the provided `export` command on your plugin binary to export the manifest and copy the binary itself. You will still need to use Elgato's `DistributionTool` for final packaging.\n\n```bash\nOVERVIEW: Conveniently export the plugin.\n\nAutomatically generate the manifest and copy the executable to the Plugins folder.\n\nUSAGE: plugin-command export \u003curi\u003e [--output \u003coutput\u003e] [--generate-manifest] [--preview-manifest] [--manifest-name \u003cmanifest-name\u003e] [--copy-executable] [--executable-name \u003cexecutable-name\u003e]\n\nARGUMENTS:\n  \u003curi\u003e                   The URI for your plugin\n\nOPTIONS:\n  -o, --output \u003coutput\u003e   The folder in which to create the plugin's directory. (default: ~/Library/Application Support/com.elgato.StreamDeck/Plugins)\n  --generate-manifest/--preview-manifest\n                          Encode the manifest for the plugin and either save or preview it.\n  -m, --manifest-name \u003cmanifest-name\u003e\n                          The name of the manifest file. (default: manifest.json)\n  -c, --copy-executable   Copy the executable file.\n  -e, --executable-name \u003cexecutable-name\u003e\n                          The name of the executable file.\n  --version               Show the version.\n  -h, --help              Show help information.\n```\n\nIf you're building a universal binary there appears to be an issue with the macro which prevents compiling for multiple architectures at once. A workaround is to compile each separately and then combine with with `lipo`.\n\n```shell\n# Build each architecture separately and then combine\necho \"Building ARM binary\"\nswift build -c release --arch arm64\n\necho \"Building Intel binary\"\nswift build -c release --arch x86_64\n\necho \"Creating universal binary\"\nlipo -create \\\n  .build/arm64-apple-macosx/counter-plugin \\\n  .build/x86_64-apple-macosx/counter-plugin \\\n  -output counter-plugin\n```\n\n## Adding `StreamDeck` as a Dependency\n\nTo use the `StreamDeck` library in a SwiftPM project,\nadd the following line to the dependencies in your `Package.swift` file:\n\n```swift\n.package(url: \"https://github.com/emorydunn/StreamDeckPlugin.git\", from: \"0.6.0\"),\n```\n\nFinally, include `\"StreamDeck\"` as a dependency for your executable target:\n\n```swift\nlet package = Package(\n    // name, products, etc.\n    platforms: [.macOS(.v11)],\n    dependencies: [\n        .package(url: \"https://github.com/emorydunn/StreamDeckPlugin.git\", from: \"0.6.0\"),\n        // other dependencies\n    ],\n    targets: [\n        .executableTarget(name: \"\u003ccommand-line-tool\u003e\", dependencies: [\n            .product(name: \"StreamDeck\", package: \"StreamDeckPlugin\")\n        ]),\n        // other targets\n    ]\n)\n```\n\n[pi]: https://docs.elgato.com/sdk/plugins/property-inspector\n[er]: https://docs.elgato.com/sdk/plugins/events-received\n[es]: https://docs.elgato.com/sdk/plugins/events-sent\n[plus_layout]: https://docs.elgato.com/sdk/plugins/layouts-sd+\n[spdi]: https://sdpi-components.dev\n","funding_links":["https://github.com/sponsors/emorydunn"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Femorydunn%2Fstreamdeckplugin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Femorydunn%2Fstreamdeckplugin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Femorydunn%2Fstreamdeckplugin/lists"}