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+x / ctrl+shift+d)

  • 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
#!/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
    }
}
#!/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
#!/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.