package main import ( "fmt" "io" "strings" "github.com/spf13/cobra" "netdome.biz/paul/qmk-rgb/internal/device" intrgb "netdome.biz/paul/qmk-rgb/internal/rgb" "netdome.biz/paul/qmk-rgb/internal/via" ) // The commands below print text, because that is what a person reads, and JSON // only when it is asked for. The flag is persistent, so it reads the same // everywhere: `qmk-rgb-tool --json info` and `qmk-rgb-tool info --json` are the // same command. An agent that parses the output passes it; a person does not // have to learn a second shape for the same information. var jsonOutput bool func addJSONFlag(cmd *cobra.Command) { cmd.PersistentFlags().BoolVar(&jsonOutput, "json", false, "Print JSON instead of text") } // printEffectListText writes one effect per line, its ID in brackets, under a // heading per channel. The ID is in the line because it is what a user needs to // check a name against the register, and what `effect ` takes. func printEffectListText(out io.Writer, catalog *intrgb.Catalog, channels []via.Channel, display map[uint16]string, source string) error { for _, ch := range channels { effects := catalog.Effects(ch) if len(effects) == 0 { continue } fmt.Fprintf(out, "%s (%s)\n", channelName(ch, display), ch.Subsystem()) for _, e := range effects { fmt.Fprintf(out, " %s (%d)\n", e.Name, e.ID) } } if source != "" { fmt.Fprintf(out, "\nNames from %s\n", source) } return nil } // printZoneInfoText writes one line per zone, which is what a person compares, // and nothing else. A header naming the keyboard's "mode" was the first zone's // effect, so a board running three different effects printed one of them twice, // as a line above the others that looked like it applied to all of them. The // effect of every channel is on its own line; `mode` remains in the JSON, which // is a shape this tool has always had. func printZoneInfoText(out io.Writer, state infoOutput) error { for _, z := range state.Zones { enabled := "off" if z.Enabled { enabled = "on" } fmt.Fprintf(out, " %-12s %-4s %-28s brightness %3d speed %3d color %s\n", z.Zone, enabled, z.Effect, z.Brightness, z.Speed, formatInfoColor(z.Color)) } return nil } // formatInfoColor renders a hue and saturation the way the color command takes // them, so a value read from the keyboard can be written back unchanged. func formatInfoColor(c infoColor) string { return fmt.Sprintf("hsv:%d,%d", c.Hue, c.Saturation) } // printKeyboardInfoText writes one line per keyboard, with the index --device // takes, because the index is the only part a user has to act on. // // An empty result explains itself, because it is the one case a user cannot read // on their own: "no keyboard" is what a board that is not connected looks like, // and it is also what a board that is connected but not reachable looks like. // The HID devices that were passed over are what tells the two apart — a keyboard // over USB has a keyboard collection, a consumer control and usually a vendor // page, and it is the vendor page at 0xFF60/0x61 that is missing. // // Those devices are listed only when there is no keyboard. On a Mac there are // around forty HID devices that are never a keyboard, and putting them beside a // successful listing buries the line the user came for; a consumer that wants // them regardless reads them from the JSON shape, which always carries them. func printKeyboardInfoText(out io.Writer, devices []keyboardLine, others []device.HIDDevice) error { if len(devices) == 0 { fmt.Fprintln(out, "No keyboard with the QMK Raw HID interface found: usage page 0xFF60, usage 0x61.") fmt.Fprintln(out, "A QMK firmware has one when Raw HID is enabled; VIA's build cannot be built without it.") if len(others) == 0 { fmt.Fprintln(out, "No other HID device is connected, so no keyboard is plugged in at all.") return nil } fmt.Fprintln(out, "\nThese HID devices are connected, and none of them has that collection:") for _, o := range others { fmt.Fprintf(out, " 0x%04X/0x%04X %s\n", o.VendorID, o.ProductID, hidDeviceName(o)) } return nil } for _, d := range devices { line := fmt.Sprintf("%d %s 0x%04X/0x%04X", d.Index, d.Name, d.VendorID, d.ProductID) if d.Effects { line += " (effect names)" } fmt.Fprintln(out, line) } return nil } // hidDeviceName is what a passed-over HID device is called in the listing, with // the usage pages it does expose. The pages are the finding: one of them being // 0xFF80/0x61 rather than 0xFF60/0x61 is a board that put its lighting on a // vendor page this tool does not address, and a reader should not have to run a // program to learn that. func hidDeviceName(o device.HIDDevice) string { name := o.Name if name == "" { name = "no product string" } pages := make([]string, 0, len(o.UsagePairs)) for _, p := range o.UsagePairs { pages = append(pages, fmt.Sprintf("0x%04X/0x%02X", p.UsagePage, p.Usage)) } return fmt.Sprintf("%-28s %s", name, strings.Join(pages, " ")) } // keyboardLine is one keyboard as the info command reports it. Whether it has // effect names is a fact about the tool, not about a data file: it is true when // a definition covers the board or a catalog is compiled in for it. Its channels // are deliberately absent, because this command does not open the board and // cannot know which ones it has. type keyboardLine struct { Index int `json:"index"` Path string `json:"path"` VendorID uint16 `json:"vendorId"` ProductID uint16 `json:"productId"` Name string `json:"name"` Effects bool `json:"effects"` } // describeDevices turns discovered devices into what the info command reports: // the name a board answers to and whether this tool has effect names for it. func describeDevices(devices []device.Device) []keyboardLine { lines := make([]keyboardLine, 0, len(devices)) for _, d := range devices { display, _ := applyDefinitionLabels(d.VendorID, d.ProductID) catalog, _, _ := resolveCatalogFor(d.VendorID, d.ProductID) // No channel list here: this command does not open the board, so it // cannot know which channels it has, and reporting the ones its // definition names would claim more than it knows. `info` opens the // keyboard and reports those. lines = append(lines, keyboardLine{ Index: d.Index, Path: d.Path, VendorID: d.VendorID, ProductID: d.ProductID, Name: boardName(d, display), Effects: catalog != nil, }) } return lines }