Major restructure and added a few example plugins.

This commit is contained in:
Sam Rose 2019-07-14 21:44:00 +01:00
parent cad454a4bf
commit 8ee19c457a
20 changed files with 810 additions and 310 deletions

10
action.go Normal file
View File

@ -0,0 +1,10 @@
package streamdeck
type Action struct {
uuid string
handlers map[string][]EventHandler
}
func (action *Action) RegisterHandler(eventName string, handler EventHandler) {
action.handlers[eventName] = append(action.handlers[eventName], handler)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 960 B

205
client.go Normal file
View File

@ -0,0 +1,205 @@
package streamdeck
import (
"context"
"encoding/json"
"fmt"
"log"
"net/url"
"os"
"os/signal"
"time"
"github.com/gorilla/websocket"
sdcontext "github.com/samwho/streamdeck/context"
"github.com/samwho/streamdeck/payload"
)
type EventHandler func(ctx context.Context, client *Client, event Event) error
type Client struct {
ctx context.Context
params RegistrationParams
c *websocket.Conn
actions map[string]*Action
handlers map[string][]EventHandler
done chan struct{}
}
func NewClient(ctx context.Context, params RegistrationParams) *Client {
return &Client{
ctx: ctx,
params: params,
actions: make(map[string]*Action),
done: make(chan struct{}),
}
}
func (client *Client) Action(uuid string) *Action {
_, ok := client.actions[uuid]
if !ok {
client.actions[uuid] = &Action{
uuid: uuid,
handlers: make(map[string][]EventHandler),
}
}
return client.actions[uuid]
}
func (client *Client) Run() error {
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
u := url.URL{Scheme: "ws", Host: fmt.Sprintf("127.0.0.1:%d", client.params.Port)}
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
return err
}
client.c = c
go func() {
defer close(client.done)
for {
messageType, message, err := client.c.ReadMessage()
if err != nil {
log.Printf("read error: %v\n", err)
return
}
if messageType == websocket.PingMessage {
log.Printf("received ping message\n")
if err := client.c.WriteMessage(websocket.PongMessage, []byte{}); err != nil {
log.Printf("error while ponging: %v\n", err)
}
continue
}
event := Event{}
if err := json.Unmarshal(message, &event); err != nil {
log.Printf("failed to unmarshal received event: %s\n", string(message))
continue
}
log.Println("recv: ", string(message))
ctx := sdcontext.WithContext(client.ctx, event.Context)
ctx = sdcontext.WithDevice(ctx, event.Device)
ctx = sdcontext.WithAction(ctx, event.Action)
if event.Action == "" {
for _, f := range client.handlers[event.Event] {
if err := f(ctx, client, event); err != nil {
log.Printf("error in handler for event %v: %v\n", event.Event, err)
}
}
continue
}
action, ok := client.actions[event.Action]
if !ok {
log.Printf("received event for nonexistent action: %v\n", event.Action)
continue
}
for _, f := range action.handlers[event.Event] {
if err := f(ctx, client, event); err != nil {
log.Printf("error in handler for event %v: %v\n", event.Event, err)
}
}
}
}()
if err := client.register(client.params); err != nil {
return err
}
select {
case <-client.done:
return nil
case <-interrupt:
log.Printf("interrupted, closing...\n")
return client.Close()
}
}
func (client *Client) register(params RegistrationParams) error {
if err := client.send(Event{UUID: params.PluginUUID, Event: params.RegisterEvent}); err != nil {
client.Close()
return err
}
return nil
}
func (client *Client) send(event Event) error {
j, _ := json.Marshal(event)
log.Printf("sending message: %v\n", string(j))
return client.c.WriteJSON(event)
}
func (client *Client) SetSettings(ctx context.Context, settings interface{}) error {
return client.send(NewEvent(ctx, SetSettings, settings))
}
func (client *Client) GetSettings(ctx context.Context) error {
return client.send(NewEvent(ctx, GetSettings, nil))
}
func (client *Client) SetGlobalSettings(ctx context.Context, settings interface{}) error {
return client.send(NewEvent(ctx, SetGlobalSettings, settings))
}
func (client *Client) GetGlobalSettings(ctx context.Context) error {
return client.send(NewEvent(ctx, GetGlobalSettings, nil))
}
func (client *Client) OpenURL(ctx context.Context, u url.URL) error {
return client.send(NewEvent(ctx, OpenURL, payload.OpenURL{URL: u.String()}))
}
func (client *Client) LogMessage(message string) error {
return client.send(NewEvent(nil, LogMessage, payload.LogMessage{Message: message}))
}
func (client *Client) SetTitle(ctx context.Context, title string, target payload.Target) error {
return client.send(NewEvent(ctx, SetTitle, payload.SetTitle{Title: title, Target: target}))
}
func (client *Client) SetImage(ctx context.Context, base64image string, target payload.Target) error {
return client.send(NewEvent(ctx, SetImage, payload.SetImage{Base64Image: base64image, Target: target}))
}
func (client *Client) ShowAlert(ctx context.Context) error {
return client.send(NewEvent(ctx, ShowAlert, nil))
}
func (client *Client) ShowOk(ctx context.Context) error {
return client.send(NewEvent(ctx, ShowOk, nil))
}
func (client *Client) SetState(ctx context.Context, state int) error {
return client.send(NewEvent(ctx, SetState, payload.SetState{State: state}))
}
func (client *Client) SwitchToProfile(ctx context.Context, profile string) error {
return client.send(NewEvent(ctx, SwitchToProfile, payload.SwitchProfile{Profile: profile}))
}
func (client *Client) SendToPropertyInspector(ctx context.Context, payload interface{}) error {
return client.send(NewEvent(ctx, SendToPropertyInspector, payload))
}
func (client *Client) SendToPlugin(ctx context.Context, payload interface{}) error {
return client.send(NewEvent(ctx, SendToPlugin, payload))
}
func (client *Client) Close() error {
err := client.c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
if err != nil {
return err
}
select {
case <-client.done:
case <-time.After(time.Second):
}
return client.c.Close()
}

