Coder Social home page Coder Social logo

emorydunn / streamdeckplugin Goto Github PK

View Code? Open in Web Editor NEW
50.0 5.0 7.0 650 KB

A library for creating Stream Deck plugins in Swift.

Home Page: https://emorydunn.github.io/StreamDeckPlugin/

License: MIT License

Swift 100.00%
elgato streamdeck elgato-stream-deck streamdecksdk streamdeck-sdk macos swift

streamdeckplugin's Introduction

StreamDeck

Swift Documentation badge

A library for creating Stream Deck plugins in Swift.

Usage

Your plugin should conform to PluginDelegate, which handles event routing to you actions and lets you interact with the Stream Deck application.

@main
class CounterPlugin: Plugin {

    static var name: String = "Counter"

    static var description: String = "Count things. On your Stream Deck!"

    static var author: String = "Emory Dunn"

    static var icon: String = "Icons/pluginIcon"

    static var version: String = "0.4"

    @ActionBuilder
    static var actions: [any Action.Type] {
      IncrementAction.self
      DecrementAction.self
      RotaryAction.self
    }

    required init() { }

}

By using the @main attribute your plugin will be automatically initialized.

Declaring a Plugin

A 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.

Many 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.

// Define actions with a builder
@ActionBuilder
static var actions: [any Action.Type] {
  IncrementAction.self
  DecrementAction.self
  RotaryAction.self
}

// Or define actions in an array
static var actions: [any Action.Type] = [
    IncrementAction.self,
    DecrementAction.self
]

Actions are provided as a type because the plugin will initialize a new instance per visible key.

The Environment and Global Settings

There are two ways to share a global state amongst actions:

  1. The Environment
  2. Global Settings

In 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.

There are two ways to declare environmental values and global settings.

Start with a struct that conforms to EnvironmentKey or GlobalSettingKey. This defines the default value and how the value will be accessed:

struct Count: EnvironmentKey {
    static let defaultValue: Int = 0
}

Next add an extension to either EnvironmentValues or GlobalSettings:

extension EnvironmentValues {
    var count: Int {
        get { self[Count.self] }
        set { self[Count.self] = newValue }
    }
}

To use the value in your actions use the corresponding property wrapper:

@Environment(\.count) var count // For an environment key
@GlobalSetting(\.count) var count // For a global settings key

The value can be read and updated from inside an action callback.

Macros

Starting in Swift 5.9 two new macros will be available to make declaring environmental values and global settings easier. The macro handles generating both the struct and variable for the key path. The key specified in the macro is used as the key of the setting.

extension EnvironmentValues {
    #environmentKey("count", defaultValue: 0, ofType: Int.self)
}

extension GlobalSettings {
    #globalSetting("count", defaultValue: 0, ofType: Int.self)
}

Creating Actions

Each action in your plugin is defined as a separate struct conforming to Action. There are several helper protocols available for specific action types.

Protocol Description
KeyAction A key action which has multiple states
StatelessKeyAction A key action which has a single state
EncoderAction A rotary encoder action on the Stream Deck +

Using 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.

For all action there are several common static properties which need to be defined.

struct IncrementAction: KeyAction {

    typealias Settings = NoSettings

    static var name: String = "Increment"

    static var uuid: String = "counter.increment"

    static var icon: String = "Icons/actionIcon"

    static var states: [PluginActionState]? = [
        PluginActionState(image: "Icons/actionDefaultImage", titleAlignment: .middle)
    ]

    var context: String

    var coordinates: StreamDeck.Coordinates?

    @GlobalSetting(\.count) var count

    required init(context: String, coordinates: StreamDeck.Coordinates?) {
        self.context = context
        self.coordinates = coordinates
    }
}

Action Settings

If your action uses a property inspector for configuration you can use a Codable struct as the Settings. The current settings will be sent in the payload in events.

struct ChooseAction: KeyAction {
    enum Settings: String, Codable {
        case optionOne
        case optionTwo
        case optionThree
    }
}

Events

Your action can both receive events from the app and send events 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.

To 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.

struct IncrementAction: KeyAction {

    func willAppear(device: String, payload: AppearEvent<NoSettings>) {
        setTitle(to: "\(count)", target: nil, state: nil)
    }

