diff --git a/src/.vscode/launch.json b/src/.vscode/launch.json new file mode 100644 index 0000000..4c54285 --- /dev/null +++ b/src/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to Process", + "type": "go", + "request": "attach", + "mode": "local", + "processId": "${command:pickGoProcess}" + } + ] +} \ No newline at end of file diff --git a/src/DistributionTool b/src/DistributionTool new file mode 100755 index 0000000..36eac52 Binary files /dev/null and b/src/DistributionTool differ diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..042538f --- /dev/null +++ b/src/Makefile @@ -0,0 +1,45 @@ +INSTALLDIR = "~/Library/Application\ Support/com.elgato.StreamDeck/Plugins/com.dlprows.macvolumecontrol.sdPlugin" +#BUILDDIR = build +#RELEASEDIR = release +#SDPLUGINDIR = "./com.dlprows.macvolumecontrol.sdPlugin" + +update: + killall Stream\ Deck.app || true + go build -o $(INSTALLDIR) . + open -a Elgato\ Stream\ Deck + +build: + go build -o com.dlprows.macvolumecontrol.sdPlugin + rm com.dlprows.macvolumecontrol.streamDeckPlugin + ./DistributionTool -b -i com.dlprows.macvolumecontrol.sdPlugin -o . + + +#.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\":\"windows\",\"version\":\"10\"},\"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}]}" +# +#sdplugin: build +#rm -rf $(SDPLUGINDIR) +#mkdir -p $(SDPLUGINDIR) +#cp *.json $(SDPLUGINDIR) +#cp *.exe $(SDPLUGINDIR) +#cp *.html $(SDPLUGINDIR) +#cp -r images $(SDPLUGINDIR) +# +#install: uninstall sdplugin +#mv $(SDPLUGINDIR) $(INSTALLDIR) +# +#uninstall: +#rm -rf $(INSTALLDIR) +# +#logs: +#tail -f "$(TMP)"/$(MAKEFILEDIR).log* +# +#release: sdplugin +#rm -rf $(RELEASEDIR) +#mkdir $(RELEASEDIR) +#DistributionTool -b -i $(SDPLUGINDIR) -o $(RELEASEDIR) \ No newline at end of file diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..96aa94e --- /dev/null +++ b/src/README.md @@ -0,0 +1,41 @@ +# StreamDeck plugin skeleton + +This is a skeleton project for getting started making StreamDeck plugins using the [Golang client library][1]. + +## Usage + +Clone this repo and copy the directory to somewhere appropriate in your `GOPATH`. From there, most things are done +using the supplied Makefile. + +Build: + + make build + +This outputs a build binary in to the project directory but doesn't do much else. To test out the plugin with your +StreamDeck, you need to do a local install. + +Install: + + make install + +This builds an `.sdPlugin` directory under the `build` directory and then copies it over to the StreamDeck plugin +directory. **Note: you'll need to close the StreamDeck software before doing this. If you don't, you'll get a device +or resource busy error.** + +View logs: + + make logs + +If you've enabled logging, logs will get dumped in to a temporary file. This make command tails all log files and needs to +be run after you've fired up the StreamDeck software, as the temporary file won't have been created before then. + +Build for release: + + make release + +To distribute your StreamDeck plugin, you need to use Elgato's [DistributionTool][2]. Download it, put it somewhere on your +path, and then `make release` should work. If you have any problems in your `manifest.json`, this tool will ask you to fix +them before it outputs a release-ready plugin. + +[1]: https://code.encyclopediaofdaniel.com/dlprows/streamdeck-sdk +[2]: https://developer.elgato.com/documentation/stream-deck/sdk/exporting-your-plugin/ \ No newline at end of file diff --git a/src/com.dlprows.macvolumecontrol.sdPlugin/images/actionIcon.png b/src/com.dlprows.macvolumecontrol.sdPlugin/images/actionIcon.png new file mode 100644 index 0000000..4e6c1b1 Binary files /dev/null and b/src/com.dlprows.macvolumecontrol.sdPlugin/images/actionIcon.png differ diff --git a/src/com.dlprows.macvolumecontrol.sdPlugin/images/actionIcon@2x.png b/src/com.dlprows.macvolumecontrol.sdPlugin/images/actionIcon@2x.png new file mode 100644 index 0000000..f3eafba Binary files /dev/null and b/src/com.dlprows.macvolumecontrol.sdPlugin/images/actionIcon@2x.png differ diff --git a/src/com.dlprows.macvolumecontrol.sdPlugin/images/categoryIcon.png b/src/com.dlprows.macvolumecontrol.sdPlugin/images/categoryIcon.png new file mode 100644 index 0000000..fdbedae Binary files /dev/null and b/src/com.dlprows.macvolumecontrol.sdPlugin/images/categoryIcon.png differ diff --git a/src/com.dlprows.macvolumecontrol.sdPlugin/images/categoryIcon@2x.png b/src/com.dlprows.macvolumecontrol.sdPlugin/images/categoryIcon@2x.png new file mode 100644 index 0000000..db36b8f Binary files /dev/null and b/src/com.dlprows.macvolumecontrol.sdPlugin/images/categoryIcon@2x.png differ diff --git a/src/com.dlprows.macvolumecontrol.sdPlugin/images/download (12).png b/src/com.dlprows.macvolumecontrol.sdPlugin/images/download (12).png new file mode 100644 index 0000000..35d74f2 Binary files /dev/null and b/src/com.dlprows.macvolumecontrol.sdPlugin/images/download (12).png differ diff --git a/src/com.dlprows.macvolumecontrol.sdPlugin/images/icon.png b/src/com.dlprows.macvolumecontrol.sdPlugin/images/icon.png new file mode 100644 index 0000000..9c9c3e6 Binary files /dev/null and b/src/com.dlprows.macvolumecontrol.sdPlugin/images/icon.png differ diff --git a/src/com.dlprows.macvolumecontrol.sdPlugin/images/icon@2x.png b/src/com.dlprows.macvolumecontrol.sdPlugin/images/icon@2x.png new file mode 100644 index 0000000..9b29711 Binary files /dev/null and b/src/com.dlprows.macvolumecontrol.sdPlugin/images/icon@2x.png differ diff --git a/src/com.dlprows.macvolumecontrol.sdPlugin/images/pluginAction.png b/src/com.dlprows.macvolumecontrol.sdPlugin/images/pluginAction.png new file mode 100644 index 0000000..e880664 Binary files /dev/null and b/src/com.dlprows.macvolumecontrol.sdPlugin/images/pluginAction.png differ diff --git a/src/com.dlprows.macvolumecontrol.sdPlugin/images/pluginAction@2x.png b/src/com.dlprows.macvolumecontrol.sdPlugin/images/pluginAction@2x.png new file mode 100644 index 0000000..9ec21b9 Binary files /dev/null and b/src/com.dlprows.macvolumecontrol.sdPlugin/images/pluginAction@2x.png differ diff --git a/src/com.dlprows.macvolumecontrol.sdPlugin/images/pluginIcon.png b/src/com.dlprows.macvolumecontrol.sdPlugin/images/pluginIcon.png new file mode 100644 index 0000000..3ddc56d Binary files /dev/null and b/src/com.dlprows.macvolumecontrol.sdPlugin/images/pluginIcon.png differ diff --git a/src/com.dlprows.macvolumecontrol.sdPlugin/images/pluginIcon@2x.png b/src/com.dlprows.macvolumecontrol.sdPlugin/images/pluginIcon@2x.png new file mode 100644 index 0000000..bef4449 Binary files /dev/null and b/src/com.dlprows.macvolumecontrol.sdPlugin/images/pluginIcon@2x.png differ diff --git a/src/com.dlprows.macvolumecontrol.sdPlugin/manifest.json b/src/com.dlprows.macvolumecontrol.sdPlugin/manifest.json new file mode 100644 index 0000000..88261ed --- /dev/null +++ b/src/com.dlprows.macvolumecontrol.sdPlugin/manifest.json @@ -0,0 +1,49 @@ +{ + "Actions": [ + { + "UUID": "com.dlprows.macvolumecontrol.dialaction", + "Name": "Volume", + "Tooltip": "Control system volume", + "Icon": "Images/icon", + "States": [ + { + "Image": "Images/pluginAction", + "TitleAlignment": "middle", + "FontSize": "12" + } + ], + "Controllers": [ "Encoder" ], + "Encoder": { + "background": "backgroundImage", + "Icon": "Images/actionIcon", + "layout": "$B1", + "StackColor": "#AABBCC", + "TriggerDescription": { + "Rotate": "System Volume", + "Push": "Mute", + "Touch": "Mute" + } + }, + "SupportedInMultiActions": false + } + ], + "Author": "dlprows", + "Name": "MacVolumeControl", + "Description": "Control volume on mac", + "URL": "https://encyclopediaofdaniel.com", + "Version": "1.0", + "CodePath": "macvolumecontrol", + "Category": "Volume Control [dlprows]", + "Icon": "Images/pluginIcon", + "CategoryIcon": "Images/categoryIcon", + "OS": [ + { + "Platform": "mac", + "MinimumVersion": "13.0" + } + ], + "SDKVersion": 2, + "Software": { + "MinimumVersion": "6.0" + } +} \ No newline at end of file diff --git a/src/go.mod b/src/go.mod new file mode 100644 index 0000000..6c7d0b6 --- /dev/null +++ b/src/go.mod @@ -0,0 +1,7 @@ +module macvolumecontrol + +go 1.20 + +require code.encyclopediaofdaniel.com/dlprows/streamdeck-sdk v1.0.1 + +require github.com/gorilla/websocket v1.5.0 // indirect diff --git a/src/go.sum b/src/go.sum new file mode 100644 index 0000000..0d31b81 --- /dev/null +++ b/src/go.sum @@ -0,0 +1,6 @@ +code.encyclopediaofdaniel.com/dlprows/streamdeck-sdk v1.0.1 h1:2IoDJrWt6Bp5IcXTtqjdygDo3Ruh5VHsS9R5MjI2WWQ= +code.encyclopediaofdaniel.com/dlprows/streamdeck-sdk v1.0.1/go.mod h1:SaMt3PIkPtNwkgrcLpD89GK/7tLaUkcEvOZaHUC66+4= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/samwho/streamdeck v0.0.0-20190725183037-2b866fdcb4a6 h1:ocHB+wIjLCdideequ8l8dUem6DxHf/A4Mmo0TRxBf1U= +github.com/samwho/streamdeck v0.0.0-20190725183037-2b866fdcb4a6/go.mod h1:OjeKL1Q8xRGm1zHGmtu9JeQUFNOXXXIcSZvCNzRP3GM= diff --git a/src/keyboard/keyboard.go b/src/keyboard/keyboard.go new file mode 100644 index 0000000..e12951d --- /dev/null +++ b/src/keyboard/keyboard.go @@ -0,0 +1,43 @@ +package keyboard + +/* + #cgo CFLAGS: -x objective-c + #cgo LDFLAGS: -framework AppKit + #import +NSEvent* createEvent(int k, bool down) { + int flags = down ? 0xa00 : 0xb00; + NSEvent* event = [NSEvent otherEventWithType:NSEventTypeSystemDefined + location:NSZeroPoint + modifierFlags:flags + timestamp:0 + windowNumber:0 + context:nil + subtype:8 + data1:(k << 16 | flags) + data2:-1 + ]; + return event; +} + +void pressKey(int k) { + @autoreleasepool { + NSEvent* down = createEvent(k, true); + NSEvent* up = createEvent(k, false); + + CGEventPost(kCGHIDEventTap, down.CGEvent); + usleep(1000); + CGEventPost(kCGHIDEventTap, up.CGEvent); + } +} +*/ +import "C" + +func PressMediaKey(key int) { + C.pressKey(C.int(key)) +} + +const ( + VOLUMEUP = 0 + VOLUMEDOWN = 1 + MUTE = 7 +) diff --git a/src/logging/logging.go b/src/logging/logging.go new file mode 100644 index 0000000..764fd0d --- /dev/null +++ b/src/logging/logging.go @@ -0,0 +1,31 @@ +package logging + +import ( + "fmt" + "log" + "os" + "path/filepath" + + "code.encyclopediaofdaniel.com/dlprows/streamdeck-sdk" +) + +func directory() string { + ex, err := os.Executable() + if err != nil { + log.Fatalf("error getting directory: %v", err) + } + return filepath.Base(filepath.Dir(ex)) +} + +// Enable turns logging on for the StreamDeck API client as well as the global +// log object. It sends both to a temp file that contains .log. +func Enable() { + d := directory() + f, err := os.CreateTemp("./", fmt.Sprintf("%s.log", d)) + if err != nil { + log.Fatalf("error creating temp file: %v", err) + } + streamdeck.Log().SetOutput(f) + log.SetPrefix(fmt.Sprintf("%s ", d)) + log.SetOutput(f) +} diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..10892a0 --- /dev/null +++ b/src/main.go @@ -0,0 +1,128 @@ +package main + +import ( + "context" + "encoding/json" + "log" + "os" + "time" + + "macvolumecontrol/keyboard" + "macvolumecontrol/logging" + + "code.encyclopediaofdaniel.com/dlprows/streamdeck-sdk" +) + +func main() { + logging.Enable() + log.Println("Starting plugin") + + params, err := streamdeck.ParseRegistrationParams(os.Args) + if err != nil { + log.Fatalf("error parsing registration params: %v", err) + } + + sd := streamdeck.NewClient(context.Background(), params) + defer sd.Close() + + setup(sd) + + if err := sd.Run(); err != nil { + log.Fatalf("error running streamdeck client: %v\n", err) + } +} + +func setup(client *streamdeck.Client) { + log.Println("Registering actions") + action := client.Action("com.dlprows.macvolumecontrol.dialaction") + + debounce := time.Now() + contexts := make(map[string]struct{}) + + action.RegisterHandler(streamdeck.DialRotate, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error { + log.Println("dial rotate") + + t := time.Now() + elapsed := t.Sub(debounce) + + if elapsed.Milliseconds() < 50 { + log.Println("dial rotate skipped") + return nil + } + debounce = t + + p := streamdeck.DialRotatePayload[any]{} + + if err := json.Unmarshal(event.Payload, &p); err != nil { + return err + } + + if p.Ticks > 0 { + keyboard.PressMediaKey(keyboard.VOLUMEUP) + } else { + keyboard.PressMediaKey(keyboard.VOLUMEDOWN) + } + + return nil + }) + + action.RegisterHandler(streamdeck.DialDown, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error { + log.Println("dial down") + keyboard.PressMediaKey(keyboard.MUTE) + return nil + }) + + action.RegisterHandler(streamdeck.WillAppear, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error { + log.Println("Will Appear") + contexts[event.Context] = struct{}{} + return nil + }) + + action.RegisterHandler(streamdeck.WillDisappear, func(ctx context.Context, client *streamdeck.Client, event streamdeck.Event) error { + log.Println("Will Disappear") + delete(contexts, event.Context) + return nil + }) + + //start background thread to keep the display up to date if changed outside the stream deck + //go func() { + + //}() +} + +/* +osascript -e 'output volume of (get volume settings)' +osascript -e 'set volume output volume 50' +osascript -e 'output muted of (get volume settings)' +osascript -e 'set volume output muted true' +*/ + +/* +for ctxStr := range contexts { +//for each context +//build a new context that can be used to perform outbound requests + 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, streamdeck.HardwareAndSoftware); err != nil { + log.Printf("error setting image: %v\n", err) + continue + } + + title := "" + if pi.ShowText { + title = fmt.Sprintf("CPU\n%d%%", int(r[0])) + } + + if err := client.SetTitle(ctx, title, streamdeck.HardwareAndSoftware); err != nil { + log.Printf("error setting title: %v\n", err) + continue + } + } +*/