package main import ( "sort" "strconv" "strings" "github.com/spf13/cobra" "netdome.biz/paul/qmk-rgb/internal/via" ) // Shell completion. The tool knows how to resolve a zone name and which profiles // exist, and a shell only offers what the binary tells it about, so the binary has // to say it. None of these open the keyboard: a shell asks while nothing is // plugged in, and an error printed into a prompt is worse than an empty list. // completeZoneNames offers the word `all`, then the QMK subsystem names, which // follow from the channel number and work on any board, then the names the // definition files give their channels, which are what a board calls them in VIA. func completeZoneNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { names := []string{zoneAll} for _, ch := range via.LightingChannels { names = append(names, ch.Subsystem()) } // Every definition the tool knows, not only the ones in the data directory: // the Impact 80's file is built into the binary, so a board that names its // channels logo, Backlight and side offers those names in a shell that has // never been given a definitions directory. A name the user cannot see is a // name they will not type. for _, def := range candidateDefinitions() { for _, label := range def.Labels { names = append(names, label) } } return narrow(names, toComplete), cobra.ShellCompDirectiveNoFileComp } // completeZoneArgs offers zone names for a command's zone and nothing after it. // The effect name `effect` takes second would have to come from a definition // file chosen for the connected board, and a shell asks while nothing is // plugged in, so a list built without a keyboard is the whole of what can be // offered honestly. func completeZoneArgs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) > 0 { return nil, cobra.ShellCompDirectiveNoFileComp } return completeZoneNames(cmd, args, toComplete) } // completeLoadArgs offers a profile name first and a zone second, which is the // order the command reads them in. The zone goes through completeZoneNames // directly rather than through completeZoneArgs, which stops after one argument // because for every other command the zone is that first argument. func completeLoadArgs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) == 0 { return completeProfileNames(cmd, args, toComplete) } return completeZoneNames(cmd, args, toComplete) } // completeDeviceNumbers offers the keyboard numbers keyboard info prints, which // come from enumeration and need no open keyboard. func completeDeviceNumbers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { devices, err := discoverAll() if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } var numbers []string for _, d := range devices { numbers = append(numbers, strconv.Itoa(d.Index)) } return narrow(numbers, toComplete), cobra.ShellCompDirectiveNoFileComp } // completeProfileNames offers the profiles that can be read, because load and // delete take a name that is a file in the profiles directory. func completeProfileNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { names, err := ListProfiles() if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } return narrow(names, toComplete), cobra.ShellCompDirectiveNoFileComp } // narrow keeps the candidates a prefix matches, in order and without repeats: a // completion list with duplicates reads as a bug, and one that ignores what has // been typed makes the shell do the work it came here for. func narrow(names []string, prefix string) []string { seen := make(map[string]bool, len(names)) var out []string for _, name := range names { if prefix != "" && !strings.HasPrefix(name, prefix) { continue } if seen[name] { continue } seen[name] = true out = append(out, name) } sort.Strings(out) return out } // registerFlagCompletions teaches the shell what the persistent flags accept. The // definition file is a path, so the shell completes paths for it on its own. The // zones are positional, so they are completed by the commands that take one, in // completeZoneArgs. func registerFlagCompletions(root *cobra.Command) { _ = root.RegisterFlagCompletionFunc("device", completeDeviceNumbers) _ = root.MarkFlagFilename("definition", "*.json") }