Add streamdeck-ctl JSON control CLI
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
This commit is contained in:
126
internal/config/edit_test.go
Normal file
126
internal/config/edit_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user