    func didReceiveGlobalSettings() {
        log.log("Global settings changed, updating title with \(self.count)")
        setTitle(to: "\(count)", target: nil, state: nil)
    }

    func keyUp(device: String, payload: KeyEvent<Settings>) {
        count += 1
    }
}

In 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.

Stream Deck Plus Layouts

Designing custom layouts 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:

Layout(id: "counter") {
  // The title of the layout
  Text(title: "Current Count")
    .textAlignment(.center)
    .frame(width: 180, height: 24)
    .position(x: (200 - 180) / 2, y: 10)

  // A large counter label
  Text(key: "count-text", value: "0")
    .textAlignment(.center)
    .font(size: 16, weight: 600)
    .frame(width: 180, height: 24)
    .position(x: (200 - 180) / 2, y: 30)

  // A bar that shows the current count
  Bar(key: "count-bar", value: 0, range: -50..<50)
    .frame(width: 180, height: 20)
    .position(x: (200 - 180) / 2, y: 60)
    .barBackground(.black)
    .barStyle(.doubleTrapezoid)
    .barBorder("#943E93")
}

The 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.

At the moment updating the values in the layout still requires manually specifying keys from the components. In our example above the counter and bar can be updated like so:

setFeedback([
 "count-text": count.formatted(),
 "count-bar" : ["value": count],
])

Any editable property can be updated this way. Please refer to the documentation for more details.

Exporting Your Plugin

Your 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.

OVERVIEW: Conveniently export the plugin.

Automatically generate the manifest and copy the executable to the Plugins folder.

USAGE: plugin-command export <uri> [--output <output>] [--generate-manifest] [--preview-manifest] [--manifest-name <manifest-name>] [--copy-executable] [--executable-name <executable-name>]

ARGUMENTS:
  <uri>                   The URI for your plugin

OPTIONS:
  -o, --output <output>   The folder in which to create the plugin's directory. (default: ~/Library/Application Support/com.elgato.StreamDeck/Plugins)
  --generate-manifest/--preview-manifest
                          Encode the manifest for the plugin and either save or preview it.
  -m, --manifest-name <manifest-name>
                          The name of the manifest file. (default: manifest.json)
  -c, --copy-executable   Copy the executable file.
  -e, --executable-name <executable-name>
                          The name of the executable file.
  --version               Show the version.
  -h, --help              Show help information.

Adding StreamDeck as a Dependency

To use the StreamDeck library in a SwiftPM project, add the following line to the dependencies in your Package.swift file:

.package(name: "StreamDeck", url: "https://github.com/emorydunn/StreamDeckPlugin.git", .branch("main"))

Finally, include "StreamDeck" as a dependency for your executable target:

let package = Package(
    // name, products, etc.
    platforms: [.macOS(.v11)],
    dependencies: [
        .package(name: "StreamDeck", url: "https://github.com/emorydunn/StreamDeckPlugin.git", .branch("main")),
        // other dependencies
    ],
    targets: [
        .executableTarget(name: "<command-line-tool>", dependencies: [
            "StreamDeck"
        ]),
        // other targets
    ]
)

streamdeckplugin's People

Contributors

emorydunn avatar sentinelite avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

streamdeckplugin's Issues

Unable to get counter example working

Hey! I'll start off by saying that this is my first real foray into swift, so there is a non zero chance that I'm doing something wrong and that all of these issue are self inflicted.

I'm trying to compile and run the example counter plugin, but I'm running into issues.

The first issue I ran into was that i was unable to build the plugin due to some of the actions not conforming to the Action protocol. Here is a PR that has the changes I needed to make for the plugin to compile.

The second issue is after compiling the plugin and exporting it with the command ./.build/debug/counter-plugin export --copy-executable --generate-manifest dev.counter I don't see the plugin appear in the Stream Deck application after restarting it. I've checked the plugins folder and see the manifest and the executable in the spots you would expect them to be. Looking at the Stream Deck logs, the only reference I ever see to the counter plugin is this message that appears when I quit the Stream Deck application.

14:02:12.7808          void ESDCustomPlugin::QuitPlugin(): Plugin 'Counter' is still alive after closing the websocket. Quit it.

