package main import ( "errors" "fmt" "strconv" "strings" "github.com/spf13/cobra" intrgb "netdome.biz/paul/qmk-rgb/internal/rgb" "netdome.biz/paul/qmk-rgb/internal/via" ) var listEffects bool // resolveEffectTargets turns a name into one target per channel. Whether the // user named a channel is passed in, because a board with a single channel is // not an explicit request for it. func resolveEffectTargets(catalog *intrgb.Catalog, name string, channels []via.Channel) ([]intrgb.EffectTarget, []string, error) { return intrgb.ResolveEffect(catalog, name, channels, targetZone != "") } func NewEffectCmd() *cobra.Command { cmd := &cobra.Command{ Use: "effect [name|index]", Short: "Set or list RGB effects", 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" + "and is driven with `effect ` instead.\n" + "A number is an effect ID; anything else is a name.", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if listEffects || len(args) == 0 { return listAllEffects(cmd) } return runEffectSet(cmd, args) }, } cmd.Flags().BoolVar(&listEffects, "list", false, "List all available effects per zone") return cmd } func listAllEffects(cmd *cobra.Command) error { proto, target, channels, err := openTarget() 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, args []string) error { proto, target, channels, err := openTarget() if err != nil { return err } 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) if err != nil { return err } targets, skipped, err := resolveEffectTargets(catalog, want.Name, channels) if err != nil { return err } if len(skipped) > 0 { fmt.Fprintf(cmd.ErrOrStderr(), "Warning: effect not supported on channel(s): %s\n", strings.Join(skipped, ", ")) } 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 }