diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..742bd2c --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2020, unix-streamdeck +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index e10cafb..73901e4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ -# Streamdeckd +# Streamdeckd + +### Installation + +- create the file `/etc/udev/rules.d/50-elgato.rules` with the following config -### Installation - -- create the file `/etc/udev/rules.d/50-elgato.rules` with the following config ``` SUBSYSTEM=="input", GROUP="input", MODE="0666" SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0060", MODE:="666", GROUP="plugdev" @@ -10,42 +11,60 @@ SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0063", MODE:="666" SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="006c", MODE:="666", GROUP="plugdev" SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="006d", MODE:="666", GROUP="plugdev" ``` - -- run `sudo udevadm control --reload-rules` to reload the udev rules - -Then xdotool will be required to simulate keypresses, to install this run: - -#### Arch - -`sudo pacman -S xdotool` - -#### Debian based - -`sudo apt install xdotool` +- run `sudo udevadm control --reload-rules` to reload the udev rules + +Then xdotool will be required to simulate keypresses, to install this run: + +#### Arch + +`sudo pacman -S xdotool` + +#### Debian based + +`sudo apt install xdotool` ### Configuration #### Manual configuration -The configuration file streamdeckd uses is a JSON file found at `~/.streamdeck-config.json` +## Warning: + +If you are updating from v1.0.0, the config file is now being set in the location as below, instead of where it used to be, in the home dir, either consider moving the config file to that dir, or running streamdeckd with the `-config` flag, which allows you to point to a config file in a custom location + +--- + + +The configuration file streamdeckd uses is a JSON file found at `$XDG_CONFIG_HOME/.streamdeck-config.json` An example config would be something like: ```json { - "pages": [ - [ - { - "switch_page": 1, - "icon": "~/icon.png" - } - ] + "modules": [ + "/home/user/module.so" + ], + "decks": [ + { + "serial": "AB12C3D45678", + "pages": [ + [ + { + "switch_page": 1, + "icon": "~/icon.png" + } + ] + ] + } ] } ``` -The outer array is the list of pages, the inner array is the list of button on that page, with the buttons going in a right to left order. +At the top is the list of custom modules, these are go plugins in the .so format, following that is the list of deck +objects, each represents a different streamdeck device, and contains its serial, and its list of pages + +The outer array in a deck is the list of pages, the inner array is the list of button on that page, with the buttons +going in a right to left order. The actions you can have on a button are: @@ -54,3 +73,78 @@ The actions you can have on a button are: - `url`: opens a url in your default browser via xdg - `brightness`: set the brightness of the streamdeck as a percentage - `switch_page`: change the active page on the streamdeck + +### D-Bus + +There is a D-Bus interface built into the daemon, the service name and interface for D-Bus +are `com.unixstreamdeck.streamdeckd` and `com/unixstreamdeck/streamdeckd` respectively, and is made up of the following +methods/signals + +#### Methods + +- GetConfig - returns the current running config +- SetConfig - sets the config, without saving to disk, takes in Stringified json, returns an error if anything breaks +- ReloadConfig - reloads the config from disk +- GetDeckInfo - Returns information about all the active streamdecks in the format of + +```json +[ + { + "icon_size": 72, + "rows": 3, + "cols": 5, + "page": 0, + "serial": "AB12C3D45678" + } +] +``` + +- SetPage - Set the page on the streamdeck to the number passed to it, returns an error if anything breaks +- CommitConfig - Commits the currently active config to disk, returns an error if anything breaks +- GetModules - Get the list of loaded modules, and the config fields those modules use +- PressButton - Simulates a button press on the streamdeck device, consumes a device serial, and a key index + + +#### Signals + +- Page - sends the number of the page switched to on the StreamDeck + +### Custom Modules + +To create custom modules, I suggest looking at the gif, counter, and time modules in the example handlers package in streamdeckd, they should be in a file with the GetModule method as shown below + +#### Loading Modules into streamdeckd + +Modules require a method on them in the main package called "GetModule" that returns an instance of [handler.Module](https://github.com/unix-streamdeck/streamdeckd/blob/575e672c26f275d35a016be6406ceb8480ccfff5/handlers/handlers.go#L9) e.g + +```go +package main + +type CustomIconHandler struct { + +} +... + +type CustomKeyHandler struct { + +} +... + +func GetModule() handlers.Module { + return handlers.Module{ + Name: "CustomModule", // the name that will be used in the icon_handler/key_handler field in the config, and that will be shown in the handler dropdown in streamdeckui + NewIcon: func() api.IconHandler { return &CustomerIconHandler{}}, // Method to create a new instance of the Icon handler, if left empty, streamdeckui will not include it in the icon handler fields + NewKey: func() api.KeyHandler { return &CustomerKeyHandler{}}, // Method to create a new instance of the Key Handler, if left empty, streamdeckui will not include it in the key handler fields + IconFields: []api.Field{ // list of fields to be shown in streamdeckui when the icon handler is selected + { + Title: "Icon", // name of field to show in UI + Name: "icon", // name of field that will be included in the iconHandlerFields map + Type: "File" // type of input to show on streamdeckui, options are Text, File, TextAlignment, and Number + FileTypes: []string{".png", ".jpg"} // Allowed file types if a File input type is used + } + }, + KeyFields: []api.Field{}, // Same as IconFields + } +} + +``` \ No newline at end of file diff --git a/config.go b/config.go deleted file mode 100644 index d2b9b7c..0000000 --- a/config.go +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import "image" - -type Page []Key - -type Config struct { - Pages []Page `json:"pages"` -} - -type Key struct { - Icon string `json:"icon,omitempty"` - SwitchPage *int `json:"switch_page,omitempty"` - Text string `json:"text,omitempty"` - Keybind string `json:"keybind,omitempty"` - Command string `json:"command,omitempty"` - Brightness *int `json:"brightness,omitempty"` - Url string `json:"url,omitempty"` - buff image.Image -} diff --git a/dbus.go b/dbus.go new file mode 100644 index 0000000..072ee14 --- /dev/null +++ b/dbus.go @@ -0,0 +1,124 @@ +package main + +import ( + "encoding/json" + "errors" + "github.com/godbus/dbus/v5" + "github.com/unix-streamdeck/api" + "github.com/unix-streamdeck/streamdeckd/handlers" + "log" +) + +var conn *dbus.Conn + +var sDbus *StreamDeckDBus +var sDInfo []api.StreamDeckInfo + +type StreamDeckDBus struct { +} + +func (s StreamDeckDBus) GetDeckInfo() (string, *dbus.Error) { + infoString, err := json.Marshal(sDInfo) + if err != nil { + return "", dbus.MakeFailedError(err) + } + return string(infoString), nil +} + +func (StreamDeckDBus) GetConfig() (string, *dbus.Error) { + configString, err := json.Marshal(config) + if err != nil { + return "", dbus.MakeFailedError(err) + } + return string(configString), nil +} + +func (StreamDeckDBus) ReloadConfig() *dbus.Error { + err := ReloadConfig() + if err != nil { + return dbus.MakeFailedError(err) + } + return nil +} + +func (StreamDeckDBus) SetPage(serial string, page int) *dbus.Error { + for s := range devs { + if devs[s].Deck.Serial == serial { + dev := devs[s] + SetPage(dev, page) + return nil + } + } + return dbus.MakeFailedError(errors.New("Device with Serial: " + serial + " could not be found")) +} + +func (StreamDeckDBus) SetConfig(configString string) *dbus.Error { + err := SetConfig(configString) + if err != nil { + return dbus.MakeFailedError(err) + } + return nil +} + +func (StreamDeckDBus) CommitConfig() *dbus.Error { + err := SaveConfig() + if err != nil { + return dbus.MakeFailedError(err) + } + return nil +} + +func (StreamDeckDBus) GetModules() (string, *dbus.Error) { + var modules []api.Module + for _, module := range handlers.AvailableModules() { + modules = append(modules, api.Module{Name: module.Name, IconFields: module.IconFields, KeyFields: module.KeyFields, IsIcon: module.NewIcon != nil, IsKey: module.NewKey != nil}) + } + modulesString, err := json.Marshal(modules) + if err != nil { + return "", dbus.MakeFailedError(err) + } + return string(modulesString), nil +} + +func (StreamDeckDBus) PressButton(serial string, keyIndex int) *dbus.Error { + dev, ok := devs[serial] + if !ok || !dev.IsOpen{ + return dbus.MakeFailedError(errors.New("Can't find connected device: " + serial)) + } + HandleInput(dev, &dev.Config[dev.Page][keyIndex], dev.Page) + return nil +} + +func InitDBUS() error { + var err error + conn, err = dbus.SessionBus() + if err != nil { + log.Println(err) + return err + } + defer conn.Close() + + sDbus = &StreamDeckDBus{} + conn.ExportAll(sDbus, "/com/unixstreamdeck/streamdeckd", "com.unixstreamdeck.streamdeckd") + reply, err := conn.RequestName("com.unixstreamdeck.streamdeckd", + dbus.NameFlagDoNotQueue) + if err != nil { + log.Println(err) + return err + } + if reply != dbus.RequestNameReplyPrimaryOwner { + return errors.New("DBus: Name already taken") + } + select {} +} + +func EmitPage(dev *VirtualDev, page int) { + if conn != nil { + conn.Emit("/com/unixstreamdeck/streamdeckd", "com.unixstreamdeck.streamdeckd.Page", dev.Deck.Serial, page) + } + for i := range sDInfo { + if sDInfo[i].Serial == dev.Deck.Serial { + sDInfo[i].Page = page + } + } +} diff --git a/go.mod b/go.mod index 69dfb3b..b5f6f98 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,12 @@ -module streamdeckd +module github.com/unix-streamdeck/streamdeckd go 1.14 require ( - github.com/fogleman/gg v1.3.0 - github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect - github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 - github.com/unix-streamdeck/streamdeck-lib v0.0.0-20200720132322-072961fa1fdd - golang.org/x/image v0.0.0-20200119044424-58c23975cae1 + github.com/godbus/dbus/v5 v5.0.4-0.20200513180336-df5ef3eb7cca + github.com/shirou/gopsutil/v3 v3.21.9 + github.com/unix-streamdeck/api v1.0.0 + github.com/unix-streamdeck/driver v0.0.0-20211119182210-fc6b90443bcd + golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect + golang.org/x/sync v0.0.0-20201207232520-09787c993a3a ) diff --git a/go.sum b/go.sum index 6b86c75..83d8343 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -14,6 +16,8 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= @@ -24,7 +28,11 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus/v5 v5.0.4-0.20200513180336-df5ef3eb7cca h1:ewc47M3S8MAZgSO1yEnPrbmHjtQz6caAhYWOQzPHBok= +github.com/godbus/dbus/v5 v5.0.4-0.20200513180336-df5ef3eb7cca/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= @@ -40,8 +48,6 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/h2non/bimg v1.1.2 h1:J75W2eM5FT0KjcwsL2aiy1Ilu0Xy0ENb0sU+HHUJAvw= -github.com/h2non/bimg v1.1.2/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= @@ -52,8 +58,10 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -65,6 +73,7 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= @@ -77,6 +86,8 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil/v3 v3.21.9 h1:Vn4MUz2uXhqLSiCbGFRc0DILbMVLAY92DSkT8bsYrHg= +github.com/shirou/gopsutil/v3 v3.21.9/go.mod h1:YWp/H8Qs5fVmf17v7JNZzA0mPJ+mS2e9JdiUF9LlKzQ= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -87,14 +98,21 @@ github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHN github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tklauser/go-sysconf v0.3.9 h1:JeUVdAOWhhxVcU6Eqr/ATFHgXk/mmiItdKeJPev3vTo= +github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= +github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ= +github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ungerik/go-cairo v0.0.0-20191014050614-4a03f432a432 h1:luu+HbKrZKlIjiaclwrldreEEvVPYX/ujRbTGkKz61c= -github.com/ungerik/go-cairo v0.0.0-20191014050614-4a03f432a432/go.mod h1:0ErpLiOxxE1oY+R4stiKut6/DbUJHnOp6U+e4d8zcTs= -github.com/unix-streamdeck/streamdeck-lib v0.0.0-20200720132322-072961fa1fdd h1:oX6jtZ+/gBcryYn49alkMPbCzqqwyo/7VA17mA4khrQ= -github.com/unix-streamdeck/streamdeck-lib v0.0.0-20200720132322-072961fa1fdd/go.mod h1:EPLAk+jRYZ0zGW7S7bJctr3CLc4bc+tgBrJN7+G1bvc= +github.com/unix-streamdeck/api v1.0.0 h1:GZIslyThiZgcZ/GIR0O/DWnciL6bijSdIA+TSnKzHsI= +github.com/unix-streamdeck/api v1.0.0/go.mod h1:Z8bzDHQnWv/2hx9wQXp0/qw6Fp4ty5pFRsgaBG5WYAI= +github.com/unix-streamdeck/driver v0.0.0-20211119182210-fc6b90443bcd h1:SZleJkNDcxwgKQaoNgpg2Ui2LYFb/feWHFkYj+SLIms= +github.com/unix-streamdeck/driver v0.0.0-20211119182210-fc6b90443bcd/go.mod h1:i3Eg6kJBslgUk2VIPJ3Cclta2fpV1KJrOnOnR8gnVKY= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -103,8 +121,10 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/image v0.0.0-20200119044424-58c23975cae1 h1:5h3ngYt7+vXCDZCup/HkCQgW5XwmSvR/nA2JmJ0RErg= golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200801110659-972c09e46d76/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -116,11 +136,16 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71 h1:ikCpsnYR+Ew0vu99XlDp55lGgDJdIMx3f4a18jfse/s= +golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -132,9 +157,12 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/handlers/examples/counter.go b/handlers/examples/counter.go new file mode 100644 index 0000000..88dcc41 --- /dev/null +++ b/handlers/examples/counter.go @@ -0,0 +1,66 @@ +package examples + +import ( + "github.com/unix-streamdeck/api" + "github.com/unix-streamdeck/streamdeckd/handlers" + "image" + "image/draw" + "log" + "strconv" +) + +type CounterIconHandler struct { + Count int + Running bool + Callback func(image image.Image) +} + +func (c *CounterIconHandler) Start(k api.Key, info api.StreamDeckInfo, callback func(image image.Image)) { + if c.Callback == nil { + c.Callback = callback + } + if c.Running { + img := image.NewRGBA(image.Rect(0, 0, info.IconSize, info.IconSize)) + draw.Draw(img, img.Bounds(), image.Black, image.ZP, draw.Src) + Count := strconv.Itoa(c.Count) + imgParsed, err := api.DrawText(img, Count, k.TextSize, k.TextAlignment) + if err != nil { + log.Println(err) + } else { + callback(imgParsed) + } + } +} + +func (c *CounterIconHandler) IsRunning() bool { + return c.Running +} + +func (c *CounterIconHandler) SetRunning(running bool) { + c.Running = running +} + +func (c CounterIconHandler) Stop() { + c.Running = false +} + +type CounterKeyHandler struct{} + +func (CounterKeyHandler) Key(key api.Key, info api.StreamDeckInfo) { + if key.IconHandler != "Counter" { + return + } + handler := key.IconHandlerStruct.(*CounterIconHandler) + handler.Count += 1 + if handler.Callback != nil { + handler.Start(key, info, handler.Callback) + } +} + +func RegisterCounter() handlers.Module { + return handlers.Module{NewIcon: func() api.IconHandler { + return &CounterIconHandler{Running: true, Count: 0} + }, NewKey: func() api.KeyHandler { + return &CounterKeyHandler{} + }, Name: "Counter"} +} \ No newline at end of file diff --git a/handlers/examples/gif.go b/handlers/examples/gif.go new file mode 100644 index 0000000..fad76e1 --- /dev/null +++ b/handlers/examples/gif.go @@ -0,0 +1,101 @@ +package examples + +import ( + "context" + "github.com/unix-streamdeck/api" + "github.com/unix-streamdeck/streamdeckd/handlers" + "golang.org/x/sync/semaphore" + "image" + "image/gif" + "log" + "os" + "strconv" + "time" +) + +type GifIconHandler struct { + Running bool + Lock *semaphore.Weighted + Quit chan bool +} + +func (s *GifIconHandler) Start(key api.Key, info api.StreamDeckInfo, callback func(image image.Image)) { + if s.Quit == nil { + s.Quit = make(chan bool) + } + if s.Lock == nil { + s.Lock = semaphore.NewWeighted(1) + } + s.Running = true + icon, ok := key.IconHandlerFields["icon"] + if !ok { + return + } + f, err := os.Open(icon) + if err != nil { + log.Println(err) + return + } + gifs, err := gif.DecodeAll(f) + if err != nil { + log.Println(err) + return + } + timeDelay := gifs.Delay[0] + frames := make([]image.Image, len(gifs.Image)) + for i, frame := range gifs.Image { + img := api.ResizeImage(frame, info.IconSize) + if key.IconHandlerFields["text"] != "" { + size, _ := strconv.ParseInt(key.IconHandlerFields["text_size"], 10, 0) + img, err = api.DrawText(img, key.IconHandlerFields["text"], int(size), key.IconHandlerFields["text_alignment"]) + if err != nil { + log.Println(err) + } + } + frames[i] = img + } + go s.loop(frames, timeDelay, callback) +} + +func (s *GifIconHandler) IsRunning() bool { + return s.Running +} + +func (s *GifIconHandler) SetRunning(running bool) { + s.Running = running +} + +func (s *GifIconHandler) Stop() { + s.Running = false + s.Quit <- true +} + +func (s *GifIconHandler) loop(frames []image.Image, timeDelay int, callback func(image image.Image)) { + ctx := context.Background() + err := s.Lock.Acquire(ctx, 1) + if err != nil { + return + } + defer s.Lock.Release(1) + gifIndex := 0 + for { + select { + case <-s.Quit: + return + default: + img := frames[gifIndex] + callback(img) + gifIndex++ + if gifIndex >= len(frames) { + gifIndex = 0 + } + time.Sleep(time.Duration(timeDelay * 10000000)) + } + } +} + +func RegisterGif() handlers.Module { + return handlers.Module{NewIcon: func() api.IconHandler { + return &GifIconHandler{Running: true, Lock: semaphore.NewWeighted(1)} + }, Name: "Gif", IconFields: []api.Field{{Title: "Icon", Name: "icon", Type: "File", FileTypes: []string{".gif"}}, {Title: "Text", Name: "text", Type: "Text"}, {Title: "Text Size", Name: "text_size", Type: "Number"}, {Title: "Text Alignment", Name: "text_alignment", Type: "TextAlignment"}}} +} diff --git a/handlers/examples/init.go b/handlers/examples/init.go new file mode 100644 index 0000000..b6783af --- /dev/null +++ b/handlers/examples/init.go @@ -0,0 +1,10 @@ +package examples + +import "github.com/unix-streamdeck/streamdeckd/handlers" + +func RegisterBaseModules() { + handlers.RegisterModule(RegisterGif()) + handlers.RegisterModule(RegisterTime()) + handlers.RegisterModule(RegisterCounter()) + handlers.RegisterModule(RegisterSpotify()) +} diff --git a/handlers/examples/spotify.go b/handlers/examples/spotify.go new file mode 100644 index 0000000..a10c283 --- /dev/null +++ b/handlers/examples/spotify.go @@ -0,0 +1,147 @@ +package examples + +import ( + "errors" + "fmt" + "image" + "log" + "net/http" + "strings" + "time" + + "github.com/godbus/dbus/v5" + "github.com/unix-streamdeck/api" + "github.com/unix-streamdeck/streamdeckd/handlers" +) + +type SpotifyIconHandler struct { + Running bool + oldUrl string + Quit chan bool +} + +func (s *SpotifyIconHandler) Start(key api.Key, info api.StreamDeckInfo, callback func(image image.Image)) { + s.Running = true + if s.Quit == nil { + s.Quit = make(chan bool) + } + serviceName := key.IconHandlerFields["serviceName"] + if serviceName == "" { + serviceName = "spotify" + } + c, err := Connect(serviceName) + if err != nil { + log.Println(err) + return + } + go s.run(c, callback) +} + +func (s *SpotifyIconHandler) IsRunning() bool { + return s.Running +} + +func (s *SpotifyIconHandler) SetRunning(running bool) { + s.Running = running +} + +func (s *SpotifyIconHandler) Stop() { + s.Running = false + s.Quit <- true +} + +func (s *SpotifyIconHandler) run(c *Connection, callback func(image image.Image)) { + defer c.Close() + for { + select { + case <-s.Quit: + return + default: + url, err := c.GetAlbumArtUrl() + if err != nil { + log.Println(err) + time.Sleep(time.Second) + continue + } + if url == s.oldUrl { + time.Sleep(time.Second) + continue + } + img, err := getImage(url) + if err != nil { + log.Println(err) + time.Sleep(time.Second) + continue + } + callback(img) + s.oldUrl = url + time.Sleep(time.Second) + } + } +} + +func RegisterSpotify() handlers.Module { + return handlers.Module{NewIcon: func() api.IconHandler { + return &SpotifyIconHandler{Running: true} + }, Name: "Spotify", IconFields: []api.Field{{Title: "Service (default 'spotify')", Name: "serviceName", Type: "Text"}}} +} + +// region DBus +func getImage(url string) (image.Image, error) { + response, err := http.Get(url) + if err != nil { + return nil, err + } + if response.StatusCode != 200 { + return nil, errors.New("Couldn't get Image from URL") + } + defer response.Body.Close() + img, _, err := image.Decode(response.Body) + if err != nil { + return nil, err + } + return img, nil +} + +type Connection struct { + busobj dbus.BusObject + conn *dbus.Conn +} + +func Connect(serviceName string) (*Connection, error) { + conn, err := dbus.ConnectSessionBus() + if err != nil { + return nil, err + } + + return &Connection{ + conn: conn, + busobj: conn.Object(fmt.Sprintf("org.mpris.MediaPlayer2.%s", serviceName), "/org/mpris/MediaPlayer2"), + }, nil +} + +func (c *Connection) GetAlbumArtUrl() (string, error) { + variant, err := c.busobj.GetProperty("org.mpris.MediaPlayer2.Player.Metadata") + if err != nil { + return "", err + } + metadataMap := variant.Value().(map[string]dbus.Variant) + var url string + for key, val := range metadataMap { + if key == "mpris:artUrl" { + url = val.String() + } + } + if url == "" { + return "", errors.New("Couldn't get URL from DBus") + } + url = strings.ReplaceAll(url, "\"", "") + url = strings.ReplaceAll(url, "https://open.spotify.com/image/", "https://i.scdn.co/image/") + return url, nil +} + +func (c *Connection) Close() { + c.conn.Close() +} + +// endregion diff --git a/handlers/examples/time.go b/handlers/examples/time.go new file mode 100644 index 0000000..3c55370 --- /dev/null +++ b/handlers/examples/time.go @@ -0,0 +1,63 @@ +package examples + +import ( + "github.com/unix-streamdeck/api" + "github.com/unix-streamdeck/streamdeckd/handlers" + "image" + "image/draw" + "log" + "time" +) + +type TimeIconHandler struct { + Running bool + Quit chan bool +} + +func (t *TimeIconHandler) Start(k api.Key, info api.StreamDeckInfo, callback func(image image.Image)) { + t.Running = true + if t.Quit == nil { + t.Quit = make(chan bool) + } + go t.timeLoop(k, info, callback) +} + +func (t *TimeIconHandler) IsRunning() bool { + return t.Running +} + +func (t *TimeIconHandler) SetRunning(running bool) { + t.Running = running +} + +func (t *TimeIconHandler) Stop() { + t.Running = false + t.Quit <- true +} + +func (t *TimeIconHandler) timeLoop(k api.Key, info api.StreamDeckInfo, callback func(image image.Image)) { + for { + select { + case <- t.Quit: + return + default: + img := image.NewRGBA(image.Rect(0, 0, info.IconSize, info.IconSize)) + draw.Draw(img, img.Bounds(), image.Black, image.ZP, draw.Src) + t := time.Now() + tString := t.Format("15:04:05") + imgParsed, err := api.DrawText(img, tString, k.TextSize, k.TextAlignment) + if err != nil { + log.Println(err) + } else { + callback(imgParsed) + } + time.Sleep(time.Second) + } + } +} + +func RegisterTime() handlers.Module { + return handlers.Module{NewIcon: func() api.IconHandler { + return &TimeIconHandler{Running: true} + }, Name: "Time"} +} \ No newline at end of file diff --git a/handlers/handlers.go b/handlers/handlers.go new file mode 100644 index 0000000..9663fcc --- /dev/null +++ b/handlers/handlers.go @@ -0,0 +1,56 @@ +package handlers + +import ( + "github.com/unix-streamdeck/api" + "log" + "plugin" +) + +type Module struct { + Name string + NewIcon func() api.IconHandler + NewKey func() api.KeyHandler + IconFields []api.Field + KeyFields []api.Field +} + + + +var modules []Module + + +func AvailableModules() []Module { + return modules +} + +func RegisterModule(m Module) { + for _, module := range modules { + if module.Name == m.Name { + log.Println("Module already loaded: " + m.Name) + return + } + } + log.Println("Loaded module " + m.Name) + modules = append(modules, m) +} + +func LoadModule(path string) { + plug, err := plugin.Open(path) + if err != nil { + //log.Println("Failed to load module: " + path) + log.Println(err) + return + } + mod, err := plug.Lookup("GetModule") + if err != nil { + log.Println(err) + return + } + var modMethod func() Module + modMethod, ok := mod.(func() Module) + if !ok { + log.Println("Failed to load module: " + path) + return + } + RegisterModule(modMethod()) +} \ No newline at end of file diff --git a/interface.go b/interface.go new file mode 100644 index 0000000..05caedf --- /dev/null +++ b/interface.go @@ -0,0 +1,212 @@ +package main + +import ( + "context" + "github.com/unix-streamdeck/api" + _ "github.com/unix-streamdeck/driver" + "github.com/unix-streamdeck/streamdeckd/handlers" + "golang.org/x/sync/semaphore" + "image" + "image/draw" + "log" + "os" + "strings" +) + + +var sem = semaphore.NewWeighted(int64(1)) + +func LoadImage(dev *VirtualDev, path string) (image.Image, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + img, _, err := image.Decode(f) + if err != nil { + return nil, err + } + + return api.ResizeImage(img, int(dev.Deck.Pixels)), nil +} + +func SetImage(dev *VirtualDev, img image.Image, i int, page int) { + ctx := context.Background() + err := sem.Acquire(ctx, 1) + if err != nil { + log.Println(err) + return + } + defer sem.Release(1) + if dev.Page == page && dev.IsOpen { + err := dev.Deck.SetImage(uint8(i), img) + if err != nil { + if strings.Contains(err.Error(), "hidapi") { + disconnect(dev) + } else if strings.Contains(err.Error(), "dimensions") { + log.Println(err) + }else { + log.Println(err) + } + } + } +} + +func SetKeyImage(dev *VirtualDev, currentKey *api.Key, i int, page int) { + if currentKey.Buff == nil { + if currentKey.Icon == "" { + img := image.NewRGBA(image.Rect(0, 0, int(dev.Deck.Pixels), int(dev.Deck.Pixels))) + draw.Draw(img, img.Bounds(), image.Black, image.ZP, draw.Src) + currentKey.Buff = img + } else { + img, err := LoadImage(dev, currentKey.Icon) + if err != nil { + log.Println(err) + return + } + currentKey.Buff = img + } + if currentKey.Text != "" { + img, err := api.DrawText(currentKey.Buff, currentKey.Text, currentKey.TextSize, currentKey.TextAlignment) + if err != nil { + log.Println(err) + } else { + currentKey.Buff = img + } + } + } + if currentKey.Buff != nil { + SetImage(dev, currentKey.Buff, i, page) + } +} + +func SetPage(dev *VirtualDev, page int) { + if page != dev.Page { + unmountPageHandlers(dev.Config[dev.Page]) + } + dev.Page = page + currentPage := dev.Config[page] + for i := 0; i < len(currentPage); i++ { + currentKey := ¤tPage[i] + go SetKey(dev, currentKey, i, page) + } + EmitPage(dev, page) +} + +func SetKey(dev *VirtualDev, currentKey *api.Key, i int, page int) { + var deckInfo api.StreamDeckInfo + for i := range sDInfo { + if sDInfo[i].Serial == dev.Deck.Serial { + deckInfo = sDInfo[i] + } + } + if currentKey.Buff == nil { + if currentKey.IconHandler == "" { + SetKeyImage(dev, currentKey, i, page) + + } else if currentKey.IconHandlerStruct == nil { + var handler api.IconHandler + modules := handlers.AvailableModules() + for _, module := range modules { + if module.Name == currentKey.IconHandler { + handler = module.NewIcon() + } + } + if handler == nil { + return + } + log.Printf("Created & Started %s\n", currentKey.IconHandler) + handler.Start(*currentKey, deckInfo, func(image image.Image) { + if image.Bounds().Max.X != int(dev.Deck.Pixels) || image.Bounds().Max.Y != int(dev.Deck.Pixels) { + image = api.ResizeImage(image, int(dev.Deck.Pixels)) + } + SetImage(dev, image, i, page) + currentKey.Buff = image + }) + currentKey.IconHandlerStruct = handler + } + } else { + SetImage(dev, currentKey.Buff, i, page) + } + if currentKey.IconHandlerStruct != nil && !currentKey.IconHandlerStruct.IsRunning() { + log.Printf("Started %s\n", currentKey.IconHandler) + currentKey.IconHandlerStruct.Start(*currentKey, deckInfo, func(image image.Image) { + if image.Bounds().Max.X != int(dev.Deck.Pixels) || image.Bounds().Max.Y != int(dev.Deck.Pixels) { + image = api.ResizeImage(image, int(dev.Deck.Pixels)) + } + SetImage(dev, image, i, page) + currentKey.Buff = image + }) + } +} + +func HandleInput(dev *VirtualDev, key *api.Key, page int) { + if key.Command != "" { + runCommand(key.Command) + } + if key.Keybind != "" { + runCommand("xdotool key " + key.Keybind) + } + if key.SwitchPage != 0 { + page = key.SwitchPage - 1 + SetPage(dev, page) + } + if key.Brightness != 0 { + err := dev.Deck.SetBrightness(uint8(key.Brightness)) + if err != nil { + log.Println(err) + } + } + if key.Url != "" { + runCommand("xdg-open " + key.Url) + } + if key.KeyHandler != "" { + var deckInfo api.StreamDeckInfo + found := false + for i := range sDInfo { + if sDInfo[i].Serial == dev.Deck.Serial { + deckInfo = sDInfo[i] + found = true + } + } + if !found { + return + } + if key.KeyHandlerStruct == nil { + var handler api.KeyHandler + modules := handlers.AvailableModules() + for _, module := range modules { + if module.Name == key.KeyHandler { + handler = module.NewKey() + } + } + if handler == nil { + return + } + key.KeyHandlerStruct = handler + } + key.KeyHandlerStruct.Key(*key, deckInfo) + } +} + +func Listen(dev *VirtualDev) { + kch, err := dev.Deck.ReadKeys() + if err != nil { + log.Println(err) + } + for dev.IsOpen { + select { + case k, ok := <-kch: + if !ok { + disconnect(dev) + return + } + if k.Pressed == true { + if len(dev.Config)-1 >= dev.Page && len(dev.Config[dev.Page])-1 >= int(k.Index) { + HandleInput(dev, &dev.Config[dev.Page][k.Index], dev.Page) + } + } + } + } +} diff --git a/main.go b/main.go index e0c08d8..8983869 100644 --- a/main.go +++ b/main.go @@ -1,14 +1,17 @@ package main import ( + "context" "encoding/json" - "github.com/fogleman/gg" - "github.com/nfnt/resize" - "github.com/unix-streamdeck/streamdeck-lib" - "golang.org/x/image/font/inconsolata" - "image" - "image/color" - "image/draw" + "errors" + "flag" + "fmt" + "github.com/shirou/gopsutil/v3/process" + "github.com/unix-streamdeck/api" + "github.com/unix-streamdeck/driver" + "github.com/unix-streamdeck/streamdeckd/handlers" + "github.com/unix-streamdeck/streamdeckd/handlers/examples" + "golang.org/x/sync/semaphore" _ "image/gif" _ "image/jpeg" _ "image/png" @@ -17,160 +20,347 @@ import ( "os" "os/exec" "os/signal" - "strings" "syscall" + "time" ) -var page = 0 -var dev streamdeck.Device -var config Config +type VirtualDev struct { + Deck streamdeck.Device + Page int + IsOpen bool + Config []api.Page +} + +var devs map[string]*VirtualDev +var config *api.Config +var migrateConfig = false +var configPath string +var disconnectSem = semaphore.NewWeighted(1) +var connectSem = semaphore.NewWeighted(1) +var basicConfig = api.Config{ + Modules: []string{}, + Decks: []api.Deck{ + {}, + }, +} +var isRunning = true func main() { - d, err := streamdeck.Devices() + checkOtherRunningInstances() + configPtr := flag.String("config", configPath, "Path to config file") + flag.Parse() + if *configPtr != "" { + configPath = *configPtr + } else { + basePath := os.Getenv("HOME") + string(os.PathSeparator) + ".config" + if os.Getenv("XDG_CONFIG_HOME") != "" { + basePath = os.Getenv("XDG_CONFIG_HOME") + } + configPath = basePath + string(os.PathSeparator) + ".streamdeck-config.json" + } + cleanupHook() + go InitDBUS() + examples.RegisterBaseModules() + loadConfig() + devs = make(map[string]*VirtualDev) + attemptConnection() +} + +func checkOtherRunningInstances() { + processes, err := process.Processes() if err != nil { - log.Fatal(err) + log.Println("Could not check for other instances of streamdeckd, assuming no others running") } - if len(d) == 0 { - log.Fatal("No Stream Deck devices found.") + for _, proc := range processes { + name, err := proc.Name() + if err == nil && name == "streamdeckd" && int(proc.Pid) != os.Getpid() { + log.Fatalln("Another instance of streamdeckd is already running, exiting...") + } + } +} + +func attemptConnection() { + for isRunning { + dev := &VirtualDev{} + dev, _ = openDevice() + if dev.IsOpen { + SetPage(dev, dev.Page) + found := false + for i := range sDInfo { + if sDInfo[i].Serial == dev.Deck.Serial { + found = true + } + } + if !found { + sDInfo = append(sDInfo, api.StreamDeckInfo{ + Cols: int(dev.Deck.Columns), + Rows: int(dev.Deck.Rows), + IconSize: int(dev.Deck.Pixels), + Page: 0, + Serial: dev.Deck.Serial, + }) + } + go Listen(dev) + } + time.Sleep(250 * time.Millisecond) } - dev = d[0] - err = dev.Open() +} + +func disconnect(dev *VirtualDev) { + ctx := context.Background() + err := disconnectSem.Acquire(ctx, 1) if err != nil { - log.Fatal(err) + return } - config, err = readConfig() - if err != nil && !os.IsNotExist(err) { - log.Fatal(err) + defer disconnectSem.Release(1) + if !dev.IsOpen { + return } - if len(config.Pages) == 0 { - config.Pages = append(config.Pages, Page{}) + log.Println("Device (" + dev.Deck.Serial + ") disconnected") + _ = dev.Deck.Close() + dev.IsOpen = false + unmountDevHandlers(dev) +} + +func openDevice() (*VirtualDev, error) { + ctx := context.Background() + err := connectSem.Acquire(ctx, 1) + if err != nil { + return &VirtualDev{}, err } - cleanupHook() - setPage() - kch, err := dev.ReadKeys() + defer connectSem.Release(1) + d, err := streamdeck.Devices() if err != nil { - log.Fatal(err) + return &VirtualDev{}, err } - for { - select { - case k, ok := <-kch: - if !ok { - err = dev.Open() + if len(d) == 0 { + return &VirtualDev{}, errors.New("No streamdeck devices found") + } + device := streamdeck.Device{Serial: ""} + for i := range d { + found := false + for s := range devs { + if d[i].ID == devs[s].Deck.ID && devs[s].IsOpen { + found = true + break + } else if d[i].Serial == s && !devs[s].IsOpen { + err = d[i].Open() if err != nil { - log.Fatal(err) - } - continue - } - if k.Pressed == true { - if len(config.Pages)-1 >= page && len(config.Pages[page])-1 >= int(k.Index) { - handleInput(config.Pages[page][k.Index]) + return &VirtualDev{}, err } + devs[s].Deck = d[i] + devs[s].IsOpen = true + return devs[s], nil } } + if !found { + device = d[i] + } + } + if len(device.Serial) != 12 { + return &VirtualDev{}, errors.New("No streamdeck devices found") + } + err = device.Open() + if err != nil { + return &VirtualDev{}, err + } + devNo := -1 + if migrateConfig { + config.Decks[0].Serial = device.Serial + _ = SaveConfig() + migrateConfig = false + } + for i := range config.Decks { + if config.Decks[i].Serial == device.Serial { + devNo = i + } + } + if devNo == -1 { + var pages []api.Page + page := api.Page{} + for i := 0; i < int(device.Rows)*int(device.Columns); i++ { + page = append(page, api.Key{}) + } + pages = append(pages, page) + config.Decks = append(config.Decks, api.Deck{Serial: device.Serial, Pages: pages}) + devNo = len(config.Decks) - 1 + } + dev := &VirtualDev{Deck: device, Page: 0, IsOpen: true, Config: config.Decks[devNo].Pages} + devs[device.Serial] = dev + log.Println("Device (" + device.Serial + ") connected") + return dev, nil +} + +func loadConfig() { + var err error + config, err = readConfig() + if err != nil && !os.IsNotExist(err) { + log.Println(err) + } else if os.IsNotExist(err) { + file, err := os.Create(configPath) + if err != nil { + log.Println(err) + } + err = file.Close() + if err != nil { + log.Println(err) + } + config = &basicConfig + err = SaveConfig() + if err != nil { + log.Println(err) + } + } + if len(config.Modules) > 0 { + for _, module := range config.Modules { + handlers.LoadModule(module) + } } } -func readConfig() (Config, error) { - data, err := ioutil.ReadFile(os.Getenv("HOME") + "/.streamdeck-config.json") +func readConfig() (*api.Config, error) { + data, err := ioutil.ReadFile(configPath) if err != nil { - return Config{}, err + return &api.Config{}, err } - var config Config + var config api.Config err = json.Unmarshal(data, &config) - if err != nil { - return Config{}, err + if err != nil || config.Decks == nil { + var deprecatedConfig api.DepracatedConfig + err = json.Unmarshal(data, &deprecatedConfig) + if err != nil { + return &api.Config{}, err + } + config = api.Config{Modules: deprecatedConfig.Modules, Decks: []api.Deck{{Pages: deprecatedConfig.Pages, Serial: ""}}} + migrateConfig = true } - return config, nil + return &config, nil } -func setImage(img image.Image, i int, p int) { - if p == page { - dev.SetImage(uint8(i), img) - } +func runCommand(command string) { + go func() { + cmd := exec.Command("/bin/sh", "-c", "/usr/bin/nohup "+command) + + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + Pgid: 0, + Pdeathsig: syscall.SIGHUP, + } + if err := cmd.Start(); err != nil { + fmt.Println("There was a problem running ", command, ":", err) + } else { + pid := cmd.Process.Pid + cmd.Process.Release() + fmt.Println(command, " has been started with pid", pid) + } + }() } -func setPage() { - currentPage := config.Pages[page] - for i, currentKey := range currentPage { - if currentKey.buff == nil { - if currentKey.Icon == "" { - img := image.NewRGBA(image.Rect(0, 0, int(dev.Pixels), int(dev.Pixels))) - draw.Draw(img, img.Bounds(), image.NewUniform(color.RGBA{0, 0, 0, 255}), image.ZP, draw.Src) - currentKey.buff = img - } else { - img, err := loadImage(currentKey.Icon) +func cleanupHook() { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGSTOP, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGINT) + go func() { + <-sigs + log.Println("Cleaning up") + isRunning = false + unmountHandlers() + var err error + for s := range devs { + if devs[s].IsOpen { + err = devs[s].Deck.Reset() if err != nil { - log.Fatal(err) + log.Println(err) + } + err = devs[s].Deck.Close() + if err != nil { + log.Println(err) } - currentKey.buff = img - } - if currentKey.Text != "" { - img := gg.NewContextForImage(currentKey.buff) - img.SetRGB(0, 0, 0) - img.Clear() - img.SetRGB(1, 1, 1) - img.SetFontFace(inconsolata.Regular8x16) - img.DrawStringAnchored(currentKey.Text, 72/2, 72/2, 0.5, 0.5) - img.Clip() - currentKey.buff = img.Image() } } - setImage(currentKey.buff, i, page) - } + os.Exit(0) + }() } -func loadImage(path string) (image.Image, error) { - f, err := os.Open(path) +func SetConfig(configString string) error { + unmountHandlers() + var err error + config = nil + err = json.Unmarshal([]byte(configString), &config) if err != nil { - return nil, err + return err } - defer f.Close() - - img, _, err := image.Decode(f) - if err != nil { - return nil, err + for s := range devs { + dev := devs[s] + for i := range config.Decks { + if dev.Deck.Serial == config.Decks[i].Serial { + dev.Config = config.Decks[i].Pages + } + } + SetPage(dev, devs[s].Page) } + return nil +} - return resize.Resize(72, 72, img, resize.Lanczos3), nil +func ReloadConfig() error { + unmountHandlers() + loadConfig() + for s := range devs { + dev := devs[s] + for i := range config.Decks { + if dev.Deck.Serial == config.Decks[i].Serial { + dev.Config = config.Decks[i].Pages + } + } + SetPage(dev, devs[s].Page) + } + return nil } -func handleInput(key Key) { - if key.Command != "" { - runCommand(key.Command) +func SaveConfig() error { + f, err := os.OpenFile(configPath, os.O_TRUNC|os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + return err } - if key.Keybind != "" { - runCommand("xdotool key " + key.Keybind) + defer f.Close() + var configString []byte + configString, err = json.Marshal(config) + if err != nil { + return err } - if key.SwitchPage != nil { - page = (*key.SwitchPage) - 1 - setPage() + _, err = f.Write(configString) + if err != nil { + return err } - if key.Brightness != nil { - _ = dev.SetBrightness(uint8(*key.Brightness)) + err = f.Sync() + if err != nil { + return err } - if key.Url != "" { - runCommand("xdg-open " + key.Url) + return nil +} +func unmountHandlers() { + for s := range devs { + dev := devs[s] + unmountDevHandlers(dev) } } -func runCommand(command string) { - args := strings.Split(command, " ") - c := exec.Command(args[0], args[1:]...) - if err := c.Start(); err != nil { - panic(err) - } - err := c.Wait() - if err != nil { - log.Printf("command failed: %s", err) +func unmountDevHandlers(dev *VirtualDev) { + for i := range dev.Config { + unmountPageHandlers(dev.Config[i]) } } -func cleanupHook() { - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1, syscall.SIGUSR2) - go func() { - <-sigs - _ = dev.Reset() - os.Exit(0) - }() +func unmountPageHandlers(page api.Page) { + for i2 := 0; i2 < len(page); i2++ { + key := &page[i2] + if key.IconHandlerStruct != nil { + log.Printf("Stopping %s\n", key.IconHandler) + if key.IconHandlerStruct.IsRunning() { + go func() { + key.IconHandlerStruct.Stop() + log.Printf("Stopped %s\n", key.IconHandler) + }() + } + } + } }