commit 5c6e496688c29fa79e3db0ed97b47ef9f0ce62e3 Author: mikx Date: Tue Oct 31 20:16:41 2023 -0400 Added support for remote0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9309839 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Vendored libs +/vendor + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# VSCode launch settings +.vscode/launch.json + +# GoLand +.idea + +# Test graph.png +graph.png + +# Debug +cpu.prof +mem.prof +cmd/hwinfostreamdeckplugin/debug + +# dep cache (protoc) +.cache/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..be9c746 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "files.eol": "\n", + "editor.formatOnSave": true, + "go.useLanguageServer": true, + "taskExplorer.pathToMake": "make" +} diff --git a/DejaVuSans-Bold.ttf b/DejaVuSans-Bold.ttf new file mode 100644 index 0000000..6d65fa7 Binary files /dev/null and b/DejaVuSans-Bold.ttf differ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..61d67f8 --- /dev/null +++ b/Makefile @@ -0,0 +1,40 @@ +GOCMD=go +GOBUILD=$(GOCMD) build +GOCLEAN=$(GOCMD) clean + +SDPLUGINDIR=./com.exension.hwinfo.sdPlugin + +PROTOS=$(wildcard ./*/**/**/*.proto) +PROTOPB=$(PROTOS:.proto=.pb.go) + +plugin: proto + $(GOBUILD) -o $(SDPLUGINDIR)/hwinfo.exe ./cmd/hwinfo_streamdeck_plugin + $(GOBUILD) -o $(SDPLUGINDIR)/hwinfo-plugin.exe ./cmd/hwinfo-plugin + cp ../go-hwinfo-hwservice-plugin/bin/hwinfo-plugin.exe $(SDPLUGINDIR)/hwinfo-plugin.exe + -@install-plugin.bat + +proto: $(PROTOPB) + +$(PROTOPB): $(PROTOS) + .cache/protoc/bin/protoc \ + --go_out=Mgrpc/service_config/service_config.proto=/internal/proto/grpc_service_config:. \ + --go-grpc_out=Mgrpc/service_config/service_config.proto=/internal/proto/grpc_service_config:. \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + $(<) + +# plugin: +# -@kill-streamdeck.bat +# @go build -o com.exension.hwinfo.sdPlugin\\hwinfo.exe github.com/shayne/hwinfo-streamdeck/cmd/hwinfo_streamdeck_plugin +# @xcopy com.exension.hwinfo.sdPlugin $(APPDATA)\\Elgato\\StreamDeck\\Plugins\\com.exension.hwinfo.sdPlugin\\ /E /Q /Y +# @start-streamdeck.bat + +debug: + $(GOBUILD) -o $(SDPLUGINDIR)/hwinfo.exe ./cmd/hwinfo_debugger + cp ../go-grpc-hardware-service/bin/hwinfo-plugin.exe $(SDPLUGINDIR)/hwinfo-plugin.exe + -@install-plugin.bat +# @xcopy com.exension.hwinfo.sdPlugin $(APPDATA)\\Elgato\\StreamDeck\\Plugins\\com.exension.hwinfo.sdPlugin\\ /E /Q /Y + +release: + -@rm build/com.exension.hwinfo.streamDeckPlugin + @DistributionTool.exe -b -i com.exension.hwinfo.sdPlugin -o build diff --git a/README.md b/README.md new file mode 100644 index 0000000..66448df --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# HWiNFO Stream Deck Plugin + +## ⚠⚠ Major refactor landed in pre-release v2.0.0, plugin code open sourced, remote monitoring infrastructure support ⚠⚠ + +--- + +>## Thank you & Looking for Maintainers +> +>Thank you everyone who has used and enjoyed this plugin. It started as a passion project and I continue to use it day to day. I am happy to finally release the full source on GitHub. When I first built it, it was closed under agreement with the HWiNFO64 project. They have since opened up the shared memory interface and now the plugin is freely open. +> +>I haven't had the time to dedicate to this project in some time and appreciate everyone for hanging in there. I hope to work with some of you who are eager to take the project over. I am happy and ready to hand over the reigns. If there are development questions I'm happy to share my thoughts on the code and structure that exists. +> +>*-Shayne* + +--- + +![alt text](images/demo.gif "HWiNFO64 Stream Deck Plugin Demo") + +> NOTICE: HWiNFO64 must be run in Sensors-only mode for the plugin to work. + +## Enabling Support in HWiNFO64 + +> NOTICE: It has been reported that running the "portable" version of HWiNFO64 doesn't work with this plugin. The recommendation is to run the version with the installer until I can figure out the issue. + +1. Download and install HWiNFO64, if you haven't already + + [HWiNFO Website](https://www.hwinfo.com) + +2. Choose "Sensors-only" mode + + ![alt text](images/sensorsonly.png "HWiNFO64 Sensors Only") + +3. Click "Settings" + + ![alt text](images/clicksettings.png "HWiNFO64 Click Settings") + +4. Ensure "Shared Memory Support" is checked + + ![alt text](images/sharedmemory.png "HWiNFO64 Settings") + +5. (Optional) Recommended launch settings + + ![alt text](images/recommendedsettings.png "Quit HWiNFO64") + +6. Click "OK" then, "Run" + + > If the plugin doesn't work immediately, you may have to quit and reopen HWiNFO64. + > + > From the system tray: + > + > ![alt text](images/contextquit.png "Quit HWiNFO64") + + +## Install and Setup the Plugin + +1. Download the latest pre-compiled plugin + + [Plugin Releases](../../releases) + + > When upgrading, first uninstall: within the Stream Deck app choose "More Actions..." (bottom-right), locate "HWiNFO" and choose "Uninstall". Your tiles and settings will be preserved. + +2. Double-click to install the plugin + +3. Choose "Install" went prompted by Stream Deck + + ![alt text](images/streamdeckinstall.png "Stream Deck Plugin Installation") + +4. Locate "HWiNFO" under "Custom" in the action list + + ![alt text](images/streamdeckactionlist.png "Stream Deck Action List") + +5. Drag the "HWiNFO" action from the list to a tile in the canvas area + + ![alt text](images/dragaction.gif "Drag Action") + +6. Configure the action to display the sensor reading you wish + + ![alt text](images/configureaction.gif "Configure Action") diff --git a/build/com.exension.hwinfo.streamDeckPlugin b/build/com.exension.hwinfo.streamDeckPlugin new file mode 100644 index 0000000..cf3ba1d Binary files /dev/null and b/build/com.exension.hwinfo.streamDeckPlugin differ diff --git a/build/images/clicksettings.png b/build/images/clicksettings.png new file mode 100644 index 0000000..72a6832 Binary files /dev/null and b/build/images/clicksettings.png differ diff --git a/build/images/configureaction.gif b/build/images/configureaction.gif new file mode 100644 index 0000000..68b88bb Binary files /dev/null and b/build/images/configureaction.gif differ diff --git a/build/images/contextquit.png b/build/images/contextquit.png new file mode 100644 index 0000000..201ac92 Binary files /dev/null and b/build/images/contextquit.png differ diff --git a/build/images/demo.gif b/build/images/demo.gif new file mode 100644 index 0000000..42dc386 Binary files /dev/null and b/build/images/demo.gif differ diff --git a/build/images/dragaction.gif b/build/images/dragaction.gif new file mode 100644 index 0000000..6bb833a Binary files /dev/null and b/build/images/dragaction.gif differ diff --git a/build/images/recommendedsettings.png b/build/images/recommendedsettings.png new file mode 100644 index 0000000..4fbf32d Binary files /dev/null and b/build/images/recommendedsettings.png differ diff --git a/build/images/sensorsonly.png b/build/images/sensorsonly.png new file mode 100644 index 0000000..53a1ad2 Binary files /dev/null and b/build/images/sensorsonly.png differ diff --git a/build/images/sharedmemory.png b/build/images/sharedmemory.png new file mode 100644 index 0000000..07a7201 Binary files /dev/null and b/build/images/sharedmemory.png differ diff --git a/build/images/streamdeckactionlist.png b/build/images/streamdeckactionlist.png new file mode 100644 index 0000000..21b7b7d Binary files /dev/null and b/build/images/streamdeckactionlist.png differ diff --git a/build/images/streamdeckinstall.png b/build/images/streamdeckinstall.png new file mode 100644 index 0000000..7fd3d21 Binary files /dev/null and b/build/images/streamdeckinstall.png differ diff --git a/cmd/hwinfo-plugin/main.go b/cmd/hwinfo-plugin/main.go new file mode 100644 index 0000000..2613ccc --- /dev/null +++ b/cmd/hwinfo-plugin/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "log" + + "github.com/hashicorp/go-plugin" + hwinfoplugin "github.com/shayne/hwinfo-streamdeck/internal/hwinfo/plugin" + hwsensorsservice "github.com/shayne/hwinfo-streamdeck/pkg/service" +) + +func main() { + service := hwinfoplugin.StartService() + go func() { + for { + err := service.Recv() + if err != nil { + log.Printf("service recv failed: %v\n", err) + } + } + }() + + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: hwsensorsservice.Handshake, + Plugins: map[string]plugin.Plugin{ + "hwinfoplugin": &hwsensorsservice.HardwareServicePlugin{Impl: &hwinfoplugin.Plugin{Service: service}}, + }, + + // A non-nil value here enables gRPC serving for this plugin... + GRPCServer: plugin.DefaultGRPCServer, + }) +} diff --git a/cmd/hwinfo_debugger/main.go b/cmd/hwinfo_debugger/main.go new file mode 100644 index 0000000..3bec856 --- /dev/null +++ b/cmd/hwinfo_debugger/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "encoding/json" + "flag" + "log" + "os" + "path/filepath" +) + +var port = flag.String("port", "", "The port that should be used to create the WebSocket") +var pluginUUID = flag.String("pluginUUID", "", "A unique identifier string that should be used to register the plugin once the WebSocket is opened") +var registerEvent = flag.String("registerEvent", "", "Registration event") +var info = flag.String("info", "", "A stringified json containing the Stream Deck application information and devices information") + +func main() { + appdata := os.Getenv("APPDATA") + logpath := filepath.Join(appdata, "Elgato/StreamDeck/Plugins/com.exension.hwinfo.sdPlugin/hwinfo.log") + f, err := os.OpenFile(logpath, os.O_RDWR|os.O_CREATE, 0666) + f.Truncate(0) + if err != nil { + log.Fatalf("OpenFile Log: %v", err) + } + defer f.Close() + log.SetOutput(f) + log.SetFlags(0) + + flag.Parse() + + args := []string{ + "-port", + *port, + "-pluginUUID", + *pluginUUID, + "-registerEvent", + *registerEvent, + "-info", + *info, + } + bytes, err := json.MarshalIndent(args, "", " ") + if err != nil { + log.Fatal("Failed to marshal args", err) + } + + log.Println(string(bytes)) +} diff --git a/cmd/hwinfo_streamdeck_plugin/main.go b/cmd/hwinfo_streamdeck_plugin/main.go new file mode 100644 index 0000000..f33d855 --- /dev/null +++ b/cmd/hwinfo_streamdeck_plugin/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "flag" + "io/ioutil" + "log" + + // "net/http" + // _ "net/http/pprof" + "os" + "path/filepath" + + plugin "github.com/shayne/hwinfo-streamdeck/internal/app/hwinfostreamdeckplugin" +) + +var port = flag.String("port", "", "The port that should be used to create the WebSocket") +var pluginUUID = flag.String("pluginUUID", "", "A unique identifier string that should be used to register the plugin once the WebSocket is opened") +var registerEvent = flag.String("registerEvent", "", "Registration event") +var info = flag.String("info", "", "A stringified json containing the Stream Deck application information and devices information") + +func main() { + // go func() { + // log.Println(http.ListenAndServe("localhost:6060", nil)) + // }() + + // make sure files are read relative to exe + err := os.Chdir(filepath.Dir(os.Args[0])) + if err != nil { + log.Fatalf("Unable to chdir: %v", err) + } + + // PRODUCTION + // LOGGING DISABLED: + // + log.SetOutput(ioutil.Discard) + + // DEBUG LOGGING: + // + // appdata := os.Getenv("APPDATA") + // logpath := filepath.Join(appdata, "Elgato/StreamDeck/Plugins/com.exension.hwinfo.sdPlugin/hwinfo.log") + // f, err := os.OpenFile(logpath, os.O_RDWR|os.O_CREATE, 0666) + // if err != nil { + // log.Fatalf("OpenFile Log: %v", err) + // } + // err = f.Truncate(0) + // if err != nil { + // log.Fatalf("Truncate Log: %v", err) + // } + // defer func() { + // err := f.Close() + // if err != nil { + // log.Fatalf("File Close: %v", err) + // } + // }() + // log.SetOutput(f) + // log.SetFlags(0) + + flag.Parse() + + p, err := plugin.NewPlugin(*port, *pluginUUID, *registerEvent, *info) + if err != nil { + log.Fatal("NewPlugin failed:", err) + } + + err = p.RunForever() + if err != nil { + log.Fatal("runForever", err) + } +} diff --git a/com.exension.hwinfo.sdPlugin/DejaVuSans-Bold.ttf b/com.exension.hwinfo.sdPlugin/DejaVuSans-Bold.ttf new file mode 100644 index 0000000..6d65fa7 Binary files /dev/null and b/com.exension.hwinfo.sdPlugin/DejaVuSans-Bold.ttf differ diff --git a/com.exension.hwinfo.sdPlugin/css/buttons.png b/com.exension.hwinfo.sdPlugin/css/buttons.png new file mode 100644 index 0000000..c60438e Binary files /dev/null and b/com.exension.hwinfo.sdPlugin/css/buttons.png differ diff --git a/com.exension.hwinfo.sdPlugin/css/buttons@2x.png b/com.exension.hwinfo.sdPlugin/css/buttons@2x.png new file mode 100644 index 0000000..16445ed Binary files /dev/null and b/com.exension.hwinfo.sdPlugin/css/buttons@2x.png differ diff --git a/com.exension.hwinfo.sdPlugin/css/caret.svg b/com.exension.hwinfo.sdPlugin/css/caret.svg new file mode 100644 index 0000000..b69162a --- /dev/null +++ b/com.exension.hwinfo.sdPlugin/css/caret.svg @@ -0,0 +1,3 @@ + + + diff --git a/com.exension.hwinfo.sdPlugin/css/check.png b/com.exension.hwinfo.sdPlugin/css/check.png new file mode 100644 index 0000000..b5e9e62 Binary files /dev/null and b/com.exension.hwinfo.sdPlugin/css/check.png differ diff --git a/com.exension.hwinfo.sdPlugin/css/check.svg b/com.exension.hwinfo.sdPlugin/css/check.svg new file mode 100644 index 0000000..5b96af0 --- /dev/null +++ b/com.exension.hwinfo.sdPlugin/css/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/com.exension.hwinfo.sdPlugin/css/elg_calendar.svg b/com.exension.hwinfo.sdPlugin/css/elg_calendar.svg new file mode 100644 index 0000000..157e01b --- /dev/null +++ b/com.exension.hwinfo.sdPlugin/css/elg_calendar.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/com.exension.hwinfo.sdPlugin/css/elg_calendar_inv.svg b/com.exension.hwinfo.sdPlugin/css/elg_calendar_inv.svg new file mode 100644 index 0000000..4f8af68 --- /dev/null +++ b/com.exension.hwinfo.sdPlugin/css/elg_calendar_inv.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/com.exension.hwinfo.sdPlugin/css/elg_calendar_inv_13.svg b/com.exension.hwinfo.sdPlugin/css/elg_calendar_inv_13.svg new file mode 100644 index 0000000..8927f47 --- /dev/null +++ b/com.exension.hwinfo.sdPlugin/css/elg_calendar_inv_13.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/com.exension.hwinfo.sdPlugin/css/local.css b/com.exension.hwinfo.sdPlugin/css/local.css new file mode 100644 index 0000000..57e0166 --- /dev/null +++ b/com.exension.hwinfo.sdPlugin/css/local.css @@ -0,0 +1,18 @@ +body, +.localbody { + height: 100%; + padding: 0; + overflow-x: hidden; + overflow-y: auto; + margin: 0; + -webkit-overflow-scrolling: touch; +} + +.localbody { + width: 350px; + margin: 0 auto; +} + +.hidden { + display: block; +} \ No newline at end of file diff --git a/com.exension.hwinfo.sdPlugin/css/pi_required.svg b/com.exension.hwinfo.sdPlugin/css/pi_required.svg new file mode 100644 index 0000000..53479b7 --- /dev/null +++ b/com.exension.hwinfo.sdPlugin/css/pi_required.svg @@ -0,0 +1,3 @@ + + + diff --git a/com.exension.hwinfo.sdPlugin/css/pi_required_ok.svg b/com.exension.hwinfo.sdPlugin/css/pi_required_ok.svg new file mode 100644 index 0000000..1a0784d --- /dev/null +++ b/com.exension.hwinfo.sdPlugin/css/pi_required_ok.svg @@ -0,0 +1,3 @@ + + + diff --git a/com.exension.hwinfo.sdPlugin/css/rcheck.svg b/com.exension.hwinfo.sdPlugin/css/rcheck.svg new file mode 100644 index 0000000..af478ee --- /dev/null +++ b/com.exension.hwinfo.sdPlugin/css/rcheck.svg @@ -0,0 +1,3 @@ + + + diff --git a/com.exension.hwinfo.sdPlugin/css/reset.min.css b/com.exension.hwinfo.sdPlugin/css/reset.min.css new file mode 100644 index 0000000..8fb0142 --- /dev/null +++ b/com.exension.hwinfo.sdPlugin/css/reset.min.css @@ -0,0 +1,43 @@ +html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, +pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, +q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, +dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, +thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, +footer, header, hgroup, menu, nav, output, ruby, section, summary, time, +mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline +} + +article, aside, details, figcaption, figure, footer, header, hgroup, +menu, nav, section { + display: block +} + +body { + line-height: 1 +} + +ol, ul { + list-style: none +} + +blockquote, q { + quotes: none +} + +blockquote:before, blockquote:after, q:before, q:after { + content: ''; + content: none +} + +table { + border-collapse: collapse; + border-spacing: 0 +} + + diff --git a/com.exension.hwinfo.sdPlugin/css/sdpi.css b/com.exension.hwinfo.sdPlugin/css/sdpi.css new file mode 100644 index 0000000..5a9ea7e --- /dev/null +++ b/com.exension.hwinfo.sdPlugin/css/sdpi.css @@ -0,0 +1,1747 @@ +html { + --sdpi-bgcolor: #2D2D2D; + --sdpi-background: #3D3D3D; + --sdpi-color: #d8d8d8; + --sdpi-bordercolor: #3a3a3a; + --sdpi-buttonbordercolor: #969696; + --sdpi-borderradius: 0px; + --sdpi-width: 224px; + --sdpi-fontweight: 600; + --sdpi-letterspacing: -0.25pt; + --sdpi-tab-color: #969696; + --sdpi-tab-left-margin: 1px; + --sdpi-tab-top-offset: 1px; + --sdpi-tab-selected-color: #333333; + --sdpi-tab-selected-top-offset: 0px; + --sdpi-tab-font-size: 9pt; + --sdpi-tab-container-left-offset: 5px; + --sdpi-tab-padding-horizontal: 12px; + --sdpi-tab-padding-vertical: 5px; + --sdpi-linecolor: #454545; + height: 100%; + width: 100%; + overflow: hidden; + touch-action: none; +} + +html, +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-size: 9pt; + background-color: var(--sdpi-bgcolor); + color: #9a9a9a; +} + +body { + height: 100%; + padding: 0; + overflow-x: hidden; + overflow-y: auto; + margin: 0; + -webkit-overflow-scrolling: touch; + -webkit-text-size-adjust: 100%; + -webkit-font-smoothing: antialiased; +} + +mark { + background-color: var(--sdpi-bgcolor); + color: var(--sdpi-color); +} + +hr, +hr2 { + -webkit-margin-before: 1em; + -webkit-margin-after: 1em; + border-style: none; + background: var(--sdpi-background); + height: 1px; +} + +hr2, +.sdpi-heading { + display: flex; + flex-basis: 100%; + align-items: center; + color: inherit; + font-size: 9pt; + margin: 8px 0px; +} + + +h1 { + font-size: 1.3em; + font-weight: 500; + text-align: center; + margin-bottom: 12px; +} + +.sdpi-heading::before, +.sdpi-heading::after { + content: ""; + flex-grow: 1; + background: var(--sdpi-background); + height: 1px; + font-size: 0px; + line-height: 0px; + margin: 0px 16px; +} + +hr2 { + height: 2px; +} + +hr, +hr2 { + margin-left: 16px; + margin-right: 16px; +} + +.sdpi-item-value, +option, +input, +select, +button { + font-size: 10pt; + font-weight: var(--sdpi-fontweight); + letter-spacing: var(--sdpi-letterspacing); +} + +.sdpi-item-value> :last-of-type, +.sdpi-item-value:last-child { + margin-bottom: 4px; +} + +.win .sdpi-item-value, +.win option, +.win input, +.win select, +.win button { + font-size: 11px; + font-style: normal; + letter-spacing: inherit; + font-weight: 100; +} + +.win button { + font-size: 12px; +} + +::-webkit-progress-value, +meter::-webkit-meter-optimum-value { + border-radius: 2px; + /* background: linear-gradient(#ccf, #99f 20%, #77f 45%, #77f 55%, #cdf); */ +} + +::-webkit-progress-bar, +meter::-webkit-meter-bar { + border-radius: 3px; + background: var(--sdpi-background); +} + +::-webkit-progress-bar:active, +meter::-webkit-meter-bar:active { + border-radius: 3px; + background: #222222; +} + +::-webkit-progress-value:active, +meter::-webkit-meter-optimum-value:active { + background: #99f; +} + +progress, +progress.sdpi-item-value { + min-height: 5px !important; + height: 5px; + background-color: #303030; +} + +progress { + margin-top: 8px !important; + margin-bottom: 8px !important; +} + +.full progress, +progress.full { + margin-top: 3px !important; +} + +::-webkit-progress-inner-element { + background-color: transparent; +} + + +.sdpi-item[type="progress"] { + margin-top: 4px !important; + margin-bottom: 12px; + min-height: 15px; +} + +.sdpi-item-child.full:last-child { + margin-bottom: 4px; +} + + +/* TABS */ + +.tabs { + /** + * Setting display to flex makes this container lay + * out its children using flexbox, the exact same + * as in the above "Stepper input" example. + */ + display: flex; + border-bottom: 1px solid rgba(255, 255, 255, 0.0); + flex-wrap: nowrap; + white-space: nowrap; + overflow-x: auto; + text-transform: capitalize; + background-color: transparent; + margin-left: var(--sdpi-tab-container-left-offset); +} + +.tabs::-webkit-scrollbar { + height: 4px; + display: none; +} + +.tabs::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + border-radius: 8px; +} + +.tabs::-webkit-scrollbar-thumb { + background-color: #444; + outline: 1px solid #444; + border-radius: 8px; +} + +.tab-separator { + margin-left: 100px; + max-width: 234px; + margin-bottom: 20px; + margin-top: -4px; +} + +.tab { + cursor: pointer; + padding: var(--sdpi-tab-padding-vertical) var(--sdpi-tab-padding-horizontal); + color: var(--sdpi-tab-color); + font-size: var(--sdpi-tab-font-size); + font-weight: var(--title-font-weight); + background-color: rgba(0, 0, 0, 0.1); + margin: 0px; + margin-top: var(--sdpi-tab-top-offset); + margin-left: var(--sdpi-tab-left-margin); + border-top-left-radius: 5px; + border-top-right-radius: 5px; + border: 1px solid rgba(255, 255, 255, 0.1); + border-bottom: 1px solid var(--sdpi-linecolor); + -webkit-user-select: none; + user-select: none; +} + +.tab:first-child { + margin-left: 0px; +} + +.tab-container { + margin-top: -14px; +} + +.tab-container>hr { + margin-left: 100px; + max-width: 234px; +} + +.tabs+hr { + margin-left: 0px; + max-width: 234px; + margin-top: -6px; +} + +.tab.selected { + color: white; + background-color: var(--sdpi-tab-selected-color); + border-bottom: 2px solid var(--sdpi-tab-selected-color); + margin-top: var(--sdpi-tab-selected-top-offset); +} + +.sdpi-item.tabgroup { + margin-top: 0px; +} + +.istab { + background-color: rgba(0, 0, 0, 0.2); + margin-bottom: 20px; + padding: 4px; +} + +select { + -webkit-appearance: none; + -moz-appearance: none; + -o-appearance: none; + appearance: none; + background: url(../assets/caret.svg) no-repeat 97% center; +} + +label.sdpi-file-label, +input[type="button"], +input[type="submit"], +input[type="reset"], +input[type="file"], +input[type=file]::-webkit-file-upload-button, +button, +select { + color: var(--sdpi-color); + border: 1pt solid #303030; + font-size: 8pt; + background-color: var(--sdpi-background); + border-radius: var(--sdpi-borderradius); +} + +label.sdpi-file-label, +input[type="button"], +input[type="submit"], +input[type="reset"], +input[type="file"], +input[type=file]::-webkit-file-upload-button, +button { + border: 1pt solid var(--sdpi-buttonbordercolor); + border-radius: var(--sdpi-borderradius); + border-color: var(--sdpi-buttonbordercolor); + min-height: 23px !important; + height: 23px !important; + margin-right: 8px; +} + +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type="file"] { + border-radius: var(--sdpi-borderradius); + max-width: 220px; +} + +option { + height: 1.5em; + padding: 4px; +} + +/* SDPI */ + +.sdpi-wrapper { + overflow-x: hidden; + height: 100%; +margin-right: 1px; /* ensure scroller thumb is not clipped */ +} + +.sdpi-item { + display: flex; + flex-direction: row; +min-height: 30px; + align-items: first baseline; + margin-top: 2px; + max-width: 344px; + -webkit-user-drag: none; +} + +.sdpi-item[type="textarea"], +.sdpi-item[type="color"], +.sdpi-item[type="canvas"], +.sdpi-item .aligncenter { + align-items: center; +} + +.sdpi-item[type="color"]>.sdpi-item-label { + line-height: 22px; +} + +.sdpi-item:first-child { + margin-top: -1px; +} + +.sdpi-item:first-of-type { + margin-top: 2px; +} + +.sdpi-item[type="radio"]:first-of-type, +.sdpi-item[type="checkbox"]:first-of-type { + margin-top: -4px; +} + +.sdpi-item:last-child { + margin-bottom: 0px; +} + +.sdpi-item>*:not(.sdpi-item-label):not(meter):not(details):not(canvas) { + min-height: 26px; +} + +.sdpi-item>*:not(.sdpi-item-label.empty):not(meter) { + min-height: 26px; +} + +.sdpi-item>input { + padding: 0px 4px; +} + +.sdpi-item-group { + padding: 0 !important; +} + +meter.sdpi-item-value { + margin-left: 6px; +} + +.sdpi-item[type="group"] { + display: block; + margin-top: 12px; + margin-bottom: 12px; + /* border: 1px solid white; */ + flex-direction: unset; + text-align: left; +} + +.sdpi-item[type="group"]>.sdpi-item-label, +.sdpi-item[type="group"].sdpi-item-label { + width: 96%; + text-align: left; + font-weight: 700; + margin-bottom: 4px; + padding-left: 4px; +} + +dl, +ul, +ol { + -webkit-margin-before: 0px; + -webkit-margin-after: 4px; + -webkit-padding-start: 1em; + max-height: 90px; + overflow-y: scroll; + cursor: pointer; + user-select: none; +} + +table.sdpi-item-value, +dl.sdpi-item-value, +ul.sdpi-item-value, +ol.sdpi-item-value { + -webkit-margin-before: 4px; + -webkit-margin-after: 8px; + -webkit-padding-start: 1em; + width: var(--sdpi-width); + text-align: center; +} + +table>caption { + margin: 2px; +} + +.list, +.sdpi-item[type="list"] { + align-items: baseline; +} + +.sdpi-item-label { + text-align: right; + flex: none; + width: 94px; + padding-right: 5px; + font-weight: 600; + -webkit-user-select: none; + line-height: normal; + margin-left: -1px; +} + +.win .sdpi-item-label, +.sdpi-item-label>small { + font-weight: normal; +} + +.sdpi-item-label:after { + content: ": "; +} + +.sdpi-item-label.empty:after { + content: ""; +} + +.sdpi-test, +.sdpi-item-value { + flex: 1 0 0; + /* flex-grow: 1; +flex-shrink: 0; */ + margin-right: 14px; + margin-left: 4px; + justify-content: space-evenly; +} + +canvas.sdpi-item-value { + max-width: 144px; + max-height: 144px; + width: 144px; + height: 144px; + margin: 0 auto; + cursor: pointer; +} + +input.sdpi-item-value { + margin-left: 5px; +} + +.sdpi-item-value button, +button.sdpi-item-value { + margin-left: 6px; + margin-right: 14px; +} + +.sdpi-item-value.range { + margin-left: 0px; +} + +table, +dl.sdpi-item-value, +ul.sdpi-item-value, +ol.sdpi-item-value, +.sdpi-item-value>dl, +.sdpi-item-value>ul, +.sdpi-item-value>ol { + list-style-type: none; + list-style-position: outside; + margin-left: -4px; + margin-right: -4px; + padding: 4px; + border: 1px solid var(--sdpi-bordercolor); +} + +dl.sdpi-item-value, +ul.sdpi-item-value, +ol.sdpi-item-value, +.sdpi-item-value>ol { + list-style-type: none; + list-style-position: inside; + margin-left: 5px; + margin-right: 12px; + padding: 4px !important; + /* display: flex; +flex-direction: column; */ +} + +.two-items li { + display: flex; +} + +.two-items li>*:first-child { + flex: 0 0 50%; + text-align: left; +} + +.two-items.thirtyseventy li>*:first-child { + flex: 0 0 30%; +} + +ol.sdpi-item-value, +.sdpi-item-value>ol[listtype="none"] { + list-style-type: none; +} + +ol.sdpi-item-value[type="decimal"], +.sdpi-item-value>ol[type="decimal"] { + list-style-type: decimal; +} + +ol.sdpi-item-value[type="decimal-leading-zero"], +.sdpi-item-value>ol[type="decimal-leading-zero"] { + list-style-type: decimal-leading-zero; +} + +ol.sdpi-item-value[type="lower-alpha"], +.sdpi-item-value>ol[type="lower-alpha"] { + list-style-type: lower-alpha; +} + +ol.sdpi-item-value[type="upper-alpha"], +.sdpi-item-value>ol[type="upper-alpha"] { + list-style-type: upper-alpha; +} + +ol.sdpi-item-value[type="upper-roman"], +.sdpi-item-value>ol[type="upper-roman"] { + list-style-type: upper-roman; +} + +ol.sdpi-item-value[type="lower-roman"], +.sdpi-item-value>ol[type="lower-roman"] { + list-style-type: upper-roman; +} + +tr:nth-child(even), +.sdpi-item-value>ul>li:nth-child(even), +.sdpi-item-value>ol>li:nth-child(even), +li:nth-child(even) { + background-color: rgba(0, 0, 0, .2) +} + +td:hover, +.sdpi-item-value>ul>li:hover:nth-child(even), +.sdpi-item-value>ol>li:hover:nth-child(even), +li:hover:nth-child(even), +li:hover { + background-color: rgba(255, 255, 255, .1); +} + +td.selected, +td.selected:hover, +li.selected:hover, +li.selected { + color: white; + background-color: #77f; +} + +tr { + border: 1px solid var(--sdpi-bordercolor); +} + +td { + border-right: 1px solid var(--sdpi-bordercolor); + -webkit-user-select: none; +} + +tr:last-child, +td:last-child { + border: none; +} + +.sdpi-item-value.select, +.sdpi-item-value>select { + margin-right: 13px; + margin-left: 4px; + padding: 0px 4px; +} + +.sdpi-item-child, +.sdpi-item-group>.sdpi-item>input[type="color"] { + margin-top: 0.4em; + margin-right: 4px; + margin-left: 4px; +} + +.full, +.full *, +.sdpi-item-value.full, +.sdpi-item-child>full>*, +.sdpi-item-child.full, +.sdpi-item-child.full>*, +.full>.sdpi-item-child, +.full>.sdpi-item-child>* { + display: flex; + flex: 1 1 0; + margin-bottom: 4px; + margin-left: 0px; + width: 100%; + justify-content: space-evenly; +} + +.sdpi-item-group>.sdpi-item>input[type="color"] { + margin-top: 0px; +} + +::-webkit-calendar-picker-indicator:focus, +input[type=file]::-webkit-file-upload-button:focus, +button:focus, +textarea:focus, +input:focus, +select:focus, +option:focus, +details:focus, +summary:focus, +.custom-select select { + outline: none; +} + +summary { + cursor: default; + -webkit-user-select: none; +} + +.pointer, +summary .pointer { + cursor: pointer; +} + +.sdpi-item.details { + align-items: first baseline; +} + +/* needs Chromium update 2023 +.sdpi-item:has(>details) { + align-items: first baseline; +} +*/ + +details * { + font-size: 12px; + font-weight: normal; +} + +details.message { + padding: 4px 18px 4px 12px; +} + +details.message summary { + font-size: 10pt; + font-weight: 600; + min-height: 18px; +} + +details.message:first-child { + margin-top: 4px; + margin-left: 0; + padding-left: 102px !important; +} + +details.message>summary:first-of-type { + line-height: 20px; +} + +details.message h1 { + text-align: left; +} + +/* details:not(.pointer)>summary { +list-style: none; +} + +details > summary::-webkit-details-marker, +.message > summary::-webkit-details-marker { +display: none; +} */ + +.info20, +.question, +.caution, +.info { + background-repeat: no-repeat; + background-position: 72px center; +} + +.info20 { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,20 C4.4771525,20 0,15.5228475 0,10 C0,4.4771525 4.4771525,0 10,0 C15.5228475,0 20,4.4771525 20,10 C20,15.5228475 15.5228475,20 10,20 Z M10,8 C8.8954305,8 8,8.84275812 8,9.88235294 L8,16.1176471 C8,17.1572419 8.8954305,18 10,18 C11.1045695,18 12,17.1572419 12,16.1176471 L12,9.88235294 C12,8.84275812 11.1045695,8 10,8 Z M10,3 C8.8954305,3 8,3.88165465 8,4.96923077 L8,5.03076923 C8,6.11834535 8.8954305,7 10,7 C11.1045695,7 12,6.11834535 12,5.03076923 L12,4.96923077 C12,3.88165465 11.1045695,3 10,3 Z'/%3E%3C/svg%3E%0A"); +} + +.info { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,18 C5.581722,18 2,14.418278 2,10 C2,5.581722 5.581722,2 10,2 C14.418278,2 18,5.581722 18,10 C18,14.418278 14.418278,18 10,18 Z M10,8 C9.44771525,8 9,8.42137906 9,8.94117647 L9,14.0588235 C9,14.5786209 9.44771525,15 10,15 C10.5522847,15 11,14.5786209 11,14.0588235 L11,8.94117647 C11,8.42137906 10.5522847,8 10,8 Z M10,5 C9.44771525,5 9,5.44082732 9,5.98461538 L9,6.01538462 C9,6.55917268 9.44771525,7 10,7 C10.5522847,7 11,6.55917268 11,6.01538462 L11,5.98461538 C11,5.44082732 10.5522847,5 10,5 Z'/%3E%3C/svg%3E%0A"); +} + +.info2 { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='15' height='15' viewBox='0 0 15 15'%3E%3Cpath fill='%23999' d='M7.5,15 C3.35786438,15 0,11.6421356 0,7.5 C0,3.35786438 3.35786438,0 7.5,0 C11.6421356,0 15,3.35786438 15,7.5 C15,11.6421356 11.6421356,15 7.5,15 Z M7.5,2 C6.67157287,2 6,2.66124098 6,3.47692307 L6,3.52307693 C6,4.33875902 6.67157287,5 7.5,5 C8.32842705,5 9,4.33875902 9,3.52307693 L9,3.47692307 C9,2.66124098 8.32842705,2 7.5,2 Z M5,6 L5,7.02155172 L6,7 L6,12 L5,12.0076778 L5,13 L10,13 L10,12 L9,12.0076778 L9,6 L5,6 Z'/%3E%3C/svg%3E%0A"); +} + +.sdpi-more-info { + background-image: linear-gradient(to right, #00000000 0%, #00000040 80%), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpolygon fill='%23999' points='4 7 8 7 8 5 12 8 8 11 8 9 4 9'/%3E%3C/svg%3E%0A"); +} + +.caution { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' fill-rule='evenodd' d='M9.03952676,0.746646542 C9.57068894,-0.245797319 10.4285735,-0.25196227 10.9630352,0.746646542 L19.7705903,17.2030214 C20.3017525,18.1954653 19.8777595,19 18.8371387,19 L1.16542323,19 C0.118729947,19 -0.302490098,18.2016302 0.231971607,17.2030214 L9.03952676,0.746646542 Z M10,2.25584053 L1.9601405,17.3478261 L18.04099,17.3478261 L10,2.25584053 Z M10,5.9375 C10.531043,5.9375 10.9615385,6.37373537 10.9615385,6.91185897 L10.9615385,11.6923077 C10.9615385,12.2304313 10.531043,12.6666667 10,12.6666667 C9.46895697,12.6666667 9.03846154,12.2304313 9.03846154,11.6923077 L9.03846154,6.91185897 C9.03846154,6.37373537 9.46895697,5.9375 10,5.9375 Z M10,13.4583333 C10.6372516,13.4583333 11.1538462,13.9818158 11.1538462,14.6275641 L11.1538462,14.6641026 C11.1538462,15.3098509 10.6372516,15.8333333 10,15.8333333 C9.36274837,15.8333333 8.84615385,15.3098509 8.84615385,14.6641026 L8.84615385,14.6275641 C8.84615385,13.9818158 9.36274837,13.4583333 10,13.4583333 Z'/%3E%3C/svg%3E%0A"); +} + +.question { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,18 C5.581722,18 2,14.418278 2,10 C2,5.581722 5.581722,2 10,2 C14.418278,2 18,5.581722 18,10 C18,14.418278 14.418278,18 10,18 Z M6.77783203,7.65332031 C6.77783203,7.84798274 6.85929281,8.02888914 7.0222168,8.19604492 C7.18514079,8.36320071 7.38508996,8.44677734 7.62207031,8.44677734 C8.02409055,8.44677734 8.29703704,8.20768468 8.44091797,7.72949219 C8.59326248,7.27245865 8.77945854,6.92651485 8.99951172,6.69165039 C9.2195649,6.45678594 9.56233491,6.33935547 10.027832,6.33935547 C10.4256205,6.33935547 10.7006836,6.37695313 11.0021973,6.68847656 C11.652832,7.53271484 10.942627,8.472229 10.3750916,9.1321106 C9.80755615,9.79199219 8.29492188,11.9897461 10.027832,12.1347656 C10.4498423,12.1700818 10.7027991,11.9147157 10.7832031,11.4746094 C11.0021973,9.59857178 13.1254883,8.82415771 13.1254883,7.53271484 C13.1254883,7.07568131 12.9974785,6.65250846 12.7414551,6.26318359 C12.4854317,5.87385873 12.1225609,5.56600048 11.652832,5.33959961 C11.1831031,5.11319874 10.6414419,5 10.027832,5 C9.36767248,5 8.79004154,5.13541531 8.29492187,5.40625 C7.79980221,5.67708469 7.42317837,6.01879677 7.16503906,6.43139648 C6.90689975,6.8439962 6.77783203,7.25130007 6.77783203,7.65332031 Z M10.0099668,15 C10.2713191,15 10.5016601,14.9108147 10.7009967,14.7324415 C10.9003332,14.5540682 11,14.3088087 11,13.9966555 C11,13.7157177 10.9047629,13.4793767 10.7142857,13.2876254 C10.5238086,13.0958742 10.2890379,13 10.0099668,13 C9.72646591,13 9.48726565,13.0958742 9.2923588,13.2876254 C9.09745196,13.4793767 9,13.7157177 9,13.9966555 C9,14.313268 9.10077419,14.5596424 9.30232558,14.735786 C9.50387698,14.9119295 9.73975502,15 10.0099668,15 Z'/%3E%3C/svg%3E%0A"); +} + +.sdpi-more-info { + position: fixed; + left: 0px; + right: 0px; + bottom: 0px; + min-height: 16px; + padding-right: 16px; + text-align: right; + -webkit-touch-callout: none; + cursor: pointer; + user-select: none; + background-position: right center; + background-repeat: no-repeat; + border-radius: var(--sdpi-borderradius); + text-decoration: none; + color: var(--sdpi-color); +} + +.sdpi-more-info-button { + display: flex; + align-self: right; + margin-left: auto; + position: fixed; + right: 17px; + bottom: 0px; + user-select: none; +} + +.sdpi-bottom-bar { + display: flex; + align-self: right; + margin-left: auto; + position: fixed; + right: 17px; + bottom: 0px; + user-select: none; +} + +.sdpi-bottom-bar.right { + right: 0px; +} + +.sdpi-bottom-bar button { + min-height: 20px !important; + height: 20px !important; +} + +details a { + background-position: right !important; + min-height: 24px; + display: inline-block; + line-height: 24px; + padding-right: 28px; +} + +input:not([type="range"]), +textarea { + -webkit-appearance: none; + background: var(--sdpi-background); + color: var(--sdpi-color); + font-weight: normal; + font-size: 9pt; + border: none; + margin-top: 2px; + margin-bottom: 2px; + min-width: 100px; +} + +textarea+label { + display: flex; + justify-content: flex-end +} + +input[type="radio"], +input[type="checkbox"] { + display: none; +} + +input[type="radio"]+label, +input[type="checkbox"]+label { + font-size: 9pt; + color: var(--sdpi-color); + font-weight: normal; + margin-right: 8px; + -webkit-user-select: none; +} + +input[type="radio"]+label:after, +input[type="checkbox"]+label:after { + content: " " !important; +} + +.sdpi-item[type="radio"]>.sdpi-item-value, +.sdpi-item[type="checkbox"]>.sdpi-item-value { + padding-top: 2px; +} + +.sdpi-item[type="checkbox"]>.sdpi-item-value>* { + margin-top: 4px; +} + +.sdpi-item[type="checkbox"] .sdpi-item-child, +.sdpi-item[type="radio"] .sdpi-item-child { + display: inline-block; +} + +.sdpi-item[type="range"] .sdpi-item-value, +.sdpi-item[type="meter"] .sdpi-item-child, +.sdpi-item[type="progress"] .sdpi-item-child { + display: flex; +} + +.sdpi-item[type="range"] .sdpi-item-value { + min-height: 26px; +} + +.sdpi-item[type="range"] .sdpi-item-value span, +.sdpi-item[type="meter"] .sdpi-item-child span, +.sdpi-item[type="progress"] .sdpi-item-child span { + margin-top: -2px; + min-width: 8px; + text-align: right; + cursor: pointer; + -webkit-user-select: none; + user-select: none; +} + +.sdpi-item[type="range"] .sdpi-item-value span { + margin-top: 7px; + text-align: right; +} + +span+input[type="range"] { + display: flex; +} + +span+.range-container>input[type="range"], +span+input[type="range"] { + max-width: 168px; +} + +.sdpi-item[type="range"] .sdpi-item-value span:first-child, +.sdpi-item[type="meter"] .sdpi-item-child span:first-child, +.sdpi-item[type="progress"] .sdpi-item-child span:first-child { + margin-right: 4px; +} + +.sdpi-item[type="range"] .sdpi-item-value span:last-child, +.sdpi-item[type="meter"] .sdpi-item-child span:last-child, +.sdpi-item[type="progress"] .sdpi-item-child span:last-child { + margin-left: 4px; +} + +.reverse { + transform: rotate(180deg); +} + +.sdpi-item[type="meter"] .sdpi-item-child meter+span:last-child { + margin-left: -10px; +} + +.sdpi-item[type="progress"] .sdpi-item-child meter+span:last-child { + margin-left: -14px; +} + +.sdpi-item[type="radio"]>.sdpi-item-value>* { + margin-top: 2px; +} + +details { + padding: 8px 18px 8px 12px; + min-width: 86px; +} + +details>h4 { + border-bottom: 1px solid var(--sdpi-bordercolor); +} + +legend { + display: none; +} + +.sdpi-item-value>textarea { + padding: 0px; + width: 219px; + margin-left: 1px; + margin-top: 3px; + padding: 4px; +} + +input[type="radio"]+label span, +input[type="checkbox"]+label span { + display: inline-block; + width: 16px; + height: 16px; + margin: 2px 4px 2px 0; + border-radius: 3px; + vertical-align: middle; + background: var(--sdpi-background); + cursor: pointer; + border: 1px solid rgb(0, 0, 0, .2); +} + +input[type="radio"]+label span { + border-radius: 100%; +} + +input[type="radio"]:checked+label span, +input[type="checkbox"]:checked+label span { + background-color: #77f; + background-image: url(../assets/check.svg); + background-repeat: no-repeat; + background-position: center center; + border: 1px solid rgb(0, 0, 0, .4); +} + +input[type="radio"]:active:checked+label span, +input[type="radio"]:active+label span, +input[type="checkbox"]:active:checked+label span, +input[type="checkbox"]:active+label span { + background-color: #303030; +} + +input[type="radio"]:checked+label span { + background-image: url(../assets/rcheck.svg); +} + +input[type="range"] { + width: var(--sdpi-width); + height: 30px; + overflow: hidden; + cursor: pointer; + background: transparent !important; +} + +.sdpi-item>input[type="range"] { + margin-left: 2px; + max-width: var(--sdpi-width); + width: var(--sdpi-width); + padding: 0px; + margin-top: 2px; +} + +/* +input[type="range"], +input[type="range"]::-webkit-slider-runnable-track, +input[type="range"]::-webkit-slider-thumb { +-webkit-appearance: none; +} +*/ + +input[type="range"]::-webkit-slider-runnable-track { + height: 5px; + background: #979797; + border-radius: 3px; + padding: 0px !important; + border: 1px solid var(--sdpi-background); +} + +input[type="range"]::-webkit-slider-thumb { + position: relative; + -webkit-appearance: none; + background-color: var(--sdpi-color); + width: 12px; + height: 12px; + border-radius: 20px; + margin-top: -5px; + border: none; +} + +input[type="range" i] { + margin: 0; +} + +input[type="range"]::-webkit-slider-thumb::before { + position: absolute; + content: ""; +height: 5px; /* equal to height of runnable track or 1 less */ +width: 500px; /* make this bigger than the widest range input element */ +left: -502px; /* this should be -2px - width */ +top: 8px; /* don't change this */ + background: #77f; +} + +input[type="color"] { + min-width: 32px; + min-height: 32px; + width: 32px; + height: 32px; + padding: 0; + background-color: var(--sdpi-bgcolor); + flex: none; +} + +::-webkit-color-swatch { + min-width: 24px; +} + +textarea { + height: 3em; + word-break: break-word; + line-height: 1.5em; +} + +.textarea { + padding: 0px !important; +} + +textarea { +width: 219px; /*98%;*/ + height: 96%; + min-height: 6em; + resize: none; + border-radius: var(--sdpi-borderradius); +} + +/* CAROUSEL */ + +.sdpi-item[type="carousel"] {} + +.sdpi-item.card-carousel-wrapper, +.sdpi-item>.card-carousel-wrapper { + padding: 0; +} + +.card-carousel-wrapper { + display: flex; + align-items: center; + justify-content: center; + margin: 12px auto; + color: #666a73; +} + +.card-carousel { + display: flex; + justify-content: center; + width: 278px; +} + +.card-carousel--overflow-container { + overflow: hidden; +} + +.card-carousel--nav__left, +.card-carousel--nav__right { + /* display: inline-block; */ + width: 12px; + height: 12px; + border-top: 2px solid #42b883; + border-right: 2px solid #42b883; + cursor: pointer; + margin: 0 4px; + transition: transform 150ms linear; +} + +.card-carousel--nav__left[disabled], +.card-carousel--nav__right[disabled] { + opacity: 0.2; + border-color: black; +} + +.card-carousel--nav__left { + transform: rotate(-135deg); +} + +.card-carousel--nav__left:active { + transform: rotate(-135deg) scale(0.85); +} + +.card-carousel--nav__right { + transform: rotate(45deg); +} + +.card-carousel--nav__right:active { + transform: rotate(45deg) scale(0.85); +} + +.card-carousel-cards { + display: flex; + transition: transform 150ms ease-out; + transform: translatex(0px); +} + +.card-carousel-cards .card-carousel--card { + margin: 0 5px; + cursor: pointer; + /* box-shadow: 0 4px 15px 0 rgba(40, 44, 53, 0.06), 0 2px 2px 0 rgba(40, 44, 53, 0.08); */ + /* background-color: #fff; */ + text-align: center; + border-radius: 4px; + z-index: 3; +} + +.xxcard-carousel-cards .card-carousel--card:first-child { + margin-left: 0; +} + +.xxcard-carousel-cards .card-carousel--card:last-child { + margin-right: 0; +} + +.card-carousel-cards .card-carousel--card img { + vertical-align: bottom; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + transition: opacity 150ms linear; + width: 60px; +} + +.card-carousel-cards .card-carousel--card img:hover { + opacity: 0.5; + background-color: rgba(255, 255, 255, .1); +} + +.card-carousel-cards .card-carousel--card--footer { + border-top: 0; + max-width: 80px; + overflow: hidden; + display: flex; + height: 100%; + flex-direction: column; +} + +.card-carousel-cards .card-carousel--card--footer p { + padding: 3px 0; + margin: 0; + margin-bottom: 2px; + font-size: 15px; + font-weight: 500; + color: #2c3e50; +} + +.card-carousel-cards .card-carousel--card--footer p:nth-of-type(2) { + font-size: 12px; + font-weight: 300; + padding: 6px; + color: #666a73; +} + +::-webkit-calendar-picker-indicator { + background: url(../assets/elg_calendar_inv.svg) no-repeat center; + font-size: 17px; +} + +::-webkit-calendar-picker-indicator:focus { + background-color: rgba(0, 0, 0, 0.2); +} + +input[type="text"]::-webkit-calendar-picker-indicator { + background: transparent; + font-size: 12px; +} + +input[type="date"] { + -webkit-align-items: center; + align-items: center; + display: -webkit-inline-flex; + overflow: hidden; + -webkit-padding-start: 1px; +} + +input::-webkit-datetime-edit { + flex: 1; + -webkit-user-modify: read-only !important; + user-modify: read-only !important; + display: inline-block; + min-width: 0; + overflow: hidden; + padding: 4px; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + +} + +input[type="file"] { + opacity: 0; + display: none; +} + +.sdpi-item>input[type="file"] { + opacity: 1; + display: flex; +} + +input[type="file"]+span { + display: flex; + flex: 0 1 auto; + background-color: #0000ff50; +} + +label.sdpi-file-label { + cursor: pointer; + user-select: none; + display: inline-block; + min-height: 21px !important; + height: 21px !important; + line-height: 20px; + padding: 0px 4px; + margin: auto; + margin-right: 0px; + float: right; +} + +.sdpi-file-label>label:active, +.sdpi-file-label.file:active, +label.sdpi-file-label:active, +label.sdpi-file-info:active, +input[type="file"]::-webkit-file-upload-button:active, +button:active { + background-color: var(--sdpi-color); + color: #303030; +} + +input:required:invalid, +input:focus:invalid { + background: var(--sdpi-background) url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5IiBoZWlnaHQ9IjkiIHZpZXdCb3g9IjAgMCA5IDkiPgogICAgPHBhdGggZmlsbD0iI0Q4RDhEOCIgZD0iTTQuNSwwIEM2Ljk4NTI4MTM3LC00LjU2NTM4NzgyZS0xNiA5LDIuMDE0NzE4NjMgOSw0LjUgQzksNi45ODUyODEzNyA2Ljk4NTI4MTM3LDkgNC41LDkgQzIuMDE0NzE4NjMsOSAzLjA0MzU5MTg4ZS0xNiw2Ljk4NTI4MTM3IDAsNC41IEMtMy4wNDM1OTE4OGUtMTYsMi4wMTQ3MTg2MyAyLjAxNDcxODYzLDQuNTY1Mzg3ODJlLTE2IDQuNSwwIFogTTQsMSBMNCw2IEw1LDYgTDUsMSBMNCwxIFogTTQuNSw4IEM0Ljc3NjE0MjM3LDggNSw3Ljc3NjE0MjM3IDUsNy41IEM1LDcuMjIzODU3NjMgNC43NzYxNDIzNyw3IDQuNSw3IEM0LjIyMzg1NzYzLDcgNCw3LjIyMzg1NzYzIDQsNy41IEM0LDcuNzc2MTQyMzcgNC4yMjM4NTc2Myw4IDQuNSw4IFoiLz4KICA8L3N2Zz4) no-repeat 98% center; +} + +input:required:valid { + background: var(--sdpi-background) url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5IiBoZWlnaHQ9IjkiIHZpZXdCb3g9IjAgMCA5IDkiPjxwb2x5Z29uIGZpbGw9IiNEOEQ4RDgiIHBvaW50cz0iNS4yIDEgNi4yIDEgNi4yIDcgMy4yIDcgMy4yIDYgNS4yIDYiIHRyYW5zZm9ybT0icm90YXRlKDQwIDQuNjc3IDQpIi8+PC9zdmc+) no-repeat 98% center; +} + +.tooltip, +:tooltip, +:title { + color: yellow; +} + +.sdpi-item-group.file { + width: 232px; + display: flex; + align-items: center; +} + +.sdpi-file-info { + overflow-wrap: break-word; + word-wrap: break-word; + hyphens: auto; + min-width: 132px; + max-width: 144px; + max-height: 32px; + margin-top: 0px; + margin-left: 5px; + display: inline-block; + overflow: hidden; + padding: 6px 4px; + background-color: var(--sdpi-background); +} + +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + margin: 4px; + border-radius: 8px; +} + +::-webkit-scrollbar-thumb { + background-color: #999999; + outline: 1px solid slategrey; + border-radius: 8px; +} + +a { + color: #7397d2; +} + +input[type="week"] { + -webkit-appearance: auto !important; + appearance: auto !important; +} + +input[type="month"]+datalist, +input[type="day"]+datalist, +input[type="week"]+datalist, +input[type=text]+datalist { + display: none !important; +} + +input[type="range"] { + -webkit-appearance: auto; + appearance: auto; + height: 6px; + margin-top: 12px; + z-index: 0; + overflow: visible; +} + +input[type="range"]::-webkit-slider-runnable-track { + border: 0px solid transparent; +} + +.sdpi-item[type="range"] .sdpi-item-value.datalist { + flex-direction: column; +} + +datalist { + --sdpi-datalist-margin: 7px; + display: flex; + justify-content: space-between; + margin-top: 0px; + padding-top: 0px; + font-size: 12px; + margin-left: var(--sdpi-datalist-margin); + width: calc(100% - calc(var(--sdpi-datalist-margin) * 2.5)); +} + +datalist>option { + display: flex; + justify-content: center; + align-items: end; + /* background-image: url(../assets/tick.svg); */ + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1' height='8' viewBox='0 0 1 8'%3E%3Crect width='1' height='6' x='0' y='1' fill='%23555'/%3E%3C/svg%3E%0A"); + padding: 0; + font-weight: 400; + font-size: 12px; + color: #9A9A99; + width: 1px; + height: 30px; + z-index: 1; + margin-top: -6px; + background-position: top 6px right 5px; + background-repeat: repeat no-repeat; /* fallback */ + background-repeat-y: no-repeat; + user-select: none; + -webkit-user-select: none; +} + +[role="spinbutton"] { + -webkit-appearance: auto; + appearance: auto; +} + +/* +input[type="range"]::-webkit-slider-thumb { +-webkit-appearance: none; +background-color: var(--sdpi-color); +width: 12px; +height: 12px; +border-radius: 20px; +margin-top: -6px; +border: none; +} */ + +:-webkit-slider-thumb { + -webkit-appearance: none; + background-color: var(--sdpi-color); + width: 16px; + height: 16px; + border-radius: 20px; + margin-top: -6px; + border: 1px solid #999999; +} + +.sdpi-item[type="range"] .sdpi-item-group { + display: flex; + flex-direction: column; +} + +.xxsdpi-item[type="range"] .sdpi-item-group input { + max-width: 204px; +} + +.sdpi-item[type="range"] .sdpi-item-group span { + margin-left: 0px !important; +} + +.sdpi-item[type="range"] .sdpi-item-group>.sdpi-item-child { + display: flex; + flex-direction: row; +} + +.rangeLabel { + position: absolute; + font-weight: normal; + margin-top: 24px; +} + +:disabled { + color: #993333; +} + +select, +select option { + color: var(--sdpi-color); +} + +select.disabled, +select option:disabled { + color: #fd9494; + font-style: italic; +} + +.runningAppsContainer { + display: none; +} + +.one-line { + min-height: 1.5em; +} + +.two-lines { + min-height: 3em; +} + +.three-lines { + min-height: 4.5em; +} + +.four-lines { + min-height: 6em; +} + +.min80>.sdpi-item-child { + min-width: 80px; +} + +.min100>.sdpi-item-child { + min-width: 100px; +} + +.min120>.sdpi-item-child { + min-width: 120px; +} + +.min140>.sdpi-item-child { + min-width: 140px; +} + +.min160>.sdpi-item-child { + min-width: 160px; +} + +.min200>.sdpi-item-child { + min-width: 200px; +} + +.max40 { + flex-basis: 40%; + flex-grow: 0; +} + +.max30 { + flex-basis: 30%; + flex-grow: 0; +} + +.max20 { + flex-basis: 20%; + flex-grow: 0; +} + +.up20 { + margin-top: -20px; +} + +.alignCenter { + align-items: center; +} + +.alignTop { + align-items: flex-start; +} + +.alignBaseline { + align-items: baseline; +} + +.noMargins, +.noMargins *, +.noInnerMargins * { + margin: 0; + padding: 0; +} + +.hidden { + display: none !important; +} + +.icon-help, +.icon-help-line, +.icon-help-fill, +.icon-help-inv, +.icon-brighter, +.icon-darker, +.icon-warmer, +.icon-cooler { + min-width: 20px; + width: 20px; + background-repeat: no-repeat; + opacity: 1; +} + +.icon-help:active, +.icon-help-line:active, +.icon-help-fill:active, +.icon-help-inv:active, +.icon-brighter:active, +.icon-darker:active, +.icon-warmer:active, +.icon-cooler:active { + opacity: 0.5; +} + +.icon-brighter, +.icon-darker, +.icon-warmer, +.icon-cooler { + margin-top: 5px !important; +} + +.icon-help, +.icon-help-line, +.icon-help-fill, +.icon-help-inv { + cursor: pointer; + margin: 0px; + margin-left: 4px; +} + +.icon-brighter { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cg fill='%23999' fill-rule='evenodd'%3E%3Ccircle cx='10' cy='10' r='4'/%3E%3Cpath d='M14.8532861,7.77530426 C14.7173255,7.4682615 14.5540843,7.17599221 14.3666368,6.90157083 L16.6782032,5.5669873 L17.1782032,6.4330127 L14.8532861,7.77530426 Z M10.5,4.5414007 C10.2777625,4.51407201 10.051423,4.5 9.82179677,4.5 C9.71377555,4.5 9.60648167,4.50311409 9.5,4.50925739 L9.5,2 L10.5,2 L10.5,4.5414007 Z M5.38028092,6.75545367 C5.18389364,7.02383457 5.01124349,7.31068015 4.86542112,7.61289977 L2.82179677,6.4330127 L3.32179677,5.5669873 L5.38028092,6.75545367 Z M4.86542112,12.3871002 C5.01124349,12.6893198 5.18389364,12.9761654 5.38028092,13.2445463 L3.32179677,14.4330127 L2.82179677,13.5669873 L4.86542112,12.3871002 Z M9.5,15.4907426 C9.60648167,15.4968859 9.71377555,15.5 9.82179677,15.5 C10.051423,15.5 10.2777625,15.485928 10.5,15.4585993 L10.5,18 L9.5,18 L9.5,15.4907426 Z M14.3666368,13.0984292 C14.5540843,12.8240078 14.7173255,12.5317385 14.8532861,12.2246957 L17.1782032,13.5669873 L16.6782032,14.4330127 L14.3666368,13.0984292 Z'/%3E%3C/g%3E%3C/svg%3E"); +} + +.icon-darker { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cg fill='%23999' fill-rule='evenodd'%3E%3Cpath d='M10 14C7.790861 14 6 12.209139 6 10 6 7.790861 7.790861 6 10 6 12.209139 6 14 7.790861 14 10 14 12.209139 12.209139 14 10 14zM10 13C11.6568542 13 13 11.6568542 13 10 13 8.34314575 11.6568542 7 10 7 8.34314575 7 7 8.34314575 7 10 7 11.6568542 8.34314575 13 10 13zM14.8532861 7.77530426C14.7173255 7.4682615 14.5540843 7.17599221 14.3666368 6.90157083L16.6782032 5.5669873 17.1782032 6.4330127 14.8532861 7.77530426zM10.5 4.5414007C10.2777625 4.51407201 10.051423 4.5 9.82179677 4.5 9.71377555 4.5 9.60648167 4.50311409 9.5 4.50925739L9.5 2 10.5 2 10.5 4.5414007zM5.38028092 6.75545367C5.18389364 7.02383457 5.01124349 7.31068015 4.86542112 7.61289977L2.82179677 6.4330127 3.32179677 5.5669873 5.38028092 6.75545367zM4.86542112 12.3871002C5.01124349 12.6893198 5.18389364 12.9761654 5.38028092 13.2445463L3.32179677 14.4330127 2.82179677 13.5669873 4.86542112 12.3871002zM9.5 15.4907426C9.60648167 15.4968859 9.71377555 15.5 9.82179677 15.5 10.051423 15.5 10.2777625 15.485928 10.5 15.4585993L10.5 18 9.5 18 9.5 15.4907426zM14.3666368 13.0984292C14.5540843 12.8240078 14.7173255 12.5317385 14.8532861 12.2246957L17.1782032 13.5669873 16.6782032 14.4330127 14.3666368 13.0984292z'/%3E%3C/g%3E%3C/svg%3E"); +} + +.icon-warmer { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cg fill='%23999' fill-rule='evenodd'%3E%3Cpath d='M12.3247275 11.4890349C12.0406216 11.0007637 11.6761954 10.5649925 11.2495475 10.1998198 11.0890394 9.83238991 11 9.42659309 11 9 11 7.34314575 12.3431458 6 14 6 15.6568542 6 17 7.34314575 17 9 17 10.6568542 15.6568542 12 14 12 13.3795687 12 12.8031265 11.8116603 12.3247275 11.4890349zM17.6232392 11.6692284C17.8205899 11.4017892 17.9890383 11.1117186 18.123974 10.8036272L20.3121778 12.0669873 19.8121778 12.9330127 17.6232392 11.6692284zM18.123974 7.19637279C17.9890383 6.88828142 17.8205899 6.5982108 17.6232392 6.33077158L19.8121778 5.0669873 20.3121778 5.9330127 18.123974 7.19637279zM14.5 4.52746439C14.3358331 4.50931666 14.1690045 4.5 14 4.5 13.8309955 4.5 13.6641669 4.50931666 13.5 4.52746439L13.5 2 14.5 2 14.5 4.52746439zM13.5 13.4725356C13.6641669 13.4906833 13.8309955 13.5 14 13.5 14.1690045 13.5 14.3358331 13.4906833 14.5 13.4725356L14.5 16 13.5 16 13.5 13.4725356zM14 11C15.1045695 11 16 10.1045695 16 9 16 7.8954305 15.1045695 7 14 7 12.8954305 7 12 7.8954305 12 9 12 10.1045695 12.8954305 11 14 11zM9.5 11C10.6651924 11.4118364 11.5 12.5 11.5 14 11.5 16 10 17.5 8 17.5 6 17.5 4.5 16 4.5 14 4.5 12.6937812 5 11.5 6.5 11L6.5 7 9.5 7 9.5 11z'/%3E%3Cpath d='M12,14 C12,16.209139 10.209139,18 8,18 C5.790861,18 4,16.209139 4,14 C4,12.5194353 4.80439726,11.2267476 6,10.5351288 L6,4 C6,2.8954305 6.8954305,2 8,2 C9.1045695,2 10,2.8954305 10,4 L10,10.5351288 C11.1956027,11.2267476 12,12.5194353 12,14 Z M11,14 C11,12.6937812 10.1651924,11.5825421 9,11.1707057 L9,4 C9,3.44771525 8.55228475,3 8,3 C7.44771525,3 7,3.44771525 7,4 L7,11.1707057 C5.83480763,11.5825421 5,12.6937812 5,14 C5,15.6568542 6.34314575,17 8,17 C9.65685425,17 11,15.6568542 11,14 Z'/%3E%3C/g%3E%3C/svg%3E"); +} + +.icon-cooler { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cg fill='%23999' fill-rule='evenodd'%3E%3Cpath d='M10.4004569 11.6239517C10.0554735 10.9863849 9.57597206 10.4322632 9 9.99963381L9 9.7450467 9.53471338 9.7450467 10.8155381 8.46422201C10.7766941 8.39376637 10.7419749 8.32071759 10.7117062 8.2454012L9 8.2454012 9 6.96057868 10.6417702 6.96057868C10.6677696 6.86753378 10.7003289 6.77722682 10.7389179 6.69018783L9.44918707 5.40045694 9 5.40045694 9 4.34532219 9.32816127 4.34532219 9.34532219 2.91912025 10.4004569 2.91912025 10.4004569 4.53471338 11.6098599 5.74411634C11.7208059 5.68343597 11.8381332 5.63296451 11.9605787 5.59396526L11.9605787 3.8884898 10.8181818 2.74609294 11.5642748 2 12.5727518 3.00847706 13.5812289 2 14.3273218 2.74609294 13.2454012 3.82801356 13.2454012 5.61756719C13.3449693 5.65339299 13.4408747 5.69689391 13.5324038 5.74735625L14.7450467 4.53471338 14.7450467 2.91912025 15.8001815 2.91912025 15.8001815 4.34532219 17.2263834 4.34532219 17.2263834 5.40045694 15.6963166 5.40045694 14.4002441 6.69652946C14.437611 6.78161093 14.4692249 6.86979146 14.4945934 6.96057868L16.2570138 6.96057868 17.3994107 5.81818182 18.1455036 6.56427476 17.1370266 7.57275182 18.1455036 8.58122888 17.3994107 9.32732182 16.3174901 8.2454012 14.4246574 8.2454012C14.3952328 8.31861737 14.3616024 8.38969062 14.3240655 8.45832192L15.6107903 9.7450467 17.2263834 9.7450467 17.2263834 10.8001815 15.8001815 10.8001815 15.8001815 12.2263834 14.7450467 12.2263834 14.7450467 10.6963166 13.377994 9.32926387C13.3345872 9.34850842 13.2903677 9.36625331 13.2454012 9.38243281L13.2454012 11.3174901 14.3273218 12.3994107 13.5812289 13.1455036 12.5848864 12.1491612 11.5642748 13.1455036 10.8181818 12.3994107 11.9605787 11.2570138 11.9605787 9.40603474C11.8936938 9.38473169 11.828336 9.36000556 11.7647113 9.33206224L10.4004569 10.6963166 10.4004569 11.6239517zM12.75 8.5C13.3022847 8.5 13.75 8.05228475 13.75 7.5 13.75 6.94771525 13.3022847 6.5 12.75 6.5 12.1977153 6.5 11.75 6.94771525 11.75 7.5 11.75 8.05228475 12.1977153 8.5 12.75 8.5zM9.5 14C8.5 16.3333333 7.33333333 17.5 6 17.5 4.66666667 17.5 3.5 16.3333333 2.5 14L9.5 14z'/%3E%3Cpath d='M10,14 C10,16.209139 8.209139,18 6,18 C3.790861,18 2,16.209139 2,14 C2,12.5194353 2.80439726,11.2267476 4,10.5351288 L4,4 C4,2.8954305 4.8954305,2 6,2 C7.1045695,2 8,2.8954305 8,4 L8,10.5351288 C9.19560274,11.2267476 10,12.5194353 10,14 Z M9,14 C9,12.6937812 8.16519237,11.5825421 7,11.1707057 L7,4 C7,3.44771525 6.55228475,3 6,3 C5.44771525,3 5,3.44771525 5,4 L5,11.1707057 C3.83480763,11.5825421 3,12.6937812 3,14 C3,15.6568542 4.34314575,17 6,17 C7.65685425,17 9,15.6568542 9,14 Z'/%3E%3C/g%3E%3C/svg%3E"); +} + +.icon-help { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cpath fill='%23999' d='M11.292 12.516l.022 1.782H9.07v-1.804c0-1.98 1.276-2.574 2.662-3.278h-.022c.814-.44 1.65-.88 1.694-2.2.044-1.386-1.122-2.728-3.234-2.728-1.518 0-2.662.902-3.366 2.354L5 5.608C5.946 3.584 7.662 2 10.17 2c3.564 0 5.632 2.442 5.588 5.06-.066 2.618-1.716 3.41-3.102 4.158-.704.374-1.364.682-1.364 1.298zm-1.122 2.442c.858 0 1.452.594 1.452 1.452 0 .682-.594 1.408-1.452 1.408-.77 0-1.386-.726-1.386-1.408 0-.858.616-1.452 1.386-1.452z'/%3E%3C/svg%3E"); +} + +.icon-help-line { + background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23999' fill-rule='evenodd'%3E%3Cpath d='M10 20C4.477 20 0 15.523 0 10S4.477 0 10 0s10 4.477 10 10-4.477 10-10 10zm0-1a9 9 0 1 0 0-18 9 9 0 0 0 0 18z'/%3E%3Cpath d='M10.848 12.307l.02 1.578H8.784v-1.597c0-1.753 1.186-2.278 2.474-2.901h-.02c.756-.39 1.533-.78 1.574-1.948.041-1.226-1.043-2.414-3.006-2.414-1.41 0-2.474.798-3.128 2.083L5 6.193C5.88 4.402 7.474 3 9.805 3 13.118 3 15.04 5.161 15 7.478c-.061 2.318-1.595 3.019-2.883 3.68-.654.332-1.268.604-1.268 1.15zM9.805 14.47c.798 0 1.35.525 1.35 1.285 0 .603-.552 1.246-1.35 1.246-.715 0-1.288-.643-1.288-1.246 0-.76.573-1.285 1.288-1.285z' fill-rule='nonzero'/%3E%3C/g%3E%3C/svg%3E"); +} + +.icon-help-fill { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Ccircle cx='10' cy='10' r='10' fill='%23999'/%3E%3Cpath fill='%23FFF' fill-rule='nonzero' d='M8.368 7.189H5C5 3.5 7.668 2 10.292 2 13.966 2 16 4.076 16 7.012c0 3.754-3.849 3.136-3.849 5.211v1.656H8.455v-1.832c0-2.164 1.4-2.893 2.778-3.6.437-.242 1.006-.574 1.006-1.236 0-2.208-3.871-2.142-3.871-.022zM10.25 18a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5z'/%3E%3C/g%3E%3C/svg%3E"); +} + +.icon-help-inv { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cpath fill='%23999' fill-rule='evenodd' d='M10 20C4.477 20 0 15.523 0 10S4.477 0 10 0s10 4.477 10 10-4.477 10-10 10zM8.368 7.189c0-2.12 3.87-2.186 3.87.022 0 .662-.568.994-1.005 1.236-1.378.707-2.778 1.436-2.778 3.6v1.832h3.696v-1.656c0-2.075 3.849-1.457 3.849-5.21C16 4.075 13.966 2 10.292 2 7.668 2 5 3.501 5 7.189h3.368zM10.25 18a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5z'/%3E%3C/svg%3E"); +} + +.kelvin::after { + content: "K"; +} + +.mired::after { + content: " Mired"; +} + +.percent::after { + content: "%"; +} + +.sdpi-item-value+.icon-cooler, +.sdpi-item-value+.icon-warmer { + margin-left: 0px !important; + margin-top: 15px !important; +} + +/** +CONTROL-CENTER STYLES +*/ +input[type="range"].colorbrightness::-webkit-slider-runnable-track, +input[type="range"].colortemperature::-webkit-slider-runnable-track { + height: 8px; + background: #979797; + border-radius: 4px; + background-image: linear-gradient(to right, #94d0ec, #ffb165); +} + +input[type="range"].colorbrightness.greyscale::-webkit-slider-runnable-track, +input[type="range"].colorbrightness.grayscale::-webkit-slider-runnable-track { + background-color: #efefef; + background-image: linear-gradient(to right, black, rgba(0, 0, 0, 0)); +} + + +input[type="range"].colorbrightness::-webkit-slider-thumb, +input[type="range"].colortemperature::-webkit-slider-thumb { + width: 16px; + height: 16px; + border-radius: 20px; + margin-top: -5px; + background-color: #86c6e8; + box-shadow: 0px 0px 1px #000000; + border: 1px solid #d8d8d8; +} + +.sdpi-info-label { + display: inline-block; + user-select: none; + position: absolute; + height: 15px; + width: auto; + text-align: center; + border-radius: 4px; + min-width: 44px; + max-width: 80px; + background: white; + font-size: 11px; + color: black; + z-index: 1000; + box-shadow: 0px 0px 12px rgba(0, 0, 0, .8); + padding: 2px; + +} + +.sdpi-info-label.hidden { + opacity: 0; + transition: opacity 0.25s linear; +} + +.sdpi-info-label.shown { + position: absolute; + opacity: 1; + transition: opacity 0.25s ease-out; +} + +/* adding some styles here that override sdpi things so we can use this as notes for sdpi updates*/ +select { + min-width: 0px; + + /* this is a clunky fix for using background image as select arrow with long text options */ + -webkit-appearance: media-slider; + text-overflow: ellipsis; +} + +/*--------- context menu ----------*/ + +.context-menu { + display: none; + position: absolute; + z-index: 10; + padding: 12px 0; + width: 120px; + background-color: #3D3D3D; + border: solid 1px #dfdfdf; + box-shadow: 1px 1px 2px #cfcfcf; +} + +.context-menu--active { + display: block; +} + +.context-menu__items { + list-style: none; + margin: 0; + padding: 0; + overflow-y: auto; +} + +.context-menu__item { + display: block; + margin-bottom: 4px; + background-color: #3D3D3D !important; +} + +.context-menu__item:last-child { + margin-bottom: 0; +} + +.context-menu__link { + display: block; + padding: 4px 12px; + color: #ffff; + text-decoration: none; + white-space: nowrap; +} + +.context-menu__link:hover { + color: #fff; + background-color: #0066aa; +} + +.context-menu_message { + cursor: default; +} diff --git a/com.exension.hwinfo.sdPlugin/css/xsdpi.css b/com.exension.hwinfo.sdPlugin/css/xsdpi.css new file mode 100644 index 0000000..0ef64e3 --- /dev/null +++ b/com.exension.hwinfo.sdPlugin/css/xsdpi.css @@ -0,0 +1,1176 @@ +html { + height: 100%; + width: 100%; + overflow: hidden; +} + +html, body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-size: 9pt; + background-color: #2D2D2D; + color: #9a9a9a; +} + +body, +.localbody { + height: 100%; + padding: 0; + overflow-x: hidden; + overflow-y: auto; + margin: 0; + -webkit-overflow-scrolling: touch; +} + +mark { + background-color: #2D2D2D; + color: #d8d8d8; +} + +.localbody { + width: 360px; + /* height: 320px; */ +} + +.hidden { + display: none; +} + +hr, hr2 { + -webkit-margin-before: 1em; + -webkit-margin-after: 1em; + border-style: none; + background: #3d3d3d; + height: 1px; +} + +hr2, +.sdpi-heading { + display: flex; + flex-basis: 100%; + align-items: center; + color: inherit; + font-size: 12px; + margin: 8px 0px; +} + +.sdpi-heading::before, +.sdpi-heading::after { + content: ""; + flex-grow: 1; + background: #3d3d3d; + height: 1px; + font-size: 0px; + line-height: 0px; + margin: 0px 16px; +} + +hr2 { + height: 2px; +} + +hr, hr2 { + margin-left:16px; + margin-right:16px; +} + +::-webkit-progress-value, +meter::-webkit-meter-optimum-value { + border-radius: 2px; + background: linear-gradient(#ccf, #99f 20%, #77f 45%, #77f 55%, #cdf); +} + +::-webkit-progress-bar, +meter::-webkit-meter-bar { + border-radius: 3px; + background: #3d3d3d; +} + +::-webkit-progress-bar:active, +meter::-webkit-meter-bar:active { + border-radius: 3px; + background: #222222; +} +::-webkit-progress-value:active, +meter::-webkit-meter-optimum-value:active { + background: #99f; +} + +progress, +progress.sdpi-item-value { + min-height: 5px !important; + height: 5px; + background-color: #303030; +} + +progress { + margin-top: 8px !important; + margin-bottom: 8px !important; +} + +.full progress, +progress.full { + margin-top: 3px !important; +} + +::-webkit-progress-inner-element { + background-color: transparent; +} + + +.sdpi-item[type="progress"] { + margin-top: 4px !important; + margin-bottom: 12px; + min-height: 15px; +} + +.sdpi-item-child.full:last-child { + margin-bottom: 4px; +} + +.tabs { + /** + * Setting display to flex makes this container lay + * out its children using flexbox, the exact same + * as in the above "Stepper input" example. + */ + display: flex; + + border-bottom: 1px solid #D7DBDD; +} + +.tab { + cursor: pointer; + padding: 5px 30px; + color: #16a2d7; + font-size: 12px; + border-bottom: 2px solid transparent; +} + +.tab.is-tab-selected { + border-bottom-color: #4ebbe4; +} + +select { + -webkit-appearance: none; + -moz-appearance: none; + -o-appearance: none; + appearance: none; + background: url(caret.svg) no-repeat 97% center; +} + +label.sdpi-file-label, +input[type="button"], +input[type="submit"], +input[type="reset"], +input[type="file"], +input[type=file]::-webkit-file-upload-button, +button, +select { + color: #d8d8d8; + border: 1pt solid #303030; + font-size: 1em; + font-weight: normal; + background-color: #3d3d3d; + border-radius: 0; +} + +label.sdpi-file-label, +input[type="button"], +input[type="submit"], +input[type="reset"], +input[type="file"], +input[type=file]::-webkit-file-upload-button, +button { + border: 1pt solid #d8d8d8; + border-radius: 4px; + font-size: 11px; + min-height: 23px !important; + height: 23px !important; + margin-right: 8px; +} + +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type="file"] { + border-radius: 0px; + max-width: 220px; +} + + +option { + height: 1.5em; + padding: 4px; +} + +/* SDPI */ + +.sdpi-wrapper { + overflow-x: hidden; +} + +.sdpi-item { + display: flex; + flex-direction: row; + min-height: 32px; + align-items: center; + margin-top: 2px; + max-width: 344px; +} + +.sdpi-item:first-child { + margin-top:0px; +} + +.sdpi-item:last-child { + margin-bottom: 0px; +} + +.sdpi-item > *:not(.sdpi-item-label):not(meter) { + min-height: 26px; + padding: 0px 4px 0px 4px; +} + +.sdpi-item-group { + padding: 0 !important; +} + +meter.sdpi-item-value { + margin-left: 6px; +} + +.sdpi-item[type="group"] { + display: block; + margin-top: 12px; + margin-bottom: 12px; + /* border: 1px solid white; */ + flex-direction: unset; + text-align: left; +} + +.sdpi-item[type="group"] > .sdpi-item-label, +.sdpi-item[type="group"].sdpi-item-label { + width: 96%; + text-align: left; + font-weight: 700; + margin-bottom: 4px; + padding-left: 4px; +} + +dl, +ul, +ol { + -webkit-margin-before: 0px; + -webkit-margin-after: 4px; + -webkit-padding-start: 1em; + max-height: 90px; + overflow-y: scroll; + cursor: pointer; + user-select: none; +} + +table.sdpi-item-value, +dl.sdpi-item-value, +ul.sdpi-item-value, +ol.sdpi-item-value { + -webkit-margin-before: 4px; + -webkit-margin-after: 8px; + -webkit-padding-start: 1em; + width: 224px; + text-align: center; +} + +table > caption { + margin: 2px; +} + +.list, +.sdpi-item[type="list"] { + align-items: baseline; +} + +.sdpi-item-label { + text-align: right; + flex: none; + width: 94px; /* 27%; +++ */ + padding-right: 4px; + font-weight: bold; +} + +.sdpi-item-label > small{ + font-weight: normal; +} + +.sdpi-item-label:after { + content: ": "; +} + +.sdpi-test, +.sdpi-item-value { + flex: 1 0 0; + /* flex-grow: 1; + flex-shrink: 0; */ + margin-right: 14px; + margin-left: 4px; + justify-content: space-evenly; +} + +input.sdpi-item-value { + margin-left: 5px; +} + +.sdpi-item-value button, +button.sdpi-item-value { + margin-left: 7px; + margin-right: 19px; +} + +.sdpi-item-value.range { + margin-left: 0px; +} + +table, +dl.sdpi-item-value, +ul.sdpi-item-value, +ol.sdpi-item-value, +.sdpi-item-value > dl, +.sdpi-item-value > ul, +.sdpi-item-value > ol +{ + list-style-type: none; + list-style-position: outside; + margin-left: -4px; + margin-right: -4px; + padding: 4px; + border: 1px solid #3a3a3a; +} + +dl.sdpi-item-value, +ul.sdpi-item-value, +ol.sdpi-item-value, +.sdpi-item-value > ol { + list-style-type: none; + list-style-position: inside; + margin-left: 5px; + margin-right: 18px; + padding: 4px !important; +} + +ol.sdpi-item-value, +.sdpi-item-value > ol[listtype="none"] { + list-style-type: none; +} +ol.sdpi-item-value[type="decimal"], +.sdpi-item-value > ol[type="decimal"] { + list-style-type: decimal; +} + +ol.sdpi-item-value[type="decimal-leading-zero"], +.sdpi-item-value > ol[type="decimal-leading-zero"] { + list-style-type: decimal-leading-zero; +} + +ol.sdpi-item-value[type="lower-alpha"], +.sdpi-item-value > ol[type="lower-alpha"] { + list-style-type: lower-alpha; +} + +ol.sdpi-item-value[type="upper-alpha"], +.sdpi-item-value > ol[type="upper-alpha"] { + list-style-type: upper-alpha; +} + +ol.sdpi-item-value[type="upper-roman"], +.sdpi-item-value > ol[type="upper-roman"] { + list-style-type: upper-roman; +} + +ol.sdpi-item-value[type="lower-roman"], +.sdpi-item-value > ol[type="lower-roman"] { + list-style-type: upper-roman; +} + +tr:nth-child(even), +.sdpi-item-value > ul > li:nth-child(even), +.sdpi-item-value > ol > li:nth-child(even), +li:nth-child(even) { + background-color: rgba(0,0,0,.2) +} + +td:hover, +.sdpi-item-value > ul > li:hover:nth-child(even), +.sdpi-item-value > ol > li:hover:nth-child(even), +li:hover:nth-child(even), +li:hover { + background-color: rgba(255,255,255,.1); +} + +td.selected, +td.selected:hover, +li.selected:hover, +li.selected { + color: white; + background-color: #77f; +} + +tr { + border: 1px solid #3a3a3a; +} + +td { + border-right: 1px solid #3a3a3a; +} + +tr:last-child, +td:last-child { + border: none; +} + +.sdpi-item-value.select, +.sdpi-item-value > select { + margin-right: 13px; + margin-left: 4px; +} + +.sdpi-item-child, +.sdpi-item-group > .sdpi-item > input[type="color"] { + margin-top: 0.4em; + margin-right: 4px; +} + +.full, +.full *, +.sdpi-item-value.full, +.sdpi-item-child > full > *, +.sdpi-item-child.full, +.sdpi-item-child.full > *, +.full > .sdpi-item-child, +.full > .sdpi-item-child > *{ + display: flex; + flex: 1 1 0; + margin-bottom: 4px; + margin-left: 0px; + width: 100%; + + justify-content: space-evenly; +} + +.sdpi-item-group > .sdpi-item > input[type="color"] { + margin-top: 0px; +} + +::-webkit-calendar-picker-indicator:focus, +input[type=file]::-webkit-file-upload-button:focus, +button:focus, +textarea:focus, +input:focus, +select:focus, +option:focus, +details:focus, +summary:focus, +.custom-select select { + outline: none; +} + +input:not([type="range"]), +textarea { + -webkit-appearance: none; + background: #3d3d3d; + color: #d8d8d8; + font-weight: normal; + font-size: 9pt; + border: none; +} + +textarea + label { + display: flex; + justify-content: flex-end +} + +input[type="checkbox"] { + display: none; +} +input[type="radio"] + label, +input[type="checkbox"] + label { + font-size: 9pt; + color: #d8d8d8; + font-weight: normal; + margin-right: 8px; +} + +input[type="radio"] + label:after, +input[type="checkbox"] + label:after { + content: " " !important; +} + +.sdpi-item[type="radio"] > .sdpi-item-value, +.sdpi-item[type="checkbox"] > .sdpi-item-value { + padding-top: 0.75em; +} + +.sdpi-item[type="checkbox"] > .sdpi-item-value > * { + margin-top: 4px; +} + +.sdpi-item[type="checkbox"] .sdpi-item-child, +.sdpi-item[type="radio"] .sdpi-item-child { + display: inline-block; +} + +.sdpi-item[type="range"] .sdpi-item-value, +.sdpi-item[type="meter"] .sdpi-item-child, +.sdpi-item[type="progress"] .sdpi-item-child { + display: flex; +} + +.vertical.sdpi-item[type="range"] .sdpi-item-value { + display: block; +} + +.sdpi-item[type="range"] .sdpi-item-value span, +.sdpi-item[type="meter"] .sdpi-item-child span, +.sdpi-item[type="progress"] .sdpi-item-child span { + margin-top: -2px; + min-width: 24px; + text-align: right; + user-select: none; + cursor: pointer; +} + +.sdpi-item[type="range"] .sdpi-item-value span { + margin-top: 7px; + text-align: right; +} + +span + input[type="range"] { + display: flex; + max-width: 168px; + +} +.sdpi-item[type="range"] .sdpi-item-value span:first-child, +.sdpi-item[type="meter"] .sdpi-item-child span:first-child, +.sdpi-item[type="progress"] .sdpi-item-child span:first-child { + margin-right: 4px; +} + +.sdpi-item[type="range"] .sdpi-item-value span:last-child, +.sdpi-item[type="meter"] .sdpi-item-child span:last-child, +.sdpi-item[type="progress"] .sdpi-item-child span:last-child { + margin-left: 4px; +} + + +.sdpi-item[type="meter"] .sdpi-item-child meter + span:last-child, +.sdpi-item[type="progress"] .sdpi-item-child meter + span:last-child { + margin-left: -14px; +} + +.sdpi-item[type="radio"] > .sdpi-item-value > * { + margin-top: 2px; +} + +details { + padding: 8px 18px 8px 12px; +} + +details > h4 { + border-bottom: 1px solid #3a3a3a; +} + +legend { + display: none; +} +.sdpi-item-value > textarea { + padding: 0px; + width: 227px; + margin-left: 1px; +} + +input[type="radio"] + label span, +input[type="checkbox"] + label span { + display: inline-block; + width: 19px; + height: 19px; + margin: -2px 4px 0 0; + vertical-align: middle; + /* background: url(buttons.png) left top no-repeat; */ + background: #3d3d3d; + cursor: pointer; +} + +input[type="checkbox"] + label span { + margin: 2px 4px 2px 0; + width: 16px; + height: 16px; + border-radius: 3px; + border: 1px solid rgb(0,0,0,0); +} + +input[type="checkbox"]:checked + label span { + /* background: url(buttons.png) -19px top no-repeat; */ + background-color: #77f; + background-image: url(check.svg); + background-repeat: no-repeat; + background-position: center center; + border: 1px solid rgb(0,0,0,.4); +} + +input[type="radio"] { + display: none; +} + +input[type="radio"] + label span { + background: url(buttons.png) -38px top no-repeat; +} + +input[type="radio"]:checked + label span { + background: url(buttons.png) -57px top no-repeat; +} + +input[type="range"] { + width: 224px; + height: 30px; + overflow: hidden; + cursor: pointer; + background: transparent !important; +} + +.sdpi-item > input[type="range"] { + margin-left: 8px; + max-width: 224px; + width: 224px; + padding: 0px; +} + +/* +input[type="range"], +input[type="range"]::-webkit-slider-runnable-track, +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; +} +*/ +::-webkit-slider-thumb, +::-webkit-slider-runnable-track, +.vertical input[type="range"]::-webkit-slider-thumb, +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none !important; +} + +.vertical input[type="range"]::-webkit-slider-runnable-track, +input[type="range"]::-webkit-slider-runnable-track { + height: 6px; + background: #979797; + border-radius: 3px; + padding:0px !important; + border: 1px solid #3d3d3d; +} + +.vertical input[type="range"]::-webkit-slider-runnable-track { + height: auto; + width: 6px; +} + +input[type="range"]::-webkit-slider-thumb { + position: relative; + height: 6px; + width: 6px; + margin-top: -5px; + background: rgb(255,0,0,.2); +} + +/* new */ +input[type="range"]::-webkit-slider-thumb { + position: relative; + height: 16px; + width: 16px; + margin-top: -5px; + background: rgb(255,0,0,1); +} + + +input[type="range" i]{ + -webkit-appearance: none; + margin: 0; +} + +input[type="range" i]::-webkit-slider-thumb, input[type="range" i]::-webkit-media-slider-thumb { + -webkit-appearance: none; + box-sizing: border-box; + display: block; + -webkit-user-modify: read-only !important; +} + + + +#thumb, +input[type="range"]::-webkit-slider-thumb::before { + position: absolute; + content: ""; + height: 5px; /* equal to height of runnable track or 1 less */ + width: 500px; /* make this bigger than the widest range input element */ + left: -502px; /* this should be -2px - width */ + top: 8px; /* don't change this */ + background: #77f; +} + +.vertical.sdpi-item:first-child, +.vertical { + margin-top: 12px; + margin-bottom: 16px; +} +.vertical > .sdpi-item-value { + margin-right: 16px; +} + +.vertical .sdpi-item-group { + width: 100%; + display: flex; + justify-content: space-evenly; +} + +.vertical input[type="range"] { + height: 100px; + width: 21px; + /*-webkit-appearance: slider-vertical;*/ + display: flex; + flex-flow: column; +} + +.vertical input[type="range"]::-webkit-slider-runnable-track { + height: auto; + width: 5px; +} + +.vertical input[type="range"]::-webkit-slider-thumb { + margin-top: 0px; + margin-left: -6px; +} + +.vertical .sdpi-item-value { + flex-flow: column; + align-items: flex-start; +} + +.vertical.sdpi-item[type="range"] .sdpi-item-value { + align-items: center; + margin-right: 16px; + text-align: center; +} + +.vertical.sdpi-item[type="range"] .sdpi-item-value span, +.vertical input[type="range"] .sdpi-item-value span { + text-align: center; + margin: 4px 0px; +} + +input[type="color"] { + min-width: 32px; + min-height: 32px; + width: 32px; + height: 32px; + padding: 0; + background-color: #2d2d2d; + flex: none; +} + +::-webkit-color-swatch { + min-width: 24px; +} + +textarea { + height: 3em; + word-break: break-word; + line-height: 1.5em; +} + +.textarea { + padding: 0px !important; +} + +input[type="textxxx"], +textareaxxx { + border-radius: 0pt; + border: #303030 1pt solid; +} + +textarea { + width: 221px; /*98%;*/ + height: 96%; + min-height: 6em; + resize: none; + border-radius: 0; +} + +/* CAROUSEL */ + +.sdpi-item[type="carousel"]{ + +} + +.sdpi-item.card-carousel-wrapper, +.sdpi-item > .card-carousel-wrapper { + padding: 0; +} + + +.card-carousel-wrapper { + display: flex; + align-items: center; + justify-content: center; + margin: 12px auto; + color: #666a73; +} + +.card-carousel { + display: flex; + justify-content: center; + width: 278px; +} +.card-carousel--overflow-container { + overflow: hidden; +} +.card-carousel--nav__left, +.card-carousel--nav__right { + /* display: inline-block; */ + width: 12px; + height: 12px; + border-top: 2px solid #42b883; + border-right: 2px solid #42b883; + cursor: pointer; + margin: 0 4px; + transition: transform 150ms linear; +} +.card-carousel--nav__left[disabled], +.card-carousel--nav__right[disabled] { + opacity: 0.2; + border-color: black; +} +.card-carousel--nav__left { + transform: rotate(-135deg); +} +.card-carousel--nav__left:active { + transform: rotate(-135deg) scale(0.85); +} +.card-carousel--nav__right { + transform: rotate(45deg); +} +.card-carousel--nav__right:active { + transform: rotate(45deg) scale(0.85); +} +.card-carousel-cards { + display: flex; + transition: transform 150ms ease-out; + transform: translatex(0px); +} +.card-carousel-cards .card-carousel--card { + margin: 0 5px; + cursor: pointer; + /* box-shadow: 0 4px 15px 0 rgba(40, 44, 53, 0.06), 0 2px 2px 0 rgba(40, 44, 53, 0.08); */ + background-color: #fff; + border-radius: 4px; + z-index: 3; +} +.xxcard-carousel-cards .card-carousel--card:first-child { + margin-left: 0; +} +.xxcard-carousel-cards .card-carousel--card:last-child { + margin-right: 0; +} +.card-carousel-cards .card-carousel--card img { + vertical-align: bottom; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + transition: opacity 150ms linear; + width: 60px; +} +.card-carousel-cards .card-carousel--card img:hover { + opacity: 0.5; +} +.card-carousel-cards .card-carousel--card--footer { + border-top: 0; + max-width: 80px; + overflow: hidden; + display: flex; + height: 100%; + flex-direction: column; +} +.card-carousel-cards .card-carousel--card--footer p { + padding: 3px 0; + margin: 0; + margin-bottom: 2px; + font-size: 15px; + font-weight: 500; + color: #2c3e50; +} +.card-carousel-cards .card-carousel--card--footer p:nth-of-type(2) { + font-size: 12px; + font-weight: 300; + padding: 6px; + color: #666a73; +} + + +h1 { + font-size: 3em; + font-weight: 100; + text-align: center; + margin-bottom: 0; + color: #42b883; +} + +/* debug +div { + background-color: rgba(64,128,255,0.2); +} +*/ + +.min80 > .sdpi-item-child { + min-width: 80px; +} + +.min100 > .sdpi-item-child { + min-width: 100px; +} + +.min120 > .sdpi-item-child { + min-width: 120px; +} + +.min140 > .sdpi-item-child { + min-width: 140px; +} + +.min160 > .sdpi-item-child { + min-width: 160px; +} + +.min200 > .sdpi-item-child { + min-width: 200px; +} + +.max40 { + flex-basis: 40%; + flex-grow: 0; +} + +.max30 { + flex-basis: 30%; + flex-grow: 0; +} + +.max20 { + flex-basis: 20%; + flex-grow: 0; +} + +.up20 { + margin-top: -20px; +} + + +::-webkit-datetime-edit { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + background: url(elg_calendar_inv.svg) no-repeat left center; + padding-right: 1em; + padding-left: 25px; + background-position: 4px 0px; + } +::-webkit-datetime-edit-fields-wrapper { + + } +::-webkit-datetime-edit-text { padding: 0 0.3em; } +::-webkit-datetime-edit-month-field { } +::-webkit-datetime-edit-day-field {} +::-webkit-datetime-edit-year-field {} +::-webkit-inner-spin-button { + + /* display: none; */ + } +::-webkit-calendar-picker-indicator { + background: transparent; + font-size: 17px; +} + +::-webkit-calendar-picker-indicator:focus { + background-color: rgba(0,0,0,0.2); +} + +input[type="date"] { + -webkit-align-items: center; + display: -webkit-inline-flex; + font-family: monospace; + overflow: hidden; + padding: 0; + -webkit-padding-start: 1px; +} + +input::-webkit-datetime-edit { + -webkit-flex: 1; + -webkit-user-modify: read-only !important; + display: inline-block; + min-width: 0; + overflow: hidden; +} + +input::-webkit-datetime-edit-fields-wrapper { + /* -webkit-user-modify: read-only !important; + display: inline-block; + padding: 1px 0; + white-space: pre; */ + +} + +/* +input[type="date"] { + background-color: red; + outline: none; +} + +input[type="date"]::-webkit-clear-button { + font-size: 18px; + height: 30px; + position: relative; +} + +input[type="date"]::-webkit-inner-spin-button { + height: 28px; +} + +input[type="date"]::-webkit-calendar-picker-indicator { + font-size: 15px; +} */ + +input[type="file"] { + opacity: 0; + display: none; +} + +.sdpi-item > input[type="file"] { + opacity: 1; + display: flex; +} + +input[type="file"] + span { + display: flex; + flex: 0 1 auto; + background-color: #0000ff50; +} + +label.sdpi-file-label { + cursor: pointer; + user-select: none; + display: inline-block; + min-height: 21px !important; + height: 21px !important; + line-height: 20px; + padding: 0px 4px; + margin: auto; + margin-right: 0px; + float:right; +} + +.sdpi-file-label > label:active, +.sdpi-file-label.file:active, +label.sdpi-file-label:active, +label.sdpi-file-info:active, +input[type="file"]::-webkit-file-upload-button:active, +button:active { + background-color: #d8d8d8; + color:#303030; +} + + +input:required:invalid, input:focus:invalid { + /* border: 1px solid red; */ + /* background: #3d3d3d url(pi_required.svg) no-repeat 98% center; */ + background: #3d3d3d url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5IiBoZWlnaHQ9IjkiIHZpZXdCb3g9IjAgMCA5IDkiPgogICAgPHBhdGggZmlsbD0iI0Q4RDhEOCIgZD0iTTQuNSwwIEM2Ljk4NTI4MTM3LC00LjU2NTM4NzgyZS0xNiA5LDIuMDE0NzE4NjMgOSw0LjUgQzksNi45ODUyODEzNyA2Ljk4NTI4MTM3LDkgNC41LDkgQzIuMDE0NzE4NjMsOSAzLjA0MzU5MTg4ZS0xNiw2Ljk4NTI4MTM3IDAsNC41IEMtMy4wNDM1OTE4OGUtMTYsMi4wMTQ3MTg2MyAyLjAxNDcxODYzLDQuNTY1Mzg3ODJlLTE2IDQuNSwwIFogTTQsMSBMNCw2IEw1LDYgTDUsMSBMNCwxIFogTTQuNSw4IEM0Ljc3NjE0MjM3LDggNSw3Ljc3NjE0MjM3IDUsNy41IEM1LDcuMjIzODU3NjMgNC43NzYxNDIzNyw3IDQuNSw3IEM0LjIyMzg1NzYzLDcgNCw3LjIyMzg1NzYzIDQsNy41IEM0LDcuNzc2MTQyMzcgNC4yMjM4NTc2Myw4IDQuNSw4IFoiLz4KICA8L3N2Zz4) no-repeat 98% center; +} + +input:required:valid { + /* background: #3d3d3d url(pi_required_ok.svg) no-repeat 98% center; */ + background: #3d3d3d url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5IiBoZWlnaHQ9IjkiIHZpZXdCb3g9IjAgMCA5IDkiPjxwb2x5Z29uIGZpbGw9IiNEOEQ4RDgiIHBvaW50cz0iNS4yIDEgNi4yIDEgNi4yIDcgMy4yIDcgMy4yIDYgNS4yIDYiIHRyYW5zZm9ybT0icm90YXRlKDQwIDQuNjc3IDQpIi8+PC9zdmc+) no-repeat 98% center; +} + +.tooltip, +:tooltip, +:title { + color: yellow; +} + +[title]:hover { + display: flex; + align-items: center; + justify-content: center; +} + +[title]:hover::after { + content: ''; + position: absolute; + bottom: -1000px; + left: 8px; + display: none; + color: #fff; + border: 8px solid transparent; + border-bottom: 8px solid #000; +} +[title]:hover::before { +content: attr(title); + display: flex; + justify-content: center; + align-self: center; + padding: 6px 12px; + border-radius: 5px; + background: rgba(0,0,0,0.8); + color: #d8d8d8; + font-size: 12px; + font-family: sans-serif; + opacity: 1; + position: absolute; + height: auto; + /* width: 50%; + left: 35%; */ + text-align: center; + bottom: 2px; + z-index: 100; + box-shadow: 0px 3px 6px rgba(0, 0, 0, .5); + /* box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); */ +} + +.sdpi-item-group.file { + width: 232px; + display: flex; + align-items: center; +} + +.sdpi-file-info { + overflow-wrap: break-word; + word-wrap: break-word; + hyphens: auto; + + min-width: 132px; + max-width: 144px; + max-height: 32px; + margin-top: 0px; + margin-left: 5px; + display: inline-block; + overflow: hidden; + padding: 6px 4px; + background-color: #3d3d3d; +} + +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); +} + +::-webkit-scrollbar-thumb { + background-color: #999999; + outline: 1px solid slategrey; + border-radius: 8px; +} diff --git a/com.exension.hwinfo.sdPlugin/defaultImage.png b/com.exension.hwinfo.sdPlugin/defaultImage.png new file mode 100644 index 0000000..fc09730 Binary files /dev/null and b/com.exension.hwinfo.sdPlugin/defaultImage.png differ diff --git a/com.exension.hwinfo.sdPlugin/defaultImage@2x.png b/com.exension.hwinfo.sdPlugin/defaultImage@2x.png new file mode 100644 index 0000000..84e3dd6 Binary files /dev/null and b/com.exension.hwinfo.sdPlugin/defaultImage@2x.png differ diff --git a/com.exension.hwinfo.sdPlugin/icon.png b/com.exension.hwinfo.sdPlugin/icon.png new file mode 100644 index 0000000..631c30b Binary files /dev/null and b/com.exension.hwinfo.sdPlugin/icon.png differ diff --git a/com.exension.hwinfo.sdPlugin/icon@2x.png b/com.exension.hwinfo.sdPlugin/icon@2x.png new file mode 100644 index 0000000..2212103 Binary files /dev/null and b/com.exension.hwinfo.sdPlugin/icon@2x.png differ diff --git a/com.exension.hwinfo.sdPlugin/index_pi.html b/com.exension.hwinfo.sdPlugin/index_pi.html new file mode 100644 index 0000000..c5cb58c --- /dev/null +++ b/com.exension.hwinfo.sdPlugin/index_pi.html @@ -0,0 +1,169 @@ + + + + + + + + + Property Inspector Samples PI + + + + + + + +
+
Plugin Error
+
+
+ Unable To Communicate With HWiNFO64 +

