completion.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. package main
  2. import (
  3. "sort"
  4. "strconv"
  5. "strings"
  6. "github.com/spf13/cobra"
  7. "netdome.biz/paul/qmk-rgb/internal/via"
  8. )
  9. // Shell completion. The tool knows how to resolve a zone name and which profiles
  10. // exist, and a shell only offers what the binary tells it about, so the binary has
  11. // to say it. None of these open the keyboard: a shell asks while nothing is
  12. // plugged in, and an error printed into a prompt is worse than an empty list.
  13. // completeZoneNames offers the word `all`, then the QMK subsystem names, which
  14. // follow from the channel number and work on any board, then the names the
  15. // definition files give their channels, which are what a board calls them in VIA.
  16. func completeZoneNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
  17. names := []string{zoneAll}
  18. for _, ch := range via.LightingChannels {
  19. names = append(names, ch.Subsystem())
  20. }
  21. // Every definition the tool knows, not only the ones in the data directory:
  22. // the Impact 80's file is built into the binary, so a board that names its
  23. // channels logo, Backlight and side offers those names in a shell that has
  24. // never been given a definitions directory. A name the user cannot see is a
  25. // name they will not type.
  26. for _, def := range candidateDefinitions() {
  27. for _, label := range def.Labels {
  28. names = append(names, label)
  29. }
  30. }
  31. return narrow(names, toComplete), cobra.ShellCompDirectiveNoFileComp
  32. }
  33. // completeZoneArgs offers zone names for a command's zone and nothing after it.
  34. // The effect name `effect` takes second would have to come from a definition
  35. // file chosen for the connected board, and a shell asks while nothing is
  36. // plugged in, so a list built without a keyboard is the whole of what can be
  37. // offered honestly.
  38. func completeZoneArgs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
  39. if len(args) > 0 {
  40. return nil, cobra.ShellCompDirectiveNoFileComp
  41. }
  42. return completeZoneNames(cmd, args, toComplete)
  43. }
  44. // completeLoadArgs offers a profile name first and a zone second, which is the
  45. // order the command reads them in. The zone goes through completeZoneNames
  46. // directly rather than through completeZoneArgs, which stops after one argument
  47. // because for every other command the zone is that first argument.
  48. func completeLoadArgs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
  49. if len(args) == 0 {
  50. return completeProfileNames(cmd, args, toComplete)
  51. }
  52. return completeZoneNames(cmd, args, toComplete)
  53. }
  54. // completeDeviceNumbers offers the keyboard numbers keyboard info prints, which
  55. // come from enumeration and need no open keyboard.
  56. func completeDeviceNumbers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
  57. devices, err := discoverAll()
  58. if err != nil {
  59. return nil, cobra.ShellCompDirectiveNoFileComp
  60. }
  61. var numbers []string
  62. for _, d := range devices {
  63. numbers = append(numbers, strconv.Itoa(d.Index))
  64. }
  65. return narrow(numbers, toComplete), cobra.ShellCompDirectiveNoFileComp
  66. }
  67. // completeProfileNames offers the profiles that can be read, because load and
  68. // delete take a name that is a file in the profiles directory.
  69. func completeProfileNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
  70. names, err := ListProfiles()
  71. if err != nil {
  72. return nil, cobra.ShellCompDirectiveNoFileComp
  73. }
  74. return narrow(names, toComplete), cobra.ShellCompDirectiveNoFileComp
  75. }
  76. // narrow keeps the candidates a prefix matches, in order and without repeats: a
  77. // completion list with duplicates reads as a bug, and one that ignores what has
  78. // been typed makes the shell do the work it came here for.
  79. func narrow(names []string, prefix string) []string {
  80. seen := make(map[string]bool, len(names))
  81. var out []string
  82. for _, name := range names {
  83. if prefix != "" && !strings.HasPrefix(name, prefix) {
  84. continue
  85. }
  86. if seen[name] {
  87. continue
  88. }
  89. seen[name] = true
  90. out = append(out, name)
  91. }
  92. sort.Strings(out)
  93. return out
  94. }
  95. // registerFlagCompletions teaches the shell what the persistent flags accept. The
  96. // definition file is a path, so the shell completes paths for it on its own. The
  97. // zones are positional, so they are completed by the commands that take one, in
  98. // completeZoneArgs.
  99. func registerFlagCompletions(root *cobra.Command) {
  100. _ = root.RegisterFlagCompletionFunc("device", completeDeviceNumbers)
  101. _ = root.MarkFlagFilename("definition", "*.json")
  102. }