diff options
| author | Jason Rayne <yo@arcayne.dev> | 2025-07-03 20:11:45 -0700 |
|---|---|---|
| committer | Jason Rayne <yo@arcayne.dev> | 2025-07-03 20:11:45 -0700 |
| commit | 75c703071a6ab176f2d7982a17cde1e593e14737 (patch) | |
| tree | 2ee675f81ef272b16d57178dbb6647522b772c93 /src/shell-integration | |
| parent | e25aa9f424097938362a9d6b02fa32c1714ab97f (diff) | |
feat(ssh): rewrite SSH cache system in native Zig
- Eliminates standalone bash dependency
- Consolidates `+list-ssh-cache` and `+clear-ssh-cache` actions into
single `+ssh-cache` action with args
- Structured cache format with timestamps and expiration support
- Memory-safe entry handling with proper file locking
- Comprehensive hostname validation (IPv4/IPv6/domains)
- Atomic updates via temp file + rename
- Updated shell integrations for improved cross-platform support and
reliability
- Cache operations are now unit-testable
Diffstat (limited to 'src/shell-integration')
| -rw-r--r-- | src/shell-integration/bash/ghostty.bash | 197 | ||||
| -rw-r--r-- | src/shell-integration/elvish/lib/ghostty-integration.elv | 293 | ||||
| -rw-r--r-- | src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish | 205 | ||||
| -rwxr-xr-x | src/shell-integration/shared/ghostty-ssh-cache | 11 | ||||
| -rw-r--r-- | src/shell-integration/zsh/ghostty-integration | 196 |
5 files changed, 655 insertions, 247 deletions
diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index b51dae9c7..8c4cd9e12 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -97,73 +97,172 @@ fi # SSH Integration if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then + : "${GHOSTTY_SSH_CACHE_TIMEOUT:=5}" + : "${GHOSTTY_SSH_CHECK_TIMEOUT:=3}" - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then - readonly _CACHE="${GHOSTTY_RESOURCES_DIR}/shell-integration/shared/ghostty-ssh-cache" - fi - - # SSH wrapper + # SSH wrapper that preserves Ghostty features across remote connections ssh() { - local env=() opts=() ctrl=() + local ssh_env=() ssh_opts=() - # Set up env vars first so terminfo installation inherits them + # Configure environment variables for remote session if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - local vars=( - COLORTERM=truecolor - TERM_PROGRAM=ghostty - ${TERM_PROGRAM_VERSION:+TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION} + local -a ssh_env_vars=( + "COLORTERM=truecolor" + "TERM_PROGRAM=ghostty" ) - for v in "${vars[@]}"; do - builtin export "${v?}" - opts+=(-o "SendEnv ${v%=*}" -o "SetEnv $v") + if [[ -n "$TERM_PROGRAM_VERSION" ]]; then + ssh_env_vars+=("TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION") + fi + + # Temporarily export variables for SSH transmission + local -a ssh_exported_vars=() + for ssh_v in "${ssh_env_vars[@]}"; do + local ssh_var_name="${ssh_v%%=*}" + + if [[ -n "${!ssh_var_name+x}" ]]; then + ssh_exported_vars+=("$ssh_var_name=${!ssh_var_name}") + else + ssh_exported_vars+=("$ssh_var_name") + fi + + builtin export "${ssh_v?}" + + # Use both SendEnv and SetEnv for maximum compatibility + ssh_opts+=(-o "SendEnv $ssh_var_name") + ssh_opts+=(-o "SetEnv $ssh_v") done + + ssh_env+=("${ssh_env_vars[@]}") fi - # Install terminfo if needed, reuse control connection for main session + # Install terminfo on remote host if needed if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then - # Get target (only when needed for terminfo) - builtin local target - target=$(builtin command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') - - if [[ -n "$target" ]] && "$_CACHE" chk "$target"; then - env+=(TERM=xterm-ghostty) - elif builtin command -v infocmp >/dev/null 2>&1; then - builtin local tinfo - tinfo=$(infocmp -x xterm-ghostty 2>/dev/null) || builtin echo "Warning: xterm-ghostty terminfo not found locally." >&2 - if [[ -n "$tinfo" ]]; then - builtin echo "Setting up Ghostty terminfo on remote host..." >&2 - builtin local cpath - cpath="/tmp/ghostty-ssh-$USER-$RANDOM-$(date +%s)" - case $(builtin echo "$tinfo" | builtin command ssh "${opts[@]}" -o ControlMaster=yes -o ControlPath="$cpath" -o ControlPersist=60s "$@" ' - infocmp xterm-ghostty >/dev/null 2>&1 && echo OK && exit - command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL - ') in - OK) - builtin echo "Terminfo setup complete." >&2 - [[ -n "$target" ]] && "$_CACHE" add "$target" - env+=(TERM=xterm-ghostty) - ctrl+=(-o "ControlPath=$cpath") - ;; - *) builtin echo "Warning: Failed to install terminfo." >&2 ;; - esac + builtin local ssh_config ssh_user ssh_hostname + ssh_config=$(builtin command ssh -G "$@" 2>/dev/null) + ssh_user=$(echo "$ssh_config" | while IFS=' ' read -r ssh_key ssh_value; do + [[ "$ssh_key" == "ssh_user" ]] && echo "$ssh_value" && break + done) + ssh_hostname=$(echo "$ssh_config" | while IFS=' ' read -r ssh_key ssh_value; do + [[ "$ssh_key" == "hostname" ]] && echo "$ssh_value" && break + done) + ssh_target="${ssh_user}@${ssh_hostname}" + + if [[ -n "$ssh_hostname" ]]; then + # Detect timeout command (BSD compatibility) + local ssh_timeout_cmd="" + if command -v timeout >/dev/null 2>&1; then + ssh_timeout_cmd="timeout" + elif command -v gtimeout >/dev/null 2>&1; then + ssh_timeout_cmd="gtimeout" + fi + + # Check if terminfo is already cached + local ssh_cache_check_success=false + if command -v ghostty >/dev/null 2>&1; then + if [[ -n "$ssh_timeout_cmd" ]]; then + $ssh_timeout_cmd "${GHOSTTY_SSH_CHECK_TIMEOUT}s" ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 && ssh_cache_check_success=true + else + ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 && ssh_cache_check_success=true + fi + fi + + if [[ "$ssh_cache_check_success" == "true" ]]; then + ssh_env+=(TERM=xterm-ghostty) + elif builtin command -v infocmp >/dev/null 2>&1; then + builtin local ssh_terminfo + + # Generate terminfo data (BSD base64 compatibility) + if base64 --help 2>&1 | grep -q GNU; then + ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 -w0 2>/dev/null) + else + ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 2>/dev/null | tr -d '\n') + fi + + if [[ -n "$ssh_terminfo" ]]; then + builtin echo "Setting up Ghostty terminfo on remote host..." >&2 + builtin local ssh_cpath_dir ssh_cpath + + 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" + + local ssh_base64_decode_cmd + if base64 --help 2>&1 | grep -q GNU; then + ssh_base64_decode_cmd="base64 -d" + else + ssh_base64_decode_cmd="base64 -D" + fi + + if builtin echo "$ssh_terminfo" | $ssh_base64_decode_cmd | builtin 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 + builtin echo "Terminfo setup complete." >&2 + ssh_env+=(TERM=xterm-ghostty) + ssh_opts+=(-o "ControlPath=$ssh_cpath") + + # Cache successful installation + if [[ -n "$ssh_target" ]] && command -v ghostty >/dev/null 2>&1; then + ( + set +m + { + if [[ -n "$ssh_timeout_cmd" ]]; then + $ssh_timeout_cmd "${GHOSTTY_SSH_CACHE_TIMEOUT}s" ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true + else + ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true + fi + } & + ) + fi + else + builtin echo "Warning: Failed to install terminfo." >&2 + ssh_env+=(TERM=xterm-256color) + fi + else + builtin echo "Warning: Could not generate terminfo data." >&2 + ssh_env+=(TERM=xterm-256color) + fi + else + builtin echo "Warning: ghostty command not available for cache management." >&2 + ssh_env+=(TERM=xterm-256color) fi else - builtin echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then + ssh_env+=(TERM=xterm-256color) + fi fi fi - # Fallback TERM only if terminfo didn't set it + # Ensure TERM is set when using ssh-env feature if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - [[ $TERM == xterm-ghostty && ! " ${env[*]} " =~ " TERM=" ]] && env+=(TERM=xterm-256color) + local ssh_term_set=false + for ssh_v in "${ssh_env[@]}"; do + if [[ "$ssh_v" =~ ^TERM= ]]; then + ssh_term_set=true + break + fi + done + if [[ "$ssh_term_set" == "false" && "$TERM" == "xterm-ghostty" ]]; then + ssh_env+=(TERM=xterm-256color) + fi fi - # Execute - if [[ ${#env[@]} -gt 0 ]]; then - env "${env[@]}" ssh "${opts[@]}" "${ctrl[@]}" "$@" - else - builtin command ssh "${opts[@]}" "${ctrl[@]}" "$@" + builtin command ssh "${ssh_opts[@]}" "$@" + local ssh_ret=$? + + # Restore original environment variables + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then + for ssh_v in "${ssh_exported_vars[@]}"; do + if [[ "$ssh_v" == *=* ]]; then + builtin export "${ssh_v?}" + else + builtin unset "${ssh_v}" + fi + done fi + + return $ssh_ret } fi diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 084861434..76fa7bafa 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -100,91 +100,238 @@ # SSH Integration use str + use re - if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) or (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo) { + if (or (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo)) { + var GHOSTTY_SSH_CACHE_TIMEOUT = (if (has-env GHOSTTY_SSH_CACHE_TIMEOUT) { echo $E:GHOSTTY_SSH_CACHE_TIMEOUT } else { echo 5 }) + var GHOSTTY_SSH_CHECK_TIMEOUT = (if (has-env GHOSTTY_SSH_CHECK_TIMEOUT) { echo $E:GHOSTTY_SSH_CHECK_TIMEOUT } else { echo 3 }) - if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo) { - var _CACHE = $E:GHOSTTY_RESOURCES_DIR/shell-integration/shared/ghostty-ssh-cache - } + # SSH wrapper that preserves Ghostty features across remote connections + fn ssh {|@args| + var ssh-env = [] + var ssh-opts = [] - # SSH wrapper - fn ssh {|@args| - var env = [] - var opts = [] - var ctrl = [] - - # Set up env vars first so terminfo installation inherits them - if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { - var vars = [ - COLORTERM=truecolor - TERM_PROGRAM=ghostty - ] - if (not-eq $E:TERM_PROGRAM_VERSION '') { - set vars = [$@vars TERM_PROGRAM_VERSION=$E:TERM_PROGRAM_VERSION] - } + # Configure environment variables for remote session + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { + var ssh-env-vars = [ + COLORTERM=truecolor + TERM_PROGRAM=ghostty + ] - for v $vars { - set-env (str:split = $v | take 1) (str:split = $v | drop 1 | str:join =) - var varname = (str:split = $v | take 1) - set opts = [$@opts -o 'SendEnv '$varname -o 'SetEnv '$v] - } - } + if (has-env TERM_PROGRAM_VERSION) { + set ssh-env-vars = [$@ssh-env-vars TERM_PROGRAM_VERSION=$E:TERM_PROGRAM_VERSION] + } - # Install terminfo if needed, reuse control connection for main session - if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo) { - # Get target - var target = '' - try { - set target = (e:ssh -G $@args 2>/dev/null | e:awk '/^(user|hostname) /{print $2}' | e:paste -sd'@') - } catch { } - - if (and (not-eq $target '') ($_CACHE chk $target)) { - set env = [$@env TERM=xterm-ghostty] - } elif (has-external infocmp) { - var tinfo = '' - try { - set tinfo = (e:infocmp -x xterm-ghostty 2>/dev/null) - } catch { - echo "Warning: xterm-ghostty terminfo not found locally." >&2 + # Store original values for restoration + var ssh-exported-vars = [] + for ssh-v $ssh-env-vars { + var ssh-var-name = (str:split &max=2 = $ssh-v)[0] + + if (has-env $ssh-var-name) { + var original-value = (get-env $ssh-var-name) + set ssh-exported-vars = [$@ssh-exported-vars $ssh-var-name=$original-value] + } else { + set ssh-exported-vars = [$@ssh-exported-vars $ssh-var-name] + } + + # Export the variable + var ssh-var-parts = (str:split &max=2 = $ssh-v) + set-env $ssh-var-parts[0] $ssh-var-parts[1] + + # Use both SendEnv and SetEnv for maximum compatibility + set ssh-opts = [$@ssh-opts -o "SendEnv "$ssh-var-name] + set ssh-opts = [$@ssh-opts -o "SetEnv "$ssh-v] + } + + set ssh-env = [$@ssh-env $@ssh-env-vars] } - if (not-eq $tinfo '') { - echo "Setting up Ghostty terminfo on remote host..." >&2 - var cpath = '/tmp/ghostty-ssh-'$E:USER'-'(randint 0 32767)'-'(date +%s) - var result = (echo $tinfo | e:ssh $@opts -o ControlMaster=yes -o ControlPath=$cpath -o ControlPersist=60s $@args ' - infocmp xterm-ghostty >/dev/null 2>&1 && echo OK && exit - command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL - ') + # Install terminfo on remote host if needed + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo) { + var ssh-config = "" + try { + set ssh-config = (external ssh -G $@args 2>/dev/null | slurp) + } catch { + set ssh-config = "" + } + + var ssh-user = "" + var ssh-hostname = "" + + for line (str:split "\n" $ssh-config) { + var parts = (str:split " " $line) + if (and (> (count $parts) 1) (eq $parts[0] user)) { + set ssh-user = $parts[1] + } + if (and (> (count $parts) 1) (eq $parts[0] hostname)) { + set ssh-hostname = $parts[1] + } + } + + var ssh-target = $ssh-user"@"$ssh-hostname + + if (not-eq $ssh-hostname "") { + # Detect timeout command (BSD compatibility) + var ssh-timeout-cmd = "" + try { + external timeout --help >/dev/null 2>&1 + set ssh-timeout-cmd = timeout + } catch { + try { + external gtimeout --help >/dev/null 2>&1 + set ssh-timeout-cmd = gtimeout + } catch { + # no timeout command available + } + } + + # Check if terminfo is already cached + var ssh-cache-check-success = $false + try { + external ghostty --help >/dev/null 2>&1 + if (not-eq $ssh-timeout-cmd "") { + try { + external $ssh-timeout-cmd $GHOSTTY_SSH_CHECK_TIMEOUT"s" ghostty +ssh-cache --host=$ssh-target >/dev/null 2>&1 + set ssh-cache-check-success = $true + } catch { + # cache check failed + } + } else { + try { + external ghostty +ssh-cache --host=$ssh-target >/dev/null 2>&1 + set ssh-cache-check-success = $true + } catch { + # cache check failed + } + } + } catch { + # ghostty not available + } + + if $ssh-cache-check-success { + set ssh-env = [$@ssh-env TERM=xterm-ghostty] + } else { + try { + external infocmp --help >/dev/null 2>&1 - if (eq $result OK) { - echo "Terminfo setup complete." >&2 - if (not-eq $target '') { $_CACHE add $target } - set env = [$@env TERM=xterm-ghostty] - set ctrl = [$@ctrl -o ControlPath=$cpath] - } else { - echo "Warning: Failed to install terminfo." >&2 - } + # Generate terminfo data (BSD base64 compatibility) + var ssh-terminfo = "" + try { + var base64-help = (external base64 --help 2>&1 | slurp) + if (str:contains $base64-help GNU) { + set ssh-terminfo = (external infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | external base64 -w0 2>/dev/null | slurp) + } else { + set ssh-terminfo = (external infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | external base64 2>/dev/null | external tr -d '\n' | slurp) + } + } catch { + set ssh-terminfo = "" + } + + if (not-eq $ssh-terminfo "") { + echo "Setting up Ghostty terminfo on remote host..." >&2 + var ssh-cpath-dir = "" + try { + set ssh-cpath-dir = (external mktemp -d "/tmp/ghostty-ssh-"$ssh-user".XXXXXX" 2>/dev/null | slurp) + } catch { + set ssh-cpath-dir = "/tmp/ghostty-ssh-"$ssh-user"."(randint 10000 99999) + } + var ssh-cpath = $ssh-cpath-dir"/socket" + + var ssh-base64-decode-cmd = "" + try { + var base64-help = (external base64 --help 2>&1 | slurp) + if (str:contains $base64-help GNU) { + set ssh-base64-decode-cmd = "base64 -d" + } else { + set ssh-base64-decode-cmd = "base64 -D" + } + } catch { + set ssh-base64-decode-cmd = "base64 -d" + } + + var terminfo-install-success = $false + try { + echo $ssh-terminfo | external sh -c $ssh-base64-decode-cmd | 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 + ' >/dev/null 2>&1 + set terminfo-install-success = $true + } catch { + set terminfo-install-success = $false + } + + if $terminfo-install-success { + echo "Terminfo setup complete." >&2 + set ssh-env = [$@ssh-env TERM=xterm-ghostty] + set ssh-opts = [$@ssh-opts -o ControlPath=$ssh-cpath] + + # Cache successful installation + if (and (not-eq $ssh-target "") (has-external ghostty)) { + if (not-eq $ssh-timeout-cmd "") { + external $ssh-timeout-cmd $GHOSTTY_SSH_CACHE_TIMEOUT"s" ghostty +ssh-cache --add=$ssh-target >/dev/null 2>&1 & + } else { + external ghostty +ssh-cache --add=$ssh-target >/dev/null 2>&1 & + } + } + } else { + echo "Warning: Failed to install terminfo." >&2 + set ssh-env = [$@ssh-env TERM=xterm-256color] + } + } else { + echo "Warning: Could not generate terminfo data." >&2 + set ssh-env = [$@ssh-env TERM=xterm-256color] + } + } catch { + echo "Warning: ghostty command not available for cache management." >&2 + set ssh-env = [$@ssh-env TERM=xterm-256color] + } + } + } else { + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { + set ssh-env = [$@ssh-env TERM=xterm-256color] + } + } } - } else { - echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 - } - } - # Fallback TERM only if terminfo didn't set it - if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { - if (and (eq $E:TERM xterm-ghostty) (not (str:contains (str:join ' ' $env) 'TERM='))) { - set env = [$@env TERM=xterm-256color] - } - } + # Ensure TERM is set when using ssh-env feature + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { + var ssh-term-set = $false + for ssh-v $ssh-env { + if (str:has-prefix $ssh-v TERM=) { + set ssh-term-set = $true + break + } + } + if (and (not $ssh-term-set) (eq $E:TERM xterm-ghostty)) { + set ssh-env = [$@ssh-env TERM=xterm-256color] + } + } + + var ssh-ret = 0 + try { + external ssh $@ssh-opts $@args + } catch e { + set ssh-ret = $e[reason][exit-status] + } - # Execute - if (> (count $env) 0) { - e:env $@env e:ssh $@opts $@ctrl $@args - } else { - e:ssh $@opts $@ctrl $@args + # Restore original environment variables + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { + for ssh-v $ssh-exported-vars { + if (str:contains $ssh-v =) { + var ssh-var-parts = (str:split &max=2 = $ssh-v) + set-env $ssh-var-parts[0] $ssh-var-parts[1] + } else { + unset-env $ssh-v + } + } + } + + if (not-eq $ssh-ret 0) { + fail ssh-failed + } } - } } defer { diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index 4c780b5a7..7dc121919 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -86,88 +86,173 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end end - # SSH Integration - if string match -qr 'ssh-(env|terminfo)' $GHOSTTY_SHELL_FEATURES - - if string match -q '*ssh-terminfo*' $GHOSTTY_SHELL_FEATURES - set -g _CACHE "$GHOSTTY_RESOURCES_DIR/shell-integration/shared/ghostty-ssh-cache" - end + # SSH Integration for Fish Shell + if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES"; or string match -q '*ssh-terminfo*' -- "$GHOSTTY_SHELL_FEATURES" + set -g GHOSTTY_SSH_CACHE_TIMEOUT (test -n "$GHOSTTY_SSH_CACHE_TIMEOUT"; and echo $GHOSTTY_SSH_CACHE_TIMEOUT; or echo 5) + set -g GHOSTTY_SSH_CHECK_TIMEOUT (test -n "$GHOSTTY_SSH_CHECK_TIMEOUT"; and echo $GHOSTTY_SSH_CHECK_TIMEOUT; or echo 3) + + # SSH wrapper that preserves Ghostty features across remote connections + function ssh --wraps=ssh --description "SSH wrapper with Ghostty integration" + set -l ssh_env + set -l ssh_opts + + # Configure environment variables for remote session + if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES" + set -l ssh_env_vars \ + "COLORTERM=truecolor" \ + "TERM_PROGRAM=ghostty" + + if test -n "$TERM_PROGRAM_VERSION" + set -a ssh_env_vars "TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION" + end - # SSH wrapper - function ssh - set -l env - set -l opts - set -l ctrl + # Store original values for restoration + set -l ssh_exported_vars + for ssh_v in $ssh_env_vars + set -l ssh_var_name (string split -m1 '=' -- $ssh_v)[1] + + if set -q $ssh_var_name + set -a ssh_exported_vars "$ssh_var_name="(eval echo \$$ssh_var_name) + else + set -a ssh_exported_vars $ssh_var_name + end - # Set up env vars first so terminfo installation inherits them - if string match -q '*ssh-env*' $GHOSTTY_SHELL_FEATURES - set -l vars \ - COLORTERM=truecolor \ - TERM_PROGRAM=ghostty + # Export the variable + set -gx (string split -m1 '=' -- $ssh_v) - if test -n "$TERM_PROGRAM_VERSION" - set -a vars "TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION" + # Use both SendEnv and SetEnv for maximum compatibility + set -a ssh_opts -o "SendEnv $ssh_var_name" + set -a ssh_opts -o "SetEnv $ssh_v" end - for v in $vars - set -l parts (string split = $v) - set -gx $parts[1] $parts[2] - set -a opts -o "SendEnv $parts[1]" -o "SetEnv $v" - end + set -a ssh_env $ssh_env_vars end - # Install terminfo if needed, reuse control connection for main session - if string match -q '*ssh-terminfo*' $GHOSTTY_SHELL_FEATURES - # Get target - set -l target (command ssh -G $argv 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') - - if test -n "$target" -a ("$_CACHE" chk "$target") - set -a env TERM=xterm-ghostty - else if command -v infocmp >/dev/null 2>&1 - set -l tinfo (infocmp -x xterm-ghostty 2>/dev/null) - set -l status_code $status + # Install terminfo on remote host if needed + if string match -q '*ssh-terminfo*' -- "$GHOSTTY_SHELL_FEATURES" + set -l ssh_config (command ssh -G $argv 2>/dev/null) + set -l ssh_user (echo $ssh_config | while read -l ssh_key ssh_value + test "$ssh_key" = "user"; and echo $ssh_value; and break + end) + set -l ssh_hostname (echo $ssh_config | while read -l ssh_key ssh_value + test "$ssh_key" = "hostname"; and echo $ssh_value; and break + end) + set -l ssh_target "$ssh_user@$ssh_hostname" + + if test -n "$ssh_hostname" + # Detect timeout command (BSD compatibility) + set -l ssh_timeout_cmd + if command -v timeout >/dev/null 2>&1 + set ssh_timeout_cmd timeout + else if command -v gtimeout >/dev/null 2>&1 + set ssh_timeout_cmd gtimeout + end - if test $status_code -ne 0 - echo "Warning: xterm-ghostty terminfo not found locally." >&2 + # Check if terminfo is already cached + set -l ssh_cache_check_success false + if command -v ghostty >/dev/null 2>&1 + if test -n "$ssh_timeout_cmd" + if $ssh_timeout_cmd "$GHOSTTY_SSH_CHECK_TIMEOUT"s ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 + set ssh_cache_check_success true + end + else + if ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 + set ssh_cache_check_success true + end + end end - if test -n "$tinfo" - echo "Setting up Ghostty terminfo on remote host..." >&2 - set -l cpath "/tmp/ghostty-ssh-$USER-"(random)"-"(date +%s) - set -l result (echo "$tinfo" | command ssh $opts -o ControlMaster=yes -o ControlPath="$cpath" -o ControlPersist=60s $argv ' - infocmp xterm-ghostty >/dev/null 2>&1 && echo OK && exit - command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL - ') - - switch $result - case OK + if test "$ssh_cache_check_success" = "true" + set -a ssh_env TERM=xterm-ghostty + else if command -v infocmp >/dev/null 2>&1 + # Generate terminfo data (BSD base64 compatibility) + set -l ssh_terminfo + if base64 --help 2>&1 | grep -q GNU + set ssh_terminfo (infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 -w0 2>/dev/null) + else + set ssh_terminfo (infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 2>/dev/null | tr -d '\n') + end + + if test -n "$ssh_terminfo" + echo "Setting up Ghostty terminfo on remote host..." >&2 + set -l ssh_cpath_dir (mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null; or echo "/tmp/ghostty-ssh-$ssh_user."(random)) + set -l ssh_cpath "$ssh_cpath_dir/socket" + + set -l ssh_base64_decode_cmd + if base64 --help 2>&1 | grep -q GNU + set ssh_base64_decode_cmd "base64 -d" + else + set ssh_base64_decode_cmd "base64 -D" + end + + if echo "$ssh_terminfo" | eval $ssh_base64_decode_cmd | 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 echo "Terminfo setup complete." >&2 - test -n "$target" && "$_CACHE" add "$target" - set -a env TERM=xterm-ghostty - set -a ctrl -o "ControlPath=$cpath" - case '*' + set -a ssh_env TERM=xterm-ghostty + set -a ssh_opts -o "ControlPath=$ssh_cpath" + + # Cache successful installation + if test -n "$ssh_target"; and command -v ghostty >/dev/null 2>&1 + fish -c " + if test -n '$ssh_timeout_cmd' + $ssh_timeout_cmd '$GHOSTTY_SSH_CACHE_TIMEOUT's ghostty +ssh-cache --add='$ssh_target' >/dev/null 2>&1; or true + else + ghostty +ssh-cache --add='$ssh_target' >/dev/null 2>&1; or true + end + " & + end + else echo "Warning: Failed to install terminfo." >&2 + set -a ssh_env TERM=xterm-256color + end + else + echo "Warning: Could not generate terminfo data." >&2 + set -a ssh_env TERM=xterm-256color end + else + echo "Warning: ghostty command not available for cache management." >&2 + set -a ssh_env TERM=xterm-256color end else - echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 + if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES" + set -a ssh_env TERM=xterm-256color + end end end - # Fallback TERM only if terminfo didn't set it - if string match -q '*ssh-env*' $GHOSTTY_SHELL_FEATURES - if test "$TERM" = xterm-ghostty -a ! (string join ' ' $env | string match -q '*TERM=*') - set -a env TERM=xterm-256color + # Ensure TERM is set when using ssh-env feature + if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES" + set -l ssh_term_set false + for ssh_v in $ssh_env + if string match -q 'TERM=*' -- $ssh_v + set ssh_term_set true + break + end + end + if test "$ssh_term_set" = "false"; and test "$TERM" = "xterm-ghostty" + set -a ssh_env TERM=xterm-256color end end - # Execute - if test (count $env) -gt 0 - env $env command ssh $opts $ctrl $argv - else - command ssh $opts $ctrl $argv + command ssh $ssh_opts $argv + set -l ssh_ret $status + + # Restore original environment variables + if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES" + for ssh_v in $ssh_exported_vars + if string match -q '*=*' -- $ssh_v + set -gx (string split -m1 '=' -- $ssh_v) + else + set -e $ssh_v + end + end end + + return $ssh_ret end end diff --git a/src/shell-integration/shared/ghostty-ssh-cache b/src/shell-integration/shared/ghostty-ssh-cache deleted file mode 100755 index e0a6d8452..000000000 --- a/src/shell-integration/shared/ghostty-ssh-cache +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -# Minimal Ghostty SSH terminfo host cache - -readonly CACHE_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/ghostty/terminfo_hosts" - -case "${1:-}" in - chk) [[ -f "$CACHE_FILE" ]] && grep -qFx "$2" "$CACHE_FILE" 2>/dev/null ;; - add) mkdir -p "${CACHE_FILE%/*}"; { [[ -f "$CACHE_FILE" ]] && cat "$CACHE_FILE"; echo "$2"; } | sort -u > "$CACHE_FILE.tmp" && mv "$CACHE_FILE.tmp" "$CACHE_FILE" && chmod 600 "$CACHE_FILE" ;; - list) [[ -s "$CACHE_FILE" ]] && echo "Hosts with Ghostty terminfo installed:" && cat "$CACHE_FILE" || echo "No cached hosts found." ;; - clear) rm -f "$CACHE_FILE" 2>/dev/null && echo "Ghostty SSH terminfo cache cleared." || echo "No Ghostty SSH terminfo cache found." ;; -esac diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 48f8cc934..9f78e9a89 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -245,78 +245,166 @@ _ghostty_deferred_init() { fi # SSH Integration - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" =~ (ssh-env|ssh-terminfo) ]]; then + : "${GHOSTTY_SSH_CACHE_TIMEOUT:=5}" + : "${GHOSTTY_SSH_CHECK_TIMEOUT:=3}" - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then - readonly _CACHE="${GHOSTTY_RESOURCES_DIR}/shell-integration/shared/ghostty-ssh-cache" - fi - - # SSH wrapper + # SSH wrapper that preserves Ghostty features across remote connections ssh() { - local -a env opts ctrl - env=() - opts=() - ctrl=() + emulate -L zsh + setopt local_options no_glob_subst + + local -a ssh_env ssh_opts - # Set up env vars first so terminfo installation inherits them + # Configure environment variables for remote session if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - local -a vars - vars=( - COLORTERM=truecolor - TERM_PROGRAM=ghostty - ${TERM_PROGRAM_VERSION:+TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION} + local -a ssh_env_vars=( + "COLORTERM=truecolor" + "TERM_PROGRAM=ghostty" ) - for v in "${vars[@]}"; do - export "${v?}" - opts+=(-o "SendEnv ${v%=*}" -o "SetEnv $v") + [[ -n "$TERM_PROGRAM_VERSION" ]] && ssh_env_vars+=("TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION") + + # Temporarily export variables for SSH transmission + local -a ssh_exported_vars=() + local ssh_v ssh_var_name + for ssh_v in "${ssh_env_vars[@]}"; do + ssh_var_name="${ssh_v%%=*}" + + if [[ -n "${(P)ssh_var_name+x}" ]]; then + ssh_exported_vars+=("$ssh_var_name=${(P)ssh_var_name}") + else + ssh_exported_vars+=("$ssh_var_name") + fi + + export "${ssh_v}" + + # Use both SendEnv and SetEnv for maximum compatibility + ssh_opts+=(-o "SendEnv $ssh_var_name") + ssh_opts+=(-o "SetEnv $ssh_v") done + + ssh_env+=("${ssh_env_vars[@]}") fi - # Install terminfo if needed, reuse control connection for main session + # Install terminfo on remote host if needed if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then - # Get target (only when needed for terminfo) - local target - target=$(command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') - - if [[ -n "$target" ]] && "$_CACHE" chk "$target"; then - env+=(TERM=xterm-ghostty) - elif command -v infocmp >/dev/null 2>&1; then - local tinfo - tinfo=$(infocmp -x xterm-ghostty 2>/dev/null) || echo "Warning: xterm-ghostty terminfo not found locally." >&2 - if [[ -n "$tinfo" ]]; then - echo "Setting up Ghostty terminfo on remote host..." >&2 - local cpath - cpath="/tmp/ghostty-ssh-$USER-$RANDOM-$(date +%s)" - case $(echo "$tinfo" | command ssh "${opts[@]}" -o ControlMaster=yes -o ControlPath="$cpath" -o ControlPersist=60s "$@" ' - infocmp xterm-ghostty >/dev/null 2>&1 && echo OK && exit - command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL - ') in - OK) - echo "Terminfo setup complete." >&2 - [[ -n "$target" ]] && "$_CACHE" add "$target" - env+=(TERM=xterm-ghostty) - ctrl+=(-o "ControlPath=$cpath") - ;; - *) echo "Warning: Failed to install terminfo." >&2 ;; - esac + local ssh_config ssh_user ssh_hostname ssh_target + ssh_config=$(command ssh -G "$@" 2>/dev/null) + ssh_user=$(printf '%s\n' "${(@f)ssh_config}" | while IFS=' ' read -r ssh_key ssh_value; do + [[ "$ssh_key" == "user" ]] && printf '%s\n' "$ssh_value" && break + done) + ssh_hostname=$(printf '%s\n' "${(@f)ssh_config}" | while IFS=' ' read -r ssh_key ssh_value; do + [[ "$ssh_key" == "hostname" ]] && printf '%s\n' "$ssh_value" && break + done) + ssh_target="${ssh_user}@${ssh_hostname}" + + if [[ -n "$ssh_hostname" ]]; then + # Detect timeout command (BSD compatibility) + local ssh_timeout_cmd="" + if (( $+commands[timeout] )); then + ssh_timeout_cmd="timeout" + elif (( $+commands[gtimeout] )); then + ssh_timeout_cmd="gtimeout" + fi + + # Check if terminfo is already cached + local ssh_cache_check_success=false + if (( $+commands[ghostty] )); then + if [[ -n "$ssh_timeout_cmd" ]]; then + $ssh_timeout_cmd "${GHOSTTY_SSH_CHECK_TIMEOUT}s" ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 && ssh_cache_check_success=true + else + ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 && ssh_cache_check_success=true + fi + fi + + if [[ "$ssh_cache_check_success" == "true" ]]; then + ssh_env+=(TERM=xterm-ghostty) + elif (( $+commands[infocmp] )); then + local ssh_terminfo + + # Generate terminfo data (BSD base64 compatibility) + if base64 --help 2>&1 | grep -q GNU; then + ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 -w0 2>/dev/null) + else + ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 2>/dev/null | tr -d '\n') + fi + + if [[ -n "$ssh_terminfo" ]]; then + print "Setting up Ghostty terminfo on remote host..." >&2 + local ssh_cpath_dir ssh_cpath + + 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" + + local ssh_base64_decode_cmd + if base64 --help 2>&1 | grep -q GNU; then + ssh_base64_decode_cmd="base64 -d" + else + ssh_base64_decode_cmd="base64 -D" + fi + + if print "$ssh_terminfo" | $ssh_base64_decode_cmd | 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 + print "Terminfo setup complete." >&2 + ssh_env+=(TERM=xterm-ghostty) + ssh_opts+=(-o "ControlPath=$ssh_cpath") + + # Cache successful installation + if [[ -n "$ssh_target" ]] && (( $+commands[ghostty] )); then + { + if [[ -n "$ssh_timeout_cmd" ]]; then + $ssh_timeout_cmd "${GHOSTTY_SSH_CACHE_TIMEOUT}s" ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true + else + ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true + fi + } &! + fi + else + print "Warning: Failed to install terminfo." >&2 + ssh_env+=(TERM=xterm-256color) + fi + else + print "Warning: Could not generate terminfo data." >&2 + ssh_env+=(TERM=xterm-256color) + fi + else + print "Warning: ghostty command not available for cache management." >&2 + ssh_env+=(TERM=xterm-256color) fi else - echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 + [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]] && ssh_env+=(TERM=xterm-256color) fi fi - # Fallback TERM only if terminfo didn't set it + # Ensure TERM is set when using ssh-env feature if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - [[ $TERM == xterm-ghostty && ! " ${env[*]} " =~ " TERM=" ]] && env+=(TERM=xterm-256color) + local ssh_term_set=false ssh_v + for ssh_v in "${ssh_env[@]}"; do + [[ "$ssh_v" =~ ^TERM= ]] && ssh_term_set=true && break + done + [[ "$ssh_term_set" == "false" && "$TERM" == "xterm-ghostty" ]] && ssh_env+=(TERM=xterm-256color) fi - # Execute - if [[ ${#env[@]} -gt 0 ]]; then - env "${env[@]}" command ssh "${opts[@]}" "${ctrl[@]}" "$@" - else - command ssh "${opts[@]}" "${ctrl[@]}" "$@" + command ssh "${ssh_opts[@]}" "$@" + local ssh_ret=$? + + # Restore original environment variables + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then + local ssh_v + for ssh_v in "${ssh_exported_vars[@]}"; do + if [[ "$ssh_v" == *=* ]]; then + export "${ssh_v}" + else + unset "${ssh_v}" + fi + done fi + + return $ssh_ret } fi |
