|
| 1 | +package bridge |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "encoding/json" |
| 6 | + "errors" |
| 7 | + "os/exec" |
| 8 | + "path/filepath" |
| 9 | + "strings" |
| 10 | + |
| 11 | + "github.com/carapace-sh/carapace" |
| 12 | + "github.com/carapace-sh/carapace/pkg/style" |
| 13 | +) |
| 14 | + |
| 15 | +// ActionKitten bridges https://github.com/kovidgoyal/kitty |
| 16 | +func ActionKitten(command ...string) carapace.Action { |
| 17 | + return actionCommand(command...)(func(command ...string) carapace.Action { |
| 18 | + return carapace.ActionCallback(func(c carapace.Context) carapace.Action { |
| 19 | + request := append(command, c.Args...) |
| 20 | + request = append(request, c.Value) |
| 21 | + |
| 22 | + matchRequests := [][]string{request} |
| 23 | + if c.Value == "-" { // some longhand flags are missing when called with `-` |
| 24 | + longhandRequest := append(command, c.Args...) |
| 25 | + longhandRequest = append(longhandRequest, "--") |
| 26 | + matchRequests = append(matchRequests, longhandRequest) |
| 27 | + } |
| 28 | + |
| 29 | + // partial directories aren't always completed (e.g. tmp directory with `kitty /tm`) |
| 30 | + filepathRequest := append(command, c.Args...) |
| 31 | + filepathRequest = append(filepathRequest, filepath.Dir(c.Value)) |
| 32 | + matchRequests = append(matchRequests, filepathRequest) |
| 33 | + |
| 34 | + m, err := json.Marshal(matchRequests) |
| 35 | + if err != nil { |
| 36 | + return carapace.ActionMessage(err.Error()) |
| 37 | + } |
| 38 | + |
| 39 | + var stdout, stderr bytes.Buffer |
| 40 | + cmd := c.Command("kitten", "__complete__", "json") |
| 41 | + cmd.Stdin = bytes.NewReader(m) |
| 42 | + cmd.Stdout = &stdout |
| 43 | + cmd.Stderr = &stderr |
| 44 | + |
| 45 | + if err := cmd.Run(); err != nil { |
| 46 | + if exitErr, ok := err.(*exec.ExitError); ok { |
| 47 | + if firstLine := strings.SplitN(string(exitErr.Stderr), "\n", 2)[0]; strings.TrimSpace(firstLine) != "" { |
| 48 | + err = errors.New(firstLine) |
| 49 | + } |
| 50 | + } |
| 51 | + return carapace.ActionMessage(err.Error()) |
| 52 | + } |
| 53 | + |
| 54 | + var matchResults []kittenResult |
| 55 | + if err := json.Unmarshal(stdout.Bytes(), &matchResults); err != nil { |
| 56 | + return carapace.ActionMessage(err.Error()) |
| 57 | + } |
| 58 | + |
| 59 | + if len(matchResults) == 0 { |
| 60 | + return carapace.ActionValues() |
| 61 | + } |
| 62 | + |
| 63 | + var nospace bool |
| 64 | + batch := carapace.Batch() |
| 65 | + for _, matchResult := range matchResults { |
| 66 | + for _, group := range matchResult.Groups { |
| 67 | + switch group.Title { |
| 68 | + case "Directories": |
| 69 | + batch = append(batch, carapace.ActionDirectories()) |
| 70 | + case "Executables": |
| 71 | + dir := filepath.ToSlash(filepath.Dir(c.Value)) |
| 72 | + batch = append(batch, |
| 73 | + carapace.ActionDirectories(), |
| 74 | + carapace.ActionExecutables(dir).Prefix(dir+"/"), |
| 75 | + ) |
| 76 | + case "Executables in PATH": |
| 77 | + batch = append(batch, carapace.ActionExecutables()) |
| 78 | + case "Options": |
| 79 | + longhandMatches := make(kittenMatches, 0) |
| 80 | + shorthandMatches := make(kittenMatches, 0) |
| 81 | + for _, match := range group.Matches { |
| 82 | + switch { |
| 83 | + case strings.HasPrefix(match.Word, "--"): |
| 84 | + longhandMatches = append(longhandMatches, match) |
| 85 | + default: |
| 86 | + shorthandMatches = append(shorthandMatches, match) |
| 87 | + } |
| 88 | + } |
| 89 | + batch = append(batch, |
| 90 | + longhandMatches.Action().Tag("longhand flags"), |
| 91 | + shorthandMatches.Action().Tag("shorthand flags"), |
| 92 | + ) |
| 93 | + case "Keywords": |
| 94 | + batch = append(batch, group.Matches.Action().Tag("keywords").StyleF(style.ForKeyword)) |
| 95 | + case "Sub-commands": |
| 96 | + batch = append(batch, group.Matches.Action().Tag("commands")) |
| 97 | + default: |
| 98 | + switch { |
| 99 | + case group.IsFiles: |
| 100 | + // TODO this tries to use carapace file completion via retain (switch to provided values if there is an issue) |
| 101 | + retain := make([]string, 0) |
| 102 | + for _, match := range group.Matches { |
| 103 | + retain = append(retain, filepath.ToSlash(match.Word)) |
| 104 | + } |
| 105 | + batch = append(batch, carapace.ActionFiles().Retain(retain...)) |
| 106 | + default: |
| 107 | + batch = append(batch, group.Matches.Action().Tag(strings.ToLower(group.Title))) |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + nospace = nospace || group.NoTrailingSpace |
| 112 | + } |
| 113 | + } |
| 114 | + |
| 115 | + if nospace { |
| 116 | + return batch.ToA().NoSpace() |
| 117 | + } |
| 118 | + return batch.ToA() |
| 119 | + }) |
| 120 | + }) |
| 121 | +} |
| 122 | + |
| 123 | +type kittenResult struct { |
| 124 | + Groups []struct { |
| 125 | + Title string `json:"title"` |
| 126 | + NoTrailingSpace bool `json:"no_trailing_space"` |
| 127 | + IsFiles bool `json:"is_files"` |
| 128 | + Matches kittenMatches `json:"matches"` |
| 129 | + } `json:"groups"` |
| 130 | + Delegate struct { |
| 131 | + } `json:"delegate"` |
| 132 | +} |
| 133 | + |
| 134 | +type kittenMatches []struct { |
| 135 | + Word string `json:"word"` |
| 136 | + Description string `json:"description"` |
| 137 | +} |
| 138 | + |
| 139 | +func (m kittenMatches) Action() carapace.Action { |
| 140 | + return carapace.ActionCallback(func(c carapace.Context) carapace.Action { |
| 141 | + vals := make([]string, 0) |
| 142 | + for _, match := range m { |
| 143 | + vals = append(vals, match.Word, match.Description) |
| 144 | + } |
| 145 | + return carapace.ActionValuesDescribed(vals...) |
| 146 | + }) |
| 147 | +} |
0 commit comments