176 lines
5.3 KiB
Go
176 lines
5.3 KiB
Go
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
|
|
|
|
package cli
|
|
|
|
import (
|
|
"bufio"
|
|
"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 := bufio.NewScanner(strings.NewReader(raw))
|
|
words := make([]string, 0, 32)
|
|
for scanner.Scan() {
|
|
words = append(words, scanner.Text())
|
|
}
|
|
if new_word {
|
|
words = append(words, "")
|
|
}
|
|
return [][]string{words}, nil
|
|
}
|
|
|
|
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)[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
|
|
}
|