From 370aa3aaa67e150236edf22a396804db79eebc89 Mon Sep 17 00:00:00 2001 From: pagedown Date: Fri, 3 Feb 2023 18:16:04 +0800 Subject: [PATCH] Completion: Delegate kitty +complete to kitten Implement `kitten __complete__ setup` in Go. Fix zsh completion script to check `kitten`. --- docs/changelog.rst | 3 ++ kitty/entry_points.py | 12 +++++++ shell-integration/zsh/completions/_kitty | 2 +- tools/cli/bash.go | 21 +++++++++++ tools/cli/completion-main.go | 18 ++++++++++ tools/cli/fish.go | 44 +++++++++++++++++++++++- tools/cli/zsh.go | 21 +++++++++++ tools/cmd/completion/kitty.go | 2 +- 8 files changed, 120 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 44a977514..9e2bd892d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -50,6 +50,9 @@ Detailed list of changes - Fix regression in previous release that caused incorrect entries in terminfo for the modifer+F3 key combinations (:pull:`5970`) +- Bring back the deprecated and removed ``kitty +complete`` and delegate it to :program:`kitten` for backward compatibility (:pull:`5977`) + + 0.27.0 [2023-01-31] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/entry_points.py b/kitty/entry_points.py index 48855d132..c44db04e1 100644 --- a/kitty/entry_points.py +++ b/kitty/entry_points.py @@ -31,6 +31,17 @@ def hold(args: List[str]) -> None: os.execvp(kitten_exe(), args) +def complete(args: List[str]) -> None: + # Delegate to kitten to maintain backward compatibility + if len(args) < 2 or args[1] not in ('setup', 'zsh', 'fish2', 'bash'): + raise SystemExit(1) + if args[1] == 'fish2': + args[1:1] = ['fish', '_legacy_completion=fish2'] + from kitty.constants import kitten_exe + args = ['kitten', '__complete__'] + args[1:] + os.execvp(kitten_exe(), args) + + def open_urls(args: List[str]) -> None: setattr(sys, 'cmdline_args_for_open', True) sys.argv = ['kitty'] + args[1:] @@ -144,6 +155,7 @@ entry_points = { } namespaced_entry_points = {k: v for k, v in entry_points.items() if k[0] not in '+@'} namespaced_entry_points['hold'] = hold +namespaced_entry_points['complete'] = complete namespaced_entry_points['runpy'] = runpy namespaced_entry_points['launch'] = launch namespaced_entry_points['open'] = open_urls diff --git a/shell-integration/zsh/completions/_kitty b/shell-integration/zsh/completions/_kitty index 3fb8880aa..359b4a3f7 100644 --- a/shell-integration/zsh/completions/_kitty +++ b/shell-integration/zsh/completions/_kitty @@ -1,6 +1,6 @@ #compdef kitty -(( ${+commands[kitty]} )) || builtin return +(( ${+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 diff --git a/tools/cli/bash.go b/tools/cli/bash.go index f2665e4e8..f01c896d8 100644 --- a/tools/cli/bash.go +++ b/tools/cli/bash.go @@ -11,6 +11,26 @@ import ( var _ = fmt.Print +func bash_completion_script(commands []string) ([]byte, error) { + script := `_ksi_completions() { + builtin local src + builtin local limit + # Send all words up to the word the cursor is currently on + builtin let limit=1+$COMP_CWORD + src=$(builtin printf "%s\n" "${COMP_WORDS[@]:0:$limit}" | builtin command kitten __complete__ bash) + if [[ $? == 0 ]]; then + builtin eval "${src}" + fi +} + +builtin complete -F _ksi_completions kitty +builtin complete -F _ksi_completions edit-in-kitty +builtin complete -F _ksi_completions clone-in-kitty +builtin complete -F _ksi_completions kitten +` + return []byte(script), nil +} + func bash_output_serializer(completions []*Completions, shell_state map[string]string) ([]byte, error) { output := strings.Builder{} f := func(format string, args ...any) { fmt.Fprintf(&output, format+"\n", args...) } @@ -51,6 +71,7 @@ func bash_init_completions(completions *Completions) { } func init() { + completion_scripts["bash"] = bash_completion_script input_parsers["bash"] = shell_input_parser output_serializers["bash"] = bash_output_serializer init_completions["bash"] = bash_init_completions diff --git a/tools/cli/completion-main.go b/tools/cli/completion-main.go index b7059a3a8..6840974ad 100644 --- a/tools/cli/completion-main.go +++ b/tools/cli/completion-main.go @@ -30,9 +30,11 @@ func json_output_serializer(completions []*Completions, shell_state map[string]s return json.Marshal(completions) } +type completion_script_func func(commands []string) ([]byte, error) type parser_func func(data []byte, shell_state map[string]string) ([][]string, error) type serializer_func func(completions []*Completions, shell_state map[string]string) ([]byte, error) +var completion_scripts = make(map[string]completion_script_func, 4) var input_parsers = make(map[string]parser_func, 4) var output_serializers = make(map[string]serializer_func, 4) var init_completions = make(map[string]func(*Completions), 4) @@ -61,6 +63,22 @@ func GenerateCompletions(args []string) error { if n < 1 { n = 1 } + if output_type == "setup" { + if len(args) == 0 { + return fmt.Errorf("The shell needs to be specified") + } + shell_name := args[0] + args = args[1:] + completion_script := completion_scripts[shell_name] + if completion_script == nil { + return fmt.Errorf("Unsupported shell: %s", shell_name) + } + output, err := completion_script(args) + if err == nil { + _, err = os.Stdout.Write(output) + } + return err + } shell_state := make(map[string]string, n) for _, arg := range args { k, v, found := utils.Cut(arg, "=") diff --git a/tools/cli/fish.go b/tools/cli/fish.go index 1a4f99a60..ea139de69 100644 --- a/tools/cli/fish.go +++ b/tools/cli/fish.go @@ -12,12 +12,53 @@ import ( var _ = fmt.Print +func fish_completion_script(commands []string) ([]byte, error) { + // One command in fish requires one completion script. + // Usage: kitten __complete__ setup fish [kitty|kitten|clone-in-kitty] + all_commands := map[string]bool{ + "kitty": true, + "clone-in-kitty": true, + "kitten": true, + } + if len(commands) == 0 { + for cmd, _ := range all_commands { + commands = append(commands, cmd) + } + } + script := strings.Builder{} + script.WriteString(`function __ksi_completions + set --local ct (commandline --current-token) + set --local tokens (commandline --tokenize --cut-at-cursor --current-process) + printf "%s\n" $tokens $ct | command kitten __complete__ fish | source - +end + +`) + for _, cmd := range commands { + if all_commands[cmd] { + fmt.Fprintf(&script, "complete -f -c %s -a \"(__ksi_completions)\"\n", cmd) + } else if strings.Contains(cmd, "=") { + // Reserved for `setup SHELL [KEY=VALUE ...]`, not used now. + continue + } else { + return nil, fmt.Errorf("No fish completion script for command: %s", cmd) + } + } + return []byte(script.String()), nil +} + func fish_output_serializer(completions []*Completions, shell_state map[string]string) ([]byte, error) { output := strings.Builder{} f := func(format string, args ...any) { fmt.Fprintf(&output, format+"\n", args...) } n := completions[0].Delegate.NumToRemove fm := markup.New(false) // fish freaks out if there are escape codes in the description strings - if n > 0 { + legacy_completion := shell_state["_legacy_completion"] + if legacy_completion == "fish2" { + for _, mg := range completions[0].Groups { + for _, m := range mg.Matches { + f("%s", strings.ReplaceAll(m.Word+"\t"+fm.Prettify(m.Description), "\n", " ")) + } + } + } else if n > 0 { words := make([]string, len(completions[0].AllWords)-n+1) words[0] = completions[0].Delegate.Command copy(words[1:], completions[0].AllWords[n:]) @@ -40,6 +81,7 @@ func fish_output_serializer(completions []*Completions, shell_state map[string]s } func init() { + completion_scripts["fish"] = fish_completion_script input_parsers["fish"] = shell_input_parser output_serializers["fish"] = fish_output_serializer } diff --git a/tools/cli/zsh.go b/tools/cli/zsh.go index 7cf88ad52..bf12de3cb 100644 --- a/tools/cli/zsh.go +++ b/tools/cli/zsh.go @@ -15,6 +15,26 @@ import ( var _ = fmt.Print +func zsh_completion_script(commands []string) ([]byte, error) { + script := `#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 +` + return []byte(script), nil +} + func shell_input_parser(data []byte, shell_state map[string]string) ([][]string, error) { raw := string(data) new_word := strings.HasSuffix(raw, "\n\n") @@ -150,6 +170,7 @@ func zsh_output_serializer(completions []*Completions, shell_state map[string]st } func init() { + completion_scripts["zsh"] = zsh_completion_script input_parsers["zsh"] = zsh_input_parser output_serializers["zsh"] = zsh_output_serializer } diff --git a/tools/cmd/completion/kitty.go b/tools/cmd/completion/kitty.go index d42a0d1dd..a6642ad14 100644 --- a/tools/cmd/completion/kitty.go +++ b/tools/cmd/completion/kitty.go @@ -87,7 +87,7 @@ func EntryPoint(tool_root *cli.Command) { Name: "__complete__", Hidden: true, Usage: "output_type [shell state...]", ShortDescription: "Generate completions for kitty commands", - HelpText: "Generate completion candidates for kitty commands. The command line is read from STDIN. output_type can be one of the supported shells or :code:`json` for JSON output.", + HelpText: "Generate completion candidates for kitty commands. The command line is read from STDIN. output_type can be one of the supported shells: :code:`zsh`, :code:`fish`, :code:`bash`, or :code:`setup` for completion setup script following with the shell name, or :code:`json` for JSON output.", Run: func(cmd *cli.Command, args []string) (ret int, err error) { return ret, cli.GenerateCompletions(args) },