|
@@ -1,7 +1,9 @@
|
|
|
package main
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
import (
|
|
|
|
|
+ "errors"
|
|
|
"fmt"
|
|
"fmt"
|
|
|
|
|
+ "strconv"
|
|
|
"strings"
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/cobra"
|
|
@@ -24,7 +26,8 @@ func NewEffectCmd() *cobra.Command {
|
|
|
Short: "Set or list RGB effects",
|
|
Short: "Set or list RGB effects",
|
|
|
Long: "Set the RGB lighting effect on the connected keyboard. Without an argument, lists all effects.\n" +
|
|
Long: "Set the RGB lighting effect on the connected keyboard. Without an argument, lists all effects.\n" +
|
|
|
"Effect names come from a per-board catalog; a keyboard without one has no names\n" +
|
|
"Effect names come from a per-board catalog; a keyboard without one has no names\n" +
|
|
|
- "and is driven with `mode <index>` instead.",
|
|
|
|
|
|
|
+ "and is driven with `effect <index>` instead.\n" +
|
|
|
|
|
+ "A number is an effect ID; anything else is a name.",
|
|
|
Args: cobra.MaximumNArgs(1),
|
|
Args: cobra.MaximumNArgs(1),
|
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
|
if listEffects || len(args) == 0 {
|
|
if listEffects || len(args) == 0 {
|
|
@@ -91,6 +94,30 @@ func printEffectListJSON(cmd *cobra.Command, catalog *intrgb.Catalog, channels [
|
|
|
return encodeJSON(cmd.OutOrStdout(), list)
|
|
return encodeJSON(cmd.OutOrStdout(), list)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// effectArgument is what was asked for: a name to resolve against the board's
|
|
|
|
|
+// definition, or a raw effect ID to write.
|
|
|
|
|
+type effectArgument struct {
|
|
|
|
|
+ Name string
|
|
|
|
|
+ ID uint8
|
|
|
|
|
+ IsID bool
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// parseEffectArgument reads the argument of the effect command. A number is an
|
|
|
|
|
+// effect ID and nothing else: a name is never written as digits, so the two
|
|
|
|
|
+// cannot be confused, and the ID is what a board without a definition needs. A
|
|
|
|
|
+// number too large to be one is reported as such rather than looked up as a name.
|
|
|
|
|
+func parseEffectArgument(arg string) (effectArgument, error) {
|
|
|
|
|
+ value, err := strconv.ParseUint(arg, 10, 8)
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ return effectArgument{ID: uint8(value), IsID: true}, nil
|
|
|
|
|
+ }
|
|
|
|
|
+ var numErr *strconv.NumError
|
|
|
|
|
+ if errors.As(err, &numErr) && errors.Is(numErr.Err, strconv.ErrRange) {
|
|
|
|
|
+ return effectArgument{}, fmt.Errorf("effect ID %s is out of range: an effect ID is 0-255", arg)
|
|
|
|
|
+ }
|
|
|
|
|
+ return effectArgument{Name: arg}, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
func runEffectSet(cmd *cobra.Command, args []string) error {
|
|
func runEffectSet(cmd *cobra.Command, args []string) error {
|
|
|
proto, target, channels, err := openTarget()
|
|
proto, target, channels, err := openTarget()
|
|
|
if err != nil {
|
|
if err != nil {
|
|
@@ -98,11 +125,19 @@ func runEffectSet(cmd *cobra.Command, args []string) error {
|
|
|
}
|
|
}
|
|
|
defer proto.Close()
|
|
defer proto.Close()
|
|
|
|
|
|
|
|
|
|
+ want, err := parseEffectArgument(args[0])
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ if want.IsID {
|
|
|
|
|
+ return setEffectID(cmd, proto, target, channels, want.ID)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
catalog, _, err := resolveCatalog(target)
|
|
catalog, _, err := resolveCatalog(target)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
return err
|
|
return err
|
|
|
}
|
|
}
|
|
|
- targets, skipped, err := resolveEffectTargets(catalog, args[0], channels)
|
|
|
|
|
|
|
+ targets, skipped, err := resolveEffectTargets(catalog, want.Name, channels)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
return err
|
|
return err
|
|
|
}
|
|
}
|
|
@@ -119,10 +154,35 @@ func runEffectSet(cmd *cobra.Command, args []string) error {
|
|
|
// contradiction this read-back exists to prevent: on the logo and side
|
|
// contradiction this read-back exists to prevent: on the logo and side
|
|
|
// channels an effect of ID 0 leaves the previous effect running.
|
|
// channels an effect of ID 0 leaves the previous effect running.
|
|
|
if anyEffectMismatch(results) {
|
|
if anyEffectMismatch(results) {
|
|
|
- fmt.Fprintln(cmd.OutOrStdout(), formatEffectResults(args[0], results))
|
|
|
|
|
|
|
+ fmt.Fprintln(cmd.OutOrStdout(), formatEffectResults(want.Name, results))
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fmt.Fprintf(cmd.OutOrStdout(), "Effect set to %q\n", want.Name)
|
|
|
|
|
+ return nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// setEffectID writes a raw effect ID to every selected channel. It needs no
|
|
|
|
|
+// catalog, which is the point: an ID is the one thing a board always has, and
|
|
|
|
|
+// which ID is which effect is a question the board does not answer.
|
|
|
|
|
+func setEffectID(cmd *cobra.Command, proto rgbProtocol, target targetDeviceData, channels []via.Channel, id uint8) error {
|
|
|
|
|
+ targets := make([]intrgb.EffectTarget, 0, len(channels))
|
|
|
|
|
+ for _, ch := range channels {
|
|
|
|
|
+ targets = append(targets, intrgb.EffectTarget{Channel: ch, ID: id})
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ results, err := setEffectVerified(proto, targets, target.Display, nil)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // The numbers are what the request was, so the numbers are what the summary
|
|
|
|
|
+ // states: a name would be one the board may not even have.
|
|
|
|
|
+ if anyEffectMismatch(results) {
|
|
|
|
|
+ fmt.Fprintln(cmd.OutOrStdout(), formatEffectIDResults(results))
|
|
|
return nil
|
|
return nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- fmt.Fprintf(cmd.OutOrStdout(), "Effect set to %q\n", args[0])
|
|
|
|
|
|
|
+ fmt.Fprintf(cmd.OutOrStdout(), "Effect set to index %d\n", id)
|
|
|
return nil
|
|
return nil
|
|
|
}
|
|
}
|