# Parts of this script are based on Kitty's bash integration. Kitty is # distributed under GPLv3, so this file is also distributed under GPLv3. # The license header is reproduced below: # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # We need to be in interactive mode to proceed. if [[ "$-" != *i* ]]; then builtin return; fi # When automatic shell integration is active, we were started in POSIX # mode and need to manually recreate the bash startup sequence. if [ -n "$GHOSTTY_BASH_INJECT" ]; then # Store a temporary copy of our startup flags and unset these global # environment variables so we can safely handle reentrancy. builtin declare __ghostty_bash_flags="$GHOSTTY_BASH_INJECT" builtin unset ENV GHOSTTY_BASH_INJECT # Restore an existing ENV that was replaced by the shell integration code. if [[ -n "$GHOSTTY_BASH_ENV" ]]; then builtin export ENV=$GHOSTTY_BASH_ENV builtin unset GHOSTTY_BASH_ENV fi # Restore bash's default 'posix' behavior. Also reset 'inherit_errexit', # which doesn't happen as part of the 'posix' reset. builtin set +o posix builtin shopt -u inherit_errexit 2>/dev/null # Unexport HISTFILE if it was set by the shell integration code. if [[ -n "$GHOSTTY_BASH_UNEXPORT_HISTFILE" ]]; then builtin export -n HISTFILE builtin unset GHOSTTY_BASH_UNEXPORT_HISTFILE fi # Manually source the startup files. See INVOCATION in bash(1) and # run_startup_files() in shell.c in the Bash source code. if builtin shopt -q login_shell; then if [[ $__ghostty_bash_flags != *"--noprofile"* ]]; then [ -r /etc/profile ] && builtin source "/etc/profile" for __ghostty_rcfile in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do [ -r "$__ghostty_rcfile" ] && { builtin source "$__ghostty_rcfile" break } done fi else if [[ $__ghostty_bash_flags != *"--norc"* ]]; then # The location of the system bashrc is determined at bash build # time via -DSYS_BASHRC and can therefore vary across distros: # Arch, Debian, Ubuntu use /etc/bash.bashrc # Fedora uses /etc/bashrc sourced from ~/.bashrc instead of SYS_BASHRC # Void Linux uses /etc/bash/bashrc # Nixos uses /etc/bashrc for __ghostty_rcfile in /etc/bash.bashrc /etc/bash/bashrc /etc/bashrc; do [ -r "$__ghostty_rcfile" ] && { builtin source "$__ghostty_rcfile" break } done if [[ -z "$GHOSTTY_BASH_RCFILE" ]]; then GHOSTTY_BASH_RCFILE="$HOME/.bashrc"; fi [ -r "$GHOSTTY_BASH_RCFILE" ] && builtin source "$GHOSTTY_BASH_RCFILE" fi fi builtin unset __ghostty_rcfile builtin unset __ghostty_bash_flags builtin unset GHOSTTY_BASH_RCFILE fi # Add Ghostty binary to PATH if the path feature is enabled if [[ "$GHOSTTY_SHELL_FEATURES" == *"path"* && -n "$GHOSTTY_BIN_DIR" ]]; then if [[ ":$PATH:" != *":$GHOSTTY_BIN_DIR:"* ]]; then export PATH="$PATH:$GHOSTTY_BIN_DIR" fi fi # Sudo if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then # Wrap `sudo` command to ensure Ghostty terminfo is preserved. # # This approach supports wrapping a `sudo` alias, but the alias definition # must come _after_ this function is defined. Otherwise, the alias expansion # will take precedence over this function, and it won't be wrapped. function sudo { builtin local sudo_has_sudoedit_flags="no" for arg in "$@"; do # Check if argument is '-e' or '--edit' (sudoedit flags) if [[ "$arg" == "-e" || $arg == "--edit" ]]; then sudo_has_sudoedit_flags="yes" builtin break fi # Check if argument is neither an option nor a key-value pair if [[ "$arg" != -* && "$arg" != *=* ]]; then builtin break fi done if [[ "$sudo_has_sudoedit_flags" == "yes" ]]; then builtin command sudo "$@" else builtin command sudo --preserve-env=TERMINFO "$@" fi } fi # SSH Integration if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then function ssh() { builtin local ssh_term ssh_opts ssh_term="xterm-256color" ssh_opts=() # Configure environment variables for remote session if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]]; then ssh_opts+=(-o "SetEnv COLORTERM=truecolor") ssh_opts+=(-o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION") fi # Install terminfo on remote host if needed if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-terminfo* ]]; then builtin local ssh_user ssh_hostname while IFS=' ' read -r ssh_key ssh_value; do case "$ssh_key" in user) ssh_user="$ssh_value" ;; hostname) ssh_hostname="$ssh_value" ;; esac [[ -n "$ssh_user" && -n "$ssh_hostname" ]] && break done < <(builtin command ssh -G "$@" 2>/dev/null) if [[ -n "$ssh_hostname" ]]; then builtin local ssh_target="${ssh_user}@${ssh_hostname}" # Check if terminfo is already cached if "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --host="$ssh_target" >/dev/null 2>&1; then ssh_term="xterm-ghostty" elif builtin command -v infocmp >/dev/null 2>&1; then builtin local ssh_terminfo ssh_cpath_dir ssh_cpath ssh_terminfo=$(infocmp -0 -x xterm-ghostty 2>/dev/null) if [[ -n "$ssh_terminfo" ]]; then builtin echo "Setting up xterm-ghostty terminfo on $ssh_hostname..." >&2 ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$" ssh_cpath="$ssh_cpath_dir/socket" if builtin echo "$ssh_terminfo" | builtin command ssh -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" ' infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 command -v tic >/dev/null 2>&1 || exit 1 mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 exit 1 ' 2>/dev/null; then ssh_term="xterm-ghostty" ssh_opts+=(-o "ControlPath=$ssh_cpath") # Cache successful installation "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true else builtin echo "Warning: Failed to install terminfo." >&2 fi else builtin echo "Warning: Could not generate terminfo data." >&2 fi else builtin echo "Warning: ghostty command not available for cache management." >&2 fi fi fi # Execute SSH with TERM environment variable TERM="$ssh_term" builtin command ssh "${ssh_opts[@]}" "$@" } fi # This is set to 1 when we're executing a command so that we don't # send prompt marks multiple times. _ghostty_executing="" _ghostty_last_reported_cwd="" function __ghostty_precmd() { local ret="$?" if test "$_ghostty_executing" != "0"; then _GHOSTTY_SAVE_PS1="$PS1" _GHOSTTY_SAVE_PS2="$PS2" # Marks. We need to do fresh line (A) at the beginning of the prompt # since if the cursor is not at the beginning of a line, the terminal # will emit a newline. PS1='\[\e]133;A;redraw=last;cl=line\a\]'$PS1'\[\e]133;B\a\]' PS2='\[\e]133;A;k=s\a\]'$PS2'\[\e]133;B\a\]' # Bash doesn't redraw the leading lines in a multiline prompt so # we mark the start of each line (after each newline) as a secondary # prompt. This correctly handles multiline prompts by setting the first # to primary and the subsequent lines to secondary. if [[ "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ]]; then builtin local __ghostty_mark=$'\\[\\e]133;A;k=s\\a\\]' PS1="${PS1//$'\n'/$'\n'$__ghostty_mark}" PS1="${PS1//\\n/\\n$__ghostty_mark}" fi # Cursor if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then [[ "$PS1" != *'\[\e[5 q\]'* ]] && PS1=$PS1'\[\e[5 q\]' # input [[ "$PS0" != *'\[\e[0 q\]'* ]] && PS0=$PS0'\[\e[0 q\]' # reset fi # Title (working directory) if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then PS1=$PS1'\[\e]2;\w\a\]' fi fi if test "$_ghostty_executing" != ""; then # End of current command. Report its status. builtin printf "\e]133;D;%s;aid=%s\a" "$ret" "$BASHPID" fi # unfortunately bash provides no hooks to detect cwd changes # in particular this means cwd reporting will not happen for a # command like cd /test && cat. PS0 is evaluated before cd is run. if [[ "$_ghostty_last_reported_cwd" != "$PWD" ]]; then _ghostty_last_reported_cwd="$PWD" builtin printf "\e]7;kitty-shell-cwd://%s%s\a" "$HOSTNAME" "$PWD" fi # Fresh line and start of prompt. builtin printf "\e]133;A;redraw=last;cl=line;aid=%s\a" "$BASHPID" _ghostty_executing=0 } function __ghostty_preexec() { builtin local cmd="$1" PS1="$_GHOSTTY_SAVE_PS1" PS2="$_GHOSTTY_SAVE_PS2" # Title (current command) if [[ -n $cmd && "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then builtin printf "\e]2;%s\a" "${cmd//[[:cntrl:]]/}" fi # End of input, start of output. builtin printf "\e]133;C;\a" _ghostty_executing=1 } if (( BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 4) )); then __ghostty_preexec_hook() { builtin local cmd cmd=$(LC_ALL=C HISTTIMEFORMAT='' builtin history 1) cmd="${cmd#*[[:digit:]][* ] }" # remove leading history number [[ -n "$cmd" ]] && __ghostty_preexec "$cmd" } # Use function substitution in 5.3+. Otherwise, use command substitution. # Any output (including escape sequences) goes to the terminal. if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 3) )); then # shellcheck disable=SC2016 builtin readonly __ghostty_ps0='${ __ghostty_preexec_hook; }' else # shellcheck disable=SC2016 builtin readonly __ghostty_ps0='$(__ghostty_preexec_hook >/dev/tty)' fi __ghostty_hook() { builtin local ret=$? __ghostty_precmd "$ret" if [[ "$PS0" != *"$__ghostty_ps0"* ]]; then PS0=$PS0"${__ghostty_ps0}" fi } # Append our hook to PROMPT_COMMAND, preserving its existing type. if [[ ";${PROMPT_COMMAND[*]:-};" != *";__ghostty_hook;"* ]]; then if [[ -z "${PROMPT_COMMAND[*]}" ]]; then if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1) )); then PROMPT_COMMAND=(__ghostty_hook) else # shellcheck disable=SC2178 PROMPT_COMMAND="__ghostty_hook" fi elif [[ $(builtin declare -p PROMPT_COMMAND 2>/dev/null) == "declare -a "* ]]; then PROMPT_COMMAND+=(__ghostty_hook) else # shellcheck disable=SC2179 PROMPT_COMMAND+="; __ghostty_hook" fi fi else builtin source "$(dirname -- "${BASH_SOURCE[0]}")/bash-preexec.sh" preexec_functions+=(__ghostty_preexec) precmd_functions+=(__ghostty_precmd) fi