kitty.bash 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. #!/bin/bash
  2. if [[ "$-" != *i* ]] ; then builtin return; fi # check in interactive mode
  3. if [[ -z "$KITTY_SHELL_INTEGRATION" ]]; then builtin return; fi
  4. # Load the normal bash startup files
  5. if [[ -n "$KITTY_BASH_INJECT" ]]; then
  6. builtin declare kitty_bash_inject="$KITTY_BASH_INJECT"
  7. builtin declare ksi_val="$KITTY_SHELL_INTEGRATION"
  8. builtin unset KITTY_SHELL_INTEGRATION # ensure manual sourcing of this file in bashrc does not have any effect
  9. builtin unset KITTY_BASH_INJECT ENV
  10. if [[ -z "$HOME" ]]; then HOME=~; fi
  11. if [[ -z "$KITTY_BASH_ETC_LOCATION" ]]; then KITTY_BASH_ETC_LOCATION="/etc"; fi
  12. _ksi_sourceable() {
  13. [[ -f "$1" && -r "$1" ]] && builtin return 0; builtin return 1;
  14. }
  15. if [[ -n "$ksi_val" && "$ksi_val" != *no-sudo* && -n "$TERMINFO" && ! ( -r "/usr/share/terminfo/x/xterm-kitty" || -r "/usr/share/terminfo/78/xterm-kitty" ) ]]; then
  16. # this must be done before sourcing user bashrc otherwise aliasing of sudo does not work
  17. sudo() {
  18. # Ensure terminfo is available in sudo
  19. builtin local is_sudoedit="n"
  20. for arg; do
  21. if [[ "$arg" == "-e" || $arg == "--edit" ]]; then
  22. is_sudoedit="y"
  23. builtin break;
  24. fi
  25. [[ "$arg" != -* && "$arg" != *=* ]] && builtin break # command found
  26. done
  27. if [[ "$is_sudoedit" == "y" ]]; then
  28. builtin command sudo "$@";
  29. else
  30. builtin command sudo TERMINFO="$TERMINFO" "$@";
  31. fi
  32. }
  33. fi
  34. if [[ "$kitty_bash_inject" == *"posix"* ]]; then
  35. _ksi_sourceable "$KITTY_BASH_POSIX_ENV" && {
  36. builtin source "$KITTY_BASH_POSIX_ENV"
  37. builtin export ENV="$KITTY_BASH_POSIX_ENV"
  38. }
  39. else
  40. builtin set +o posix
  41. builtin shopt -u inherit_errexit 2>/dev/null # resetting posix does not clear this
  42. if [[ -n "$KITTY_BASH_UNEXPORT_HISTFILE" ]]; then
  43. builtin export -n HISTFILE
  44. builtin unset KITTY_BASH_UNEXPORT_HISTFILE
  45. fi
  46. # See run_startup_files() in shell.c in the Bash source code
  47. if builtin shopt -q login_shell; then
  48. if [[ "$kitty_bash_inject" != *"no-profile"* ]]; then
  49. _ksi_sourceable "$KITTY_BASH_ETC_LOCATION/profile" && builtin source "$KITTY_BASH_ETC_LOCATION/profile"
  50. for _ksi_i in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do
  51. _ksi_sourceable "$_ksi_i" && { builtin source "$_ksi_i"; break; }
  52. done
  53. fi
  54. else
  55. if [[ "$kitty_bash_inject" != *"no-rc"* ]]; then
  56. # Linux distros build bash with -DSYS_BASHRC. Unfortunately, there is
  57. # no way to probe bash for it and different distros use different files
  58. # Arch, Debian, Ubuntu use /etc/bash.bashrc
  59. # Fedora uses /etc/bashrc sourced from ~/.bashrc instead of SYS_BASHRC
  60. # Void Linux uses /etc/bash/bashrc
  61. for _ksi_i in "$KITTY_BASH_ETC_LOCATION/bash.bashrc" "$KITTY_BASH_ETC_LOCATION/bash/bashrc" ; do
  62. _ksi_sourceable "$_ksi_i" && { builtin source "$_ksi_i"; break; }
  63. done
  64. if [[ -z "$KITTY_BASH_RCFILE" ]]; then KITTY_BASH_RCFILE="$HOME/.bashrc"; fi
  65. _ksi_sourceable "$KITTY_BASH_RCFILE" && builtin source "$KITTY_BASH_RCFILE"
  66. fi
  67. fi
  68. fi
  69. builtin unset KITTY_BASH_RCFILE KITTY_BASH_POSIX_ENV KITTY_BASH_ETC_LOCATION
  70. builtin unset -f _ksi_sourceable
  71. builtin export KITTY_SHELL_INTEGRATION="$ksi_val"
  72. builtin unset _ksi_i ksi_val kitty_bash_inject
  73. fi
  74. if [ "${BASH_VERSINFO:-0}" -lt 4 ]; then
  75. builtin unset KITTY_SHELL_INTEGRATION
  76. builtin printf "%s\n" "Bash version ${BASH_VERSION} too old, kitty shell integration disabled" > /dev/stderr
  77. builtin return
  78. fi
  79. if [[ "${_ksi_prompt[sourced]}" == "y" ]]; then
  80. # we have already run
  81. builtin unset KITTY_SHELL_INTEGRATION
  82. builtin return
  83. fi
  84. # this is defined outside _ksi_main to make it global without using declare -g
  85. # which is not available on older bash
  86. builtin declare -A _ksi_prompt
  87. _ksi_prompt=(
  88. [cursor]='y' [title]='y' [mark]='y' [complete]='y' [cwd]='y' [sudo]='y' [ps0]='' [ps0_suffix]='' [ps1]='' [ps1_suffix]='' [ps2]=''
  89. [hostname_prefix]='' [sourced]='y' [last_reported_cwd]=''
  90. )
  91. _ksi_main() {
  92. builtin local ifs="$IFS" i
  93. IFS=" "
  94. for i in ${KITTY_SHELL_INTEGRATION[@]}; do
  95. case "$i" in
  96. "no-cursor") _ksi_prompt[cursor]='n';;
  97. "no-title") _ksi_prompt[title]='n';;
  98. "no-prompt-mark") _ksi_prompt[mark]='n';;
  99. "no-complete") _ksi_prompt[complete]='n';;
  100. "no-cwd") _ksi_prompt[cwd]='n';;
  101. "no-sudo") _ksi_prompt[sudo]='n';;
  102. esac
  103. done
  104. IFS="$ifs"
  105. builtin unset KITTY_SHELL_INTEGRATION
  106. if [[ -n "$SSH_KITTEN_KITTY_DIR" ]]; then
  107. if [[ ! "$PATH" =~ (^|:)${SSH_KITTEN_KITTY_DIR}(:|$) ]] && [[ -z "$(builtin command -v kitten)" ]]; then
  108. builtin export PATH="${PATH}:${SSH_KITTEN_KITTY_DIR}"
  109. fi
  110. builtin unset SSH_KITTEN_KITTY_DIR
  111. fi
  112. _ksi_debug_print() {
  113. # print a line to STDERR of parent kitty process
  114. builtin local b
  115. b=$(builtin command base64 <<< "${@}")
  116. builtin printf "\eP@kitty-print|%s\e\\" "${b//[[:space:]]}}"
  117. }
  118. _ksi_set_mark() {
  119. _ksi_prompt["${1}_mark"]="\[\e]133;k;${1}_kitty\a\]"
  120. }
  121. _ksi_set_mark start
  122. _ksi_set_mark end
  123. _ksi_set_mark start_secondary
  124. _ksi_set_mark end_secondary
  125. _ksi_set_mark start_suffix
  126. _ksi_set_mark end_suffix
  127. builtin unset -f _ksi_set_mark
  128. _ksi_prompt[secondary_prompt]="\n${_ksi_prompt[start_secondary_mark]}\[\e]133;A;k=s\a\]${_ksi_prompt[end_secondary_mark]}"
  129. _ksi_prompt_command() {
  130. # we first remove any previously added kitty code from the prompt variables and then add
  131. # it back, to ensure we have only a single instance
  132. if [[ -n "${_ksi_prompt[ps0]}" ]]; then
  133. PS0=${PS0//\\\[\\e\]133;k;start_kitty\\a\\\]*end_kitty\\a\\\]}
  134. PS0="${_ksi_prompt[ps0]}$PS0"
  135. fi
  136. if [[ -n "${_ksi_prompt[ps0_suffix]}" ]]; then
  137. PS0=${PS0//\\\[\\e\]133;k;start_suffix_kitty\\a\\\]*end_suffix_kitty\\a\\\]}
  138. PS0="${PS0}${_ksi_prompt[ps0_suffix]}"
  139. fi
  140. # restore PS1 to its pristine state without our additions
  141. if [[ -n "${_ksi_prompt[ps1]}" ]]; then
  142. PS1=${PS1//\\\[\\e\]133;k;start_kitty\\a\\\]*end_kitty\\a\\\]}
  143. PS1=${PS1//\\\[\\e\]133;k;start_secondary_kitty\\a\\\]*end_secondary_kitty\\a\\\]}
  144. fi
  145. if [[ -n "${_ksi_prompt[ps1_suffix]}" ]]; then
  146. PS1=${PS1//\\\[\\e\]133;k;start_suffix_kitty\\a\\\]*end_suffix_kitty\\a\\\]}
  147. fi
  148. if [[ -n "${_ksi_prompt[ps1]}" ]]; then
  149. if [[ "${_ksi_prompt[mark]}" == "y" && ( "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ) ]]; then
  150. builtin local oldval
  151. oldval=$(builtin shopt -p extglob)
  152. builtin shopt -s extglob
  153. # bash does not redraw the leading lines in a multiline prompt so
  154. # mark the last line as a secondary prompt. Otherwise on resize the
  155. # lines before the last line will be erased by kitty.
  156. # the first part removes everything from the last \n onwards
  157. # the second part appends a newline with the secondary marking
  158. # the third part appends everything after the last newline
  159. PS1=${PS1%@('\n'|$'\n')*}${_ksi_prompt[secondary_prompt]}${PS1##*@('\n'|$'\n')}
  160. builtin eval "$oldval"
  161. fi
  162. PS1="${_ksi_prompt[ps1]}$PS1"
  163. fi
  164. if [[ -n "${_ksi_prompt[ps1_suffix]}" ]]; then
  165. PS1="${PS1}${_ksi_prompt[ps1_suffix]}"
  166. fi
  167. if [[ -n "${_ksi_prompt[ps2]}" ]]; then
  168. PS2=${PS2//\\\[\\e\]133;k;start_kitty\\a\\\]*end_kitty\\a\\\]}
  169. PS2="${_ksi_prompt[ps2]}$PS2"
  170. fi
  171. if [[ "${_ksi_prompt[cwd]}" == "y" ]]; then
  172. # unfortunately bash provides no hooks to detect cwd changes
  173. # in particular this means cwd reporting will not happen for a
  174. # command like cd /test && cat. PS0 is evaluated before cd is run.
  175. if [[ "${_ksi_prompt[last_reported_cwd]}" != "$PWD" ]]; then
  176. _ksi_prompt[last_reported_cwd]="$PWD"
  177. builtin printf "\e]7;kitty-shell-cwd://%s%s\a" "$HOSTNAME" "$PWD"
  178. fi
  179. fi
  180. }
  181. if [[ "${_ksi_prompt[cursor]}" == "y" ]]; then
  182. _ksi_prompt[ps1_suffix]+="\[\e[5 q\]" # blinking bar cursor
  183. _ksi_prompt[ps0_suffix]+="\[\e[0 q\]" # blinking default cursor
  184. fi
  185. if [[ "${_ksi_prompt[title]}" == "y" || "${_ksi_prompt[mark]}" ]]; then
  186. _ksi_get_current_command() {
  187. builtin local last_cmd
  188. last_cmd=$(HISTTIMEFORMAT= builtin history 1)
  189. last_cmd="${last_cmd#*[[:digit:]]*[[:space:]]}" # remove leading history number
  190. last_cmd="${last_cmd#"${last_cmd%%[![:space:]]*}"}" # remove remaining leading whitespace
  191. if [[ "${_ksi_prompt[title]}" == "y" ]]; then
  192. builtin printf "\e]2;%s%s\a" "${_ksi_prompt[hostname_prefix]@P}" "${last_cmd//[[:cntrl:]]}" # removes any control characters
  193. fi
  194. if [[ "${_ksi_prompt[mark]}" == "y" ]]; then
  195. builtin printf "\e]133;C;cmdline=%q\a" "$last_cmd"
  196. fi
  197. }
  198. _ksi_prompt[ps0]+='$(_ksi_get_current_command > /dev/tty)';
  199. fi
  200. if [[ "${_ksi_prompt[title]}" == "y" ]]; then
  201. if [[ -z "$KITTY_PID" ]]; then
  202. if [[ -n "$SSH_TTY" || -n "$SSH2_TTY$KITTY_WINDOW_ID" ]]; then
  203. # connected to most SSH servers
  204. # or use ssh kitten to connected to some SSH servers that do not set SSH_TTY
  205. _ksi_prompt[hostname_prefix]="\h: "
  206. elif [[ -n "$(builtin command -v who)" && "$(builtin command who -m 2> /dev/null)" =~ "\([a-fA-F.:0-9]+\)$" ]]; then
  207. # the shell integration script is installed manually on the remote system
  208. # the environment variables are cleared after sudo
  209. # OpenSSH's sshd creates entries in utmp for every login so use those
  210. _ksi_prompt[hostname_prefix]="\h: "
  211. fi
  212. fi
  213. # see https://www.gnu.org/software/bash/manual/html_node/Controlling-the-Prompt.html#Controlling-the-Prompt
  214. # we use suffix here because some distros add title setting to their bashrc files by default
  215. _ksi_prompt[ps1_suffix]+="\[\e]2;${_ksi_prompt[hostname_prefix]}\w\a\]"
  216. if [[ "$HISTCONTROL" == *"ignoreboth"* ]] || [[ "$HISTCONTROL" == *"ignorespace"* ]]; then
  217. _ksi_debug_print "ignoreboth or ignorespace present in bash HISTCONTROL setting, showing running command will not be robust"
  218. fi
  219. fi
  220. if [[ "${_ksi_prompt[mark]}" == "y" ]]; then
  221. # this can result in multiple D prompt marks or ones that dont
  222. # correspond to a cmd but kitty handles this gracefully, only
  223. # taking into account the first D after a C.
  224. _ksi_prompt[ps1]+="\[\e]133;D;\$?\a\e]133;A\a\]"
  225. _ksi_prompt[ps2]+="\[\e]133;A;k=s\a\]"
  226. fi
  227. builtin alias edit-in-kitty="kitten edit-in-kitty"
  228. if [[ "${_ksi_prompt[complete]}" == "y" ]]; then
  229. _ksi_completions() {
  230. builtin local src
  231. builtin local limit
  232. # Send all words up to the word the cursor is currently on
  233. builtin let limit=1+$COMP_CWORD
  234. src=$(builtin printf "%s\n" "${COMP_WORDS[@]:0:$limit}" | builtin command kitten __complete__ bash)
  235. if [[ $? == 0 ]]; then
  236. builtin eval "${src}"
  237. fi
  238. }
  239. builtin complete -F _ksi_completions kitty
  240. builtin complete -F _ksi_completions edit-in-kitty
  241. builtin complete -F _ksi_completions clone-in-kitty
  242. builtin complete -F _ksi_completions kitten
  243. fi
  244. # wrap our prompt additions in markers we can use to remove them using
  245. # bash's anemic pattern substitution
  246. if [[ -n "${_ksi_prompt[ps0]}" ]]; then
  247. _ksi_prompt[ps0]="${_ksi_prompt[start_mark]}${_ksi_prompt[ps0]}${_ksi_prompt[end_mark]}"
  248. fi
  249. if [[ -n "${_ksi_prompt[ps0_suffix]}" ]]; then
  250. _ksi_prompt[ps0_suffix]="${_ksi_prompt[start_suffix_mark]}${_ksi_prompt[ps0_suffix]}${_ksi_prompt[end_suffix_mark]}"
  251. fi
  252. if [[ -n "${_ksi_prompt[ps1]}" ]]; then
  253. _ksi_prompt[ps1]="${_ksi_prompt[start_mark]}${_ksi_prompt[ps1]}${_ksi_prompt[end_mark]}"
  254. fi
  255. if [[ -n "${_ksi_prompt[ps1_suffix]}" ]]; then
  256. _ksi_prompt[ps1_suffix]="${_ksi_prompt[start_suffix_mark]}${_ksi_prompt[ps1_suffix]}${_ksi_prompt[end_suffix_mark]}"
  257. fi
  258. if [[ -n "${_ksi_prompt[ps2]}" ]]; then
  259. _ksi_prompt[ps2]="${_ksi_prompt[start_mark]}${_ksi_prompt[ps2]}${_ksi_prompt[end_mark]}"
  260. fi
  261. # BASH aborts the entire script when doing unset with failglob set, somebody should report this upstream
  262. builtin local oldval
  263. oldval=$(builtin shopt -p failglob)
  264. builtin shopt -u failglob
  265. builtin unset _ksi_prompt[start_mark] _ksi_prompt[end_mark] _ksi_prompt[start_suffix_mark] _ksi_prompt[end_suffix_mark] _ksi_prompt[start_secondary_mark] _ksi_prompt[end_secondary_mark]
  266. builtin eval "$oldval"
  267. # install our prompt command, using an array if it is unset or already an array,
  268. # otherwise append a string. We check if _ksi_prompt_command exists as some shell
  269. # scripts stupidly export PROMPT_COMMAND making it inherited by all programs launched
  270. # from the shell
  271. builtin local pc
  272. pc='builtin declare -F _ksi_prompt_command > /dev/null 2> /dev/null && _ksi_prompt_command'
  273. if [[ -z "${PROMPT_COMMAND[*]}" ]]; then
  274. PROMPT_COMMAND=([0]="$pc")
  275. elif [[ $(builtin declare -p PROMPT_COMMAND 2> /dev/null) =~ 'declare -a PROMPT_COMMAND' ]]; then
  276. PROMPT_COMMAND+=("$pc")
  277. else
  278. builtin local oldval
  279. oldval=$(builtin shopt -p extglob)
  280. builtin shopt -s extglob
  281. PROMPT_COMMAND="${PROMPT_COMMAND%%+([[:space:]])}"
  282. PROMPT_COMMAND="${PROMPT_COMMAND%%+(;)}"
  283. builtin eval "$oldval"
  284. PROMPT_COMMAND+="; $pc"
  285. fi
  286. if [ -n "${KITTY_IS_CLONE_LAUNCH}" ]; then
  287. builtin local orig_conda_env="$CONDA_DEFAULT_ENV"
  288. builtin eval "${KITTY_IS_CLONE_LAUNCH}"
  289. builtin hash -r 2> /dev/null 1> /dev/null
  290. builtin local venv="${VIRTUAL_ENV}/bin/activate"
  291. builtin local sourced=""
  292. _ksi_s_is_ok() {
  293. [[ -z "$sourced" && "$KITTY_CLONE_SOURCE_STRATEGIES" == *",$1,"* ]] && builtin return 0
  294. builtin return 1
  295. }
  296. if _ksi_s_is_ok "venv" && [ -n "${VIRTUAL_ENV}" -a -r "$venv" ]; then
  297. sourced="y"
  298. builtin unset VIRTUAL_ENV
  299. builtin source "$venv"
  300. fi; if _ksi_s_is_ok "conda" && [ -n "${CONDA_DEFAULT_ENV}" ] && builtin command -v conda >/dev/null 2>/dev/null && [ "${CONDA_DEFAULT_ENV}" != "$orig_conda_env" ]; then
  301. sourced="y"
  302. conda activate "${CONDA_DEFAULT_ENV}"
  303. fi; if _ksi_s_is_ok "env_var" && [[ -n "${KITTY_CLONE_SOURCE_CODE}" ]]; then
  304. sourced="y"
  305. builtin eval "${KITTY_CLONE_SOURCE_CODE}"
  306. fi; if _ksi_s_is_ok "path" && [[ -r "${KITTY_CLONE_SOURCE_PATH}" ]]; then
  307. sourced="y"
  308. builtin source "${KITTY_CLONE_SOURCE_PATH}"
  309. fi
  310. builtin unset -f _ksi_s_is_ok
  311. # Ensure PATH has no duplicate entries
  312. if [ -n "$PATH" ]; then
  313. builtin local old_PATH=$PATH:; PATH=
  314. while [ -n "$old_PATH" ]; do
  315. builtin local x
  316. x=${old_PATH%%:*}
  317. case $PATH: in
  318. *:"$x":*) ;;
  319. *) PATH=$PATH:$x;;
  320. esac
  321. old_PATH=${old_PATH#*:}
  322. done
  323. PATH=${PATH#:}
  324. fi
  325. fi
  326. builtin unset KITTY_IS_CLONE_LAUNCH KITTY_CLONE_SOURCE_STRATEGIES
  327. }
  328. _ksi_main
  329. builtin unset -f _ksi_main
  330. case :$SHELLOPTS: in
  331. *:posix:*) ;;
  332. *)
  333. _ksi_transmit_data() {
  334. builtin local data
  335. data="${1//[[:space:]]}"
  336. builtin local pos=0
  337. builtin local chunk_num=0
  338. while [ $pos -lt ${#data} ]; do
  339. builtin local chunk="${data:$pos:2048}"
  340. pos=$(($pos+2048))
  341. builtin printf '\eP@kitty-%s|%s:%s\e\\' "${2}" "${chunk_num}" "${chunk}"
  342. chunk_num=$(($chunk_num+1))
  343. done
  344. # save history so it is available in new shell
  345. [ "$3" = "save_history" ] && builtin history -a
  346. builtin printf '\eP@kitty-%s|\e\\' "${2}"
  347. }
  348. clone-in-kitty() {
  349. builtin local bv="${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}.${BASH_VERSINFO[2]}"
  350. builtin local data="shell=bash,pid=$$,bash_version=$bv,cwd=$(builtin printf "%s" "$PWD" | builtin command base64),envfmt=bash,env=$(builtin export | builtin command base64)"
  351. while :; do
  352. case "$1" in
  353. "") break;;
  354. -h|--help)
  355. builtin printf "%s\n\n%s\n" "Clone the current bash session into a new kitty window." "For usage instructions see: https://sw.kovidgoyal.net/kitty/shell-integration/#clone-shell"
  356. builtin return
  357. ;;
  358. *) data="$data,a=$(builtin printf "%s" "$1" | builtin command base64)";;
  359. esac
  360. shift
  361. done
  362. _ksi_transmit_data "$data" "clone" "save_history"
  363. }
  364. ;;
  365. esac