From 6d790237c3ad3057e82e28fcb9dbb7f5b08114f1 Mon Sep 17 00:00:00 2001 From: Roman Perepelitsa Date: Tue, 21 Dec 2021 13:38:57 +0100 Subject: [PATCH 1/2] Rewrite zsh integration Note: - Very lightly tested. - Docs not updated. --- shell-integration/zsh/.zshenv | 29 ++- shell-integration/zsh/completions/_kitty | 7 + shell-integration/zsh/kitty-integration | 263 +++++++++++++++++++++++ shell-integration/zsh/kitty.zsh | 169 ++------------- 4 files changed, 307 insertions(+), 161 deletions(-) create mode 100644 shell-integration/zsh/completions/_kitty create mode 100644 shell-integration/zsh/kitty-integration diff --git a/shell-integration/zsh/.zshenv b/shell-integration/zsh/.zshenv index d152a25961a..17a0bcdf722 100644 --- a/shell-integration/zsh/.zshenv +++ b/shell-integration/zsh/.zshenv @@ -1,11 +1,15 @@ +# This file can get sourced with aliases enabled. To avoid alias expansion +# we quote everything that can be quoted. Some aliases will still break us +# though. + # Don't use [[ -v ... ]] because it doesn't work in zsh < 5.4. -if (( ${+KITTY_ORIG_ZDOTDIR} )); then +if [[ -n "${KITTY_ORIG_ZDOTDIR+X}" ]]; then # Normally ZDOTDIR shouldn't be exported but it was in the environment # of Kitty, so we export it. - export ZDOTDIR=$KITTY_ORIG_ZDOTDIR - unset KITTY_ORIG_ZDOTDIR + 'builtin' 'export' ZDOTDIR="$KITTY_ORIG_ZDOTDIR" + 'builtin' 'unset' 'KITTY_ORIG_ZDOTDIR' else - unset ZDOTDIR + 'builtin' 'unset' 'ZDOTDIR' fi # Use try-always to have the right error code. @@ -17,16 +21,19 @@ fi # # Use typeset in case we are in a function with warn_create_global in # effect. Unlikely but better safe than sorry. - typeset _ksi_source=${ZDOTDIR-~}/.zshenv + 'builtin' 'typeset' _ksi_file=${ZDOTDIR-~}"/.zshenv" # Zsh ignores unreadable rc files. We do the same. # Zsh ignores rc files that are directories, and so does source. - [[ ! -r $_ksi_source ]] || source -- "$_ksi_source" + [[ ! -r "$_ksi_file" ]] || 'builtin' 'source' '--' "$_ksi_file" } always { - if [[ -o interactive ]]; then + if [[ -o 'interactive' && -n "${KITTY_SHELL_INTEGRATION-}" ]]; then # ${(%):-%x} is the path to the current file. - # On top of it we add :a:h to get the directory. - typeset _ksi_source=${${(%):-%x}:A:h}/kitty.zsh - [[ ! -r $_ksi_source ]] || source -- "$_ksi_source" + # On top of it we add :A:h to get the directory. + 'builtin' 'typeset' _ksi_file="${${(%):-%x}:A:h}"/kitty-integration + if [[ -r "$_ksi_file" ]]; then + 'builtin' 'autoload' '-Uz' '--' "$_ksi_file" + "${_ksi_file:t}" + fi fi - unset _ksi_source + 'builtin' 'unset' '_ksi_file' } diff --git a/shell-integration/zsh/completions/_kitty b/shell-integration/zsh/completions/_kitty new file mode 100644 index 00000000000..694131ee645 --- /dev/null +++ b/shell-integration/zsh/completions/_kitty @@ -0,0 +1,7 @@ +#compdef kitty + +(( ${+commands[kitty]} )) || 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 kitty +complete zsh "_matcher=$_matcher" <<<$cmd) || builtin return +builtin eval "$src" diff --git a/shell-integration/zsh/kitty-integration b/shell-integration/zsh/kitty-integration new file mode 100644 index 00000000000..c01402e657e --- /dev/null +++ b/shell-integration/zsh/kitty-integration @@ -0,0 +1,263 @@ +#!/bin/zsh +# +# Enables integration between zsh and Kitty based on KITTY_SHELL_INTEGRATION. +# The latter is set by Kitty based on kitty.conf. +# +# This is an autoloadable function. It's invoked automatically in shells +# directly spawned by Kitty but not in any other shells. For example, running +# `exec zsh`, `sudo -E zsh`, `tmux`, or plain `zsh` will create a shell where +# kitty-integration won't automatically run. Zsh users who want integration with +# Kitty in all shells should add the following lines to their .zshrc: +# +# if [[ -n $KITTY_INSTALLATION_DIR ]]; then +# autoload -Uz -- "$KITTY_INSTALLATION_DIR"/shell-integration/zsh/kitty-integration +# kitty-integration +# fi +# +# Implementation note: We can assume that alias expansion is disabled in this +# file, so no need to quote defensively. We still have to defensively prefix all +# builtins with `builtin` to avoid accidentally invoking user-defined functions. +# We avoid `function` reserved word as an additional defensive measure. + +builtin emulate -L zsh -o no_warn_create_global + +[[ -o interactive ]] || builtin return 0 # non-interactive shell +[[ -n $KITTY_SHELL_INTEGRATION ]] || builtin return 0 # integration disabled +(( ! _ksi_state )) || builtin return 0 # already initialized + +if (( ! $+_ksi_state )); then + # 0: not initialized; deferred initialization can start now. + # 1: not initialized; waiting for deferred initialization. + # 2: initialized; no OSC 133 [AC] marks have been written yet. + # 3: initialized; the last written OSC 133 C has not been closed with D yet. + # 4: initialized; none of the above. + builtin typeset -gi _ksi_state=1 + + # Asks Kitty to print $@ to its stdout. This is for debugging. + _ksi_debug_print() { + builtin local data + data=$(command base64 <<<"${(j: :}@}") || builtin return + builtin printf '\eP@kitty-print|%s\e\\' "${data//$'\n'}" + } + + _ksi_deferred_init() { + (( _ksi_state = 0, 1 )) # `, 1` in case err_return is set + kitty-integration + } + + # We defer initialization until precmd for several reasons: + # + # - Oh My Zsh and many other configs remove zle-line-init and + # zle-line-finish hooks when they initialize. + # - By deferring initialization we allow user rc files to opt out from some + # parts of integration. For example, if a zshrc theme prints OSC 133 + # marks, it can append " no-prompt-mark" to KITTY_SHELL_INTEGRATION during + # intialization to avoid redundant marks from our code. + builtin typeset -ag precmd_functions + precmd_functions+=(_ksi_deferred_init) + builtin return +fi + +# The rest of kitty-integration performs deferred initialization. We are being +# run from _ksi_deferred_init here. + +(( _ksi_state = 2 )) + +# Recognized options: no-cursor, no-title, no-prompt-mark, no-complete. +builtin local -a opt +opt=(${(s: :)KITTY_SHELL_INTEGRATION}) +unset KITTY_SHELL_INTEGRATION + +# The directory where kitty-integration is located: /.../shell-integration/zsh. +builtin local self_dir=${functions_source[kitty-integration]:A:h} +# The directory with _kitty. We store it in a directory of its own rather than +# in $self_dir because we are adding it to fpath and we don't want any other +# files to be accidentally autoloadable. +builtin local comp_dir=$self_dir/completions + +# Enable completions for `kitty` command. +if (( ! opt[(Ie)no-complete] )) && [[ -r $comp_dir/_kitty ]]; then + if (( $+functions[compdef] )); then + # If compdef is defined, then either compinit has already run or it's + # a shim that records all calls for the purpose of replaying them after + # compinit. Either way we clobber the existing completion for kitty and + # install our own. + builtin unset "functions[_kitty]" + builtin autoload -Uz -- $comp_dir/_kitty + compdef _kitty kitty + fi + + # If compdef is not set, compinit has not run yet. In this case we must + # add our completions directory to fpath so that _kitty gets picked up by + # compinit. + # + # We extend fpath even if compinit has run because it might run again. + # Without our completions directory in fpath compinit would our _comp + # mapping. + builtin typeset -ga fpath + fpath=($comp_dir ${fpath:#$comp_dir}) +fi + +# Enable cursor shape changes depending on the current keymap. +if (( ! opt[(Ie)no-cursor] )); then + _ksi_zle_line_init _ksi_zle_line_finish _ksi_zle_keymap_select() { + case ${KEYMAP-} in + vicmd|visual) builtin print -n '\e[1 q';; # blinking block cursor + *) builtin print -n '\e[5 q';; # blinking bar cursor + esac + } +fi + +# Enable semantic markup with OSC 133. +if (( ! opt[(Ie)no-prompt-mark] )); then + _ksi_precmd() { + builtin local -i cmd_status=$? + builtin emulate -L zsh -o no_warn_create_global + + # Don't write OSC 133 D when our precmd handler is invoked from zle. + # Some plugins do that to update prompt on cd. + if ! builtin zle; then + # This code works incorrectly in the presence of a precmd or chpwd + # hook that prints. For example, sindresorhus/pure prints an empty + # line on precmd and marlonrichert/zsh-snap prints $PWD on chpwd. + # We'll end up writing our OSC 133 D mark too late. + # + # Another failure mode is when the output of a command doesn't end + # with LF and prompst_sp is set (it is by default). In this case + # we'll incorrectly state that '%' from prompt_sp is a part of the + # command's output. + if (( _ksi_state == 3 )); then + # The last written OSC 133 C has not been closed with D yet. + # Close it and supply status. + builtin printf '\e]133;D;%s\a' $cmd_status + (( _ksi_state = 4 )) + elif (( _ksi_state == 4 )); then + # There might be an unclosed OSC 133 C. Close that. + builtin print -n '\e]133;D\a' + fi + fi + + builtin local mark1=$'%{\e]133;A\a%}' + if [[ -o prompt_percent ]]; then + builtin typeset -g precmd_functions + if [[ ${precmd_functions[-1]} == _ksi_precmd ]]; then + # This is the best case for us: we can add our marks to PS1 and + # PS2. This way our marks will be printed whenever zsh + # redisplays prompt: on reset-prompt, on SIGWINCH, and on + # SIGCHLD if notify is set. Themes that update prompt + # asynchronously from a `zle -F` handler might still remove our + # marks. Oh well. + builtin local mark2=$'%{\e]133;A;k=s\a%}' + # Add marks conditionally to avoid a situation where we have + # several marks in place. These conditions can have false + # positives and false negatives though. + # + # - False positive (with prompt_percent): PS1="%(?.$mark1.)" + # - False negative (with prompt_subst): PS1='$mark1' + [[ $PS1 == *$mark1* ]] || PS1=${mark1}${PS1} + [[ $PS2 == *$mark2* ]] || PS2=${mark2}${PS2} + (( _ksi_state = 4 )) + else + # If our precmd hook is not the last, we cannot rely on prompt + # changes to stick, so we don't even try. At least we can move + # our hook to the end to have better luck next time. If there is + # another piece of code that wants to take this privileged + # position, this won't work well. We'll break them as much as + # they are breaking us. + precmd_functions=(${precmd_functions:#_ksi_precmd} _ksi_precmd) + # Plugins that invoke precmd hooks from zle do that before zle + # is trashed. This means that the cursor is in the middle of + # BUFFER and we cannot print our mark there. Prompt might + # already have a mark, so the following reset-prompt will write + # it. If it doesn't, there is nothing we can do. + if ! builtin zle; then + builtin print -rn -- $mark1[3,-3] + (( _ksi_state = 4 )) + fi + fi + elif ! builtin zle; then + # Without prompt_percent we cannot patch prompt. Just print the + # mark, except when we are invoked from zle. In the latter case we + # cannot do anything. + builtin print -rn -- $mark1[3,-3] + (( _ksi_state = 4 )) + fi + } + + _ksi_preexec() { + builtin emulate -L zsh -o no_warn_create_global + + # This can potentially break user prompt. Oh well. The robustness of + # this code can be improved in the case prompt_subst is set because + # it'll allow us distinguish (not perfectly but close enough) between + # our own prompt, user prompt, and our own prompt with user additions on + # top. We cannot force prompt_subst on the user though, so we would + # still need this code for the no_prompt_subst case. + PS1=${PS1//$'%{\e]133;A\a%}'} + PS2=${PS2//$'%{\e]133;A;k=s\a%}'} + + # This will work incorrectly in the presence of a preexec hook that + # prints. For example, if MichaelAquilina/zsh-you-should-use installs + # its preexec hook before us, we'll incorrectly mark its output as + # belonging to the command (as if the user typed it into zle) rather + # than command output. + builtin print -n '\e]133;C\a' + (( _ksi_state = 3 )) + } + + functions[_ksi_zle_line_init]+=' + builtin print -n "\\e]133;B\\a"' +fi + +# Enable terminal title changes. +if (( ! opt[(Ie)no-title] )); then + # We don't use `print -P` because it depends on prompt options, which + # we don't control and cannot change. + # + # We use (V) in preexec to convert control characters to something visible + # (LF becomes \n, etc.). This isn't necessary in precmd because (%) does it + # for us. + functions[_ksi_precmd]+=' + builtin printf "\\e]2;%s\\a" "${(%):-%(4~|…/%3~|%~)}"' + functions[_ksi_preexec]+=' + builtin printf "\\e]2;%s\\a" "${(V)1}"' +fi + +# Some zsh users manually run `source ~/.zshrc` in order to apply rc file +# changes to the current shell. This is a terrible practice that breaks many +# things, including our shell integration. For example, Oh My Zsh and Prezto +# (both very popular among zsh users) will remove zle-line-init and +# zle-line-finish hooks if .zshrc is manually sourced. Prezto will also remove +# zle-keymap-select. +# +# Another common (and much more robust) way to apply rc file changes to the +# current shell is `exec zsh`. This will remove our integration from the shell +# unless it's explicitly invoked from .zshrc. This is not an issue with +# `exec zsh` but rather with our implementation of automatic shell integration. +builtin autoload -Uz add-zle-hook-widget +if (( $+functions[_ksi_zle_line_init] )); then + add-zle-hook-widget line-init _ksi_zle_line_init +fi +if (( $+functions[_ksi_zle_line_finish] )); then + add-zle-hook-widget line-finish _ksi_zle_line_finish +fi +if (( $+functions[_ksi_zle_keymap_select] )); then + add-zle-hook-widget keymap-select _ksi_zle_keymap_select +fi + +if (( $+functions[_ksi_preexec] )); then + builtin typeset -ag preexec_functions + preexec_functions+=(_ksi_preexec) +fi + +builtin typeset -ag precmd_functions +if (( $+functions[_ksi_precmd] )); then + precmd_functions=(${precmd_functions:/_ksi_deferred_init/_ksi_precmd}) + _ksi_precmd +else + precmd_functions=(${precmd_functions:#_ksi_deferred_init}) +fi + +# Unfunction what we don't need to save memory. +builtin unfunction _ksi_deferred_init kitty-integration +builtin autoload -Uz -- $self_dir/kitty-integration diff --git a/shell-integration/zsh/kitty.zsh b/shell-integration/zsh/kitty.zsh index 7d5970e1c8b..76c9a7921fd 100644 --- a/shell-integration/zsh/kitty.zsh +++ b/shell-integration/zsh/kitty.zsh @@ -1,151 +1,20 @@ #!/bin/zsh - -() { - if [[ ! -o interactive ]]; then return; fi - if [[ -z "$KITTY_SHELL_INTEGRATION" ]]; then return; fi - if [[ ! -z "$_ksi_prompt" ]]; then return; fi - typeset -g -A _ksi_prompt - _ksi_prompt=(state first-run is_last_precmd y cursor y title y mark y complete y) - for i in ${=KITTY_SHELL_INTEGRATION}; do - if [[ "$i" == "no-cursor" ]]; then _ksi_prompt[cursor]='n'; fi - if [[ "$i" == "no-title" ]]; then _ksi_prompt[title]='n'; fi - if [[ "$i" == "no-prompt-mark" ]]; then _ksi_prompt[mark]='n'; fi - if [[ "$i" == "no-complete" ]]; then _ksi_prompt[complete]='n'; fi - done - unset KITTY_SHELL_INTEGRATION - - function _ksi_debug_print() { - # print a line to STDOUT of parent kitty process - local b=$(printf "%s\n" "$1" | base64 | tr -d \\n) - printf "\eP@kitty-print|%s\e\\" "$b" - } - - function _ksi_change_cursor_shape () { - # change cursor shape depending on mode - if [[ "$_ksi_prompt[cursor]" == "y" ]]; then - case $KEYMAP in - vicmd | visual) - # the command mode for vi - printf "\e[1 q" # blinking block cursor - ;; - *) - printf "\e[5 q" # blinking bar cursor - ;; - esac - fi - } - - function _ksi_osc() { - printf "\e]%s\a" "$1" - } - - function _ksi_mark() { - # tell kitty to mark the current cursor position using OSC 133 - if [[ "$_ksi_prompt[mark]" == "y" ]]; then _ksi_osc "133;$1"; fi - } - _ksi_prompt[start_mark]="%{$(_ksi_mark A)%}" - _ksi_prompt[secondary_mark]="%{$(_ksi_mark 'A;k=s')%}" - - function _ksi_set_title() { - if [[ "$_ksi_prompt[title]" == "y" ]]; then _ksi_osc "2;$1"; fi - } - - function _ksi_install_completion() { - if [[ "$_ksi_prompt[complete]" == "y" ]]; then - # compdef is only defined if compinit has been called - if whence compdef > /dev/null; then - compdef _ksi_complete kitty - fi - fi - } - - function _ksi_precmd() { - local cmd_status=$? - # Set kitty window title to the cwd, appropriately shortened, see - # https://unix.stackexchange.com/questions/273529/shorten-path-in-zsh-prompt - _ksi_set_title $(print -P '%(4~|…/%3~|%~)') - - # Prompt marking - if [[ "$_ksi_prompt[mark]" == "y" ]]; then - if [[ "$_ksi_prompt[state]" == "preexec" ]]; then - _ksi_mark "D;$cmd_status" - else - if [[ "$_ksi_prompt[state]" != "first-run" ]]; then _ksi_mark "D"; fi - fi - # we must use PS1 to set the prompt start mark as precmd functions are - # not called when the prompt is redrawn after a window resize or when a background - # job finishes. However, if we are not the last function in precmd_functions which - # can be the case on first run, PS1 might be broken by a following function, so - # output the mark directly in that case - if [[ "$_ksi_prompt[is_last_precmd]" != "y" ]]; then - _ksi_mark "A"; - _ksi_prompt[is_last_precmd]="y"; - else - if [[ "$PS1" != *"$_ksi_prompt[start_mark]"* ]]; then PS1="$_ksi_prompt[start_mark]$PS1" fi - fi - # PS2 is used for prompt continuation. On resize with a continued prompt only the last - # prompt is redrawn so we need to mark it - if [[ "$PS2" != *"$_ksi_prompt[secondary_mark]"* ]]; then PS2="$_ksi_prompt[secondary_mark]$PS2" fi - fi - _ksi_prompt[state]="precmd" - } - - function _ksi_zle_line_init() { - if [[ "$_ksi_prompt[mark]" == "y" ]]; then _ksi_mark "B"; fi - _ksi_change_cursor_shape - _ksi_prompt[state]="line-init" - } - - function _ksi_zle_line_finish() { - _ksi_change_cursor_shape - _ksi_prompt[state]="line-finish" - } - - function _ksi_preexec() { - if [[ "$_ksi_prompt[mark]" == "y" ]]; then - _ksi_mark "C"; - # remove the prompt mark sequence while the command is executing as it could read/modify the value of PS1 - PS1="${PS1//$_ksi_prompt[start_mark]/}" - PS2="${PS2//$_ksi_prompt[secondary_mark]/}" - fi - # Set kitty window title to the currently executing command - _ksi_set_title "$1" - _ksi_prompt[state]="preexec" - } - - function _ksi_first_run() { - # We install the real precmd and preexec functions here and remove this function - # from precmd_functions. This ensures that our functions are last. This is needed - # because the zsh prompt_init package actually sets PS1 in a precmd function and the user - # could have setup their own precmd function to set the prompt as well. - _ksi_install_completion - typeset -a -g precmd_functions - local idx=$precmd_functions[(ie)_ksi_first_run] - if [[ $idx -gt 0 ]]; then - if [[ $idx -lt ${#precmd_functions[@]} ]]; then - _ksi_prompt[is_last_precmd]="n" - fi - add-zsh-hook -d precmd _ksi_first_run - add-zsh-hook precmd _ksi_precmd - add-zsh-hook preexec _ksi_preexec - add-zle-hook-widget keymap-select _ksi_change_cursor_shape - add-zle-hook-widget line-init _ksi_zle_line_init - add-zle-hook-widget line-finish _ksi_zle_line_finish - _ksi_precmd - fi - } - - # Completion for kitty - _ksi_complete() { - local src - # Send all words up to the word the cursor is currently on - src=$(printf "%s\n" "${(@)words[1,$CURRENT]}" | kitty +complete zsh "_matcher=$_matcher") - if [[ $? == 0 ]]; then - eval ${src} - fi - } - - autoload -Uz add-zsh-hook - autoload -Uz add-zle-hook-widget - add-zsh-hook precmd _ksi_first_run -} +# +# This file can get sourced with aliases enabled. Moreover, it be sourced from +# zshrc, so the chance of having some aliases already defined is high. To avoid +# alias expansion we quote everything that can be quoted. Some aliases will +# still break us. For example: +# +# alias -g -- -r='$RANDOM' +# +# For this reason users are discouraged from sourcing kitty.zsh in favor of +# invoking kitty-integration directly. + +# ${(%):-%x} is the path to the current file. +# On top of it we add :A:h to get the directory. +'builtin' 'typeset' _ksi_file="${${(%):-%x}:A:h}"/kitty-integration +if [[ -r "$_ksi_file" ]]; then + 'builtin' 'autoload' '-Uz' '--' "$_ksi_file" + "${_ksi_file:t}" +fi +'builtin' 'unset' '_ksi_file' From 37741ac808946dcaf71032ab358461b127637f57 Mon Sep 17 00:00:00 2001 From: Roman Perepelitsa Date: Wed, 22 Dec 2021 15:04:31 +0100 Subject: [PATCH 2/2] Add a comment within zsh integration related to cursor shape changes --- shell-integration/zsh/kitty-integration | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shell-integration/zsh/kitty-integration b/shell-integration/zsh/kitty-integration index c01402e657e..392a29b6530 100644 --- a/shell-integration/zsh/kitty-integration +++ b/shell-integration/zsh/kitty-integration @@ -100,6 +100,9 @@ fi # Enable cursor shape changes depending on the current keymap. if (( ! opt[(Ie)no-cursor] )); then + # This implementation leaks blinking block cursor into external commands + # executed from zle. For example, users of fzf-based widgets may find + # themselves with a blinking block cursor within fzf. _ksi_zle_line_init _ksi_zle_line_finish _ksi_zle_keymap_select() { case ${KEYMAP-} in vicmd|visual) builtin print -n '\e[1 q';; # blinking block cursor