| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- 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 <index>` 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
- }
|