From ea50cc0b9c1c2bb9f208a7685ced6bc40e8cc5cc Mon Sep 17 00:00:00 2001 From: Henrik Danielsson Date: Sun, 27 Aug 2023 13:41:29 +0200 Subject: [PATCH] Initial implementation of REST based API. --- README.md | 20 + api.go | 919 ++++++++++++++++++++++++++++++++++++++++++ config.go | 60 ++- deck.go | 126 ++++-- go.mod | 58 ++- go.sum | 282 +++++++++++++ main.go | 42 +- swagger-overrides.yml | 23 ++ swagger.json | 834 ++++++++++++++++++++++++++++++++++++++ widget.go | 18 + widget_button.go | 12 + 11 files changed, 2338 insertions(+), 56 deletions(-) create mode 100644 api.go create mode 100644 swagger-overrides.yml create mode 100644 swagger.json diff --git a/README.md b/README.md index c8ac30b..8516484 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,12 @@ An application to control your Elgato Stream Deck on Linux - Emulate a key-press - Paste to clipboard - Trigger a dbus call +- REST based API: + - Read and update device state like brightness and sleep/fade time. + - Wake or put the device to sleep. + - Read and modify the active deck config or an individual key configuration. + - Read widget state. + - Interactive API documentation using Swagger ## Installation @@ -134,6 +140,16 @@ Set a sleep timeout after which the screen gets turned off: deckmaster -sleep 10m ``` +```bash +deckmaster -rest-listen [localhost:4321] [-rest-trusted-proxies [10.0.0.1,10.0.0.2]] [-rest-docs] +``` + +Set an interface/ip and/or port to start a REST server on. +No authentication is required so it is **STRONGLY** recommended to only use *localhost*, else your device may be accessible over the internet. +If you do intend to expose the API to anything other than the local interface, use a reverse proxy to handle authentication and set the `-rest-trusted-proxies` flag. +If the `X-Forwarded-For` request header is set its value will only replace the client IP if the connection IP is in the optional `-rest-trusted-proxies` list, otherwise any client could spoof their IP with this header. It is not needed if you are not using a reverse proxy. +The interactive Swagger documentation will be available as `/docs` if `-rest-docs` is also set. + ## Configuration You can find a few example configurations in the [decks](https://github.com/muesli/deckmaster/tree/master/decks) @@ -146,11 +162,15 @@ Any widget is build up the following way: ```toml [[keys]] index = 0 + name = "My key" # optional ``` `index` needs to be present in every widget and describes the position of the widget on the streamdeck. `index` is 0-indexed and counted from top to bottom and left to right. +`name` is an optional string which can be used instead of the index to identify +keys/widgets in the REST API. Must be unique inside the active deck but may be +reused across different decks. #### Update interval for widgets diff --git a/api.go b/api.go new file mode 100644 index 0000000..7f2b097 --- /dev/null +++ b/api.go @@ -0,0 +1,919 @@ +// Package main Deckmaster API implements a REST API for controlling Stream Decks. +// +// Swagger file generated from source by the go-swagger package. +// +// `swagger generate spec -o swagger.json -i swagger-overrides.yml -m` +// +// https://goswagger.io/ +// https://github.com/go-swagger/go-swagger/ +// +// Allows controlling a single Stream Deck from anywhere using a REST-like API. +// The device is modeled using four main concepts: +// +// - The device: +// Holds read-only information about the connected device itself, such as +// the unique serial number, key layout, awake state, and brightness. +// Some properties can be changed to immediately update the device state. +// +// - The deck: +// Holds the configuration initially loaded from a deck file. Properties can +// be completely or partially updated to reload the correspodning widget(s) +// with the new confiuration. Only one deck is loaded at a time. +// +// - A Key configuration: +// Holds the configuration for a single key. Updating it immediately reloads +// the corresponding widget with the new configuration. A key is referenced +// using its one-dimensional index number, or a string name which must be +// unique per deck. Some keys may not yet have a configuration. +// +// - A widget: +// Holds the active widget state of a configured key. The widget state +// cannot be updated directly and contents vary by type of widget. +// +// Schemes: http +// Host: localhost:4321 +// BasePath: /v1 +// Version: 1.0.0 +// +// Consumes: +// +// - application/json +// +// Produces: +// +// - application/json +// +// - image/png +// +// swagger:meta +package main + +import ( + "encoding/json" + "errors" + "fmt" + "image" + "image/png" + "log" + "net/http" + "reflect" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-openapi/runtime/middleware" + adapter "github.com/gwatts/gin-adapter" + "github.com/jub0bs/fcors" + "github.com/lesismal/nbio/nbhttp" +) + +// Read-only properties of a Stream Deck device. +// +// swagger:model DeviceStateRead +type jsonDeviceRead struct { + // Device serial number. + Serial string `json:"serial"` + // Platform-specific device path. + ID string `json:"id"` + // Current brightness level (previous brightness if asleep). + Brightness *uint `json:"brightness"` + // Is the device asleep (screen off)? + Asleep bool `json:"asleep"` + // Number of key columns. + Columns uint8 `json:"columns"` + // Number of key rows. + Rows uint8 `json:"rows"` + // Number of keys. + Keys uint8 `json:"keys"` + // Number of screen pixels. + Pixels uint `json:"pixels"` + // Screen resolution in dots per inches. + DPI uint `json:"dpi"` + // Padding around buttons in pixels. + Padding uint `json:"padding"` + // Fade out time in human readable form like "250ms". + FadeDuration string `json:"fadeDuration"` + // Sleep timeout in human readable form like "1m30s". + SleepTimeout string `json:"sleepTimeout"` +} + +// Writeable properties of a Stream Deck device. +// +// swagger:model DeviceStateWrite +type jsonDeviceWrite struct { + // Brightness level. + Brightness *uint `json:"brightness"` + // Sleep toggle. + Asleep bool `json:"asleep"` + // Fade out time in human readable form like "250ms". + FadeDuration string `json:"fadeDuration"` + // Sleep timout in human readable form like "1m30s". + SleepTimeout string `json:"sleepTimeout"` +} + +// API representation of an active deck. +// +// swagger:model DeckState +type apiDeck struct { + // The path to the loaded deck file. + File string `json:"file"` + // The path to the loaded background image. + Background string `json:"background"` + // The active widgets. + Widgets []apiWidget `json:"widgets"` +} + +// API representation of an active widget. +// +// swagger:model WidgetState +type apiWidget struct { + // Type of widget used for the key. + Type string `json:"type"` + // An action to perform when the key is pressed and released. + Action *ActionConfig `json:"action,omitempty"` + // An action to perform when the key is held. + ActionHold *ActionConfig `json:"action_hold,omitempty"` + // Widget specific state. + State Widget `json:"state"` +} + +// An API error response. +// +// swagger:model ApiError +type apiError struct { + // An error message. + Message string `json:"error"` + // An optional internal error string with more details. + Description string `json:"description,omitempty"` + // An optional internal error object with more details. + Object error `json:"details,omitempty"` +} + +// Impements JSON marshaling for an error response. +// Transforms the supplied error into a message with an optional details +// message if an internal error was supplied, and includes the JSON +// representation of the internal error object if possible. +func (e apiError) MarshalJSON() ([]byte, error) { + var message = e.Message + if len(message) == 0 { + if e.Object != nil { + message = e.Object.Error() + } else { + return nil, errors.New("apiError must have a message or error object") + } + } else if e.Object != nil { + e.Description = e.Object.Error() + } + + errorOut, errEncoding := json.Marshal(e.Object) + + if errEncoding == nil && string(errorOut) == "{}" { + return json.Marshal(&struct { + Message string `json:"error"` + Details string `json:"details,omitempty"` + }{ + Message: message, + Details: e.Description, + }) + } + return json.Marshal(&struct { + Message string `json:"error"` + Details string `json:"details,omitempty"` + Object error `json:"object"` + }{ + Message: message, + Details: e.Description, + Object: e.Object, + }) + +} + +func restGetDevice(c *gin.Context) { + // swagger:route GET /device getDeviceState + // + // Gets the complete device state. + // + // Responses: + // 200: DeviceStateRead + c.JSON(http.StatusOK, jsonDeviceRead{ + Serial: deck.dev.Serial, + ID: deck.dev.ID, + Brightness: brightness, + Asleep: deck.dev.Asleep(), + Columns: deck.dev.Columns, + Rows: deck.dev.Rows, + Keys: deck.dev.Keys, + Pixels: deck.dev.Pixels, + DPI: deck.dev.DPI, + Padding: deck.dev.Padding, + FadeDuration: fadeDuration.String(), + SleepTimeout: sleepTimeout.String(), + }) +} + +func restPutDevice(c *gin.Context) { + // swagger:route PUT /device putDeviceState + // + // Updates the device state. + // + // Parameters: + // + name: body + // in: body + // type: DeviceStateWrite + // + // Responses: + // 200: DeviceStateRead + content := jsonDeviceWrite{ + Brightness: brightness, + Asleep: deck.dev.Asleep(), + } + c.BindJSON(&content) + var err error + newSleepTimeout := sleepTimeout + // Adjust sleep timeout. + if len(content.SleepTimeout) > 0 { + newSleepTimeout, err = time.ParseDuration(content.SleepTimeout) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, apiError{Message: "Could not parse sleep timeout.", Object: err}) + return + } + } + // Adjust fade duration. + newFadeDuration := fadeDuration + if len(content.FadeDuration) > 0 { + newFadeDuration, err = time.ParseDuration(content.FadeDuration) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, apiError{Message: "Could not parse fade duration.", Object: err}) + return + } + } + if newSleepTimeout != sleepTimeout { + sleepTimeout = newSleepTimeout + deck.dev.SetSleepTimeout(sleepTimeout) + } + fadeDuration = newFadeDuration + deck.dev.SetSleepFadeDuration(fadeDuration) + + // Toggle wake state. + if content.Asleep && !deck.dev.Asleep() { + deck.dev.Sleep() + } else if !content.Asleep && deck.dev.Asleep() { + deck.dev.Wake() + } + // Adjust brightness. + if *content.Brightness > 100 { + *content.Brightness = 100 + } + deck.dev.SetBrightness(uint8(*content.Brightness)) + + restGetDevice(c) +} + +func restGetDeck(c *gin.Context) { + // swagger:route GET /deck getDeckState + // + // Gets the complete deck state. + // + // Responses: + // 200: DeckState + apiModel := apiDeck{ + File: deck.File, + } + for _, w := range deck.Widgets { + apiModel.Widgets = append(apiModel.Widgets, apiConvertWidget(w)) + } + c.JSON(http.StatusOK, apiModel) +} + +func restPutDeck(c *gin.Context) { + // swagger:route PUT /deck putDeckState + // + // Reloads the current configration from the deck file on disk. + // + // Decks which have overwritten the file property can not be reloaded. + // + // Responses: + // 200: DeckState + // 500: ApiError + if deck.File == "" { + restAbortWithErrorStatus(http.StatusInternalServerError, errors.New("The current deck was not loaded from a file"), c) + return + } + newDeck, err := LoadDeck(deck.dev, ".", deck.File) + if err != nil { + restAbortWithErrorStatus(http.StatusInternalServerError, err, c) + return + } + deck = newDeck + deck.updateWidgets() + restGetDeck(c) +} + +func restGetDeckBackground(c *gin.Context) { + // swagger:route GET /deck/background putDeckBackground + // + // Get the active deck background. + // + // Set the Accepts header to get a PNG or the native representation as JSON. + // + // Produces: + // - application/json + // - image/png + // + // Responses: + // 200: image.Image + // 404: ApiError + // 500: ApiError + switch c.Request.Header.Get("Accepts") { + case "image/png": + c.Status(200) + png.Encode(c.Writer, deck.Background) + break + + case "application/json": + c.JSON(http.StatusOK, deck.Background) + break + + default: + c.Status(http.StatusUnsupportedMediaType) + } + return +} + +func restPutDeckBackground(c *gin.Context) { + // swagger:route GET /deck/background putDeckBackground + // + // Set the active deck background. + // + // Accepts the file in a multipart/form-data property named "background". + // + // Responses: + // 200: + // 404: ApiError + // 500: ApiError + upload, err := c.FormFile("background") + if err != nil { + restAbortWithErrorStatus(http.StatusBadRequest, err, c) + return + } + file, err := upload.Open() + if err != nil { + restAbortWithErrorStatus(http.StatusBadRequest, err, c) + return + } + image, _, err := image.Decode(file) + if err != nil { + restAbortWithErrorStatus(http.StatusBadRequest, err, c) + return + } + + if err != nil { + restAbortWithErrorStatus(http.StatusBadRequest, err, c) + return + } + err = deck.replaceBackground(image) + if err != nil { + restAbortWithErrorStatus(http.StatusBadRequest, err, c) + return + } + c.JSON(http.StatusOK, nil) +} + +func restGetDeckConfig(c *gin.Context) { + // swagger:route GET /deck/config getDeckConfig + // + // Gets the deck config. + // + // Outputs the entire configuration for the displayed deck. + // + // Responses: + // 200: DeckConfig + c.JSON(http.StatusOK, deck.Config) +} + +func restPostDeckConfig(c *gin.Context) { + // swagger:route POST /deck/config postDeckConfig + // + // Sets the deck config. + // + // Overwrites the entire configuration for the displayed deck. + // Parameters: + // + name: body + // in: body + // type: DeckConfig + // + // Responses: + // 200: DeckConfig + restHandleDeckConfigRequest(c, false) +} + +func restPutDeckConfig(c *gin.Context) { + // swagger:route PUT /deck/config putDeckConfig + // + // Updates the deck config. + // + // Merges in changes to the configuration for the displayed deck. + // + // Parameters: + // + name: body + // in: body + // type: DeckConfig + // + // Responses: + // 200: DeckConfig + restHandleDeckConfigRequest(c, true) +} + +// Handle a request to update the deck config +// The merge parameter decides if to merge the incoming config into the existing +// config, else replace it completely. +func restHandleDeckConfigRequest(c *gin.Context, merge bool) { + var newConfig DeckConfig + err := c.BindJSON(&newConfig) + if err != nil { + restAbortWithError(err, c) + return + } + if newConfig.Parent != "" { + parentConfig, err := LoadConfig(newConfig.Parent) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, apiError{Message: "The parent config could not be loaded.", Object: err}) + return + } + newConfig = MergeDeckConfig(&newConfig, &parentConfig) + } + if merge { + newConfig = MergeDeckConfig(&newConfig, &deck.Config) + } + if err = setDeckConfig(newConfig, deck); err != nil { + restAbortWithError(err, c) + return + } + deck.updateWidgets() + c.JSON(http.StatusOK, deck.Config) +} + +func restGetWidget(c *gin.Context) { + // swagger:route GET /deck/widgets/{id} getWidget + // + // Gets the state of a single widget. + // + // Parameters: + // + name: id + // in: path + // description: A 0-based key index (left to right, top to bottom) or the unique name of a key + // required: true + // type: string + // + // + // Responses: + // 200: WidgetState + // 404: ApiError + // 500: ApiError + index, err := apiGetKeyIndex(c.Param("id")) + if err != nil { + restAbortWithErrorStatus(http.StatusNotFound, err, c) + return + } + for _, w := range deck.Widgets { + if int(w.Key()) == index { + c.JSON(http.StatusOK, apiConvertWidget(w)) + return + } + } + c.AbortWithStatusJSON(http.StatusInternalServerError, apiError{Message: "Widget not found."}) +} + +func restGetWidgetButtonIcon(c *gin.Context) { + // swagger:route GET /deck/widgets/{id}/icon getWidgetButtonIcon + // + // Get the active icon of a button widget. + // + // Set the Accepts header to get a PNG or the native representation as JSON. + // + // Parameters: + // + name: id + // in: path + // description: A 0-based key index (left to right, top to bottom) or the unique name of a key + // required: true + // type: string + // + // Produces: + // - application/json + // - image/png + // + // Responses: + // 200: image.Image + // 404: ApiError + // 500: ApiError + index, err := apiGetKeyIndex(c.Param("id")) + if err != nil { + restAbortWithErrorStatus(http.StatusNotFound, err, c) + return + } + for _, w := range deck.Widgets { + if int(w.Key()) == index { + button, ok := w.(*ButtonWidget) + if ok { + switch c.Request.Header.Get("Accept") { + case "image/png": + c.Status(200) + png.Encode(c.Writer, button.icon) + break + + case "application/json": + c.JSON(http.StatusOK, button.icon) + break + + default: + c.Status(http.StatusUnsupportedMediaType) + } + return + } else { + c.AbortWithStatusJSON(http.StatusBadRequest, apiError{Message: "Widget is not a button with icon"}) + return + } + } + } + c.AbortWithStatusJSON(http.StatusInternalServerError, apiError{Message: "Widget not found."}) +} + +func restPutWidgetButtonIcon(c *gin.Context) { + // swagger:route PUT /deck/widgets/{id}/icon putWidgetButtonIcon + // + // Replace the active icon of a button widget. + // + // Accepts the file in a multipart/form-data property named "icon". + // + // sParameters: + // + name: id + // in: path + // description: A 0-based key index (left to right, top to bottom) or the unique name of a key + // required: true + // type: string + // + // Responses: + // 200: + // 404: ApiError + // 500: ApiError + index, err := apiGetKeyIndex(c.Param("id")) + if err != nil { + restAbortWithErrorStatus(http.StatusNotFound, err, c) + return + } + for _, w := range deck.Widgets { + if int(w.Key()) == index { + button, ok := w.(*ButtonWidget) + if ok { + upload, err := c.FormFile("icon") + if err != nil { + restAbortWithErrorStatus(http.StatusBadRequest, err, c) + return + } + file, err := upload.Open() + if err != nil { + restAbortWithErrorStatus(http.StatusBadRequest, err, c) + return + } + image, _, err := image.Decode(file) + if err != nil { + restAbortWithErrorStatus(http.StatusBadRequest, err, c) + return + } + + if err != nil { + restAbortWithErrorStatus(http.StatusBadRequest, err, c) + return + } + button.SetImage(image) + button.Update() + c.JSON(http.StatusOK, nil) + return + } else { + c.AbortWithStatusJSON(http.StatusBadRequest, apiError{Message: "Widget is not a button with icon"}) + return + } + } + } + c.AbortWithStatusJSON(http.StatusInternalServerError, apiError{Message: "Widget not found."}) +} + +func restGetKeyConfig(c *gin.Context) { + // swagger:route GET /deck/keys/{id}/config getKeyConfig + // + // Gets the config for a single key. + // + // Parameters: + // + name: id + // in: path + // description: A 0-based key index (left to right, top to bottom) or the unique name of a key + // required: true + // type: string + // + // Responses: + // 200: KeyConfig + // 404: ApiError + // 500: ApiError + indexParam, err := apiGetKeyIndex(c.Param("id")) + if err != nil { + restAbortWithErrorStatus(http.StatusNotFound, err, c) + return + } + index := uint8(indexParam) + for _, config := range deck.Config.Keys { + if config.Index == index { + c.JSON(http.StatusOK, config) + return + } + } + restAbortWithErrorStatus(http.StatusInternalServerError, errors.New("Widget not found."), c) +} + +// Transforms a key index string or key name to a widget index. +func apiGetKeyIndex(id string) (int, error) { + indexParam, err := strconv.Atoi(id) + if err != nil { + for _, k := range deck.Config.Keys { + if k.Name != "" && k.Name == id { + return int(k.Index), nil + } + } + return -1, fmt.Errorf("Key name '%s' not found.", id) + } + if indexParam < 0 || indexParam > len(deck.Widgets)-1 { + return -1, fmt.Errorf("Key id '%s' is out of range.", id) + } + return indexParam, nil +} + +func restPutKeyConfig(c *gin.Context) { + // swagger:route PUT /deck/keys/{id}/config putKeyConfig + // + // Updates the config for a single key. + // + // Parameters: + // + name: id + // in: path + // description: A 0-based key index (left to right, top to bottom) or the unique name of a key + // required: true + // type: string + // + // Parameters: + // + name: body + // in: body + // type: KeyConfig + // + // Responses: + // 200: KeyConfig + // 404: ApiError + // 500: ApiError + restHandleWidgetConfigRequest(c, true) +} + +func restPostWidgetConfig(c *gin.Context) { + // swagger:route POST /deck/keys/{id}/config postKeyConfig + // + // Sets the config for a single key. + // + // Parameters: + // + name: id + // in: path + // description: A 0-based key index (left to right, top to bottom) or the unique name of a key + // required: true + // type: string + // + // Parameters: + // + name: body + // in: body + // type: KeyConfig + // + // Responses: + // 200: KeyConfig + // 404: ApiError + // 500: ApiError + restHandleWidgetConfigRequest(c, false) +} + +func restHandleWidgetConfigRequest(c *gin.Context, merge bool) { + indexParam, err := apiGetKeyIndex(c.Param("id")) + if err != nil { + restAbortWithErrorStatus(http.StatusNotFound, err, c) + return + } + index := uint8(indexParam) + // Default to a new empty widget configuration. + config := KeyConfig{Index: index} + names := map[string]uint8{} + configIndex := -1 + // Collect widget names to check for duplicates and map to configs. + for i, c := range deck.Config.Keys { + if c.Name != "" { + names[c.Name] = c.Index + } + // Find existing configuration to merge into, if allowed. + if merge && c.Index == index { + config = c + configIndex = i + } + } + // Merge the request config into a copy of the original config. + configCopy, _ := json.Marshal(config) + var newConfig KeyConfig + json.Unmarshal(configCopy, &newConfig) + err = c.BindJSON(&newConfig) + if err != nil || newConfig.Index != index { + restAbortWithError(errors.New("The index can not be changed."), c) + return + } + if newConfig.Name != "" { + i, ok := names[newConfig.Name] + if ok && i != newConfig.Index { + restAbortWithError(fmt.Errorf("The name '%s' is already taken.", newConfig.Name), c) + return + } + } + // Replace existing config and recreate the widget. + var w Widget + w, err = LoadWidget(deck, index, newConfig) + if err != nil { + restAbortWithErrorStatus(http.StatusBadRequest, err, c) + return + } + if configIndex == -1 { + deck.Config.Keys = append(deck.Config.Keys, newConfig) + } else { + deck.Config.Keys[configIndex] = newConfig + } + deck.Widgets[index] = w + // Reload the widget. + updateMutex.Lock() + w.Update() + updateMutex.Unlock() + c.JSON(http.StatusOK, newConfig) +} + +func restDeleteKeyConfig(c *gin.Context) { + // swagger:route DELETE /deck/keys/{id}/config deleteKeyConfig + // + // Deletes the config for a single key. + // + // Parameters: + // + name: id + // in: path + // description: A 0-based key index (left to right, top to bottom) or the unique name of a key + // required: true + // type: string + // + // Responses: + // 204: + // 404: ApiError + indexParam, err := apiGetKeyIndex(c.Param("id")) + if err != nil { + restAbortWithErrorStatus(http.StatusNotFound, err, c) + return + } + index := uint8(indexParam) + configIndex := -1 + // Map to configs. + for i, c := range deck.Config.Keys { + // Find existing configuration to merge into, if allowed. + if c.Index == index { + configIndex = i + break + } + } + var w Widget + w, err = LoadWidget(deck, index, KeyConfig{}) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, apiError{Message: "Unable to load widgets.", Object: err}) + return + } + if configIndex != -1 { + deck.Config.Keys = append(deck.Config.Keys[:configIndex], deck.Config.Keys[configIndex+1:]...) + } + deck.Widgets[index] = w + c.JSON(http.StatusNoContent, nil) +} + +// Convert a Widget into its API representation, including a type name. +func apiConvertWidget(w Widget) apiWidget { + return apiWidget{ + Type: strings.Trim(reflect.TypeOf(w).String(), "*"), + Action: w.Action(), + ActionHold: w.ActionHold(), + State: w, + } +} + +func restPostDeviceWake(c *gin.Context) { + // swagger:route POST /device/wake wakeDevice + // + // Wake the device if asleep. + // + // Responses: + // 200: + if deck.dev.Asleep() { + err := deck.dev.Wake() + if err != nil { + restAbortWithErrorStatus(http.StatusInternalServerError, err, c) + return + } + } + c.Status(http.StatusOK) +} + +func restPostDeviceSleep(c *gin.Context) { + // swagger:route POST /device/sleep wakeDevice + // + // Put the device to sleep if awake. + // + // Responses: + // 200: + if !deck.dev.Asleep() { + err := deck.dev.Sleep() + if err != nil { + restAbortWithErrorStatus(http.StatusInternalServerError, err, c) + return + } + } + c.Status(http.StatusOK) +} + +// Helper to quickly generate a BadRequest response from an error. +func restAbortWithError(err error, c *gin.Context) { + c.AbortWithStatusJSON(http.StatusBadRequest, apiError{Object: err}) +} + +// Helper to quickly generate a specific response code from an error. +func restAbortWithErrorStatus(code int, err error, c *gin.Context) { + c.AbortWithStatusJSON(code, apiError{Object: err}) +} + +// Initialize the REST API. +func initRestApi(listenAddress string, trustedProxies string, enabledDocs bool) *nbhttp.Server { + router := gin.New() + router.Use(gin.Logger()) + router.Use(gin.Recovery()) + + if len(trustedProxies) > 0 { + router.SetTrustedProxies(strings.Split(trustedProxies, ",")) + } + + // Configure the CORS middleware. + cors, err := fcors.AllowAccess( + fcors.FromAnyOrigin(), + fcors.WithMethods( + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodDelete, + ), + fcors.WithRequestHeaders( + "Content-Type", + ), + fcors.MaxAgeInSeconds(86400), + ) + if err != nil { + log.Fatal(err) + } + + // Apply the CORS middleware to the engine. + router.Use(adapter.Wrap(cors)) + + v1 := router.Group("/v1") + { + v1.GET("/device", restGetDevice) + v1.PUT("/device", restPutDevice) + v1.POST("/device/wake", restPostDeviceWake) + v1.POST("/device/sleep", restPostDeviceSleep) + v1.GET("/deck/background", restGetDeckBackground) + v1.PUT("/deck/background", restPutDeckBackground) + v1.GET("/deck/widgets/:id", restGetWidget) + v1.GET("/deck/widgets/:id/icon", restGetWidgetButtonIcon) + v1.PUT("/deck/widgets/:id/icon", restPutWidgetButtonIcon) + v1.GET("/deck", restGetDeck) + v1.PUT("/deck", restPutDeck) + v1.GET("/deck/config", restGetDeckConfig) + v1.PUT("/deck/config", restPutDeckConfig) + v1.POST("/deck/config", restPostDeckConfig) + v1.GET("/deck/keys/:id/config", restGetKeyConfig) + v1.PUT("/deck/keys/:id/config", restPutKeyConfig) + v1.POST("/deck/keys/:id/config", restPostWidgetConfig) + v1.DELETE("/deck/keys/:id/config", restDeleteKeyConfig) + } + + if enabledDocs { + verbosef("Documentation served on /docs.") + router.StaticFile("/swagger.json", "./swagger.json") + router.GET("/docs", gin.WrapH(middleware.SwaggerUI(middleware.SwaggerUIOpts{ + BasePath: "/", + SpecURL: "/swagger.json", + }, http.NotFoundHandler()))) + } + + return nbhttp.NewServer(nbhttp.Config{ + Network: "tcp", + Addrs: []string{listenAddress}, + }, router, nil, nil) +} diff --git a/config.go b/config.go index e21add0..8041f51 100644 --- a/config.go +++ b/config.go @@ -17,45 +17,65 @@ import ( // DBusConfig describes a dbus action. type DBusConfig struct { - Object string `toml:"object,omitempty"` - Path string `toml:"path,omitempty"` - Method string `toml:"method,omitempty"` - Value string `toml:"value,omitempty"` + Object string `toml:"object,omitempty" json:"object,omitempty"` + Path string `toml:"path,omitempty" json:"path,omitempty"` + Method string `toml:"method,omitempty" json:"method,omitempty"` + Value string `toml:"value,omitempty" json:"value,omitempty"` } // ActionConfig describes an action that can be triggered. type ActionConfig struct { - Deck string `toml:"deck,omitempty"` - Keycode string `toml:"keycode,omitempty"` - Exec string `toml:"exec,omitempty"` - Paste string `toml:"paste,omitempty"` - Device string `toml:"device,omitempty"` - DBus DBusConfig `toml:"dbus,omitempty"` + // The name of a deck file to load. + Deck string `toml:"deck,omitempty" json:"deck,omitempty"` + // A keycode to send. + Keycode string `toml:"keycode,omitempty" json:"keycode,omitempty"` + // A command to execute. + Exec string `toml:"exec,omitempty" json:"exec,omitempty"` + // A string to paste. + Paste string `toml:"paste,omitempty" json:"paste,omitempty"` + // A device command execute, like "sleep". + Device string `toml:"device,omitempty" json:"device,omitempty"` + // A DBus command to execute. + DBus *DBusConfig `toml:"dbus,omitempty" json:"dbus,omitempty"` } // WidgetConfig describes configuration data for widgets. type WidgetConfig struct { - ID string `toml:"id,omitempty"` - Interval uint `toml:"interval,omitempty"` - Config map[string]interface{} `toml:"config,omitempty"` + // The type of widget to use for the key. + ID string `toml:"id,omitempty" json:"id,omitempty"` + // The widget's update interval in human readable format like "1s". + Interval uint `toml:"interval,omitempty" json:"interval,omitempty"` + // The widget specific configuration. + Config map[string]interface{} `toml:"config,omitempty" json:"config,omitempty"` } // KeyConfig holds the entire configuration for a single key. type KeyConfig struct { - Index uint8 `toml:"index"` - Widget WidgetConfig `toml:"widget"` - Action *ActionConfig `toml:"action,omitempty"` - ActionHold *ActionConfig `toml:"action_hold,omitempty"` + // They key index to configure. + Index uint8 `toml:"index" json:"index"` + // An identifying name for the key, unique per deck. + Name string `tomk:"name,omitempty" json:"name,omitempty"` + // The widget configuration. + Widget WidgetConfig `toml:"widget" json:"widget"` + // An action to perform when the key is pressed and released. + Action *ActionConfig `toml:"action,omitempty" json:"action,omitempty"` + // An action to perform when the key is held. + ActionHold *ActionConfig `toml:"action_hold,omitempty" json:"action_hold,omitempty"` } // Keys is a slice of keys. type Keys []KeyConfig // DeckConfig is the central configuration struct. +// +// swagger:model type DeckConfig struct { - Background string `toml:"background,omitempty"` - Parent string `toml:"parent,omitempty"` - Keys Keys `toml:"keys"` + // The deck background image. + Background string `toml:"background,omitempty" json:"background,omitempty"` + // A parent deck the configuration overrides. + Parent string `toml:"parent,omitempty" json:"parent,omitempty"` + // Configuration for individual keys + Keys Keys `toml:"keys" json:"keys"` } // MergeDeckConfig merges key configuration from multiple configs. diff --git a/deck.go b/deck.go index a260afb..bf7b323 100644 --- a/deck.go +++ b/deck.go @@ -20,6 +20,8 @@ import ( // Deck is a set of widgets. type Deck struct { File string + dev *streamdeck.Device + Config DeckConfig Background image.Image Widgets []Widget } @@ -38,40 +40,71 @@ func LoadDeck(dev *streamdeck.Device, base string, deck string) (*Deck, error) { } d := Deck{ - File: path, + File: path, + dev: dev, + Config: dc, } + err = setDeckConfig(dc, &d) + if err != nil { + return nil, err + } + return &d, nil +} + +func setDeckConfig(dc DeckConfig, d *Deck) error { if dc.Background != "" { - bgpath, err := expandPath(filepath.Dir(path), dc.Background) + bgpath, err := expandPath(filepath.Dir(d.File), dc.Background) if err != nil { - return nil, err + return err } - if err := d.loadBackground(dev, bgpath); err != nil { - return nil, err + if err := d.loadBackground(d.dev, bgpath); err != nil { + return err } } + d.Config = dc + return LoadWidgets(d) +} +func LoadWidgets(deck *Deck) error { + deck.Widgets = []Widget{} keyMap := map[uint8]KeyConfig{} - for _, k := range dc.Keys { + names := map[string]bool{} + for _, k := range deck.Config.Keys { + if k.Name != "" { + if names[k.Name] { + return fmt.Errorf("duplicate widgets with the name '%s'", k.Name) + } + names[k.Name] = true + } keyMap[k.Index] = k } - - for i := uint8(0); i < dev.Keys; i++ { - bg := d.backgroundForKey(dev, i) - - var w Widget - if k, found := keyMap[i]; found { - w, err = NewWidget(dev, filepath.Dir(path), k, bg) - if err != nil { - return nil, err - } - } else { - w = NewBaseWidget(dev, filepath.Dir(path), i, nil, nil, bg) + for i := uint8(0); i < deck.dev.Keys; i++ { + w, err := LoadWidget(deck, i, keyMap[i]) + if err != nil { + return err } + deck.Widgets = append(deck.Widgets, w) + } + return nil +} - d.Widgets = append(d.Widgets, w) +func LoadWidget(deck *Deck, i uint8, keyConfig KeyConfig) (Widget, error) { + var err error + bg := deck.backgroundForKey(deck.dev, i) + var w Widget + if (keyConfig.Index == i) && keyConfig.Widget.ID != "" { + w, err = NewWidget(deck.dev, filepath.Dir(deck.File), keyConfig, bg) + if err != nil { + return nil, err + } + } else { + w = NewBaseWidget(deck.dev, filepath.Dir(deck.File), i, nil, nil, bg) } + return w, nil +} - return &d, nil +func (d *Deck) GetDevice() *streamdeck.Device { + return d.dev } // loads a background image. @@ -87,6 +120,17 @@ func (d *Deck) loadBackground(dev *streamdeck.Device, bg string) error { return err } + err = d.validateBackground(background) + if err != nil { + return err + } + + d.Background = background + return nil +} + +func (deck *Deck) validateBackground(background image.Image) error { + dev := deck.dev rows := int(dev.Rows) cols := int(dev.Columns) padding := int(dev.Padding) @@ -98,8 +142,6 @@ func (d *Deck) loadBackground(dev *streamdeck.Device, bg string) error { background.Bounds().Dy() != height { return fmt.Errorf("supplied background image has wrong dimensions, expected %dx%d pixels", width, height) } - - d.Background = background return nil } @@ -118,6 +160,22 @@ func (d Deck) backgroundForKey(dev *streamdeck.Device, key uint8) image.Image { return bg } +func (deck *Deck) ValidateWidgetBackground(background image.Image) error { + dev := deck.dev + rows := int(dev.Rows) + cols := int(dev.Columns) + padding := int(dev.Padding) + pixels := int(dev.Pixels) + + width := cols*pixels + (cols-1)*padding + height := rows*pixels + (rows-1)*padding + if background.Bounds().Dx() != width || + background.Bounds().Dy() != height { + return fmt.Errorf("supplied widget image has wrong dimensions, expected %dx%d pixels", width, height) + } + return nil +} + // handles keypress with delay. func emulateKeyPressWithDelay(keys string) { kd := strings.Split(keys, "+") @@ -178,6 +236,7 @@ func emulateClipboard(text string) { func executeDBusMethod(object, path, method, args string) { call := dbusConn.Object(object, dbus.ObjectPath(path)).Call(method, 0, args) if call.Err != nil { + fmt.Fprintf(os.Stderr, "dbus call failed: %s\n", call.Err) } } @@ -244,7 +303,7 @@ func (d *Deck) triggerAction(dev *streamdeck.Device, index uint8, hold bool) { if a.Paste != "" { emulateClipboard(a.Paste) } - if a.DBus.Method != "" { + if a.DBus != nil && a.DBus.Method != "" { executeDBusMethod(a.DBus.Object, a.DBus.Path, a.DBus.Method, a.DBus.Value) } if a.Exec != "" { @@ -269,6 +328,7 @@ func (d *Deck) triggerAction(dev *streamdeck.Device, index uint8, hold bool) { // updateWidgets updates/repaints all the widgets. func (d *Deck) updateWidgets() { + updateMutex.Lock() for _, w := range d.Widgets { if !w.RequiresUpdate() { continue @@ -279,6 +339,26 @@ func (d *Deck) updateWidgets() { fatalf("error: %v", err) } } + updateMutex.Unlock() +} + +func (d *Deck) replaceBackground(image image.Image) error { + updateMutex.Lock() + err := d.validateBackground(image) + if err != nil { + updateMutex.Unlock() + return err + } + deck.Background = image + for _, w := range deck.Widgets { + w.reloadBackground() + // fmt.Println("Repaint", w.Key()) + if err := w.Update(); err != nil { + fatalf("error: %v", err) + } + } + updateMutex.Unlock() + return nil } // adjustBrightness adjusts the brightness. diff --git a/go.mod b/go.mod index bf198a4..b6a9925 100644 --- a/go.mod +++ b/go.mod @@ -7,28 +7,82 @@ require ( github.com/atotto/clipboard v0.1.4 github.com/bendahl/uinput v1.6.1 github.com/flopp/go-findfont v0.1.0 + github.com/g8rswimmer/error-chain v1.0.0 + github.com/gin-gonic/gin v1.9.1 + github.com/go-openapi/loads v0.21.2 + github.com/go-openapi/runtime v0.26.0 github.com/godbus/dbus v4.1.0+incompatible github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 + github.com/gwatts/gin-adapter v1.0.0 github.com/jezek/xgb v1.1.0 github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66 + github.com/jub0bs/fcors v0.5.1 + github.com/lesismal/nbio v1.3.17 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/mitchellh/go-homedir v1.1.0 github.com/muesli/streamdeck v0.4.0 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 + github.com/santosh/gingo v0.0.0-20221207111602-0ef9ded9b180 github.com/shirou/gopsutil v3.21.11+incompatible + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 golang.org/x/image v0.7.0 + golang.org/x/sync v0.1.0 ) require ( github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-openapi/analysis v0.21.4 // indirect + github.com/go-openapi/errors v0.20.3 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/spec v0.20.8 // indirect + github.com/go-openapi/strfmt v0.21.7 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-openapi/validate v0.22.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/karalabe/hid v1.0.1-0.20190806082151-9c14560f9ee8 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/lesismal/llib v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.2.2 // indirect + github.com/stretchr/testify v1.8.3 // indirect + github.com/swaggo/swag v1.8.12 // indirect github.com/tklauser/go-sysconf v0.3.9 // indirect github.com/tklauser/numcpus v0.3.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect - golang.org/x/sys v0.5.0 // indirect + go.mongodb.org/mongo-driver v1.11.3 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.11.0 // indirect + golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect + golang.org/x/net v0.12.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/text v0.11.0 // indirect + golang.org/x/tools v0.7.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7bf1e95..39009bc 100644 --- a/go.sum +++ b/go.sum @@ -2,90 +2,372 @@ github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJ github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ= github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g= github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/bendahl/uinput v1.6.1 h1:A8b6mtqC3E7ZkpLdQWNeyZNmLPhqxGS+fScrim3TV/k= github.com/bendahl/uinput v1.6.1/go.mod h1:Np7w3DINc9wB83p12fTAM3DPPhFnAKP0WTXRqCQJ6Z8= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/flopp/go-findfont v0.1.0 h1:lPn0BymDUtJo+ZkV01VS3661HL6F4qFlkhcJN55u6mU= github.com/flopp/go-findfont v0.1.0/go.mod h1:wKKxRDjD024Rh7VMwoU90i6ikQRCr+JTHB5n4Ejkqvw= +github.com/g8rswimmer/error-chain v1.0.0 h1:WnwnunlvqtGPHVHmBfbmUyAgrtag8Y6nNpwLWmtSYOQ= +github.com/g8rswimmer/error-chain v1.0.0/go.mod h1:XPJ/brUsL7yzc5VRlIxtf9GvoUqnOKVI9fg2MB5DWx8= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= +github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= +github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= +github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8= +github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.3 h1:rz6kiC84sqNQoqrtulzaL/VERgkoCyB6WdEkc2ujzUc= +github.com/go-openapi/errors v0.20.3/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/loads v0.21.1/go.mod h1:/DtAMXXneXFjbQMGEtbamCZb+4x7eGwkvZCvBmwUG+g= +github.com/go-openapi/loads v0.21.2 h1:r2a/xFIYeZ4Qd2TnGpWDIQNcP80dIaZgf704za8enro= +github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw= +github.com/go-openapi/runtime v0.26.0 h1:HYOFtG00FM1UvqrcxbEJg/SwvDRvYLQKGhw2zaQjTcc= +github.com/go-openapi/runtime v0.26.0/go.mod h1:QgRGeZwrUcSHdeh4Ka9Glvo0ug1LC5WyE+EV88plZrQ= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/spec v0.20.8 h1:ubHmXNY3FCIOinT8RNrrPfGc9t7I1qhPtdOGoG2AxRU= +github.com/go-openapi/spec v0.20.8/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg= +github.com/go-openapi/strfmt v0.21.1/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= +github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o= +github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= +github.com/go-openapi/strfmt v0.21.7 h1:rspiXgNWgeUzhjo1YU01do6qsahtJNByjLVbPLNHb8k= +github.com/go-openapi/strfmt v0.21.7/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/validate v0.22.1 h1:G+c2ub6q47kfX1sOBLwIQwzBVt8qmOAARyo/9Fqs9NU= +github.com/go-openapi/validate v0.22.1/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= +github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= +github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= +github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= +github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= +github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= +github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= +github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= +github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= +github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= +github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= +github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= +github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= +github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= +github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= 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/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gwatts/gin-adapter v1.0.0 h1:TsmmhYTR79/RMTsfYJ2IQvI1F5KZ3ZFJxuQSYEOpyIA= +github.com/gwatts/gin-adapter v1.0.0/go.mod h1:44AEV+938HsS0mjfXtBDCUZS9vONlF2gwvh8wu4sRYc= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk= github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66 h1:+wPhoJD8EH0/bXipIq8Lc2z477jfox9zkXPCJdhvHj8= github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66/go.mod h1:KACeV+k6b+aoLTVrrurywEbu3UpqoQcQywj4qX8aQKM= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jub0bs/fcors v0.5.1 h1:KWM904a2zZmKOQWtJi4O5J9X0v49DquliQmdHb57ALY= +github.com/jub0bs/fcors v0.5.1/go.mod h1:EiHVYaJkAmzMKJcIW78xUB87f0PleIWTRW3dBH392nE= github.com/karalabe/hid v1.0.1-0.20190806082151-9c14560f9ee8 h1:AP5krei6PpUCFOp20TSmxUS4YLoLvASBcArJqM/V+DY= github.com/karalabe/hid v1.0.1-0.20190806082151-9c14560f9ee8/go.mod h1:Vr51f8rUOLYrfrWDFlV12GGQgM5AT8sVh+2fY4MPeu8= +github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lesismal/llib v1.1.12 h1:KJFB8bL02V+QGIvILEw/w7s6bKj9Ps9Px97MZP2EOk0= +github.com/lesismal/llib v1.1.12/go.mod h1:70tFXXe7P1FZ02AU9l8LgSOK7d7sRrpnkUr3rd3gKSg= +github.com/lesismal/nbio v1.3.17 h1:rsDwVdRfFK/QcFckDBgdGIZTf6Y98XWHRQWDY+EldO8= +github.com/lesismal/nbio v1.3.17/go.mod h1:KWlouFT5cgDdW5sMX8RsHASUMGniea9X0XIellZ0B38= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/muesli/coral v1.0.0/go.mod h1:bf91M/dkp7iHQw73HOoR9PekdTJMTD6ihJgWoDitde8= github.com/muesli/streamdeck v0.4.0 h1:kBV1RCLFz3dYfVlBvCPG0cWxTFK7IOdcc1Jw2T4Qz4E= github.com/muesli/streamdeck v0.4.0/go.mod h1:6Fjt/9so3B22BtraQLRTPHu33c7yVgUIcDPiZqzSHfE= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/santosh/gingo v0.0.0-20221207111602-0ef9ded9b180 h1:houhKtoTI0PBSiaSVCkIyavSo4sxf3FHo3HzQZAhJuc= +github.com/santosh/gingo v0.0.0-20221207111602-0ef9ded9b180/go.mod h1:vcljLtYRV+geYpIbqcw+yxDF8WgUNBg8queTd6Bm5mo= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w= +github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tklauser/go-sysconf v0.3.9 h1:JeUVdAOWhhxVcU6Eqr/ATFHgXk/mmiItdKeJPev3vTo= github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ= github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= +go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= +go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg= +go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= +go.mongodb.org/mongo-driver v1.11.3 h1:Ql6K6qYHEzB6xvu4+AU0BoRoqf9vFPcc4o7MUIdPW8Y= +go.mongodb.org/mongo-driver v1.11.3/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210513122933-cd7d49e622d5/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw= golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20210816074244-15123e1e1f71/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-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.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/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 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.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 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/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= 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/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/main.go b/main.go index c808619..477fea0 100644 --- a/main.go +++ b/main.go @@ -28,23 +28,30 @@ var ( deck *Deck - dbusConn *dbus.Conn - keyboard uinput.Keyboard - shutdown = make(chan error) + dbusConn *dbus.Conn + keyboard uinput.Keyboard + shutdown = make(chan error) + updateMutex sync.Mutex + // Would not need this if there was a streamdeck.GetSleepTimeout() + sleepTimeout time.Duration = 0 + // Would not need this if there was a streamdeck.GetFadeDuration() + fadeDuration time.Duration = 250 * time.Millisecond xorg *Xorg recentWindows []Window - deckFile = flag.String("deck", "main.deck", "path to deck config file") - device = flag.String("device", "", "which device to use (serial number)") - brightness = flag.Uint("brightness", 80, "brightness in percent") - sleep = flag.String("sleep", "", "sleep timeout") - verbose = flag.Bool("verbose", false, "verbose output") - version = flag.Bool("version", false, "display version") + deckFile = flag.String("deck", "main.deck", "path to deck config file") + device = flag.String("device", "", "which device to use (serial number)") + brightness = flag.Uint("brightness", 80, "brightness in percent") + sleep = flag.String("sleep", "", "sleep timeout") + restListen = flag.String("rest-listen", "", "REST [ip]:[port] to listen to") + restTrustedProxies = flag.String("rest-trusted-proxies", "", "A comma-separated list of reverse proxies to trust, accepts IPv4 addresses, IPv4 CIDRs, IPv6 addresses or IPv6 CIDRs") + restDocs = flag.Bool("rest-docs", false, "Enable Swagger based /docs endpoint") + verbose = flag.Bool("verbose", false, "verbose output") + version = flag.Bool("version", false, "display version") ) const ( - fadeDuration = 250 * time.Millisecond longPressDuration = 350 * time.Millisecond ) @@ -232,7 +239,7 @@ func initDevice() (*streamdeck.Device, error) { if err != nil { return &dev, err } - + sleepTimeout = timeout dev.SetSleepTimeout(timeout) } @@ -282,6 +289,19 @@ func run() error { } deck.updateWidgets() + // Initialize REST server. + if len(*restListen) > 0 { + server := initRestApi(*restListen, *restTrustedProxies, *restDocs) + err = server.Start() + if err != nil { + fmt.Fprintf(os.Stderr, "Could not start REST server: %s\n", err) + } else { + defer server.Stop() + } + } else { + verbosef("REST server disabled.") + } + return eventLoop(dev, tch) } diff --git a/swagger-overrides.yml b/swagger-overrides.yml new file mode 100644 index 0000000..8161146 --- /dev/null +++ b/swagger-overrides.yml @@ -0,0 +1,23 @@ +paths: + /deck/background: + put: + operationId: putDeckBackground + parameters: + - description: Set the background of the active deck + in: formData + name: background + required: true + type: file + /deck/widgets/{id}/icon: + put: + operationId: putWidgetButtonIcon + parameters: + - description: Set the icon of a button widget. + in: formData + name: icon + required: true + type: file +definitions: + Widget: + type: object + title: "Widget specific state" \ No newline at end of file diff --git a/swagger.json b/swagger.json new file mode 100644 index 0000000..7c0a88a --- /dev/null +++ b/swagger.json @@ -0,0 +1,834 @@ +{ + "consumes": [ + "application/json" + ], + "produces": [ + "application/json", + "image/png" + ], + "schemes": [ + "http" + ], + "swagger": "2.0", + "info": { + "description": "Swagger file generated from source by the go-swagger package.\n\n`swagger generate spec -o swagger.json -i swagger-overrides.yml -m`\n\nhttps://goswagger.io/\nhttps://github.com/go-swagger/go-swagger/\n\nAllows controlling a single Stream Deck from anywhere using a REST-like API.\nThe device is modeled using four main concepts:\n\nThe device:\nHolds read-only information about the connected device itself, such as\nthe unique serial number, key layout, awake state, and brightness.\nSome properties can be changed to immediately update the device state.\n\nThe deck:\nHolds the configuration initially loaded from a deck file. Properties can\nbe completely or partially updated to reload the correspodning widget(s)\nwith the new confiuration. Only one deck is loaded at a time.\n\nA Key configuration:\nHolds the configuration for a single key. Updating it immediately reloads\nthe corresponding widget with the new configuration. A key is referenced\nusing its one-dimensional index number, or a string name which must be\nunique per deck. Some keys may not yet have a configuration.\n\nA widget:\nHolds the active widget state of a configured key. The widget state\ncannot be updated directly and contents vary by type of widget.", + "title": "Deckmaster API implements a REST API for controlling Stream Decks.", + "version": "1.0.0" + }, + "host": "localhost:4321", + "basePath": "/v1", + "paths": { + "/deck": { + "get": { + "summary": "Gets the complete deck state.", + "operationId": "getDeckState", + "responses": { + "200": { + "description": "DeckState", + "schema": { + "$ref": "#/definitions/DeckState" + } + } + } + }, + "put": { + "description": "Decks which have overwritten the file property can not be reloaded.", + "summary": "Reloads the current configration from the deck file on disk.", + "operationId": "putDeckState", + "responses": { + "200": { + "description": "DeckState", + "schema": { + "$ref": "#/definitions/DeckState" + } + }, + "500": { + "description": "ApiError", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + } + }, + "/deck/background": { + "get": { + "description": "Accepts the file in a multipart/form-data property named \"background\".", + "produces": [ + "application/json", + "image/png" + ], + "summary": "Set the active deck background.", + "operationId": "putDeckBackground", + "parameters": [ + { + "type": "file", + "description": "Set the background of the active deck", + "name": "background", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "" + }, + "404": { + "description": "ApiError", + "schema": { + "$ref": "#/definitions/ApiError" + } + }, + "500": { + "description": "ApiError", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + }, + "put": { + "description": "Accepts the file in a multipart/form-data property named \"background\".", + "produces": [ + "application/json", + "image/png" + ], + "summary": "Set the active deck background.", + "operationId": "putDeckBackground", + "parameters": [ + { + "type": "file", + "description": "Set the background of the active deck", + "name": "background", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "" + }, + "404": { + "description": "ApiError", + "schema": { + "$ref": "#/definitions/ApiError" + } + }, + "500": { + "description": "ApiError", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + } + }, + "/deck/config": { + "get": { + "description": "Outputs the entire configuration for the displayed deck.", + "summary": "Gets the deck config.", + "operationId": "getDeckConfig", + "responses": { + "200": { + "description": "DeckConfig", + "schema": { + "$ref": "#/definitions/DeckConfig" + } + } + } + }, + "put": { + "description": "Merges in changes to the configuration for the displayed deck.", + "summary": "Updates the deck config.", + "operationId": "putDeckConfig", + "parameters": [ + { + "name": "body", + "in": "body", + "schema": { + "type": "object", + "$ref": "#/definitions/DeckConfig" + } + } + ], + "responses": { + "200": { + "description": "DeckConfig", + "schema": { + "$ref": "#/definitions/DeckConfig" + } + } + } + }, + "post": { + "description": "Overwrites the entire configuration for the displayed deck.", + "summary": "Sets the deck config.", + "operationId": "postDeckConfig", + "parameters": [ + { + "name": "body", + "in": "body", + "schema": { + "type": "object", + "$ref": "#/definitions/DeckConfig" + } + } + ], + "responses": { + "200": { + "description": "DeckConfig", + "schema": { + "$ref": "#/definitions/DeckConfig" + } + } + } + } + }, + "/deck/keys/{id}/config": { + "get": { + "summary": "Gets the config for a single key.", + "operationId": "getKeyConfig", + "parameters": [ + { + "type": "string", + "description": "A 0-based key index (left to right, top to bottom) or the unique name of a key", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "KeyConfig", + "schema": { + "$ref": "#/definitions/KeyConfig" + } + }, + "404": { + "description": "ApiError", + "schema": { + "$ref": "#/definitions/ApiError" + } + }, + "500": { + "description": "ApiError", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + }, + "put": { + "summary": "Updates the config for a single key.", + "operationId": "putKeyConfig", + "parameters": [ + { + "type": "string", + "description": "A 0-based key index (left to right, top to bottom) or the unique name of a key", + "name": "id", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "type": "object", + "$ref": "#/definitions/KeyConfig" + } + } + ], + "responses": { + "200": { + "description": "KeyConfig", + "schema": { + "$ref": "#/definitions/KeyConfig" + } + }, + "404": { + "description": "ApiError", + "schema": { + "$ref": "#/definitions/ApiError" + } + }, + "500": { + "description": "ApiError", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + }, + "post": { + "summary": "Sets the config for a single key.", + "operationId": "postKeyConfig", + "parameters": [ + { + "type": "string", + "description": "A 0-based key index (left to right, top to bottom) or the unique name of a key", + "name": "id", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "type": "object", + "$ref": "#/definitions/KeyConfig" + } + } + ], + "responses": { + "200": { + "description": "KeyConfig", + "schema": { + "$ref": "#/definitions/KeyConfig" + } + }, + "404": { + "description": "ApiError", + "schema": { + "$ref": "#/definitions/ApiError" + } + }, + "500": { + "description": "ApiError", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + }, + "delete": { + "summary": "Deletes the config for a single key.", + "operationId": "deleteKeyConfig", + "parameters": [ + { + "type": "string", + "description": "A 0-based key index (left to right, top to bottom) or the unique name of a key", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "" + }, + "404": { + "description": "ApiError", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + } + }, + "/deck/widgets/{id}": { + "get": { + "summary": "Gets the state of a single widget.", + "operationId": "getWidget", + "parameters": [ + { + "type": "string", + "description": "A 0-based key index (left to right, top to bottom) or the unique name of a key", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "WidgetState", + "schema": { + "$ref": "#/definitions/WidgetState" + } + }, + "404": { + "description": "ApiError", + "schema": { + "$ref": "#/definitions/ApiError" + } + }, + "500": { + "description": "ApiError", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + } + }, + "/deck/widgets/{id}/icon": { + "get": { + "description": "Set the Accepts header to get a PNG or the native representation as JSON.", + "produces": [ + "application/json", + "image/png" + ], + "summary": "Get the active icon of a button widget.", + "operationId": "getWidgetButtonIcon", + "parameters": [ + { + "type": "string", + "description": "A 0-based key index (left to right, top to bottom) or the unique name of a key", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/image.Image" + }, + "404": { + "description": "ApiError", + "schema": { + "$ref": "#/definitions/ApiError" + } + }, + "500": { + "description": "ApiError", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + }, + "put": { + "description": "Accepts the file in a multipart/form-data property named \"icon\".", + "summary": "Replace the active icon of a button widget.", + "operationId": "putWidgetButtonIcon", + "parameters": [ + { + "type": "file", + "description": "Set the icon of a button widget.", + "name": "icon", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "A 0-based key index (left to right, top to bottom) or the unique name of a key", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "" + }, + "404": { + "description": "ApiError", + "schema": { + "$ref": "#/definitions/ApiError" + } + }, + "500": { + "description": "ApiError", + "schema": { + "$ref": "#/definitions/ApiError" + } + } + } + } + }, + "/device": { + "get": { + "summary": "Gets the complete device state.", + "operationId": "getDeviceState", + "responses": { + "200": { + "description": "DeviceStateRead", + "schema": { + "$ref": "#/definitions/DeviceStateRead" + } + } + } + }, + "put": { + "summary": "Updates the device state.", + "operationId": "putDeviceState", + "parameters": [ + { + "name": "body", + "in": "body", + "schema": { + "type": "object", + "$ref": "#/definitions/DeviceStateWrite" + } + } + ], + "responses": { + "200": { + "description": "DeviceStateRead", + "schema": { + "$ref": "#/definitions/DeviceStateRead" + } + } + } + } + }, + "/device/sleep": { + "post": { + "summary": "Put the device to sleep if awake.", + "operationId": "wakeDevice", + "responses": { + "200": { + "description": "" + } + } + } + }, + "/device/wake": { + "post": { + "summary": "Wake the device if asleep.", + "operationId": "wakeDevice", + "responses": { + "200": { + "description": "" + } + } + } + } + }, + "definitions": { + "ActionConfig": { + "type": "object", + "title": "ActionConfig describes an action that can be triggered.", + "properties": { + "dbus": { + "$ref": "#/definitions/DBusConfig" + }, + "deck": { + "description": "The name of a deck file to load.", + "type": "string", + "x-go-name": "Deck" + }, + "device": { + "description": "A device command execute, like \"sleep\".", + "type": "string", + "x-go-name": "Device" + }, + "exec": { + "description": "A command to execute.", + "type": "string", + "x-go-name": "Exec" + }, + "keycode": { + "description": "A keycode to send.", + "type": "string", + "x-go-name": "Keycode" + }, + "paste": { + "description": "A string to paste.", + "type": "string", + "x-go-name": "Paste" + } + }, + "x-go-package": "github.com/muesli/deckmaster" + }, + "ApiError": { + "type": "object", + "title": "An API error response.", + "properties": { + "description": { + "description": "An optional internal error string with more details.", + "type": "string", + "x-go-name": "Description" + }, + "details": { + "description": "An optional internal error object with more details.", + "type": "string", + "x-go-name": "Object" + }, + "error": { + "description": "An error message.", + "type": "string", + "x-go-name": "Message" + } + }, + "x-go-name": "apiError", + "x-go-package": "github.com/muesli/deckmaster" + }, + "DBusConfig": { + "type": "object", + "title": "DBusConfig describes a dbus action.", + "properties": { + "method": { + "type": "string", + "x-go-name": "Method" + }, + "object": { + "type": "string", + "x-go-name": "Object" + }, + "path": { + "type": "string", + "x-go-name": "Path" + }, + "value": { + "type": "string", + "x-go-name": "Value" + } + }, + "x-go-package": "github.com/muesli/deckmaster" + }, + "DeckConfig": { + "type": "object", + "title": "DeckConfig is the central configuration struct.", + "properties": { + "background": { + "description": "The deck background image.", + "type": "string", + "x-go-name": "Background" + }, + "keys": { + "$ref": "#/definitions/Keys" + }, + "parent": { + "description": "A parent deck the configuration overrides.", + "type": "string", + "x-go-name": "Parent" + } + }, + "x-go-package": "github.com/muesli/deckmaster" + }, + "DeckState": { + "type": "object", + "title": "API representation of an active deck.", + "properties": { + "background": { + "description": "The path to the loaded background image.", + "type": "string", + "x-go-name": "Background" + }, + "file": { + "description": "The path to the loaded deck file.", + "type": "string", + "x-go-name": "File" + }, + "widgets": { + "description": "The active widgets.", + "type": "array", + "items": { + "$ref": "#/definitions/WidgetState" + }, + "x-go-name": "Widgets" + } + }, + "x-go-name": "apiDeck", + "x-go-package": "github.com/muesli/deckmaster" + }, + "DeviceStateRead": { + "type": "object", + "title": "Read-only properties of a Stream Deck device.", + "properties": { + "asleep": { + "description": "Is the device asleep (screen off)?", + "type": "boolean", + "x-go-name": "Asleep" + }, + "brightness": { + "description": "Current brightness level (previous brightness if asleep).", + "type": "integer", + "format": "uint64", + "x-go-name": "Brightness" + }, + "columns": { + "description": "Number of key columns.", + "type": "integer", + "format": "uint8", + "x-go-name": "Columns" + }, + "dpi": { + "description": "Screen resolution in dots per inches.", + "type": "integer", + "format": "uint64", + "x-go-name": "DPI" + }, + "fadeDuration": { + "description": "Fade out time in human readable form like \"250ms\".", + "type": "string", + "x-go-name": "FadeDuration" + }, + "id": { + "description": "Platform-specific device path.", + "type": "string", + "x-go-name": "ID" + }, + "keys": { + "description": "Number of keys.", + "type": "integer", + "format": "uint8", + "x-go-name": "Keys" + }, + "padding": { + "description": "Padding around buttons in pixels.", + "type": "integer", + "format": "uint64", + "x-go-name": "Padding" + }, + "pixels": { + "description": "Number of screen pixels.", + "type": "integer", + "format": "uint64", + "x-go-name": "Pixels" + }, + "rows": { + "description": "Number of key rows.", + "type": "integer", + "format": "uint8", + "x-go-name": "Rows" + }, + "serial": { + "description": "Device serial number.", + "type": "string", + "x-go-name": "Serial" + }, + "sleepTimeout": { + "description": "Sleep timeout in human readable form like \"1m30s\".", + "type": "string", + "x-go-name": "SleepTimeout" + } + }, + "x-go-name": "jsonDeviceRead", + "x-go-package": "github.com/muesli/deckmaster" + }, + "DeviceStateWrite": { + "type": "object", + "title": "Writeable properties of a Stream Deck device.", + "properties": { + "asleep": { + "description": "Sleep toggle.", + "type": "boolean", + "x-go-name": "Asleep" + }, + "brightness": { + "description": "Brightness level.", + "type": "integer", + "format": "uint64", + "x-go-name": "Brightness" + }, + "fadeDuration": { + "description": "Fade out time in human readable form like \"250ms\".", + "type": "string", + "x-go-name": "FadeDuration" + }, + "sleepTimeout": { + "description": "Sleep timout in human readable form like \"1m30s\".", + "type": "string", + "x-go-name": "SleepTimeout" + } + }, + "x-go-name": "jsonDeviceWrite", + "x-go-package": "github.com/muesli/deckmaster" + }, + "KeyConfig": { + "type": "object", + "title": "KeyConfig holds the entire configuration for a single key.", + "properties": { + "action": { + "$ref": "#/definitions/ActionConfig" + }, + "action_hold": { + "$ref": "#/definitions/ActionConfig" + }, + "index": { + "description": "They key index to configure.", + "type": "integer", + "format": "uint8", + "x-go-name": "Index" + }, + "name": { + "description": "An identifying name for the key, unique per deck.", + "type": "string", + "x-go-name": "Name" + }, + "widget": { + "$ref": "#/definitions/WidgetConfig" + } + }, + "x-go-package": "github.com/muesli/deckmaster" + }, + "Keys": { + "type": "array", + "title": "Keys is a slice of keys.", + "items": { + "$ref": "#/definitions/KeyConfig" + }, + "x-go-package": "github.com/muesli/deckmaster" + }, + "Widget": { + "type": "object", + "title": "Widget is an interface implemented by all available widgets.", + "properties": { + "Action": { + "$ref": "#/definitions/ActionConfig" + }, + "ActionHold": { + "$ref": "#/definitions/ActionConfig" + }, + "Key": { + "type": "integer", + "format": "uint8" + }, + "RequiresUpdate": { + "type": "boolean" + }, + "Update": { + "type": "string" + } + }, + "x-go-package": "github.com/muesli/deckmaster" + }, + "WidgetConfig": { + "type": "object", + "title": "WidgetConfig describes configuration data for widgets.", + "properties": { + "config": { + "description": "The widget specific configuration.", + "type": "object", + "additionalProperties": {}, + "x-go-name": "Config" + }, + "id": { + "description": "The type of widget to use for the key.", + "type": "string", + "x-go-name": "ID" + }, + "interval": { + "description": "The widget's update interval in human readable format like \"1s\".", + "type": "integer", + "format": "uint64", + "x-go-name": "Interval" + } + }, + "x-go-package": "github.com/muesli/deckmaster" + }, + "WidgetState": { + "type": "object", + "title": "API representation of an active widget.", + "properties": { + "action": { + "$ref": "#/definitions/ActionConfig" + }, + "action_hold": { + "$ref": "#/definitions/ActionConfig" + }, + "state": { + "$ref": "#/definitions/Widget" + }, + "type": { + "description": "Type of widget used for the key.", + "type": "string", + "x-go-name": "Type" + } + }, + "x-go-name": "apiWidget", + "x-go-package": "github.com/muesli/deckmaster" + } + } +} \ No newline at end of file diff --git a/widget.go b/widget.go index 60764b1..f55c6ac 100644 --- a/widget.go +++ b/widget.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "fmt" "image" "image/color" @@ -25,6 +26,7 @@ type Widget interface { Key() uint8 RequiresUpdate() bool Update() error + reloadBackground() Action() *ActionConfig ActionHold() *ActionConfig TriggerAction(hold bool) @@ -78,6 +80,22 @@ func (w *BaseWidget) Update() error { return w.render(w.dev, nil) } +func (w BaseWidget) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Key uint8 `json:"key"` + Interval time.Duration `json:"interval,omitempty"` + LastUpdate time.Time `json:"lastUpdate"` + }{ + Interval: w.interval, + LastUpdate: w.lastUpdate, + }) +} + +func (w *BaseWidget) reloadBackground() { + w.background = deck.backgroundForKey(deck.dev, w.key) + w.lastUpdate = time.Time{} +} + // NewBaseWidget returns a new BaseWidget. func NewBaseWidget(dev *streamdeck.Device, base string, index uint8, action, actionHold *ActionConfig, bg image.Image) *BaseWidget { return &BaseWidget{ diff --git a/widget_button.go b/widget_button.go index e57025d..46eec9c 100644 --- a/widget_button.go +++ b/widget_button.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "image" "image/color" "time" @@ -120,3 +121,14 @@ func (w *ButtonWidget) Update() error { return w.render(w.dev, img) } + +func (w ButtonWidget) MarshalJSON() ([]byte, error) { + out := map[string]interface{}{} + inputJson, _ := w.BaseWidget.MarshalJSON() + json.Unmarshal(inputJson, &out) + out["label"] = w.label + out["fontsize"] = w.fontsize + out["color"] = w.color + out["flatten"] = w.flatten + return json.Marshal(out) +}