View File

@ -1,7 +1,5 @@
package streamdeck
import "context"
const (
DidReceiveSettings = "didReceiveSettings"
DidReceiveGlobalSettings = "didReceiveGlobalSettings"
@ -33,21 +31,3 @@ const (
SetState = "setState"
SwitchToProfile = "switchToProfile"
)
type contextKeyType int
const (
contextKey contextKeyType = iota
)
func getContext(ctx context.Context) string {
if ctx == nil {
return ""
}
return ctx.Value(contextKey).(string)
}
func setContext(ctx context.Context, streamdeckContext string) context.Context {
return context.WithValue(ctx, contextKey, streamdeckContext)
}

55
context/context.go Normal file
View File

@ -0,0 +1,55 @@
package context
import (
"context"
)
type keyType int
const (
contextKey keyType = iota
deviceKey
actionKey
)
func Context(ctx context.Context) string {
return get(ctx, contextKey)
}
func WithContext(ctx context.Context, streamdeckContext string) context.Context {
return context.WithValue(ctx, contextKey, streamdeckContext)
}
func Device(ctx context.Context) string {
return get(ctx, deviceKey)
}
func WithDevice(ctx context.Context, streamdeckDevice string) context.Context {
return context.WithValue(ctx, deviceKey, streamdeckDevice)
}
func Action(ctx context.Context) string {
return get(ctx, actionKey)
}
func WithAction(ctx context.Context, streamdeckAction string) context.Context {
return context.WithValue(ctx, actionKey, streamdeckAction)
}
func get(ctx context.Context, key keyType) string {
if ctx == nil {
return ""
}
val := ctx.Value(key)
if val == nil {
return ""
}
valStr, ok := val.(string)
if !ok {
panic("found non-string in context")
}
return valStr
}

53
event.go Normal file
View File

@ -0,0 +1,53 @@
package streamdeck
import (
"context"
"encoding/json"
sdcontext "github.com/samwho/streamdeck/context"
)
type Event struct {
Action string `json:"action,omitempty"`
Event string `json:"event,omitempty"`
UUID string `json:"uuid,omitempty"`
Context string `json:"context,omitempty"`
Device string `json:"device,omitempty"`
DeviceInfo DeviceInfo `json:"deviceInfo,omitempty"`
Payload json.RawMessage `json:"payload,omitempty"`
}
type DeviceInfo struct {
DeviceName string `json:"deviceName,omitempty"`
Type DeviceType `json:"type,omitempty"`
Size DeviceSize `json:"size,omitempty"`
}
type DeviceSize struct {
Columns int `json:"columns,omitempty"`
Rows int `json:"rows,omitempty"`
}
type DeviceType int
const (
StreamDeck DeviceType = 0
StreamDeckMini DeviceType = 1
StreamDeckXL DeviceType = 2
StreamDeckMobile DeviceType = 3
)
func NewEvent(ctx context.Context, name string, payload interface{}) Event {
p, err := json.Marshal(payload)
if err != nil {
panic(err)
}
return Event{
Event: name,
Action: sdcontext.Action(ctx),
Context: sdcontext.Context(ctx),
Device: sdcontext.Device(ctx),
Payload: p,
}
}

View File

@ -1,9 +1,9 @@
GO = go
GOFLAGS =
INSTALLDIR = "$(APPDATA)\Elgato\StreamDeck\Plugins\dev.samwho.streamdeck.livesplit.sdPlugin"
INSTALLDIR = "$(APPDATA)\Elgato\StreamDeck\Plugins\dev.samwho.streamdeck.counter.sdPlugin"
LOGDIR = "$(APPDATA)\Elgato\StreamDeck\logs"
.PHONY: test install build
.PHONY: test install build logs
build:
$(GO) build $(GOFLAGS)
@ -14,9 +14,8 @@ test:
install: build
rm -rf $(INSTALLDIR)
mkdir $(INSTALLDIR)
cp *.png $(INSTALLDIR)
cp *.json $(INSTALLDIR)
cp *.exe $(INSTALLDIR)
logs:
tail -f $(LOGDIR)/streamdeck-livesplit.log
tail -f $(LOGDIR)/counter.log

112
examples/counter/main.go Normal file
View File

@ -0,0 +1,112 @@
package main
import (
"context"
"encoding/json"
"fmt"
"image"
"image/color"
"log"
"os"
"strconv"
"github.com/samwho/streamdeck"
"github.com/samwho/streamdeck/payload"
)
const (
logFile = "C:\\Users\\samwh\\AppData\\Roaming\\Elgato\\StreamDeck\\logs\\streamdeck-livesplit.log"
)
type Settings struct {
Counter int `json:"counter"`
}
func main() {
f, err := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatalf("error opening file: %v", err)
}
defer f.Close()
log.SetOutput(f)
ctx := context.Background()
if err := run(ctx); err != nil {
log.Fatalf("%v\n", err)
}
}
func run(ctx context.Context) error {
params, err := streamdeck.ParseRegistrationParams(os.Args)
if err != nil {
return err
}
client := streamdeck.NewClient(ctx, params)
setupCounter(client)
return client.Run()
}
func setupCounter(client *streamdeck.Client) {
action := client.Action("dev.samwho.streamdeck.counter")
settings := make(map[string]*Settings)
action.RegisterHandler(streamdeck.WillAppear, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error {
p := payload.WillAppear{}
if err := json.Unmarshal(event.Payload, &p); err != nil {
return err
}
s, ok := settings[event.Context]
if !ok {
s = &Settings{}
settings[event.Context] = s
}
if err := json.Unmarshal(p.Settings, s); err != nil {
return err
}
bg, err := streamdeck.Image(background())
if err != nil {
return err
}
if err := client.SetImage(ctx, bg, payload.HardwareAndSoftware); err != nil {
return err
}
return client.SetTitle(ctx, strconv.Itoa(s.Counter), payload.HardwareAndSoftware)
})
action.RegisterHandler(streamdeck.WillDisappear, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error {
s, _ := settings[event.Context]
s.Counter = 0
return client.SetSettings(ctx, s)
})
action.RegisterHandler(streamdeck.KeyDown, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error {
s, ok := settings[event.Context]
if !ok {
return fmt.Errorf("couldn't find settings for context %v", event.Context)
}
s.Counter++
if err := client.SetSettings(ctx, s); err != nil {
return err
}
return client.SetTitle(ctx, strconv.Itoa(s.Counter), payload.HardwareAndSoftware)
})
}
func background() image.Image {
img := image.NewRGBA(image.Rect(0, 0, 20, 20))
for x := 0; x < 20; x++ {
for y := 0; y < 20; y++ {
img.Set(x, y, color.Black)
}
}
return img
}

