streamdeck-ctl reports device/daemon/key status as JSON and edits the config (brightness, key set/clear/press, daemon control) so front-ends don't have to reimplement YAML handling or device probing. - internal/config/edit.go: comment-preserving, node-level YAML edits with atomic writes; key indices written as !!int so config.Load accepts them. - internal/device: add model names, Lookup() and Present() for probing without opening the HID handle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BjuZBBfzXqZgxRhxzJvkkC
127 lines
3.2 KiB
Go
127 lines
3.2 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func writeConfig(t *testing.T, content string) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "config.yaml")
|
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
// Regression: a new key's index must be written as an int scalar. A !!str key
|
|
// ("25":) makes config.Load fail — yaml.v3 refuses string keys for map[int] —
|
|
// which bricked the config on the daemon's next hot reload.
|
|
func TestApplyKeyEditNewSlotLoadsBack(t *testing.T) {
|
|
path := writeConfig(t, "icons_dir: ./icons\nbrightness: 70\nkeys: {}\n")
|
|
|
|
err := ApplyKeyEdit(path, 25, KeyEdit{Set: map[string]any{
|
|
"icon": "test.png",
|
|
"command": "echo hi",
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("ApplyKeyEdit: %v", err)
|
|
}
|
|
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load after edit: %v", err)
|
|
}
|
|
key, ok := cfg.Keys[25]
|
|
if !ok {
|
|
t.Fatalf("key 25 missing after edit; keys=%v", cfg.Keys)
|
|
}
|
|
if key.Icon != "test.png" || key.Command != "echo hi" {
|
|
t.Fatalf("key 25 = %+v", key)
|
|
}
|
|
|
|
raw, _ := os.ReadFile(path)
|
|
if strings.Contains(string(raw), `"25"`) {
|
|
t.Fatalf("index written as a quoted string:\n%s", raw)
|
|
}
|
|
}
|
|
|
|
// Editing an existing key must merge, and comments/blank lines must survive.
|
|
func TestApplyKeyEditMergePreservesFormatting(t *testing.T) {
|
|
path := writeConfig(t, `icons_dir: ./icons
|
|
|
|
# the deck
|
|
keys:
|
|
0:
|
|
icon: a.png # keep me
|
|
command: run-a
|
|
`)
|
|
|
|
if err := ApplyKeyEdit(path, 0, KeyEdit{Set: map[string]any{"text": "Hi"}}); err != nil {
|
|
t.Fatalf("ApplyKeyEdit: %v", err)
|
|
}
|
|
|
|
raw, _ := os.ReadFile(path)
|
|
text := string(raw)
|
|
for _, want := range []string{"# the deck", "# keep me", "command: run-a", "text: Hi", "\n\n"} {
|
|
if !strings.Contains(text, want) {
|
|
t.Fatalf("output missing %q:\n%s", want, text)
|
|
}
|
|
}
|
|
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if cfg.Keys[0].Icon != "a.png" || cfg.Keys[0].Text != "Hi" {
|
|
t.Fatalf("merge lost fields: %+v", cfg.Keys[0])
|
|
}
|
|
}
|
|
|
|
// Unset is applied before Set, so a field named in both ends up set.
|
|
func TestApplyKeyEditSetWinsOverUnset(t *testing.T) {
|
|
path := writeConfig(t, "keys:\n 0:\n command: old\n")
|
|
|
|
err := ApplyKeyEdit(path, 0, KeyEdit{
|
|
Set: map[string]any{"command": "new"},
|
|
Unset: []string{"command"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ApplyKeyEdit: %v", err)
|
|
}
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if cfg.Keys[0].Command != "new" {
|
|
t.Fatalf("command = %q, want %q", cfg.Keys[0].Command, "new")
|
|
}
|
|
}
|
|
|
|
func TestClearKeyAndSetBrightness(t *testing.T) {
|
|
path := writeConfig(t, "brightness: 70\nkeys:\n 3:\n command: x\n")
|
|
|
|
if err := ClearKey(path, 3); err != nil {
|
|
t.Fatalf("ClearKey: %v", err)
|
|
}
|
|
if err := ClearKey(path, 99); err != nil { // absent key is not an error
|
|
t.Fatalf("ClearKey absent: %v", err)
|
|
}
|
|
if err := SetBrightness(path, 140); err != nil { // clamped
|
|
t.Fatalf("SetBrightness: %v", err)
|
|
}
|
|
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if len(cfg.Keys) != 0 {
|
|
t.Fatalf("keys not cleared: %v", cfg.Keys)
|
|
}
|
|
if cfg.Brightness != 100 {
|
|
t.Fatalf("brightness = %d, want 100", cfg.Brightness)
|
|
}
|
|
}
|