Other info that would probably be helpful to know:

  • Machine: 2021 M1 MacBook Pro running Ventura 13.3.1
  • Stream Deck Version: Stream Deck MK. 2
  • Stream Deck Software Version: 6.1.0
  • Swift Version: swift-driver version: 1.75.2 Apple Swift version 5.8 (swiftlang-5.8.0.124.2 clang-1403.0.22.11.100) Target: arm64-apple-macosx13.0

automate plugin packaging

i just found your repository yesterday and had a quick qo on a plugin for my home automation project. i really like writing a stremdeck plugin in swift, but after the first trials i got very annoyed with the manual packaging of the resulting plugin.

i first thought of using a package plugin for a post build step, but this is currently not possible. only pre build and build plugins currently.

then i just had a very quick and dirty go at a post-build step for the build scheme. unfortunately this seems to be also quite limited for packages, but with some brute force i got it to work.

maybe this is also useful for others...

prerequisites:

  • a Tools directory on the same level as the plugin's Sources with the DistributionTool binary and the following createPlugin.sh script which should be executable
  • a Resources directory, also on the same level as Sources, with everything else required for the finished plugin like Icons/..., previews/... and language json files

then add the script to a post-build phases of the relevant build schemes.

all this can be under source control

the script will create a plugin directory, assemble all the parts for the finished plugin and then call DistributionTool to create the packaged plugin there. and opens a finder window. you can just double click the .streamDeckPlugin file as usual to install the plugin into the streamdeck software.
a log file for the process is created in /tmp/

you probably want to exclude pluginfrom source control. or change the script to create it somewhere else.
the $URI should also adjusted to match the api specs and your prefix.

createPlugin.sh:

#/bin/sh

SELF=`basename $0 .sh`
exec > /tmp/${SELF}.log 2>&1

echo `date`

PROJECT_DIR=${WORKSPACE_PATH}/../../..
cd ${PROJECT_DIR}

RESULT_DIR=./plugin

echo "using `pwd` as project dir"
echo "using ${RESULT_DIR} as result dir"

rm -fr ${RESULT_DIR}
mkdir ${RESULT_DIR}

# as of the api doc this sould rather be something like com.….<plugin-name>
URI=${PRODUCT_NAME}

# create manifest & combine with binary 
${TARGET_BUILD_DIR}/${PRODUCT_NAME} export ${URI} --generate-manifest --copy-executable --output ${RESULT_DIR}

