Major restructure and added a few example plugins.
This commit is contained in:
parent
cad454a4bf
commit
8ee19c457a
10
action.go
Normal file
10
action.go
Normal 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 |
BIN
actionIcon.png
BIN
actionIcon.png
Binary file not shown.
Before Width: | Height: | Size: 960 B |
205
client.go
Normal file
205
client.go
Normal 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()
|
||||
}
|
@ -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
55
context/context.go
Normal 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
53
event.go
Normal 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,
|
||||
}
|
||||
}
|
@ -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
112
examples/counter/main.go
Normal 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
|
||||
}
|
@ -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
21
examples/cpu/Makefile
Normal 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
118
examples/cpu/main.go
Normal 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
|
||||
}
|
37
examples/cpu/manifest.json
Normal file
37
examples/cpu/manifest.json
Normal 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
33
image.go
Normal 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
65
main.go
@ -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
110
payload/payload.go
Normal 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
46
registration.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
@ -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"`
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package streamdeck
|
||||
|
||||
type RegistrationParams struct {
|
||||
Port int
|
||||
PluginUUID string
|
||||
RegisterEvent string
|
||||
Info string
|
||||
}
|
Loading…
Reference in New Issue
Block a user