effect.go 6.0 KB

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