Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
c64a8e7a63 | |||
8abdbb6683 | |||
ffc606ec77 | |||
2eeaae2232 | |||
bfc17b45d4 | |||
4d2ebbd805 | |||
5b349724eb | |||
e3436a553a | |||
6cbf1cc0d2 |
63
README.md
63
README.md
@ -1,2 +1,65 @@
|
|||||||
|
# Stream Deck plugin library for Go
|
||||||
|
|
||||||
[](https://pkg.go.dev/github.com/tardisx/streamdeck-plugin)
|
[](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()
|
||||||
|
}
|
||||||
|
```
|
@ -1,10 +1,27 @@
|
|||||||
package streamdeck
|
package events
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"reflect"
|
"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{}
|
var receivedEventTypeMap = map[string]reflect.Type{}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
@ -1,26 +1,22 @@
|
|||||||
package streamdeck
|
package events
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"image"
|
|
||||||
"image/png"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type EventTarget int
|
type EventTarget int
|
||||||
|
|
||||||
const EventTargetBoth = 0
|
const EventTargetBoth = EventTarget(0)
|
||||||
const EventTargetHardware = 1
|
const EventTargetHardware = EventTarget(1)
|
||||||
const EventTargetSoftware = 2
|
const EventTargetSoftware = EventTarget(2)
|
||||||
|
|
||||||
type ESCommon struct {
|
type ESCommon struct {
|
||||||
Event string `json:"event"`
|
Event string `json:"event"` // name of this event type
|
||||||
Context string `json:"context"`
|
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 {
|
type ESCommonNoContext struct {
|
||||||
Event string `json:"event"`
|
Event string `json:"event"` // name of this event type
|
||||||
}
|
}
|
||||||
|
|
||||||
type ESOpenMessage struct {
|
type ESOpenMessage struct {
|
||||||
@ -162,12 +158,12 @@ type ESSetImage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ESSetImagePayload struct {
|
type ESSetImagePayload struct {
|
||||||
Image string `json:"title"`
|
Image string `json:"image"`
|
||||||
Target EventTarget `json:"target"`
|
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{
|
return ESSetImage{
|
||||||
ESCommon: ESCommon{
|
ESCommon: ESCommon{
|
||||||
Event: "setImage",
|
Event: "setImage",
|
||||||
@ -286,13 +282,13 @@ type ESSetStatePayload struct {
|
|||||||
State int `json:"state"`
|
State int `json:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewESSetState(context string, state int) ESSetImage {
|
func NewESSetState(context string, state int) ESSetState {
|
||||||
return ESSetImage{
|
return ESSetState{
|
||||||
ESCommon: ESCommon{
|
ESCommon: ESCommon{
|
||||||
Event: "setState",
|
Event: "setState",
|
||||||
Context: context,
|
Context: context,
|
||||||
},
|
},
|
||||||
Payload: ESSetImagePayload{
|
Payload: ESSetStatePayload{
|
||||||
State: state,
|
State: state,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -362,24 +358,3 @@ func NewESSendToPlugin(context string, action string, payload json.RawMessage) E
|
|||||||
Payload: payload,
|
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
|
|
||||||
}
|
|
@ -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
|
package streamdeck
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -8,21 +10,16 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/tardisx/streamdeck-plugin/events"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// these are automatically populated by parseFlags
|
||||||
var flagPort int
|
var flagPort int
|
||||||
var flagEvent, flagInfo string
|
var flagEvent, flagInfo string
|
||||||
var UUID string // the UUID this plugin is assigned
|
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 {
|
type logger interface {
|
||||||
Info(string, ...any)
|
Info(string, ...any)
|
||||||
Error(string, ...any)
|
Error(string, ...any)
|
||||||
@ -35,11 +32,6 @@ func (nl nullLogger) Info(string, ...any) {}
|
|||||||
func (nl nullLogger) Error(string, ...any) {}
|
func (nl nullLogger) Error(string, ...any) {}
|
||||||
func (nl nullLogger) Debug(string, ...any) {}
|
func (nl nullLogger) Debug(string, ...any) {}
|
||||||
|
|
||||||
type EventHandler struct {
|
|
||||||
MsgType string
|
|
||||||
Handler func()
|
|
||||||
}
|
|
||||||
|
|
||||||
type Connection struct {
|
type Connection struct {
|
||||||
ws *websocket.Conn
|
ws *websocket.Conn
|
||||||
logger logger
|
logger logger
|
||||||
@ -48,9 +40,7 @@ type Connection struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new struct for communication with the streamdeck
|
// New creates a new struct for communication with the streamdeck
|
||||||
// plugin API. The command line flags required for the API to
|
// plugin API. The websocket will not connect until Connect is called.
|
||||||
// communicate with your plugin have already been parsed.
|
|
||||||
// The websocket will not connect until Connect is called.
|
|
||||||
func New() Connection {
|
func New() Connection {
|
||||||
return Connection{
|
return Connection{
|
||||||
handlers: make(map[reflect.Type]reflect.Value),
|
handlers: make(map[reflect.Type]reflect.Value),
|
||||||
@ -67,6 +57,16 @@ func NewWithLogger(l logger) Connection {
|
|||||||
return c
|
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.
|
// Connect connects the plugin to the Stream Deck API via the websocket.
|
||||||
// Once connected, events will be passed to handlers you have registered.
|
// Once connected, events will be passed to handlers you have registered.
|
||||||
// Handlers should thus be registered via RegisterHandler before calling
|
// 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.
|
// then call WaitForPluginExit to block until the connection is closed.
|
||||||
func (conn *Connection) Connect() error {
|
func (conn *Connection) Connect() error {
|
||||||
|
|
||||||
|
parseFlags()
|
||||||
c, _, err := websocket.DefaultDialer.Dial(fmt.Sprintf("ws://localhost:%d", flagPort), nil)
|
c, _, err := websocket.DefaultDialer.Dial(fmt.Sprintf("ws://localhost:%d", flagPort), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.ws = c
|
conn.ws = c
|
||||||
msg := ESOpenMessage{
|
msg := events.ESOpenMessage{
|
||||||
ESCommonNoContext: ESCommonNoContext{
|
ESCommonNoContext: events.ESCommonNoContext{
|
||||||
Event: flagEvent,
|
Event: flagEvent,
|
||||||
},
|
},
|
||||||
UUID: UUID,
|
UUID: UUID,
|
||||||
@ -103,8 +104,8 @@ func (conn *Connection) Connect() error {
|
|||||||
|
|
||||||
// WaitForPluginExit waits until the Stream Deck API closes
|
// WaitForPluginExit waits until the Stream Deck API closes
|
||||||
// the websocket connection.
|
// the websocket connection.
|
||||||
func (c *Connection) WaitForPluginExit() {
|
func (conn *Connection) WaitForPluginExit() {
|
||||||
<-c.done
|
<-conn.done
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterHandler registers a function to be called for a particular event. The
|
// 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 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
|
// function is passed in, or if you try to register more than one for a single
|
||||||
// event type.
|
// event type.
|
||||||
func (r *Connection) RegisterHandler(handler any) {
|
func (conn *Connection) RegisterHandler(handler any) {
|
||||||
hType := reflect.TypeOf(handler)
|
hType := reflect.TypeOf(handler)
|
||||||
if hType.Kind() != reflect.Func {
|
if hType.Kind() != reflect.Func {
|
||||||
panic("handler must be a function")
|
panic("handler must be a function")
|
||||||
@ -127,27 +128,20 @@ func (r *Connection) RegisterHandler(handler any) {
|
|||||||
argType := hType.In(0)
|
argType := hType.In(0)
|
||||||
|
|
||||||
// check its a valid one (one that matches an event type)
|
// check its a valid one (one that matches an event type)
|
||||||
valid := false
|
if !events.ValidEventType(argType) {
|
||||||
for i := range receivedEventTypeMap {
|
|
||||||
if receivedEventTypeMap[i] == argType {
|
|
||||||
valid = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !valid {
|
|
||||||
panic("you cannot register a handler with this argument type")
|
panic("you cannot register a handler with this argument type")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, alreadyExists := r.handlers[argType]
|
_, alreadyExists := conn.handlers[argType]
|
||||||
if alreadyExists {
|
if alreadyExists {
|
||||||
panic("handler for " + argType.Name() + " already exists")
|
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
|
// 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 {
|
func (conn *Connection) Send(e any) error {
|
||||||
b, _ := json.Marshal(e)
|
b, _ := json.Marshal(e)
|
||||||
conn.logger.Debug(fmt.Sprintf("sending: %s", string(b)))
|
conn.logger.Debug(fmt.Sprintf("sending: %s", string(b)))
|
||||||
@ -185,14 +179,14 @@ func (conn *Connection) reader() {
|
|||||||
|
|
||||||
b := bytes.Buffer{}
|
b := bytes.Buffer{}
|
||||||
r = io.TeeReader(r, &b)
|
r = io.TeeReader(r, &b)
|
||||||
base := ERBase{}
|
base := events.ERBase{}
|
||||||
err = json.NewDecoder(r).Decode(&base)
|
err = json.NewDecoder(r).Decode(&base)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.logger.Error("cannot decode: " + err.Error())
|
conn.logger.Error("cannot decode: " + err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
t, ok := receivedEventTypeMap[base.Event]
|
t, ok := events.TypeForEvent(base.Event)
|
||||||
if !ok {
|
if !ok {
|
||||||
conn.logger.Error(fmt.Sprintf("no type registered for event '%s'", base.Event))
|
conn.logger.Error(fmt.Sprintf("no type registered for event '%s'", base.Event))
|
||||||
continue
|
continue
|
||||||
|
@ -2,6 +2,8 @@ package streamdeck
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/tardisx/streamdeck-plugin/events"
|
||||||
)
|
)
|
||||||
|
|
||||||
type testLogger struct {
|
type testLogger struct {
|
||||||
@ -16,10 +18,10 @@ func TestReflection(t *testing.T) {
|
|||||||
|
|
||||||
c := NewWithLogger(testLogger{t: t})
|
c := NewWithLogger(testLogger{t: t})
|
||||||
// incoming
|
// incoming
|
||||||
in := ERDidReceiveSettingsPayload{}
|
in := events.ERDidReceiveSettingsPayload{}
|
||||||
|
|
||||||
ranHandler := false
|
ranHandler := false
|
||||||
c.RegisterHandler(func(event ERDidReceiveSettingsPayload) {
|
c.RegisterHandler(func(event events.ERDidReceiveSettingsPayload) {
|
||||||
ranHandler = true
|
ranHandler = true
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -52,13 +54,14 @@ func TestUmmarshal(t *testing.T) {
|
|||||||
}`)
|
}`)
|
||||||
|
|
||||||
c := NewWithLogger(testLogger{t: 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 {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
realKeyUp, ok := keyUp.(ERKeyUp)
|
realKeyUp, ok := keyUp.(events.ERKeyUp)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("wrong type (is %T)", keyUp)
|
t.Errorf("wrong type (is %T)", keyUp)
|
||||||
}
|
}
|
||||||
|
29
tools/tools.go
Normal file
29
tools/tools.go
Normal 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))
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user