package main import ( "errors" "fmt" "strconv" "github.com/spf13/cobra" intrgb "netdome.biz/paul/qmk-rgb/internal/rgb" "netdome.biz/paul/qmk-rgb/internal/via" ) // resolveEffectTargets turns a name into one target per channel. Whether the // channels were named one by one is passed in, because a name the board does not // have on a channel it was not asked about is a skip, and the same name on a // channel it was asked about is a mistake worth reporting. func resolveEffectTargets(catalog *intrgb.Catalog, name string, channels []via.Channel, named bool) ([]intrgb.EffectTarget, []string, error) { return intrgb.ResolveEffect(catalog, name, channels, named) } func NewEffectCmd() *cobra.Command { return withZoneArgs(&cobra.Command{ Use: "effect [name|index]", Short: "Set or list RGB effects on a zone", Long: writesLighting("Set the RGB lighting effect on the named zones. Effect names come from a\n" + "per-board catalog; a keyboard without one has no names and is driven with an\n" + "effect index instead. A number is an effect ID; anything else is a name.\n" + "\n" + "Without a name this lists the effects each named zone has. Listing is not a\n" + "write, so `effect all` reports every channel the keyboard has."), RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 1 { return listZoneEffects(cmd, args[0]) } return runEffectSet(cmd, args[0], args[1]) }, }, 1, 2, "a zone, and an effect name or index to set") } func listZoneEffects(cmd *cobra.Command, zone string) error { proto, target, channels, err := openTarget(zone) if err != nil { return err } defer proto.Close() catalog, source, err := resolveCatalog(target) if err != nil { return err } return printEffectList(cmd, catalog, channels, target.Display, source) } // printEffectList writes the catalog per channel, as text unless --json asks // for the machine shape. The source of the names travels along, because a list // without it does not say whether it came from a definition file or from the // tool itself. func printEffectList(cmd *cobra.Command, catalog *intrgb.Catalog, channels []via.Channel, display map[uint16]string, source string) error { if !jsonOutput { return printEffectListText(cmd.OutOrStdout(), catalog, channels, display, source) } return printEffectListJSON(cmd, catalog, channels, display) } func printEffectListJSON(cmd *cobra.Command, catalog *intrgb.Catalog, channels []via.Channel, display map[uint16]string) error { type ZoneEffectList struct { Zone string `json:"zone"` Channel uint8 `json:"channel"` Subsystem string `json:"subsystem"` Effect string `json:"effect"` ID uint8 `json:"id"` } type EffectList struct { Catalog string `json:"catalog"` Zones []ZoneEffectList `json:"zones"` } list := EffectList{Catalog: catalog.Name(), Zones: []ZoneEffectList{}} for _, ch := range channels { for _, e := range catalog.Effects(ch) { list.Zones = append(list.Zones, ZoneEffectList{ Zone: channelName(ch, display), Channel: uint8(ch), Subsystem: ch.Subsystem(), Effect: e.Name, ID: e.ID, }) } } 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, zone, arg string) error { proto, target, channels, err := openTarget(zone) if err != nil { return err } defer proto.Close() want, err := parseEffectArgument(arg) if err != nil { return err } if want.IsID { return setEffectID(cmd, proto, target, channels, want.ID) } catalog, _, err := resolveCatalog(target) if err != nil { return err } // A zone was named, so a channel that cannot do this effect is a mistake and // not a reason to write the other ones and report a success. targets, _, err := resolveEffectTargets(catalog, want.Name, channels, true) if err != nil { return err } results, err := setEffectVerified(proto, targets, target.Display, catalog) if err != nil { return err } // Claiming the requested effect when the keyboard kept another one is the // contradiction this read-back exists to prevent: on the logo and side // channels an effect of ID 0 leaves the previous effect running. if anyEffectMismatch(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 } fmt.Fprintf(cmd.OutOrStdout(), "Effect set to index %d\n", id) return nil }