123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178 |
- // License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
- package cli
- import (
- "fmt"
- "kitty/tools/cli/markup"
- "kitty/tools/tty"
- "kitty/tools/utils"
- "kitty/tools/utils/style"
- "kitty/tools/wcswidth"
- "strings"
- )
- var _ = fmt.Print
- func zsh_completion_script(commands []string) (string, error) {
- return `#compdef kitty
- _kitty() {
- (( ${+commands[kitten]} )) || builtin return
- builtin local src cmd=${(F)words:0:$CURRENT}
- # Send all words up to the word the cursor is currently on.
- src=$(builtin command kitten __complete__ zsh "_matcher=$_matcher" <<<$cmd) || builtin return
- builtin eval "$src"
- }
- if (( $+functions[compdef] )); then
- compdef _kitty kitty
- compdef _kitty clone-in-kitty
- compdef _kitty kitten
- fi
- `, nil
- }
- func shell_input_parser(data []byte, shell_state map[string]string) ([][]string, error) {
- raw := string(data)
- new_word := strings.HasSuffix(raw, "\n\n")
- raw = strings.TrimRight(raw, "\n \t")
- scanner := utils.NewLineScanner(raw)
- words := make([]string, 0, 32)
- for scanner.Scan() {
- words = append(words, scanner.Text())
- }
- if new_word {
- words = append(words, "")
- }
- return [][]string{words}, nil
- }
- var debugprintln = tty.DebugPrintln
- var _ = debugprintln
- func zsh_input_parser(data []byte, shell_state map[string]string) ([][]string, error) {
- matcher := shell_state["_matcher"]
- q := ""
- if matcher != "" {
- q = strings.Split(strings.ToLower(matcher), ":")[0][:1]
- }
- if q != "" && strings.Contains("lrbe", q) {
- // this is zsh anchor based matching
- // https://zsh.sourceforge.io/Doc/Release/Completion-Widgets.html#Completion-Matching-Control
- // can be specified with matcher-list and some systems do it by default,
- // for example, Debian, which adds the following to zshrc
- // zstyle ':completion:*' matcher-list '' 'm:{a-z}={A-Z}' 'm:{a-zA-Z}={A-Za-z}' 'r:|[._-]=* r:|=* l:|=*'
- // For some reason that I dont have the
- // time/interest to figure out, returning completion candidates for
- // these matcher types break completion, so just abort in this case.
- return nil, fmt.Errorf("ZSH anchor based matching active, cannot complete")
- }
- return shell_input_parser(data, shell_state)
- }
- func (self *Match) FormatForCompletionList(max_word_len int, f *markup.Context, screen_width int) string {
- word := self.Word
- desc := self.Description
- if desc == "" {
- return word
- }
- word_len := wcswidth.Stringwidth(word)
- line, _, _ := strings.Cut(strings.TrimSpace(desc), "\n")
- desc = f.Prettify(line)
- multiline := false
- max_desc_len := screen_width - max_word_len - 3
- if word_len > max_word_len {
- multiline = true
- } else {
- word += strings.Repeat(" ", max_word_len-word_len)
- }
- if wcswidth.Stringwidth(desc) > max_desc_len {
- desc = style.WrapTextAsLines(desc, max_desc_len-2, style.WrapOptions{})[0] + "…"
- }
- if multiline {
- return word + "\n" + strings.Repeat(" ", max_word_len+2) + desc
- }
- return word + " " + desc
- }
- func serialize(completions *Completions, f *markup.Context, screen_width int) ([]byte, error) {
- output := strings.Builder{}
- if completions.Delegate.NumToRemove > 0 {
- for i := 0; i < completions.Delegate.NumToRemove; i++ {
- fmt.Fprintln(&output, "shift words")
- fmt.Fprintln(&output, "(( CURRENT-- ))")
- }
- service := utils.QuoteStringForSH(completions.Delegate.Command)
- fmt.Fprintln(&output, "words[1]="+service)
- fmt.Fprintln(&output, "_normal -p", service)
- } else {
- for _, mg := range completions.Groups {
- cmd := strings.Builder{}
- escape_ourselves := mg.IsFiles // zsh quoting quotes a leading ~/ in filenames which is wrong
- cmd.WriteString("compadd -U ")
- if escape_ourselves {
- cmd.WriteString("-Q ")
- }
- cmd.WriteString("-J ")
- cmd.WriteString(utils.QuoteStringForSH(mg.Title))
- cmd.WriteString(" -X ")
- cmd.WriteString(utils.QuoteStringForSH("%B" + mg.Title + "%b"))
- if mg.NoTrailingSpace {
- cmd.WriteString(" -S ''")
- }
- if mg.IsFiles {
- cmd.WriteString(" -f")
- }
- lcp := mg.remove_common_prefix()
- if lcp != "" {
- cmd.WriteString(" -p ")
- cmd.WriteString(utils.QuoteStringForSH(lcp))
- }
- if mg.has_descriptions() {
- fmt.Fprintln(&output, "compdescriptions=(")
- limit := mg.max_visual_word_length(16)
- for _, m := range mg.Matches {
- fmt.Fprintln(&output, utils.QuoteStringForSH(wcswidth.StripEscapeCodes(m.FormatForCompletionList(limit, f, screen_width))))
- }
- fmt.Fprintln(&output, ")")
- cmd.WriteString(" -l -d compdescriptions")
- }
- cmd.WriteString(" --")
- for _, m := range mg.Matches {
- cmd.WriteString(" ")
- w := m.Word
- if escape_ourselves {
- w = utils.EscapeSHMetaCharacters(m.Word)
- }
- cmd.WriteString(utils.QuoteStringForSH(w))
- }
- fmt.Fprintln(&output, cmd.String(), ";")
- }
- }
- // debugf("%#v", output.String())
- return []byte(output.String()), nil
- }
- func zsh_output_serializer(completions []*Completions, shell_state map[string]string) ([]byte, error) {
- var f *markup.Context
- screen_width := 80
- ctty, err := tty.OpenControllingTerm()
- if err == nil {
- sz, err := ctty.GetSize()
- ctty.Close()
- if err == nil {
- screen_width = int(sz.Col)
- }
- }
- f = markup.New(false) // ZSH freaks out if there are escape codes in the description strings
- return serialize(completions[0], f, screen_width)
- }
- func init() {
- completion_scripts["zsh"] = zsh_completion_script
- input_parsers["zsh"] = zsh_input_parser
- output_serializers["zsh"] = zsh_output_serializer
- }
|