View File

@ -1,26 +1,23 @@
{
"Actions": [
{
"Icon": "actionIcon",
"Name": "LiveSplit Go",
"Name": "Counter",
"States": [
{
"Image": "actionDefaultImage",
"TitleAlignment": "middle",
"FontSize": "16"
"FontSize": "24"
}
],
"SupportedInMultiActions": false,
"Tooltip": "LiveSplit",
"UUID": "dev.samwho.streamdeck.livesplit"
"Tooltip": "Count something!",
"UUID": "dev.samwho.streamdeck.counter"
}
],
"SDKVersion": 2,
"Author": "Sam Rose",
"CodePath": "streamdeck-livesplit.exe",
"Description": "Test",
"Name": "Test",
"Icon": "pluginIcon",
"CodePath": "counter.exe",
"Description": "Count something!",
"Name": "Counter",
"URL": "https://samwho.dev",
"Version": "0.1",
"OS": [

21
examples/cpu/Makefile Normal file
View File

@ -0,0 +1,21 @@
GO = go
GOFLAGS =
INSTALLDIR = "$(APPDATA)\Elgato\StreamDeck\Plugins\dev.samwho.streamdeck.cpu.sdPlugin"
LOGDIR = "$(APPDATA)\Elgato\StreamDeck\logs"
.PHONY: test install build logs
build:
$(GO) build $(GOFLAGS)
test:
$(GO) run $(GOFLAGS) main.go -port 12345 -pluginUUID 213 -registerEvent test -info "{\"application\":{\"language\":\"en\",\"platform\":\"mac\",\"version\":\"4.1.0\"},\"plugin\":{\"version\":\"1.1\"},\"devicePixelRatio\":2,\"devices\":[{\"id\":\"55F16B35884A859CCE4FFA1FC8D3DE5B\",\"name\":\"Device Name\",\"size\":{\"columns\":5,\"rows\":3},\"type\":0},{\"id\":\"B8F04425B95855CF417199BCB97CD2BB\",\"name\":\"Another Device\",\"size\":{\"columns\":3,\"rows\":2},\"type\":1}]}"
install: build
rm -rf $(INSTALLDIR)
mkdir $(INSTALLDIR)
cp *.json $(INSTALLDIR)
cp *.exe $(INSTALLDIR)
logs:
tail -f $(LOGDIR)/cpu.log

118
examples/cpu/main.go Normal file
View File

@ -0,0 +1,118 @@
package main
import (
"context"
"fmt"
"image"
"image/color"
"log"
"os"
"time"
"github.com/samwho/streamdeck/payload"
"github.com/samwho/streamdeck"
sdcontext "github.com/samwho/streamdeck/context"
"github.com/shirou/gopsutil/cpu"
)
const (
logFile = "C:\\Users\\samwh\\AppData\\Roaming\\Elgato\\StreamDeck\\logs\\cpu.log"
imgX = 72
imgY = 72
)
func main() {
f, err := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatalf("error opening file: %v", err)
}
defer f.Close()
log.SetOutput(f)
ctx := context.Background()
if err := run(ctx); err != nil {
log.Fatalf("%v\n", err)
}
}
func run(ctx context.Context) error {
params, err := streamdeck.ParseRegistrationParams(os.Args)
if err != nil {
return err
}
client := streamdeck.NewClient(ctx, params)
setup(client)
return client.Run()
}
func setup(client *streamdeck.Client) {
action := client.Action("dev.samwho.streamdeck.cpu")
contexts := make(map[string]struct{})
action.RegisterHandler(streamdeck.WillAppear, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error {
contexts[event.Context] = struct{}{}
return nil
})
action.RegisterHandler(streamdeck.WillDisappear, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error {
delete(contexts, event.Context)
return nil
})
readings := make([]float64, imgX, imgX)
go func() {
for range time.Tick(time.Second) {
for i := 0; i < imgX-1; i++ {
readings[i] = readings[i+1]
}
r, err := cpu.Percent(0, false)
if err != nil {
log.Printf("error getting CPU reading: %v\n", err)
}
readings[imgX-1] = r[0]
for ctxStr := range contexts {
ctx := context.Background()
ctx = sdcontext.WithContext(ctx, ctxStr)
img, err := streamdeck.Image(graph(readings))
if err != nil {
log.Printf("error creating image: %v\n", err)
continue
}
if err := client.SetImage(ctx, img, payload.HardwareAndSoftware); err != nil {
log.Printf("error setting image: %v\n", err)
continue
}
if err := client.SetTitle(ctx, fmt.Sprintf("CPU\n%d%%", int(r[0])), payload.HardwareAndSoftware); err != nil {
log.Printf("error setting title: %v\n", err)
continue
}
}
}
}()
}
func graph(readings []float64) image.Image {
img := image.NewRGBA(image.Rect(0, 0, imgX, imgY))
for x := 0; x < imgX; x++ {
reading := readings[x] / 100
upto := int(float64(imgY) * reading)
for y := 0; y < upto; y++ {
img.Set(x, imgY-y, color.RGBA{R: 255, A: 255})
}
for y := upto; y < imgY; y++ {
img.Set(x, imgY-y, color.Black)
}
}
return img
}

View File

@ -0,0 +1,37 @@
{
"Actions": [
{
"Name": "CPU graph",
"States": [
{
"TitleAlignment": "middle",
"FontSize": "14"
}
],
"SupportedInMultiActions": false,
"Tooltip": "Show a pretty little CPU graph",
"UUID": "dev.samwho.streamdeck.cpu"
}
],
"SDKVersion": 2,
"Author": "Sam Rose",
"CodePath": "cpu.exe",
"Description": "Show a pretty little CPU graph",
"Name": "CPU graph",
"URL": "https://samwho.dev",
"Version": "0.1",
"OS": [
{
"Platform": "mac",
"MinimumVersion" : "10.11"
},
{
"Platform": "windows",
"MinimumVersion" : "10"
}
],
"Software":
{
"MinimumVersion" : "4.1"
}
}

33
image.go Normal file
View File

@ -0,0 +1,33 @@
package streamdeck
import (
"bufio"
"bytes"
"encoding/base64"
"image"
"image/png"
)
func Image(i image.Image) (string, error) {
var b bytes.Buffer
bw := bufio.NewWriter(&b)
if _, err := bw.WriteString("data:image/png;base64,"); err != nil {
return "", err
}
w := base64.NewEncoder(base64.StdEncoding, bw)
if err := png.Encode(w, i); err != nil {
return "", err
}
if err := w.Close(); err != nil {
return "", err
}
if err := bw.Flush(); err != nil {
return "", err
}
return b.String(), nil
}

65
main.go
View File

@ -1,65 +0,0 @@
package main
import (
"context"
"flag"
"log"
"os"
"strconv"
"github.com/samwho/streamdeck-livesplit/streamdeck"
)
const (
logFile = "C:\\Users\\samwh\\AppData\\Roaming\\Elgato\\StreamDeck\\logs\\streamdeck-livesplit.log"
)
var (
port = flag.Int("port", -1, "")
pluginUUID = flag.String("pluginUUID", "", "")
registerEvent = flag.String("registerEvent", "", "")
info = flag.String("info", "", "")
)
func main() {
f, err := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatalf("error opening file: %v", err)
}
defer f.Close()
log.SetOutput(f)
flag.Parse()
params := streamdeck.RegistrationParams{
Port: *port,
PluginUUID: *pluginUUID,
RegisterEvent: *registerEvent,
Info: *info,
}
log.Printf("registration params: %v\n", params)
ctx := context.Background()
if err := run(ctx, params); err != nil {
log.Fatalf("%v\n", err)
}
}
func run(ctx context.Context, params streamdeck.RegistrationParams) error {
client, err := streamdeck.NewClient(ctx, params)
if err != nil {
return err
}
counter := 0
client.RegisterHandler(streamdeck.KeyDown, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error {
counter++
log.Printf("key down! counter: %d\n", counter)
return client.SetTitle(ctx, strconv.Itoa(counter), streamdeck.HardwareAndSoftware)
})
log.Println("waiting for connection to close...")
client.Join()
return nil
}