# copy resources like Icons, previews & translations to components dir
COMPONENTS_DIR=${RESULT_DIR}/${URI}.sdPlugin
cp -r Resources/* ${COMPONENTS_DIR}

# pack everything into the plugin
Tools/DistributionTool -b -i ${COMPONENTS_DIR} -o ${RESULT_DIR}

open ${RESULT_DIR}

Setting an Image in the Bundle Doesn't Pass State

The method, Action.setImage(toImage:withExtension:subdirectory:target:state:) has parameters for target and state but these aren't used when calling the the final setImage(to:target:state:).

Passing in nil for the image also results in unexpected behavior, with url(forResource:withExtension:subdirectory:) returning the first image found rather than resetting the icon like other methods.

Finish Implementing Sent Events

Check that all events the plugin can send are implemented and work correctly.

  • setSettings
  • getSettings
  • setGlobalSettings
  • getGlobalSettings
  • openUrl
  • logMessage
  • setTitle
  • setImage
  • showAlert
  • showOk
  • setState
  • switchToProfile
  • sendToPropertyInspector
  • sendToPlugin

Support Rotary Encoders on the StreamDeck+

The StreamDeck+ remains undocumented, but Elgato updated their sample plugins, so some of the manifest can be updated.

Documentation is up and this is the full list of changes:

  • Add UserTitleEnabled property to the manifest.
  • Add the device type kESDSDKDeviceType_StreamDeckPlus to detect Stream Deck + devices.
  • Add Encoder to the manifest for Stream Deck + devices.
  • Add TriggerDescription to the manifest for Stream Deck + devices.
  • Add Layouts for Stream Deck + displays.
  • Add setFeedback event for Stream Deck + displays.
  • Add setFeedbackLayout event for Stream Deck + displays.
  • Add touchTap event for Stream Deck + displays.
  • Add dialPress event for Stream Deck + encoders.
  • Add dialRotate event for Stream Deck + encoders.
  • Update willAppear and willDisappear events to include the controller property.

Look Into SwiftLog

The plugin makes heavy use of OSLog for internal logging, which is important, especially for any plugin communication messages. Logging through the StreamDeck app is useful for actual plugins, but the library can't rely on the WebSocket being open. While this is fine for development it's hard for collect logs from users if needed.

Using (SwiftLog)[https://github.com/apple/swift-log] as the logging system could allow for easier user logging. There are a couple of options here.

One would simply be to replace OSLog with SwiftLog without specifying a backend. This would leave any log collection up to the plugin developer, allowing for whatever logging backing they'd like to use.
The other option would be to implement a basic file logger (or use an existing library) to provide a default log.

capitalisation for keys in global settings (and possibly in other locations)

i have just noticed that the stringified json that is send for setGlobalSettings has the names of the keys changed from mixed case (like 'apiKey') to first letter capitalised (like 'Apikey') this is not noticeable in the plugin itself as this is reverted in the didReceiveGlobalSettings event so everything including your tests look normal.

but it is a problem in using the property inspector as the keys seen there are not what was send. also settings saved from the property inspector will not match what is expected on the plugin side.

i have verified that the problem is indeed on the plugin side as even the message in the debugger will show the wrong case: Setting value for Apikey to Optional("1"). also the json string received on the pi java script side has the wrong case before even decoding the string.

i think the problem lies here in GlobalSettingKey.swift:

extension GlobalSettingKey {
	public static var name: String {
		String(describing: self)
	}
}

Websocket crashes when sending data from the Property Inspector.

Hey, Emory!

Whenever I open the property inspector from within the StreamDeck interface, the backend plugin crashes, & automatically restarts. After the second opening of the PI, on the same key context, or even two separate contexts, the plugin will crash for ~2 minutes.

I've monitored the console.app's logs for my app streamDeckWSTEST, & I can see the web socket keeps restarting & seeing 'Starting macOS plugin'. As a side note: only sometimes do I see Corpse allowed x of 5.

I'm not really sure if it's something I'm doing or not. It seems to happen with even a minimal project sending to the PI. I've attached some files & crash logs. I'll also attach my current plugin repo, along with the .sdPlugin file, so you can dig a little deeper if need be.

Again, thank you for this VERY helpful plugin!

.ips log

Finish Implementing Received Events

Not all received events been handled.

  • didReceiveSettings
  • didReceiveGlobalSettings
  • keyDown
  • keyUp
  • willAppear
  • willDisappear
  • titleParametersDidChange
  • deviceDidConnect
  • deviceDidDisconnect
  • applicationDidLaunch
  • applicationDidTerminate
  • systemDidWakeUp
  • propertyInspectorDidAppear
  • propertyInspectorDidDisappear
  • sendToPlugin

missing events

would you mind adding some missing events/messages:

  • didReceiveDeepLink (receiving)
  • openURL (sending)

also: i think it would be nice if there was a way to handle unknown events maybe by registering a handler for a specific unknown event or a general handler that will receive everything that is not known.

and probably the same for sending

Decoding Events Fails on Non-String Settings

Settings are hard-coded to decode as [String:String], however if the PI sends other (valid) JSON types back the event will fail to decode. For instance, checkboxes might be sent as {"someKey": true}.

Settings should support any valid JSON. The simple fix is to deliver a Data blob to the Action to decode itself. A nicer solution would be to allow the Action to declare a Settings type that the plugin could then decode before forwarding to an Action.

Manifest isn't generating correctly.

Running packageName generate-manifest --output /Users/userName/manifest.json doesn't generate the correct output. It appears it's generating mostly hard-coded values? Actions also aren't generated, causing the plugin to not function. I've compared the intended output (from the example script & wiki file), with other Stream Deck plugins, & ended up diff-checking & creating my own manifest.json file. I ended up getting the example project working with this fix.

Manifest_Comparison

Explore Options for Incorporating Actions into the Event Stream

Currently actions are defined in the manifest, which is only used for generating JSON. The plugin itself has no knowledge of what actions it supports, simply calling methods for events with the ID sent from the Stream Deck app. For small plugins this is fine and can be easily handled with a switch, however once a plugin has a number of actions or actions that involve more complex logic for handling events this becomes messy with each method needing to switch over every action.

If the ActionInstances were registered with the plugin itself then the event handler could also route events to specific actions automatically, only calling the generic event handler if no matching action was found (or if the action doesn't respond to an event).

Multi Support Not Working?

Hey there!

For a few months now, my plugin hasn't been able to use Multi-Action support. I dumbed this down to an error on my side, but after extreme debugging, I couldn't get it to work.

I went ahead & forked the latest, current branch of the Counter plugin, & can't seem to get any logs or any of the actions to work with-in a Multi-Action.

Could you take a look & let me know if I'm missing something, or if this is indeed a bug?

Thanks!

another capitalisation problem

i think i found a another capitalisation bug.

i could not get the monitoring of start and stop of an application to work. there were never any didLaunch or didTerminate events generated.

after some digging i found that the automatic manifest creation will create

"ApplicationsToMonitor" : {
    "Mac" : [
      ...
    ],
    "Windows" : [
    ]
  },

but as per the api documentation "mac" and "windows" have to be all lower case. manually changing this will result in a plugin that gets the launch and terminate messages.

i have not checked jet, but maybe the reason is similar to issue #25 and i suspect there are more of these.

also maybe relying on the autogenerated codable implementations for custom types is problematic if you have to follow a api specification that seems to use more or less random capitalisation of the keys.

getGlobalSettings is throwing an error.

Hey, Emory!
When trying to access the Plugin's global settings, it's failing when unwrapping the StreamDeckPlugin.shared.uuid.

Error:

StreamDeck/Delegate+Sent.swift:57: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value

@main
class CounterPlugin: PluginDelegate {
...
    required init() {
        getGlobalSettings()
    }
}

The `device` Key is Technically Optional

In doing some tests on a laptop that hasn't had a Stream Deck connected I'm getting a decoding error:

09:34:10.3280 Failed to decode data for event titleParametersDidChange
09:34:10.3282 The data couldn’t be read because it is missing.
09:34:10.3288 keyNotFound(CodingKeys(stringValue: "device", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"device\", intValue: nil) (\"device\").", underlyingError: nil))

Admittedly this is kind of an unusual case.

[NSConcreteTask waitUntilExit] Error

Hey, Emory!

This bug's been haunting me for many, many months. I think it's related to the WebSocket stuff, can you confirm?

I'll add that this was on far older versions of the package, but I'm still seeing them up-to the more recent #21's GlobalSettings branch. I haven't isolated it to the most recent main branch, but I assume it's still there?

Some people say it occurs when the computer wakes up from a sleep cycle, network changes (VPNs/WIFI, etc).

It's extremely hard to reproduce. I've (personally_ only had it a handful of times, since I picked up this project.

I'm also unable to attach to the process via Xcode's debugger tools.

Lastly, is there anyway to get the StreamDeckPlugin's osLogs to filter through Elgato's logger? (logMessage), so end-user debugging is easier?

From a support ticket:

It could be related to changing WiFi networks. Every day I go between office and home. Maybe this is invalidating the client/server socket addresses due to routing/gateway changes and they need to be closed and restarted. Basically if you can get any socket logging enabled this will probably help.

SENTINELITE/StreamDeck-Shortcuts#19

Consider Deprecating Optional Booleans

The plugin manifest makes extensive use of optional booleans, such that not including the value uses the default. This makes sense for hand-edited JSON, but for generated manifest it means almost every action attribute is option, which is a little odd in Swift.

Instead these properties should be marked as non-optional with a default implementation providing the SDK default value. This will also simplify the user's code, as they won't need to have a bunch of unused properties.

thanks ! and one small question

thanks for the three fixes/changes. everything works like a charm. including me routing the deep link event from the plugin to the specific action it has to go to.

just one question at the moment, but also not really important as hardcoding works for now:
for generating the deep link callback url i have to access the uuid/uri of my plugin. i had hoped to use
PluginCommunication.shared.uuid for this, but of course this is the runtime uuid to for the websocket communication and not the id/uri that is used to generate the manifest.

do you have an idea if it is possible to access this information from the manifest generation at runtime in the plugin?
maybe setting it in the plugin/plugin delagate so it has not to be fiven at the cmd line later?

the api and documentation is quite sloppy here also. it mixes uuid and uri and both are not really in a format they should be if the names were to be taken literally...

Milti Action Support

Does this library support Plugins with multiple actions? If so, is there any documentation or an example available?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.