Shell integration - kitty
Description: kitty has the ability to integrate closely within common shells, such as zsh, fish and bash to enable features such as jumping to previous prompts in the scrollback, viewing the output of the last ...

# Shell integration

kitty has the ability to integrate closely within common shells, such as zsh, fish and bash to enable features such as jumping to previous prompts in the scrollback, viewing the output of the last command in less, using the mouse to move the cursor while editing prompts, etc.

New in version 0.22.0.

# Features

- Open the output of the last command in a pager such as less (`ctrl+shift+g`)

- Jump to the previous/next prompt in the scrollback (`ctrl+shift+z` / `ctrl+shift+x`)

- Click with the mouse anywhere in the current command to move the cursor there

- The current working directory or the command being executed are automatically displayed in the kitty window titlebar/tab title.

- The text cursor is changed to a bar when editing commands at the shell prompt

- Glitch free window resizing even with complex prompts. Achieved by erasing the prompt on resize and allowing the shell to redraw it cleanly.

- Sophisticated completion for the kitty command in the shell

# Configuration

Shell integration is controlled by `shell_integration`. By default, all shell integration is enabled. Individual features can be turned off or it can be disabled entirely as well. The `shell_integration` option takes a space separated list of keywords:

disabled

  turn off all shell integration

no-rc

  dont modify the shell's rc files to enable integration. Useful if you prefer to manually enable integration.

no-cursor

  turn off changing of the text cursor to a bar when editing text

no-title

  turn off setting the kitty window/tab title based on shell state

no-prompt-mark

  turn off marking of prompts. This disables jumping to prompt, browsing output of last command and click to move cursor functionality.

no-complete

  turn off completion for the kitty command.

# How it works

At startup kitty detects if the shell you have configured (either system wide or in kitty.conf) is a supported shell. If so, kitty adds a couple of lines to the bottom of the shell's rc files (in an atomic manner) to load the shell integration code.

Then, when launching the shell, kitty sets the environment variable `KITTY_SHELL_INTEGRATION` to the value of the `shell_integration` option. The shell integration code reads the environment variable, turns on the specified integration functionality and then unsets the variable so as to not pollute the system. This has the nice effect that the changes to the shell's rc files become no-ops when running the shell in anything other than kitty itself.

The actual shell integration code uses hooks provided by each shell to send special escape codes to kitty, to perform the various tasks. You can see the code used for each shell below:

Click to toggle shell integration code
zsh

    #!/bin/zsh

    () {
        if [[ ! -o interactive ]]; then return; fi
        if [[ -z "$KITTY_SHELL_INTEGRATION" ]]; then return; fi
        typeset -g -A _ksi_prompt=([state]='first-run' [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_zle_keymap_select() {
            _ksi_change_cursor_shape
        }
        function _ksi_zle_keymap_select_with_original() { zle kitty-zle-keymap-select-original; _ksi_zle_keymap_select }
        zle -A zle-keymap-select kitty-zle-keymap-select-original 2>/dev/null
        if [[ $? == 0 ]]; then
            zle -N zle-keymap-select _ksi_zle_keymap_select_with_original
        else
            zle -N zle-keymap-select _ksi_zle_keymap_select
        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)%}"

        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=$?
            if [[ "$_ksi_prompt[state]" == "first-run" ]]; then
                _ksi_install_completion
            fi
            # 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
                if [[ "$PS1" != *"$_ksi_prompt[start_mark]"* ]]; then PS1="$_ksi_prompt[start_mark]$PS1" 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_init_with_orginal() { zle kitty-zle-line-init-original; _ksi_zle_line_init }
        zle -A zle-line-init kitty-zle-line-init-original 2>/dev/null
        if [[ $? == 0 ]]; then
            zle -N zle-line-init _ksi_zle_line_init_with_orginal
        else
            zle -N zle-line-init _ksi_zle_line_init
        fi

        function _ksi_zle_line_finish() {
            _ksi_change_cursor_shape
            _ksi_prompt[state]="line-finish"
        }
        function _ksi_zle_line_finish_with_orginal() { zle kitty-zle-line-finish-original; _ksi_zle_line_finish }
        zle -A zle-line-finish kitty-zle-line-finish-original 2>/dev/null
        if [[ $? == 0 ]]; then
            zle -N zle-line-finish _ksi_zle_line_finish_with_orginal
        else
            zle -N zle-line-finish _ksi_zle_line_finish
        fi

        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]/}"
            fi
            # Set kitty window title to the currently executing command
            _ksi_set_title "$1"
            _ksi_prompt[state]="preexec"
        }

        typeset -a -g precmd_functions
        precmd_functions=($precmd_functions _ksi_precmd)
        typeset -a -g preexec_functions
        preexec_functions=($preexec_functions _ksi_preexec)

        # 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)
            if [[ $? == 0 ]]; then
                eval ${src}
            fi
        }
    }

