output.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. package main
  2. import (
  3. "fmt"
  4. "io"
  5. "strings"
  6. "github.com/spf13/cobra"
  7. "netdome.biz/paul/qmk-rgb/internal/device"
  8. intrgb "netdome.biz/paul/qmk-rgb/internal/rgb"
  9. "netdome.biz/paul/qmk-rgb/internal/via"
  10. )
  11. // The commands below print text, because that is what a person reads, and JSON
  12. // only when it is asked for. The flag is persistent, so it reads the same
  13. // everywhere: `qmk-rgb-tool --json info` and `qmk-rgb-tool info --json` are the
  14. // same command. An agent that parses the output passes it; a person does not
  15. // have to learn a second shape for the same information.
  16. var jsonOutput bool
  17. func addJSONFlag(cmd *cobra.Command) {
  18. cmd.PersistentFlags().BoolVar(&jsonOutput, "json", false, "Print JSON instead of text")
  19. }
  20. // printEffectListText writes one effect per line, its ID in brackets, under a
  21. // heading per channel. The ID is in the line because it is what a user needs to
  22. // check a name against the register, and what `effect <index>` takes.
  23. func printEffectListText(out io.Writer, catalog *intrgb.Catalog, channels []via.Channel, display map[uint16]string, source string) error {
  24. for _, ch := range channels {
  25. effects := catalog.Effects(ch)
  26. if len(effects) == 0 {
  27. continue
  28. }
  29. fmt.Fprintf(out, "%s (%s)\n", channelName(ch, display), ch.Subsystem())
  30. for _, e := range effects {
  31. fmt.Fprintf(out, " %s (%d)\n", e.Name, e.ID)
  32. }
  33. }
  34. if source != "" {
  35. fmt.Fprintf(out, "\nNames from %s\n", source)
  36. }
  37. return nil
  38. }
  39. // printZoneInfoText writes one line per zone, which is what a person compares,
  40. // and nothing else. A header naming the keyboard's "mode" was the first zone's
  41. // effect, so a board running three different effects printed one of them twice,
  42. // as a line above the others that looked like it applied to all of them. The
  43. // effect of every channel is on its own line; `mode` remains in the JSON, which
  44. // is a shape this tool has always had.
  45. func printZoneInfoText(out io.Writer, state infoOutput) error {
  46. for _, z := range state.Zones {
  47. enabled := "off"
  48. if z.Enabled {
  49. enabled = "on"
  50. }
  51. fmt.Fprintf(out, " %-12s %-4s %-28s brightness %3d speed %3d color %s\n",
  52. z.Zone, enabled, z.Effect, z.Brightness, z.Speed, formatInfoColor(z.Color))
  53. }
  54. return nil
  55. }
  56. // formatInfoColor renders a hue and saturation the way the color command takes
  57. // them, so a value read from the keyboard can be written back unchanged.
  58. func formatInfoColor(c infoColor) string {
  59. return fmt.Sprintf("hsv:%d,%d", c.Hue, c.Saturation)
  60. }
  61. // printKeyboardInfoText writes one line per keyboard, with the index --device
  62. // takes, because the index is the only part a user has to act on.
  63. //
  64. // An empty result explains itself, because it is the one case a user cannot read
  65. // on their own: "no keyboard" is what a board that is not connected looks like,
  66. // and it is also what a board that is connected but not reachable looks like.
  67. // The HID devices that were passed over are what tells the two apart — a keyboard
  68. // over USB has a keyboard collection, a consumer control and usually a vendor
  69. // page, and it is the vendor page at 0xFF60/0x61 that is missing.
  70. //
  71. // Those devices are listed only when there is no keyboard. On a Mac there are
  72. // around forty HID devices that are never a keyboard, and putting them beside a
  73. // successful listing buries the line the user came for; a consumer that wants
  74. // them regardless reads them from the JSON shape, which always carries them.
  75. func printKeyboardInfoText(out io.Writer, devices []keyboardLine, others []device.HIDDevice) error {
  76. if len(devices) == 0 {
  77. fmt.Fprintln(out, "No keyboard with the QMK Raw HID interface found: usage page 0xFF60, usage 0x61.")
  78. fmt.Fprintln(out, "A QMK firmware has one when Raw HID is enabled; VIA's build cannot be built without it.")
  79. if len(others) == 0 {
  80. fmt.Fprintln(out, "No other HID device is connected, so no keyboard is plugged in at all.")
  81. return nil
  82. }
  83. fmt.Fprintln(out, "\nThese HID devices are connected, and none of them has that collection:")
  84. for _, o := range others {
  85. fmt.Fprintf(out, " 0x%04X/0x%04X %s\n", o.VendorID, o.ProductID, hidDeviceName(o))
  86. }
  87. return nil
  88. }
  89. for _, d := range devices {
  90. line := fmt.Sprintf("%d %s 0x%04X/0x%04X", d.Index, d.Name, d.VendorID, d.ProductID)
  91. if d.Effects {
  92. line += " (effect names)"
  93. }
  94. fmt.Fprintln(out, line)
  95. }
  96. return nil
  97. }
  98. // hidDeviceName is what a passed-over HID device is called in the listing, with
  99. // the usage pages it does expose. The pages are the finding: one of them being
  100. // 0xFF80/0x61 rather than 0xFF60/0x61 is a board that put its lighting on a
  101. // vendor page this tool does not address, and a reader should not have to run a
  102. // program to learn that.
  103. func hidDeviceName(o device.HIDDevice) string {
  104. name := o.Name
  105. if name == "" {
  106. name = "no product string"
  107. }
  108. pages := make([]string, 0, len(o.UsagePairs))
  109. for _, p := range o.UsagePairs {
  110. pages = append(pages, fmt.Sprintf("0x%04X/0x%02X", p.UsagePage, p.Usage))
  111. }
  112. return fmt.Sprintf("%-28s %s", name, strings.Join(pages, " "))
  113. }
  114. // keyboardLine is one keyboard as the info command reports it. Whether it has
  115. // effect names is a fact about the tool, not about a data file: it is true when
  116. // a definition covers the board or a catalog is compiled in for it. Its channels
  117. // are deliberately absent, because this command does not open the board and
  118. // cannot know which ones it has.
  119. type keyboardLine struct {
  120. Index int `json:"index"`
  121. Path string `json:"path"`
  122. VendorID uint16 `json:"vendorId"`
  123. ProductID uint16 `json:"productId"`
  124. Name string `json:"name"`
  125. Effects bool `json:"effects"`
  126. }
  127. // describeDevices turns discovered devices into what the info command reports:
  128. // the name a board answers to and whether this tool has effect names for it.
  129. func describeDevices(devices []device.Device) []keyboardLine {
  130. lines := make([]keyboardLine, 0, len(devices))
  131. for _, d := range devices {
  132. display, _ := applyDefinitionLabels(d.VendorID, d.ProductID)
  133. catalog, _, _ := resolveCatalogFor(d.VendorID, d.ProductID)
  134. // No channel list here: this command does not open the board, so it
  135. // cannot know which channels it has, and reporting the ones its
  136. // definition names would claim more than it knows. `info` opens the
  137. // keyboard and reports those.
  138. lines = append(lines, keyboardLine{
  139. Index: d.Index,
  140. Path: d.Path,
  141. VendorID: d.VendorID,
  142. ProductID: d.ProductID,
  143. Name: boardName(d, display),
  144. Effects: catalog != nil,
  145. })
  146. }
  147. return lines
  148. }