├── .gitignore ├── LICENSE ├── README.md ├── dbus.go ├── go.mod ├── go.sum ├── handlers ├── examples │ ├── counter.go │ ├── gif.go │ ├── init.go │ ├── spotify.go │ └── time.go └── handlers.go ├── interface.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .idea 15 | streamdeckd -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, unix-streamdeck 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Streamdeckd 2 | 3 | # Help Wanted! 4 | 5 | If you want to help with the development of streamdeckd and it's related repos, either by submitting code, finding/fixing bugs, or just replying to issues, please join this discord server: https://discord.gg/nyhuVEJWMQ 6 | ### Installation 7 | 8 | - create the file `/etc/udev/rules.d/50-elgato.rules` with the following config 9 | 10 | ``` 11 | SUBSYSTEM=="input", GROUP="input", MODE="0666" 12 | SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0060", MODE:="666", GROUP="plugdev" 13 | SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0063", MODE:="666", GROUP="plugdev" 14 | SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="006c", MODE:="666", GROUP="plugdev" 15 | SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="006d", MODE:="666", GROUP="plugdev" 16 | ``` 17 | 18 | - run `sudo udevadm control --reload-rules` to reload the udev rules 19 | 20 | Then xdotool will be required to simulate keypresses, to install this run: 21 | 22 | #### Arch 23 | 24 | `sudo pacman -S xdotool` 25 | 26 | #### Debian based 27 | 28 | `sudo apt install xdotool` 29 | 30 | ### Configuration 31 | 32 | #### Manual configuration 33 | 34 | ## Warning: 35 | 36 | 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 37 | 38 | --- 39 | 40 | 41 | The configuration file streamdeckd uses is a JSON file found at `$XDG_CONFIG_HOME/.streamdeck-config.json` 42 | 43 | An example config would be something like: 44 | 45 | ```json 46 | { 47 | "modules": [ 48 | "/home/user/module.so" 49 | ], 50 | "decks": [ 51 | { 52 | "serial": "AB12C3D45678", 53 | "pages": [ 54 | [ 55 | { 56 | "switch_page": 1, 57 | "icon": "~/icon.png" 58 | } 59 | ] 60 | ] 61 | } 62 | ] 63 | } 64 | ``` 65 | 66 | At the top is the list of custom modules, these are go plugins in the .so format, following that is the list of deck 67 | objects, each represents a different streamdeck device, and contains its serial, and its list of pages 68 | 69 | 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 70 | going in a right to left order. 71 | 72 | The actions you can have on a button are: 73 | 74 | - `command`: runs a native shell command, something like `notify-send "Hello World"` 75 | - `keybind`: simulates the indicated keybind via xdtotool 76 | - `url`: opens a url in your default browser via xdg 77 | - `brightness`: set the brightness of the streamdeck as a percentage 78 | - `switch_page`: change the active page on the streamdeck 79 | 80 | ### D-Bus 81 | 82 | There is a D-Bus interface built into the daemon, the service name and interface for D-Bus 83 | are `com.unixstreamdeck.streamdeckd` and `com/unixstreamdeck/streamdeckd` respectively, and is made up of the following 84 | methods/signals 85 | 86 | #### Methods 87 | 88 | - GetConfig - returns the current running config 89 | - SetConfig - sets the config, without saving to disk, takes in Stringified json, returns an error if anything breaks 90 | - ReloadConfig - reloads the config from disk 91 | - GetDeckInfo - Returns information about all the active streamdecks in the format of 92 | 93 | ```json 94 | [ 95 | { 96 | "icon_size": 72, 97 | "rows": 3, 98 | "cols": 5, 99 | "page": 0, 100 | "serial": "AB12C3D45678" 101 | } 102 | ] 103 | ``` 104 | 105 | - SetPage - Set the page on the streamdeck to the number passed to it, returns an error if anything breaks 106 | - CommitConfig - Commits the currently active config to disk, returns an error if anything breaks 107 | - GetModules - Get the list of loaded modules, and the config fields those modules use 108 | - PressButton - Simulates a button press on the streamdeck device, consumes a device serial, and a key index 109 | 110 | 111 | #### Signals 112 | 113 | - Page - sends the number of the page switched to on the StreamDeck 114 | 115 | ### Custom Modules 116 | 117 | 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 118 | 119 | #### Loading Modules into streamdeckd 120 | 121 | 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 122 | 123 | ```go 124 | package main 125 | 126 | type CustomIconHandler struct { 127 | 128 | } 129 | ... 130 | 131 | type CustomKeyHandler struct { 132 | 133 | } 134 | ... 135 | 136 | func GetModule() handlers.Module { 137 | return handlers.Module{ 138 | 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 139 | 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 140 | 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 141 | IconFields: []api.Field{ // list of fields to be shown in streamdeckui when the icon handler is selected 142 | { 143 | Title: "Icon", // name of field to show in UI 144 | Name: "icon", // name of field that will be included in the iconHandlerFields map 145 | Type: "File" // type of input to show on streamdeckui, options are Text, File, TextAlignment, and Number 146 | FileTypes: []string{".png", ".jpg"} // Allowed file types if a File input type is used 147 | } 148 | }, 149 | KeyFields: []api.Field{}, // Same as IconFields 150 | } 151 | } 152 | 153 | ``` 154 | -------------------------------------------------------------------------------- /dbus.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/godbus/dbus/v5" 7 | "github.com/unix-streamdeck/api" 8 | "github.com/unix-streamdeck/streamdeckd/handlers" 9 | "log" 10 | ) 11 | 12 | var conn *dbus.Conn 13 | 14 | var sDbus *StreamDeckDBus 15 | var sDInfo []api.StreamDeckInfo 16 | 17 | type StreamDeckDBus struct { 18 | } 19 | 20 | func (s StreamDeckDBus) GetDeckInfo() (string, *dbus.Error) { 21 | infoString, err := json.Marshal(sDInfo) 22 | if err != nil { 23 | return "", dbus.MakeFailedError(err) 24 | } 25 | return string(infoString), nil 26 | } 27 | 28 | func (StreamDeckDBus) GetConfig() (string, *dbus.Error) { 29 | configString, err := json.Marshal(config) 30 | if err != nil { 31 | return "", dbus.MakeFailedError(err) 32 | } 33 | return string(configString), nil 34 | } 35 | 36 | func (StreamDeckDBus) ReloadConfig() *dbus.Error { 37 | err := ReloadConfig() 38 | if err != nil { 39 | return dbus.MakeFailedError(err) 40 | } 41 | return nil 42 | } 43 | 44 | func (StreamDeckDBus) SetPage(serial string, page int) *dbus.Error { 45 | for s := range devs { 46 | if devs[s].Deck.Serial == serial { 47 | dev := devs[s] 48 | SetPage(dev, page) 49 | return nil 50 | } 51 | } 52 | return dbus.MakeFailedError(errors.New("Device with Serial: " + serial + " could not be found")) 53 | } 54 | 55 | func (StreamDeckDBus) SetConfig(configString string) *dbus.Error { 56 | err := SetConfig(configString) 57 | if err != nil { 58 | return dbus.MakeFailedError(err) 59 | } 60 | return nil 61 | } 62 | 63 | func (StreamDeckDBus) CommitConfig() *dbus.Error { 64 | err := SaveConfig() 65 | if err != nil { 66 | return dbus.MakeFailedError(err) 67 | } 68 | return nil 69 | } 70 | 71 | func (StreamDeckDBus) GetModules() (string, *dbus.Error) { 72 | var modules []api.Module 73 | for _, module := range handlers.AvailableModules() { 74 | modules = append(modules, api.Module{Name: module.Name, IconFields: module.IconFields, KeyFields: module.KeyFields, IsIcon: module.NewIcon != nil, IsKey: module.NewKey != nil}) 75 | } 76 | modulesString, err := json.Marshal(modules) 77 | if err != nil { 78 | return "", dbus.MakeFailedError(err) 79 | } 80 | return string(modulesString), nil 81 | } 82 | 83 | func (StreamDeckDBus) PressButton(serial string, keyIndex int) *dbus.Error { 84 | dev, ok := devs[serial] 85 | if !ok || !dev.IsOpen{ 86 | return dbus.MakeFailedError(errors.New("Can't find connected device: " + serial)) 87 | } 88 | HandleInput(dev, &dev.Config[dev.Page][keyIndex], dev.Page) 89 | return nil 90 | } 91 | 92 | func InitDBUS() error { 93 | var err error 94 | conn, err = dbus.SessionBus() 95 | if err != nil { 96 | log.Println(err) 97 | return err 98 | } 99 | defer conn.Close() 100 | 101 | sDbus = &StreamDeckDBus{} 102 | conn.ExportAll(sDbus, "/com/unixstreamdeck/streamdeckd", "com.unixstreamdeck.streamdeckd") 103 | reply, err := conn.RequestName("com.unixstreamdeck.streamdeckd", 104 | dbus.NameFlagDoNotQueue) 105 | if err != nil { 106 | log.Println(err) 107 | return err 108 | } 109 | if reply != dbus.RequestNameReplyPrimaryOwner { 110 | return errors.New("DBus: Name already taken") 111 | } 112 | select {} 113 | } 114 | 115 | func EmitPage(dev *VirtualDev, page int) { 116 | if conn != nil { 117 | conn.Emit("/com/unixstreamdeck/streamdeckd", "com.unixstreamdeck.streamdeckd.Page", dev.Deck.Serial, page) 118 | } 119 | for i := range sDInfo { 120 | if sDInfo[i].Serial == dev.Deck.Serial { 121 | sDInfo[i].Page = page 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/unix-streamdeck/streamdeckd 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/godbus/dbus/v5 v5.0.4-0.20200513180336-df5ef3eb7cca 7 | github.com/shirou/gopsutil/v3 v3.21.9 8 | github.com/unix-streamdeck/api v1.0.1 9 | github.com/unix-streamdeck/driver v0.0.0-20211119182210-fc6b90443bcd 10 | golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect 11 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 4 | github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= 5 | github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= 6 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 7 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 8 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 9 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 10 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 11 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 12 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 13 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 14 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 15 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 16 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 17 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 18 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 21 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 23 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 24 | github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= 25 | github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 26 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 27 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 28 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 29 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 30 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 31 | github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY= 32 | github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 33 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 34 | github.com/godbus/dbus/v5 v5.0.4-0.20200513180336-df5ef3eb7cca h1:ewc47M3S8MAZgSO1yEnPrbmHjtQz6caAhYWOQzPHBok= 35 | github.com/godbus/dbus/v5 v5.0.4-0.20200513180336-df5ef3eb7cca/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 36 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 37 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 38 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 39 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 40 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 41 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 42 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 43 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 44 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 45 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 46 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 47 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 48 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 49 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 50 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 51 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 52 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 53 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 54 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 55 | github.com/karalabe/hid v1.0.1-0.20190806082151-9c14560f9ee8 h1:AP5krei6PpUCFOp20TSmxUS4YLoLvASBcArJqM/V+DY= 56 | github.com/karalabe/hid v1.0.1-0.20190806082151-9c14560f9ee8/go.mod h1:Vr51f8rUOLYrfrWDFlV12GGQgM5AT8sVh+2fY4MPeu8= 57 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 58 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 59 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 60 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 61 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 62 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 63 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 64 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 65 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 66 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 67 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 68 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 69 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 70 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 71 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= 72 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 73 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 74 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 75 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 76 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 77 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 78 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 79 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 80 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 81 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 82 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 83 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 84 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 85 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 86 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 87 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 88 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 89 | github.com/shirou/gopsutil/v3 v3.21.9 h1:Vn4MUz2uXhqLSiCbGFRc0DILbMVLAY92DSkT8bsYrHg= 90 | github.com/shirou/gopsutil/v3 v3.21.9/go.mod h1:YWp/H8Qs5fVmf17v7JNZzA0mPJ+mS2e9JdiUF9LlKzQ= 91 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 92 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 93 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 94 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 95 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 96 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 97 | github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 98 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 99 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 100 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 101 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 102 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 103 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 104 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 105 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 106 | github.com/tklauser/go-sysconf v0.3.9 h1:JeUVdAOWhhxVcU6Eqr/ATFHgXk/mmiItdKeJPev3vTo= 107 | github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= 108 | github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ= 109 | github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8= 110 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 111 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 112 | github.com/unix-streamdeck/api v1.0.1 h1:B5P5p1d4uqvoy8N2OhiWdy/iuhHRCYI+pP3+lwYzDsU= 113 | github.com/unix-streamdeck/api v1.0.1/go.mod h1:Z8bzDHQnWv/2hx9wQXp0/qw6Fp4ty5pFRsgaBG5WYAI= 114 | github.com/unix-streamdeck/driver v0.0.0-20211119182210-fc6b90443bcd h1:SZleJkNDcxwgKQaoNgpg2Ui2LYFb/feWHFkYj+SLIms= 115 | github.com/unix-streamdeck/driver v0.0.0-20211119182210-fc6b90443bcd/go.mod h1:i3Eg6kJBslgUk2VIPJ3Cclta2fpV1KJrOnOnR8gnVKY= 116 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 117 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 118 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 119 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 120 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 121 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 122 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 123 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 124 | golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 125 | golang.org/x/image v0.0.0-20200801110659-972c09e46d76/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 126 | golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= 127 | golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 128 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 129 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 130 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 131 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 132 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 133 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 134 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 135 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 136 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 137 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 138 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 139 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= 140 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 141 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 142 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 143 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 144 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 145 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 146 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 147 | golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71 h1:ikCpsnYR+Ew0vu99XlDp55lGgDJdIMx3f4a18jfse/s= 148 | golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 149 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 150 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 151 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 152 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 153 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 154 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 155 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 156 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 157 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 158 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 159 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 160 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 161 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 162 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 163 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 164 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 165 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 166 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 167 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 168 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 169 | -------------------------------------------------------------------------------- /handlers/examples/counter.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "github.com/unix-streamdeck/api" 5 | "github.com/unix-streamdeck/streamdeckd/handlers" 6 | "image" 7 | "image/draw" 8 | "log" 9 | "strconv" 10 | ) 11 | 12 | type CounterIconHandler struct { 13 | Count int 14 | Running bool 15 | Callback func(image image.Image) 16 | } 17 | 18 | func (c *CounterIconHandler) Start(k api.Key, info api.StreamDeckInfo, callback func(image image.Image)) { 19 | if c.Callback == nil { 20 | c.Callback = callback 21 | } 22 | if c.Running { 23 | img := image.NewRGBA(image.Rect(0, 0, info.IconSize, info.IconSize)) 24 | draw.Draw(img, img.Bounds(), image.Black, image.ZP, draw.Src) 25 | Count := strconv.Itoa(c.Count) 26 | imgParsed, err := api.DrawText(img, Count, k.TextSize, k.TextAlignment) 27 | if err != nil { 28 | log.Println(err) 29 | } else { 30 | callback(imgParsed) 31 | } 32 | } 33 | } 34 | 35 | func (c *CounterIconHandler) IsRunning() bool { 36 | return c.Running 37 | } 38 | 39 | func (c *CounterIconHandler) SetRunning(running bool) { 40 | c.Running = running 41 | } 42 | 43 | func (c CounterIconHandler) Stop() { 44 | c.Running = false 45 | } 46 | 47 | type CounterKeyHandler struct{} 48 | 49 | func (CounterKeyHandler) Key(key api.Key, info api.StreamDeckInfo) { 50 | if key.IconHandler != "Counter" { 51 | return 52 | } 53 | handler := key.IconHandlerStruct.(*CounterIconHandler) 54 | handler.Count += 1 55 | if handler.Callback != nil { 56 | handler.Start(key, info, handler.Callback) 57 | } 58 | } 59 | 60 | func RegisterCounter() handlers.Module { 61 | return handlers.Module{NewIcon: func() api.IconHandler { 62 | return &CounterIconHandler{Running: true, Count: 0} 63 | }, NewKey: func() api.KeyHandler { 64 | return &CounterKeyHandler{} 65 | }, Name: "Counter"} 66 | } -------------------------------------------------------------------------------- /handlers/examples/gif.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "context" 5 | "github.com/unix-streamdeck/api" 6 | "github.com/unix-streamdeck/streamdeckd/handlers" 7 | "golang.org/x/sync/semaphore" 8 | "image" 9 | "image/gif" 10 | "log" 11 | "os" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | type GifIconHandler struct { 17 | Running bool 18 | Lock *semaphore.Weighted 19 | Quit chan bool 20 | } 21 | 22 | func (s *GifIconHandler) Start(key api.Key, info api.StreamDeckInfo, callback func(image image.Image)) { 23 | if s.Quit == nil { 24 | s.Quit = make(chan bool) 25 | } 26 | if s.Lock == nil { 27 | s.Lock = semaphore.NewWeighted(1) 28 | } 29 | s.Running = true 30 | icon, ok := key.IconHandlerFields["icon"] 31 | if !ok { 32 | return 33 | } 34 | f, err := os.Open(icon) 35 | if err != nil { 36 | log.Println(err) 37 | return 38 | } 39 | gifs, err := gif.DecodeAll(f) 40 | if err != nil { 41 | log.Println(err) 42 | return 43 | } 44 | timeDelay := gifs.Delay[0] 45 | frames := make([]image.Image, len(gifs.Image)) 46 | for i, frame := range gifs.Image { 47 | img := api.ResizeImage(frame, info.IconSize) 48 | if key.IconHandlerFields["text"] != "" { 49 | size, _ := strconv.ParseInt(key.IconHandlerFields["text_size"], 10, 0) 50 | img, err = api.DrawText(img, key.IconHandlerFields["text"], int(size), key.IconHandlerFields["text_alignment"]) 51 | if err != nil { 52 | log.Println(err) 53 | } 54 | } 55 | frames[i] = img 56 | } 57 | go s.loop(frames, timeDelay, callback) 58 | } 59 | 60 | func (s *GifIconHandler) IsRunning() bool { 61 | return s.Running 62 | } 63 | 64 | func (s *GifIconHandler) SetRunning(running bool) { 65 | s.Running = running 66 | } 67 | 68 | func (s *GifIconHandler) Stop() { 69 | s.Running = false 70 | s.Quit <- true 71 | } 72 | 73 | func (s *GifIconHandler) loop(frames []image.Image, timeDelay int, callback func(image image.Image)) { 74 | ctx := context.Background() 75 | err := s.Lock.Acquire(ctx, 1) 76 | if err != nil { 77 | return 78 | } 79 | defer s.Lock.Release(1) 80 | gifIndex := 0 81 | for { 82 | select { 83 | case <-s.Quit: 84 | return 85 | default: 86 | img := frames[gifIndex] 87 | callback(img) 88 | gifIndex++ 89 | if gifIndex >= len(frames) { 90 | gifIndex = 0 91 | } 92 | time.Sleep(time.Duration(timeDelay * 10000000)) 93 | } 94 | } 95 | } 96 | 97 | func RegisterGif() handlers.Module { 98 | return handlers.Module{NewIcon: func() api.IconHandler { 99 | return &GifIconHandler{Running: true, Lock: semaphore.NewWeighted(1)} 100 | }, 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"}}} 101 | } 102 | -------------------------------------------------------------------------------- /handlers/examples/init.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "github.com/unix-streamdeck/streamdeckd/handlers" 4 | 5 | func RegisterBaseModules() { 6 | handlers.RegisterModule(RegisterGif()) 7 | handlers.RegisterModule(RegisterTime()) 8 | handlers.RegisterModule(RegisterCounter()) 9 | handlers.RegisterModule(RegisterSpotify()) 10 | } 11 | -------------------------------------------------------------------------------- /handlers/examples/spotify.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "errors" 5 | "github.com/godbus/dbus/v5" 6 | "github.com/unix-streamdeck/api" 7 | "github.com/unix-streamdeck/streamdeckd/handlers" 8 | "image" 9 | "log" 10 | "net/http" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | type SpotifyIconHandler struct { 16 | Running bool 17 | oldUrl string 18 | Quit chan bool 19 | } 20 | 21 | func (s *SpotifyIconHandler) Start(key api.Key, info api.StreamDeckInfo, callback func(image image.Image)) { 22 | s.Running = true 23 | if s.Quit == nil { 24 | s.Quit = make(chan bool) 25 | } 26 | c, err := Connect() 27 | if err != nil { 28 | log.Println(err) 29 | return 30 | } 31 | go s.run(c, callback) 32 | } 33 | 34 | func (s *SpotifyIconHandler) IsRunning() bool { 35 | return s.Running 36 | } 37 | 38 | func (s *SpotifyIconHandler) SetRunning(running bool) { 39 | s.Running = running 40 | } 41 | 42 | func (s *SpotifyIconHandler) Stop() { 43 | s.Running = false 44 | s.Quit <- true 45 | } 46 | 47 | func (s *SpotifyIconHandler) run(c *Connection, callback func(image image.Image)) { 48 | defer c.Close() 49 | for { 50 | select { 51 | case <-s.Quit: 52 | return 53 | default: 54 | url, err := c.GetAlbumArtUrl() 55 | if err != nil { 56 | log.Println(err) 57 | time.Sleep(time.Second) 58 | continue 59 | } 60 | if url == s.oldUrl { 61 | time.Sleep(time.Second) 62 | continue 63 | } 64 | img, err := getImage(url) 65 | if err != nil { 66 | log.Println(err) 67 | time.Sleep(time.Second) 68 | continue 69 | } 70 | callback(img) 71 | s.oldUrl = url 72 | time.Sleep(time.Second) 73 | } 74 | } 75 | } 76 | 77 | func RegisterSpotify() handlers.Module { 78 | return handlers.Module{NewIcon: func() api.IconHandler { 79 | return &SpotifyIconHandler{Running: true} 80 | }, Name: "Spotify"} 81 | } 82 | 83 | // region DBus 84 | func getImage(url string) (image.Image, error) { 85 | response, err := http.Get(url) 86 | if err != nil { 87 | return nil, err 88 | } 89 | if response.StatusCode != 200 { 90 | return nil, errors.New("Couldn't get Image from URL") 91 | } 92 | defer response.Body.Close() 93 | img, _, err := image.Decode(response.Body) 94 | if err != nil { 95 | return nil, err 96 | } 97 | return img, nil 98 | } 99 | 100 | 101 | type Connection struct { 102 | busobj dbus.BusObject 103 | conn *dbus.Conn 104 | } 105 | 106 | func Connect() (*Connection, error) { 107 | conn, err := dbus.ConnectSessionBus() 108 | if err != nil { 109 | return nil, err 110 | } 111 | return &Connection{ 112 | conn: conn, 113 | busobj: conn.Object("org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2"), 114 | }, nil 115 | } 116 | 117 | func (c *Connection) GetAlbumArtUrl() (string, error) { 118 | variant, err := c.busobj.GetProperty("org.mpris.MediaPlayer2.Player.Metadata") 119 | if err != nil { 120 | return "", err 121 | } 122 | metadataMap := variant.Value().(map[string]dbus.Variant) 123 | var url string 124 | for key, val := range metadataMap { 125 | if key == "mpris:artUrl" { 126 | url = val.String() 127 | } 128 | } 129 | if url == "" { 130 | return "", errors.New("Couldn't get URL from DBus") 131 | } 132 | url = strings.ReplaceAll(url, "\"", "") 133 | url = strings.ReplaceAll(url, "https://open.spotify.com/image/", "https://i.scdn.co/image/") 134 | return url, nil 135 | } 136 | 137 | func (c *Connection) Close() { 138 | c.conn.Close() 139 | } 140 | 141 | // endregion -------------------------------------------------------------------------------- /handlers/examples/time.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "github.com/unix-streamdeck/api" 5 | "github.com/unix-streamdeck/streamdeckd/handlers" 6 | "image" 7 | "image/draw" 8 | "log" 9 | "time" 10 | ) 11 | 12 | type TimeIconHandler struct { 13 | Running bool 14 | Quit chan bool 15 | } 16 | 17 | func (t *TimeIconHandler) Start(k api.Key, info api.StreamDeckInfo, callback func(image image.Image)) { 18 | t.Running = true 19 | if t.Quit == nil { 20 | t.Quit = make(chan bool) 21 | } 22 | go t.timeLoop(k, info, callback) 23 | } 24 | 25 | func (t *TimeIconHandler) IsRunning() bool { 26 | return t.Running 27 | } 28 | 29 | func (t *TimeIconHandler) SetRunning(running bool) { 30 | t.Running = running 31 | } 32 | 33 | func (t *TimeIconHandler) Stop() { 34 | t.Running = false 35 | t.Quit <- true 36 | } 37 | 38 | func (t *TimeIconHandler) timeLoop(k api.Key, info api.StreamDeckInfo, callback func(image image.Image)) { 39 | for { 40 | select { 41 | case <- t.Quit: 42 | return 43 | default: 44 | img := image.NewRGBA(image.Rect(0, 0, info.IconSize, info.IconSize)) 45 | draw.Draw(img, img.Bounds(), image.Black, image.ZP, draw.Src) 46 | t := time.Now() 47 | tString := t.Format("15:04:05") 48 | imgParsed, err := api.DrawText(img, tString, k.TextSize, k.TextAlignment) 49 | if err != nil { 50 | log.Println(err) 51 | } else { 52 | callback(imgParsed) 53 | } 54 | time.Sleep(time.Second) 55 | } 56 | } 57 | } 58 | 59 | func RegisterTime() handlers.Module { 60 | return handlers.Module{NewIcon: func() api.IconHandler { 61 | return &TimeIconHandler{Running: true} 62 | }, Name: "Time"} 63 | } -------------------------------------------------------------------------------- /handlers/handlers.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/unix-streamdeck/api" 5 | "log" 6 | "plugin" 7 | ) 8 | 9 | type Module struct { 10 | Name string 11 | NewIcon func() api.IconHandler 12 | NewKey func() api.KeyHandler 13 | IconFields []api.Field 14 | KeyFields []api.Field 15 | } 16 | 17 | 18 | 19 | var modules []Module 20 | 21 | 22 | func AvailableModules() []Module { 23 | return modules 24 | } 25 | 26 | func RegisterModule(m Module) { 27 | for _, module := range modules { 28 | if module.Name == m.Name { 29 | log.Println("Module already loaded: " + m.Name) 30 | return 31 | } 32 | } 33 | log.Println("Loaded module " + m.Name) 34 | modules = append(modules, m) 35 | } 36 | 37 | func LoadModule(path string) { 38 | plug, err := plugin.Open(path) 39 | if err != nil { 40 | //log.Println("Failed to load module: " + path) 41 | log.Println(err) 42 | return 43 | } 44 | mod, err := plug.Lookup("GetModule") 45 | if err != nil { 46 | log.Println(err) 47 | return 48 | } 49 | var modMethod func() Module 50 | modMethod, ok := mod.(func() Module) 51 | if !ok { 52 | log.Println("Failed to load module: " + path) 53 | return 54 | } 55 | RegisterModule(modMethod()) 56 | } -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "github.com/unix-streamdeck/api" 6 | _ "github.com/unix-streamdeck/driver" 7 | "github.com/unix-streamdeck/streamdeckd/handlers" 8 | "golang.org/x/sync/semaphore" 9 | "image" 10 | "image/draw" 11 | "log" 12 | "os" 13 | "strings" 14 | ) 15 | 16 | 17 | var sem = semaphore.NewWeighted(int64(1)) 18 | 19 | func LoadImage(dev *VirtualDev, path string) (image.Image, error) { 20 | f, err := os.Open(path) 21 | if err != nil { 22 | return nil, err 23 | } 24 | defer f.Close() 25 | 26 | img, _, err := image.Decode(f) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return api.ResizeImage(img, int(dev.Deck.Pixels)), nil 32 | } 33 | 34 | func SetImage(dev *VirtualDev, img image.Image, i int, page int) { 35 | ctx := context.Background() 36 | err := sem.Acquire(ctx, 1) 37 | if err != nil { 38 | log.Println(err) 39 | return 40 | } 41 | defer sem.Release(1) 42 | if dev.Page == page && dev.IsOpen { 43 | err := dev.Deck.SetImage(uint8(i), img) 44 | if err != nil { 45 | if strings.Contains(err.Error(), "hidapi") { 46 | disconnect(dev) 47 | } else if strings.Contains(err.Error(), "dimensions") { 48 | log.Println(err) 49 | }else { 50 | log.Println(err) 51 | } 52 | } 53 | } 54 | } 55 | 56 | func SetKeyImage(dev *VirtualDev, currentKey *api.Key, i int, page int) { 57 | if currentKey.Buff == nil { 58 | if currentKey.Icon == "" { 59 | img := image.NewRGBA(image.Rect(0, 0, int(dev.Deck.Pixels), int(dev.Deck.Pixels))) 60 | draw.Draw(img, img.Bounds(), image.Black, image.ZP, draw.Src) 61 | currentKey.Buff = img 62 | } else { 63 | img, err := LoadImage(dev, currentKey.Icon) 64 | if err != nil { 65 | log.Println(err) 66 | return 67 | } 68 | currentKey.Buff = img 69 | } 70 | if currentKey.Text != "" { 71 | img, err := api.DrawText(currentKey.Buff, currentKey.Text, currentKey.TextSize, currentKey.TextAlignment) 72 | if err != nil { 73 | log.Println(err) 74 | } else { 75 | currentKey.Buff = img 76 | } 77 | } 78 | } 79 | if currentKey.Buff != nil { 80 | SetImage(dev, currentKey.Buff, i, page) 81 | } 82 | } 83 | 84 | func SetPage(dev *VirtualDev, page int) { 85 | if page != dev.Page { 86 | unmountPageHandlers(dev.Config[dev.Page]) 87 | } 88 | dev.Page = page 89 | currentPage := dev.Config[page] 90 | for i := 0; i < len(currentPage); i++ { 91 | currentKey := ¤tPage[i] 92 | go SetKey(dev, currentKey, i, page) 93 | } 94 | EmitPage(dev, page) 95 | } 96 | 97 | func SetKey(dev *VirtualDev, currentKey *api.Key, i int, page int) { 98 | var deckInfo api.StreamDeckInfo 99 | for i := range sDInfo { 100 | if sDInfo[i].Serial == dev.Deck.Serial { 101 | deckInfo = sDInfo[i] 102 | } 103 | } 104 | if currentKey.Buff == nil { 105 | if currentKey.IconHandler == "" { 106 | SetKeyImage(dev, currentKey, i, page) 107 | 108 | } else if currentKey.IconHandlerStruct == nil { 109 | var handler api.IconHandler 110 | modules := handlers.AvailableModules() 111 | for _, module := range modules { 112 | if module.Name == currentKey.IconHandler { 113 | handler = module.NewIcon() 114 | } 115 | } 116 | if handler == nil { 117 | return 118 | } 119 | log.Printf("Created & Started %s\n", currentKey.IconHandler) 120 | handler.Start(*currentKey, deckInfo, func(image image.Image) { 121 | if image.Bounds().Max.X != int(dev.Deck.Pixels) || image.Bounds().Max.Y != int(dev.Deck.Pixels) { 122 | image = api.ResizeImage(image, int(dev.Deck.Pixels)) 123 | } 124 | SetImage(dev, image, i, page) 125 | currentKey.Buff = image 126 | }) 127 | currentKey.IconHandlerStruct = handler 128 | } 129 | } else { 130 | SetImage(dev, currentKey.Buff, i, page) 131 | } 132 | if currentKey.IconHandlerStruct != nil && !currentKey.IconHandlerStruct.IsRunning() { 133 | log.Printf("Started %s\n", currentKey.IconHandler) 134 | currentKey.IconHandlerStruct.Start(*currentKey, deckInfo, func(image image.Image) { 135 | if image.Bounds().Max.X != int(dev.Deck.Pixels) || image.Bounds().Max.Y != int(dev.Deck.Pixels) { 136 | image = api.ResizeImage(image, int(dev.Deck.Pixels)) 137 | } 138 | SetImage(dev, image, i, page) 139 | currentKey.Buff = image 140 | }) 141 | } 142 | } 143 | 144 | func HandleInput(dev *VirtualDev, key *api.Key, page int) { 145 | if key.Command != "" { 146 | runCommand(key.Command) 147 | } 148 | if key.Keybind != "" { 149 | runCommand("xdotool key " + key.Keybind) 150 | } 151 | if key.SwitchPage != 0 { 152 | page = key.SwitchPage - 1 153 | SetPage(dev, page) 154 | } 155 | if key.Brightness != 0 { 156 | err := dev.Deck.SetBrightness(uint8(key.Brightness)) 157 | if err != nil { 158 | log.Println(err) 159 | } 160 | } 161 | if key.Url != "" { 162 | runCommand("xdg-open " + key.Url) 163 | } 164 | if key.KeyHandler != "" { 165 | var deckInfo api.StreamDeckInfo 166 | found := false 167 | for i := range sDInfo { 168 | if sDInfo[i].Serial == dev.Deck.Serial { 169 | deckInfo = sDInfo[i] 170 | found = true 171 | } 172 | } 173 | if !found { 174 | return 175 | } 176 | if key.KeyHandlerStruct == nil { 177 | var handler api.KeyHandler 178 | modules := handlers.AvailableModules() 179 | for _, module := range modules { 180 | if module.Name == key.KeyHandler { 181 | handler = module.NewKey() 182 | } 183 | } 184 | if handler == nil { 185 | return 186 | } 187 | key.KeyHandlerStruct = handler 188 | } 189 | key.KeyHandlerStruct.Key(*key, deckInfo) 190 | } 191 | } 192 | 193 | func Listen(dev *VirtualDev) { 194 | kch, err := dev.Deck.ReadKeys() 195 | if err != nil { 196 | log.Println(err) 197 | } 198 | for dev.IsOpen { 199 | select { 200 | case k, ok := <-kch: 201 | if !ok { 202 | disconnect(dev) 203 | return 204 | } 205 | if k.Pressed == true { 206 | if len(dev.Config)-1 >= dev.Page && len(dev.Config[dev.Page])-1 >= int(k.Index) { 207 | HandleInput(dev, &dev.Config[dev.Page][k.Index], dev.Page) 208 | } 209 | } 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "github.com/shirou/gopsutil/v3/process" 10 | "github.com/unix-streamdeck/api" 11 | "github.com/unix-streamdeck/driver" 12 | "github.com/unix-streamdeck/streamdeckd/handlers" 13 | "github.com/unix-streamdeck/streamdeckd/handlers/examples" 14 | "golang.org/x/sync/semaphore" 15 | _ "image/gif" 16 | _ "image/jpeg" 17 | _ "image/png" 18 | "io/ioutil" 19 | "log" 20 | "os" 21 | "os/exec" 22 | "os/signal" 23 | "syscall" 24 | "time" 25 | ) 26 | 27 | type VirtualDev struct { 28 | Deck streamdeck.Device 29 | Page int 30 | IsOpen bool 31 | Config []api.Page 32 | } 33 | 34 | var devs map[string]*VirtualDev 35 | var config *api.Config 36 | var migrateConfig = false 37 | var configPath string 38 | var disconnectSem = semaphore.NewWeighted(1) 39 | var connectSem = semaphore.NewWeighted(1) 40 | var basicConfig = api.Config{ 41 | Modules: []string{}, 42 | Decks: []api.Deck{ 43 | {}, 44 | }, 45 | } 46 | var isRunning = true 47 | 48 | func main() { 49 | checkOtherRunningInstances() 50 | configPtr := flag.String("config", configPath, "Path to config file") 51 | flag.Parse() 52 | if *configPtr != "" { 53 | configPath = *configPtr 54 | } else { 55 | basePath := os.Getenv("HOME") + string(os.PathSeparator) + ".config" 56 | if os.Getenv("XDG_CONFIG_HOME") != "" { 57 | basePath = os.Getenv("XDG_CONFIG_HOME") 58 | } 59 | configPath = basePath + string(os.PathSeparator) + ".streamdeck-config.json" 60 | } 61 | cleanupHook() 62 | go InitDBUS() 63 | examples.RegisterBaseModules() 64 | loadConfig() 65 | devs = make(map[string]*VirtualDev) 66 | attemptConnection() 67 | } 68 | 69 | func checkOtherRunningInstances() { 70 | processes, err := process.Processes() 71 | if err != nil { 72 | log.Println("Could not check for other instances of streamdeckd, assuming no others running") 73 | } 74 | for _, proc := range processes { 75 | name, err := proc.Name() 76 | if err == nil && name == "streamdeckd" && int(proc.Pid) != os.Getpid() { 77 | log.Fatalln("Another instance of streamdeckd is already running, exiting...") 78 | } 79 | } 80 | } 81 | 82 | func attemptConnection() { 83 | for isRunning { 84 | dev := &VirtualDev{} 85 | dev, _ = openDevice() 86 | if dev.IsOpen { 87 | SetPage(dev, dev.Page) 88 | found := false 89 | for i := range sDInfo { 90 | if sDInfo[i].Serial == dev.Deck.Serial { 91 | found = true 92 | } 93 | } 94 | if !found { 95 | sDInfo = append(sDInfo, api.StreamDeckInfo{ 96 | Cols: int(dev.Deck.Columns), 97 | Rows: int(dev.Deck.Rows), 98 | IconSize: int(dev.Deck.Pixels), 99 | Page: 0, 100 | Serial: dev.Deck.Serial, 101 | }) 102 | } 103 | go Listen(dev) 104 | } 105 | time.Sleep(250 * time.Millisecond) 106 | } 107 | } 108 | 109 | func disconnect(dev *VirtualDev) { 110 | ctx := context.Background() 111 | err := disconnectSem.Acquire(ctx, 1) 112 | if err != nil { 113 | return 114 | } 115 | defer disconnectSem.Release(1) 116 | if !dev.IsOpen { 117 | return 118 | } 119 | log.Println("Device (" + dev.Deck.Serial + ") disconnected") 120 | _ = dev.Deck.Close() 121 | dev.IsOpen = false 122 | unmountDevHandlers(dev) 123 | } 124 | 125 | func openDevice() (*VirtualDev, error) { 126 | ctx := context.Background() 127 | err := connectSem.Acquire(ctx, 1) 128 | if err != nil { 129 | return &VirtualDev{}, err 130 | } 131 | defer connectSem.Release(1) 132 | d, err := streamdeck.Devices() 133 | if err != nil { 134 | return &VirtualDev{}, err 135 | } 136 | if len(d) == 0 { 137 | return &VirtualDev{}, errors.New("No streamdeck devices found") 138 | } 139 | device := streamdeck.Device{Serial: ""} 140 | for i := range d { 141 | found := false 142 | for s := range devs { 143 | if d[i].ID == devs[s].Deck.ID && devs[s].IsOpen { 144 | found = true 145 | break 146 | } else if d[i].Serial == s && !devs[s].IsOpen { 147 | err = d[i].Open() 148 | if err != nil { 149 | return &VirtualDev{}, err 150 | } 151 | devs[s].Deck = d[i] 152 | devs[s].IsOpen = true 153 | return devs[s], nil 154 | } 155 | } 156 | if !found { 157 | device = d[i] 158 | } 159 | } 160 | if len(device.Serial) != 12 { 161 | return &VirtualDev{}, errors.New("No streamdeck devices found") 162 | } 163 | err = device.Open() 164 | if err != nil { 165 | return &VirtualDev{}, err 166 | } 167 | devNo := -1 168 | if migrateConfig { 169 | config.Decks[0].Serial = device.Serial 170 | _ = SaveConfig() 171 | migrateConfig = false 172 | } 173 | for i := range config.Decks { 174 | if config.Decks[i].Serial == device.Serial { 175 | devNo = i 176 | } 177 | } 178 | if devNo == -1 { 179 | var pages []api.Page 180 | page := api.Page{} 181 | for i := 0; i < int(device.Rows)*int(device.Columns); i++ { 182 | page = append(page, api.Key{}) 183 | } 184 | pages = append(pages, page) 185 | config.Decks = append(config.Decks, api.Deck{Serial: device.Serial, Pages: pages}) 186 | devNo = len(config.Decks) - 1 187 | } 188 | dev := &VirtualDev{Deck: device, Page: 0, IsOpen: true, Config: config.Decks[devNo].Pages} 189 | devs[device.Serial] = dev 190 | log.Println("Device (" + device.Serial + ") connected") 191 | return dev, nil 192 | } 193 | 194 | func loadConfig() { 195 | var err error 196 | config, err = readConfig() 197 | if err != nil && !os.IsNotExist(err) { 198 | log.Println(err) 199 | } else if os.IsNotExist(err) { 200 | file, err := os.Create(configPath) 201 | if err != nil { 202 | log.Println(err) 203 | } 204 | err = file.Close() 205 | if err != nil { 206 | log.Println(err) 207 | } 208 | config = &basicConfig 209 | err = SaveConfig() 210 | if err != nil { 211 | log.Println(err) 212 | } 213 | } 214 | if len(config.Modules) > 0 { 215 | for _, module := range config.Modules { 216 | handlers.LoadModule(module) 217 | } 218 | } 219 | } 220 | 221 | func readConfig() (*api.Config, error) { 222 | data, err := ioutil.ReadFile(configPath) 223 | if err != nil { 224 | return &api.Config{}, err 225 | } 226 | var config api.Config 227 | err = json.Unmarshal(data, &config) 228 | if err != nil || config.Decks == nil { 229 | var deprecatedConfig api.DepracatedConfig 230 | err = json.Unmarshal(data, &deprecatedConfig) 231 | if err != nil { 232 | return &api.Config{}, err 233 | } 234 | config = api.Config{Modules: deprecatedConfig.Modules, Decks: []api.Deck{{Pages: deprecatedConfig.Pages, Serial: ""}}} 235 | migrateConfig = true 236 | } 237 | return &config, nil 238 | } 239 | 240 | func runCommand(command string) { 241 | go func() { 242 | cmd := exec.Command("/bin/sh", "-c", "/usr/bin/nohup "+command) 243 | 244 | cmd.SysProcAttr = &syscall.SysProcAttr{ 245 | Setpgid: true, 246 | Pgid: 0, 247 | Pdeathsig: syscall.SIGHUP, 248 | } 249 | if err := cmd.Start(); err != nil { 250 | fmt.Println("There was a problem running ", command, ":", err) 251 | } else { 252 | pid := cmd.Process.Pid 253 | cmd.Process.Release() 254 | fmt.Println(command, " has been started with pid", pid) 255 | } 256 | }() 257 | } 258 | 259 | func cleanupHook() { 260 | sigs := make(chan os.Signal, 1) 261 | signal.Notify(sigs, syscall.SIGSTOP, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGINT) 262 | go func() { 263 | <-sigs 264 | log.Println("Cleaning up") 265 | isRunning = false 266 | unmountHandlers() 267 | var err error 268 | for s := range devs { 269 | if devs[s].IsOpen { 270 | err = devs[s].Deck.Reset() 271 | if err != nil { 272 | log.Println(err) 273 | } 274 | err = devs[s].Deck.Close() 275 | if err != nil { 276 | log.Println(err) 277 | } 278 | } 279 | } 280 | os.Exit(0) 281 | }() 282 | } 283 | 284 | func SetConfig(configString string) error { 285 | unmountHandlers() 286 | var err error 287 | config = nil 288 | err = json.Unmarshal([]byte(configString), &config) 289 | if err != nil { 290 | return err 291 | } 292 | for s := range devs { 293 | dev := devs[s] 294 | for i := range config.Decks { 295 | if dev.Deck.Serial == config.Decks[i].Serial { 296 | dev.Config = config.Decks[i].Pages 297 | } 298 | } 299 | SetPage(dev, devs[s].Page) 300 | } 301 | return nil 302 | } 303 | 304 | func ReloadConfig() error { 305 | unmountHandlers() 306 | loadConfig() 307 | for s := range devs { 308 | dev := devs[s] 309 | for i := range config.Decks { 310 | if dev.Deck.Serial == config.Decks[i].Serial { 311 | dev.Config = config.Decks[i].Pages 312 | } 313 | } 314 | SetPage(dev, devs[s].Page) 315 | } 316 | return nil 317 | } 318 | 319 | func SaveConfig() error { 320 | f, err := os.OpenFile(configPath, os.O_TRUNC|os.O_RDWR|os.O_CREATE, 0755) 321 | if err != nil { 322 | return err 323 | } 324 | defer f.Close() 325 | var configString []byte 326 | configString, err = json.Marshal(config) 327 | if err != nil { 328 | return err 329 | } 330 | _, err = f.Write(configString) 331 | if err != nil { 332 | return err 333 | } 334 | err = f.Sync() 335 | if err != nil { 336 | return err 337 | } 338 | return nil 339 | } 340 | func unmountHandlers() { 341 | for s := range devs { 342 | dev := devs[s] 343 | unmountDevHandlers(dev) 344 | } 345 | } 346 | 347 | func unmountDevHandlers(dev *VirtualDev) { 348 | for i := range dev.Config { 349 | unmountPageHandlers(dev.Config[i]) 350 | } 351 | } 352 | 353 | func unmountPageHandlers(page api.Page) { 354 | for i2 := 0; i2 < len(page); i2++ { 355 | key := &page[i2] 356 | if key.IconHandlerStruct != nil { 357 | log.Printf("Stopping %s\n", key.IconHandler) 358 | if key.IconHandlerStruct.IsRunning() { 359 | go func() { 360 | key.IconHandlerStruct.Stop() 361 | log.Printf("Stopped %s\n", key.IconHandler) 362 | }() 363 | } 364 | } 365 | } 366 | } 367 | --------------------------------------------------------------------------------