fish

    #!/bin/fish

    function _ksi_main
        test -z "$KITTY_SHELL_INTEGRATION" && return
        set --local _ksi (string split " " -- "$KITTY_SHELL_INTEGRATION")
        set --erase KITTY_SHELL_INTEGRATION

        function _ksi_osc
            printf "\e]%s\a" "$argv[1]"
        end

        if not contains "no-complete" $_ksi
            function _ksi_completions
                set --local ct (commandline --current-token)
                set --local tokens (commandline --tokenize --cut-at-cursor --current-process)
                printf "%s\n" $tokens $ct | kitty +complete fish2
            end
        end

        if not contains "no-cursor" $_ksi
            function _ksi_bar_cursor --on-event fish_prompt
                printf "\e[5 q"
            end
            function _ksi_block_cursor --on-event fish_preexec
                printf "\e[2 q"
            end
            _ksi_bar_cursor
        end

        if not contains "no-title" $_ksi
            function fish_title
                if set -q argv[1]
                    echo $argv[1]
                else
                    echo (prompt_pwd)
                end
            end
        end

        if not contains "no-prompt-mark" $_ksi
            set --global _ksi_prompt_state "first-run"

            function _ksi_function_is_not_empty -d "Check if the specified function exists and is not empty"
                test (functions $argv[1] | grep -cvE '^ *(#|function |end$|$)') != 0
            end

            function _ksi_mark -d "tell kitty to mark the current cursor position using OSC 133"
                _ksi_osc "133;$argv[1]";
            end

            function _ksi_start_prompt
                if test "$_ksi_prompt_state" != "postexec" -a "$_ksi_prompt_state" != "first-run"
                    _ksi_mark "D"
                end
                set --global _ksi_prompt_state "prompt_start"
                _ksi_mark "A"
            end

            function _ksi_end_prompt
                _ksi_original_fish_prompt
                set --global _ksi_prompt_state "prompt_end"
                _ksi_mark "B"
            end

            functions -c fish_prompt _ksi_original_fish_prompt

            if _ksi_function_is_not_empty fish_mode_prompt
                # see https://github.com/starship/starship/issues/1283
                # for why we have to test for a non-empty fish_mode_prompt
                functions -c fish_mode_prompt _ksi_original_fish_mode_prompt
                function fish_mode_prompt
                    _ksi_start_prompt
                    _ksi_original_fish_mode_prompt
                end
                function fish_prompt
                    _ksi_end_prompt
                end
            else
                function fish_prompt
                    _ksi_start_prompt
                    _ksi_end_prompt
                end
            end

            function _ksi_mark_output_start --on-event fish_preexec
                set --global _ksi_prompt_state "preexec"
                _ksi_mark "C"
            end

            function _ksi_mark_output_end --on-event fish_postexec
                set --global _ksi_prompt_state "postexec"
                _ksi_mark "D;$status"
            end
            # with prompt marking kitty clears the current prompt on resize so we need
            # fish to redraw it
            set --global fish_handle_reflow 1
        end
        functions --erase _ksi_main
        functions --erase _ksi_schedule
    end

    if status --is-interactive
        function _ksi_schedule --on-event fish_prompt -d "Setup kitty integration after other scripts have run, we hope"
            _ksi_main
        end
    else
        functions --erase _ksi_main
    end

bash

    #!/bin/bash

    _ksi_main() {
        if [[ $- != *i* ]] ; then return; fi  # check in interactive mode
        if [[ -z "$KITTY_SHELL_INTEGRATION" ]]; then return; fi
        declare -A _ksi_prompt=( [cursor]='y' [title]='y' [mark]='y' [complete]='y' )
        set -f
        for i in ${KITTY_SHELL_INTEGRATION}; do
            set +f
            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
        set +f

        unset KITTY_SHELL_INTEGRATION

        _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"
            # "
        }

        if [[ "${_ksi_prompt[cursor]}" == "y" ]]; then
            PS1="\[\e[5 q\]$PS1"  # blinking bar cursor
            PS0="\[\e[1 q\]$PS0"  # blinking block cursor
        fi

        if [[ "${_ksi_prompt[title]}" == "y" ]]; then
            # see https://www.gnu.org/software/bash/manual/html_node/Controlling-the-Prompt.html#Controlling-the-Prompt
            PS1="\[\e]2;\w\a\]$PS1"
            if [[ "$HISTCONTROL" == *"ignoreboth"* ]] || [[ "$HISTCONTROL" == *"ignorespace"* ]]; then
                _ksi_debug_print "ignoreboth or ignorespace present in bash HISTCONTROL setting, showing running command in window title will not be robust"
            fi
            local orig_ps0="$PS0"
            PS0='$(printf "\e]2;%s\a" "$(HISTTIMEFORMAT= history 1 | sed -e "s/^[ ]*[0-9]*[ ]*//")")'
            PS0+="$orig_ps0"
        fi

        if [[ "${_ksi_prompt[mark]}" == "y" ]]; then
            PS1="\[\e]133;A\a\]$PS1"
            PS0="\[\e]133;C\a\]$PS0"
        fi

        if [[ "${_ksi_prompt[complete]}" == "y" ]]; then
            _ksi_completions() {
                local src
                local limit
                # Send all words up to the word the cursor is currently on
                let limit=1+$COMP_CWORD
                src=$(printf "%s\n" "${COMP_WORDS[@]: 0:$limit}" | kitty +complete bash)
                if [[ $? == 0 ]]; then
                    eval ${src}
                fi
            }
            complete -o nospace -F _ksi_completions kitty
        fi
    }
    _ksi_main

# Manual shell integration

If you do not want to rely on kitty's automatic shell integration or if you want to setup shell integration for a remote system over SSH, in `kitty.conf` set:

    shell_integration disabled

Then in your shell's rc file, add the lines:

    export KITTY_SHELL_INTEGRATION="enabled"
    source /path/to/integration/script

You can get the path to the directory containing the various shell integration scripts by looking at the directory displayed by:

    kitty +runpy "from kitty.constants import *; print(shell_integration_dir)"

The value of `KITTY_SHELL_INTEGRATION` is the same as that for `shell_integration`, except if you want to disable shell integration completely, in which case simply do not set the `KITTY_SHELL_INTEGRATION` variable at all.