110
payload/payload.go Normal file
View File

@ -0,0 +1,110 @@
package payload
import (
"encoding/json"
)
type Target int
const (
HardwareAndSoftware Target = 0
OnlyHardware Target = 1
OnlySoftware Target = 2
)
type LogMessage struct {
Message string `json:"message"`
}
type OpenURL struct {
URL string `json:"url"`
}
type SetTitle struct {
Title string `json:"title"`
Target Target `json:"target"`
}
type SetImage struct {
Base64Image string `json:"image"`
Target Target `json:"target"`
}
type SetState struct {
State int `json:"state"`
}
type SwitchProfile struct {
Profile string `json:"profile"`
}
type DidReceiveSettings struct {
Settings json.RawMessage `json:"settings,omitempty"`
Coordinates Coordinates `json:"coordinates,omitempty"`
IsInMultiAction bool `json:"isInMultiAction,omitempty"`
}
type Coordinates struct {
Column int `json:"column,omitempty"`
Row int `json:"row,omitempty"`
}
type DidReceiveGlobalSettings struct {
Settings json.RawMessage `json:"settings,omitempty"`
}
type KeyDown struct {
Settings json.RawMessage `json:"settings,omitempty"`
Coordinates Coordinates `json:"coordinates,omitempty"`
State int `json:"state,omitempty"`
UserDesiredState int `json:"userDesiredState,omitempty"`
IsInMultiAction bool `json:"isInMultiAction,omitempty"`
}
type KeyUp struct {
Settings json.RawMessage `json:"settings,omitempty"`
Coordinates Coordinates `json:"coordinates,omitempty"`
State int `json:"state,omitempty"`
UserDesiredState int `json:"userDesiredState,omitempty"`
IsInMultiAction bool `json:"isInMultiAction,omitempty"`
}
type WillAppear struct {
Settings json.RawMessage `json:"settings,omitempty"`
Coordinates Coordinates `json:"coordinates,omitempty"`
State int `json:"state,omitempty"`
IsInMultiAction bool `json:"isInMultiAction,omitempty"`
}
type WillDisappear struct {
Settings json.RawMessage `json:"settings,omitempty"`
Coordinates Coordinates `json:"coordinates,omitempty"`
State int `json:"state,omitempty"`
IsInMultiAction bool `json:"isInMultiAction,omitempty"`
}
type TitleParametersDidChange struct {
Settings json.RawMessage `json:"settings,omitempty"`
Coordinates Coordinates `json:"coordinates,omitempty"`
State int `json:"state,omitempty"`
Title string `json:"title,omitempty"`
TitleParameters TitleParameters `json:"titleParameters,omitempty"`
}
type TitleParameters struct {
FontFamily string `json:"fontFamily,omitempty"`
FontSize int `json:"fontSize,omitempty"`
FontStyle string `json:"fontStyle,omitempty"`
FontUnderline bool `json:"fontUnderline,omitempty"`
ShowTitle bool `json:"showTitle,omitempty"`
TitleAlignment string `json:"titleAlignment,omitempty"`
TitleColor string `json:"titleColor,omitempty"`
}
type ApplicationDidLaunch struct {
Application string `json:"application,omitempty"`
}
type ApplicationDidTerminate struct {
Application string `json:"application,omitempty"`
}

