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:
Levi Woodard
2026-09-27 18:21:33 -06:00
parent cef6f72712
commit 8cff4f8418
6 changed files with 1399 additions and 3 deletions

View File

@@ -19,6 +19,7 @@ const VendorID = 0x0fd9
// ModelInfo describes hardware-specific constants for a Stream Deck model.
type ModelInfo struct {
Name string
KeyCount int
Cols int
Rows int
@@ -31,9 +32,35 @@ type ModelInfo struct {
// models maps USB product IDs to their hardware specs.
var models = map[uint16]ModelInfo{
0x00ba: {KeyCount: 32, Cols: 8, Rows: 4, ImageWidth: 96, ImageHeight: 96, FlipX: true, FlipY: true}, // XL v2
0x006c: {KeyCount: 32, Cols: 8, Rows: 4, ImageWidth: 96, ImageHeight: 96, FlipX: true, FlipY: true}, // XL v1
0x006d: {KeyCount: 15, Cols: 5, Rows: 3, ImageWidth: 72, ImageHeight: 72, FlipX: true, FlipY: true}, // MK.2
0x00ba: {Name: "Stream Deck XL v2", KeyCount: 32, Cols: 8, Rows: 4, ImageWidth: 96, ImageHeight: 96, FlipX: true, FlipY: true},
0x006c: {Name: "Stream Deck XL v1", KeyCount: 32, Cols: 8, Rows: 4, ImageWidth: 96, ImageHeight: 96, FlipX: true, FlipY: true},
0x006d: {Name: "Stream Deck MK.2", KeyCount: 15, Cols: 5, Rows: 3, ImageWidth: 72, ImageHeight: 72, FlipX: true, FlipY: true},
}
// Lookup returns the hardware spec for a product ID without opening the device.
// Callers that only need geometry (key count, grid shape) can use this while the
// daemon holds the HID handle.
func Lookup(productID uint16) (ModelInfo, bool) {
m, ok := models[productID]
return m, ok
}
// Present reports whether a Stream Deck with the given USB IDs is currently
// enumerated. It does not open the device, so it is safe to call while the
// daemon has it open.
func Present(vendorID, productID uint16) (bool, error) {
if err := hid.Init(); err != nil {
return false, fmt.Errorf("hid init: %w", err)
}
found := false
err := hid.Enumerate(vendorID, productID, func(*hid.DeviceInfo) error {
found = true
return nil
})
if err != nil {
return false, fmt.Errorf("hid enumerate: %w", err)
}
return found, nil
}
const (