zsh.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. // License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package cli
  3. import (
  4. "fmt"
  5. "kitty/tools/cli/markup"
  6. "kitty/tools/tty"
  7. "kitty/tools/utils"
  8. "kitty/tools/utils/style"
  9. "kitty/tools/wcswidth"
  10. "strings"
  11. )
  12. var _ = fmt.Print
  13. func zsh_completion_script(commands []string) (string, error) {
  14. return `#compdef kitty
  15. _kitty() {
  16. (( ${+commands[kitten]} )) || builtin return
  17. builtin local src cmd=${(F)words:0:$CURRENT}
  18. # Send all words up to the word the cursor is currently on.
  19. src=$(builtin command kitten __complete__ zsh "_matcher=$_matcher" <<<$cmd) || builtin return
  20. builtin eval "$src"
  21. }
  22. if (( $+functions[compdef] )); then
  23. compdef _kitty kitty
  24. compdef _kitty clone-in-kitty
  25. compdef _kitty kitten
  26. fi
  27. `, nil
  28. }
  29. func shell_input_parser(data []byte, shell_state map[string]string) ([][]string, error) {
  30. raw := string(data)
  31. new_word := strings.HasSuffix(raw, "\n\n")
  32. raw = strings.TrimRight(raw, "\n \t")
  33. scanner := utils.NewLineScanner(raw)
  34. words := make([]string, 0, 32)
  35. for scanner.Scan() {
  36. words = append(words, scanner.Text())
  37. }
  38. if new_word {
  39. words = append(words, "")
  40. }
  41. return [][]string{words}, nil
  42. }
  43. var debugprintln = tty.DebugPrintln
  44. var _ = debugprintln
  45. func zsh_input_parser(data []byte, shell_state map[string]string) ([][]string, error) {
  46. matcher := shell_state["_matcher"]
  47. q := ""
  48. if matcher != "" {
  49. q = strings.Split(strings.ToLower(matcher), ":")[0][:1]
  50. }
  51. if q != "" && strings.Contains("lrbe", q) {
  52. // this is zsh anchor based matching
  53. // https://zsh.sourceforge.io/Doc/Release/Completion-Widgets.html#Completion-Matching-Control
  54. // can be specified with matcher-list and some systems do it by default,
  55. // for example, Debian, which adds the following to zshrc
  56. // zstyle ':completion:*' matcher-list '' 'm:{a-z}={A-Z}' 'm:{a-zA-Z}={A-Za-z}' 'r:|[._-]=* r:|=* l:|=*'
  57. // For some reason that I dont have the
  58. // time/interest to figure out, returning completion candidates for
  59. // these matcher types break completion, so just abort in this case.
  60. return nil, fmt.Errorf("ZSH anchor based matching active, cannot complete")
  61. }
  62. return shell_input_parser(data, shell_state)
  63. }
  64. func (self *Match) FormatForCompletionList(max_word_len int, f *markup.Context, screen_width int) string {
  65. word := self.Word
  66. desc := self.Description
  67. if desc == "" {
  68. return word
  69. }
  70. word_len := wcswidth.Stringwidth(word)
  71. line, _, _ := strings.Cut(strings.TrimSpace(desc), "\n")
  72. desc = f.Prettify(line)
  73. multiline := false
  74. max_desc_len := screen_width - max_word_len - 3
  75. if word_len > max_word_len {
  76. multiline = true
  77. } else {
  78. word += strings.Repeat(" ", max_word_len-word_len)
  79. }
  80. if wcswidth.Stringwidth(desc) > max_desc_len {
  81. desc = style.WrapTextAsLines(desc, max_desc_len-2, style.WrapOptions{})[0] + "…"
  82. }
  83. if multiline {
  84. return word + "\n" + strings.Repeat(" ", max_word_len+2) + desc
  85. }
  86. return word + " " + desc
  87. }
  88. func serialize(completions *Completions, f *markup.Context, screen_width int) ([]byte, error) {
  89. output := strings.Builder{}
  90. if completions.Delegate.NumToRemove > 0 {
  91. for i := 0; i < completions.Delegate.NumToRemove; i++ {
  92. fmt.Fprintln(&output, "shift words")
  93. fmt.Fprintln(&output, "(( CURRENT-- ))")
  94. }
  95. service := utils.QuoteStringForSH(completions.Delegate.Command)
  96. fmt.Fprintln(&output, "words[1]="+service)
  97. fmt.Fprintln(&output, "_normal -p", service)
  98. } else {
  99. for _, mg := range completions.Groups {
  100. cmd := strings.Builder{}
  101. escape_ourselves := mg.IsFiles // zsh quoting quotes a leading ~/ in filenames which is wrong
  102. cmd.WriteString("compadd -U ")
  103. if escape_ourselves {
  104. cmd.WriteString("-Q ")
  105. }
  106. cmd.WriteString("-J ")
  107. cmd.WriteString(utils.QuoteStringForSH(mg.Title))
  108. cmd.WriteString(" -X ")
  109. cmd.WriteString(utils.QuoteStringForSH("%B" + mg.Title + "%b"))
  110. if mg.NoTrailingSpace {
  111. cmd.WriteString(" -S ''")
  112. }
  113. if mg.IsFiles {
  114. cmd.WriteString(" -f")
  115. }
  116. lcp := mg.remove_common_prefix()
  117. if lcp != "" {
  118. cmd.WriteString(" -p ")
  119. cmd.WriteString(utils.QuoteStringForSH(lcp))
  120. }
  121. if mg.has_descriptions() {
  122. fmt.Fprintln(&output, "compdescriptions=(")
  123. limit := mg.max_visual_word_length(16)
  124. for _, m := range mg.Matches {
  125. fmt.Fprintln(&output, utils.QuoteStringForSH(wcswidth.StripEscapeCodes(m.FormatForCompletionList(limit, f, screen_width))))
  126. }
  127. fmt.Fprintln(&output, ")")
  128. cmd.WriteString(" -l -d compdescriptions")
  129. }
  130. cmd.WriteString(" --")
  131. for _, m := range mg.Matches {
  132. cmd.WriteString(" ")
  133. w := m.Word
  134. if escape_ourselves {
  135. w = utils.EscapeSHMetaCharacters(m.Word)
  136. }
  137. cmd.WriteString(utils.QuoteStringForSH(w))
  138. }
  139. fmt.Fprintln(&output, cmd.String(), ";")
  140. }
  141. }
  142. // debugf("%#v", output.String())
  143. return []byte(output.String()), nil
  144. }
  145. func zsh_output_serializer(completions []*Completions, shell_state map[string]string) ([]byte, error) {
  146. var f *markup.Context
  147. screen_width := 80
  148. ctty, err := tty.OpenControllingTerm()
  149. if err == nil {
  150. sz, err := ctty.GetSize()
  151. ctty.Close()
  152. if err == nil {
  153. screen_width = int(sz.Col)
  154. }
  155. }
  156. f = markup.New(false) // ZSH freaks out if there are escape codes in the description strings
  157. return serialize(completions[0], f, screen_width)
  158. }
  159. func init() {
  160. completion_scripts["zsh"] = zsh_completion_script
  161. input_parsers["zsh"] = zsh_input_parser
  162. output_serializers["zsh"] = zsh_output_serializer
  163. }