diff --git a/action.go b/action.go new file mode 100644 index 0000000..af3b177 --- /dev/null +++ b/action.go @@ -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) +} diff --git a/actionDefaultImage.png b/actionDefaultImage.png deleted file mode 100644 index b558c11..0000000 Binary files a/actionDefaultImage.png and /dev/null differ diff --git a/actionIcon.png b/actionIcon.png deleted file mode 100644 index baf95bb..0000000 Binary files a/actionIcon.png and /dev/null differ diff --git a/client.go b/client.go new file mode 100644 index 0000000..de4da73 --- /dev/null +++ b/client.go @@ -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() +} diff --git a/streamdeck/constants.go b/constants.go similarity index 78% rename from streamdeck/constants.go rename to constants.go index 4517338..c2018b2 100644 --- a/streamdeck/constants.go +++ b/constants.go @@ -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) -} diff --git a/context/context.go b/context/context.go new file mode 100644 index 0000000..d175192 --- /dev/null +++ b/context/context.go @@ -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 +} diff --git a/event.go b/event.go new file mode 100644 index 0000000..23a77ec --- /dev/null +++ b/event.go @@ -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, + } +} diff --git a/Makefile b/examples/counter/Makefile similarity index 86% rename from Makefile rename to examples/counter/Makefile index 48a6794..69c5d42 100644 --- a/Makefile +++ b/examples/counter/Makefile @@ -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 \ No newline at end of file + tail -f $(LOGDIR)/counter.log \ No newline at end of file diff --git a/examples/counter/main.go b/examples/counter/main.go new file mode 100644 index 0000000..9e187d7 --- /dev/null +++ b/examples/counter/main.go @@ -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 +} diff --git a/manifest.json b/examples/counter/manifest.json similarity index 60% rename from manifest.json rename to examples/counter/manifest.json index bb40291..46ffde5 100644 --- a/manifest.json +++ b/examples/counter/manifest.json @@ -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": [ diff --git a/examples/cpu/Makefile b/examples/cpu/Makefile new file mode 100644 index 0000000..aa7fb52 --- /dev/null +++ b/examples/cpu/Makefile @@ -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 \ No newline at end of file diff --git a/examples/cpu/main.go b/examples/cpu/main.go new file mode 100644 index 0000000..37d7e10 --- /dev/null +++ b/examples/cpu/main.go @@ -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 +} diff --git a/examples/cpu/manifest.json b/examples/cpu/manifest.json new file mode 100644 index 0000000..68733b9 --- /dev/null +++ b/examples/cpu/manifest.json @@ -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" + } +} \ No newline at end of file diff --git a/image.go b/image.go new file mode 100644 index 0000000..c98e084 --- /dev/null +++ b/image.go @@ -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 +} diff --git a/main.go b/main.go deleted file mode 100644 index c803f1f..0000000 --- a/main.go +++ /dev/null @@ -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 -} diff --git a/payload/payload.go b/payload/payload.go new file mode 100644 index 0000000..df3daf0 --- /dev/null +++ b/payload/payload.go @@ -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"` +} diff --git a/registration.go b/registration.go new file mode 100644 index 0000000..9877cc0 --- /dev/null +++ b/registration.go @@ -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 +} diff --git a/streamdeck/client.go b/streamdeck/client.go deleted file mode 100644 index 76be362..0000000 --- a/streamdeck/client.go +++ /dev/null @@ -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 -} diff --git a/streamdeck/messages.go b/streamdeck/messages.go deleted file mode 100644 index 18a50cd..0000000 --- a/streamdeck/messages.go +++ /dev/null @@ -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"` -} diff --git a/streamdeck/registration.go b/streamdeck/registration.go deleted file mode 100644 index f2fa646..0000000 --- a/streamdeck/registration.go +++ /dev/null @@ -1,8 +0,0 @@ -package streamdeck - -type RegistrationParams struct { - Port int - PluginUUID string - RegisterEvent string - Info string -}