package main import ( "os" "sort" "strconv" "strings" "github.com/spf13/cobra" intrgb "netdome.biz/paul/qmk-rgb/internal/rgb" "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 QMK subsystem names, which follow from the channel // number and work on any board, plus 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) { var names []string for _, ch := range via.LightingChannels { names = append(names, ch.Subsystem()) } if defs, err := intrgb.LoadDefinitionsDir(definitionsPath()); err == nil { for _, def := range defs { for _, label := range def.Labels { names = append(names, label) } } } return narrow(names, toComplete), cobra.ShellCompDirectiveNoFileComp } // 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 in the directory, because load and // delete take a name that is a file there. func completeProfileNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { entries, err := os.ReadDir(profilesPath()) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } var names []string for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") { continue } names = append(names, strings.TrimSuffix(entry.Name(), ".json")) } 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. func registerFlagCompletions(root *cobra.Command) { _ = root.RegisterFlagCompletionFunc("zone", completeZoneNames) _ = root.RegisterFlagCompletionFunc("device", completeDeviceNumbers) _ = root.MarkFlagFilename("definition", "*.json") }