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..79f5c27 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,19 +11,18 @@ 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 @@ -34,18 +34,30 @@ 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 +66,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..b077407 --- /dev/null +++ b/dbus.go @@ -0,0 +1,191 @@ +package main + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "github.com/godbus/dbus/v5" + "github.com/unix-streamdeck/api" + "github.com/unix-streamdeck/streamdeckd/handlers" + "image" + "image/png" + "log" + "time" +) + +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 (StreamDeckDBus) GetHandlerExample(serial string, keyString string) (string, *dbus.Error) { + var key *api.Key + err := json.Unmarshal([]byte(keyString), &key) + if err != nil { + return "", dbus.MakeFailedError(err) + } + if key.IconHandler == "" || key.IconHandler == "Default" { + return "", dbus.MakeFailedError(errors.New("Invalid icon handler")) + } + var handler api.IconHandler + modules := handlers.AvailableModules() + for _, module := range modules { + if module.Name == key.IconHandler { + handler = module.NewIcon() + break + } + } + if handler == nil { + return "", dbus.MakeFailedError(errors.New("Invalid icon handler")) + } + var dev api.StreamDeckInfo + for _, info := range sDInfo { + if info.Serial == serial { + dev = info + break + } + } + if dev.Serial != serial { + return "", dbus.MakeFailedError(errors.New("could not find device")) + } + var img image.Image + log.Println("Created and running " + key.IconHandler + " for dbus") + handler.Start(*key, dev, func(image image.Image) { + if image.Bounds().Max.X != dev.IconSize || image.Bounds().Max.Y != dev.IconSize { + image = api.ResizeImage(image, dev.IconSize) + } + img = image + log.Println("Stopping " + key.IconHandler + " for dbus") + handler.Stop() + log.Println("Stopped " + key.IconHandler + " for dbus") + }) + timer := time.NewTimer(5 * time.Second) + go func() { + <-timer.C + if handler.IsRunning() { + log.Println("Handler still running") + handler.Stop() + } else { + log.Println("Handler had stopped") + } + }() + for handler.IsRunning() { + } + if img == nil { + return "", dbus.MakeFailedError(errors.New("Handler did not respond in a timely fashion")) + } + buf := new(bytes.Buffer) + err = png.Encode(buf, img) + imageBits := buf.Bytes() + return "data:image/png;base64," + base64.StdEncoding.EncodeToString(imageBits), 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..644eb94 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,11 @@ -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/unix-streamdeck/api v1.0.0 + github.com/unix-streamdeck/driver v0.0.0-20200817173808-cdaf123c076b + 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..9dc5a71 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 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-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 +42,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= @@ -91,10 +91,10 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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-20200817173808-cdaf123c076b h1:27gVti9+OevmBC2BnWlKC0dQ0eiIHh7PvYTWxt4vb6A= +github.com/unix-streamdeck/driver v0.0.0-20200817173808-cdaf123c076b/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 +103,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,6 +118,8 @@ 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= 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..c9ae281 --- /dev/null +++ b/handlers/examples/spotify.go @@ -0,0 +1,141 @@ +package examples + +import ( + "errors" + "github.com/godbus/dbus/v5" + "github.com/unix-streamdeck/api" + "github.com/unix-streamdeck/streamdeckd/handlers" + "image" + "log" + "net/http" + "strings" + "time" +) + +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) + } + c, err := Connect() + 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"} +} + +// 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() (*Connection, error) { + conn, err := dbus.ConnectSessionBus() + if err != nil { + return nil, err + } + return &Connection{ + conn: conn, + busobj: conn.Object("org.mpris.MediaPlayer2.spotify", "/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 \ No newline at end of file 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..701ab33 100644 --- a/main.go +++ b/main.go @@ -1,14 +1,16 @@ 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/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 +19,328 @@ 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 = os.Getenv("HOME") + string(os.PathSeparator) + ".streamdeck-config.json" +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() - if err != nil { - log.Fatal(err) + configPtr := flag.String("config", configPath, "Path to config file") + flag.Parse() + if *configPtr != "" { + configPath = *configPtr } - if len(d) == 0 { - log.Fatal("No Stream Deck devices found.") + cleanupHook() + go InitDBUS() + examples.RegisterBaseModules() + loadConfig() + devs = make(map[string]*VirtualDev) + attemptConnection() +} + +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 + } + if len(d) == 0 { + return &VirtualDev{}, errors.New("No streamdeck devices found") } - for { - select { - case k, ok := <-kch: - if !ok { - err = dev.Open() + 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 readConfig() (Config, error) { - data, err := ioutil.ReadFile(os.Getenv("HOME") + "/.streamdeck-config.json") +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() (*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) + }() + } + } + } }