completion.go 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  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 defs, err := intrgb.LoadDefinitionsDir(definitionsPath()); err == nil {
  24. for _, def := range defs {
  25. for _, label := range def.Labels {
  26. names = append(names, label)
  27. }
  28. }
  29. }
  30. return narrow(names, toComplete), cobra.ShellCompDirectiveNoFileComp
  31. }
  32. // completeDeviceNumbers offers the keyboard numbers keyboard info prints, which
  33. // come from enumeration and need no open keyboard.
  34. func completeDeviceNumbers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
  35. devices, err := discoverAll()
  36. if err != nil {
  37. return nil, cobra.ShellCompDirectiveNoFileComp
  38. }
  39. var numbers []string
  40. for _, d := range devices {
  41. numbers = append(numbers, strconv.Itoa(d.Index))
  42. }
  43. return narrow(numbers, toComplete), cobra.ShellCompDirectiveNoFileComp
  44. }
  45. // completeProfileNames offers the profiles in the directory, because load and
  46. // delete take a name that is a file there.
  47. func completeProfileNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
  48. entries, err := os.ReadDir(profilesPath())
  49. if err != nil {
  50. return nil, cobra.ShellCompDirectiveNoFileComp
  51. }
  52. var names []string
  53. for _, entry := range entries {
  54. if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
  55. continue
  56. }
  57. names = append(names, strings.TrimSuffix(entry.Name(), ".json"))
  58. }
  59. return narrow(names, toComplete), cobra.ShellCompDirectiveNoFileComp
  60. }
  61. // narrow keeps the candidates a prefix matches, in order and without repeats: a
  62. // completion list with duplicates reads as a bug, and one that ignores what has
  63. // been typed makes the shell do the work it came here for.
  64. func narrow(names []string, prefix string) []string {
  65. seen := make(map[string]bool, len(names))
  66. var out []string
  67. for _, name := range names {
  68. if prefix != "" && !strings.HasPrefix(name, prefix) {
  69. continue
  70. }
  71. if seen[name] {
  72. continue
  73. }
  74. seen[name] = true
  75. out = append(out, name)
  76. }
  77. sort.Strings(out)
  78. return out
  79. }
  80. // registerFlagCompletions teaches the shell what the persistent flags accept. The
  81. // definition file is a path, so the shell completes paths for it on its own.
  82. func registerFlagCompletions(root *cobra.Command) {
  83. _ = root.RegisterFlagCompletionFunc("zone", completeZoneNames)
  84. _ = root.RegisterFlagCompletionFunc("device", completeDeviceNumbers)
  85. _ = root.MarkFlagFilename("definition", "*.json")
  86. }