package device import ( "encoding/json" "fmt" "os" "path/filepath" "sort" "netdome.biz/paul/qmk-rgb/internal/hid" ) // Keyboard is a metadata record from keyboards.json. // It names a known keyboard, but it is not required for discovery: unknown // QMK keyboards are usable too. Fields the tool does not read are ignored, // so existing entries may keep carrying them. type Keyboard struct { Name string `json:"name"` VendorID uint16 `json:"vendorId"` ProductID uint16 `json:"productId"` } // Device is a connected QMK Raw HID keyboard. // Index is 1-based and is what --device accepts. type Device struct { Index int `json:"index"` Path string `json:"path"` VendorID uint16 `json:"vendorId"` ProductID uint16 `json:"productId"` Name string `json:"name,omitempty"` Known bool `json:"known"` } // DiscoverAll returns every connected QMK Raw HID keyboard, ordered // deterministically and numbered from 1. Keyboards absent from // keyboards.json are included with Known false and no Name. func DiscoverAll() ([]Device, error) { infos, err := hid.DiscoverAll() if err != nil { return nil, fmt.Errorf("discover qmk devices: %w", err) } known, err := knownKeyboards() if err != nil { // Missing or malformed keyboards.json only costs us names, not // the ability to drive the keyboard. known = nil } devices := make([]Device, 0, len(infos)) for _, info := range infos { kb, ok := known[deviceKey(info.VendorID, info.ProductID)] devices = append(devices, Device{ Path: info.Path, VendorID: info.VendorID, ProductID: info.ProductID, Name: kb.Name, Known: ok, }) } return indexDevices(devices), nil } // indexDevices sorts devices deterministically and assigns 1-based indexes. // The input slice is left untouched. func indexDevices(devices []Device) []Device { sorted := make([]Device, len(devices)) copy(sorted, devices) sort.SliceStable(sorted, func(i, j int) bool { a, b := sorted[i], sorted[j] switch { case a.VendorID != b.VendorID: return a.VendorID < b.VendorID case a.ProductID != b.ProductID: return a.ProductID < b.ProductID default: return a.Path < b.Path } }) for i := range sorted { sorted[i].Index = i + 1 } return sorted } func deviceKey(vendorID, productID uint16) string { return fmt.Sprintf("%04x:%04x", vendorID, productID) } func knownKeyboards() (map[string]Keyboard, error) { keyboards, err := LoadKeyboards() if err != nil { return nil, err } known := make(map[string]Keyboard, len(keyboards)) for _, kb := range keyboards { known[deviceKey(kb.VendorID, kb.ProductID)] = kb } return known, nil } // LoadKeyboards reads keyboard definitions from keyboards.json. func LoadKeyboards() ([]Keyboard, error) { path, err := findKeyboardsJSON() if err != nil { return nil, fmt.Errorf("find keyboards.json: %w", err) } data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read keyboards.json: %w", err) } var keyboards []Keyboard if err := json.Unmarshal(data, &keyboards); err != nil { return nil, fmt.Errorf("parse keyboards.json: %w", err) } return keyboards, nil } // findKeyboardsJSON searches for keyboards.json in common locations. var findKeyboardsJSON = func() (string, error) { exe, err := os.Executable() if err == nil { path := filepath.Join(filepath.Dir(exe), "keyboards.json") if _, err := os.Stat(path); err == nil { return path, nil } } path := "keyboards.json" if _, err := os.Stat(path); err == nil { return path, nil } cwd, err := os.Getwd() if err == nil { path := filepath.Join(cwd, "keyboards.json") if _, err := os.Stat(path); err == nil { return path, nil } } return "", fmt.Errorf("keyboards.json not found") }