Compare commits

..

11 Commits
v0.0.1 ... main

7 changed files with 482 additions and 377 deletions

65
README.md Normal file
View File

@ -0,0 +1,65 @@
# Stream Deck plugin library for Go
[![Go Reference](https://pkg.go.dev/badge/github.com/tardisx/streamdeck-plugin.svg)](https://pkg.go.dev/github.com/tardisx/streamdeck-plugin)
You can find fully-formed examples using this library in
[streamdeck-plugin-examples](https://github.com/tardisx/streamdeck-plugin-examples)
## Basic usage
```go
package main
import (
"fmt"
"log/slog"
"time"
"github.com/tardisx/streamdeck-plugin"
"github.com/tardisx/streamdeck-plugin/events"
)
// keep track of instances we've seen
var contexts = map[string]bool{}
func main() {
slog.Info("Starting up")
c := streamdeck.New()
slog.Info("Registering handlers")
c.RegisterHandler(func(e events.ERWillAppear) {
slog.Info(fmt.Sprintf("action %s appeared, context %s", e.Action, e.Context))
contexts[e.Context] = true
})
c.RegisterHandler(func(e events.ERWillDisappear) {
slog.Info(fmt.Sprintf("action %s disappeared, context %s", e.Action, e.Context))
delete(contexts, e.Context)
})
c.RegisterHandler(func(e events.ERKeyDown) {
slog.Info(fmt.Sprintf("action %s appeared, context %s", e.Action, e.Context))
})
slog.Info("Connecting web socket")
err := c.Connect()
if err != nil {
panic(err)
}
// update the title once a second, for all "seen" contexts
go func() {
for {
for context := range contexts {
c.Send(events.NewESSetTitle(
context,
time.Now().Format(time.Kitchen),
events.EventTargetBoth,
0))
}
time.Sleep(time.Second)
}
}()
slog.Info("waiting for the end")
c.WaitForPluginExit()
}
```

340
events/events_received.go Normal file
View File

@ -0,0 +1,340 @@
package events
import (
"encoding/json"
"reflect"
)
// ValidEventType returns a boolean indicating whether or not
// this is a valid event type
func ValidEventType(t reflect.Type) bool {
for i := range receivedEventTypeMap {
if receivedEventTypeMap[i] == t {
return true
}
}
return false
}
// TypeForEvent returns the type for a particular event type string
func TypeForEvent(e string) (reflect.Type, bool) {
t, ok := receivedEventTypeMap[e]
return t, ok
}
var receivedEventTypeMap = map[string]reflect.Type{}
func init() {
receivedEventTypeMap["keyUp"] = reflect.TypeOf(ERKeyUp{})
receivedEventTypeMap["didReceiveSettingsPayload"] = reflect.TypeOf(ERDidReceiveSettingsPayload{})
receivedEventTypeMap["didReceiveSettings"] = reflect.TypeOf(ERDidReceiveSettings{})
receivedEventTypeMap["globalSettings"] = reflect.TypeOf(ERDidReceiveGlobalSettings{})
receivedEventTypeMap["didReceiveDeepLink"] = reflect.TypeOf(ERDidReceiveDeepLink{})
receivedEventTypeMap["touchTap"] = reflect.TypeOf(ERTouchTap{})
receivedEventTypeMap["dialDown"] = reflect.TypeOf(ERDialDown{})
receivedEventTypeMap["dialUp"] = reflect.TypeOf(ERDialUp{})
receivedEventTypeMap["dialRotate"] = reflect.TypeOf(ERDialRotate{})
receivedEventTypeMap["keyDown"] = reflect.TypeOf(ERKeyDown{})
receivedEventTypeMap["willAppear"] = reflect.TypeOf(ERWillAppear{})
receivedEventTypeMap["willDisappear"] = reflect.TypeOf(ERWillDisappear{})
receivedEventTypeMap["titleParametersDidChange"] = reflect.TypeOf(ERTitleParametersDidChange{})
receivedEventTypeMap["deviceDidConnect"] = reflect.TypeOf(ERDeviceDidConnect{})
receivedEventTypeMap["deviceDidDisconnect"] = reflect.TypeOf(ERDeviceDidDisconnect{})
receivedEventTypeMap["applicationDidLaunch"] = reflect.TypeOf(ERApplicationDidLaunch{})
receivedEventTypeMap["applicationDidTerminate"] = reflect.TypeOf(ERApplicationDidTerminate{})
receivedEventTypeMap["applicationSystemDidWakeUp"] = reflect.TypeOf(ERApplicationSystemDidWakeUp{})
receivedEventTypeMap["applicationPropertyInspectorDidAppear"] = reflect.TypeOf(ERApplicationPropertyInspectorDidAppear{})
receivedEventTypeMap["applicationPropertyInspectorDidDisappear"] = reflect.TypeOf(ERApplicationPropertyInspectorDidDisappear{})
receivedEventTypeMap["applicationPropertySendToPlugin"] = reflect.TypeOf(ERApplicationPropertySendToPlugin{})
receivedEventTypeMap["applicationPropertySendToPropertyInspector"] = reflect.TypeOf(ERApplicationPropertySendToPropertyInspector{})
}
type ERBase struct {
Event string `json:"event"`
}
//{Action: Event:deviceDidConnect Context: Device:A1DA463F033AD2616E05636CD16F064F Payload:[]}"}
type ERCommon struct {
Action string `json:"action"` // The action's unique identifier. If your plugin supports multiple actions, you should use this value to see which action was triggered
Event string `json:"event"`
Context string `json:"context"` // A value identifying the instance's action. You will need to pass this opaque value to several APIs like the setTitle API.
Device string `json:"device"` // A value to identify the device.
}
// ERDidReceiveSettings - The didReceiveSettings event is received after calling the getSettings API to retrieve the persistent data stored for the action.
// https://docs.elgato.com/sdk/plugins/events-received#didreceivesettings
type ERDidReceiveSettings struct {
ERCommon
Payload ERDidReceiveSettingsPayload `json:"payload"`
}
type ERDidReceiveSettingsPayload struct {
Settings json.RawMessage `json:"settings"` // This JSON object contains persistently stored data
Coordinates struct { //The coordinates of the action triggered
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
State *int `json:"state"` // Only set when the action has multiple states defined in its manifest.json. The 0-based value contains the current state of the action
IsInMultiAction bool `json:"isInMultiAction"` // Boolean indicating if the action is inside a Multi-Action
}
// ERDidReceiveGlobalSettings - The didReceiveGlobalSettings event is received after calling the getGlobalSettings API to retrieve the global persistent data stored for the plugin.
// https://docs.elgato.com/sdk/plugins/events-received#didreceiveglobalsettings
type ERDidReceiveGlobalSettings struct {
Event string `json:"event"`
Payload ERDidReceiveGlobalSettingsPayload `json:"payload"`
}
type ERDidReceiveGlobalSettingsPayload struct {
Settings json.RawMessage `json:"settings"` // This JSON object contains persistently stored data
}
// ERDidReceiveDeepLink - Occurs when Stream Deck receives a deep-link message intended for the plugin. The message is re-routed to the plugin, and provided as part of the payload. One-way deep-link message can be routed to the plugin using the URL format:
//
// streamdeck://plugins/message/<PLUGIN_UUID>/{MESSAGE}
//
// https://docs.elgato.com/sdk/plugins/events-received#didreceivedeeplink
type ERDidReceiveDeepLink struct {
Event string `json:"event"`
Payload struct {
Url string `json:"url"` // The deep-link URL, with the prefix omitted. For example the URL streamdeck://plugins/message/com.elgato.test/hello-world would result in a url of hello-world
} `json:"payload"`
}
// ERTouchTap - When the user touches the display, the plugin will receive the touchTap event
//
// https://docs.elgato.com/sdk/plugins/events-received#touchtap-sd
type ERTouchTap struct {
ERCommon
Payload struct {
Settings json.RawMessage `json:"settings"`
Controller string `json:"controller"`
Coordinates struct {
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
TapPosition []int `json:"tapPos"`
Hold bool `json:"hold"`
} `json:"payload"`
}
// ERDialDown - When the user presses the encoder down, the plugin will receive the dialDown event (SD+).
// https://docs.elgato.com/sdk/plugins/events-received#dialdown-sd
type ERDialDown struct {
ERCommon
Payload struct {
Settings json.RawMessage `json:"settings"`
Controller string `json:"controller"`
Coordinates struct {
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
} `json:"payload"`
}
// ERDialUp - When the user releases a pressed encoder, the plugin will receive the dialUp event (SD+).
// https://docs.elgato.com/sdk/plugins/events-received#dialup-sd
type ERDialUp struct {
ERCommon
Payload struct {
Settings json.RawMessage `json:"settings"`
Controller string `json:"controller"`
Coordinates struct {
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
} `json:"payload"`
}
// ERDialRotate - When the user rotates the encoder, the plugin will receive the dialRotate event.
// https://docs.elgato.com/sdk/plugins/events-received#dialrotate-sd
type ERDialRotate struct {
ERCommon
Payload struct {
Settings json.RawMessage `json:"settings"`
Controller string `json:"controller"`
Coordinates struct {
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
Ticks int `json:"ticks"` // The integer which holds the number of "ticks" on encoder rotation. Positive values are for clockwise rotation, negative values are for counterclockwise rotation, zero value is never happen
Pressed bool `json:"pressed"` // Boolean which is true on rotation when encoder pressed
} `json:"payload"`
}
// ERKeyDown - When the user presses a key, the plugin will receive the keyDown event.
// https://docs.elgato.com/sdk/plugins/events-received#keydown
type ERKeyDown struct {
ERCommon
Payload struct {
Settings json.RawMessage `json:"settings"`
Coordinates struct {
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
State *int `json:"state"` // Only set when the action has multiple states defined in its manifest.json. The 0-based value contains the current state of the action.
UserDesiredState *int `json:"userDesiredState"` // Only set when the action is triggered with a specific value from a Multi-Action. For example, if the user sets the Game Capture Record action to be disabled in a Multi-Action, you would see the value 1. 0 and 1 are valid.
IsInMultiAction bool `json:"isInMultiAction"` // Boolean indicating if the action is inside a Multi-Action.
} `json:"payload"`
}
// ERKeyUp - When the user releases a key, the plugin will receive the keyUp event
// https://docs.elgato.com/sdk/plugins/events-received#keyup
type ERKeyUp struct {
ERCommon
Payload struct {
Settings json.RawMessage `json:"settings"`
Coordinates struct {
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
State *int `json:"state"` // Only set when the action has multiple states defined in its manifest.json. The 0-based value contains the current state of the action
UserDesiredState *int `json:"userDesiredState"` // Only set when the action is triggered with a specific value from a Multi-Action. For example, if the user sets the Game Capture Record action to be disabled in a Multi-Action, you would see the value 1. 0 and 1 are valid.
IsInMultiAction bool `json:"isInMultiAction"` // Boolean indicating if the action is inside a Multi-Action
} `json:"payload"`
}
// ERWillAppear - When an instance of an action is displayed on Stream Deck, for example, when the hardware is first plugged in or when a folder containing that action is entered, the plugin will receive a willAppear event. You will see such an event when:
// - the Stream Deck application is started
// - the user switches between profiles
// - the user sets a key to use your action
//
// https://docs.elgato.com/sdk/plugins/events-received#willappear
type ERWillAppear struct {
ERCommon
Payload struct {
Settings json.RawMessage `json:"settings"`
Coordinates struct {
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
Controller string `json:"controller"` // Defines the controller type the action is applicable to. Keypad refers to a standard action on a Stream Deck device, e.g. 1 of the 15 buttons on the Stream Deck MK.2, or a pedal on the Stream Deck Pedal, etc., whereas an Encoder refers to a dial / touchscreen on the Stream Deck+.
State *int `json:"state"` // Only set when the action has multiple states defined in its manifest.json. The 0-based value contains the current state of the action.
IsInMultiAction bool `json:"isInMultiAction"` // Boolean indicating if the action is inside a Multi-Action.
} `json:"payload"`
}
// ERWillDisappear - When an instance of an action ceases to be displayed on Stream Deck, for example, when switching profiles or folders, the plugin will receive a willDisappear event. You will see such an event when:
// - the user switches between profiles
// - the user deletes an action
//
// https://docs.elgato.com/sdk/plugins/events-received#willdisappear
type ERWillDisappear struct {
ERCommon
Payload struct {
Settings json.RawMessage `json:"settings"`
Coordinates struct {
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
Controller string `json:"controller"` // Defines the controller type the action is applicable to. Keypad refers to a standard action on a Stream Deck device, e.g. 1 of the 15 buttons on the Stream Deck MK.2, or a pedal on the Stream Deck Pedal, etc., whereas an Encoder refers to a dial / touchscreen on the Stream Deck+.
State *int `json:"state"` // Only set when the action has multiple states defined in its manifest.json. The 0-based value contains the current state of the action.
IsInMultiAction bool `json:"isInMultiAction"` //Boolean indicating if the action is inside a Multi-Action
} `json:"payload"`
}
// ERTitleParametersDidChange - When the user changes the title or title parameters of the instance of an action, the plugin will receive a titleParametersDidChange event
// https://docs.elgato.com/sdk/plugins/events-received#titleparametersdidchange
type ERTitleParametersDidChange struct {
ERCommon
Payload struct {
Settings json.RawMessage `json:"settings"` // This JSON object contains data that you can set and is stored persistently.
Coordinates struct {
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
State int `json:"state"` // This value indicates which state of the action the title or title parameters have been changed.
Title string `json:"title"` //The new title.
TitleParameters struct {
FontFamily string `json:"fontFamily"` //The font family for the title.
FontSize int `json:"fontSize"` // The font size for the title.
FontStyle string `json:"fontStyle"` // The font style for the title
FontUnderline bool `json:"fontUnderline"` //Boolean indicating an underline under the title
ShowTitle bool `json:"showTitle"` //Boolean indicating if the title is visible
TitleAlignment string `json:"titleAlignment"` //Vertical alignment of the title. Possible values are "top", "bottom" and "middle".
TitleColor string `json:"titleColor"` // Title color.
} `json:"titleParameters"`
} `json:"payload"`
}
// ERDeviceDidConnect - When a device is plugged into the computer, the plugin will receive a deviceDidConnect event
// https://docs.elgato.com/sdk/plugins/events-received#devicedidconnect
type ERDeviceDidConnect struct {
Event string `json:"event"`
Device string `json:"device"`
DeviceInfo struct {
Name string `json:"name"` // The name of the device set by the user.
DeviceType int `json:"type"` // Type of device. Possible values are kESDSDKDeviceType_StreamDeck (0), kESDSDKDeviceType_StreamDeckMini (1), kESDSDKDeviceType_StreamDeckXL (2), kESDSDKDeviceType_StreamDeckMobile (3) and kESDSDKDeviceType_CorsairGKeys (4)
Size struct {
Columns int `json:"columns"`
Rows int `json:"rows"`
} `json:"size"` // The number of columns and rows of keys that the device owns
} `json:"deviceInfo"`
}
// ERDeviceDidDisconnect - When a device is unplugged from the computer, the plugin will receive a deviceDidDisconnect event
// https://docs.elgato.com/sdk/plugins/events-received#devicediddisconnect
type ERDeviceDidDisconnect struct {
Event string `json:"event"`
Device string `json:"device"`
}
// ERApplicationDidLaunch - A plugin can request in its manifest.json to be notified when some applications are launched or terminated. The manifest.json should contain an ApplicationsToMonitor object specifying the list of application identifiers to monitor. On macOS, the application bundle identifier is used while the exe filename is used on Windows.
// https://docs.elgato.com/sdk/plugins/events-received#applicationdidlaunch
type ERApplicationDidLaunch struct {
Event string `json:"event"`
Payload struct {
Application string `json:"application"` // The identifier of the application that has been launched
}
}
// ERApplicationDidTerminate - A plugin can request in its manifest.json to be notified when some applications are launched or terminated. The manifest.json should contain an ApplicationsToMonitor object specifying the list of application identifiers to monitor. On macOS, the application bundle identifier is used while the exe filename is used on Windows.
// https://docs.elgato.com/sdk/plugins/events-received#applicationdidterminate
type ERApplicationDidTerminate struct {
Event string `json:"event"`
Payload struct {
Application string `json:"application"` // The identifier of the application that has been launched
}
}
// When the computer wakes up, the plugin will receive the systemDidWakeUp event
// Several important points to note:
// - A plugin could get multiple systemDidWakeUp events when waking up the computer
// - When the plugin receives the systemDidWakeUp event, there is no guarantee that the devices are available
//
// https://docs.elgato.com/sdk/plugins/events-received#systemdidwakeup
type ERApplicationSystemDidWakeUp struct {
Event string `json:"event"`
}
// ERApplicationPropertyInspectorDidAppear - The plugin will receive a propertyInspectorDidAppear event when the Property Inspector appears
// https://docs.elgato.com/sdk/plugins/events-received#propertyinspectordidappear
type ERApplicationPropertyInspectorDidAppear struct {
ERCommon
}
// ERApplicationPropertyInspectorDidDisappear - The plugin will receive a propertyInspectorDidDisappear event when the Property Inspector disappears
// https://docs.elgato.com/sdk/plugins/events-received#propertyinspectordiddisappear
type ERApplicationPropertyInspectorDidDisappear struct {
ERCommon
}
// ERApplicationPropertySendToPlugin - The plugin will receive a sendToPlugin event when the Property Inspector sends a sendToPlugin event
// https://docs.elgato.com/sdk/plugins/events-received#sendtoplugin
type ERApplicationPropertySendToPlugin struct {
Action string `json:"action"`
Event string `json:"event"`
Context string `json:"context"`
Payload json.RawMessage `json:"payload"`
}
// ERApplicationPropertySendToPropertyInspector - The Property Inspector will receive a sendToPropertyInspector event when the plugin sends a sendToPropertyInspector event
// https://docs.elgato.com/sdk/plugins/events-received#sendtopropertyinspector
type ERApplicationPropertySendToPropertyInspector struct {
Action string `json:"action"`
Event string `json:"event"`
Context string `json:"context"`
Payload json.RawMessage `json:"payload"`
}

View File

@ -1,26 +1,22 @@
package streamdeck
package events
import (
"bytes"
"encoding/base64"
"encoding/json"
"image"
"image/png"
)
type EventTarget int
const EventTargetBoth = 0
const EventTargetHardware = 1
const EventTargetSoftware = 2
const EventTargetBoth = EventTarget(0)
const EventTargetHardware = EventTarget(1)
const EventTargetSoftware = EventTarget(2)
type ESCommon struct {
Event string `json:"event"`
Context string `json:"context"`
Event string `json:"event"` // name of this event type
Context string `json:"context"` // A value to Identify the instance's action or Property Inspector. This value is received by the Property Inspector as a parameter of the connectElgatoStreamDeckSocket function.
}
type ESCommonNoContext struct {
Event string `json:"event"`
Event string `json:"event"` // name of this event type
}
type ESOpenMessage struct {
@ -162,12 +158,12 @@ type ESSetImage struct {
}
type ESSetImagePayload struct {
Image string `json:"title"`
Image string `json:"image"`
Target EventTarget `json:"target"`
State int `json:"state"`
State *int `json:"state,omitempty"`
}
func NewESSetImagePayload(context string, imageBase64 string, target EventTarget, state int) ESSetImage {
func NewESSetImage(context string, imageBase64 string, target EventTarget, state *int) ESSetImage {
return ESSetImage{
ESCommon: ESCommon{
Event: "setImage",
@ -286,13 +282,13 @@ type ESSetStatePayload struct {
State int `json:"state"`
}
func NewESSetState(context string, state int) ESSetImage {
return ESSetImage{
func NewESSetState(context string, state int) ESSetState {
return ESSetState{
ESCommon: ESCommon{
Event: "setState",
Context: context,
},
Payload: ESSetImagePayload{
Payload: ESSetStatePayload{
State: state,
},
}
@ -362,24 +358,3 @@ func NewESSendToPlugin(context string, action string, payload json.RawMessage) E
Payload: payload,
}
}
/// --------- helpers ----------
// Turns an image.Image into a string suitable for delivering
// via a ESSetImage struct
func ImageToPayload(i image.Image) string {
out := bytes.Buffer{}
b64 := base64.NewEncoder(base64.RawStdEncoding, &out)
err := png.Encode(b64, i)
if err != nil {
panic(err)
}
return "data:image/png;base64," + out.String()
}
// Turns an image.Image into a string suitable for delivering
// via a ESSetImage struct
func SVGToPayload(context string, svg string) string {
return "data:image/svg+xml;charset=utf8," + svg
}

View File

@ -1,301 +0,0 @@
package streamdeck
import (
"encoding/json"
"reflect"
)
var receivedEventTypeMap = map[string]reflect.Type{}
func init() {
receivedEventTypeMap["keyUp"] = reflect.TypeOf(ERKeyUp{})
receivedEventTypeMap["didReceiveSettingsPayload"] = reflect.TypeOf(ERDidReceiveSettingsPayload{})
receivedEventTypeMap["didReceiveSettings"] = reflect.TypeOf(ERDidReceiveSettings{})
receivedEventTypeMap["globalSettings"] = reflect.TypeOf(ERGlobalSettings{})
receivedEventTypeMap["didReceiveDeepLink"] = reflect.TypeOf(ERDidReceiveDeepLink{})
receivedEventTypeMap["touchTap"] = reflect.TypeOf(ERTouchTap{})
receivedEventTypeMap["dialDown"] = reflect.TypeOf(ERDialDown{})
receivedEventTypeMap["dialUp"] = reflect.TypeOf(ERDialUp{})
receivedEventTypeMap["dialRotate"] = reflect.TypeOf(ERDialRotate{})
receivedEventTypeMap["keyDown"] = reflect.TypeOf(ERKeyDown{})
receivedEventTypeMap["willAppear"] = reflect.TypeOf(ERWillAppear{})
receivedEventTypeMap["willDisappear"] = reflect.TypeOf(ERWillDisappear{})
receivedEventTypeMap["titleParametersDidChange"] = reflect.TypeOf(ERTitleParametersDidChange{})
receivedEventTypeMap["deviceDidConnect"] = reflect.TypeOf(ERDeviceDidConnect{})
receivedEventTypeMap["deviceDidDisconnect"] = reflect.TypeOf(ERDeviceDidDisconnect{})
receivedEventTypeMap["applicationDidLaunch"] = reflect.TypeOf(ERApplicationDidLaunch{})
receivedEventTypeMap["applicationDidTerminate"] = reflect.TypeOf(ERApplicationDidTerminate{})
receivedEventTypeMap["applicationSystemDidWakeUp"] = reflect.TypeOf(ERApplicationSystemDidWakeUp{})
receivedEventTypeMap["applicationPropertyInspectorDidAppear"] = reflect.TypeOf(ERApplicationPropertyInspectorDidAppear{})
receivedEventTypeMap["applicationPropertyInspectorDidDisappear"] = reflect.TypeOf(ERApplicationPropertyInspectorDidDisappear{})
receivedEventTypeMap["applicationPropertySendToPlugin"] = reflect.TypeOf(ERApplicationPropertySendToPlugin{})
receivedEventTypeMap["applicationPropertySendToPropertyInspector"] = reflect.TypeOf(ERApplicationPropertySendToPropertyInspector{})
}
type ERBase struct {
Event string `json:"event"`
}
//{Action: Event:deviceDidConnect Context: Device:A1DA463F033AD2616E05636CD16F064F Payload:[]}"}
type ERCommon struct {
Action string `json:"action"`
Event string `json:"event"`
Context string `json:"context"`
Device string `json:"device"`
}
// https://docs.elgato.com/sdk/plugins/events-received#didreceivesettings
type ERDidReceiveSettingsPayload struct {
Settings json.RawMessage `json:"settings"`
Coordinates struct {
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
IsInMultiAction bool `json:"isInMultiAction"`
}
type ERDidReceiveSettings struct {
ERCommon
Payload ERDidReceiveSettingsPayload `json:"payload"`
}
// https://docs.elgato.com/sdk/plugins/events-received#didreceiveglobalsettings
type ERGlobalSettings struct {
Event string `json:"event"`
Payload json.RawMessage `json:"payload"`
}
// https://docs.elgato.com/sdk/plugins/events-received#didreceivedeeplink
type ERDidReceiveDeepLink struct {
Event string `json:"event"`
Payload struct {
Url string `json:"url"`
} `json:"payload"`
}
// https://docs.elgato.com/sdk/plugins/events-received#touchtap-sd
type ERTouchTap struct {
ERCommon
Payload struct {
Settings json.RawMessage `json:"settings"`
Controller string `json:"controller"`
Coordinates struct {
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
TapPosition []int `json:"tapPos"`
Hold bool `json:"hold"`
} `json:"payload"`
}
// https://docs.elgato.com/sdk/plugins/events-received#dialdown-sd
type ERDialDown struct {
ERCommon
Payload struct {
Settings json.RawMessage `json:"settings"`
Controller string `json:"controller"`
Coordinates struct {
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
} `json:"payload"`
}
// https://docs.elgato.com/sdk/plugins/events-received#dialup-sd
type ERDialUp struct {
ERCommon
Payload struct {
Settings json.RawMessage `json:"settings"`
Controller string `json:"controller"`
Coordinates struct {
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
} `json:"payload"`
}
// https://docs.elgato.com/sdk/plugins/events-received#dialrotate-sd
type ERDialRotate struct {
ERCommon
Payload struct {
Settings json.RawMessage `json:"settings"`
Controller string `json:"controller"`
Coordinates struct {
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
Ticks int `json:"ticks"`
Pressed bool `json:"pressed"`
} `json:"payload"`
}
// https://docs.elgato.com/sdk/plugins/events-received#keydown
type ERKeyDown struct {
ERCommon
Payload struct {
Settings json.RawMessage `json:"settings"`
Coordinates struct {
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
State int `json:"state"`
UserDesiredState int `json:"userDesiredState"`
IsInMultiAction bool `json:"isInMultiAction"`
} `json:"payload"`
}
// https://docs.elgato.com/sdk/plugins/events-received#keyup
type ERKeyUp struct {
ERCommon
Payload struct {
Settings json.RawMessage `json:"settings"`
Coordinates struct {
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
State int `json:"state"`
UserDesiredState int `json:"userDesiredState"`
IsInMultiAction bool `json:"isInMultiAction"`
} `json:"payload"`
}
// https://docs.elgato.com/sdk/plugins/events-received#willappear
type ERWillAppear struct {
ERCommon
Payload struct {
Settings json.RawMessage `json:"settings"`
Coordinates struct {
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
Controller string `json:"controller"`
State int `json:"state"`
IsInMultiAction bool `json:"isInMultiAction"`
} `json:"payload"`
}
// https://docs.elgato.com/sdk/plugins/events-received#willdisappear
const ERWillDisappearAction = "willDisappear"
type ERWillDisappear struct {
ERCommon
Payload struct {
Settings json.RawMessage `json:"settings"`
Coordinates struct {
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
Controller string `json:"controller"`
State int `json:"state"`
IsInMultiAction bool `json:"isInMultiAction"`
} `json:"payload"`
}
// https://docs.elgato.com/sdk/plugins/events-received#titleparametersdidchange
type ERTitleParametersDidChange struct {
ERCommon
Payload struct {
Settings json.RawMessage `json:"settings"`
Coordinates struct {
Column int `json:"column"`
Row int `json:"row"`
} `json:"coordinates"`
State int `json:"state"`
Title string `json:"title"`
TitleParameters struct {
FontFamily string `json:"fontFamily"`
FontSize int `json:"fontSize"`
FontStyle string `json:"fontStyle"`
FontUnderline bool `json:"fontUnderline"`
ShowTitle bool `json:"showTitle"`
TitleAlignment string `json:"titleAlignment"`
TitleColor string `json:"titleColor"`
} `json:"titleParameters"`
} `json:"payload"`
}
// https://docs.elgato.com/sdk/plugins/events-received#devicedidconnect
type ERDeviceDidConnect struct {
Event string `json:"event"`
Device string `json:"device"`
DeviceInfo struct {
Name string `json:"name"`
DeviceType int `json:"type"`
Size struct {
Columns int `json:"columns"`
Rows int `json:"rows"`
} `json:"size"`
} `json:"deviceInfo"`
}
// https://docs.elgato.com/sdk/plugins/events-received#devicediddisconnect
type ERDeviceDidDisconnect struct {
Event string `json:"event"`
Device string `json:"device"`
}
// https://docs.elgato.com/sdk/plugins/events-received#applicationdidlaunch
type ERApplicationDidLaunch struct {
Event string `json:"event"`
Payload struct {
Application string `json:"application"`
}
}
// https://docs.elgato.com/sdk/plugins/events-received#applicationdidterminate
type ERApplicationDidTerminate struct {
Event string `json:"event"`
Payload struct {
Application string `json:"application"`
}
}
// https://docs.elgato.com/sdk/plugins/events-received#systemdidwakeup
type ERApplicationSystemDidWakeUp struct {
Event string `json:"event"`
}
// https://docs.elgato.com/sdk/plugins/events-received#propertyinspectordidappear
type ERApplicationPropertyInspectorDidAppear struct {
ERCommon
}
// https://docs.elgato.com/sdk/plugins/events-received#propertyinspectordiddisappear
type ERApplicationPropertyInspectorDidDisappear struct {
ERCommon
}
// https://docs.elgato.com/sdk/plugins/events-received#sendtoplugin
type ERApplicationPropertySendToPlugin struct {
Action string `json:"action"`
Event string `json:"event"`
Context string `json:"context"`
Payload json.RawMessage `json:"payload"`
}
// https://docs.elgato.com/sdk/plugins/events-received#sendtopropertyinspector
type ERApplicationPropertySendToPropertyInspector struct {
Action string `json:"action"`
Event string `json:"event"`
Context string `json:"context"`
Payload json.RawMessage `json:"payload"`
}

View File

@ -1,3 +1,5 @@
// Package streamdeck interfaces with the Stream Deck plugin API,
// allowing you to create go-based plugins for the platform.
package streamdeck
import (
@ -8,21 +10,16 @@ import (
"io"
"reflect"
"github.com/tardisx/streamdeck-plugin/events"
"github.com/gorilla/websocket"
)
// these are automatically populated by parseFlags
var flagPort int
var flagEvent, flagInfo string
var UUID string // the UUID this plugin is assigned
func init() {
flag.IntVar(&flagPort, "port", 0, "streamdeck sdk port")
flag.StringVar(&flagEvent, "registerEvent", "", "streamdeck sdk register event")
flag.StringVar(&flagInfo, "info", "", "streamdeck application info")
flag.StringVar(&UUID, "pluginUUID", "", "uuid")
flag.Parse()
}
type logger interface {
Info(string, ...any)
Error(string, ...any)
@ -35,11 +32,6 @@ func (nl nullLogger) Info(string, ...any) {}
func (nl nullLogger) Error(string, ...any) {}
func (nl nullLogger) Debug(string, ...any) {}
type EventHandler struct {
MsgType string
Handler func()
}
type Connection struct {
ws *websocket.Conn
logger logger
@ -48,9 +40,7 @@ type Connection struct {
}
// New creates a new struct for communication with the streamdeck
// plugin API. The command line flags required for the API to
// communicate with your plugin have already been parsed.
// The websocket will not connect until Connect is called.
// plugin API. The websocket will not connect until Connect is called.
func New() Connection {
return Connection{
handlers: make(map[reflect.Type]reflect.Value),
@ -67,6 +57,16 @@ func NewWithLogger(l logger) Connection {
return c
}
// parseFlags parses the command line flags to get the values provided
// by the Stream Deck plugin API.
func parseFlags() {
flag.IntVar(&flagPort, "port", 0, "streamdeck sdk port")
flag.StringVar(&flagEvent, "registerEvent", "", "streamdeck sdk register event")
flag.StringVar(&flagInfo, "info", "", "streamdeck application info")
flag.StringVar(&UUID, "pluginUUID", "", "uuid")
flag.Parse()
}
// Connect connects the plugin to the Stream Deck API via the websocket.
// Once connected, events will be passed to handlers you have registered.
// Handlers should thus be registered via RegisterHandler before calling
@ -75,14 +75,15 @@ func NewWithLogger(l logger) Connection {
// then call WaitForPluginExit to block until the connection is closed.
func (conn *Connection) Connect() error {
parseFlags()
c, _, err := websocket.DefaultDialer.Dial(fmt.Sprintf("ws://localhost:%d", flagPort), nil)
if err != nil {
return err
}
conn.ws = c
msg := ESOpenMessage{
ESCommonNoContext: ESCommonNoContext{
msg := events.ESOpenMessage{
ESCommonNoContext: events.ESCommonNoContext{
Event: flagEvent,
},
UUID: UUID,
@ -103,8 +104,8 @@ func (conn *Connection) Connect() error {
// WaitForPluginExit waits until the Stream Deck API closes
// the websocket connection.
func (c *Connection) WaitForPluginExit() {
<-c.done
func (conn *Connection) WaitForPluginExit() {
<-conn.done
}
// RegisterHandler registers a function to be called for a particular event. The
@ -114,7 +115,7 @@ func (c *Connection) WaitForPluginExit() {
// function per event type. This function will panic if the wrong kind of
// function is passed in, or if you try to register more than one for a single
// event type.
func (r *Connection) RegisterHandler(handler any) {
func (conn *Connection) RegisterHandler(handler any) {
hType := reflect.TypeOf(handler)
if hType.Kind() != reflect.Func {
panic("handler must be a function")
@ -127,27 +128,20 @@ func (r *Connection) RegisterHandler(handler any) {
argType := hType.In(0)
// check its a valid one (one that matches an event type)
valid := false
for i := range receivedEventTypeMap {
if receivedEventTypeMap[i] == argType {
valid = true
break
}
}
if !valid {
if !events.ValidEventType(argType) {
panic("you cannot register a handler with this argument type")
}
_, alreadyExists := r.handlers[argType]
_, alreadyExists := conn.handlers[argType]
if alreadyExists {
panic("handler for " + argType.Name() + " already exists")
}
r.handlers[argType] = reflect.ValueOf(handler)
conn.handlers[argType] = reflect.ValueOf(handler)
}
// Send sends a message to the API. It should be one of the
// ES* structs, such as ESOpenURL.
// events.ES* structs, such as events.ESOpenURL.
func (conn *Connection) Send(e any) error {
b, _ := json.Marshal(e)
conn.logger.Debug(fmt.Sprintf("sending: %s", string(b)))
@ -185,14 +179,14 @@ func (conn *Connection) reader() {
b := bytes.Buffer{}
r = io.TeeReader(r, &b)
base := ERBase{}
base := events.ERBase{}
err = json.NewDecoder(r).Decode(&base)
if err != nil {
conn.logger.Error("cannot decode: " + err.Error())
continue
}
t, ok := receivedEventTypeMap[base.Event]
t, ok := events.TypeForEvent(base.Event)
if !ok {
conn.logger.Error(fmt.Sprintf("no type registered for event '%s'", base.Event))
continue

View File

@ -2,6 +2,8 @@ package streamdeck
import (
"testing"
"github.com/tardisx/streamdeck-plugin/events"
)
type testLogger struct {
@ -16,10 +18,10 @@ func TestReflection(t *testing.T) {
c := NewWithLogger(testLogger{t: t})
// incoming
in := ERDidReceiveSettingsPayload{}
in := events.ERDidReceiveSettingsPayload{}
ranHandler := false
c.RegisterHandler(func(event ERDidReceiveSettingsPayload) {
c.RegisterHandler(func(event events.ERDidReceiveSettingsPayload) {
ranHandler = true
})
@ -52,13 +54,14 @@ func TestUmmarshal(t *testing.T) {
}`)
c := NewWithLogger(testLogger{t: t})
keyUp, err := c.unmarshalToConcrete(receivedEventTypeMap["keyUp"], b)
e, _ := events.TypeForEvent("keyUp")
keyUp, err := c.unmarshalToConcrete(e, b)
if err != nil {
t.Error(err)
}
realKeyUp, ok := keyUp.(ERKeyUp)
realKeyUp, ok := keyUp.(events.ERKeyUp)
if !ok {
t.Errorf("wrong type (is %T)", keyUp)
}

29
tools/tools.go Normal file
View File

@ -0,0 +1,29 @@
// Package tools provides some helper functions to assist with
// creating Stream Deck plugins
package tools
import (
"bytes"
"encoding/base64"
"image"
"image/png"
)
// Turns an image.Image into a string suitable for delivering
// via an events.ESSetImage struct
func ImageToPayload(i image.Image) string {
out := bytes.Buffer{}
b64 := base64.NewEncoder(base64.RawStdEncoding, &out)
err := png.Encode(b64, i)
if err != nil {
panic(err)
}
return "data:image/png;base64," + out.String()
}
// SVGToPayload create the string necessary to send an SVG
// via a ESSetImage struct
func SVGToPayload(svg string) string {
return "data:image/svg+xml;charset=utf8;base64," + base64.StdEncoding.EncodeToString([]byte(svg))
}