effect.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. package main
  2. import (
  3. "errors"
  4. "fmt"
  5. "strconv"
  6. "github.com/spf13/cobra"
  7. intrgb "netdome.biz/paul/qmk-rgb/internal/rgb"
  8. "netdome.biz/paul/qmk-rgb/internal/via"
  9. )
  10. // resolveEffectTargets turns a name into one target per channel. Whether the
  11. // channels were named one by one is passed in, because a name the board does not
  12. // have on a channel it was not asked about is a skip, and the same name on a
  13. // channel it was asked about is a mistake worth reporting.
  14. func resolveEffectTargets(catalog *intrgb.Catalog, name string, channels []via.Channel, named bool) ([]intrgb.EffectTarget, []string, error) {
  15. return intrgb.ResolveEffect(catalog, name, channels, named)
  16. }
  17. func NewEffectCmd() *cobra.Command {
  18. return withZoneArgs(&cobra.Command{
  19. Use: "effect <zone> [name|index]",
  20. Short: "Set or list RGB effects on a zone",
  21. Long: writesLighting("Set the RGB lighting effect on the named zones. Effect names come from a\n" +
  22. "per-board catalog; a keyboard without one has no names and is driven with an\n" +
  23. "effect index instead. A number is an effect ID; anything else is a name.\n" +
  24. "\n" +
  25. "Without a name this lists the effects each named zone has. Listing is not a\n" +
  26. "write, so `effect all` reports every channel the keyboard has."),
  27. RunE: func(cmd *cobra.Command, args []string) error {
  28. if len(args) == 1 {
  29. return listZoneEffects(cmd, args[0])
  30. }
  31. return runEffectSet(cmd, args[0], args[1])
  32. },
  33. }, 1, 2, "a zone, and an effect name or index to set")
  34. }
  35. func listZoneEffects(cmd *cobra.Command, zone string) error {
  36. proto, target, channels, err := openTarget(zone)
  37. if err != nil {
  38. return err
  39. }
  40. defer proto.Close()
  41. catalog, source, err := resolveCatalog(target)
  42. if err != nil {
  43. return err
  44. }
  45. return printEffectList(cmd, catalog, channels, target.Display, source)
  46. }
  47. // printEffectList writes the catalog per channel, as text unless --json asks
  48. // for the machine shape. The source of the names travels along, because a list
  49. // without it does not say whether it came from a definition file or from the
  50. // tool itself.
  51. func printEffectList(cmd *cobra.Command, catalog *intrgb.Catalog, channels []via.Channel, display map[uint16]string, source string) error {
  52. if !jsonOutput {
  53. return printEffectListText(cmd.OutOrStdout(), catalog, channels, display, source)
  54. }
  55. return printEffectListJSON(cmd, catalog, channels, display)
  56. }
  57. func printEffectListJSON(cmd *cobra.Command, catalog *intrgb.Catalog, channels []via.Channel, display map[uint16]string) error {
  58. type ZoneEffectList struct {
  59. Zone string `json:"zone"`
  60. Channel uint8 `json:"channel"`
  61. Subsystem string `json:"subsystem"`
  62. Effect string `json:"effect"`
  63. ID uint8 `json:"id"`
  64. }
  65. type EffectList struct {
  66. Catalog string `json:"catalog"`
  67. Zones []ZoneEffectList `json:"zones"`
  68. }
  69. list := EffectList{Catalog: catalog.Name(), Zones: []ZoneEffectList{}}
  70. for _, ch := range channels {
  71. for _, e := range catalog.Effects(ch) {
  72. list.Zones = append(list.Zones, ZoneEffectList{
  73. Zone: channelName(ch, display),
  74. Channel: uint8(ch),
  75. Subsystem: ch.Subsystem(),
  76. Effect: e.Name,
  77. ID: e.ID,
  78. })
  79. }
  80. }
  81. return encodeJSON(cmd.OutOrStdout(), list)
  82. }
  83. // effectArgument is what was asked for: a name to resolve against the board's
  84. // definition, or a raw effect ID to write.
  85. type effectArgument struct {
  86. Name string
  87. ID uint8
  88. IsID bool
  89. }
  90. // parseEffectArgument reads the argument of the effect command. A number is an
  91. // effect ID and nothing else: a name is never written as digits, so the two
  92. // cannot be confused, and the ID is what a board without a definition needs. A
  93. // number too large to be one is reported as such rather than looked up as a name.
  94. func parseEffectArgument(arg string) (effectArgument, error) {
  95. value, err := strconv.ParseUint(arg, 10, 8)
  96. if err == nil {
  97. return effectArgument{ID: uint8(value), IsID: true}, nil
  98. }
  99. var numErr *strconv.NumError
  100. if errors.As(err, &numErr) && errors.Is(numErr.Err, strconv.ErrRange) {
  101. return effectArgument{}, fmt.Errorf("effect ID %s is out of range: an effect ID is 0-255", arg)
  102. }
  103. return effectArgument{Name: arg}, nil
  104. }
  105. func runEffectSet(cmd *cobra.Command, zone, arg string) error {
  106. proto, target, channels, err := openTarget(zone)
  107. if err != nil {
  108. return err
  109. }
  110. defer proto.Close()
  111. want, err := parseEffectArgument(arg)
  112. if err != nil {
  113. return err
  114. }
  115. if want.IsID {
  116. return setEffectID(cmd, proto, target, channels, want.ID)
  117. }
  118. catalog, _, err := resolveCatalog(target)
  119. if err != nil {
  120. return err
  121. }
  122. // A zone was named, so a channel that cannot do this effect is a mistake and
  123. // not a reason to write the other ones and report a success.
  124. targets, _, err := resolveEffectTargets(catalog, want.Name, channels, true)
  125. if err != nil {
  126. return err
  127. }
  128. results, err := setEffectVerified(proto, targets, target.Display, catalog)
  129. if err != nil {
  130. return err
  131. }
  132. // Claiming the requested effect when the keyboard kept another one is the
  133. // contradiction this read-back exists to prevent: on the logo and side
  134. // channels an effect of ID 0 leaves the previous effect running.
  135. if anyEffectMismatch(results) {
  136. fmt.Fprintln(cmd.OutOrStdout(), formatEffectResults(want.Name, results))
  137. return nil
  138. }
  139. fmt.Fprintf(cmd.OutOrStdout(), "Effect set to %q\n", want.Name)
  140. return nil
  141. }
  142. // setEffectID writes a raw effect ID to every selected channel. It needs no
  143. // catalog, which is the point: an ID is the one thing a board always has, and
  144. // which ID is which effect is a question the board does not answer.
  145. func setEffectID(cmd *cobra.Command, proto rgbProtocol, target targetDeviceData, channels []via.Channel, id uint8) error {
  146. targets := make([]intrgb.EffectTarget, 0, len(channels))
  147. for _, ch := range channels {
  148. targets = append(targets, intrgb.EffectTarget{Channel: ch, ID: id})
  149. }
  150. results, err := setEffectVerified(proto, targets, target.Display, nil)
  151. if err != nil {
  152. return err
  153. }
  154. // The numbers are what the request was, so the numbers are what the summary
  155. // states: a name would be one the board may not even have.
  156. if anyEffectMismatch(results) {
  157. fmt.Fprintln(cmd.OutOrStdout(), formatEffectIDResults(results))
  158. return nil
  159. }
  160. fmt.Fprintf(cmd.OutOrStdout(), "Effect set to index %d\n", id)
  161. return nil
  162. }