completion.go 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. package main
  2. import (
  3. "os"
  4. "sort"
  5. "strconv"
  6. "strings"
  7. "github.com/spf13/cobra"
  8. intrgb "netdome.biz/paul/qmk-rgb/internal/rgb"
  9. "netdome.biz/paul/qmk-rgb/internal/via"
  10. )
  11. // Shell completion. The tool knows how to resolve a zone name and which profiles
  12. // exist, and a shell only offers what the binary tells it about, so the binary has
  13. // to say it. None of these open the keyboard: a shell asks while nothing is
  14. // plugged in, and an error printed into a prompt is worse than an empty list.
  15. // completeZoneNames offers the QMK subsystem names, which follow from the channel
  16. // number and work on any board, plus the names the definition files give their
  17. // channels, which are what a board calls them in VIA.
  18. func completeZoneNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
  19. var names []string
  20. for _, ch := range via.LightingChannels {
  21. names = append(names, ch.Subsystem())
  22. }
  23. if dir, err := definitionsDir(); err == nil {
  24. if defs, loadErr := intrgb.LoadDefinitionsDir(dir); loadErr == nil {
  25. for _, def := range defs {
  26. for _, label := range def.Labels {
  27. names = append(names, label)
  28. }
  29. }
  30. }
  31. }
  32. return narrow(names, toComplete), cobra.ShellCompDirectiveNoFileComp
  33. }
  34. // completeDeviceNumbers offers the keyboard numbers keyboard info prints, which
  35. // come from enumeration and need no open keyboard.
  36. func completeDeviceNumbers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
  37. devices, err := discoverAll()
  38. if err != nil {
  39. return nil, cobra.ShellCompDirectiveNoFileComp
  40. }
  41. var numbers []string
  42. for _, d := range devices {
  43. numbers = append(numbers, strconv.Itoa(d.Index))
  44. }
  45. return narrow(numbers, toComplete), cobra.ShellCompDirectiveNoFileComp
  46. }
  47. // completeProfileNames offers the profiles in the directory, because load and
  48. // delete take a name that is a file there.
  49. func completeProfileNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
  50. entries, err := os.ReadDir(profilesDir)
  51. if err != nil {
  52. return nil, cobra.ShellCompDirectiveNoFileComp
  53. }
  54. var names []string
  55. for _, entry := range entries {
  56. if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
  57. continue
  58. }
  59. names = append(names, strings.TrimSuffix(entry.Name(), ".json"))
  60. }
  61. return narrow(names, toComplete), cobra.ShellCompDirectiveNoFileComp
  62. }
  63. // narrow keeps the candidates a prefix matches, in order and without repeats: a
  64. // completion list with duplicates reads as a bug, and one that ignores what has
  65. // been typed makes the shell do the work it came here for.
  66. func narrow(names []string, prefix string) []string {
  67. seen := make(map[string]bool, len(names))
  68. var out []string
  69. for _, name := range names {
  70. if prefix != "" && !strings.HasPrefix(name, prefix) {
  71. continue
  72. }
  73. if seen[name] {
  74. continue
  75. }
  76. seen[name] = true
  77. out = append(out, name)
  78. }
  79. sort.Strings(out)
  80. return out
  81. }
  82. // registerFlagCompletions teaches the shell what the persistent flags accept. The
  83. // definition file is a path, so the shell completes paths for it on its own.
  84. func registerFlagCompletions(root *cobra.Command) {
  85. _ = root.RegisterFlagCompletionFunc("zone", completeZoneNames)
  86. _ = root.RegisterFlagCompletionFunc("device", completeDeviceNumbers)
  87. _ = root.MarkFlagFilename("definition", "*.json")
  88. }