+ The plugin is unable to communicate with HWiNFO64 +

+

Make sure it's running and configured properly

+

+ For help on how to properly setup HWiNFO64, refer to the + + documentation + +

+
+
+
+ +
+
Font Sizes
+ +
+
Title
+
+ 8 + + 20 +
+
+ +
+
Value
+
+ 8 + + 20 +
+
+ +
HWiNFO Sensors
+ +
+
Sensor
+ +
+ +
+
Reading
+ +
+ +
Value Params
+ +
+
Min/Max
+ +
+ + +
+
+ +
+ Advanced +
+
Format
+ +
+
+
+ Help +

Format can be used to modify format of the value.

+

+ For more information on how to use this field, + click here. +

+
+
+ +
+
Divisor
+ +
+
+
+ Help +

Divisor can be used to convert the value.

+

+ For example, converting bytes to megabits, set the divisor to: + "125" +

+
+
+
+ +
Graph Colors
+ +
+
Background
+ +
Value Text
+ +
+ +
+
Foreground
+ +
Highlight
+ +
+
+ + + + + \ No newline at end of file diff --git a/com.exension.hwinfo.sdPlugin/index_pi.js b/com.exension.hwinfo.sdPlugin/index_pi.js new file mode 100644 index 0000000..b9fa0d0 --- /dev/null +++ b/com.exension.hwinfo.sdPlugin/index_pi.js @@ -0,0 +1,571 @@ +// this is our global websocket, used to communicate from/to Stream Deck software +// and some info about our plugin, as sent by Stream Deck software +var websocket = null, + uuid = null, + actionInfo = {}, + inInfo = {}, + runningApps = [], + isQT = navigator.appVersion.includes("QtWebEngine"), + onchangeevt = "onchange"; // 'oninput'; // change this, if you want interactive elements act on any change, or while they're modified + +function connectElgatoStreamDeckSocket(inPort, inUUID, inRegisterEvent, inInfo, inActionInfo) { + uuid = inUUID; + // please note: the incoming arguments are of type STRING, so + // in case of the inActionInfo, we must parse it into JSON first + actionInfo = JSON.parse(inActionInfo); // cache the info + inInfo = JSON.parse(inInfo); + websocket = new WebSocket("ws://localhost:" + inPort); + + /** Since the PI doesn't have access to native settings + * Stream Deck sends some color settings to PI + * We use these to adjust some styles (e.g. highlight-colors for checkboxes) + */ + addDynamicStyles(inInfo.colors, "connectSocket"); + initPropertyInspector(5); + + // if connection was established, the websocket sends + // an 'onopen' event, where we need to register our PI + websocket.onopen = function () { + var json = { + event: inRegisterEvent, + uuid: inUUID, + }; + // register property inspector to Stream Deck + websocket.send(JSON.stringify(json)); + sendValueToPlugin("propertyInspectorConnected", "property_inspector"); + }; + + websocket.onmessage = function (evt) { + // Received message from Stream Deck + var jsonObj = JSON.parse(evt.data); + var event = jsonObj["event"]; + if ( + "boolean" === typeof getPropFromString(jsonObj, "payload.error") && + event === "sendToPropertyInspector" + ) { + if (jsonObj.payload.error === true) { + document.querySelector("#ui").style = "display:none"; + document.querySelector("#error").style = "display:block"; + } else if (jsonObj.payload.message === "show_ui") { + document.querySelector("#ui").style = "display:block"; + document.querySelector("#error").style = "display:none"; + sendValueToPlugin("propertyInspectorConnected", "property_inspector"); + } + } + if ( + getPropFromString(jsonObj, "payload.sensors") && + event === "sendToPropertyInspector" + ) { + addSensors( + document.querySelector("#sensorSelect"), + jsonObj.payload.sensors, + jsonObj.payload.settings + ); + } + if ( + getPropFromString(jsonObj, "payload.readings") && + event === "sendToPropertyInspector" + ) { + addReadings( + document.querySelector("#readingSelect"), + jsonObj.payload.readings, + jsonObj.payload.settings + ); + } + if (getPropFromString(jsonObj, "payload.settings")) { + var settings = jsonObj.payload.settings; + if (settings.min === 0 && settings.max === 0) { + // don't show 0, 0 min/max + } else { + document.querySelector("#min").value = settings.min; + document.querySelector("#max").value = settings.max; + } + document.querySelector("#format input").value = settings.format; + document.querySelector("#divisor input").value = settings.divisor || ""; + if ( + settings.format.length > 0 || + (settings.divisor && settings.divisor.length > 0) + ) { + var attr = document.createAttribute("open"); + attr.value = "open"; + document + .querySelector("#advanced_details") + .attributes.setNamedItem(attr); + } + if (settings.foregroundColor !== "") { + document.querySelector("#foreground").value = settings.foregroundColor; + } + if (settings.backgroundColor !== "") { + document.querySelector("#background").value = settings.backgroundColor; + } + if (settings.highlightColor !== "") { + document.querySelector("#highlight").value = settings.highlightColor; + } + if (settings.valueTextColor !== "") { + document.querySelector("#valuetext").value = settings.valueTextColor; + } + if (settings.titleFontSize !== "") { + document.querySelector("#titleFontSize input").value = + settings.titleFontSize || 10.5; + } + if (settings.valueFontSize !== "") { + document.querySelector("#valueFontSize input").value = + settings.valueFontSize || 10.5; + } + } + }; +} + +function sortBy(key) { + return function (a, b) { + if (a[key] > b[key]) return 1; + if (b[key] > a[key]) return -1; + return 0; + }; +} + +function addSensors(el, sensors, settings) { + var i; + for (i = el.options.length - 1; i >= 0; i--) { + el.remove(i); + } + + el.removeAttribute("disabled"); + + var option = document.createElement("option"); + option.text = "Choose a sensor"; + option.disabled = true; + if (settings.isValid !== true) { + option.selected = true; + } + el.add(option); + var sortByName = sortBy("name"); + sensors.sort(sortByName).forEach((s) => { + var option = document.createElement("option"); + option.text = s.name; + option.value = s.uid; + if (settings.isValid === true && settings.sensorUid === s.uid) { + option.selected = true; + setTimeout(function () { + var event = new Event("change"); + el.dispatchEvent(event); + }, 0); + } + el.add(option); + }); +} + +function addReadings(el, readings, settings) { + var i; + for (i = el.options.length - 1; i >= 0; i--) { + el.remove(i); + } + + el.removeAttribute("disabled"); + + var option = document.createElement("option"); + option.text = "Choose a reading"; + option.disabled = true; + if (settings.isValid !== true) { + option.selected = true; + } + el.add(option); + + var sortByLabel = sortBy("label"); + var maxL = 0; + readings.sort(sortByLabel).forEach((r) => { + var l = r.prefix.length; + if (l > maxL) { + maxL = l; + } + }); + readings.sort(sortByLabel).forEach((r) => { + var option = document.createElement("option"); + option.style = "white-space: pre"; + var spaces = " "; + for (i = 0; i < maxL - r.prefix.length; ++i) { + spaces += " "; + } + option.innerHTML = `${r.prefix}${spaces}${r.label}`; + option.value = r.id; + if (settings.isValid === true && settings.readingId === r.id) { + option.selected = true; + } + el.add(option); + }); +} + +function initPropertyInspector(initDelay) { + prepareDOMElements(document); +} + +function revealSdpiWrapper() { + const el = document.querySelector(".sdpi-wrapper"); + el && el.classList.remove("hidden"); +} + +// openUrl in default browser +function openUrl(url) { + if (websocket && websocket.readyState === 1) { + const json = { + event: "openUrl", + payload: { + url: url, + }, + }; + websocket.send(JSON.stringify(json)); + } +} + +// our method to pass values to the plugin +function sendValueToPlugin(value, param) { + if (websocket && websocket.readyState === 1) { + const json = { + action: actionInfo["action"], + event: "sendToPlugin", + context: uuid, + payload: { + [param]: value, + }, + }; + websocket.send(JSON.stringify(json)); + } +} + +if (!isQT) { + document.addEventListener("DOMContentLoaded", function () { + initPropertyInspector(100); + }); +} + +/** the beforeunload event is fired, right before the PI will remove all nodes */ +window.addEventListener("beforeunload", function (e) { + e.preventDefault(); + sendValueToPlugin("propertyInspectorWillDisappear", "property_inspector"); + // Don't set a returnValue to the event, otherwise Chromium with throw an error. // e.returnValue = ''; +}); + +/** the pagehide event is fired, when the view disappears */ +/* +window.addEventListener('pagehide', function (event) { + console.log('%c%s','background: green; font-size: 22px; font-weight: bold;','window --->> pagehide.'); + sendValueToPlugin('propertyInspectorPagehide', 'property_inspector'); + +}); +*/ + +/** the unload event is fired, when the PI will finally disappear */ +/* +window.addEventListener('unload', function (event) { + console.log('%c%s','background: orange; font-size: 22px; font-weight: bold;','window --->> onunload.'); + sendValueToPlugin('propertyInspectorDisconnected', 'property_inspector'); +}); +*/ + +/** if you prefer, you can apply these listeners to PI's body, like so: + * + * + * + * + */ + +/** CREATE INTERACTIVE HTML-DOM + * where elements can be clicked or act on their 'change' event. + * Messages are then processed using the 'handleSdpiItemClick' method below. + */ + +function prepareDOMElements(baseElement) { + baseElement = baseElement || document; + Array.from(baseElement.querySelectorAll(".sdpi-item-value")).forEach( + (el, i) => { + const elementsToClick = [ + "BUTTON", + "OL", + "UL", + "TABLE", + "METER", + "PROGRESS", + "CANVAS", + ].includes(el.tagName); + const evt = elementsToClick ? "onclick" : onchangeevt || "onchange"; + // console.log(el.type, el.tagName, elementsToClick, el, evt); + + /** Look for combinations, where we consider the span as label for the input + * we don't use `labels` for that, because a range could have 2 labels. + */ + const inputGroup = el.querySelectorAll("input, span"); + if (inputGroup.length === 2) { + const offs = inputGroup[0].tagName === "INPUT" ? 1 : 0; + inputGroup[offs].innerText = inputGroup[1 - offs].value; + inputGroup[1 - offs]["oninput"] = function () { + inputGroup[offs].innerText = inputGroup[1 - offs].value; + }; + } + /** We look for elements which have an 'clickable' attribute + * we use these e.g. on an 'inputGroup' () to adjust the value of + * the corresponding range-control + */ + Array.from(el.querySelectorAll(".clickable")).forEach((subel, subi) => { + subel["onclick"] = function (e) { + handleSdpiItemClick(e.target, subi); + }; + }); + el[evt] = function (e) { + handleSdpiItemClick(e.target, i); + }; + } + ); + + baseElement.querySelectorAll("textarea").forEach((e) => { + const maxl = e.getAttribute("maxlength"); + e.targets = baseElement.querySelectorAll(`[for='${e.id}']`); + if (e.targets.length) { + let fn = () => { + for (let x of e.targets) { + x.innerText = maxl + ? `${e.value.length}/${maxl}` + : `${e.value.length}`; + } + }; + fn(); + e.onkeyup = fn; + } + }); +} + +function handleSdpiItemClick(e, idx) { + /** Following items are containers, so we won't handle clicks on them */ + if (["OL", "UL", "TABLE"].includes(e.tagName)) { + return; + } + // console.log('--- handleSdpiItemClick ---', e, `type: ${e.type}`, e.tagName, `inner: ${e.innerText}`); + + /** SPANS are used inside a control as 'labels' + * If a SPAN element calls this function, it has a class of 'clickable' set and is thereby handled as + * clickable label. + */ + + if (e.tagName === "SPAN") { + const inp = e.parentNode.querySelector("input"); + if (e.getAttribute("value")) { + return inp && (inp.value = e.getAttribute("value")); + } + } + + const selectedElements = []; + const isList = ["LI", "OL", "UL", "DL", "TD"].includes(e.tagName); + const sdpiItem = e.closest(".sdpi-item"); + const sdpiItemGroup = e.closest(".sdpi-item-group"); + let sdpiItemChildren = isList + ? sdpiItem.querySelectorAll(e.tagName === "LI" ? "li" : "td") + : sdpiItem.querySelectorAll(".sdpi-item-child > input"); + + if (isList) { + const siv = e.closest(".sdpi-item-value"); + if (!siv.classList.contains("multi-select")) { + for (let x of sdpiItemChildren) x.classList.remove("selected"); + } + if (!siv.classList.contains("no-select")) { + e.classList.toggle("selected"); + } + } + + if (sdpiItemGroup && !sdpiItemChildren.length) { + for (let x of ["input", "meter", "progress"]) { + sdpiItemChildren = sdpiItemGroup.querySelectorAll(x); + if (sdpiItemChildren.length) break; + } + } + + if (e.selectedIndex) { + idx = e.selectedIndex; + } else { + sdpiItemChildren.forEach((ec, i) => { + if (ec.classList.contains("selected")) { + selectedElements.push(ec.innerText); + } + if (ec === e) idx = i; + }); + } + + const returnValue = { + key: e.id || sdpiItem.id, + value: isList + ? e.innerText + : e.value + ? e.type === "file" + ? decodeURIComponent(e.value.replace(/^C:\\fakepath\\/, "")) + : e.value + : e.getAttribute("value"), + group: sdpiItemGroup ? sdpiItemGroup.id : false, + index: idx, + selection: selectedElements, + checked: e.checked, + }; + + /** Just simulate the original file-selector: + * If there's an element of class '.sdpi-file-info' + * show the filename there + */ + if (e.type === "file") { + const info = sdpiItem.querySelector(".sdpi-file-info"); + if (info) { + const s = returnValue.value.split("/").pop(); + info.innerText = + s.length > 28 + ? s.substr(0, 10) + "..." + s.substr(s.length - 10, s.length) + : s; + } + } + + sendValueToPlugin(returnValue, "sdpi_collection"); +} + +function updateKeyForDemoCanvas(cnv) { + sendValueToPlugin( + { + key: "your_canvas", + value: cnv.toDataURL(), + }, + "sdpi_collection" + ); +} + +/** Stream Deck software passes system-highlight color information + * to Property Inspector. Here we 'inject' the CSS styles into the DOM + * when we receive this information. */ + +function addDynamicStyles(clrs, fromWhere) { + const node = + document.getElementById("#sdpi-dynamic-styles") || + document.createElement("style"); + if (!clrs.mouseDownColor) + clrs.mouseDownColor = fadeColor(clrs.highlightColor, -100); + const clr = clrs.highlightColor.slice(0, 7); + const clr1 = fadeColor(clr, 100); + const clr2 = fadeColor(clr, 60); + const metersActiveColor = fadeColor(clr, -60); + + node.setAttribute("id", "sdpi-dynamic-styles"); + node.innerHTML = ` + + input[type="radio"]:checked + label span, + input[type="checkbox"]:checked + label span { + background-color: ${clrs.highlightColor}; + } + + input[type="radio"]:active:checked + label span, + input[type="radio"]:active + label span, + input[type="checkbox"]:active:checked + label span, + input[type="checkbox"]:active + label span { + background-color: ${clrs.mouseDownColor}; + } + + input[type="radio"]:active + label span, + input[type="checkbox"]:active + label span { + background-color: ${clrs.buttonPressedBorderColor}; + } + + td.selected, + td.selected:hover, + li.selected:hover, + li.selected { + color: white; + background-color: ${clrs.highlightColor}; + } + + .sdpi-file-label > label:active, + .sdpi-file-label.file:active, + label.sdpi-file-label:active, + label.sdpi-file-info:active, + input[type="file"]::-webkit-file-upload-button:active, + button:active { + background-color: ${clrs.buttonPressedBackgroundColor}; + color: ${clrs.buttonPressedTextColor}; + border-color: ${clrs.buttonPressedBorderColor}; + } + + ::-webkit-progress-value, + meter::-webkit-meter-optimum-value { + background: linear-gradient(${clr2}, ${clr1} 20%, ${clr} 45%, ${clr} 55%, ${clr2}) + } + + ::-webkit-progress-value:active, + meter::-webkit-meter-optimum-value:active { + background: linear-gradient(${clr}, ${clr2} 20%, ${metersActiveColor} 45%, ${metersActiveColor} 55%, ${clr}) + } + `; + document.body.appendChild(node); +} + +/** UTILITIES */ + +/** Helper function to construct a list of running apps + * from a template string. + * -> information about running apps is received from the plugin + */ + +function sdpiCreateList(el, obj, cb) { + if (el) { + el.style.display = obj.value.length ? "block" : "none"; + Array.from(document.querySelectorAll(`.${el.id}`)).forEach((subel, i) => { + subel.style.display = obj.value.length ? "flex" : "none"; + }); + if (obj.value.length) { + el.innerHTML = `
+
${obj.label || ""}
+
    + ${obj.value.map((e) => `
  • ${e.name}
  • `).join("")} +