46
registration.go Normal file
View File

@ -0,0 +1,46 @@
package streamdeck
import (
"flag"
"fmt"
)
type RegistrationParams struct {
Port int
PluginUUID string
RegisterEvent string
Info string
}
func ParseRegistrationParams(args []string) (RegistrationParams, error) {
f := flag.NewFlagSet("registration_params", flag.ContinueOnError)
port := f.Int("port", -1, "")
pluginUUID := f.String("pluginUUID", "", "")
registerEvent := f.String("registerEvent", "", "")
info := f.String("info", "", "")
if err := f.Parse(args[1:]); err != nil {
return RegistrationParams{}, err
}
if *port == -1 {
return RegistrationParams{}, fmt.Errorf("missing -port flag")
}
if *pluginUUID == "" {
return RegistrationParams{}, fmt.Errorf("missing -pluginUUID flag")
}
if *registerEvent == "" {
return RegistrationParams{}, fmt.Errorf("missing -registerEvent flag")
}
if *info == "" {
return RegistrationParams{}, fmt.Errorf("missing -info flag")
}
return RegistrationParams{
Port: *port,
PluginUUID: *pluginUUID,
RegisterEvent: *registerEvent,
Info: *info,
}, nil
}

View File

@ -1,155 +0,0 @@
package streamdeck
import (
"context"
"encoding/json"
"fmt"
"log"
"net/url"
"os"
"os/signal"
"time"
"github.com/gorilla/websocket"
)
type EventHandler func(ctx context.Context, client *Client, event Event) error
type Client struct {
c *websocket.Conn
handlers map[string][]EventHandler
done chan struct{}
}
func NewClient(ctx context.Context, params RegistrationParams) (*Client, error) {
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
u := url.URL{Scheme: "ws", Host: fmt.Sprintf("127.0.0.1:%d", params.Port)}
log.Printf("connecting to StreamDeck at %v\n", u)
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
return nil, err
}
log.Printf("connected to StreamDeck\n")
done := make(chan struct{})
client := &Client{
c: c,
handlers: make(map[string][]EventHandler),
done: done,
}
go func() {
defer close(done)
log.Println("starting read loop")
for {
messageType, message, err := client.c.ReadMessage()
if err != nil {
log.Printf("read error: %v\n", err)
return
}
if messageType == websocket.PingMessage {
log.Printf("received ping message\n")
if err := client.c.WriteMessage(websocket.PongMessage, []byte{}); err != nil {
log.Printf("error while ponging: %v\n", err)
}
continue
}
if messageType == websocket.CloseMessage {
// handle close message
panic("websocket close!")
}
event := Event{}
if err := json.Unmarshal(message, &event); err != nil {
log.Printf("failed to unmarshal received event: %s\n", string(message))
continue
}
log.Println("recv: ", string(message))
ctx := setContext(ctx, event.Context)
for _, f := range client.handlers[event.Event] {
if err := f(ctx, client, event); err != nil {
log.Printf("error in handler for event %v: %v\n", event.Event, err)
}
}
}
}()
if err := client.register(params); err != nil {
return nil, err
}
return client, nil
}
func (client *Client) register(params RegistrationParams) error {
log.Println("sending register event...")
if err := client.send(Event{UUID: params.PluginUUID, Event: params.RegisterEvent}); err != nil {
client.Close()
return err
}
return nil
}
func (client *Client) send(event Event) error {
j, _ := json.Marshal(event)
log.Printf("sending message: %v\n", string(j))
return client.c.WriteJSON(event)
}
func (client *Client) SetSettings(ctx context.Context, settings interface{}) error {
return client.send(NewEvent(ctx, SetSettings, settings))
}
func (client *Client) GetSettings(ctx context.Context) error {
return client.send(NewEvent(ctx, GetSettings, nil))
}
func (client *Client) SetGlobalSettings(ctx context.Context, settings interface{}) error {
return client.send(NewEvent(ctx, SetGlobalSettings, settings))
}
func (client *Client) GetGlobalSettings(ctx context.Context) error {
return client.send(NewEvent(ctx, GetGlobalSettings, nil))
}
func (client *Client) OpenURL(ctx context.Context, u url.URL) error {
return client.send(NewEvent(ctx, OpenURL, OpenURLPayload{URL: u.String()}))
}
func (client *Client) LogMessage(message string) error {
return client.send(NewEvent(nil, LogMessage, LogMessagePayload{Message: message}))
}
func (client *Client) SetTitle(ctx context.Context, title string, target Target) error {
return client.send(NewEvent(ctx, SetTitle, SetTitlePayload{Title: title, Target: target}))
}
func (client *Client) SetImage(ctx context.Context, base64image string, target Target) error {
return client.send(NewEvent(ctx, SetImage, SetImagePayload{Base64Image: base64image, Target: target}))
}
func (client *Client) RegisterHandler(eventName string, handler EventHandler) {
client.handlers[eventName] = append(client.handlers[eventName], handler)
}
func (client *Client) Close() error {
err := client.c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
if err != nil {
return err
}
select {
case <-client.done:
case <-time.After(time.Second):
}
return client.c.Close()
}
func (client *Client) Join() {
<-client.done
}

View File

@ -1,48 +0,0 @@
package streamdeck
import (
"context"
)
type Event struct {
Action string `json:"action,omitempty"`
Event string `json:"event,omitempty"`
UUID string `json:"uuid,omitempty"`
Context string `json:"context,omitempty"`
Device string `json:"device,omitempty"`
Payload interface{} `json:"payload,omitempty"`
}
func NewEvent(ctx context.Context, name string, payload interface{}) Event {
return Event{
Event: name,
Context: getContext(ctx),
Payload: payload,
}
}
type LogMessagePayload struct {
Message string `json:"message"`
}
type OpenURLPayload struct {
URL string `json:"url"`
}
type Target int
const (
HardwareAndSoftware Target = 0
OnlyHardware Target = 1
OnlySoftware Target = 2
)
type SetTitlePayload struct {
Title string `json:"title"`
Target Target `json:"target"`
}
type SetImagePayload struct {
Base64Image string `json:"image"`
Target Target `json:"target"`
}

View File

@ -1,8 +0,0 @@
package streamdeck
type RegistrationParams struct {
Port int
PluginUUID string
RegisterEvent string
Info string
}