From ecdf4967ae65a6249014d4e9a1cfebfc87450f38 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Fri, 26 Feb 2021 23:08:55 +0100 Subject: [PATCH] Move configuration to more stable format (#8) --- cmd/streamdeck/action_exec.go | 39 +++-------- cmd/streamdeck/action_keyPress.go | 49 ++++--------- cmd/streamdeck/action_page.go | 11 ++- cmd/streamdeck/action_pulsevolume.go | 60 ++++------------ cmd/streamdeck/action_reload.go | 2 +- cmd/streamdeck/action_toggleDisplay.go | 2 +- cmd/streamdeck/config.go | 97 ++++++++++++++++++++------ cmd/streamdeck/display_color.go | 12 ++-- cmd/streamdeck/display_exec.go | 95 +++++++------------------ cmd/streamdeck/display_image.go | 15 ++-- cmd/streamdeck/display_pulsevolume.go | 59 ++++++---------- cmd/streamdeck/display_text.go | 41 ++++------- cmd/streamdeck/registry.go | 12 ++-- cmd/streamdeck/system_pages.go | 8 +-- 14 files changed, 207 insertions(+), 295 deletions(-) diff --git a/cmd/streamdeck/action_exec.go b/cmd/streamdeck/action_exec.go index ab0a0e4..e3a3304 100644 --- a/cmd/streamdeck/action_exec.go +++ b/cmd/streamdeck/action_exec.go @@ -14,46 +14,25 @@ func init() { type actionExec struct{} -func (actionExec) Execute(attributes map[string]interface{}) error { - cmd, ok := attributes["command"].([]interface{}) - if !ok { +func (actionExec) Execute(attributes attributeCollection) error { + if attributes.Command == nil { return errors.New("No command supplied") } - var args []string - for _, c := range cmd { - if v, ok := c.(string); ok { - args = append(args, v) - continue - } - return errors.New("Command conatins non-string argument") - } - processEnv := env.ListToMap(os.Environ()) - if e, ok := attributes["env"].(map[interface{}]interface{}); ok { - for k, v := range e { - key, ok := k.(string) - if !ok { - continue - } - value, ok := v.(string) - if !ok { - continue - } - - processEnv[key] = value - } + for k, v := range attributes.Env { + processEnv[k] = v } - command := exec.Command(args[0], args[1:]...) + command := exec.Command(attributes.Command[0], attributes.Command[1:]...) command.Env = env.MapToList(processEnv) - if v, ok := attributes["attach_stdout"].(bool); ok && v { + if attributes.AttachStdout { command.Stdout = os.Stdout } - if v, ok := attributes["attach_stderr"].(bool); ok && v { + if attributes.AttachStderr { command.Stdout = os.Stderr } @@ -61,8 +40,8 @@ func (actionExec) Execute(attributes map[string]interface{}) error { return errors.Wrap(err, "Unable to start command") } - // If "wait" is set and set to true start command and wait for execution - if v, ok := attributes["wait"].(bool); ok && v { + // If "wait" is set to true start command and wait for execution + if attributes.Wait { return errors.Wrap(command.Wait(), "Command was not successful") } diff --git a/cmd/streamdeck/action_keyPress.go b/cmd/streamdeck/action_keyPress.go index 1791a3c..ee4ae4f 100644 --- a/cmd/streamdeck/action_keyPress.go +++ b/cmd/streamdeck/action_keyPress.go @@ -1,7 +1,6 @@ package main import ( - "strconv" "time" "github.com/pkg/errors" @@ -14,71 +13,53 @@ func init() { type actionKeyPress struct{} -func (actionKeyPress) Execute(attributes map[string]interface{}) error { +func (actionKeyPress) Execute(attributes attributeCollection) error { var ( - delay time.Duration execCodes []uint16 - ok bool - keyNames []interface{} - keyCodes []interface{} + keyNames []string + keyCodes []int useKeyNames bool ) - keyCodes, ok = attributes["key_codes"].([]interface{}) - if !ok { - keyNames, ok = attributes["keys"].([]interface{}) - if !ok { + keyCodes = attributes.KeyCodes + if keyCodes == nil { + keyNames = attributes.Keys + if keyNames == nil { return errors.New("No key_codes or keys array present") } useKeyNames = true } - if v, ok := attributes["delay"].(string); ok { - if d, err := time.ParseDuration(v); err == nil { - delay = d - } - } - if useKeyNames { for _, k := range keyNames { - // Convert misdetections into strings - switch k.(type) { - case int: - k = strconv.Itoa(k.(int)) - } - - if kv, ok := k.(string); ok { - if kc, ok := uinputKeyMapping[kv]; ok { - execCodes = append(execCodes, kc) - } else { - return errors.Errorf("Unknown key %q", kv) - } + if kc, ok := uinputKeyMapping[k]; ok { + execCodes = append(execCodes, kc) } else { - return errors.New("Unknown key type detected") + return errors.Errorf("Unknown key %q", k) } } } else { for _, k := range keyCodes { - execCodes = append(execCodes, uint16(k.(int))) + execCodes = append(execCodes, uint16(k)) } } - if v, ok := attributes["mod_shift"].(bool); ok && v { + if attributes.ModShift { if err := kbd.KeyDown(uinput.KeyLeftShift); err != nil { return errors.Wrap(err, "Unable to set shift") } defer kbd.KeyUp(uinput.KeyLeftShift) } - if v, ok := attributes["mod_alt"].(bool); ok && v { + if attributes.ModAlt { if err := kbd.KeyDown(uinput.KeyLeftAlt); err != nil { return errors.Wrap(err, "Unable to set shift") } defer kbd.KeyUp(uinput.KeyLeftAlt) } - if v, ok := attributes["mod_ctrl"].(bool); ok && v { + if attributes.ModCtrl { if err := kbd.KeyDown(uinput.KeyLeftCtrl); err != nil { return errors.Wrap(err, "Unable to set shift") } @@ -89,7 +70,7 @@ func (actionKeyPress) Execute(attributes map[string]interface{}) error { if err := kbd.KeyPress(kc); err != nil { return errors.Wrap(err, "Unable to press key") } - time.Sleep(delay) + time.Sleep(attributes.Delay) } return nil diff --git a/cmd/streamdeck/action_page.go b/cmd/streamdeck/action_page.go index 79594a5..94b4e95 100644 --- a/cmd/streamdeck/action_page.go +++ b/cmd/streamdeck/action_page.go @@ -12,15 +12,12 @@ func init() { type actionPage struct{} -func (actionPage) Execute(attributes map[string]interface{}) error { - name, nameOk := attributes["name"].(string) - relative, relativeOk := attributes["relative"].(int) - - if nameOk && name != "" { - return errors.Wrap(togglePage(name), "switch page") +func (actionPage) Execute(attributes attributeCollection) error { + if attributes.Name != "" { + return errors.Wrap(togglePage(attributes.Name), "switch page") } - if absRel := int(math.Abs(float64(relative))); relativeOk && absRel != 0 && absRel < len(pageStack) { + if absRel := int(math.Abs(float64(attributes.Relative))); absRel != 0 && absRel < len(pageStack) { nextPage := pageStack[absRel] pageStack = pageStack[absRel+1:] return errors.Wrap(togglePage(nextPage), "switch relative page") diff --git a/cmd/streamdeck/action_pulsevolume.go b/cmd/streamdeck/action_pulsevolume.go index 4f3d123..0579404 100644 --- a/cmd/streamdeck/action_pulsevolume.go +++ b/cmd/streamdeck/action_pulsevolume.go @@ -3,8 +3,6 @@ package main import ( - "strconv" - "github.com/pkg/errors" ) @@ -14,86 +12,56 @@ func init() { type actionPulseVolume struct{} -func (actionPulseVolume) Execute(attributes map[string]interface{}) error { +func (actionPulseVolume) Execute(attributes attributeCollection) error { if pulseClient == nil { return errors.New("PulseAudio client not initialized") } - devType, ok := attributes["device"].(string) - if !ok { + if attributes.Device == "" { return errors.New("Missing 'device' attribute") } - match, ok := attributes["match"].(string) - if !ok { + if attributes.Match == "" { return errors.New("Missing 'match' attribute") } - // Read mute value - var ( - mute string - mutev = attributes["mute"] - ) - switch mutev.(type) { - case string: - mute = mutev.(string) - - case bool: - mute = strconv.FormatBool(mutev.(bool)) - } - // Read volume var ( volAbs bool volVal float64 ) - for attr, abs := range map[string]bool{ - "set_volume": true, - "change_volume": false, - } { - val, ok := attributes[attr] - if !ok { - continue - } - switch val.(type) { - case float64: - volVal = val.(float64) / 100 - - case int: - volVal = float64(val.(int)) / 100 - - case int64: - volVal = float64(val.(int64)) / 100 - } - - volAbs = abs - break + if attributes.SetVolume != nil { + volVal = *attributes.SetVolume / 100 + volAbs = true + } else if attributes.ChangeVolume != nil { + volVal = *attributes.ChangeVolume / 100 + volAbs = false } // Execute change - switch devType { + switch attributes.Device { case "input": return errors.Wrap( - pulseClient.SetSinkInputVolume(match, mute, volVal, volAbs), + pulseClient.SetSinkInputVolume(attributes.Match, attributes.Mute, volVal, volAbs), "Unable to set sink input volume", ) case "sink": return errors.Wrap( - pulseClient.SetSinkVolume(match, mute, volVal, volAbs), + pulseClient.SetSinkVolume(attributes.Match, attributes.Mute, volVal, volAbs), "Unable to set sink volume", ) case "source": return errors.Wrap( - pulseClient.SetSourceVolume(match, mute, volVal, volAbs), + pulseClient.SetSourceVolume(attributes.Match, attributes.Mute, volVal, volAbs), "Unable to set source volume", ) default: - return errors.Errorf("Unsupported device type: %q", devType) + return errors.Errorf("Unsupported device type: %q", attributes.Device) } } diff --git a/cmd/streamdeck/action_reload.go b/cmd/streamdeck/action_reload.go index 91ca42f..274fa77 100644 --- a/cmd/streamdeck/action_reload.go +++ b/cmd/streamdeck/action_reload.go @@ -8,7 +8,7 @@ func init() { type actionReloadConfig struct{} -func (actionReloadConfig) Execute(attributes map[string]interface{}) error { +func (actionReloadConfig) Execute(attributes attributeCollection) error { if err := loadConfig(); err != nil { return errors.Wrap(err, "Unable to reload config") } diff --git a/cmd/streamdeck/action_toggleDisplay.go b/cmd/streamdeck/action_toggleDisplay.go index eb70401..49e9c52 100644 --- a/cmd/streamdeck/action_toggleDisplay.go +++ b/cmd/streamdeck/action_toggleDisplay.go @@ -10,7 +10,7 @@ var actionToggleDisplayPreviousBrightness int type actionToggleDisplay struct{} -func (actionToggleDisplay) Execute(attributes map[string]interface{}) error { +func (actionToggleDisplay) Execute(attributes attributeCollection) error { var newB int if currentBrightness > 0 { actionToggleDisplayPreviousBrightness = currentBrightness diff --git a/cmd/streamdeck/config.go b/cmd/streamdeck/config.go index c7085bd..7da66e9 100644 --- a/cmd/streamdeck/config.go +++ b/cmd/streamdeck/config.go @@ -1,6 +1,9 @@ package main import ( + "bytes" + "encoding/gob" + "image/color" "os" "time" @@ -10,36 +13,90 @@ import ( const defaultLongPressDuration = 500 * time.Millisecond +func init() { + gob.Register(attributeCollection{}) +} + +type attributeCollection struct { + AttachStderr bool `json:"attach_stderr,omitempty" yaml:"attach_stderr,omitempty"` + AttachStdout bool `json:"attach_stdout,omitempty" yaml:"attach_stdout,omitempty"` + Border *int `json:"border,omitempty" yaml:"border,omitempty"` + Caption string `json:"caption,omitempty" yaml:"caption,omitempty"` + ChangeVolume *float64 `json:"change_volume,omitempty" yaml:"change_volume,omitempty"` + Color string `json:"color,omitempty" yaml:"color,omitempty"` + Command []string `json:"command,omitempty" yaml:"command,omitempty"` + Delay time.Duration `json:"delay,omitempty" yaml:"delay,omitempty"` + Device string `json:"device,omitempty" yaml:"device,omitempty"` + Env map[string]string `json:"env,omitempty" yaml:"env,omitempty"` + FontSize *float64 `json:"font_size,omitempty" yaml:"font_size,omitempty"` + Image string `json:"image,omitempty" yaml:"image,omitempty"` + Interval time.Duration `json:"interval,omitempty" yaml:"interval,omitempty"` + KeyCodes []int `json:"key_codes,omitempty" yaml:"key_codes,omitempty"` + Keys []string `json:"keys,omitempty" yaml:"keys,omitempty"` + Match string `json:"match,omitempty" yaml:"match,omitempty"` + ModAlt bool `json:"mod_alt,omitempty" yaml:"mod_alt,omitempty"` + ModCtrl bool `json:"mod_ctrl,omitempty" yaml:"mod_ctrl,omitempty"` + ModShift bool `json:"mod_shift,omitempty" yaml:"mod_shift,omitempty"` + Mute string `json:"mute,omitempty" yaml:"mute,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Path string `json:"path,omitempty" yaml:"path,omitempty"` + Relative int `json:"relative,omitempty" yaml:"relative,omitempty"` + RGBA []int `json:"rgba,omitempty" yaml:"rgba,omitempty"` + SetVolume *float64 `json:"set_volume,omitempty" yaml:"set_volume,omitempty"` + Text string `json:"text,omitempty" yaml:"text,omitempty"` + URL string `json:"url,omitempty" yaml:"url,omitempty"` + Wait bool `json:"wait,omitempty" yaml:"wait,omitempty"` +} + +func (a attributeCollection) Clone() attributeCollection { + var ( + buf = new(bytes.Buffer) + out attributeCollection + ) + + gob.NewEncoder(buf).Encode(a) + gob.NewDecoder(buf).Decode(&out) + + return out +} + +func (a attributeCollection) RGBAToColor() color.RGBA { + if len(a.RGBA) != 4 { + return color.RGBA{} + } + return color.RGBA{uint8(a.RGBA[0]), uint8(a.RGBA[1]), uint8(a.RGBA[2]), uint8(a.RGBA[3])} +} + type config struct { - AutoReload bool `yaml:"auto_reload"` - CaptionBorder int `yaml:"caption_border"` - CaptionColor [4]int `yaml:"caption_color"` - CaptionFont string `yaml:"caption_font"` - CaptionFontSize float64 `yaml:"caption_font_size"` - CaptionPosition captionPosition `yaml:"caption_position"` - DefaultBrightness int `yaml:"default_brightness"` - DefaultPage string `yaml:"default_page"` - DisplayOffTime time.Duration `yaml:"display_off_time"` - LongPressDuration time.Duration `yaml:"long_press_duration"` - Pages map[string]page `yaml:"pages"` - RenderFont string `yaml:"render_font"` + AutoReload bool `json:"auto_reload" yaml:"auto_reload"` + CaptionBorder int `json:"caption_border" yaml:"caption_border"` + CaptionColor [4]int `json:"caption_color" yaml:"caption_color"` + CaptionFont string `json:"caption_font" yaml:"caption_font"` + CaptionFontSize float64 `json:"caption_font_size" yaml:"caption_font_size"` + CaptionPosition captionPosition `json:"caption_position" yaml:"caption_position"` + DefaultBrightness int `json:"default_brightness" yaml:"default_brightness"` + DefaultPage string `json:"default_page" yaml:"default_page"` + DisplayOffTime time.Duration `json:"display_off_time" yaml:"display_off_time"` + LongPressDuration time.Duration `json:"long_press_duration" yaml:"long_press_duration"` + Pages map[string]page `json:"pages" yaml:"pages"` + RenderFont string `json:"render_font" yaml:"render_font"` } type page struct { - Keys map[int]keyDefinition `yaml:"keys"` - Overlay string `yaml:"overlay"` - Underlay string `yaml:"underlay"` + Keys map[int]keyDefinition `json:"keys" yaml:"keys"` + Overlay string `json:"overlay" yaml:"overlay"` + Underlay string `json:"underlay" yaml:"underlay"` } type keyDefinition struct { - Display dynamicElement `yaml:"display"` - Actions []dynamicElement `yaml:"actions"` + Display dynamicElement `json:"display" yaml:"display"` + Actions []dynamicElement `json:"actions" yaml:"actions"` } type dynamicElement struct { - Type string `yaml:"type"` - LongPress bool `yaml:"long_press"` - Attributes map[string]interface{} `yaml:"attributes"` + Type string `json:"type" yaml:"type"` + LongPress bool `json:"long_press" yaml:"long_press"` + Attributes attributeCollection `json:"attributes" yaml:"attributes"` } func newConfig() config { diff --git a/cmd/streamdeck/display_color.go b/cmd/streamdeck/display_color.go index 72fe985..23230fd 100644 --- a/cmd/streamdeck/display_color.go +++ b/cmd/streamdeck/display_color.go @@ -13,13 +13,13 @@ func init() { type displayElementColor struct{} -func (d displayElementColor) Display(ctx context.Context, idx int, attributes map[string]interface{}) error { - if name, ok := attributes["color"].(string); ok { - return d.displayPredefinedColor(idx, name) +func (d displayElementColor) Display(ctx context.Context, idx int, attributes attributeCollection) error { + if attributes.Color != "" { + return d.displayPredefinedColor(idx, attributes.Color) } - if rgba, ok := attributes["rgba"].([]interface{}); ok { - if len(rgba) != 4 { + if attributes.RGBA != nil { + if len(attributes.RGBA) != 4 { return errors.New("RGBA color definition needs 4 hex values") } @@ -28,7 +28,7 @@ func (d displayElementColor) Display(ctx context.Context, idx int, attributes ma return err } - return sd.FillColor(idx, color.RGBA{uint8(rgba[0].(int)), uint8(rgba[1].(int)), uint8(rgba[2].(int)), uint8(rgba[3].(int))}) + return sd.FillColor(idx, attributes.RGBAToColor()) } return errors.New("No valid attributes specified for type color") diff --git a/cmd/streamdeck/display_exec.go b/cmd/streamdeck/display_exec.go index 791ebbc..ac83f26 100644 --- a/cmd/streamdeck/display_exec.go +++ b/cmd/streamdeck/display_exec.go @@ -25,48 +25,27 @@ type displayElementExec struct { running bool } -func (d displayElementExec) Display(ctx context.Context, idx int, attributes map[string]interface{}) error { +func (d displayElementExec) Display(ctx context.Context, idx int, attributes attributeCollection) error { var ( err error imgRenderer = newTextOnImageRenderer() ) // Initialize command - cmd, ok := attributes["command"].([]interface{}) - if !ok { + if attributes.Command == nil { return errors.New("No command supplied") } - var args []string - for _, c := range cmd { - if v, ok := c.(string); ok { - args = append(args, v) - continue - } - return errors.New("Command conatins non-string argument") - } - // Execute command and parse it buf := new(bytes.Buffer) processEnv := env.ListToMap(os.Environ()) - if e, ok := attributes["env"].(map[interface{}]interface{}); ok { - for k, v := range e { - key, ok := k.(string) - if !ok { - continue - } - value, ok := v.(string) - if !ok { - continue - } - - processEnv[key] = value - } + for k, v := range attributes.Env { + processEnv[k] = v } - command := exec.Command(args[0], args[1:]...) + command := exec.Command(attributes.Command[0], attributes.Command[1:]...) command.Env = env.MapToList(processEnv) command.Stdout = buf @@ -74,65 +53,50 @@ func (d displayElementExec) Display(ctx context.Context, idx int, attributes map return errors.Wrap(err, "Command has exit != 0") } - attributes["text"] = strings.TrimSpace(buf.String()) + attributes.Text = strings.TrimSpace(buf.String()) - tmpAttrs := map[string]interface{}{} + tmpAttrs := attributes.Clone() if err = json.Unmarshal(buf.Bytes(), &tmpAttrs); err == nil { // Reset text to empty as it was parsable json - attributes["text"] = "" - - for k, v := range tmpAttrs { - attributes[k] = v - } + attributes = tmpAttrs } // Initialize background - if filename, ok := attributes["image"].(string); ok { - if err = imgRenderer.DrawBackgroundFromFile(filename); err != nil { + if attributes.Image != "" { + if err = imgRenderer.DrawBackgroundFromFile(attributes.Image); err != nil { return errors.Wrap(err, "Unable to draw background from disk") } } // Initialize color var textColor color.Color = color.RGBA{0xff, 0xff, 0xff, 0xff} - if rgba, ok := attributes["color"].([]interface{}); ok { - if len(rgba) != 4 { + if attributes.RGBA != nil { + if len(attributes.RGBA) != 4 { return errors.New("RGBA color definition needs 4 hex values") } - tmpCol := color.RGBA{} - - for cidx, vp := range []*uint8{&tmpCol.R, &tmpCol.G, &tmpCol.B, &tmpCol.A} { - switch rgba[cidx].(type) { - case int: - *vp = uint8(rgba[cidx].(int)) - case float64: - *vp = uint8(rgba[cidx].(float64)) - } - } - - textColor = tmpCol + textColor = attributes.RGBAToColor() } // Initialize fontsize var fontsize float64 = 120 - if v, ok := attributes["font_size"].(float64); ok { - fontsize = v + if attributes.FontSize != nil { + fontsize = *attributes.FontSize } border := 10 - if v, ok := attributes["border"].(int); ok { - border = v + if attributes.Border != nil { + border = *attributes.Border } - if strings.TrimSpace(attributes["text"].(string)) != "" { - if err = imgRenderer.DrawBigText(strings.TrimSpace(attributes["text"].(string)), fontsize, border, textColor); err != nil { + if strings.TrimSpace(attributes.Text) != "" { + if err = imgRenderer.DrawBigText(strings.TrimSpace(attributes.Text), fontsize, border, textColor); err != nil { return errors.Wrap(err, "Unable to render text") } } - if caption, ok := attributes["caption"].(string); ok && strings.TrimSpace(caption) != "" { - if err = imgRenderer.DrawCaptionText(strings.TrimSpace(caption)); err != nil { + if strings.TrimSpace(attributes.Caption) != "" { + if err = imgRenderer.DrawCaptionText(strings.TrimSpace(attributes.Caption)); err != nil { return errors.Wrap(err, "Unable to render caption") } } @@ -149,24 +113,15 @@ func (d displayElementExec) Display(ctx context.Context, idx int, attributes map return errors.Wrap(sd.FillImage(idx, imgRenderer.GetImage()), "Unable to set image") } -func (d displayElementExec) NeedsLoop(attributes map[string]interface{}) bool { - if v, ok := attributes["interval"].(int); ok { - return v > 0 - } - - return false +func (d displayElementExec) NeedsLoop(attributes attributeCollection) bool { + return attributes.Interval > 0 } -func (d *displayElementExec) StartLoopDisplay(ctx context.Context, idx int, attributes map[string]interface{}) error { +func (d *displayElementExec) StartLoopDisplay(ctx context.Context, idx int, attributes attributeCollection) error { d.running = true - interval := 5 * time.Second - if v, ok := attributes["interval"].(int); ok { - interval = time.Duration(v) * time.Second - } - go func() { - for tick := time.NewTicker(interval); ; <-tick.C { + for tick := time.NewTicker(attributes.Interval); ; <-tick.C { if !d.running { return } diff --git a/cmd/streamdeck/display_image.go b/cmd/streamdeck/display_image.go index 4249678..57a4c3f 100644 --- a/cmd/streamdeck/display_image.go +++ b/cmd/streamdeck/display_image.go @@ -21,22 +21,21 @@ func init() { type displayElementImage struct{} -func (d displayElementImage) Display(ctx context.Context, idx int, attributes map[string]interface{}) error { - filename, ok := attributes["path"].(string) - if !ok { - url, ok := attributes["url"].(string) - if !ok { +func (d displayElementImage) Display(ctx context.Context, idx int, attributes attributeCollection) error { + filename := attributes.Path + if filename == "" { + if attributes.URL == "" { return errors.New("No path or url attribute specified") } var err error - filename, err = d.getCacheFileName(url) + filename, err = d.getCacheFileName(attributes.URL) if err != nil { return errors.Wrap(err, "Unable to get cache filename for image url") } if _, err := os.Stat(filename); os.IsNotExist(err) { - resp, err := http.Get(url) + resp, err := http.Get(attributes.URL) if err != nil { return errors.Wrap(err, "Unable to request image url") } @@ -84,7 +83,7 @@ func (d displayElementImage) getCacheFileName(url string) (string, error) { } cacheDir := path.Join(ucd, "io.luzifer.streamdeck") - if err = os.MkdirAll(cacheDir, 0750); err != nil { + if err = os.MkdirAll(cacheDir, 0o750); err != nil { return "", errors.Wrap(err, "Unable to create cache dir") } diff --git a/cmd/streamdeck/display_pulsevolume.go b/cmd/streamdeck/display_pulsevolume.go index abf4cb0..c551e3d 100644 --- a/cmd/streamdeck/display_pulsevolume.go +++ b/cmd/streamdeck/display_pulsevolume.go @@ -19,18 +19,16 @@ func init() { type displayElementPulseVolume struct{} -func (d displayElementPulseVolume) Display(ctx context.Context, idx int, attributes map[string]interface{}) error { +func (d displayElementPulseVolume) Display(ctx context.Context, idx int, attributes attributeCollection) error { if pulseClient == nil { return errors.New("PulseAudio client not initialized") } - devType, ok := attributes["device"].(string) - if !ok { + if attributes.Device == "" { return errors.New("Missing 'device' attribute") } - match, ok := attributes["match"].(string) - if !ok { + if attributes.Match == "" { return errors.New("Missing 'match' attribute") } @@ -41,19 +39,19 @@ func (d displayElementPulseVolume) Display(ctx context.Context, idx int, attribu volume float64 ) - switch devType { + switch attributes.Device { case "input": - volume, mute, _, _, err = pulseClient.GetSinkInputVolume(match) + volume, mute, _, _, err = pulseClient.GetSinkInputVolume(attributes.Match) case "sink": - volume, mute, _, _, err = pulseClient.GetSinkVolume(match) + volume, mute, _, _, err = pulseClient.GetSinkVolume(attributes.Match) case "source": - volume, mute, _, _, err = pulseClient.GetSourceVolume(match) + volume, mute, _, _, err = pulseClient.GetSourceVolume(attributes.Match) default: - return errors.Errorf("Unsupported device type: %q", devType) + return errors.Errorf("Unsupported device type: %q", attributes.Device) } @@ -79,42 +77,31 @@ func (d displayElementPulseVolume) Display(ctx context.Context, idx int, attribu } // Initialize color - if rgba, ok := attributes["color"].([]interface{}); ok { - if len(rgba) != 4 { + if attributes.RGBA != nil { + if len(attributes.RGBA) != 4 { return errors.New("RGBA color definition needs 4 hex values") } - tmpCol := color.RGBA{} - - for cidx, vp := range []*uint8{&tmpCol.R, &tmpCol.G, &tmpCol.B, &tmpCol.A} { - switch rgba[cidx].(type) { - case int: - *vp = uint8(rgba[cidx].(int)) - case float64: - *vp = uint8(rgba[cidx].(float64)) - } - } - - textColor = tmpCol + textColor = attributes.RGBAToColor() } // Initialize fontsize var fontsize float64 = 120 - if v, ok := attributes["font_size"].(float64); ok { - fontsize = v + if attributes.FontSize != nil { + fontsize = *attributes.FontSize } - var border = 10 - if v, ok := attributes["border"].(int); ok { - border = v + border := 10 + if attributes.Border != nil { + border = *attributes.Border } if err = img.DrawBigText(text, fontsize, border, textColor); err != nil { return errors.Wrap(err, "Unable to draw text") } - if caption, ok := attributes["caption"].(string); ok && strings.TrimSpace(caption) != "" { - if err = img.DrawCaptionText(strings.TrimSpace(caption)); err != nil { + if strings.TrimSpace(attributes.Caption) != "" { + if err = img.DrawCaptionText(strings.TrimSpace(attributes.Caption)); err != nil { return errors.Wrap(err, "Unable to render caption") } } @@ -127,12 +114,12 @@ func (d displayElementPulseVolume) Display(ctx context.Context, idx int, attribu return errors.Wrap(sd.FillImage(idx, img.GetImage()), "Unable to set image") } -func (d displayElementPulseVolume) NeedsLoop(attributes map[string]interface{}) bool { return true } +func (d displayElementPulseVolume) NeedsLoop(attributes attributeCollection) bool { return true } -func (d *displayElementPulseVolume) StartLoopDisplay(ctx context.Context, idx int, attributes map[string]interface{}) error { - var interval = time.Second - if v, ok := attributes["interval"].(int); ok { - interval = time.Duration(v) * time.Second +func (d *displayElementPulseVolume) StartLoopDisplay(ctx context.Context, idx int, attributes attributeCollection) error { + interval := time.Second + if attributes.Interval > 0 { + interval = attributes.Interval } go func() { diff --git a/cmd/streamdeck/display_text.go b/cmd/streamdeck/display_text.go index 2fdded6..3340305 100644 --- a/cmd/streamdeck/display_text.go +++ b/cmd/streamdeck/display_text.go @@ -17,59 +17,48 @@ type displayElementText struct { running bool } -func (d displayElementText) Display(ctx context.Context, idx int, attributes map[string]interface{}) error { +func (d displayElementText) Display(ctx context.Context, idx int, attributes attributeCollection) error { var ( err error imgRenderer = newTextOnImageRenderer() ) // Initialize background - if filename, ok := attributes["image"].(string); ok { - if err = imgRenderer.DrawBackgroundFromFile(filename); err != nil { + if attributes.Image != "" { + if err = imgRenderer.DrawBackgroundFromFile(attributes.Image); err != nil { return errors.Wrap(err, "Unable to draw background from disk") } } // Initialize color var textColor color.Color = color.RGBA{0xff, 0xff, 0xff, 0xff} - if rgba, ok := attributes["color"].([]interface{}); ok { - if len(rgba) != 4 { + if attributes.RGBA != nil { + if len(attributes.RGBA) != 4 { return errors.New("RGBA color definition needs 4 hex values") } - tmpCol := color.RGBA{} - - for cidx, vp := range []*uint8{&tmpCol.R, &tmpCol.G, &tmpCol.B, &tmpCol.A} { - switch rgba[cidx].(type) { - case int: - *vp = uint8(rgba[cidx].(int)) - case float64: - *vp = uint8(rgba[cidx].(float64)) - } - } - - textColor = tmpCol + textColor = attributes.RGBAToColor() } // Initialize fontsize var fontsize float64 = 120 - if v, ok := attributes["font_size"].(float64); ok { - fontsize = v + if attributes.FontSize != nil { + fontsize = *attributes.FontSize } border := 10 - if v, ok := attributes["border"].(int); ok { - border = v + if attributes.Border != nil { + border = *attributes.Border } - if strings.TrimSpace(attributes["text"].(string)) != "" { - if err = imgRenderer.DrawBigText(strings.TrimSpace(attributes["text"].(string)), fontsize, border, textColor); err != nil { + if strings.TrimSpace(attributes.Text) != "" { + if err = imgRenderer.DrawBigText(strings.TrimSpace(attributes.Text), fontsize, border, textColor); err != nil { return errors.Wrap(err, "Unable to render text") } } - if caption, ok := attributes["caption"].(string); ok && strings.TrimSpace(caption) != "" { - if err = imgRenderer.DrawCaptionText(strings.TrimSpace(caption)); err != nil { + if strings.TrimSpace(attributes.Caption) != "" { + if err = imgRenderer.DrawCaptionText(strings.TrimSpace(attributes.Caption)); err != nil { return errors.Wrap(err, "Unable to render caption") } } @@ -86,4 +75,4 @@ func (d displayElementText) Display(ctx context.Context, idx int, attributes map return errors.Wrap(sd.FillImage(idx, imgRenderer.GetImage()), "Unable to set image") } -func (d displayElementText) NeedsLoop(attributes map[string]interface{}) bool { return false } +func (d displayElementText) NeedsLoop(attributes attributeCollection) bool { return false } diff --git a/cmd/streamdeck/registry.go b/cmd/streamdeck/registry.go index 88a19f2..7e08034 100644 --- a/cmd/streamdeck/registry.go +++ b/cmd/streamdeck/registry.go @@ -11,21 +11,21 @@ import ( const errorDisplayElementType = "color" -var errorDisplayElementAttributes = map[string]interface{}{ - "rgba": []interface{}{0xff, 0x0, 0x0, 0xff}, +var errorDisplayElementAttributes = attributeCollection{ + RGBA: []int{0xff, 0x0, 0x0, 0xff}, } type action interface { - Execute(attributes map[string]interface{}) error + Execute(attributes attributeCollection) error } type displayElement interface { - Display(ctx context.Context, idx int, attributes map[string]interface{}) error + Display(ctx context.Context, idx int, attributes attributeCollection) error } type refreshingDisplayElement interface { - NeedsLoop(attributes map[string]interface{}) bool - StartLoopDisplay(ctx context.Context, idx int, attributes map[string]interface{}) error + NeedsLoop(attributes attributeCollection) bool + StartLoopDisplay(ctx context.Context, idx int, attributes attributeCollection) error StopLoopDisplay() error } diff --git a/cmd/streamdeck/system_pages.go b/cmd/streamdeck/system_pages.go index 79a5faf..ea5a371 100644 --- a/cmd/streamdeck/system_pages.go +++ b/cmd/streamdeck/system_pages.go @@ -7,15 +7,15 @@ func applySystemPages(conf *config) { blankPage.Keys[i] = keyDefinition{ Display: dynamicElement{ Type: "color", - Attributes: map[string]interface{}{ - "rgba": []interface{}{0x0, 0x0, 0x0, 0xff}, + Attributes: attributeCollection{ + RGBA: []int{0x0, 0x0, 0x0, 0xff}, }, }, Actions: []dynamicElement{ { Type: "page", - Attributes: map[string]interface{}{ - "relative": 1, + Attributes: attributeCollection{ + Relative: 1, }, }, },