Compare commits

...

9 Commits
v0.0.2 ... main

6 changed files with 158 additions and 77 deletions

View File

@ -1,2 +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) [![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()
}
```

View File

@ -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() {

View File

@ -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
}

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

View File

@ -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
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))
}