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