+
`; + setTimeout(function () { + prepareDOMElements(el); + if (cb) cb(); + }, 10); + return; + } + } + if (cb) cb(); +} + +/** get a JSON property from a (dot-separated) string + * Works on nested JSON, e.g.: + * jsn = { + * propA: 1, + * propB: 2, + * propC: { + * subA: 3, + * subB: { + * testA: 5, + * testB: 'Hello' + * } + * } + * } + * getPropFromString(jsn,'propC.subB.testB') will return 'Hello'; + */ +const getPropFromString = (jsn, str, sep = ".") => { + const arr = str.split(sep); + return arr.reduce( + (obj, key) => (obj && obj.hasOwnProperty(key) ? obj[key] : undefined), + jsn + ); +}; + +/* + Quick utility to lighten or darken a color (doesn't take color-drifting, etc. into account) + Usage: + fadeColor('#061261', 100); // will lighten the color + fadeColor('#200867'), -100); // will darken the color +*/ +function fadeColor(col, amt) { + const min = Math.min, + max = Math.max; + const num = parseInt(col.replace(/#/g, ""), 16); + const r = min(255, max((num >> 16) + amt, 0)); + const g = min(255, max((num & 0x0000ff) + amt, 0)); + const b = min(255, max(((num >> 8) & 0x00ff) + amt, 0)); + return "#" + (g | (b << 8) | (r << 16)).toString(16).padStart(6, 0); +} diff --git a/com.exension.hwinfo.sdPlugin/launch-hwinfo.png b/com.exension.hwinfo.sdPlugin/launch-hwinfo.png new file mode 100644 index 0000000..617e384 Binary files /dev/null and b/com.exension.hwinfo.sdPlugin/launch-hwinfo.png differ diff --git a/com.exension.hwinfo.sdPlugin/manifest.json b/com.exension.hwinfo.sdPlugin/manifest.json new file mode 100644 index 0000000..5379533 --- /dev/null +++ b/com.exension.hwinfo.sdPlugin/manifest.json @@ -0,0 +1,41 @@ +{ + "SDKVersion": 2, + "Software": { + "MinimumVersion": "4.1" + }, + "Actions": [ + { + "Icon": "icon", + "Name": "HWiNFO", + "States": [ + { + "Image": "defaultImage", + "TitleAlignment": "top", + "FontSize": "9", + "TitleColor": "#b7b7b7", + "ShowTitle": false + } + ], + "SupportedInMultiActions": false, + "Tooltip": "Display sensor readings from HWiNFO", + "UUID": "com.exension.hwinfo.reading" + } + ], + "Author": "shayne", + "CodePathWin": "hwinfo.exe", + "PropertyInspectorPath": "index_pi.html", + "Description": "Display sensor readings from HWiNFO64. This plugin is not affiliated with HWiNFO64, for more information and to download HWiNFO64 visit https://www.hwinfo.com", + "Name": "HWiNFO", + "Icon": "pluginIcon", + "URL": "https://github.com/shayne/hwinfo-streamdeck", + "Version": "2.0.5", + "ApplicationsToMonitor": { + "windows": ["HWiNFO64.EXE", "HWiNFO64.exe"] + }, + "OS": [ + { + "Platform": "windows", + "MinimumVersion": "10" + } + ] +} diff --git a/com.exension.hwinfo.sdPlugin/pluginIcon.png b/com.exension.hwinfo.sdPlugin/pluginIcon.png new file mode 100644 index 0000000..dd47fab Binary files /dev/null and b/com.exension.hwinfo.sdPlugin/pluginIcon.png differ diff --git a/com.exension.hwinfo.sdPlugin/pluginIcon@2x.png b/com.exension.hwinfo.sdPlugin/pluginIcon@2x.png new file mode 100644 index 0000000..9de7c3f Binary files /dev/null and b/com.exension.hwinfo.sdPlugin/pluginIcon@2x.png differ diff --git a/com.exension.hwinfo.streamDeckPlugin b/com.exension.hwinfo.streamDeckPlugin new file mode 100644 index 0000000..e1a7c9b Binary files /dev/null and b/com.exension.hwinfo.streamDeckPlugin differ diff --git a/examples/bench/DejaVuSans-Bold.ttf b/examples/bench/DejaVuSans-Bold.ttf new file mode 100644 index 0000000..6d65fa7 Binary files /dev/null and b/examples/bench/DejaVuSans-Bold.ttf differ diff --git a/examples/bench/main.go b/examples/bench/main.go new file mode 100644 index 0000000..37e4ac2 --- /dev/null +++ b/examples/bench/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "runtime" +) + +func main() { + _, filename, _, _ := runtime.Caller(0) + fmt.Println("Current test filename: " + filename) + os.Chdir(filepath.Dir(filename)) +} diff --git a/examples/bench/main_test.go b/examples/bench/main_test.go new file mode 100644 index 0000000..f9d9069 --- /dev/null +++ b/examples/bench/main_test.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" + "image/color" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/shayne/hwinfo-streamdeck/pkg/graph" +) + +func BenchmarkFoo(b *testing.B) { + _, filename, _, _ := runtime.Caller(0) + fmt.Println("Current test filename: " + filename) + os.Chdir(filepath.Dir(filename)) + + g := graph.NewGraph(72, 72, 0., 100., + &color.RGBA{255, 255, 255, 255}, + &color.RGBA{0, 0, 0, 255}, + &color.RGBA{255, 255, 255, 255}) + g.SetLabel(0, "CPU °C", 15, &color.RGBA{183, 183, 183, 255}) + g.SetLabel(1, "5%", 40, &color.RGBA{255, 255, 255, 255}) + + data := []float64{ + 0., 0., 0., 0., 0., + 10., 10., 10., 10., 10., + 20., 20., 20., 20., 20., + 30., 30., 30., 30., 30., + 40., 40., 40., 40., 40., + 50., 50., 50., 50., 50., + 60., 60., 60., 60., 60., + 70., 70., 70., 70., 70., + 80., 80., 80., 80., 80., + 90., 90., 90., 90., 90., + 100., 100., 100., 100., 100., + // 0., 0., 0., 0., 0., + // 10., 10., 10., 10., 10., + // 20., 20., 20., 20., 20., + // 30., 30., 30., 30., 30., + // 40., 40., 40., 40., 40., + // 50., 50., 50., 50., 50., + // 60., 60., 60., 60., 60., + // 70., 70., 70., 70., 70., + // 80., 80., 80., 80., 80., + // 90., 90., 90., 90., 90., + // 100., 100., 100., 100., 100., + } + _ = data // FIXME + for i := 0; i < b.N; i++ { + // FIXME: updateChart does not exist + // for _, v := range data { + // g.UpdateChart(v) + // } + _, err := g.EncodePNG() + if err != nil { + b.Fatal("failed to encode png") + } + } +} diff --git a/examples/graph/main.go b/examples/graph/main.go new file mode 100644 index 0000000..08831bb --- /dev/null +++ b/examples/graph/main.go @@ -0,0 +1,86 @@ +package main + +import ( + "fmt" + "image/color" + "io/ioutil" + "log" + "math/rand" + "time" + + "github.com/shayne/hwinfo-streamdeck/pkg/graph" +) + +const ( + dev = 40 +) + +func main() { + g := graph.NewGraph(72, 72, 0., 100., + &color.RGBA{255, 255, 255, 255}, + &color.RGBA{0, 0, 0, 255}, + &color.RGBA{255, 255, 255, 255}) + g.SetLabel(0, "CPU °C", 15, &color.RGBA{183, 183, 183, 255}) + g.SetLabel(1, "5%", 40, &color.RGBA{255, 255, 255, 255}) + + data := makeFakeData() + // data := []float64{ + // 0., 0., 0., 0., 0., + // 10., 10., 10., 10., 10., + // 20., 20., 20., 20., 20., + // 30., 30., 30., 30., 30., + // 40., 40., 40., 40., 40., + // 50., 50., 50., 50., 50., + // 60., 60., 60., 60., 60., + // 70., 70., 70., 70., 70., + // 80., 80., 80., 80., 80., + // 90., 90., 90., 90., 90., + // 100., 100., 100., 100., 100., + // } + for _, v := range data { + g.Update(v) + } + lastv := data[len(data)-1] + + ticker := time.NewTicker(time.Second) + for { + select { + case <-ticker.C: + s := rand.NewSource(time.Now().UnixNano()) + r := rand.New(s) + ndev := r.Intn(dev) - (dev / 2) + v := lastv + float64(ndev) + if v > 100 { + v = 100 + } else if v < 0 { + v = 0 + } + fmt.Println(v) + g.Update(v) + lastv = v + bts, err := g.EncodePNG() + if err != nil { + log.Fatal("failed to encode png") + } + err = ioutil.WriteFile("graph.png", bts, 0644) + if err != nil { + log.Fatal("failed to write png") + } + } + } +} + +func makeFakeData() []float64 { + s := rand.NewSource(time.Now().UnixNano()) + r := rand.New(s) + data := make([]float64, 72) + v := r.Intn(100) + lastv := v + data[0] = float64(v) + for i := 1; i < 72; i++ { + ndev := r.Intn(dev) - (dev / 2) + v = lastv + ndev + data[i] = float64(v) + } + return data +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b6d57bb --- /dev/null +++ b/go.mod @@ -0,0 +1,28 @@ +module github.com/shayne/hwinfo-streamdeck + +go 1.19 + +require ( + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 + github.com/golang/protobuf v1.5.2 + github.com/gorilla/websocket v1.5.0 + github.com/hashicorp/go-plugin v1.4.8 + github.com/shayne/go-winpeg v0.0.0-20200807055429-803ae16a07c6 + golang.org/x/image v0.3.0 + golang.org/x/sys v0.4.0 + golang.org/x/text v0.6.0 + google.golang.org/grpc v1.52.0 + google.golang.org/protobuf v1.28.1 +) + +require ( + github.com/fatih/color v1.13.0 // indirect + github.com/hashicorp/go-hclog v1.4.0 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/oklog/run v1.1.0 // indirect + golang.org/x/net v0.5.0 // indirect + google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ac6cf9f --- /dev/null +++ b/go.sum @@ -0,0 +1,91 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-hclog v1.4.0 h1:ctuWFGrhFha8BnnzxqeRGidlEcQkDyL5u8J8t5eA11I= +github.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.4.8 h1:CHGwpxYDOttQOY7HOWgETU9dyVjOXzniXDqJcYJE1zM= +github.com/hashicorp/go-plugin v1.4.8/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/shayne/go-winpeg v0.0.0-20200807055429-803ae16a07c6 h1:HKw6S9JJ7+Z4jc0ygiefl253IwWgk4/ohsQ/5tFWVs0= +github.com/shayne/go-winpeg v0.0.0-20200807055429-803ae16a07c6/go.mod h1:gkGydh7Q4gy2dbfmfQ5++JV6nn8jb1iRj62BWtNPQOg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/image v0.3.0 h1:HTDXbdK9bjfSWkPzDJIw89W8CAtfFGduujWs33NLLsg= +golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200806125547-5acd03effb82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5 h1:wJT65XLOzhpSPCdAmmKfz94SlmnQzDzjm3Cj9k3fsXY= +google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/grpc v1.52.0 h1:kd48UiU7EHsV4rnLyOJRuP/Il/UHE7gdDAQ+SZI7nZk= +google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/images/clicksettings.png b/images/clicksettings.png new file mode 100644 index 0000000..72a6832 Binary files /dev/null and b/images/clicksettings.png differ diff --git a/images/configureaction.gif b/images/configureaction.gif new file mode 100644 index 0000000..68b88bb Binary files /dev/null and b/images/configureaction.gif differ diff --git a/images/contextquit.png b/images/contextquit.png new file mode 100644 index 0000000..201ac92 Binary files /dev/null and b/images/contextquit.png differ diff --git a/images/demo.gif b/images/demo.gif new file mode 100644 index 0000000..42dc386 Binary files /dev/null and b/images/demo.gif differ diff --git a/images/dragaction.gif b/images/dragaction.gif new file mode 100644 index 0000000..6bb833a Binary files /dev/null and b/images/dragaction.gif differ diff --git a/images/recommendedsettings.png b/images/recommendedsettings.png new file mode 100644 index 0000000..4fbf32d Binary files /dev/null and b/images/recommendedsettings.png differ diff --git a/images/sensorsonly.png b/images/sensorsonly.png new file mode 100644 index 0000000..53a1ad2 Binary files /dev/null and b/images/sensorsonly.png differ diff --git a/images/sharedmemory.png b/images/sharedmemory.png new file mode 100644 index 0000000..07a7201 Binary files /dev/null and b/images/sharedmemory.png differ diff --git a/images/streamdeckactionlist.png b/images/streamdeckactionlist.png new file mode 100644 index 0000000..21b7b7d Binary files /dev/null and b/images/streamdeckactionlist.png differ diff --git a/images/streamdeckinstall.png b/images/streamdeckinstall.png new file mode 100644 index 0000000..7fd3d21 Binary files /dev/null and b/images/streamdeckinstall.png differ diff --git a/install-plugin.bat b/install-plugin.bat new file mode 100644 index 0000000..49bc495 --- /dev/null +++ b/install-plugin.bat @@ -0,0 +1,4 @@ +@echo off +CALL .\kill-streamdeck.bat +xcopy com.exension.hwinfo.sdPlugin %APPDATA%\\Elgato\\StreamDeck\\Plugins\\com.exension.hwinfo.sdPlugin\\ /E /Q /Y +CALL .\start-streamdeck.bat \ No newline at end of file diff --git a/internal/app/hwinfostreamdeckplugin/action_manager.go b/internal/app/hwinfostreamdeckplugin/action_manager.go new file mode 100644 index 0000000..95620a0 --- /dev/null +++ b/internal/app/hwinfostreamdeckplugin/action_manager.go @@ -0,0 +1,54 @@ +package hwinfostreamdeckplugin + +import ( + "fmt" + "sync" + "time" +) + +type actionManager struct { + mux sync.RWMutex + actions map[string]*actionData +} + +func newActionManager() *actionManager { + return &actionManager{actions: make(map[string]*actionData)} +} + +func (tm *actionManager) Run(updateTiles func(*actionData)) { + go func() { + ticker := time.NewTicker(time.Second) + for range ticker.C { + tm.mux.RLock() + for _, data := range tm.actions { + if data.settings.IsValid { + updateTiles(data) + } + } + tm.mux.RUnlock() + } + }() +} + +func (tm *actionManager) SetAction(action, context string, settings *actionSettings) { + tm.mux.Lock() + tm.actions[context] = &actionData{action, context, settings} + tm.mux.Unlock() +} + +func (tm *actionManager) RemoveAction(context string) { + tm.mux.Lock() + delete(tm.actions, context) + tm.mux.Unlock() +} + +func (tm *actionManager) getSettings(context string) (actionSettings, error) { + tm.mux.RLock() + data, ok := tm.actions[context] + tm.mux.RUnlock() + if !ok { + return actionSettings{}, fmt.Errorf("getSettings invalid key: %s", context) + } + // return full copy of settings, not reference to stored settings + return *data.settings, nil +} diff --git a/internal/app/hwinfostreamdeckplugin/delegate.go b/internal/app/hwinfostreamdeckplugin/delegate.go new file mode 100644 index 0000000..81f043b --- /dev/null +++ b/internal/app/hwinfostreamdeckplugin/delegate.go @@ -0,0 +1,219 @@ +package hwinfostreamdeckplugin + +import ( + "encoding/json" + "image/color" + "log" + + "github.com/gorilla/websocket" + "github.com/shayne/hwinfo-streamdeck/pkg/graph" + "github.com/shayne/hwinfo-streamdeck/pkg/streamdeck" +) + +const ( + tileWidth = 72 + tileHeight = 72 +) + +// OnConnected event +func (p *Plugin) OnConnected(c *websocket.Conn) { + log.Println("OnConnected") +} + +// OnWillAppear event +func (p *Plugin) OnWillAppear(event *streamdeck.EvWillAppear) { + var settings actionSettings + err := json.Unmarshal(*event.Payload.Settings, &settings) + if err != nil { + log.Println("OnWillAppear settings unmarshal", err) + } + tfSize := 10.5 + vfSize := 10.5 + var fgColor *color.RGBA + var bgColor *color.RGBA + var hlColor *color.RGBA + var tColor *color.RGBA + var vtColor *color.RGBA + if settings.TitleFontSize != 0 { + tfSize = settings.TitleFontSize + } + if settings.ValueFontSize != 0 { + vfSize = settings.ValueFontSize + } + if settings.ForegroundColor == "" { + fgColor = &color.RGBA{0, 81, 40, 255} + } else { + fgColor = hexToRGBA(settings.ForegroundColor) + } + if settings.BackgroundColor == "" { + bgColor = &color.RGBA{0, 0, 0, 255} + } else { + bgColor = hexToRGBA(settings.BackgroundColor) + } + if settings.HighlightColor == "" { + hlColor = &color.RGBA{0, 158, 0, 255} + } else { + hlColor = hexToRGBA(settings.HighlightColor) + } + if settings.TitleColor == "" { + tColor = &color.RGBA{183, 183, 183, 255} + } else { + tColor = hexToRGBA(settings.TitleColor) + } + if settings.ValueTextColor == "" { + vtColor = &color.RGBA{255, 255, 255, 255} + } else { + vtColor = hexToRGBA(settings.ValueTextColor) + } + g := graph.NewGraph(tileWidth, tileHeight, settings.Min, settings.Max, fgColor, bgColor, hlColor) + g.SetLabel(0, "", 19, tColor) + g.SetLabelFontSize(0, tfSize) + g.SetLabel(1, "", 44, vtColor) + g.SetLabelFontSize(1, vfSize) + p.graphs[event.Context] = g + p.am.SetAction(event.Action, event.Context, &settings) +} + +// OnWillDisappear event +func (p *Plugin) OnWillDisappear(event *streamdeck.EvWillDisappear) { + var settings actionSettings + err := json.Unmarshal(*event.Payload.Settings, &settings) + if err != nil { + log.Println("OnWillAppear settings unmarshal", err) + } + delete(p.graphs, event.Context) + p.am.RemoveAction(event.Context) +} + +// OnApplicationDidLaunch event +func (p *Plugin) OnApplicationDidLaunch(event *streamdeck.EvApplication) { + p.appLaunched = true +} + +// OnApplicationDidTerminate event +func (p *Plugin) OnApplicationDidTerminate(event *streamdeck.EvApplication) { + p.appLaunched = false +} + +// OnTitleParametersDidChange event +func (p *Plugin) OnTitleParametersDidChange(event *streamdeck.EvTitleParametersDidChange) { + var settings actionSettings + err := json.Unmarshal(*event.Payload.Settings, &settings) + if err != nil { + log.Println("OnWillAppear settings unmarshal", err) + } + g, ok := p.graphs[event.Context] + if !ok { + log.Printf("handleSetMax no graph for context: %s\n", event.Context) + return + } + g.SetLabelText(0, event.Payload.Title) + if event.Payload.TitleParameters.TitleColor != "" { + tClr := hexToRGBA(event.Payload.TitleParameters.TitleColor) + g.SetLabelColor(0, tClr) + } + + settings.Title = event.Payload.Title + settings.TitleColor = event.Payload.TitleParameters.TitleColor + err = p.sd.SetSettings(event.Context, &settings) + if err != nil { + log.Printf("handleSetTitle SetSettings: %v\n", err) + return + } + p.am.SetAction(event.Action, event.Context, &settings) +} + +// OnPropertyInspectorConnected event +func (p *Plugin) OnPropertyInspectorConnected(event *streamdeck.EvSendToPlugin) { + settings, err := p.am.getSettings(event.Context) + if err != nil { + log.Println("OnPropertyInspectorConnected getSettings", err) + } + sensors, err := p.hw.Sensors() + if err != nil { + log.Println("OnPropertyInspectorConnected Sensors", err) + payload := evStatus{Error: true, Message: "HWiNFO Unavailable"} + err := p.sd.SendToPropertyInspector(event.Action, event.Context, payload) + settings.InErrorState = true + err = p.sd.SetSettings(event.Context, &settings) + if err != nil { + log.Printf("OnPropertyInspectorConnected SetSettings: %v\n", err) + return + } + p.am.SetAction(event.Action, event.Context, &settings) + if err != nil { + log.Println("updateTiles SendToPropertyInspector", err) + } + return + } + evsensors := make([]*evSendSensorsPayloadSensor, 0, len(sensors)) + for _, s := range sensors { + evsensors = append(evsensors, &evSendSensorsPayloadSensor{UID: s.ID(), Name: s.Name()}) + } + payload := evSendSensorsPayload{Sensors: evsensors, Settings: &settings} + err = p.sd.SendToPropertyInspector(event.Action, event.Context, payload) + if err != nil { + log.Println("OnPropertyInspectorConnected SendToPropertyInspector", err) + } +} + +// OnSendToPlugin event +func (p *Plugin) OnSendToPlugin(event *streamdeck.EvSendToPlugin) { + var payload map[string]*json.RawMessage + err := json.Unmarshal(*event.Payload, &payload) + if err != nil { + log.Println("OnSendToPlugin unmarshal", err) + } + if data, ok := payload["sdpi_collection"]; ok { + sdpi := evSdpiCollection{} + err = json.Unmarshal(*data, &sdpi) + if err != nil { + log.Println("SDPI unmarshal", err) + } + switch sdpi.Key { + case "sensorSelect": + err = p.handleSensorSelect(event, &sdpi) + if err != nil { + log.Println("handleSensorSelect", err) + } + case "readingSelect": + err = p.handleReadingSelect(event, &sdpi) + if err != nil { + log.Println("handleReadingSelect", err) + } + case "min": + err := p.handleSetMin(event, &sdpi) + if err != nil { + log.Println("handleSetMin", err) + } + case "max": + err := p.handleSetMax(event, &sdpi) + if err != nil { + log.Println("handleSetMax", err) + } + case "format": + err := p.handleSetFormat(event, &sdpi) + if err != nil { + log.Println("handleSetFormat", err) + } + case "divisor": + err := p.handleDivisor(event, &sdpi) + if err != nil { + log.Println("handleDivisor", err) + } + case "foreground", "background", "highlight", "valuetext": + err := p.handleColorChange(event, sdpi.Key, &sdpi) + if err != nil { + log.Println("handleColorChange", err) + } + case "titleFontSize", "valueFontSize": + err := p.handleSetFontSize(event, sdpi.Key, &sdpi) + if err != nil { + log.Println("handleSetTitleFontSize", err) + } + default: + log.Printf("Unknown sdpi key: %s\n", sdpi.Key) + } + return + } +} diff --git a/internal/app/hwinfostreamdeckplugin/handlers.go b/internal/app/hwinfostreamdeckplugin/handlers.go new file mode 100644 index 0000000..9cab6bf --- /dev/null +++ b/internal/app/hwinfostreamdeckplugin/handlers.go @@ -0,0 +1,272 @@ +package hwinfostreamdeckplugin + +import ( + "fmt" + "image/color" + "strconv" + + hwsensorsservice "github.com/shayne/hwinfo-streamdeck/pkg/service" + "github.com/shayne/hwinfo-streamdeck/pkg/streamdeck" +) + +func (p *Plugin) handleSensorSelect(event *streamdeck.EvSendToPlugin, sdpi *evSdpiCollection) error { + sensorid := sdpi.Value + readings, err := p.hw.ReadingsForSensorID(sensorid) + if err != nil { + return fmt.Errorf("handleSensorSelect ReadingsBySensor failed: %v", err) + } + evreadings := []*evSendReadingsPayloadReading{} + for _, r := range readings { + evreadings = append(evreadings, &evSendReadingsPayloadReading{ID: r.ID(), Label: r.Label(), Prefix: r.Unit()}) + } + settings, err := p.am.getSettings(event.Context) + if err != nil { + return fmt.Errorf("handleReadingSelect getSettings: %v", err) + } + // only update settings if SensorUID is changing + // this covers case where PI sends event when tile + // selected in SD UI + if settings.SensorUID != sensorid { + settings.SensorUID = sensorid + settings.ReadingID = 0 + settings.IsValid = false + } + payload := evSendReadingsPayload{Readings: evreadings, Settings: &settings} + err = p.sd.SendToPropertyInspector(event.Action, event.Context, payload) + if err != nil { + return fmt.Errorf("sensorsSelect SendToPropertyInspector: %v", err) + } + err = p.sd.SetSettings(event.Context, &settings) + if err != nil { + return fmt.Errorf("handleSensorSelect SetSettings: %v", err) + } + p.am.SetAction(event.Action, event.Context, &settings) + return nil +} + +func getDefaultMinMaxForReading(r hwsensorsservice.Reading) (int, int) { + switch r.Unit() { + case "%": + return 0, 100 + case "Yes/No": + return 0, 1 + } + min := r.ValueMin() + max := r.ValueMax() + min -= min * .2 + if min <= 0 { + min = 0. + } + max += max * .2 + return int(min), int(max) +} + +func (p *Plugin) handleReadingSelect(event *streamdeck.EvSendToPlugin, sdpi *evSdpiCollection) error { + rid64, err := strconv.ParseInt(sdpi.Value, 10, 32) + if err != nil { + return fmt.Errorf("handleReadingSelect Atoi failed: %s, %v", sdpi.Value, err) + } + rid := int32(rid64) + settings, err := p.am.getSettings(event.Context) + if err != nil { + return fmt.Errorf("handleReadingSelect getSettings: %v", err) + } + + // no action if reading didn't change + if settings.ReadingID == rid { + return nil + } + + settings.ReadingID = rid + + // set default min/max + r, err := p.getReading(settings.SensorUID, settings.ReadingID) + if err != nil { + return fmt.Errorf("handleReadingSelect getReading: %v", err) + } + + g, ok := p.graphs[event.Context] + if !ok { + return fmt.Errorf("handleReadingSelect no graph for context: %s", event.Context) + } + defaultMin, defaultMax := getDefaultMinMaxForReading(r) + settings.Min = defaultMin + g.SetMin(settings.Min) + settings.Max = defaultMax + g.SetMax(settings.Max) + settings.IsValid = true // set IsValid once we choose reading + + err = p.sd.SetSettings(event.Context, &settings) + if err != nil { + return fmt.Errorf("handleReadingSelect SetSettings: %v", err) + } + p.am.SetAction(event.Action, event.Context, &settings) + return nil +} + +func (p *Plugin) handleSetMin(event *streamdeck.EvSendToPlugin, sdpi *evSdpiCollection) error { + min, err := strconv.Atoi(sdpi.Value) + if err != nil { + return fmt.Errorf("handleSetMin strconv: %v", err) + } + g, ok := p.graphs[event.Context] + if !ok { + return fmt.Errorf("handleSetMax no graph for context: %s", event.Context) + } + g.SetMin(min) + settings, err := p.am.getSettings(event.Context) + if err != nil { + return fmt.Errorf("handleSetMin getSettings: %v", err) + } + settings.Min = min + err = p.sd.SetSettings(event.Context, &settings) + if err != nil { + return fmt.Errorf("handleSetMin SetSettings: %v", err) + } + p.am.SetAction(event.Action, event.Context, &settings) + return nil +} + +func (p *Plugin) handleSetMax(event *streamdeck.EvSendToPlugin, sdpi *evSdpiCollection) error { + max, err := strconv.Atoi(sdpi.Value) + if err != nil { + return fmt.Errorf("handleSetMax strconv: %v", err) + } + g, ok := p.graphs[event.Context] + if !ok { + return fmt.Errorf("handleSetMax no graph for context: %s", event.Context) + } + g.SetMax(max) + settings, err := p.am.getSettings(event.Context) + if err != nil { + return fmt.Errorf("handleSetMax getSettings: %v", err) + } + settings.Max = max + err = p.sd.SetSettings(event.Context, &settings) + if err != nil { + return fmt.Errorf("handleSetMax SetSettings: %v", err) + } + p.am.SetAction(event.Action, event.Context, &settings) + return nil +} + +func (p *Plugin) handleSetFormat(event *streamdeck.EvSendToPlugin, sdpi *evSdpiCollection) error { + format := sdpi.Value + settings, err := p.am.getSettings(event.Context) + if err != nil { + return fmt.Errorf("handleSetFormat getSettings: %v", err) + } + settings.Format = format + err = p.sd.SetSettings(event.Context, &settings) + if err != nil { + return fmt.Errorf("handleSetFormat SetSettings: %v", err) + } + p.am.SetAction(event.Action, event.Context, &settings) + return nil +} + +func (p *Plugin) handleDivisor(event *streamdeck.EvSendToPlugin, sdpi *evSdpiCollection) error { + divisor := sdpi.Value + settings, err := p.am.getSettings(event.Context) + if err != nil { + return fmt.Errorf("handleDivisor getSettings: %v", err) + } + settings.Divisor = divisor + err = p.sd.SetSettings(event.Context, &settings) + if err != nil { + return fmt.Errorf("handleDivisor SetSettings: %v", err) + } + p.am.SetAction(event.Action, event.Context, &settings) + return nil +} + +const ( + hexFormat = "#%02x%02x%02x" + hexShortFormat = "#%1x%1x%1x" + hexToRGBFactor = 17 +) + +func hexToRGBA(hex string) *color.RGBA { + var r, g, b uint8 + + if len(hex) == 4 { + fmt.Sscanf(hex, hexShortFormat, &r, &g, &b) + r *= hexToRGBFactor + g *= hexToRGBFactor + b *= hexToRGBFactor + } else { + fmt.Sscanf(hex, hexFormat, &r, &g, &b) + } + + return &color.RGBA{R: r, G: g, B: b, A: 255} +} + +func (p *Plugin) handleColorChange(event *streamdeck.EvSendToPlugin, key string, sdpi *evSdpiCollection) error { + hex := sdpi.Value + settings, err := p.am.getSettings(event.Context) + if err != nil { + return fmt.Errorf("handleDivisor getSettings: %v", err) + } + g, ok := p.graphs[event.Context] + if !ok { + return fmt.Errorf("handleSetMax no graph for context: %s", event.Context) + } + clr := hexToRGBA(hex) + switch key { + case "foreground": + settings.ForegroundColor = hex + g.SetForegroundColor(clr) + case "background": + settings.BackgroundColor = hex + g.SetBackgroundColor(clr) + case "highlight": + settings.HighlightColor = hex + g.SetHighlightColor(clr) + case "valuetext": + settings.ValueTextColor = hex + g.SetLabelColor(1, clr) + } + err = p.sd.SetSettings(event.Context, &settings) + if err != nil { + return fmt.Errorf("handleColorChange SetSettings: %v", err) + } + p.am.SetAction(event.Action, event.Context, &settings) + return nil +} + +func (p *Plugin) handleSetFontSize(event *streamdeck.EvSendToPlugin, key string, sdpi *evSdpiCollection) error { + sv := sdpi.Value + size, err := strconv.ParseFloat(sv, 64) + if err != nil { + return fmt.Errorf("failed to convert value to float: %w", err) + } + + settings, err := p.am.getSettings(event.Context) + if err != nil { + return fmt.Errorf("getSettings failed: %w", err) + } + + g, ok := p.graphs[event.Context] + if !ok { + return fmt.Errorf("no graph for context: %s", event.Context) + } + + switch key { + case "titleFontSize": + settings.TitleFontSize = size + g.SetLabelFontSize(0, size) + case "valueFontSize": + settings.ValueFontSize = size + g.SetLabelFontSize(1, size) + default: + return fmt.Errorf("invalid key: %s", sdpi.Key) + } + + err = p.sd.SetSettings(event.Context, &settings) + if err != nil { + return fmt.Errorf("SetSettings failed: %w", err) + } + + p.am.SetAction(event.Action, event.Context, &settings) + return nil +} diff --git a/internal/app/hwinfostreamdeckplugin/plugin.go b/internal/app/hwinfostreamdeckplugin/plugin.go new file mode 100644 index 0000000..f379ddf --- /dev/null +++ b/internal/app/hwinfostreamdeckplugin/plugin.go @@ -0,0 +1,231 @@ +package hwinfostreamdeckplugin + +import ( + "fmt" + "io/ioutil" + "log" + "os/exec" + "strconv" + "time" + + "github.com/hashicorp/go-plugin" + "github.com/shayne/go-winpeg" + "github.com/shayne/hwinfo-streamdeck/pkg/graph" + hwsensorsservice "github.com/shayne/hwinfo-streamdeck/pkg/service" + "github.com/shayne/hwinfo-streamdeck/pkg/streamdeck" +) + +// Plugin handles information between HWiNFO and Stream Deck +type Plugin struct { + c *plugin.Client + peg winpeg.ProcessExitGroup + hw hwsensorsservice.HardwareService + sd *streamdeck.StreamDeck + am *actionManager + graphs map[string]*graph.Graph + + appLaunched bool +} + +func (p *Plugin) startClient() error { + cmd := exec.Command("./hwinfo-plugin.exe") + + // We're a host. Start by launching the plugin process. + client := plugin.NewClient(&plugin.ClientConfig{ + HandshakeConfig: hwsensorsservice.Handshake, + Plugins: hwsensorsservice.PluginMap, + Cmd: cmd, + AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, + AutoMTLS: true, + }) + + // Connect via RPC + rpcClient, err := client.Client() + if err != nil { + return err + } + + g, err := winpeg.NewProcessExitGroup() + if err != nil { + return err + } + + if err := g.AddProcess(cmd.Process); err != nil { + return err + } + + // Request the plugin + raw, err := rpcClient.Dispense("hwinfoplugin") + if err != nil { + return err + } + + p.c = client + p.peg = g + p.hw = raw.(hwsensorsservice.HardwareService) + + return nil +} + +// NewPlugin creates an instance and initializes the plugin +func NewPlugin(port, uuid, event, info string) (*Plugin, error) { + // We don't want to see the plugin logs. + // log.SetOutput(ioutil.Discard) + p := &Plugin{ + am: newActionManager(), + graphs: make(map[string]*graph.Graph), + } + p.startClient() + p.sd = streamdeck.NewStreamDeck(port, uuid, event, info) + return p, nil +} + +// RunForever starts the plugin and waits for events, indefinitely +func (p *Plugin) RunForever() error { + defer func() { + p.c.Kill() + p.peg.Dispose() + }() + + p.sd.SetDelegate(p) + p.am.Run(p.updateTiles) + + go func() { + for { + if p.c.Exited() { + p.startClient() + } + time.Sleep(1 * time.Second) + } + }() + + err := p.sd.Connect() + if err != nil { + return fmt.Errorf("StreamDeck Connect: %v", err) + } + defer p.sd.Close() + p.sd.ListenAndWait() + return nil +} + +func (p *Plugin) getReading(suid string, rid int32) (hwsensorsservice.Reading, error) { + rbs, err := p.hw.ReadingsForSensorID(suid) + if err != nil { + return nil, fmt.Errorf("getReading ReadingsBySensor failed: %v", err) + } + for _, r := range rbs { + if r.ID() == rid { + return r, nil + } + } + return nil, fmt.Errorf("ReadingID does not exist: %s", suid) +} + +func (p *Plugin) applyDefaultFormat(v float64, t hwsensorsservice.ReadingType, u string) string { + switch t { + case hwsensorsservice.ReadingTypeNone: + return fmt.Sprintf("%0.f %s", v, u) + case hwsensorsservice.ReadingTypeTemp: + return fmt.Sprintf("%.0f %s", v, u) + case hwsensorsservice.ReadingTypeVolt: + return fmt.Sprintf("%.0f %s", v, u) + case hwsensorsservice.ReadingTypeFan: + return fmt.Sprintf("%.0f %s", v, u) + case hwsensorsservice.ReadingTypeCurrent: + return fmt.Sprintf("%.0f %s", v, u) + case hwsensorsservice.ReadingTypePower: + return fmt.Sprintf("%0.f %s", v, u) + case hwsensorsservice.ReadingTypeClock: + return fmt.Sprintf("%.0f %s", v, u) + case hwsensorsservice.ReadingTypeUsage: + return fmt.Sprintf("%.0f%s", v, u) + case hwsensorsservice.ReadingTypeOther: + return fmt.Sprintf("%.0f %s", v, u) + } + return "Bad Format" +} + +func (p *Plugin) updateTiles(data *actionData) { + if data.action != "com.exension.hwinfor0.reading" { + log.Printf("Unknown action updateTiles: %s\n", data.action) + return + } + + g, ok := p.graphs[data.context] + if !ok { + log.Printf("Graph not found for context: %s\n", data.context) + return + } + + if !p.appLaunched { + if !data.settings.InErrorState { + payload := evStatus{Error: true, Message: "HWiNFO Unavailable"} + err := p.sd.SendToPropertyInspector("com.exension.hwinfor0.reading", data.context, payload) + if err != nil { + log.Println("updateTiles SendToPropertyInspector", err) + } + data.settings.InErrorState = true + p.sd.SetSettings(data.context, &data.settings) + } + bts, err := ioutil.ReadFile("./launch-hwinfo.png") + if err != nil { + log.Printf("Failed to read launch-hwinfo.png: %v\n", err) + return + } + err = p.sd.SetImage(data.context, bts) + if err != nil { + log.Printf("Failed to setImage: %v\n", err) + return + } + return + } + + // show ui on property inspector if in error state + if data.settings.InErrorState { + payload := evStatus{Error: false, Message: "show_ui"} + err := p.sd.SendToPropertyInspector("com.exension.hwinfor0.reading", data.context, payload) + if err != nil { + log.Println("updateTiles SendToPropertyInspector", err) + } + data.settings.InErrorState = false + p.sd.SetSettings(data.context, &data.settings) + } + + s := data.settings + r, err := p.getReading(s.SensorUID, s.ReadingID) + if err != nil { + log.Printf("getReading failed: %v\n", err) + return + } + + v := r.Value() + if s.Divisor != "" { + fdiv := 1. + fdiv, err := strconv.ParseFloat(s.Divisor, 64) + if err != nil { + log.Printf("Failed to parse float: %s\n", s.Divisor) + return + } + v = r.Value() / fdiv + } + g.Update(v) + var text string + if f := s.Format; f != "" { + text = fmt.Sprintf(f, v) + } else { + text = p.applyDefaultFormat(v, hwsensorsservice.ReadingType(r.TypeI()), r.Unit()) + } + g.SetLabelText(1, text) + + b, err := g.EncodePNG() + if err != nil { + log.Printf("Failed to encode graph: %v\n", err) + return + } + + err = p.sd.SetImage(data.context, b) + if err != nil { + log.Printf("Failed to setImage: %v\n", err) + return + } +} diff --git a/internal/app/hwinfostreamdeckplugin/types.go b/internal/app/hwinfostreamdeckplugin/types.go new file mode 100644 index 0000000..068746a --- /dev/null +++ b/internal/app/hwinfostreamdeckplugin/types.go @@ -0,0 +1,60 @@ +package hwinfostreamdeckplugin + +type actionSettings struct { + SensorUID string `json:"sensorUid"` + ReadingID int32 `json:"readingId,string"` + Title string `json:"title"` + TitleFontSize float64 `json:"titleFontSize"` + ValueFontSize float64 `json:"valueFontSize"` + Min int `json:"min"` + Max int `json:"max"` + Format string `json:"format"` + Divisor string `json:"divisor"` + IsValid bool `json:"isValid"` + TitleColor string `json:"titleColor"` + ForegroundColor string `json:"foregroundColor"` + BackgroundColor string `json:"backgroundColor"` + HighlightColor string `json:"highlightColor"` + ValueTextColor string `json:"valueTextColor"` + InErrorState bool `json:"inErrorState"` +} + +type actionData struct { + action string + context string + settings *actionSettings +} + +type evStatus struct { + Error bool `json:"error"` + Message string `json:"message"` +} + +type evSendSensorsPayloadSensor struct { + UID string `json:"uid"` + Name string `json:"name"` +} + +type evSendSensorsPayload struct { + Sensors []*evSendSensorsPayloadSensor `json:"sensors"` + Settings *actionSettings `json:"settings"` +} + +type evSendReadingsPayloadReading struct { + ID int32 `json:"id,string"` + Label string `json:"label"` + Prefix string `json:"prefix"` +} + +type evSendReadingsPayload struct { + Readings []*evSendReadingsPayloadReading `json:"readings"` + Settings *actionSettings `json:"settings"` +} + +type evSdpiCollection struct { + Group bool `json:"group"` + Index int `json:"index"` + Key string `json:"key"` + Selection []string `json:"selection"` + Value string `json:"value"` +} diff --git a/internal/hwinfo/hwinfo.go b/internal/hwinfo/hwinfo.go new file mode 100644 index 0000000..0295eba --- /dev/null +++ b/internal/hwinfo/hwinfo.go @@ -0,0 +1,163 @@ +package hwinfo + +/* +#include +#include "hwisenssm2.h" +*/ +import "C" + +import ( + "fmt" + "log" + "time" + "unsafe" + + "github.com/shayne/hwinfo-streamdeck/internal/hwinfo/shmem" + "github.com/shayne/hwinfo-streamdeck/internal/hwinfo/util" +) + +// SharedMemory provides access to the HWiNFO shared memory +type SharedMemory struct { + data []byte + shmem C.PHWiNFO_SENSORS_SHARED_MEM2 +} + +// ReadSharedMem reads data from HWiNFO shared memory +// creating a copy of the data +func ReadSharedMem() (*SharedMemory, error) { + data, err := shmem.ReadBytes() + if err != nil { + return nil, err + } + + return &SharedMemory{ + data: append([]byte(nil), data...), + shmem: C.PHWiNFO_SENSORS_SHARED_MEM2(unsafe.Pointer(&data[0])), + }, nil +} + +// Result for streamed shared memory updates +type Result struct { + Shmem *SharedMemory + Err error +} + +func readAndSend(ch chan<- Result) { + shmem, err := ReadSharedMem() + ch <- Result{Shmem: shmem, Err: err} +} + +// StreamSharedMem delivers shared memory hardware sensors updates +// over a channel +func StreamSharedMem() <-chan Result { + ch := make(chan Result) + go func() { + readAndSend(ch) + // TODO: don't use time.Tick, cancellable? + for range time.Tick(1 * time.Second) { + readAndSend(ch) + } + }() + return ch +} + +// Signature "HWiS" if active, 'DEAD' when inactive +func (s *SharedMemory) Signature() string { + return util.DecodeCharPtr(unsafe.Pointer(&s.shmem.dwSignature), C.sizeof_DWORD) +} + +// Version v1 is latest +func (s *SharedMemory) Version() int { + return int(s.shmem.dwVersion) +} + +// Revision revision of version +func (s *SharedMemory) Revision() int { + return int(s.shmem.dwRevision) +} + +// PollTime last polling time +func (s *SharedMemory) PollTime() uint64 { + addr := unsafe.Pointer(uintptr(unsafe.Pointer(&s.shmem.dwRevision)) + C.sizeof_DWORD) + return uint64(*(*C.__time64_t)(addr)) +} + +// OffsetOfSensorSection offset of the Sensor section from beginning of HWiNFO_SENSORS_SHARED_MEM2 +func (s *SharedMemory) OffsetOfSensorSection() int { + return int(s.shmem.dwOffsetOfSensorSection) +} + +// SizeOfSensorElement size of each sensor element = sizeof( HWiNFO_SENSORS_SENSOR_ELEMENT ) +func (s *SharedMemory) SizeOfSensorElement() int { + return int(s.shmem.dwSizeOfSensorElement) +} + +// NumSensorElements number of sensor elements +func (s *SharedMemory) NumSensorElements() int { + return int(s.shmem.dwNumSensorElements) +} + +// OffsetOfReadingSection offset of the Reading section from beginning of HWiNFO_SENSORS_SHARED_MEM2 +func (s *SharedMemory) OffsetOfReadingSection() int { + return int(s.shmem.dwOffsetOfReadingSection) +} + +// SizeOfReadingElement size of each Reading element = sizeof( HWiNFO_SENSORS_READING_ELEMENT ) +func (s *SharedMemory) SizeOfReadingElement() int { + return int(s.shmem.dwSizeOfReadingElement) +} + +// NumReadingElements number of Reading elements +func (s *SharedMemory) NumReadingElements() int { + return int(s.shmem.dwNumReadingElements) +} + +func (s *SharedMemory) dataForSensor(pos int) ([]byte, error) { + if pos >= s.NumSensorElements() { + return nil, fmt.Errorf("dataForSensor pos out of range, %d for size %d", pos, s.NumSensorElements()) + } + start := s.OffsetOfSensorSection() + (pos * s.SizeOfSensorElement()) + end := start + s.SizeOfSensorElement() + return s.data[start:end], nil +} + +// IterSensors iterate over each sensor +func (s *SharedMemory) IterSensors() <-chan Sensor { + ch := make(chan Sensor) + go func() { + for i := 0; i < s.NumSensorElements(); i++ { + data, err := s.dataForSensor(i) + if err != nil { + log.Fatalf("TODO: failed to read dataForSensor: %v", err) + } + ch <- NewSensor(data) + } + close(ch) + }() + return ch +} + +func (s *SharedMemory) dataForReading(pos int) ([]byte, error) { + if pos >= s.NumReadingElements() { + return nil, fmt.Errorf("dataForReading pos out of range, %d for size %d", pos, s.NumSensorElements()) + } + start := s.OffsetOfReadingSection() + (pos * s.SizeOfReadingElement()) + end := start + s.SizeOfReadingElement() + return s.data[start:end], nil +} + +// IterReadings iterate over each sensor +func (s *SharedMemory) IterReadings() <-chan Reading { + ch := make(chan Reading) + go func() { + for i := 0; i < s.NumReadingElements(); i++ { + data, err := s.dataForReading(i) + if err != nil { + log.Fatalf("TODO: failed to read dataForReading: %v", err) + } + ch <- NewReading(data) + } + close(ch) + }() + return ch +} diff --git a/internal/hwinfo/hwisenssm2.h b/internal/hwinfo/hwisenssm2.h new file mode 100644 index 0000000..3c74fe1 --- /dev/null +++ b/internal/hwinfo/hwisenssm2.h @@ -0,0 +1,126 @@ +#ifndef _HWISENSSM2_H_INCLUDED_ +#define _HWISENSSM2_H_INCLUDED_ + +// Name of the file mapping object that needs to be opened using OpenFileMapping Function: +#define HWiNFO_SENSORS_MAP_FILE_NAME2 "Global\\HWiNFO_SENS_SM2" + +// Name of the global mutex which is acquired when accessing the Shared Memory space. Release as quick as possible ! +#define HWiNFO_SENSORS_SM2_MUTEX "Global\\HWiNFO_SM2_MUTEX" + +#define HWiNFO_SENSORS_STRING_LEN2 128 +#define HWiNFO_UNIT_STRING_LEN 16 + +enum SENSOR_READING_TYPE +{ + SENSOR_TYPE_NONE = 0, + SENSOR_TYPE_TEMP, + SENSOR_TYPE_VOLT, + SENSOR_TYPE_FAN, + SENSOR_TYPE_CURRENT, + SENSOR_TYPE_POWER, + SENSOR_TYPE_CLOCK, + SENSOR_TYPE_USAGE, + SENSOR_TYPE_OTHER +}; +typedef enum SENSOR_READING_TYPE SENSOR_READING_TYPE; + +// No alignment of structure members +#pragma pack(1) + +typedef struct _HWiNFO_SENSORS_READING_ELEMENT +{ + + SENSOR_READING_TYPE tReading; // Type of sensor reading + DWORD dwSensorIndex; // This is the index of sensor in the Sensors[] array to which this reading belongs to + DWORD dwReadingID; // A unique ID of the reading within a particular sensor + char szLabelOrig[HWiNFO_SENSORS_STRING_LEN2]; // Original label (e.g. "Chassis2 Fan") + char szLabelUser[HWiNFO_SENSORS_STRING_LEN2]; // Label displayed, which might have been renamed by user + char szUnit[HWiNFO_UNIT_STRING_LEN]; // e.g. "RPM" + double Value; + double ValueMin; + double ValueMax; + double ValueAvg; + +} HWiNFO_SENSORS_READING_ELEMENT, *PHWiNFO_SENSORS_READING_ELEMENT; + +typedef struct _HWiNFO_SENSORS_SENSOR_ELEMENT +{ + + DWORD dwSensorID; // A unique Sensor ID + DWORD dwSensorInst; // The instance of the sensor (together with dwSensorID forms a unique ID) + char szSensorNameOrig[HWiNFO_SENSORS_STRING_LEN2]; // Original sensor name + char szSensorNameUser[HWiNFO_SENSORS_STRING_LEN2]; // Sensor name displayed, which might have been renamed by user + +} HWiNFO_SENSORS_SENSOR_ELEMENT, *PHWiNFO_SENSORS_SENSOR_ELEMENT; + +typedef struct _HWiNFO_SENSORS_SHARED_MEM2 +{ + + DWORD dwSignature; // "HWiS" if active, 'DEAD' when inactive + DWORD dwVersion; // v1 is latest + DWORD dwRevision; // + __time64_t poll_time; // last polling time + + // descriptors for the Sensors section + DWORD dwOffsetOfSensorSection; // Offset of the Sensor section from beginning of HWiNFO_SENSORS_SHARED_MEM2 + DWORD dwSizeOfSensorElement; // Size of each sensor element = sizeof( HWiNFO_SENSORS_SENSOR_ELEMENT ) + DWORD dwNumSensorElements; // Number of sensor elements + + // descriptors for the Readings section + DWORD dwOffsetOfReadingSection; // Offset of the Reading section from beginning of HWiNFO_SENSORS_SHARED_MEM2 + DWORD dwSizeOfReadingElement; // Size of each Reading element = sizeof( HWiNFO_SENSORS_READING_ELEMENT ) + DWORD dwNumReadingElements; // Number of Reading elements + +} HWiNFO_SENSORS_SHARED_MEM2, *PHWiNFO_SENSORS_SHARED_MEM2; + +#pragma pack() + +#endif + +// *************************************************************************************************************** +// HWiNFO Shared Memory Footprint +// *************************************************************************************************************** +// +// |-----------------------------|-----------------------------------|-----------------------------------| +// Content | HWiNFO_SENSORS_SHARED_MEM2 | HWiNFO_SENSORS_SENSOR_ELEMENT[] | HWiNFO_SENSORS_READING_ELEMENT[] | +// |-----------------------------|-----------------------------------|-----------------------------------| +// Pointer |<--0 |<--dwOffsetOfSensorSection |<--dwOffsetOfReadingSection | +// |-----------------------------|-----------------------------------|-----------------------------------| +// Size | dwOffsetOfSensorSection | dwSizeOfSensorElement | dwSizeOfReadingElement | +// | | * dwNumSensorElement | * dwNumReadingElement | +// |-----------------------------|-----------------------------------|-----------------------------------| +// +// *************************************************************************************************************** +// Code Example +// *************************************************************************************************************** +/* + +HANDLE hHWiNFOMemory = OpenFileMapping( FILE_MAP_READ, FALSE, HWiNFO_SENSORS_MAP_FILE_NAME2 ); +if (hHWiNFOMemory) + PHWiNFO_SENSORS_SHARED_MEM2 pHWiNFOMemory = + (PHWiNFO_SENSORS_SHARED_MEM2) MapViewOfFile( hHWiNFOMemory, FILE_MAP_READ, 0, 0, 0 ); + + // TODO: process signature, version, revision and poll time + + // loop through all available sensors + for (DWORD dwSensor = 0; dwSensor < pHWiNFOMemory->dwNumSensorElements; dwSensor++) + { + PHWiNFO_SENSORS_SENSOR_ELEMENT sensor = (PHWiNFO_SENSORS_SENSOR_ELEMENT) ((BYTE*)pHWiNFOMemory + + pHWiNFOMemory->dwOffsetOfSensorSection + + (pHWiNFOMemory->dwSizeOfSensorElement * dwSensor)); + + // TODO: process sensor + } + + // loop through all available readings + for (DWORD dwReading = 0; dwReading < pHWiNFOMemory->dwNumReadingElements; dwReading++) + { + PHWiNFO_SENSORS_READING_ELEMENT reading = (PHWiNFO_SENSORS_READING_ELEMENT) ((BYTE*)pHWiNFOMemory + + pHWiNFOMemory->dwOffsetOfReadingSection + + (pHWiNFOMemory->dwSizeOfReadingElement * dwReading)); + + // TODO: process reading + } +} + +*/ \ No newline at end of file diff --git a/internal/hwinfo/mutex/mutex.go b/internal/hwinfo/mutex/mutex.go new file mode 100644 index 0000000..1ba3ca8 --- /dev/null +++ b/internal/hwinfo/mutex/mutex.go @@ -0,0 +1,38 @@ +package mutex + +/* +#include +#include "../hwisenssm2.h" +*/ +import "C" +import ( + "fmt" + "sync" + "unsafe" + + "github.com/shayne/hwinfo-streamdeck/internal/hwinfo/util" +) + +var ghnd C.HANDLE +var imut = sync.Mutex{} + +// Lock the global mutex +func Lock() error { + imut.Lock() + lpName := C.CString(C.HWiNFO_SENSORS_SM2_MUTEX) + defer C.free(unsafe.Pointer(lpName)) + + ghnd = C.OpenMutex(C.READ_CONTROL, C.FALSE, lpName) + if ghnd == C.HANDLE(C.NULL) { + errstr := util.HandleLastError(uint64(C.GetLastError())) + return fmt.Errorf("failed to lock global mutex: %w", errstr) + } + + return nil +} + +// Unlock the global mutex +func Unlock() { + defer imut.Unlock() + C.CloseHandle(ghnd) +} diff --git a/internal/hwinfo/plugin/plugin.go b/internal/hwinfo/plugin/plugin.go new file mode 100644 index 0000000..f1e00e4 --- /dev/null +++ b/internal/hwinfo/plugin/plugin.go @@ -0,0 +1,70 @@ +package plugin + +import ( + "github.com/shayne/hwinfo-streamdeck/internal/hwinfo" + hwsensorsservice "github.com/shayne/hwinfo-streamdeck/pkg/service" +) + +// Plugin implementation +type Plugin struct { + Service *Service +} + +// PollTime implementation for plugin +func (p *Plugin) PollTime() (uint64, error) { + shmem, err := p.Service.Shmem() + if err != nil { + return 0, err + } + return shmem.PollTime(), nil +} + +// Sensors implementation for plugin +func (p *Plugin) Sensors() ([]hwsensorsservice.Sensor, error) { + shmem, err := p.Service.Shmem() + if err != nil { + return nil, err + } + var sensors []hwsensorsservice.Sensor + for s := range shmem.IterSensors() { + sensors = append(sensors, &sensor{s}) + } + return sensors, nil +} + +// ReadingsForSensorID implementation for plugin +func (p *Plugin) ReadingsForSensorID(id string) ([]hwsensorsservice.Reading, error) { + res, err := p.Service.ReadingsBySensorID(id) + if err != nil { + return nil, err + } + var readings []hwsensorsservice.Reading + for _, r := range res { + readings = append(readings, &reading{r}) + } + return readings, nil +} + +type sensor struct { + hwinfo.Sensor +} + +func (s sensor) Name() string { + return s.NameOrig() +} + +type reading struct { + hwinfo.Reading +} + +func (r reading) Label() string { + return r.LabelOrig() +} + +func (r reading) Type() string { + return r.Reading.Type().String() +} + +func (r reading) TypeI() int32 { + return int32(r.Reading.Type()) +} diff --git a/internal/hwinfo/plugin/service.go b/internal/hwinfo/plugin/service.go new file mode 100644 index 0000000..3516503 --- /dev/null +++ b/internal/hwinfo/plugin/service.go @@ -0,0 +1,128 @@ +package plugin + +import ( + "fmt" + "sync" + + "github.com/shayne/hwinfo-streamdeck/internal/hwinfo" +) + +// Service wraps hwinfo shared mem streaming +// and provides convenient methods for data access +type Service struct { + streamch <-chan hwinfo.Result + mu sync.RWMutex + sensorIDByIdx []string + readingsBySensorID map[string][]hwinfo.Reading + shmem *hwinfo.SharedMemory + readingsBuilt bool +} + +// Start starts the service providing updating hardware info +func StartService() *Service { + return &Service{ + streamch: hwinfo.StreamSharedMem(), + } +} + +func (s *Service) recvShmem(shmem *hwinfo.SharedMemory) error { + if shmem == nil { + return fmt.Errorf("shmem nil") + } + s.mu.Lock() + defer s.mu.Unlock() + + s.shmem = shmem + + s.sensorIDByIdx = s.sensorIDByIdx[:0] + for k, v := range s.readingsBySensorID { + s.readingsBySensorID[k] = v[:0] + } + s.readingsBuilt = false + + return nil +} + +// Recv receives new hardware sensor updates +func (s *Service) Recv() error { + select { + case r := <-s.streamch: + if r.Err != nil { + return r.Err + } + return s.recvShmem(r.Shmem) + } +} + +// Shmem provides access to underlying hwinfo shared memory +func (s *Service) Shmem() (*hwinfo.SharedMemory, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + if s.shmem != nil { + return s.shmem, nil + } + return nil, fmt.Errorf("shmem nil") +} + +// SensorIDByIdx returns ordered slice of sensor IDs +func (s *Service) SensorIDByIdx() ([]string, error) { + s.mu.RLock() + if len(s.sensorIDByIdx) > 0 { + defer s.mu.RUnlock() + return s.sensorIDByIdx, nil + } + s.mu.RUnlock() + + s.mu.Lock() + defer s.mu.Unlock() + + for sens := range s.shmem.IterSensors() { + s.sensorIDByIdx = append(s.sensorIDByIdx, sens.ID()) + } + + return s.sensorIDByIdx, nil +} + +// ReadingsBySensorID returns slice of hwinfoReading for a given sensor ID +func (s *Service) ReadingsBySensorID(id string) ([]hwinfo.Reading, error) { + s.mu.RLock() + if s.readingsBySensorID != nil && s.readingsBuilt { + defer s.mu.RUnlock() + readings, ok := s.readingsBySensorID[id] + if !ok { + return nil, fmt.Errorf("readings for sensor id %s do not exist", id) + } + return readings, nil + } + s.mu.RUnlock() + + sids, err := s.SensorIDByIdx() + if err != nil { + return nil, err + } + + s.mu.Lock() + defer s.mu.Unlock() + + if s.readingsBySensorID == nil { + s.readingsBySensorID = make(map[string][]hwinfo.Reading) + } + + for r := range s.shmem.IterReadings() { + sidx := int(r.SensorIndex()) + if sidx < len(sids) { + sid := sids[sidx] + s.readingsBySensorID[sid] = append(s.readingsBySensorID[sid], r) + } else { + return nil, fmt.Errorf("sensor at index %d out of range ", sidx) + } + } + s.readingsBuilt = true + + readings, ok := s.readingsBySensorID[id] + if !ok { + return nil, fmt.Errorf("readings for sensor id %s do not exist", id) + } + return readings, nil +} diff --git a/internal/hwinfo/reading.go b/internal/hwinfo/reading.go new file mode 100644 index 0000000..71c5a27 --- /dev/null +++ b/internal/hwinfo/reading.go @@ -0,0 +1,124 @@ +package hwinfo + +/* +#include +#include "hwisenssm2.h" +*/ +import "C" +import ( + "unsafe" + + "github.com/shayne/hwinfo-streamdeck/internal/hwinfo/util" +) + +// ReadingType enum of value/unit type for reading +type ReadingType int + +const ( + // ReadingTypeNone no type + ReadingTypeNone ReadingType = iota + // ReadingTypeTemp temperature in celsius + ReadingTypeTemp + // ReadingTypeVolt voltage + ReadingTypeVolt + // ReadingTypeFan RPM + ReadingTypeFan + // ReadingTypeCurrent amps + ReadingTypeCurrent + // ReadingTypePower watts + ReadingTypePower + // ReadingTypeClock Mhz + ReadingTypeClock + // ReadingTypeUsage e.g. MBs + ReadingTypeUsage + // ReadingTypeOther other + ReadingTypeOther +) + +func (t ReadingType) String() string { + return [...]string{"None", "Temp", "Volt", "Fan", "Current", "Power", "Clock", "Usage", "Other"}[t] +} + +// Reading element (e.g. usage, power, mhz...) +type Reading struct { + cr C.PHWiNFO_SENSORS_READING_ELEMENT +} + +// NewReading contructs a Reading +func NewReading(data []byte) Reading { + return Reading{ + cr: C.PHWiNFO_SENSORS_READING_ELEMENT(unsafe.Pointer(&data[0])), + } +} + +// ID unique ID of the reading within a particular sensor +func (r *Reading) ID() int32 { + return int32(r.cr.dwReadingID) +} + +// Type of sensor reading +func (r *Reading) Type() ReadingType { + return ReadingType(r.cr.tReading) +} + +// SensorIndex this is the index of sensor in the Sensors[] array to +// which this reading belongs to +func (r *Reading) SensorIndex() uint64 { + return uint64(r.cr.dwSensorIndex) +} + +// ReadingID a unique ID of the reading within a particular sensor +func (r *Reading) ReadingID() uint64 { + return uint64(r.cr.dwReadingID) +} + +// LabelOrig original label (e.g. "Chassis2 Fan") +func (r *Reading) LabelOrig() string { + return util.DecodeCharPtr(unsafe.Pointer(&r.cr.szLabelOrig), C.HWiNFO_SENSORS_STRING_LEN2) +} + +// LabelUser label displayed, which might have been renamed by user +func (r *Reading) LabelUser() string { + return util.DecodeCharPtr(unsafe.Pointer(&r.cr.szLabelUser), C.HWiNFO_SENSORS_STRING_LEN2) +} + +// Unit e.g. "RPM" +func (r *Reading) Unit() string { + return util.DecodeCharPtr(unsafe.Pointer(&r.cr.szUnit), C.HWiNFO_UNIT_STRING_LEN) +} + +func (r *Reading) valuePtr() unsafe.Pointer { + return unsafe.Pointer(uintptr(unsafe.Pointer(&r.cr.szUnit)) + C.HWiNFO_UNIT_STRING_LEN) +} + +// Value current value +func (r *Reading) Value() float64 { + return float64(*(*C.double)(r.valuePtr())) +} + +func (r *Reading) valueMinPtr() unsafe.Pointer { + return unsafe.Pointer(uintptr(r.valuePtr()) + C.sizeof_double) +} + +// ValueMin current value +func (r *Reading) ValueMin() float64 { + return float64(*(*C.double)(r.valueMinPtr())) +} + +func (r *Reading) valueMaxPtr() unsafe.Pointer { + return unsafe.Pointer(uintptr(r.valueMinPtr()) + C.sizeof_double) +} + +// ValueMax current value +func (r *Reading) ValueMax() float64 { + return float64(*(*C.double)(r.valueMaxPtr())) +} + +func (r *Reading) valueAvgPtr() unsafe.Pointer { + return unsafe.Pointer(uintptr(r.valueMaxPtr()) + C.sizeof_double) +} + +// ValueAvg current value +func (r *Reading) ValueAvg() float64 { + return float64(*(*C.double)(r.valueAvgPtr())) +} diff --git a/internal/hwinfo/sensor.go b/internal/hwinfo/sensor.go new file mode 100644 index 0000000..1e5c876 --- /dev/null +++ b/internal/hwinfo/sensor.go @@ -0,0 +1,52 @@ +package hwinfo + +/* +#include +#include "hwisenssm2.h" +*/ +import "C" + +import ( + "strconv" + "unsafe" + + "github.com/shayne/hwinfo-streamdeck/internal/hwinfo/util" +) + +// Sensor element (e.g. motherboard, cpu, gpu...) +type Sensor struct { + cs C.PHWiNFO_SENSORS_SENSOR_ELEMENT +} + +// NewSensor constructs a Sensor +func NewSensor(data []byte) Sensor { + return Sensor{ + cs: C.PHWiNFO_SENSORS_SENSOR_ELEMENT(unsafe.Pointer(&data[0])), + } +} + +// SensorID a unique Sensor ID +func (s *Sensor) SensorID() uint64 { + return uint64(s.cs.dwSensorID) +} + +// SensorInst the instance of the sensor (together with SensorID forms a unique ID) +func (s *Sensor) SensorInst() uint64 { + return uint64(s.cs.dwSensorInst) +} + +// ID a unique ID combining SensorID and SensorInst +func (s *Sensor) ID() string { + // keeping old method used in legacy steam deck plugin + return strconv.FormatUint(s.SensorID()*100+s.SensorInst(), 10) +} + +// NameOrig original name of sensor +func (s *Sensor) NameOrig() string { + return util.DecodeCharPtr(unsafe.Pointer(&s.cs.szSensorNameOrig), C.HWiNFO_SENSORS_STRING_LEN2) +} + +// NameUser sensor name displayed, which might have been renamed by user +func (s *Sensor) NameUser() string { + return util.DecodeCharPtr(unsafe.Pointer(&s.cs.szSensorNameUser), C.HWiNFO_SENSORS_STRING_LEN2) +} diff --git a/internal/hwinfo/shmem/shmem.go b/internal/hwinfo/shmem/shmem.go new file mode 100644 index 0000000..cfc0bfb --- /dev/null +++ b/internal/hwinfo/shmem/shmem.go @@ -0,0 +1,95 @@ +package shmem + +/* +#include +#include "../hwisenssm2.h" +*/ +import "C" + +import ( + "fmt" + "reflect" + "syscall" + "unsafe" + + "github.com/shayne/hwinfo-streamdeck/internal/hwinfo/mutex" + "github.com/shayne/hwinfo-streamdeck/internal/hwinfo/util" + "golang.org/x/sys/windows" +) + +var buf = make([]byte, 200000) + +func copyBytes(addr uintptr) []byte { + headerLen := C.sizeof_HWiNFO_SENSORS_SHARED_MEM2 + + var d []byte + dh := (*reflect.SliceHeader)(unsafe.Pointer(&d)) + + dh.Data = addr + dh.Len, dh.Cap = headerLen, headerLen + + cheader := C.PHWiNFO_SENSORS_SHARED_MEM2(unsafe.Pointer(&d[0])) + fullLen := int(cheader.dwOffsetOfReadingSection + (cheader.dwSizeOfReadingElement * cheader.dwNumReadingElements)) + + if fullLen > cap(buf) { + buf = append(buf, make([]byte, fullLen-cap(buf))...) + } + + dh.Len, dh.Cap = fullLen, fullLen + + copy(buf, d) + + return buf[:fullLen] +} + +// ReadBytes copies bytes from global shared memory +func ReadBytes() ([]byte, error) { + err := mutex.Lock() + defer mutex.Unlock() + if err != nil { + return nil, err + } + + hnd, err := openFileMapping() + if err != nil { + return nil, err + } + addr, err := mapViewOfFile(hnd) + if err != nil { + return nil, err + } + defer unmapViewOfFile(addr) + defer windows.CloseHandle(windows.Handle(unsafe.Pointer(hnd))) + + return copyBytes(addr), nil +} + +func openFileMapping() (C.HANDLE, error) { + lpName := C.CString("Global\\HWiNFO_SENS_SM2_REMOTE_0") + defer C.free(unsafe.Pointer(lpName)) + + hnd := C.OpenFileMapping(syscall.FILE_MAP_READ, 0, lpName) + if hnd == C.HANDLE(C.NULL) { + errstr := util.HandleLastError(uint64(C.GetLastError())) + return nil, fmt.Errorf("OpenFileMapping: %w", errstr) + } + + return hnd, nil +} + +func mapViewOfFile(hnd C.HANDLE) (uintptr, error) { + addr, err := windows.MapViewOfFile(windows.Handle(unsafe.Pointer(hnd)), C.FILE_MAP_READ, 0, 0, 0) + if err != nil { + return 0, fmt.Errorf("MapViewOfFile: %w", err) + } + + return addr, nil +} + +func unmapViewOfFile(ptr uintptr) error { + err := windows.UnmapViewOfFile(ptr) + if err != nil { + return fmt.Errorf("UnmapViewOfFile: %w", err) + } + return nil +} diff --git a/internal/hwinfo/util/util.go b/internal/hwinfo/util/util.go new file mode 100644 index 0000000..1dd3491 --- /dev/null +++ b/internal/hwinfo/util/util.go @@ -0,0 +1,61 @@ +package util + +import "C" + +import ( + "errors" + "fmt" + "log" + "strings" + "unsafe" + + "golang.org/x/text/encoding/charmap" +) + +// ErrFileNotFound Windows error +var ErrFileNotFound = errors.New("file not found") + +// ErrInvalidHandle Windows error +var ErrInvalidHandle = errors.New("invalid handle") + +// UnknownError unhandled Windows error +type UnknownError struct { + Code uint64 +} + +func (e UnknownError) Error() string { + return fmt.Sprintf("unknown error code: %d", e.Code) +} + +// HandleLastError converts C.GetLastError() to golang error +func HandleLastError(code uint64) error { + switch code { + case 2: // ERROR_FILE_NOT_FOUND + return ErrFileNotFound + case 6: // ERROR_INVALID_HANDLE + return ErrInvalidHandle + default: + return UnknownError{Code: code} + } +} + +func goStringFromPtr(ptr unsafe.Pointer, len int) string { + s := C.GoStringN((*C.char)(ptr), C.int(len)) + return s[:strings.IndexByte(s, 0)] +} + +// DecodeCharPtr decodes ISO8859_1 string to UTF-8 +func DecodeCharPtr(ptr unsafe.Pointer, len int) string { + s := goStringFromPtr(ptr, len) + ds, err := decodeISO8859_1(s) + if err != nil { + log.Fatalf("TODO: failed to decode: %v", err) + } + return ds +} + +var isodecoder = charmap.ISO8859_1.NewDecoder() + +func decodeISO8859_1(in string) (string, error) { + return isodecoder.String(in) +} diff --git a/kill-streamdeck.bat b/kill-streamdeck.bat new file mode 100644 index 0000000..94185cd --- /dev/null +++ b/kill-streamdeck.bat @@ -0,0 +1,2 @@ +@echo off +taskkill /F /IM StreamDeck.exe /T \ No newline at end of file diff --git a/make-release.bat b/make-release.bat new file mode 100644 index 0000000..b98b234 --- /dev/null +++ b/make-release.bat @@ -0,0 +1,3 @@ +@echo off +del build\com.exension.hwinfo.streamDeckPlugin +DistributionTool.exe com.exension.hwinfo.sdPlugin build diff --git a/pkg/graph/graph.go b/pkg/graph/graph.go new file mode 100644 index 0000000..74ddf57 --- /dev/null +++ b/pkg/graph/graph.go @@ -0,0 +1,327 @@ +package graph + +import ( + "bytes" + "fmt" + "io/ioutil" + "log" + "math" + "regexp" + + "github.com/golang/freetype/truetype" + "golang.org/x/image/font" + "golang.org/x/image/math/fixed" + + "image" + "image/color" + "image/png" + "sync" +) + +// Label struct contains text, position and color information +type Label struct { + text string + y uint + fontSize float64 + clr *color.RGBA +} + +// Graph is used to display a histogram of data passed to Update +type Graph struct { + img *image.RGBA + + lvay int + width int + height int + min int + max int + + yvals []uint8 + + fgColor *color.RGBA + bgColor *color.RGBA + hlColor *color.RGBA + + labels map[int]*Label + drawn bool + redraw bool +} + +// FontFaceManager builds and caches fonts based on size +type FontFaceManager struct { + mux sync.Mutex + fontCache map[float64]font.Face +} + +// NewFontFaceManager constructs new manager +func NewFontFaceManager() *FontFaceManager { + return &FontFaceManager{fontCache: make(map[float64]font.Face)} +} + +func (f *FontFaceManager) newFace(size float64) font.Face { + b, err := ioutil.ReadFile("DejaVuSans-Bold.ttf") + if err != nil { + log.Fatal(err) + } + tt, err := truetype.Parse(b) + if err != nil { + log.Fatal("failed to parse font") + } + face := truetype.NewFace(tt, &truetype.Options{Size: size, DPI: 72}) + return face +} + +// GetFaceOfSize returns font face for given size +func (f *FontFaceManager) GetFaceOfSize(size float64) font.Face { + f.mux.Lock() + defer f.mux.Unlock() + if f, ok := f.fontCache[size]; ok { + return f + } + nf := f.newFace(size) + f.fontCache[size] = nf + return nf +} + +type singleshared struct { + fontFaceManager *FontFaceManager + pngEnc *png.Encoder + pngBuf *bytes.Buffer +} + +var sharedinstance *singleshared +var once sync.Once + +func shared() *singleshared { + once.Do(func() { + sharedinstance = &singleshared{ + pngEnc: &png.Encoder{ + CompressionLevel: png.NoCompression, + }, + pngBuf: bytes.NewBuffer(make([]byte, 0, 15697)), + } + sharedinstance.fontFaceManager = NewFontFaceManager() + }) + return sharedinstance +} + +// NewGraph initializes a new Graph for rendering +func NewGraph(width, height, min, max int, fgColor, bgColor, hlColor *color.RGBA) *Graph { + img := image.NewRGBA(image.Rect(0, 0, int(width), int(height))) + labels := make(map[int]*Label) + + return &Graph{ + img: img, + lvay: -1, + width: width, + height: height, + min: min, + max: max, + labels: labels, + + yvals: make([]uint8, 0, width), + + fgColor: fgColor, + bgColor: bgColor, + hlColor: hlColor, + } +} + +// SetForegroundColor sets the foreground color of the graph +func (g *Graph) SetForegroundColor(clr *color.RGBA) { + g.fgColor = clr + g.redraw = true +} + +// SetBackgroundColor sets the background color of the graph +func (g *Graph) SetBackgroundColor(clr *color.RGBA) { + g.bgColor = clr + g.redraw = true +} + +// SetHighlightColor sets the highlight color of the graph +func (g *Graph) SetHighlightColor(clr *color.RGBA) { + g.hlColor = clr + g.redraw = true +} + +// SetMin sets the min value for the graph scale +func (g *Graph) SetMin(min int) { + g.min = min +} + +// SetMax sets the max value for the graph scale +func (g *Graph) SetMax(max int) { + g.max = max +} + +// SetLabel given a key, set the initial text, position and color +func (g *Graph) SetLabel(key int, text string, y uint, clr *color.RGBA) { + l := &Label{text: text, y: y, clr: clr} + g.labels[key] = l +} + +// SetLabelText given a key, update the text for a pre-set label +func (g *Graph) SetLabelText(key int, text string) error { + l, ok := g.labels[key] + if !ok { + return fmt.Errorf("Label with key (%d) does not exist", key) + } + l.text = text + return nil +} + +// SetLabelFontSize given a key, update the text for a pre-set label +func (g *Graph) SetLabelFontSize(key int, size float64) error { + l, ok := g.labels[key] + if !ok { + return fmt.Errorf("Label with key (%d) does not exist", key) + } + l.fontSize = size + return nil +} + +// SetLabelColor given a key and color, sets the color of the text +func (g *Graph) SetLabelColor(key int, clr *color.RGBA) error { + l, ok := g.labels[key] + if !ok { + return fmt.Errorf("Label with key (%d) does not exist", key) + } + l.clr = clr + return nil +} + +func (g *Graph) drawGraph(x, vay, maxx int) { + var clr *color.RGBA + for ; x <= maxx; x++ { + for y := 0; y < g.height; y++ { + if y == vay { + clr = g.hlColor + } else if g.lvay != -1 && vay > g.lvay && vay >= y && y >= g.lvay { + clr = g.hlColor + } else if g.lvay != -1 && vay < g.lvay && vay <= y && y <= g.lvay { + clr = g.hlColor + } else if vay > y { + clr = g.fgColor + } else { + clr = g.bgColor + } + i := g.img.PixOffset(x, g.width-1-y) + g.img.Pix[i+0] = clr.R + g.img.Pix[i+1] = clr.G + g.img.Pix[i+2] = clr.B + g.img.Pix[i+3] = clr.A + } + g.lvay = vay + } +} + +// Update given a value draws the graph, shifting contents left. Call EncodePNG to get a rendered PNG +func (g *Graph) Update(value float64) { + vay := vAsY(g.height-1, value, g.min, g.max) + + if len(g.yvals) >= g.width { + _, a := g.yvals[0], g.yvals[1:] + g.yvals = a + } + g.yvals = append(g.yvals, uint8(vay)) + + if g.redraw { + g.lvay = -1 + lyvals := len(g.yvals) + for idx := lyvals - 1; idx >= 0; idx-- { + x := g.width - lyvals + idx + maxx := x + if idx == 0 { + x = 0 + } + v := int(g.yvals[idx]) + g.drawGraph(x, v, maxx) + } + g.lvay = int(g.yvals[lyvals-1]) + g.redraw = false + } else if g.drawn { + // shift the graph left 1px + for y := 0; y < g.height; y++ { + idx := g.img.PixOffset(0, y) + p1 := g.img.Pix[:idx] + p2 := g.img.Pix[idx+4 : idx+(g.width*4)] + p3 := g.img.Pix[idx+(g.width*4):] + g.img.Pix = append(p1, append(append(p2, []uint8{0, 0, 0, 0}...), p3...)...) + } + g.drawGraph(int(g.width)-1, int(vay), g.width-1) + } else { + g.drawGraph(0, vay, g.width-1) + g.drawn = true + } +} + +// EncodePNG renders the current state of the graph +func (g *Graph) EncodePNG() ([]byte, error) { + bak := append(g.img.Pix[:0:0], g.img.Pix...) + for _, l := range g.labels { + g.drawLabel(l) + } + shared := shared() + err := shared.pngEnc.Encode(shared.pngBuf, g.img) + if err != nil { + return nil, err + } + g.img.Pix = bak + bts := shared.pngBuf.Bytes() + shared.pngBuf.Reset() + return bts, nil +} + +func vAsY(maxY int, v float64, minV, maxV int) int { + r := maxV - minV + v1 := v - float64(minV) + yf := v1 / float64(r) * float64(maxY) + yi := int(math.Round(yf)) + return yi +} + +func unfix(x fixed.Int26_6) float64 { + const shift, mask = 6, 1<<6 - 1 + if x >= 0 { + return float64(x>>shift) + float64(x&mask)/64 + } + x = -x + if x >= 0 { + return -(float64(x>>shift) + float64(x&mask)/64) + } + return 0 +} + +var newlineRegex = regexp.MustCompile("(\n|\\\\n)+") + +func (g *Graph) drawLabel(l *Label) { + shared := shared() + lines := newlineRegex.Split(l.text, -1) + face := shared.fontFaceManager.GetFaceOfSize(l.fontSize) + curY := l.y - uint(10.5-float64(face.Metrics().Height.Round())) + + for _, line := range lines { + var lwidth float64 + for _, x := range line { + awidth, ok := face.GlyphAdvance(rune(x)) + if ok != true { + log.Println("drawLabel: Failed to GlyphAdvance") + return + } + lwidth += unfix(awidth) + } + + lx := (float64(g.width) / 2.) - (lwidth / 2.) + point := fixed.Point26_6{X: fixed.Int26_6(lx * 64), Y: fixed.Int26_6(curY * 64)} + + d := &font.Drawer{ + Dst: g.img, + Src: image.NewUniform(l.clr), + Face: face, + Dot: point, + } + d.DrawString(line) + curY += 12 + } +} diff --git a/pkg/service/grpc.go b/pkg/service/grpc.go new file mode 100644 index 0000000..d0a0afc --- /dev/null +++ b/pkg/service/grpc.go @@ -0,0 +1,126 @@ +package hwsensorsservice + +import ( + "context" + "errors" + "io" + + "github.com/golang/protobuf/ptypes/empty" + "github.com/shayne/hwinfo-streamdeck/pkg/service/proto" +) + +// GRPCClient is an implementation of KV that talks over RPC. +type GRPCClient struct { + Client proto.HWServiceClient +} + +// PollTime rpc call +func (c *GRPCClient) PollTime() (uint64, error) { + resp, err := c.Client.PollTime(context.Background(), &empty.Empty{}) + if err != nil { + return 0, err + } + return resp.GetPollTime(), nil +} + +// Sensors implementation +func (c *GRPCClient) Sensors() ([]Sensor, error) { + stream, err := c.Client.Sensors(context.Background(), &empty.Empty{}) + if err != nil { + return nil, err + } + + var sensors []Sensor + for { + s, err := stream.Recv() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, err + } + sensors = append(sensors, &sensor{s}) + } + + return sensors, nil +} + +// ReadingsForSensorID implementation +func (c *GRPCClient) ReadingsForSensorID(id string) ([]Reading, error) { + stream, err := c.Client.ReadingsForSensorID(context.Background(), &proto.SensorIDRequest{Id: id}) + if err != nil { + return nil, err + } + + var readings []Reading + for { + r, err := stream.Recv() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, err + } + readings = append(readings, &reading{r}) + } + + return readings, nil +} + +// GRPCServer is the gRPC server that GRPCClient talks to. +type GRPCServer struct { + // This is the real implementation + Impl HardwareService + proto.UnimplementedHWServiceServer +} + +// PollTime gRPC wrapper +func (s *GRPCServer) PollTime(ctx context.Context, _ *empty.Empty) (*proto.PollTimeReply, error) { + v, err := s.Impl.PollTime() + return &proto.PollTimeReply{PollTime: v}, err +} + +// Sensors gRPC wrapper +func (s *GRPCServer) Sensors(_ *empty.Empty, stream proto.HWService_SensorsServer) error { + sensors, err := s.Impl.Sensors() + if err != nil { + return err + } + + for _, sensor := range sensors { + if err := stream.Send(&proto.Sensor{ + ID: sensor.ID(), + Name: sensor.Name(), + }); err != nil { + return err + } + } + + return nil +} + +// ReadingsForSensorID gRPC wrapper +func (s *GRPCServer) ReadingsForSensorID(req *proto.SensorIDRequest, stream proto.HWService_ReadingsForSensorIDServer) error { + readings, err := s.Impl.ReadingsForSensorID(req.GetId()) + if err != nil { + return err + } + + for _, reading := range readings { + if err := stream.Send(&proto.Reading{ + ID: reading.ID(), + TypeI: reading.TypeI(), + Type: reading.Type(), + Label: reading.Label(), + Unit: reading.Unit(), + Value: reading.Value(), + ValueMin: reading.ValueMin(), + ValueMax: reading.ValueMax(), + ValueAvg: reading.ValueAvg(), + }); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/service/interface.go b/pkg/service/interface.go new file mode 100644 index 0000000..b2b3ca6 --- /dev/null +++ b/pkg/service/interface.go @@ -0,0 +1,149 @@ +package hwsensorsservice + +import ( + "context" + + "google.golang.org/grpc" + + "github.com/hashicorp/go-plugin" + "github.com/shayne/hwinfo-streamdeck/pkg/service/proto" +) + +// Handshake is a common handshake that is shared by plugin and host. +var Handshake = plugin.HandshakeConfig{ + // This isn't required when using VersionedPlugins + ProtocolVersion: 1, + MagicCookieKey: "BASIC_PLUGIN", + MagicCookieValue: "hello", +} + +// PluginMap is the map of plugins we can dispense. +var PluginMap = map[string]plugin.Plugin{ + "hwinfoplugin": &HardwareServicePlugin{}, +} + +// HardwareService is the interface that we're exposing as a plugin. +type HardwareService interface { + PollTime() (uint64, error) + Sensors() ([]Sensor, error) + ReadingsForSensorID(id string) ([]Reading, error) +} + +// HardwareServicePlugin is the implementation of plugin.GRPCPlugin so we can serve/consume this. +type HardwareServicePlugin struct { + // GRPCPlugin must still implement the Plugin interface + plugin.Plugin + // Concrete implementation, written in Go. This is only used for plugins + // that are written in Go. + Impl HardwareService +} + +// GRPCServer constructor +func (p *HardwareServicePlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { + proto.RegisterHWServiceServer(s, &GRPCServer{Impl: p.Impl}) + return nil +} + +// GRPCClient constructor +func (p *HardwareServicePlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { + return &GRPCClient{Client: proto.NewHWServiceClient(c)}, nil +} + +// Sensor is the common hardware interface for a sensor +type Sensor interface { + ID() string + Name() string +} + +// ReadingType enum of value/unit type for reading +type ReadingType int + +const ( + // ReadingTypeNone no type + ReadingTypeNone ReadingType = iota + // ReadingTypeTemp temperature in celsius + ReadingTypeTemp + // ReadingTypeVolt voltage + ReadingTypeVolt + // ReadingTypeFan RPM + ReadingTypeFan + // ReadingTypeCurrent amps + ReadingTypeCurrent + // ReadingTypePower watts + ReadingTypePower + // ReadingTypeClock Mhz + ReadingTypeClock + // ReadingTypeUsage e.g. MBs + ReadingTypeUsage + // ReadingTypeOther other + ReadingTypeOther +) + +func (t ReadingType) String() string { + return [...]string{"None", "Temp", "Volt", "Fan", "Current", "Power", "Clock", "Usage", "Other"}[t] +} + +// Reading is the common hardware interface for a sensor's reading +type Reading interface { + ID() int32 + TypeI() int32 + Type() string + Label() string + Unit() string + Value() float64 + ValueMin() float64 + ValueMax() float64 + ValueAvg() float64 +} + +type sensor struct { + *proto.Sensor +} + +func (s sensor) ID() string { + return s.Sensor.GetID() +} + +func (s sensor) Name() string { + return s.Sensor.GetName() +} + +type reading struct { + *proto.Reading +} + +func (r reading) ID() int32 { + return r.Reading.GetID() +} + +func (r reading) Label() string { + return r.Reading.GetLabel() +} + +func (r reading) Type() string { + return r.Reading.GetType() +} + +func (r reading) TypeI() int32 { + return r.Reading.GetTypeI() +} + +func (r reading) Unit() string { + return r.Reading.GetUnit() +} + +func (r reading) Value() float64 { + return r.Reading.GetValue() +} + +func (r reading) ValueMin() float64 { + return r.Reading.GetValueMin() +} + +func (r reading) ValueMax() float64 { + return r.Reading.GetValueMax() +} + +func (r reading) ValueAvg() float64 { + return r.Reading.GetValueAvg() +} diff --git a/pkg/service/proto/hwservice.pb.go b/pkg/service/proto/hwservice.pb.go new file mode 100644 index 0000000..d968784 --- /dev/null +++ b/pkg/service/proto/hwservice.pb.go @@ -0,0 +1,439 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.21.12 +// source: pkg/service/proto/hwservice.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type PollTimeReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PollTime uint64 `protobuf:"varint,1,opt,name=pollTime,proto3" json:"pollTime,omitempty"` +} + +func (x *PollTimeReply) Reset() { + *x = PollTimeReply{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_service_proto_hwservice_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PollTimeReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PollTimeReply) ProtoMessage() {} + +func (x *PollTimeReply) ProtoReflect() protoreflect.Message { + mi := &file_pkg_service_proto_hwservice_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PollTimeReply.ProtoReflect.Descriptor instead. +func (*PollTimeReply) Descriptor() ([]byte, []int) { + return file_pkg_service_proto_hwservice_proto_rawDescGZIP(), []int{0} +} + +func (x *PollTimeReply) GetPollTime() uint64 { + if x != nil { + return x.PollTime + } + return 0 +} + +type Sensor struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *Sensor) Reset() { + *x = Sensor{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_service_proto_hwservice_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Sensor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Sensor) ProtoMessage() {} + +func (x *Sensor) ProtoReflect() protoreflect.Message { + mi := &file_pkg_service_proto_hwservice_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Sensor.ProtoReflect.Descriptor instead. +func (*Sensor) Descriptor() ([]byte, []int) { + return file_pkg_service_proto_hwservice_proto_rawDescGZIP(), []int{1} +} + +func (x *Sensor) GetID() string { + if x != nil { + return x.ID + } + return "" +} + +func (x *Sensor) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type SensorIDRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *SensorIDRequest) Reset() { + *x = SensorIDRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_service_proto_hwservice_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SensorIDRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SensorIDRequest) ProtoMessage() {} + +func (x *SensorIDRequest) ProtoReflect() protoreflect.Message { + mi := &file_pkg_service_proto_hwservice_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SensorIDRequest.ProtoReflect.Descriptor instead. +func (*SensorIDRequest) Descriptor() ([]byte, []int) { + return file_pkg_service_proto_hwservice_proto_rawDescGZIP(), []int{2} +} + +func (x *SensorIDRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type Reading struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ID int32 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` + TypeI int32 `protobuf:"varint,2,opt,name=typeI,proto3" json:"typeI,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + Label string `protobuf:"bytes,4,opt,name=label,proto3" json:"label,omitempty"` + Unit string `protobuf:"bytes,5,opt,name=unit,proto3" json:"unit,omitempty"` + Value float64 `protobuf:"fixed64,6,opt,name=value,proto3" json:"value,omitempty"` + ValueMin float64 `protobuf:"fixed64,7,opt,name=valueMin,proto3" json:"valueMin,omitempty"` + ValueMax float64 `protobuf:"fixed64,8,opt,name=valueMax,proto3" json:"valueMax,omitempty"` + ValueAvg float64 `protobuf:"fixed64,9,opt,name=valueAvg,proto3" json:"valueAvg,omitempty"` +} + +func (x *Reading) Reset() { + *x = Reading{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_service_proto_hwservice_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Reading) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Reading) ProtoMessage() {} + +func (x *Reading) ProtoReflect() protoreflect.Message { + mi := &file_pkg_service_proto_hwservice_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Reading.ProtoReflect.Descriptor instead. +func (*Reading) Descriptor() ([]byte, []int) { + return file_pkg_service_proto_hwservice_proto_rawDescGZIP(), []int{3} +} + +func (x *Reading) GetID() int32 { + if x != nil { + return x.ID + } + return 0 +} + +func (x *Reading) GetTypeI() int32 { + if x != nil { + return x.TypeI + } + return 0 +} + +func (x *Reading) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Reading) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *Reading) GetUnit() string { + if x != nil { + return x.Unit + } + return "" +} + +func (x *Reading) GetValue() float64 { + if x != nil { + return x.Value + } + return 0 +} + +func (x *Reading) GetValueMin() float64 { + if x != nil { + return x.ValueMin + } + return 0 +} + +func (x *Reading) GetValueMax() float64 { + if x != nil { + return x.ValueMax + } + return 0 +} + +func (x *Reading) GetValueAvg() float64 { + if x != nil { + return x.ValueAvg + } + return 0 +} + +var File_pkg_service_proto_hwservice_proto protoreflect.FileDescriptor + +var file_pkg_service_proto_hwservice_proto_rawDesc = []byte{ + 0x0a, 0x21, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2f, 0x68, 0x77, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, + 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2b, 0x0a, 0x0d, 0x50, 0x6f, 0x6c, 0x6c, 0x54, + 0x69, 0x6d, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6f, 0x6c, 0x6c, + 0x54, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x70, 0x6f, 0x6c, 0x6c, + 0x54, 0x69, 0x6d, 0x65, 0x22, 0x2c, 0x0a, 0x06, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x12, 0x0e, + 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x22, 0x21, 0x0a, 0x0f, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x49, 0x44, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0xd7, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x61, 0x64, 0x69, 0x6e, + 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x49, + 0x44, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x79, 0x70, 0x65, 0x49, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x05, 0x74, 0x79, 0x70, 0x65, 0x49, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6c, + 0x61, 0x62, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, + 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x6e, 0x69, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x75, 0x6e, 0x69, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x4d, 0x69, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x4d, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x4d, 0x61, 0x78, 0x18, 0x08, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x4d, 0x61, 0x78, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x41, 0x76, 0x67, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x41, 0x76, 0x67, 0x32, + 0xc0, 0x01, 0x0a, 0x09, 0x48, 0x57, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3a, 0x0a, + 0x08, 0x50, 0x6f, 0x6c, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x1a, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x6f, 0x6c, 0x6c, 0x54, 0x69, + 0x6d, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x07, 0x53, 0x65, 0x6e, + 0x73, 0x6f, 0x72, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x22, 0x00, 0x30, 0x01, 0x12, + 0x41, 0x0a, 0x13, 0x52, 0x65, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x46, 0x6f, 0x72, 0x53, 0x65, + 0x6e, 0x73, 0x6f, 0x72, 0x49, 0x44, 0x12, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, + 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x22, 0x00, + 0x30, 0x01, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x73, 0x68, 0x61, 0x79, 0x6e, 0x65, 0x2f, 0x68, 0x77, 0x69, 0x6e, 0x66, 0x6f, 0x2d, 0x73, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x64, 0x65, 0x63, 0x6b, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +} + +var ( + file_pkg_service_proto_hwservice_proto_rawDescOnce sync.Once + file_pkg_service_proto_hwservice_proto_rawDescData = file_pkg_service_proto_hwservice_proto_rawDesc +) + +func file_pkg_service_proto_hwservice_proto_rawDescGZIP() []byte { + file_pkg_service_proto_hwservice_proto_rawDescOnce.Do(func() { + file_pkg_service_proto_hwservice_proto_rawDescData = protoimpl.X.CompressGZIP(file_pkg_service_proto_hwservice_proto_rawDescData) + }) + return file_pkg_service_proto_hwservice_proto_rawDescData +} + +var file_pkg_service_proto_hwservice_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_pkg_service_proto_hwservice_proto_goTypes = []interface{}{ + (*PollTimeReply)(nil), // 0: proto.PollTimeReply + (*Sensor)(nil), // 1: proto.Sensor + (*SensorIDRequest)(nil), // 2: proto.SensorIDRequest + (*Reading)(nil), // 3: proto.Reading + (*emptypb.Empty)(nil), // 4: google.protobuf.Empty +} +var file_pkg_service_proto_hwservice_proto_depIdxs = []int32{ + 4, // 0: proto.HWService.PollTime:input_type -> google.protobuf.Empty + 4, // 1: proto.HWService.Sensors:input_type -> google.protobuf.Empty + 2, // 2: proto.HWService.ReadingsForSensorID:input_type -> proto.SensorIDRequest + 0, // 3: proto.HWService.PollTime:output_type -> proto.PollTimeReply + 1, // 4: proto.HWService.Sensors:output_type -> proto.Sensor + 3, // 5: proto.HWService.ReadingsForSensorID:output_type -> proto.Reading + 3, // [3:6] is the sub-list for method output_type + 0, // [0:3] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_pkg_service_proto_hwservice_proto_init() } +func file_pkg_service_proto_hwservice_proto_init() { + if File_pkg_service_proto_hwservice_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_pkg_service_proto_hwservice_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PollTimeReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_service_proto_hwservice_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Sensor); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_service_proto_hwservice_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SensorIDRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_service_proto_hwservice_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Reading); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_pkg_service_proto_hwservice_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_pkg_service_proto_hwservice_proto_goTypes, + DependencyIndexes: file_pkg_service_proto_hwservice_proto_depIdxs, + MessageInfos: file_pkg_service_proto_hwservice_proto_msgTypes, + }.Build() + File_pkg_service_proto_hwservice_proto = out.File + file_pkg_service_proto_hwservice_proto_rawDesc = nil + file_pkg_service_proto_hwservice_proto_goTypes = nil + file_pkg_service_proto_hwservice_proto_depIdxs = nil +} diff --git a/pkg/service/proto/hwservice.proto b/pkg/service/proto/hwservice.proto new file mode 100644 index 0000000..8bd388a --- /dev/null +++ b/pkg/service/proto/hwservice.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +option go_package = "github.com/shayne/hwinfo-streamdeck/pkg/service/proto"; + +import "google/protobuf/empty.proto"; + +package proto; + +service HWService { + rpc PollTime(google.protobuf.Empty) returns (PollTimeReply) {} + rpc Sensors(google.protobuf.Empty) returns (stream Sensor) {} + rpc ReadingsForSensorID(SensorIDRequest) returns (stream Reading) {} +} + +message PollTimeReply { uint64 pollTime = 1; } + +message Sensor { + string ID = 1; + string name = 2; +} + +message SensorIDRequest { string id = 1; } + +message Reading { + int32 ID = 1; + int32 typeI = 2; + string type = 3; + string label = 4; + string unit = 5; + double value = 6; + double valueMin = 7; + double valueMax = 8; + double valueAvg = 9; +} diff --git a/pkg/service/proto/hwservice_grpc.pb.go b/pkg/service/proto/hwservice_grpc.pb.go new file mode 100644 index 0000000..fc707ee --- /dev/null +++ b/pkg/service/proto/hwservice_grpc.pb.go @@ -0,0 +1,233 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.21.12 +// source: pkg/service/proto/hwservice.proto + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// HWServiceClient is the client API for HWService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type HWServiceClient interface { + PollTime(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*PollTimeReply, error) + Sensors(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (HWService_SensorsClient, error) + ReadingsForSensorID(ctx context.Context, in *SensorIDRequest, opts ...grpc.CallOption) (HWService_ReadingsForSensorIDClient, error) +} + +type hWServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewHWServiceClient(cc grpc.ClientConnInterface) HWServiceClient { + return &hWServiceClient{cc} +} + +func (c *hWServiceClient) PollTime(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*PollTimeReply, error) { + out := new(PollTimeReply) + err := c.cc.Invoke(ctx, "/proto.HWService/PollTime", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *hWServiceClient) Sensors(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (HWService_SensorsClient, error) { + stream, err := c.cc.NewStream(ctx, &HWService_ServiceDesc.Streams[0], "/proto.HWService/Sensors", opts...) + if err != nil { + return nil, err + } + x := &hWServiceSensorsClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type HWService_SensorsClient interface { + Recv() (*Sensor, error) + grpc.ClientStream +} + +type hWServiceSensorsClient struct { + grpc.ClientStream +} + +func (x *hWServiceSensorsClient) Recv() (*Sensor, error) { + m := new(Sensor) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *hWServiceClient) ReadingsForSensorID(ctx context.Context, in *SensorIDRequest, opts ...grpc.CallOption) (HWService_ReadingsForSensorIDClient, error) { + stream, err := c.cc.NewStream(ctx, &HWService_ServiceDesc.Streams[1], "/proto.HWService/ReadingsForSensorID", opts...) + if err != nil { + return nil, err + } + x := &hWServiceReadingsForSensorIDClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type HWService_ReadingsForSensorIDClient interface { + Recv() (*Reading, error) + grpc.ClientStream +} + +type hWServiceReadingsForSensorIDClient struct { + grpc.ClientStream +} + +func (x *hWServiceReadingsForSensorIDClient) Recv() (*Reading, error) { + m := new(Reading) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// HWServiceServer is the server API for HWService service. +// All implementations must embed UnimplementedHWServiceServer +// for forward compatibility +type HWServiceServer interface { + PollTime(context.Context, *emptypb.Empty) (*PollTimeReply, error) + Sensors(*emptypb.Empty, HWService_SensorsServer) error + ReadingsForSensorID(*SensorIDRequest, HWService_ReadingsForSensorIDServer) error + mustEmbedUnimplementedHWServiceServer() +} + +// UnimplementedHWServiceServer must be embedded to have forward compatible implementations. +type UnimplementedHWServiceServer struct { +} + +func (UnimplementedHWServiceServer) PollTime(context.Context, *emptypb.Empty) (*PollTimeReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method PollTime not implemented") +} +func (UnimplementedHWServiceServer) Sensors(*emptypb.Empty, HWService_SensorsServer) error { + return status.Errorf(codes.Unimplemented, "method Sensors not implemented") +} +func (UnimplementedHWServiceServer) ReadingsForSensorID(*SensorIDRequest, HWService_ReadingsForSensorIDServer) error { + return status.Errorf(codes.Unimplemented, "method ReadingsForSensorID not implemented") +} +func (UnimplementedHWServiceServer) mustEmbedUnimplementedHWServiceServer() {} + +// UnsafeHWServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to HWServiceServer will +// result in compilation errors. +type UnsafeHWServiceServer interface { + mustEmbedUnimplementedHWServiceServer() +} + +func RegisterHWServiceServer(s grpc.ServiceRegistrar, srv HWServiceServer) { + s.RegisterService(&HWService_ServiceDesc, srv) +} + +func _HWService_PollTime_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HWServiceServer).PollTime(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.HWService/PollTime", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HWServiceServer).PollTime(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _HWService_Sensors_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(emptypb.Empty) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(HWServiceServer).Sensors(m, &hWServiceSensorsServer{stream}) +} + +type HWService_SensorsServer interface { + Send(*Sensor) error + grpc.ServerStream +} + +type hWServiceSensorsServer struct { + grpc.ServerStream +} + +func (x *hWServiceSensorsServer) Send(m *Sensor) error { + return x.ServerStream.SendMsg(m) +} + +func _HWService_ReadingsForSensorID_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(SensorIDRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(HWServiceServer).ReadingsForSensorID(m, &hWServiceReadingsForSensorIDServer{stream}) +} + +type HWService_ReadingsForSensorIDServer interface { + Send(*Reading) error + grpc.ServerStream +} + +type hWServiceReadingsForSensorIDServer struct { + grpc.ServerStream +} + +func (x *hWServiceReadingsForSensorIDServer) Send(m *Reading) error { + return x.ServerStream.SendMsg(m) +} + +// HWService_ServiceDesc is the grpc.ServiceDesc for HWService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var HWService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "proto.HWService", + HandlerType: (*HWServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "PollTime", + Handler: _HWService_PollTime_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "Sensors", + Handler: _HWService_Sensors_Handler, + ServerStreams: true, + }, + { + StreamName: "ReadingsForSensorID", + Handler: _HWService_ReadingsForSensorID_Handler, + ServerStreams: true, + }, + }, + Metadata: "pkg/service/proto/hwservice.proto", +} diff --git a/pkg/streamdeck/streamdeck.go b/pkg/streamdeck/streamdeck.go new file mode 100644 index 0000000..613557a --- /dev/null +++ b/pkg/streamdeck/streamdeck.go @@ -0,0 +1,295 @@ +package streamdeck + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "log" + "net/url" + "os" + "os/signal" + "time" + + "github.com/gorilla/websocket" +) + +// EventDelegate receives callbacks for Stream Deck SDK events +type EventDelegate interface { + OnConnected(*websocket.Conn) + OnWillAppear(*EvWillAppear) + OnTitleParametersDidChange(*EvTitleParametersDidChange) + OnPropertyInspectorConnected(*EvSendToPlugin) + OnSendToPlugin(*EvSendToPlugin) + OnApplicationDidLaunch(*EvApplication) + OnApplicationDidTerminate(*EvApplication) +} + +// StreamDeck SDK APIs +type StreamDeck struct { + Port string + PluginUUID string + RegisterEvent string + Info string + delegate EventDelegate + conn *websocket.Conn + done chan struct{} +} + +// NewStreamDeck prepares StreamDeck struct +func NewStreamDeck(port, pluginUUID, registerEvent, info string) *StreamDeck { + return &StreamDeck{ + Port: port, + PluginUUID: pluginUUID, + RegisterEvent: registerEvent, + Info: info, + done: make(chan struct{}), + } +} + +// SetDelegate sets the delegate for receiving Stream Deck SDK event callbacks +func (sd *StreamDeck) SetDelegate(ed EventDelegate) { + sd.delegate = ed +} + +func (sd *StreamDeck) register() error { + reg := evRegister{Event: sd.RegisterEvent, UUID: sd.PluginUUID} + data, err := json.Marshal(reg) + log.Println(string(data)) + if err != nil { + return err + } + err = sd.conn.WriteMessage(websocket.TextMessage, data) + if err != nil { + return err + } + return nil +} + +// Connect establishes WebSocket connection to StreamDeck software +func (sd *StreamDeck) Connect() error { + u := url.URL{Scheme: "ws", Host: fmt.Sprintf("127.0.0.1:%s", sd.Port)} + c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) + if err != nil { + return err + } + + sd.conn = c + + err = sd.register() + if err != nil { + return fmt.Errorf("failed register: %v", err) + } + + if sd.delegate != nil { + sd.delegate.OnConnected(sd.conn) + } + + return nil +} + +// Close closes the websocket connection, defer after Connect +func (sd *StreamDeck) Close() { + sd.conn.Close() +} + +func (sd *StreamDeck) onPropertyInspectorMessage(value string, ev *EvSendToPlugin) error { + switch value { + case "propertyInspectorConnected": + if sd.delegate != nil { + sd.delegate.OnPropertyInspectorConnected(ev) + } + default: + log.Printf("Unknown property_inspector value: %s\n", value) + } + return nil +} + +func (sd *StreamDeck) onSendToPlugin(ev *EvSendToPlugin) error { + payload := make(map[string]*json.RawMessage) + err := json.Unmarshal(*ev.Payload, &payload) + if err != nil { + return fmt.Errorf("onSendToPlugin payload unmarshal: %v", err) + } + if raw, ok := payload["property_inspector"]; ok { + var value string + err := json.Unmarshal(*raw, &value) + if err != nil { + return fmt.Errorf("onSendToPlugin unmarshal property_inspector value: %v", err) + } + err = sd.onPropertyInspectorMessage(value, ev) + if err != nil { + return fmt.Errorf("onPropertyInspectorMessage: %v", err) + } + return nil + } + if sd.delegate != nil { + sd.delegate.OnSendToPlugin(ev) + } + return nil +} + +func (sd *StreamDeck) spawnMessageReader() { + defer close(sd.done) + for { + _, message, err := sd.conn.ReadMessage() + if err != nil { + log.Println("read:", err) + return + } + log.Printf("recv: %s", message) + + var objmap map[string]*json.RawMessage + err = json.Unmarshal(message, &objmap) + if err != nil { + log.Fatal("message unmarshal", err) + } + var event string + err = json.Unmarshal(*objmap["event"], &event) + if err != nil { + log.Fatal("event unmarshal", err) + } + switch event { + case "willAppear": + var ev EvWillAppear + err := json.Unmarshal(message, &ev) + if err != nil { + log.Fatal("willAppear unmarshal", err) + } + if sd.delegate != nil { + sd.delegate.OnWillAppear(&ev) + } + case "titleParametersDidChange": + var ev EvTitleParametersDidChange + err := json.Unmarshal(message, &ev) + if err != nil { + log.Fatal("titleParametersDidChange unmarshal", err) + } + if sd.delegate != nil { + sd.delegate.OnTitleParametersDidChange(&ev) + } + case "sendToPlugin": + var ev EvSendToPlugin + err := json.Unmarshal(message, &ev) + if err != nil { + log.Fatal("onSendToPlugin event unmarshal", err) + } + err = sd.onSendToPlugin(&ev) + if err != nil { + log.Fatal("onSendToPlugin", err) + } + case "applicationDidLaunch": + var ev EvApplication + err := json.Unmarshal(message, &ev) + if err != nil { + log.Fatal("applicationDidLaunch unmarshal", err) + } + if sd.delegate != nil { + sd.delegate.OnApplicationDidLaunch(&ev) + } + case "applicationDidTerminate": + var ev EvApplication + err := json.Unmarshal(message, &ev) + if err != nil { + log.Fatal("applicationDidTerminate unmarshal", err) + } + if sd.delegate != nil { + sd.delegate.OnApplicationDidTerminate(&ev) + } + default: + log.Printf("Unknown event: %s\n", event) + } + } +} + +// ListenAndWait processes messages and waits until closed +func (sd *StreamDeck) ListenAndWait() { + go sd.spawnMessageReader() + + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + + for { + select { + case <-sd.done: + return + case <-interrupt: + log.Println("interrupt") + + // Cleanly close the connection by sending a close message and then + // waiting (with timeout) for the server to close the connection. + err := sd.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + if err != nil { + log.Println("write close:", err) + return + } + select { + case <-sd.done: + case <-time.After(time.Second): + } + } + } +} + +// SendToPropertyInspector sends a payload to the Property Inspector +func (sd *StreamDeck) SendToPropertyInspector(action, context string, payload interface{}) error { + event := evSendToPropertyInspector{Action: action, Event: "sendToPropertyInspector", + Context: context, Payload: payload} + data, err := json.Marshal(event) + if err != nil { + return fmt.Errorf("sendToPropertyInspector: %v", err) + } + err = sd.conn.WriteMessage(websocket.TextMessage, data) + if err != nil { + return fmt.Errorf("setTitle write: %v", err) + } + return nil +} + +// SetTitle dynamically changes the title displayed by an instance of an action +func (sd *StreamDeck) SetTitle(context, title string) error { + event := evSetTitle{Event: "setTitle", Context: context, Payload: evSetTitlePayload{ + Title: title, + Target: 0, + }} + data, err := json.Marshal(event) + if err != nil { + return fmt.Errorf("setTitle: %v", err) + } + err = sd.conn.WriteMessage(websocket.TextMessage, data) + if err != nil { + return fmt.Errorf("setTitle write: %v", err) + } + return nil +} + +// SetSettings saves persistent data for the action's instance +func (sd *StreamDeck) SetSettings(context string, payload interface{}) error { + event := evSetSettings{Event: "setSettings", Context: context, Payload: payload} + data, err := json.Marshal(event) + if err != nil { + return fmt.Errorf("setSettings: %v", err) + } + err = sd.conn.WriteMessage(websocket.TextMessage, data) + if err != nil { + return fmt.Errorf("setSettings write: %v", err) + } + return nil +} + +// SetImage dynamically changes the image displayed by an instance of an action +func (sd *StreamDeck) SetImage(context string, bts []byte) error { + b64 := base64.StdEncoding.EncodeToString(bts) + event := evSetImage{Event: "setImage", Context: context, Payload: evSetImagePayload{ + Image: fmt.Sprintf("data:image/png;base64, %s", b64), + Target: 0, + }} + data, err := json.Marshal(event) + if err != nil { + return fmt.Errorf("setImage: %v", err) + } + err = sd.conn.WriteMessage(websocket.TextMessage, data) + if err != nil { + return fmt.Errorf("setImage write: %v", err) + } + return nil +} diff --git a/pkg/streamdeck/types.go b/pkg/streamdeck/types.go new file mode 100644 index 0000000..d47e703 --- /dev/null +++ b/pkg/streamdeck/types.go @@ -0,0 +1,124 @@ +package streamdeck + +import "encoding/json" + +type evRegister struct { + Event string `json:"event"` + UUID string `json:"uuid"` +} + +// EvCoordinates is the coordinates structure from events +type EvCoordinates struct { + Column int `json:"column"` + Row int `json:"row"` +} + +// EvWillAppearPayload is the Payload structure from the willAppear event +type EvWillAppearPayload struct { + Settings *json.RawMessage `json:"settings"` + Coordinates EvCoordinates `json:"coordinates"` + Device string `json:"device"` + State int `json:"state"` + IsInMultiAction bool `json:"isInMultiAction"` +} + +// EvWillAppear is the payload from the willAppear event +type EvWillAppear struct { + Action string `json:"action"` + Event string `json:"event"` + Context string `json:"context"` + Device string `json:"device"` + Payload EvWillAppearPayload `json:"payload"` +} + +// EvWillDisappearPayload is the Payload structure from willDisappear event +type EvWillDisappearPayload struct { + EvWillAppearPayload +} + +// EvWillDisappear is the payload from the willDisappear event +type EvWillDisappear struct { + EvWillAppear +} + +// EvApplicationPayload is the sub-strcture from the EvApplication struct +type EvApplicationPayload struct { + Application string `json:"application"` +} + +// EvApplication is the payload from the applicatioDidLaunch/Terminate events +type EvApplication struct { + Payload EvApplicationPayload `json:"payload"` +} + +// EvTitleParameters is sub-structure from EvTitleParametersDidChangePayload +type EvTitleParameters struct { + FontFamily string `json:"fontFamily"` + FontSize int `json:"fontSize"` + FontStyle string `json:"fontStyle"` + FontUnderline bool `json:"fontUnderline"` + ShowTitle bool `json:"showTitle"` + TitleAlignment string `json:"titleAlignment"` + TitleColor string `json:"titleColor"` +} + +// EvTitleParametersDidChangePayload is the payload structure of EvTitleParametersDidChange +type EvTitleParametersDidChangePayload struct { + Coordinates EvCoordinates `json:"coordinates"` + Settings *json.RawMessage `json:"settings"` + TitleParameters EvTitleParameters `json:"titleParameters"` + Title string `json:"title"` + State int `json:"state"` +} + +// EvTitleParametersDidChange is the payload from the titleParametersDidChange event +type EvTitleParametersDidChange struct { + Action string `json:"action"` + Event string `json:"event"` + Context string `json:"context"` + Device string `json:"device"` + Payload EvTitleParametersDidChangePayload `json:"payload"` +} + +// EvSendToPlugin is received from the Property Inspector +type EvSendToPlugin struct { + Action string `json:"action"` + Event string `json:"event"` + Context string `json:"context"` + Payload *json.RawMessage `json:"payload"` +} + +type evSendToPropertyInspector struct { + Action string `json:"action"` + Event string `json:"event"` + Context string `json:"context"` + Payload interface{} `json:"payload"` +} + +type evSetTitlePayload struct { + Title string `json:"title"` + Target int `json:"target"` +} + +type evSetTitle struct { + Event string `json:"event"` + Context string `json:"context"` + Payload evSetTitlePayload `json:"payload"` +} + +type evSetSettings struct { + Event string `json:"event"` + Context string `json:"context"` + Payload interface{} `json:"payload"` +} + +type evSetImagePayload struct { + Image string `json:"image"` + Target int `json:"target"` +} + +type evSetImage struct { + Event string `json:"event"` + Context string `json:"context"` + Payload evSetImagePayload `json:"payload"` +} diff --git a/start-streamdeck.bat b/start-streamdeck.bat new file mode 100644 index 0000000..a4af8e2 --- /dev/null +++ b/start-streamdeck.bat @@ -0,0 +1,2 @@ +@echo off +start "" /B "C:\Program Files\Elgato\StreamDeck\StreamDeck.exe" \ No newline at end of file