1
0
Fork 0
mirror of https://github.com/Luzifer/streamdeck.git synced 2024-10-18 13:04:25 +00:00

Move configuration to more stable format (#8)

This commit is contained in:
Knut Ahlers 2021-02-26 23:08:55 +01:00 committed by GitHub
parent 393ba030a2
commit ecdf4967ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 207 additions and 295 deletions

View file

@ -14,46 +14,25 @@ func init() {
type actionExec struct{} type actionExec struct{}
func (actionExec) Execute(attributes map[string]interface{}) error { func (actionExec) Execute(attributes attributeCollection) error {
cmd, ok := attributes["command"].([]interface{}) if attributes.Command == nil {
if !ok {
return errors.New("No command supplied") 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()) processEnv := env.ListToMap(os.Environ())
if e, ok := attributes["env"].(map[interface{}]interface{}); ok { for k, v := range attributes.Env {
for k, v := range e { processEnv[k] = v
key, ok := k.(string)
if !ok {
continue
}
value, ok := v.(string)
if !ok {
continue
} }
processEnv[key] = value command := exec.Command(attributes.Command[0], attributes.Command[1:]...)
}
}
command := exec.Command(args[0], args[1:]...)
command.Env = env.MapToList(processEnv) command.Env = env.MapToList(processEnv)
if v, ok := attributes["attach_stdout"].(bool); ok && v { if attributes.AttachStdout {
command.Stdout = os.Stdout command.Stdout = os.Stdout
} }
if v, ok := attributes["attach_stderr"].(bool); ok && v { if attributes.AttachStderr {
command.Stdout = os.Stderr command.Stdout = os.Stderr
} }
@ -61,8 +40,8 @@ func (actionExec) Execute(attributes map[string]interface{}) error {
return errors.Wrap(err, "Unable to start command") return errors.Wrap(err, "Unable to start command")
} }
// If "wait" is set and set to true start command and wait for execution // If "wait" is set to true start command and wait for execution
if v, ok := attributes["wait"].(bool); ok && v { if attributes.Wait {
return errors.Wrap(command.Wait(), "Command was not successful") return errors.Wrap(command.Wait(), "Command was not successful")
} }

View file

@ -1,7 +1,6 @@
package main package main
import ( import (
"strconv"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -14,71 +13,53 @@ func init() {
type actionKeyPress struct{} type actionKeyPress struct{}
func (actionKeyPress) Execute(attributes map[string]interface{}) error { func (actionKeyPress) Execute(attributes attributeCollection) error {
var ( var (
delay time.Duration
execCodes []uint16 execCodes []uint16
ok bool
keyNames []interface{} keyNames []string
keyCodes []interface{} keyCodes []int
useKeyNames bool useKeyNames bool
) )
keyCodes, ok = attributes["key_codes"].([]interface{}) keyCodes = attributes.KeyCodes
if !ok { if keyCodes == nil {
keyNames, ok = attributes["keys"].([]interface{}) keyNames = attributes.Keys
if !ok { if keyNames == nil {
return errors.New("No key_codes or keys array present") return errors.New("No key_codes or keys array present")
} }
useKeyNames = true useKeyNames = true
} }
if v, ok := attributes["delay"].(string); ok {
if d, err := time.ParseDuration(v); err == nil {
delay = d
}
}
if useKeyNames { if useKeyNames {
for _, k := range keyNames { for _, k := range keyNames {
// Convert misdetections into strings if kc, ok := uinputKeyMapping[k]; ok {
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) execCodes = append(execCodes, kc)
} else { } else {
return errors.Errorf("Unknown key %q", kv) return errors.Errorf("Unknown key %q", k)
}
} else {
return errors.New("Unknown key type detected")
} }
} }
} else { } else {
for _, k := range keyCodes { 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 { if err := kbd.KeyDown(uinput.KeyLeftShift); err != nil {
return errors.Wrap(err, "Unable to set shift") return errors.Wrap(err, "Unable to set shift")
} }
defer kbd.KeyUp(uinput.KeyLeftShift) 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 { if err := kbd.KeyDown(uinput.KeyLeftAlt); err != nil {
return errors.Wrap(err, "Unable to set shift") return errors.Wrap(err, "Unable to set shift")
} }
defer kbd.KeyUp(uinput.KeyLeftAlt) 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 { if err := kbd.KeyDown(uinput.KeyLeftCtrl); err != nil {
return errors.Wrap(err, "Unable to set shift") 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 { if err := kbd.KeyPress(kc); err != nil {
return errors.Wrap(err, "Unable to press key") return errors.Wrap(err, "Unable to press key")
} }
time.Sleep(delay) time.Sleep(attributes.Delay)
} }
return nil return nil

View file

@ -12,15 +12,12 @@ func init() {
type actionPage struct{} type actionPage struct{}
func (actionPage) Execute(attributes map[string]interface{}) error { func (actionPage) Execute(attributes attributeCollection) error {
name, nameOk := attributes["name"].(string) if attributes.Name != "" {
relative, relativeOk := attributes["relative"].(int) return errors.Wrap(togglePage(attributes.Name), "switch page")
if nameOk && name != "" {
return errors.Wrap(togglePage(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] nextPage := pageStack[absRel]
pageStack = pageStack[absRel+1:] pageStack = pageStack[absRel+1:]
return errors.Wrap(togglePage(nextPage), "switch relative page") return errors.Wrap(togglePage(nextPage), "switch relative page")

View file

@ -3,8 +3,6 @@
package main package main
import ( import (
"strconv"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -14,86 +12,56 @@ func init() {
type actionPulseVolume struct{} type actionPulseVolume struct{}
func (actionPulseVolume) Execute(attributes map[string]interface{}) error { func (actionPulseVolume) Execute(attributes attributeCollection) error {
if pulseClient == nil { if pulseClient == nil {
return errors.New("PulseAudio client not initialized") return errors.New("PulseAudio client not initialized")
} }
devType, ok := attributes["device"].(string) if attributes.Device == "" {
if !ok {
return errors.New("Missing 'device' attribute") return errors.New("Missing 'device' attribute")
} }
match, ok := attributes["match"].(string) if attributes.Match == "" {
if !ok {
return errors.New("Missing 'match' attribute") 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 // Read volume
var ( var (
volAbs bool volAbs bool
volVal float64 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) { if attributes.SetVolume != nil {
case float64: volVal = *attributes.SetVolume / 100
volVal = val.(float64) / 100 volAbs = true
} else if attributes.ChangeVolume != nil {
case int: volVal = *attributes.ChangeVolume / 100
volVal = float64(val.(int)) / 100 volAbs = false
case int64:
volVal = float64(val.(int64)) / 100
}
volAbs = abs
break
} }
// Execute change // Execute change
switch devType { switch attributes.Device {
case "input": case "input":
return errors.Wrap( return errors.Wrap(
pulseClient.SetSinkInputVolume(match, mute, volVal, volAbs), pulseClient.SetSinkInputVolume(attributes.Match, attributes.Mute, volVal, volAbs),
"Unable to set sink input volume", "Unable to set sink input volume",
) )
case "sink": case "sink":
return errors.Wrap( return errors.Wrap(
pulseClient.SetSinkVolume(match, mute, volVal, volAbs), pulseClient.SetSinkVolume(attributes.Match, attributes.Mute, volVal, volAbs),
"Unable to set sink volume", "Unable to set sink volume",
) )
case "source": case "source":
return errors.Wrap( return errors.Wrap(
pulseClient.SetSourceVolume(match, mute, volVal, volAbs), pulseClient.SetSourceVolume(attributes.Match, attributes.Mute, volVal, volAbs),
"Unable to set source volume", "Unable to set source volume",
) )
default: default:
return errors.Errorf("Unsupported device type: %q", devType) return errors.Errorf("Unsupported device type: %q", attributes.Device)
} }
} }

View file

@ -8,7 +8,7 @@ func init() {
type actionReloadConfig struct{} type actionReloadConfig struct{}
func (actionReloadConfig) Execute(attributes map[string]interface{}) error { func (actionReloadConfig) Execute(attributes attributeCollection) error {
if err := loadConfig(); err != nil { if err := loadConfig(); err != nil {
return errors.Wrap(err, "Unable to reload config") return errors.Wrap(err, "Unable to reload config")
} }

View file

@ -10,7 +10,7 @@ var actionToggleDisplayPreviousBrightness int
type actionToggleDisplay struct{} type actionToggleDisplay struct{}
func (actionToggleDisplay) Execute(attributes map[string]interface{}) error { func (actionToggleDisplay) Execute(attributes attributeCollection) error {
var newB int var newB int
if currentBrightness > 0 { if currentBrightness > 0 {
actionToggleDisplayPreviousBrightness = currentBrightness actionToggleDisplayPreviousBrightness = currentBrightness

View file

@ -1,6 +1,9 @@
package main package main
import ( import (
"bytes"
"encoding/gob"
"image/color"
"os" "os"
"time" "time"
@ -10,36 +13,90 @@ import (
const defaultLongPressDuration = 500 * time.Millisecond 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 { type config struct {
AutoReload bool `yaml:"auto_reload"` AutoReload bool `json:"auto_reload" yaml:"auto_reload"`
CaptionBorder int `yaml:"caption_border"` CaptionBorder int `json:"caption_border" yaml:"caption_border"`
CaptionColor [4]int `yaml:"caption_color"` CaptionColor [4]int `json:"caption_color" yaml:"caption_color"`
CaptionFont string `yaml:"caption_font"` CaptionFont string `json:"caption_font" yaml:"caption_font"`
CaptionFontSize float64 `yaml:"caption_font_size"` CaptionFontSize float64 `json:"caption_font_size" yaml:"caption_font_size"`
CaptionPosition captionPosition `yaml:"caption_position"` CaptionPosition captionPosition `json:"caption_position" yaml:"caption_position"`
DefaultBrightness int `yaml:"default_brightness"` DefaultBrightness int `json:"default_brightness" yaml:"default_brightness"`
DefaultPage string `yaml:"default_page"` DefaultPage string `json:"default_page" yaml:"default_page"`
DisplayOffTime time.Duration `yaml:"display_off_time"` DisplayOffTime time.Duration `json:"display_off_time" yaml:"display_off_time"`
LongPressDuration time.Duration `yaml:"long_press_duration"` LongPressDuration time.Duration `json:"long_press_duration" yaml:"long_press_duration"`
Pages map[string]page `yaml:"pages"` Pages map[string]page `json:"pages" yaml:"pages"`
RenderFont string `yaml:"render_font"` RenderFont string `json:"render_font" yaml:"render_font"`
} }
type page struct { type page struct {
Keys map[int]keyDefinition `yaml:"keys"` Keys map[int]keyDefinition `json:"keys" yaml:"keys"`
Overlay string `yaml:"overlay"` Overlay string `json:"overlay" yaml:"overlay"`
Underlay string `yaml:"underlay"` Underlay string `json:"underlay" yaml:"underlay"`
} }
type keyDefinition struct { type keyDefinition struct {
Display dynamicElement `yaml:"display"` Display dynamicElement `json:"display" yaml:"display"`
Actions []dynamicElement `yaml:"actions"` Actions []dynamicElement `json:"actions" yaml:"actions"`
} }
type dynamicElement struct { type dynamicElement struct {
Type string `yaml:"type"` Type string `json:"type" yaml:"type"`
LongPress bool `yaml:"long_press"` LongPress bool `json:"long_press" yaml:"long_press"`
Attributes map[string]interface{} `yaml:"attributes"` Attributes attributeCollection `json:"attributes" yaml:"attributes"`
} }
func newConfig() config { func newConfig() config {

View file

@ -13,13 +13,13 @@ func init() {
type displayElementColor struct{} type displayElementColor struct{}
func (d displayElementColor) Display(ctx context.Context, idx int, attributes map[string]interface{}) error { func (d displayElementColor) Display(ctx context.Context, idx int, attributes attributeCollection) error {
if name, ok := attributes["color"].(string); ok { if attributes.Color != "" {
return d.displayPredefinedColor(idx, name) return d.displayPredefinedColor(idx, attributes.Color)
} }
if rgba, ok := attributes["rgba"].([]interface{}); ok { if attributes.RGBA != nil {
if len(rgba) != 4 { if len(attributes.RGBA) != 4 {
return errors.New("RGBA color definition needs 4 hex values") 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 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") return errors.New("No valid attributes specified for type color")

View file

@ -25,48 +25,27 @@ type displayElementExec struct {
running bool 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 ( var (
err error err error
imgRenderer = newTextOnImageRenderer() imgRenderer = newTextOnImageRenderer()
) )
// Initialize command // Initialize command
cmd, ok := attributes["command"].([]interface{}) if attributes.Command == nil {
if !ok {
return errors.New("No command supplied") 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 // Execute command and parse it
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
processEnv := env.ListToMap(os.Environ()) processEnv := env.ListToMap(os.Environ())
if e, ok := attributes["env"].(map[interface{}]interface{}); ok { for k, v := range attributes.Env {
for k, v := range e { processEnv[k] = v
key, ok := k.(string)
if !ok {
continue
}
value, ok := v.(string)
if !ok {
continue
} }
processEnv[key] = value command := exec.Command(attributes.Command[0], attributes.Command[1:]...)
}
}
command := exec.Command(args[0], args[1:]...)
command.Env = env.MapToList(processEnv) command.Env = env.MapToList(processEnv)
command.Stdout = buf 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") 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 { if err = json.Unmarshal(buf.Bytes(), &tmpAttrs); err == nil {
// Reset text to empty as it was parsable json // Reset text to empty as it was parsable json
attributes["text"] = "" attributes = tmpAttrs
for k, v := range tmpAttrs {
attributes[k] = v
}
} }
// Initialize background // Initialize background
if filename, ok := attributes["image"].(string); ok { if attributes.Image != "" {
if err = imgRenderer.DrawBackgroundFromFile(filename); err != nil { if err = imgRenderer.DrawBackgroundFromFile(attributes.Image); err != nil {
return errors.Wrap(err, "Unable to draw background from disk") return errors.Wrap(err, "Unable to draw background from disk")
} }
} }
// Initialize color // Initialize color
var textColor color.Color = color.RGBA{0xff, 0xff, 0xff, 0xff} var textColor color.Color = color.RGBA{0xff, 0xff, 0xff, 0xff}
if rgba, ok := attributes["color"].([]interface{}); ok { if attributes.RGBA != nil {
if len(rgba) != 4 { if len(attributes.RGBA) != 4 {
return errors.New("RGBA color definition needs 4 hex values") return errors.New("RGBA color definition needs 4 hex values")
} }
tmpCol := color.RGBA{} textColor = attributes.RGBAToColor()
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
} }
// Initialize fontsize // Initialize fontsize
var fontsize float64 = 120 var fontsize float64 = 120
if v, ok := attributes["font_size"].(float64); ok { if attributes.FontSize != nil {
fontsize = v fontsize = *attributes.FontSize
} }
border := 10 border := 10
if v, ok := attributes["border"].(int); ok { if attributes.Border != nil {
border = v border = *attributes.Border
} }
if strings.TrimSpace(attributes["text"].(string)) != "" { if strings.TrimSpace(attributes.Text) != "" {
if err = imgRenderer.DrawBigText(strings.TrimSpace(attributes["text"].(string)), fontsize, border, textColor); err != nil { if err = imgRenderer.DrawBigText(strings.TrimSpace(attributes.Text), fontsize, border, textColor); err != nil {
return errors.Wrap(err, "Unable to render text") return errors.Wrap(err, "Unable to render text")
} }
} }
if caption, ok := attributes["caption"].(string); ok && strings.TrimSpace(caption) != "" { if strings.TrimSpace(attributes.Caption) != "" {
if err = imgRenderer.DrawCaptionText(strings.TrimSpace(caption)); err != nil { if err = imgRenderer.DrawCaptionText(strings.TrimSpace(attributes.Caption)); err != nil {
return errors.Wrap(err, "Unable to render caption") 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") return errors.Wrap(sd.FillImage(idx, imgRenderer.GetImage()), "Unable to set image")
} }
func (d displayElementExec) NeedsLoop(attributes map[string]interface{}) bool { func (d displayElementExec) NeedsLoop(attributes attributeCollection) bool {
if v, ok := attributes["interval"].(int); ok { return attributes.Interval > 0
return v > 0
}
return false
} }
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 d.running = true
interval := 5 * time.Second
if v, ok := attributes["interval"].(int); ok {
interval = time.Duration(v) * time.Second
}
go func() { go func() {
for tick := time.NewTicker(interval); ; <-tick.C { for tick := time.NewTicker(attributes.Interval); ; <-tick.C {
if !d.running { if !d.running {
return return
} }

View file

@ -21,22 +21,21 @@ func init() {
type displayElementImage struct{} type displayElementImage struct{}
func (d displayElementImage) Display(ctx context.Context, idx int, attributes map[string]interface{}) error { func (d displayElementImage) Display(ctx context.Context, idx int, attributes attributeCollection) error {
filename, ok := attributes["path"].(string) filename := attributes.Path
if !ok { if filename == "" {
url, ok := attributes["url"].(string) if attributes.URL == "" {
if !ok {
return errors.New("No path or url attribute specified") return errors.New("No path or url attribute specified")
} }
var err error var err error
filename, err = d.getCacheFileName(url) filename, err = d.getCacheFileName(attributes.URL)
if err != nil { if err != nil {
return errors.Wrap(err, "Unable to get cache filename for image url") return errors.Wrap(err, "Unable to get cache filename for image url")
} }
if _, err := os.Stat(filename); os.IsNotExist(err) { if _, err := os.Stat(filename); os.IsNotExist(err) {
resp, err := http.Get(url) resp, err := http.Get(attributes.URL)
if err != nil { if err != nil {
return errors.Wrap(err, "Unable to request image url") 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") 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") return "", errors.Wrap(err, "Unable to create cache dir")
} }

View file

@ -19,18 +19,16 @@ func init() {
type displayElementPulseVolume struct{} 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 { if pulseClient == nil {
return errors.New("PulseAudio client not initialized") return errors.New("PulseAudio client not initialized")
} }
devType, ok := attributes["device"].(string) if attributes.Device == "" {
if !ok {
return errors.New("Missing 'device' attribute") return errors.New("Missing 'device' attribute")
} }
match, ok := attributes["match"].(string) if attributes.Match == "" {
if !ok {
return errors.New("Missing 'match' attribute") return errors.New("Missing 'match' attribute")
} }
@ -41,19 +39,19 @@ func (d displayElementPulseVolume) Display(ctx context.Context, idx int, attribu
volume float64 volume float64
) )
switch devType { switch attributes.Device {
case "input": case "input":
volume, mute, _, _, err = pulseClient.GetSinkInputVolume(match) volume, mute, _, _, err = pulseClient.GetSinkInputVolume(attributes.Match)
case "sink": case "sink":
volume, mute, _, _, err = pulseClient.GetSinkVolume(match) volume, mute, _, _, err = pulseClient.GetSinkVolume(attributes.Match)
case "source": case "source":
volume, mute, _, _, err = pulseClient.GetSourceVolume(match) volume, mute, _, _, err = pulseClient.GetSourceVolume(attributes.Match)
default: 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 // Initialize color
if rgba, ok := attributes["color"].([]interface{}); ok { if attributes.RGBA != nil {
if len(rgba) != 4 { if len(attributes.RGBA) != 4 {
return errors.New("RGBA color definition needs 4 hex values") return errors.New("RGBA color definition needs 4 hex values")
} }
tmpCol := color.RGBA{} textColor = attributes.RGBAToColor()
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
} }
// Initialize fontsize // Initialize fontsize
var fontsize float64 = 120 var fontsize float64 = 120
if v, ok := attributes["font_size"].(float64); ok { if attributes.FontSize != nil {
fontsize = v fontsize = *attributes.FontSize
} }
var border = 10 border := 10
if v, ok := attributes["border"].(int); ok { if attributes.Border != nil {
border = v border = *attributes.Border
} }
if err = img.DrawBigText(text, fontsize, border, textColor); err != nil { if err = img.DrawBigText(text, fontsize, border, textColor); err != nil {
return errors.Wrap(err, "Unable to draw text") return errors.Wrap(err, "Unable to draw text")
} }
if caption, ok := attributes["caption"].(string); ok && strings.TrimSpace(caption) != "" { if strings.TrimSpace(attributes.Caption) != "" {
if err = img.DrawCaptionText(strings.TrimSpace(caption)); err != nil { if err = img.DrawCaptionText(strings.TrimSpace(attributes.Caption)); err != nil {
return errors.Wrap(err, "Unable to render caption") 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") 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 { func (d *displayElementPulseVolume) StartLoopDisplay(ctx context.Context, idx int, attributes attributeCollection) error {
var interval = time.Second interval := time.Second
if v, ok := attributes["interval"].(int); ok { if attributes.Interval > 0 {
interval = time.Duration(v) * time.Second interval = attributes.Interval
} }
go func() { go func() {

View file

@ -17,59 +17,48 @@ type displayElementText struct {
running bool 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 ( var (
err error err error
imgRenderer = newTextOnImageRenderer() imgRenderer = newTextOnImageRenderer()
) )
// Initialize background // Initialize background
if filename, ok := attributes["image"].(string); ok { if attributes.Image != "" {
if err = imgRenderer.DrawBackgroundFromFile(filename); err != nil { if err = imgRenderer.DrawBackgroundFromFile(attributes.Image); err != nil {
return errors.Wrap(err, "Unable to draw background from disk") return errors.Wrap(err, "Unable to draw background from disk")
} }
} }
// Initialize color // Initialize color
var textColor color.Color = color.RGBA{0xff, 0xff, 0xff, 0xff} var textColor color.Color = color.RGBA{0xff, 0xff, 0xff, 0xff}
if rgba, ok := attributes["color"].([]interface{}); ok { if attributes.RGBA != nil {
if len(rgba) != 4 { if len(attributes.RGBA) != 4 {
return errors.New("RGBA color definition needs 4 hex values") return errors.New("RGBA color definition needs 4 hex values")
} }
tmpCol := color.RGBA{} textColor = attributes.RGBAToColor()
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
} }
// Initialize fontsize // Initialize fontsize
var fontsize float64 = 120 var fontsize float64 = 120
if v, ok := attributes["font_size"].(float64); ok { if attributes.FontSize != nil {
fontsize = v fontsize = *attributes.FontSize
} }
border := 10 border := 10
if v, ok := attributes["border"].(int); ok { if attributes.Border != nil {
border = v border = *attributes.Border
} }
if strings.TrimSpace(attributes["text"].(string)) != "" { if strings.TrimSpace(attributes.Text) != "" {
if err = imgRenderer.DrawBigText(strings.TrimSpace(attributes["text"].(string)), fontsize, border, textColor); err != nil { if err = imgRenderer.DrawBigText(strings.TrimSpace(attributes.Text), fontsize, border, textColor); err != nil {
return errors.Wrap(err, "Unable to render text") return errors.Wrap(err, "Unable to render text")
} }
} }
if caption, ok := attributes["caption"].(string); ok && strings.TrimSpace(caption) != "" { if strings.TrimSpace(attributes.Caption) != "" {
if err = imgRenderer.DrawCaptionText(strings.TrimSpace(caption)); err != nil { if err = imgRenderer.DrawCaptionText(strings.TrimSpace(attributes.Caption)); err != nil {
return errors.Wrap(err, "Unable to render caption") 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") 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 }

View file

@ -11,21 +11,21 @@ import (
const errorDisplayElementType = "color" const errorDisplayElementType = "color"
var errorDisplayElementAttributes = map[string]interface{}{ var errorDisplayElementAttributes = attributeCollection{
"rgba": []interface{}{0xff, 0x0, 0x0, 0xff}, RGBA: []int{0xff, 0x0, 0x0, 0xff},
} }
type action interface { type action interface {
Execute(attributes map[string]interface{}) error Execute(attributes attributeCollection) error
} }
type displayElement interface { 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 { type refreshingDisplayElement interface {
NeedsLoop(attributes map[string]interface{}) bool NeedsLoop(attributes attributeCollection) bool
StartLoopDisplay(ctx context.Context, idx int, attributes map[string]interface{}) error StartLoopDisplay(ctx context.Context, idx int, attributes attributeCollection) error
StopLoopDisplay() error StopLoopDisplay() error
} }

View file

@ -7,15 +7,15 @@ func applySystemPages(conf *config) {
blankPage.Keys[i] = keyDefinition{ blankPage.Keys[i] = keyDefinition{
Display: dynamicElement{ Display: dynamicElement{
Type: "color", Type: "color",
Attributes: map[string]interface{}{ Attributes: attributeCollection{
"rgba": []interface{}{0x0, 0x0, 0x0, 0xff}, RGBA: []int{0x0, 0x0, 0x0, 0xff},
}, },
}, },
Actions: []dynamicElement{ Actions: []dynamicElement{
{ {
Type: "page", Type: "page",
Attributes: map[string]interface{}{ Attributes: attributeCollection{
"relative": 1, Relative: 1,
}, },
}, },
}, },