diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/shell-integration | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/shell-integration')
8 files changed, 1883 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/shell-integration/README.md b/vendor/libghostty-vt/src/shell-integration/README.md new file mode 100644 index 00000000..590632da --- /dev/null +++ b/vendor/libghostty-vt/src/shell-integration/README.md @@ -0,0 +1,117 @@ +# Shell Integration Code + +This is the shell-specific shell-integration code that is +used for the shell-integration feature set that Ghostty +supports. + +This README is meant as developer documentation and not as +user documentation. For user documentation, see the main +README or [ghostty.org](https://ghostty.org/docs) + +## Implementation Details + +### Bash + +Automatic [Bash](https://www.gnu.org/software/bash/) shell integration works by +starting Bash in POSIX mode and using the `ENV` environment variable to load +our integration script (`bash/ghostty.bash`). This prevents Bash from loading +its normal startup files, which becomes our script's responsibility (along with +disabling POSIX mode). + +Bash shell integration can also be sourced manually from `bash/ghostty.bash`. +This also works for older versions of Bash. + +```bash +# Ghostty shell integration for Bash. This must be at the top of your bashrc! +if [ -n "${GHOSTTY_RESOURCES_DIR}" ]; then + builtin source "${GHOSTTY_RESOURCES_DIR}/shell-integration/bash/ghostty.bash" +fi +``` + +> [!NOTE] +> +> The version of Bash distributed with macOS (`/bin/bash`) does not support +> automatic shell integration. You'll need to manually source the shell +> integration script (as shown above). You can also install a standard +> version of Bash from Homebrew or elsewhere and set it as your shell. + +### Elvish + +For [Elvish](https://elv.sh), `$GHOSTTY_RESOURCES_DIR/src/shell-integration` +contains an `./elvish/lib/ghostty-integration.elv` file. + +Elvish, on startup, searches for paths defined in `XDG_DATA_DIRS` +variable for `./elvish/lib/*.elv` files and imports them. They are thus +made available for use as modules by way of `use <filename>`. + +Ghostty launches Elvish, passing the environment with `XDG_DATA_DIRS` prepended +with `$GHOSTTY_RESOURCES_DIR/src/shell-integration`. It contains +`./elvish/lib/ghostty-integration.elv`. The user can then import it +by `use ghostty-integration` every time after shell startup or +autostart integration in `$XDG_CONFIG_HOME/elvish/rc.elv`, +which will run the integration routines. + +If you decide to autostart `ghostty-integration` with `rc.elv`, you should +detect whether the terminal is Ghostty or not. To do this, add this to the end +of your `rc.elv` file: + +```elvish +if (eq $E:TERM "xterm-ghostty") { + try { use ghostty-integration } catch { } +} +``` + +The [Elvish](https://elv.sh) shell integration is supported by +the community and is not officially supported by Ghostty. We distribute +it for ease of access and use but do not provide support for it. +If you experience issues with the Elvish shell integration, I welcome +any contributions to fix them. Thank you! + +### Fish + +For [Fish](https://fishshell.com/), Ghostty prepends to the +`XDG_DATA_DIRS` directory. Fish automatically loads configuration +files in `<XDG_DATA_DIR>/fish/vendor_conf.d/*.fish` on startup, +allowing us to automatically integrate with the shell. For details +on the Fish startup process, see the +[Fish documentation](https://fishshell.com/docs/current/language.html). + +### Nushell + +For [Nushell](https://www.nushell.sh/), Ghostty prepends to the +`XDG_DATA_DIRS` directory, making the `ghostty` module available through +Nushell's vendor autoload mechanism. Ghostty then automatically imports +the module using the `-e "use ghostty *"` flag when starting Nushell. + +Nushell provides many shell features itself, such as `title` and `cursor`, +so our integration focuses on Ghostty-specific features like `sudo`, +`ssh-env`, and `ssh-terminfo`. + +The shell integration is automatically enabled when running Nushell in Ghostty, +but you can also load it manually is shell integration is disabled: + +```nushell +source $GHOSTTY_RESOURCES_DIR/shell-integration/nushell/vendor/autoload/ghostty.nu +use ghostty * +``` + +### Zsh + +Automatic [Zsh](https://www.zsh.org/) integration works by temporarily setting +`ZDOTDIR` to our `zsh` directory. An existing `ZDOTDIR` environment variable +value will be retained and restored after our shell integration scripts are +run. + +However, if `ZDOTDIR` is set in a system-wide file like `/etc/zshenv`, it will +override Ghostty's `ZDOTDIR` value, preventing the shell integration from being +loaded. In this case, the shell integration needs to be loaded manually. + +To load the Zsh shell integration manually: + +```zsh +if [[ -n $GHOSTTY_RESOURCES_DIR ]]; then + source "$GHOSTTY_RESOURCES_DIR"/shell-integration/zsh/ghostty-integration +fi +``` + +Shell integration requires Zsh 5.1+. diff --git a/vendor/libghostty-vt/src/shell-integration/bash/bash-preexec.sh b/vendor/libghostty-vt/src/shell-integration/bash/bash-preexec.sh new file mode 100644 index 00000000..f20ece17 --- /dev/null +++ b/vendor/libghostty-vt/src/shell-integration/bash/bash-preexec.sh @@ -0,0 +1,382 @@ +# bash-preexec.sh -- Bash support for ZSH-like 'preexec' and 'precmd' functions. +# https://github.com/rcaloras/bash-preexec +# +# +# 'preexec' functions are executed before each interactive command is +# executed, with the interactive command as its argument. The 'precmd' +# function is executed before each prompt is displayed. +# +# Author: Ryan Caloras (ryan@bashhub.com) +# Forked from Original Author: Glyph Lefkowitz +# +# V0.6.0 +# + +# General Usage: +# +# 1. Source this file at the end of your bash profile so as not to interfere +# with anything else that's using PROMPT_COMMAND. +# +# 2. Add any precmd or preexec functions by appending them to their arrays: +# e.g. +# precmd_functions+=(my_precmd_function) +# precmd_functions+=(some_other_precmd_function) +# +# preexec_functions+=(my_preexec_function) +# +# 3. Consider changing anything using the DEBUG trap or PROMPT_COMMAND +# to use preexec and precmd instead. Preexisting usages will be +# preserved, but doing so manually may be less surprising. +# +# Note: This module requires two Bash features which you must not otherwise be +# using: the "DEBUG" trap, and the "PROMPT_COMMAND" variable. If you override +# either of these after bash-preexec has been installed it will most likely break. + +# Tell shellcheck what kind of file this is. +# shellcheck shell=bash + +# Make sure this is bash that's running and return otherwise. +# Use POSIX syntax for this line: +if [ -z "${BASH_VERSION-}" ]; then + return 1 +fi + +# We only support Bash 3.1+. +# Note: BASH_VERSINFO is first available in Bash-2.0. +if [[ -z "${BASH_VERSINFO-}" ]] || (( BASH_VERSINFO[0] < 3 || (BASH_VERSINFO[0] == 3 && BASH_VERSINFO[1] < 1) )); then + return 1 +fi + +# Avoid duplicate inclusion +if [[ -n "${bash_preexec_imported:-}" || -n "${__bp_imported:-}" ]]; then + return 0 +fi +bash_preexec_imported="defined" + +# WARNING: This variable is no longer used and should not be relied upon. +# Use ${bash_preexec_imported} instead. +# shellcheck disable=SC2034 +__bp_imported="${bash_preexec_imported}" + +# Should be available to each precmd and preexec +# functions, should they want it. $? and $_ are available as $? and $_, but +# $PIPESTATUS is available only in a copy, $BP_PIPESTATUS. +# TODO: Figure out how to restore PIPESTATUS before each precmd or preexec +# function. +__bp_last_ret_value="$?" +BP_PIPESTATUS=("${PIPESTATUS[@]}") +__bp_last_argument_prev_command="$_" + +__bp_inside_precmd=0 +__bp_inside_preexec=0 + +# Initial PROMPT_COMMAND string that is removed from PROMPT_COMMAND post __bp_install +__bp_install_string=$'__bp_trap_string="$(trap -p DEBUG)"\ntrap - DEBUG\n__bp_install' + +# Fails if any of the given variables are readonly +# Reference https://stackoverflow.com/a/4441178 +__bp_require_not_readonly() { + local var + for var; do + if ! ( unset "$var" 2> /dev/null ); then + echo "bash-preexec requires write access to ${var}" >&2 + return 1 + fi + done +} + +# Remove ignorespace and or replace ignoreboth from HISTCONTROL +# so we can accurately invoke preexec with a command from our +# history even if it starts with a space. +__bp_adjust_histcontrol() { + local histcontrol + histcontrol="${HISTCONTROL:-}" + histcontrol="${histcontrol//ignorespace}" + # Replace ignoreboth with ignoredups + if [[ "$histcontrol" == *"ignoreboth"* ]]; then + histcontrol="ignoredups:${histcontrol//ignoreboth}" + fi + export HISTCONTROL="$histcontrol" +} + +# This variable describes whether we are currently in "interactive mode"; +# i.e. whether this shell has just executed a prompt and is waiting for user +# input. It documents whether the current command invoked by the trace hook is +# run interactively by the user; it's set immediately after the prompt hook, +# and unset as soon as the trace hook is run. +__bp_preexec_interactive_mode="" + +# These arrays are used to add functions to be run before, or after, prompts. +declare -a precmd_functions +declare -a preexec_functions + +# Trims leading and trailing whitespace from $2 and writes it to the variable +# name passed as $1 +__bp_trim_whitespace() { + local var=${1:?} text=${2:-} + text="${text#"${text%%[![:space:]]*}"}" # remove leading whitespace characters + text="${text%"${text##*[![:space:]]}"}" # remove trailing whitespace characters + printf -v "$var" '%s' "$text" +} + + +# Trims whitespace and removes any leading or trailing semicolons from $2 and +# writes the resulting string to the variable name passed as $1. Used for +# manipulating substrings in PROMPT_COMMAND +__bp_sanitize_string() { + local var=${1:?} text=${2:-} sanitized + __bp_trim_whitespace sanitized "$text" + sanitized=${sanitized%;} + sanitized=${sanitized#;} + __bp_trim_whitespace sanitized "$sanitized" + printf -v "$var" '%s' "$sanitized" +} + +# This function is installed as part of the PROMPT_COMMAND; +# It sets a variable to indicate that the prompt was just displayed, +# to allow the DEBUG trap to know that the next command is likely interactive. +__bp_interactive_mode() { + __bp_preexec_interactive_mode="on" +} + + +# This function is installed as part of the PROMPT_COMMAND. +# It will invoke any functions defined in the precmd_functions array. +__bp_precmd_invoke_cmd() { + # Save the returned value from our last command, and from each process in + # its pipeline. Note: this MUST be the first thing done in this function. + # BP_PIPESTATUS may be unused, ignore + # shellcheck disable=SC2034 + + __bp_last_ret_value="$?" BP_PIPESTATUS=("${PIPESTATUS[@]}") + + # Don't invoke precmds if we are inside an execution of an "original + # prompt command" by another precmd execution loop. This avoids infinite + # recursion. + if (( __bp_inside_precmd > 0 )); then + return + fi + local __bp_inside_precmd=1 + + # Invoke every function defined in our function array. + local precmd_function + for precmd_function in "${precmd_functions[@]}"; do + + # Only execute this function if it actually exists. + # Test existence of functions with: declare -[Ff] + if type -t "$precmd_function" 1>/dev/null; then + __bp_set_ret_value "$__bp_last_ret_value" "$__bp_last_argument_prev_command" + # Quote our function invocation to prevent issues with IFS + "$precmd_function" + fi + done + + __bp_set_ret_value "$__bp_last_ret_value" +} + +# Sets a return value in $?. We may want to get access to the $? variable in our +# precmd functions. This is available for instance in zsh. We can simulate it in bash +# by setting the value here. +__bp_set_ret_value() { + return ${1:+"$1"} +} + +__bp_in_prompt_command() { + + local prompt_command_array IFS=$'\n;' + read -rd '' -a prompt_command_array <<< "${PROMPT_COMMAND[*]:-}" + + local trimmed_arg + __bp_trim_whitespace trimmed_arg "${1:-}" + + local command trimmed_command + for command in "${prompt_command_array[@]:-}"; do + __bp_trim_whitespace trimmed_command "$command" + if [[ "$trimmed_command" == "$trimmed_arg" ]]; then + return 0 + fi + done + + return 1 +} + +# This function is installed as the DEBUG trap. It is invoked before each +# interactive prompt display. Its purpose is to inspect the current +# environment to attempt to detect if the current command is being invoked +# interactively, and invoke 'preexec' if so. +__bp_preexec_invoke_exec() { + + # Save the contents of $_ so that it can be restored later on. + # https://stackoverflow.com/questions/40944532/bash-preserve-in-a-debug-trap#40944702 + __bp_last_argument_prev_command="${1:-}" + # Don't invoke preexecs if we are inside of another preexec. + if (( __bp_inside_preexec > 0 )); then + return + fi + local __bp_inside_preexec=1 + + # Checks if the file descriptor is not standard out (i.e. '1') + # __bp_delay_install checks if we're in test. Needed for bats to run. + # Prevents preexec from being invoked for functions in PS1 + if [[ ! -t 1 && -z "${__bp_delay_install:-}" ]]; then + return + fi + + if [[ -n "${COMP_POINT:-}" || -n "${READLINE_POINT:-}" ]]; then + # We're in the middle of a completer or a keybinding set up by "bind + # -x". This obviously can't be an interactively issued command. + return + fi + if [[ -z "${__bp_preexec_interactive_mode:-}" ]]; then + # We're doing something related to displaying the prompt. Let the + # prompt set the title instead of me. + return + else + # If we're in a subshell, then the prompt won't be re-displayed to put + # us back into interactive mode, so let's not set the variable back. + # In other words, if you have a subshell like + # (sleep 1; sleep 2) + # You want to see the 'sleep 2' as a set_command_title as well. + if [[ 0 -eq "${BASH_SUBSHELL:-}" ]]; then + __bp_preexec_interactive_mode="" + fi + fi + + if __bp_in_prompt_command "${BASH_COMMAND:-}"; then + # If we're executing something inside our prompt_command then we don't + # want to call preexec. Bash prior to 3.1 can't detect this at all :/ + __bp_preexec_interactive_mode="" + return + fi + + local this_command + this_command=$(LC_ALL=C HISTTIMEFORMAT='' builtin history 1) + this_command="${this_command#*[[:digit:]][* ] }" + + # Sanity check to make sure we have something to invoke our function with. + if [[ -z "$this_command" ]]; then + return + fi + + # Invoke every function defined in our function array. + local preexec_function + local preexec_function_ret_value + local preexec_ret_value=0 + for preexec_function in "${preexec_functions[@]:-}"; do + + # Only execute each function if it actually exists. + # Test existence of function with: declare -[fF] + if type -t "$preexec_function" 1>/dev/null; then + __bp_set_ret_value "${__bp_last_ret_value:-}" + # Quote our function invocation to prevent issues with IFS + "$preexec_function" "$this_command" + preexec_function_ret_value="$?" + if [[ "$preexec_function_ret_value" != 0 ]]; then + preexec_ret_value="$preexec_function_ret_value" + fi + fi + done + + # Restore the last argument of the last executed command, and set the return + # value of the DEBUG trap to be the return code of the last preexec function + # to return an error. + # If `extdebug` is enabled a non-zero return value from any preexec function + # will cause the user's command not to execute. + # Run `shopt -s extdebug` to enable + __bp_set_ret_value "$preexec_ret_value" "$__bp_last_argument_prev_command" +} + +__bp_install() { + # Exit if we already have this installed. + if [[ "${PROMPT_COMMAND[*]:-}" == *"__bp_precmd_invoke_cmd"* ]]; then + return 1 + fi + + trap '__bp_preexec_invoke_exec "$_"' DEBUG + + # Preserve any prior DEBUG trap as a preexec function + eval "local trap_argv=(${__bp_trap_string:-})" + local prior_trap=${trap_argv[2]:-} + unset __bp_trap_string + if [[ -n "$prior_trap" ]]; then + eval '__bp_original_debug_trap() { + '"$prior_trap"' + }' + preexec_functions+=(__bp_original_debug_trap) + fi + + # Adjust our HISTCONTROL Variable if needed. + # + # GHOSTTY: Don't modify HISTCONTROL. This hack is only needed to improve the + # accuracy of the command argument passed to the preexec functions, and we + # don't use that argument in our bash shell integration script (and nor does + # the __bp_original_debug_trap function above, which is the only other active + # preexec function). + #__bp_adjust_histcontrol + + # Issue #25. Setting debug trap for subshells causes sessions to exit for + # backgrounded subshell commands (e.g. (pwd)& ). Believe this is a bug in Bash. + # + # Disabling this by default. It can be enabled by setting this variable. + if [[ -n "${__bp_enable_subshells:-}" ]]; then + + # Set so debug trap will work be invoked in subshells. + set -o functrace > /dev/null 2>&1 + shopt -s extdebug > /dev/null 2>&1 + fi + + local existing_prompt_command + # Remove setting our trap install string and sanitize the existing prompt command string + existing_prompt_command="${PROMPT_COMMAND:-}" + # Edge case of appending to PROMPT_COMMAND + existing_prompt_command="${existing_prompt_command//$__bp_install_string/:}" # no-op + existing_prompt_command="${existing_prompt_command//$'\n':$'\n'/$'\n'}" # remove known-token only + existing_prompt_command="${existing_prompt_command//$'\n':;/$'\n'}" # remove known-token only + __bp_sanitize_string existing_prompt_command "$existing_prompt_command" + if [[ "${existing_prompt_command:-:}" == ":" ]]; then + existing_prompt_command= + fi + + # Install our hooks in PROMPT_COMMAND to allow our trap to know when we've + # actually entered something. + PROMPT_COMMAND='__bp_precmd_invoke_cmd' + PROMPT_COMMAND+=${existing_prompt_command:+$'\n'$existing_prompt_command} + if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1) )); then + PROMPT_COMMAND+=('__bp_interactive_mode') + else + # shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0 + PROMPT_COMMAND+=$'\n__bp_interactive_mode' + fi + + # Add two functions to our arrays for convenience + # of definition. + precmd_functions+=(precmd) + preexec_functions+=(preexec) + + # Invoke our two functions manually that were added to $PROMPT_COMMAND + __bp_precmd_invoke_cmd + __bp_interactive_mode +} + +# Sets an installation string as part of our PROMPT_COMMAND to install +# after our session has started. This allows bash-preexec to be included +# at any point in our bash profile. +__bp_install_after_session_init() { + # bash-preexec needs to modify these variables in order to work correctly + # if it can't, just stop the installation + __bp_require_not_readonly PROMPT_COMMAND HISTCONTROL HISTTIMEFORMAT || return + + local sanitized_prompt_command + __bp_sanitize_string sanitized_prompt_command "${PROMPT_COMMAND:-}" + if [[ -n "$sanitized_prompt_command" ]]; then + # shellcheck disable=SC2178 # PROMPT_COMMAND is not an array in bash <= 5.0 + PROMPT_COMMAND=${sanitized_prompt_command}$'\n' + fi + # shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0 + PROMPT_COMMAND+=${__bp_install_string} +} + +# Run our install so long as we're not delaying it. +if [[ -z "${__bp_delay_install:-}" ]]; then + __bp_install_after_session_init +fi diff --git a/vendor/libghostty-vt/src/shell-integration/bash/ghostty.bash b/vendor/libghostty-vt/src/shell-integration/bash/ghostty.bash new file mode 100644 index 00000000..7eaf1397 --- /dev/null +++ b/vendor/libghostty-vt/src/shell-integration/bash/ghostty.bash @@ -0,0 +1,324 @@ +# 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 <http://www.gnu.org/licenses/>. + +# 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 "SendEnv COLORTERM 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" COLORTERM=truecolor 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" + + # Use 133;P (not 133;A) inside PS1 to avoid fresh-line behavior on + # readline redraws (e.g., vi mode switches, Ctrl-L). The initial + # 133;A with fresh-line is emitted once via printf below. + PS1='\[\e]133;P;k=i\a\]'$PS1'\[\e]133;B\a\]' + PS2='\[\e]133;P;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. + # + # We only replace the \n prompt escape, not literal newlines ($'\n'), + # because literal newlines may appear inside $(...) command substitutions + # where inserting escape sequences would break shell syntax. + if [[ "$PS1" == *"\n"* ]]; then + PS1="${PS1//\\n/\\n$'\\[\\e]133;P;k=s\\a\\]'}" + fi + + # Cursor + if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then + builtin local cursor=5 # blinking bar + [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor:steady"* ]] && cursor=6 # steady bar + + [[ "$PS1" != *"\[\e[${cursor} q\]"* ]] && PS1=$PS1"\[\e[${cursor} q\]" + [[ "$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 + + # Fresh line and start of prompt. When ble.sh is active, emit 133;P instead + # of 133;A because ble.sh maintains its own cursor position tracking. 133;A's + # cursor movement (CR+LF when not at column 0) is invisible to ble.sh and + # desyncs its position state, causing display artifacts like duplicate + # prompts. See: https://github.com/akinomyoga/ble.sh/issues/684 + if [[ -n "${BLE_VERSION-}" ]]; then + builtin printf "\e]133;P;k=i\a" + else + builtin printf "\e]133;A;redraw=last;cl=line;aid=%s\a" "$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 + + _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" + } + + __ghostty_hook() { + builtin local ret=$? + __ghostty_precmd "$ret" + + # Append preexec hook to PS0 if not already present. + # Use function substitution in 5.3+, otherwise command substitution. + if [[ "$PS0" != *"__ghostty_preexec_hook"* ]]; then + if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 3) )); then + # shellcheck disable=SC2016 + PS0+='${ __ghostty_preexec_hook; }' + else + # shellcheck disable=SC2016 + PS0+='$(__ghostty_preexec_hook >/dev/tty)' + fi + fi + } + + # Append our hook to PROMPT_COMMAND, preserving its existing type. + # + # The 2>/dev/null suppresses "command not found" in subshells that inherit + # PROMPT_COMMAND without the function definition. This also silences any + # errors from inside __ghostty_hook itself, but those are all terminal escape + # sequences and non-actionable. + # + # shellcheck disable=SC2128,SC2178,SC2179 + if [[ ";${PROMPT_COMMAND[*]:-};" != *";__ghostty_hook 2>/dev/null;"* ]]; then + if [[ -z "${PROMPT_COMMAND[*]}" ]]; then + if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1) )); then + PROMPT_COMMAND=("__ghostty_hook 2>/dev/null") + else + PROMPT_COMMAND="__ghostty_hook 2>/dev/null" + fi + elif [[ $(builtin declare -p PROMPT_COMMAND 2>/dev/null) == "declare -a "* ]]; then + PROMPT_COMMAND+=("__ghostty_hook 2>/dev/null") + else + [[ "${PROMPT_COMMAND}" =~ (\;[[:space:]]*|$'\n')$ ]] || PROMPT_COMMAND+=";" + PROMPT_COMMAND+="__ghostty_hook 2>/dev/null" + fi + fi +else + builtin source "$(dirname -- "${BASH_SOURCE[0]}")/bash-preexec.sh" + preexec_functions+=(__ghostty_preexec) + precmd_functions+=(__ghostty_precmd) +fi diff --git a/vendor/libghostty-vt/src/shell-integration/elvish/lib/ghostty-integration.elv b/vendor/libghostty-vt/src/shell-integration/elvish/lib/ghostty-integration.elv new file mode 100644 index 00000000..31ebf094 --- /dev/null +++ b/vendor/libghostty-vt/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -0,0 +1,190 @@ +{ + use platform + use str + + # Clean up XDG_DATA_DIRS by removing GHOSTTY_SHELL_INTEGRATION_XDG_DIR + if (and (has-env GHOSTTY_SHELL_INTEGRATION_XDG_DIR) (has-env XDG_DATA_DIRS)) { + set-env XDG_DATA_DIRS (str:replace $E:GHOSTTY_SHELL_INTEGRATION_XDG_DIR":" "" $E:XDG_DATA_DIRS) + unset-env GHOSTTY_SHELL_INTEGRATION_XDG_DIR + } + + # List of enabled shell integration features + var features = [(str:split ',' $E:GHOSTTY_SHELL_FEATURES)] + + # State tracking for semantic prompt sequences + # Values: 'prompt-start', 'pre-exec', 'post-exec' + fn set-prompt-state {|new| set-env __ghostty_prompt_state $new } + + fn mark-prompt-start { + if (not-eq $E:__ghostty_prompt_state 'prompt-start') { + printf "\e]133;D;aid="$pid"\a" + } + set-prompt-state 'prompt-start' + printf "\e]133;A;aid="$pid"\a" + } + + fn mark-output-start {|_| + set-prompt-state 'pre-exec' + printf "\e]133;C\a" + } + + fn mark-output-end {|cmd-info| + set-prompt-state 'post-exec' + + var exit-status = 0 + + # in case of error: retrieve exit status, + # unless does not exist (= builtin function failure), then default to 1 + if (not-eq $nil $cmd-info[error]) { + set exit-status = 1 + + if (has-key $cmd-info[error] reason) { + if (has-key $cmd-info[error][reason] exit-status) { + set exit-status = $cmd-info[error][reason][exit-status] + } + } + } + + printf "\e]133;D;"$exit-status";aid="$pid"\a" + } + + # NOTE: OSC 133;B (end of prompt, start of input) cannot be reliably + # implemented at the script level in Elvish. The prompt function's output is + # escaped, and writing to /dev/tty has timing issues because Elvish renders + # its prompts on a background thread. Full semantic prompt support requires a + # native implementation: https://github.com/elves/elvish/pull/1917 + + fn sudo-with-terminfo {|@args| + var sudoedit = $false + for arg $args { + if (str:has-prefix $arg --) { + if (eq $arg --edit) { + set sudoedit = $true + break + } + } elif (str:has-prefix $arg -) { + if (str:contains (str:trim-prefix $arg -) e) { + set sudoedit = $true + break + } + } elif (not (str:contains $arg =)) { + break + } + } + + if (not $sudoedit) { set args = [ --preserve-env=TERMINFO $@args ] } + (external sudo) $@args + } + + fn ssh-integration {|@args| + var ssh-term = "xterm-256color" + var ssh-opts = [] + + # Configure environment variables for remote session + if (has-value $features ssh-env) { + set ssh-opts = (conj $ssh-opts ^ + -o "SendEnv COLORTERM TERM_PROGRAM TERM_PROGRAM_VERSION") + } + + if (has-value $features ssh-terminfo) { + var ssh-user = "" + var ssh-hostname = "" + + # Parse ssh config + for line [((external ssh) -G $@args)] { + var parts = [(str:fields $line)] + if (> (count $parts) 1) { + var ssh-key = $parts[0] + var ssh-value = $parts[1] + if (eq $ssh-key user) { + set ssh-user = $ssh-value + } elif (eq $ssh-key hostname) { + set ssh-hostname = $ssh-value + } + if (and (not-eq $ssh-user "") (not-eq $ssh-hostname "")) { + break + } + } + } + + if (not-eq $ssh-hostname "") { + var ghostty = $E:GHOSTTY_BIN_DIR/"ghostty" + var ssh-target = $ssh-user"@"$ssh-hostname + + # Check if terminfo is already cached + if (bool ?($ghostty +ssh-cache --host=$ssh-target)) { + set ssh-term = "xterm-ghostty" + } elif (has-external infocmp) { + var ssh-terminfo = ((external infocmp) -0 -x xterm-ghostty 2>/dev/null | slurp) + + if (not-eq $ssh-terminfo "") { + echo "Setting up xterm-ghostty terminfo on "$ssh-hostname"..." >&2 + + use os + var ssh-cpath-dir = (os:temp-dir "ghostty-ssh-"$ssh-user".*") + var ssh-cpath = $ssh-cpath-dir"/socket" + + if (bool ?(echo $ssh-terminfo | (external ssh) $@ssh-opts -o ControlMaster=yes -o ControlPath=$ssh-cpath -o ControlPersist=60s $@args ' + 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)) { + set ssh-term = "xterm-ghostty" + set ssh-opts = (conj $ssh-opts -o ControlPath=$ssh-cpath) + + # Cache successful installation + $ghostty +ssh-cache --add=$ssh-target >/dev/null + } else { + echo "Warning: Failed to install terminfo." >&2 + } + } else { + echo "Warning: Could not generate terminfo data." >&2 + } + } else { + echo "Warning: ghostty command not available for cache management." >&2 + } + } + } + + with [E:TERM = $ssh-term E:COLORTERM = truecolor] { + (external ssh) $@ssh-opts $@args + } + } + + defer { + mark-prompt-start + } + + set edit:before-readline = (conj $edit:before-readline $mark-prompt-start~) + set edit:after-readline = (conj $edit:after-readline $mark-output-start~) + set edit:after-command = (conj $edit:after-command $mark-output-end~) + + if (str:contains $E:GHOSTTY_SHELL_FEATURES "cursor") { + var cursor = "5" # blinking bar + if (has-value $features cursor:steady) { + set cursor = "6" # steady bar + } + + fn beam { printf "\e["$cursor" q" } + fn reset { printf "\e[0 q" } + set edit:before-readline = (conj $edit:before-readline $beam~) + set edit:after-readline = (conj $edit:after-readline {|_| reset }) + } + if (and (has-value $features path) (has-env GHOSTTY_BIN_DIR)) { + if (not (has-value $paths $E:GHOSTTY_BIN_DIR)) { + set paths = [$@paths $E:GHOSTTY_BIN_DIR] + } + } + if (and (has-value $features sudo) (not-eq "" $E:TERMINFO) (has-external sudo)) { + edit:add-var sudo~ $sudo-with-terminfo~ + } + if (and (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-) (has-external ssh)) { + edit:add-var ssh~ $ssh-integration~ + } + + # Report changes to the current directory. + fn report-pwd { printf "\e]7;kitty-shell-cwd://%s%s\a" (platform:hostname) $pwd } + set after-chdir = (conj $after-chdir {|_| report-pwd }) + report-pwd +} diff --git a/vendor/libghostty-vt/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/vendor/libghostty-vt/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish new file mode 100644 index 00000000..f8bfe091 --- /dev/null +++ b/vendor/libghostty-vt/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -0,0 +1,245 @@ +# This shell script aims to be written in a way where it can't really fail +# or all failure scenarios are handled, so that we never leave the shell in +# a weird state. If you find a way to break this, please report a bug! + +function ghostty_restore_xdg_data_dir -d "restore the original XDG_DATA_DIR value" + # If we don't have our own data dir then we don't need to do anything. + if not set -q GHOSTTY_SHELL_INTEGRATION_XDG_DIR + return + end + + # If the data dir isn't set at all then we don't need to do anything. + if not set -q XDG_DATA_DIRS + return + end + + # We need to do this so that XDG_DATA_DIRS turns into an array. + set --function --path xdg_data_dirs "$XDG_DATA_DIRS" + + # If our data dir is in the list then remove it. + if set --function index (contains --index "$GHOSTTY_SHELL_INTEGRATION_XDG_DIR" $xdg_data_dirs) + set --erase --function xdg_data_dirs[$index] + end + + # Re-export our data dir + if set -q xdg_data_dirs[1] + set --global --export --unpath XDG_DATA_DIRS "$xdg_data_dirs" + else + set --erase --global XDG_DATA_DIRS + end + + set --erase GHOSTTY_SHELL_INTEGRATION_XDG_DIR +end + +function ghostty_exit -d "exit the shell integration setup" + functions -e ghostty_restore_xdg_data_dir + functions -e ghostty_exit + exit 0 +end + +# We always try to restore the XDG data dir +ghostty_restore_xdg_data_dir + +# If we aren't interactive or we've already run, don't run. +status --is-interactive || ghostty_exit + +# We do the full setup on the first prompt render. We do this so that other +# shell integrations that setup the prompt and modify things are able to run +# first. We want to run _last_. +function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" + functions -e __ghostty_setup + + set --local features (string split , $GHOSTTY_SHELL_FEATURES) + + # Parse the fish version for feature detection. + # Default to 0.0 if version is unavailable or malformed. + set -l fish_major 0 + set -l fish_minor 0 + if set -q version[1] + set -l fish_ver (string match -r '(\d+)\.(\d+)' -- $version[1]) + if set -q fish_ver[2]; and test -n "$fish_ver[2]" + set fish_major "$fish_ver[2]" + end + if set -q fish_ver[3]; and test -n "$fish_ver[3]" + set fish_minor "$fish_ver[3]" + end + end + + # Our OSC133A (prompt start) sequence. If we're using Fish >= 4.1 + # then it supports click_events so we enable that. + set -g __ghostty_prompt_start_mark "\e]133;A\a" + if test "$fish_major" -gt 4; or test "$fish_major" -eq 4 -a "$fish_minor" -ge 1 + set -g __ghostty_prompt_start_mark "\e]133;A;click_events=1\a" + end + + if string match -q 'cursor*' -- $features + set -l cursor 5 # blinking bar + contains cursor:steady $features && set cursor 6 # steady bar + + # Change the cursor to a beam on prompt. + function __ghostty_set_cursor_beam --on-event fish_prompt -V cursor -d "Set cursor shape" + if not functions -q fish_vi_cursor_handle + echo -en "\e[$cursor q" + end + end + function __ghostty_reset_cursor --on-event fish_preexec -d "Reset cursor shape" + if not functions -q fish_vi_cursor_handle + echo -en "\e[0 q" + end + end + end + + # Add Ghostty binary to PATH if the path feature is enabled + if contains path $features; and test -n "$GHOSTTY_BIN_DIR" + fish_add_path --global --path --append "$GHOSTTY_BIN_DIR" + end + + # When using sudo shell integration feature, ensure $TERMINFO is set + # and `sudo` is not already a function or alias + if contains sudo $features; and test -n "$TERMINFO"; and test file = (type -t sudo 2> /dev/null; or echo "x") + # Wrap `sudo` command to ensure Ghostty terminfo is preserved + function sudo -d "Wrap sudo to preserve terminfo" + set --function sudo_has_sudoedit_flags no + for arg in $argv + # Check if argument is '-e' or '--edit' (sudoedit flags) + if string match -q -- -e "$arg"; or string match -q -- --edit "$arg" + set --function sudo_has_sudoedit_flags yes + break + end + # Check if argument is neither an option nor a key-value pair + if not string match -r -q -- "^-" "$arg"; and not string match -r -q -- "=" "$arg" + break + end + end + if test "$sudo_has_sudoedit_flags" = yes + command sudo $argv + else + command sudo --preserve-env=TERMINFO $argv + end + end + end + + # SSH Integration + set -l features (string split ',' -- "$GHOSTTY_SHELL_FEATURES") + if contains ssh-env $features; or contains ssh-terminfo $features + function ssh --wraps=ssh --description "SSH wrapper with Ghostty integration" + set -l features (string split ',' -- "$GHOSTTY_SHELL_FEATURES") + set -l ssh_term xterm-256color + set -l ssh_opts + + # Configure environment variables for remote session + if contains ssh-env $features + set -a ssh_opts -o "SendEnv COLORTERM TERM_PROGRAM TERM_PROGRAM_VERSION" + end + + # Install terminfo on remote host if needed + if contains ssh-terminfo $features + set -l ssh_user + set -l ssh_hostname + + for line in (command ssh -G $argv 2>/dev/null) + set -l parts (string split ' ' -- $line) + if test (count $parts) -ge 2 + switch $parts[1] + case user + set ssh_user $parts[2] + case hostname + set ssh_hostname $parts[2] + end + if test -n "$ssh_user"; and test -n "$ssh_hostname" + break + end + end + end + + if test -n "$ssh_hostname" + set -l ssh_target "$ssh_user@$ssh_hostname" + + # Check if terminfo is already cached + if test -x "$GHOSTTY_BIN_DIR/ghostty"; and "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --host="$ssh_target" >/dev/null 2>&1 + set ssh_term xterm-ghostty + else if command -q infocmp + set -l ssh_terminfo + set -l ssh_cpath_dir + set -l ssh_cpath + + set ssh_terminfo "$(infocmp -0 -x xterm-ghostty 2>/dev/null)" + + if test -n "$ssh_terminfo" + echo "Setting up xterm-ghostty terminfo on $ssh_hostname..." >&2 + + set ssh_cpath_dir (mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null; or echo "/tmp/ghostty-ssh-$ssh_user."(random)) + set ssh_cpath "$ssh_cpath_dir/socket" + + if echo "$ssh_terminfo" | command ssh $ssh_opts -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s $argv ' + 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 + set ssh_term xterm-ghostty + set -a ssh_opts -o "ControlPath=$ssh_cpath" + + # Cache successful installation + if test -x "$GHOSTTY_BIN_DIR/ghostty" + "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --add="$ssh_target" >/dev/null 2>&1; or true + end + else + echo "Warning: Failed to install terminfo." >&2 + end + else + echo "Warning: Could not generate terminfo data." >&2 + end + else + echo "Warning: ghostty command not available for cache management." >&2 + end + end + end + + # Execute SSH with TERM environment variable + TERM="$ssh_term" COLORTERM=truecolor command ssh $ssh_opts $argv + end + end + + # Setup prompt marking + function __ghostty_mark_prompt_start --on-event fish_prompt --on-event fish_posterror + # If we never got the output end event, then we need to send it now. + if test "$__ghostty_prompt_state" != prompt-start + echo -en "\e]133;D\a" + end + + set --global __ghostty_prompt_state prompt-start + echo -en $__ghostty_prompt_start_mark + end + + function __ghostty_mark_output_start --on-event fish_preexec + set --global __ghostty_prompt_state pre-exec + echo -en "\e]133;C\a" + end + + function __ghostty_mark_output_end --on-event fish_postexec + set --global __ghostty_prompt_state post-exec + echo -en "\e]133;D;$status\a" + end + + # Report pwd. This is actually built-in to fish but only for terminals + # that match an allowlist and that isn't us. + function __update_cwd_osc --on-variable PWD -d 'Notify capable terminals when $PWD changes' + if status --is-command-substitution || set -q INSIDE_EMACS + return + end + printf \e\]7\;file://%s%s\a $hostname (string escape --style=url $PWD) + end + + # Enable fish to handle reflow because Ghostty clears the prompt on resize. + set --global fish_handle_reflow 1 + + # Initial calls for first prompt + if string match -q 'cursor*' -- $features + __ghostty_set_cursor_beam + end + __ghostty_mark_prompt_start + __update_cwd_osc +end + +ghostty_exit diff --git a/vendor/libghostty-vt/src/shell-integration/nushell/vendor/autoload/ghostty.nu b/vendor/libghostty-vt/src/shell-integration/nushell/vendor/autoload/ghostty.nu new file mode 100644 index 00000000..17970f51 --- /dev/null +++ b/vendor/libghostty-vt/src/shell-integration/nushell/vendor/autoload/ghostty.nu @@ -0,0 +1,110 @@ +# Ghostty shell integration +export module ghostty { + def has_feature [feature: string] { + $feature in ($env.GHOSTTY_SHELL_FEATURES | default "" | split row ',') + } + + # Wrap `ssh` with Ghostty TERMINFO support + export def --wrapped ssh [...args] { + mut ssh_env = {} + mut ssh_opts = [] + + # `ssh-env`: use xterm-256color and propagate COLORTERM/TERM_PROGRAM vars + if (has_feature "ssh-env") { + $ssh_env.TERM = "xterm-256color" + $ssh_env.COLORTERM = "truecolor" + $ssh_opts = [ + "-o" "SendEnv COLORTERM TERM_PROGRAM TERM_PROGRAM_VERSION" + ] + } + + # `ssh-terminfo`: auto-install xterm-ghostty terminfo on remote hosts + if (has_feature "ssh-terminfo") { + let ghostty = ($env.GHOSTTY_BIN_DIR? | default "") | path join "ghostty" + + let ssh_cfg = ^ssh -G ...$args + | lines + | parse "{key} {value}" + | where key in ["user" "hostname"] + | select key value + | transpose -rd + | default {user: $env.USER hostname: "localhost"} + let ssh_id = $"($ssh_cfg.user)@($ssh_cfg.hostname)" + + if (^$ghostty "+ssh-cache" $"--host=($ssh_id)" | complete | $in.exit_code == 0) { + $ssh_env.TERM = "xterm-ghostty" + } else { + $ssh_env.TERM = "xterm-256color" + + let terminfo = try { + ^infocmp -0 -x xterm-ghostty + } catch { + print -e "infocmp failed, using xterm-256color" + } + + if ($terminfo | is-not-empty) { + print $"Setting up xterm-ghostty terminfo on ($ssh_cfg.hostname)..." + + let ctrl_path = ( + mktemp -td $"ghostty-ssh-($ssh_cfg.user).XXXXXX" + | path join "socket" + ) + + let remote_args = $ssh_opts ++ [ + "-o" "ControlMaster=yes" + "-o" $"ControlPath=($ctrl_path)" + "-o" "ControlPersist=60s" + ] ++ $args + + $terminfo | ^ssh ...$remote_args ' + 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' + | complete + | if $in.exit_code == 0 { + ^$ghostty "+ssh-cache" $"--add=($ssh_id)" e>| print -e + $ssh_env.TERM = "xterm-ghostty" + $ssh_opts = ($ssh_opts ++ ["-o" $"ControlPath=($ctrl_path)"]) + } else { + print -e "terminfo install failed, using xterm-256color" + } + } + } + } + + let ssh_args = $ssh_opts ++ $args + with-env $ssh_env { + ^ssh ...$ssh_args + } + } + + # Wrap `sudo` to preserve Ghostty's TERMINFO environment variable + export def --wrapped sudo [...args] { + mut sudo_args = $args + + if (has_feature "sudo") { + # Extract just the sudo options (before the command) + let sudo_options = ( + $args | take until {|arg| + not (($arg | str starts-with "-") or ($arg | str contains "=")) + } + ) + + # Prepend TERMINFO preservation flag if not using sudoedit + if (not ("-e" in $sudo_options or "--edit" in $sudo_options)) { + $sudo_args = ($args | prepend "--preserve-env=TERMINFO") + } + } + + ^sudo ...$sudo_args + } +} + +# Clean up XDG_DATA_DIRS by removing GHOSTTY_SHELL_INTEGRATION_XDG_DIR +if 'GHOSTTY_SHELL_INTEGRATION_XDG_DIR' in $env { + if 'XDG_DATA_DIRS' in $env { + $env.XDG_DATA_DIRS = ($env.XDG_DATA_DIRS | str replace $"($env.GHOSTTY_SHELL_INTEGRATION_XDG_DIR):" "") + } + hide-env GHOSTTY_SHELL_INTEGRATION_XDG_DIR +} diff --git a/vendor/libghostty-vt/src/shell-integration/zsh/.zshenv b/vendor/libghostty-vt/src/shell-integration/zsh/.zshenv new file mode 100644 index 00000000..62dcf273 --- /dev/null +++ b/vendor/libghostty-vt/src/shell-integration/zsh/.zshenv @@ -0,0 +1,61 @@ +# Based on (started as) a copy of Kitty's zsh 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 <http://www.gnu.org/licenses/>. + +# This script is sourced automatically by zsh when ZDOTDIR is set to this +# directory. It therefore assumes it's running within our shell integration +# environment and should not be sourced manually (unlike ghostty-integration). +# +# This file can get sourced with aliases enabled. To avoid alias expansion +# we quote everything that can be quoted. Some aliases will still break us +# though. + +# Restore the original ZDOTDIR value if GHOSTTY_ZSH_ZDOTDIR is set. +# Otherwise, unset the ZDOTDIR that was set during shell injection. +if [[ -n "${GHOSTTY_ZSH_ZDOTDIR+X}" ]]; then + 'builtin' 'export' ZDOTDIR="$GHOSTTY_ZSH_ZDOTDIR" + 'builtin' 'unset' 'GHOSTTY_ZSH_ZDOTDIR' +else + 'builtin' 'unset' 'ZDOTDIR' +fi + +# Use try-always to have the right error code. +{ + # Zsh treats unset ZDOTDIR as if it was HOME. We do the same. + # + # Source the user's .zshenv before sourcing ghostty-integration because the + # former might set fpath and other things without which ghostty-integration + # won't work. + # + # Use typeset in case we are in a function with warn_create_global in + # effect. Unlikely but better safe than sorry. + 'builtin' 'typeset' _ghostty_file=${ZDOTDIR-$HOME}"/.zshenv" + # Zsh ignores unreadable rc files. We do the same. + # Zsh ignores rc files that are directories, and so does source. + [[ ! -r "$_ghostty_file" ]] || 'builtin' 'source' '--' "$_ghostty_file" +} always { + if [[ -o 'interactive' ]]; then + # ${(%):-%x} is the path to the current file. + # On top of it we add :A:h to get the directory. + 'builtin' 'typeset' _ghostty_file="${${(%):-%x}:A:h}"/ghostty-integration + if [[ -r "$_ghostty_file" ]]; then + 'builtin' 'autoload' '-Uz' '--' "$_ghostty_file" + "${_ghostty_file:t}" + 'builtin' 'unfunction' '--' "${_ghostty_file:t}" + fi + fi + 'builtin' 'unset' '_ghostty_file' +} diff --git a/vendor/libghostty-vt/src/shell-integration/zsh/ghostty-integration b/vendor/libghostty-vt/src/shell-integration/zsh/ghostty-integration new file mode 100644 index 00000000..76c5ce24 --- /dev/null +++ b/vendor/libghostty-vt/src/shell-integration/zsh/ghostty-integration @@ -0,0 +1,454 @@ +# vim:ft=zsh +# +# Based on (started as) a copy of Kitty's zsh 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 <http://www.gnu.org/licenses/>. + +# +# Enables integration between zsh and ghostty. +# +# This is an autoloadable function. It's invoked automatically in shells +# directly spawned by Ghostty but not in any other shells. For example, running +# `exec zsh`, `sudo -E zsh`, `tmux`, or plain `zsh` will create a shell where +# ghostty-integration won't automatically run. Zsh users who want integration with +# Ghostty in all shells should add the following lines to their .zshrc: +# +# if [[ -n $GHOSTTY_RESOURCES_DIR ]]; then +# source "$GHOSTTY_RESOURCES_DIR"/shell-integration/zsh/ghostty-integration +# fi +# +# Implementation note: We can assume that alias expansion is disabled in this +# file, so no need to quote defensively. We still have to defensively prefix all +# builtins with `builtin` to avoid accidentally invoking user-defined functions. +# We avoid `function` reserved word as an additional defensive measure. + +# Note that updating options with `builtin emulate -L zsh` affects the global options +# if it's called outside of a function. So nearly all code has to be in functions. +_entrypoint() { + builtin emulate -L zsh -o no_warn_create_global -o no_aliases + + [[ -o interactive ]] || builtin return 0 # non-interactive shell + (( ! $+_ghostty_state )) || builtin return 0 # already initialized + + # We require zsh 5.1+ (released Sept 2015) for features like functions_source, + # introspection arrays, and array pattern substitution. + if ! { builtin autoload -- is-at-least 2>/dev/null && is-at-least 5.1; }; then + builtin echo "Zsh ${ZSH_VERSION} is too old for ghostty shell integration (5.1+ required)" >&2 + builtin return 1 + fi + + # 0: no OSC 133 [AC] marks have been written yet. + # 1: the last written OSC 133 C has not been closed with D yet. + # 2: none of the above. + builtin typeset -gi _ghostty_state + + # Attempt to create a writable file descriptor to the TTY so that we can print + # to the TTY later even when STDOUT is redirected. This code is fairly subtle. + # + # - It's tempting to do `[[ -t 1 ]] && exec {_ghostty_state}>&1` but we cannot do this + # because it'll create a file descriptor >= 10 without O_CLOEXEC. This file + # descriptor will leak to child processes. + # - If we do `exec {3}>&1`, the file descriptor won't leak to the child processes + # but it'll still leak if the current process is replaced with another. In + # addition, it'll break user code that relies on fd 3 being available. + # - Zsh doesn't expose dup3, which would have allowed us to copy STDOUT with + # O_CLOEXEC. The only way to create a file descriptor with O_CLOEXEC is via + # sysopen. + # - `zmodload zsh/system` and `sysopen -o cloexec -wu _ghostty_fd -- /dev/tty` can + # fail with an error message to STDERR (the latter can happen even if /dev/tty + # is writable), hence the redirection of STDERR. We do it for the whole block + # for performance reasons (redirections are slow). + # - We must open the file descriptor right here rather than in _ghostty_deferred_init + # because there are broken zsh plugins out there that run `exec {fd}< <(cmd)` + # and then close the file descriptor more than once while suppressing errors. + # This could end up closing our file descriptor if we opened it in + # _ghostty_deferred_init. + typeset -gi _ghostty_fd + { + builtin zmodload zsh/system && (( $+builtins[sysopen] )) && { + { [[ -w $TTY ]] && builtin sysopen -o cloexec -wu _ghostty_fd -- $TTY } || + { [[ -w /dev/tty ]] && builtin sysopen -o cloexec -wu _ghostty_fd -- /dev/tty } + } + } 2>/dev/null || (( _ghostty_fd = 1 )) + + # Defer initialization so that other zsh init files can be configure + # the integration. + builtin typeset -ag precmd_functions + precmd_functions+=(_ghostty_deferred_init) +} + +_ghostty_deferred_init() { + builtin emulate -L zsh -o no_warn_create_global -o no_aliases + + # Enable semantic markup with OSC 133. + _ghostty_precmd() { + builtin local -i cmd_status=$? + builtin emulate -L zsh -o no_warn_create_global -o no_aliases + + # Don't write OSC 133 D when our precmd handler is invoked from zle. + # Some plugins do that to update prompt on cd. + if ! builtin zle; then + # This code works incorrectly in the presence of a precmd or chpwd + # hook that prints. For example, sindresorhus/pure prints an empty + # line on precmd and marlonrichert/zsh-snap prints $PWD on chpwd. + # We'll end up writing our OSC 133 D mark too late. + # + # Another failure mode is when the output of a command doesn't end + # with LF and prompst_sp is set (it is by default). In this case + # we'll incorrectly state that '%' from prompt_sp is a part of the + # command's output. + if (( _ghostty_state == 1 )); then + # The last written OSC 133 C has not been closed with D yet. + # Close it and supply status. + builtin print -nu $_ghostty_fd '\e]133;D;'$cmd_status'\a' + (( _ghostty_state = 2 )) + elif (( _ghostty_state == 2 )); then + # There might be an unclosed OSC 133 C. Close that. + builtin print -nu $_ghostty_fd '\e]133;D\a' + fi + fi + + builtin local mark1=$'%{\e]133;A;cl=line\a%}' + if [[ -o prompt_percent ]]; then + builtin typeset -g precmd_functions + if [[ ${precmd_functions[-1]} == _ghostty_precmd ]]; then + # This is the best case for us: we can add our marks to PS1 and + # PS2. This way our marks will be printed whenever zsh + # redisplays prompt: on reset-prompt, on SIGWINCH, and on + # SIGCHLD if notify is set. Themes that update prompt + # asynchronously from a `zle -F` handler might still remove our + # marks. Oh well. + + # Restore PS1/PS2 to their pre-mark state if nothing else has + # modified them since we last added marks. This avoids exposing + # PS1 with our marks to other hooks (which can break themes like + # Pure that use pattern matching to strip/rebuild the prompt). + # If PS1 was modified (by a theme, async update, etc.), we + # keep the modified version, prioritizing the theme's changes. + builtin local ps1_changed=0 + if [[ -n ${_ghostty_saved_ps1+x} ]]; then + if [[ $PS1 == $_ghostty_marked_ps1 ]]; then + PS1=$_ghostty_saved_ps1 + PS2=$_ghostty_saved_ps2 + elif [[ $PS1 != $_ghostty_saved_ps1 ]]; then + ps1_changed=1 + fi + fi + + # Save the clean PS1/PS2 before we add marks. + _ghostty_saved_ps1=$PS1 + _ghostty_saved_ps2=$PS2 + + # Add our marks. Since we always start from a clean PS1 + # (either restored above or freshly set by a theme), we can + # unconditionally add mark1 and markB. + builtin local mark2=$'%{\e]133;P;k=s\a%}' + builtin local markB=$'%{\e]133;B\a%}' + # If PS1 ends with a bare '%', it combines with the '{' + # in markB to form a '%{' prompt escape, swallowing the + # marker and producing a visible '{'. Fix by doubling the + # trailing '%' so it becomes a literal '%%'. + [[ $PS1 == *[^%]% || $PS1 == % ]] && PS1=$PS1% + PS1=${mark1}${PS1}${markB} + + # Handle multiline prompts by marking newline-separated + # continuation lines with k=s (mark2). + # + # We skip this when PS1 changed because injecting marks into + # newlines can break pattern matching in themes that + # strip/rebuild the prompt dynamically (e.g., Pure). + if (( ! ps1_changed )) && [[ $PS1 == *$'\n'* ]]; then + PS1=${PS1//$'\n'/$'\n'${mark2}} + fi + + # PS2 mark is needed when clearing the prompt on resize + [[ $PS2 == *[^%]% || $PS2 == % ]] && PS2=$PS2% + PS2=${mark2}${PS2}${markB} + + # Save the marked PS1 so we can detect modifications + # by other hooks in the next cycle. + _ghostty_marked_ps1=$PS1 + (( _ghostty_state = 2 )) + else + # If our precmd hook is not the last, we cannot rely on prompt + # changes to stick, so we don't even try. At least we can move + # our hook to the end to have better luck next time. If there is + # another piece of code that wants to take this privileged + # position, this won't work well. We'll break them as much as + # they are breaking us. + precmd_functions=(${precmd_functions:#_ghostty_precmd} _ghostty_precmd) + # Plugins that invoke precmd hooks from zle do that before zle + # is trashed. This means that the cursor is in the middle of + # BUFFER and we cannot print our mark there. Prompt might + # already have a mark, so the following reset-prompt will write + # it. If it doesn't, there is nothing we can do. + if ! builtin zle; then + builtin print -rnu $_ghostty_fd -- $mark1[3,-3] + (( _ghostty_state = 2 )) + fi + fi + elif ! builtin zle; then + # Without prompt_percent we cannot patch prompt. Just print the + # mark, except when we are invoked from zle. In the latter case we + # cannot do anything. + builtin print -rnu $_ghostty_fd -- $mark1[3,-3] + (( _ghostty_state = 2 )) + fi + } + + _ghostty_preexec() { + builtin emulate -L zsh -o no_warn_create_global -o no_aliases + + # Restore the original PS1/PS2 if nothing else has modified them + # since our precmd added marks. This ensures other preexec hooks + # see a clean PS1 without our marks. If PS1 was modified (e.g., + # by an async theme update), we leave it alone. + if [[ -n ${_ghostty_saved_ps1+x} && $PS1 == $_ghostty_marked_ps1 ]]; then + PS1=$_ghostty_saved_ps1 + PS2=$_ghostty_saved_ps2 + fi + + # This will work incorrectly in the presence of a preexec hook that + # prints. For example, if MichaelAquilina/zsh-you-should-use installs + # its preexec hook before us, we'll incorrectly mark its output as + # belonging to the command (as if the user typed it into zle) rather + # than command output. + builtin print -nu $_ghostty_fd '\e]133;C\a' + (( _ghostty_state = 1 )) + } + + # Enable reporting current working dir to terminal. Ghostty supports + # the kitty-shell-cwd format. + _ghostty_report_pwd() { builtin print -nu $_ghostty_fd '\e]7;kitty-shell-cwd://'"$HOST""$PWD"'\a'; } + chpwd_functions=(${chpwd_functions[@]} "_ghostty_report_pwd") + # An executed program could change cwd and report the changed cwd, so also report cwd at each new prompt + # as in this case chpwd_functions is insufficient. chpwd_functions is still needed for things like: cd x && something + functions[_ghostty_precmd]+=" + _ghostty_report_pwd" + _ghostty_report_pwd + + if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then + # Enable terminal title changes, formatted for user-friendly display. + functions[_ghostty_precmd]+=" + builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(%):-%(4~|…/%3~|%~)}\"\$'\\a'" + functions[_ghostty_preexec]+=" + builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${1//[[:cntrl:]]}\"\$'\\a'" + fi + + if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then + # Enable cursor shape changes depending on the current keymap. + # This implementation leaks blinking block cursor into external commands + # executed from zle. For example, users of fzf-based widgets may find + # themselves with a blinking block cursor within fzf. + _ghostty_zle_line_init _ghostty_zle_line_finish _ghostty_zle_keymap_select() { + builtin local steady=0 + [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor:steady"* ]] && steady=1 + case ${KEYMAP-} in + vicmd|visual) builtin print -nu "$_ghostty_fd" "\e[$(( 1 + steady )) q" ;; # block + *) builtin print -nu "$_ghostty_fd" "\e[$(( 5 + steady )) q" ;; # bar + esac + } + # Restore the default shape before executing an external command + functions[_ghostty_preexec]+=" + builtin print -rnu $_ghostty_fd \$'\\e[0 q'" + fi + + # Emit semantic prompt markers at line-init if PS1 doesn't contain our + # marks. This ensures the terminal sees prompt markers even if another + # plugin (like zinit or oh-my-posh) regenerated PS1 after our precmd ran. + # We use 133;P instead of 133;A to avoid fresh-line behavior which would + # disrupt the display since the prompt has already been drawn. We also + # emit 133;B to mark the input area, which is needed for click-to-move. + (( $+functions[_ghostty_zle_line_init] )) || _ghostty_zle_line_init() { builtin true; } + functions[_ghostty_zle_line_init]=" + if [[ \$PS1 != *$'%{\\e]133;A'* ]]; then + builtin print -nu \$_ghostty_fd '\\e]133;P;k=i\\a\\e]133;B\\a' + fi + "${functions[_ghostty_zle_line_init]} + + # 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 + builtin 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 + 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() { + emulate -L zsh + setopt local_options no_glob_subst + + 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 "SendEnv COLORTERM TERM_PROGRAM TERM_PROGRAM_VERSION") + fi + + # Install terminfo on remote host if needed + if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-terminfo* ]]; then + 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 < <(command ssh -G "$@" 2>/dev/null) + + if [[ -n "$ssh_hostname" ]]; then + 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 (( $+commands[infocmp] )); then + local ssh_terminfo ssh_cpath_dir ssh_cpath + + ssh_terminfo=$(infocmp -0 -x xterm-ghostty 2>/dev/null) + + if [[ -n "$ssh_terminfo" ]]; then + print "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 print -r "$ssh_terminfo" | command ssh "${ssh_opts[@]}" -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 + print "Warning: Failed to install terminfo." >&2 + fi + else + print "Warning: Could not generate terminfo data." >&2 + fi + else + print "Warning: ghostty command not available for cache management." >&2 + fi + fi + fi + + # Execute SSH with TERM environment variable + TERM="$ssh_term" COLORTERM=truecolor command ssh "${ssh_opts[@]}" "$@" + } + fi + + # Some zsh users manually run `source ~/.zshrc` in order to apply rc file + # changes to the current shell. This is a terrible practice that breaks many + # things, including our shell integration. For example, Oh My Zsh and Prezto + # (both very popular among zsh users) will remove zle-line-init and + # zle-line-finish hooks if .zshrc is manually sourced. Prezto will also remove + # zle-keymap-select. + # + # Another common (and much more robust) way to apply rc file changes to the + # current shell is `exec zsh`. This will remove our integration from the shell + # unless it's explicitly invoked from .zshrc. This is not an issue with + # `exec zsh` but rather with our implementation of automatic shell integration. + + # In the ideal world we would use add-zle-hook-widget to hook zle-line-init + # and similar widget. This breaks user configs though, so we have do this + # horrible thing instead. + builtin local hook func widget orig_widget flag + for hook in line-init line-finish keymap-select; do + func=_ghostty_zle_${hook/-/_} + (( $+functions[$func] )) || builtin continue + widget=zle-$hook + if [[ $widgets[$widget] == user:azhw:* && + $+functions[add-zle-hook-widget] -eq 1 ]]; then + # If the widget is already hooked by add-zle-hook-widget at the top + # level, add our hook at the end. We MUST do it this way. We cannot + # just wrap the widget ourselves in this case because it would + # trigger bugs in add-zle-hook-widget. + add-zle-hook-widget $hook $func + else + if (( $+widgets[$widget] )); then + # There is a widget but it's not from add-zle-hook-widget. We + # can rename the original widget, install our own and invoke + # the original when we are called. + # + # Note: The leading dot is to work around bugs in + # zsh-syntax-highlighting. + orig_widget=._ghostty_orig_$widget + builtin zle -A $widget $orig_widget + if [[ $widgets[$widget] == user:* ]]; then + # No -w here to preserve $WIDGET within the original widget. + flag= + else + flag=w + fi + functions[$func]+=" + builtin zle $orig_widget -N$flag -- \"\$@\"" + fi + builtin zle -N $widget $func + fi + done + + if (( $+functions[_ghostty_preexec] )); then + builtin typeset -ag preexec_functions + preexec_functions+=(_ghostty_preexec) + fi + + builtin typeset -ag precmd_functions + if (( $+functions[_ghostty_precmd] )); then + precmd_functions=(${precmd_functions:#_ghostty_deferred_init} _ghostty_precmd) + _ghostty_precmd + else + precmd_functions=(${precmd_functions:#_ghostty_deferred_init}) + fi + + # Unfunction _ghostty_deferred_init to save memory. Don't unfunction + # ghostty-integration though because decent public functions aren't supposed to + # to unfunction themselves when invoked. Unfunctioning is done by calling code. + builtin unfunction _ghostty_deferred_init +} + +_entrypoint |
